From 4f4f4078e31ac577abb4b5ceb9fecd361a57f7fa Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Wed, 15 Jul 2026 16:52:29 -0500 Subject: [PATCH 01/21] Add adoption xml files --- .../queries/study/adoptions.query.xml | 47 +++++++++++++++++++ .../queries/study/adoptions/.qview.xml | 27 +++++++++++ 2 files changed, 74 insertions(+) create mode 100644 WNPRC_EHR/resources/queries/study/adoptions.query.xml create mode 100644 WNPRC_EHR/resources/queries/study/adoptions/.qview.xml diff --git a/WNPRC_EHR/resources/queries/study/adoptions.query.xml b/WNPRC_EHR/resources/queries/study/adoptions.query.xml new file mode 100644 index 000000000..77f200580 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/adoptions.query.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + Infant Id + + + yyyy-MM-dd HH:mm + Date + + + + + + + Foster Dam + + +
+
+
+
\ No newline at end of file diff --git a/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml b/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml new file mode 100644 index 000000000..4aa4af1da --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file From 5f847a440f9957d423c6ae456c7372e76d190b7c Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 16 Jul 2026 16:35:15 -0500 Subject: [PATCH 02/21] Update housing form with better UI --- .../components/AutoCompleteEditCell.tsx | 80 ++++++++ .../client/components/DateTimeGridField.tsx | 180 ++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 CageUI/src/client/components/AutoCompleteEditCell.tsx create mode 100644 CageUI/src/client/components/DateTimeGridField.tsx diff --git a/CageUI/src/client/components/AutoCompleteEditCell.tsx b/CageUI/src/client/components/AutoCompleteEditCell.tsx new file mode 100644 index 000000000..ebb154caa --- /dev/null +++ b/CageUI/src/client/components/AutoCompleteEditCell.tsx @@ -0,0 +1,80 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ +import * as React from 'react'; +import { useState } from 'react'; +import { GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid'; +import { Autocomplete, TextField } from '@mui/material'; + +interface AutoCompleteEditCellParams { + options: any[] + required: boolean; + multiple?: boolean; + disableClearable?: boolean; +} + +export const AutoCompleteEditCell = (props: GridRenderEditCellParams & AutoCompleteEditCellParams) => { + const { id, field, value, options, required, multiple, disableClearable } = props; + const apiRef = useGridApiContext(); + const [open, setOpen] = useState(true); + + const handleChange = (event: any, newValue: any) => { + apiRef.current.setEditCellValue({ id, field, value: newValue }); + if (!multiple && (newValue || newValue === null)) { + apiRef.current.stopCellEditMode({ id, field }); + } + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Tab') { + apiRef.current.stopCellEditMode({ id, field }); + } + }; + + const isError = required && (value === null || value === undefined || (Array.isArray(value) && value.length === 0) || value === ''); + const selectedOption = multiple ? (value || []) : (options.find(opt => opt.value === value || opt === value) || null); + + return ( + option.label || ''} + value={selectedOption} + onChange={handleChange} + open={open} + onOpen={() => setOpen(true)} + onClose={(event, reason) => { + if (reason === 'selectOption' || reason === 'blur' || reason === 'escape') { + setOpen(false); + } + }} + fullWidth + multiple={multiple} + disableClearable={disableClearable} + isOptionEqualToValue={(option, value) => option.value === value.value} + renderInput={(params) => ( + + )} + /> + ); +}; \ No newline at end of file diff --git a/CageUI/src/client/components/DateTimeGridField.tsx b/CageUI/src/client/components/DateTimeGridField.tsx new file mode 100644 index 000000000..bd44ffe0a --- /dev/null +++ b/CageUI/src/client/components/DateTimeGridField.tsx @@ -0,0 +1,180 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ +import * as React from 'react'; +import { + DataGrid, + GridColDef, + GridRowsProp, + useGridApiContext, + GridRenderEditCellParams, + GRID_DATE_COL_DEF, + GRID_DATETIME_COL_DEF, + GridColTypeDef, + GridFilterInputValueProps, + getGridDateOperators, +} from '@mui/x-data-grid'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; // Says this import is unused but will break typescript if removed +import { enUS as locale } from 'date-fns/locale'; +import { format } from 'date-fns/format'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import { Dayjs } from 'dayjs'; + +AdapterDateFns; // this is here to prevent intellij/ide "remove unused imports" from cleaning up the required import above. + +// Check out the code used here at this link to explain it further. (MUI X v9.3.0) +// https://mui.com/x/react-data-grid/custom-columns/#date-pickers +/** + * `date` column + */ + +const dateColumnType: GridColTypeDef = { + ...GRID_DATE_COL_DEF, + resizable: false, + renderEditCell: (params) => { + return ; + }, + filterOperators: getGridDateOperators(false).map((item) => ({ + ...item, + InputComponent: GridFilterDateInput, + InputComponentProps: { showTime: false }, + })), + valueFormatter: (value) => { + if (value) { + return format(value, 'MM/dd/yyyy', { locale }); + } + return ''; + }, +}; + +function GridEditDateCell({ + id, + field, + value, + colDef, + hasFocus, + }: GridRenderEditCellParams) { + const apiRef = useGridApiContext(); + const inputRef = React.useRef(null); + const [open, setOpen] = React.useState(true); + const Component = colDef.type === 'dateTime' ? DateTimePicker : DatePicker; + + const handleChange = (newValue: unknown) => { + apiRef.current.setEditCellValue({ id, field, value: newValue }); + }; + + const handleAccept = (newValue: unknown) => { + apiRef.current.setEditCellValue({ id, field, value: newValue }); + apiRef.current.stopCellEditMode({ id, field }); + }; + + const handleClose = () => { + setOpen(false); + apiRef.current.stopCellEditMode({ id, field }); + }; + + useEnhancedEffect(() => { + if (hasFocus) { + inputRef.current!.focus(); + } + }, [hasFocus]); + + return ( + setOpen(true)} + onClose={handleClose} + onChange={handleChange} + onAccept={handleAccept} + closeOnSelect={false} + timeSteps={{ minutes: 1 }} + slotProps={{ + actionBar: { + actions: ['cancel', 'accept'], + }, + textField: { + inputRef, + variant: 'standard', + fullWidth: true, + sx: { + padding: '0 9px', + justifyContent: 'center', + '& .MuiInput-underline:after': { + borderBottomColor: value ? 'primary' : 'error.main', + }, + }, + error: !value, + slotProps: { + input: { + disableUnderline: false, + sx: { fontSize: 'inherit' }, + }, + }, + }, + }} + /> + ); +} + +function GridFilterDateInput( + props: GridFilterInputValueProps & { showTime?: boolean }, +) { + const { item, showTime, applyValue, apiRef } = props; + + const Component = showTime ? DateTimePicker : DatePicker; + + const handleFilterChange = (newValue: unknown) => { + applyValue({ ...item, value: newValue }); + }; + + return ( + + ); +} + +/** + * `dateTime` column + */ + +export const dateTimeColumnType: GridColTypeDef = { + ...GRID_DATETIME_COL_DEF, + resizable: true, + renderEditCell: (params) => { + return ; + }, + filterOperators: getGridDateOperators(true).map((item) => ({ + ...item, + InputComponent: GridFilterDateInput, + InputComponentProps: { showTime: true }, + })), + valueFormatter: (value: Dayjs) => { + if (value) { + return format(value.toDate(), 'MM/dd/yyyy hh:mm a', { locale }); + } + return ''; + }, +}; From a94b5e8fdc5a78626c1332b717e1e964f3624962 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 16 Jul 2026 16:35:31 -0500 Subject: [PATCH 03/21] Update adoption form --- .../postgresql/cageui-26.002-26.003.sql | 61 +++++ .../adoptionDataEntry/AdoptionForm.tsx | 254 ++++++++++++++++++ CageUI/src/client/types/adoptionFormTypes.ts | 41 +++ 3 files changed, 356 insertions(+) create mode 100644 CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql create mode 100644 CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx create mode 100644 CageUI/src/client/types/adoptionFormTypes.ts diff --git a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql new file mode 100644 index 000000000..b837c2fe0 --- /dev/null +++ b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql @@ -0,0 +1,61 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +DROP TABLE IF EXISTS cageui.ghost_cages; +CREATE TABLE cageui.ghost_cages +( + rowid SERIAL NOT NULL, + cage_objectid VARCHAR NOT NULL, + positionid INTEGER, + rack_group INTEGER NOT NULL, + rack_objectid VARCHAR NOT NULL, + group_rotation INTEGER NOT NULL, + cage INTEGER NOT NULL, + container entityid NOT NULL, + createdby userid, + created TIMESTAMP, + modifiedby userid, + modified TIMESTAMP, + CONSTRAINT PK_ghost_cages PRIMARY KEY (rowid), + CONSTRAINT FK_ghost_cages_container FOREIGN KEY (container) REFERENCES core.Containers (EntityId) +); + +insert into ehr_lookups.lookups (set_name,container,value, category, title, description) +select setname, container, 8 as value, 'Caging' as category, 'Ghost Cage' as title, 4 as description from ehr_lookups.lookup_sets where setname='cageui_item_types'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 'ghostCage' as value, '/cageui/static/cage.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) +select 'adoption_status' as setname, + 'Adoption Status Field Values' as label, + 'List of possible adoption progress statuses' as description, + 'value' as keyField, + container from ehr_lookups.lookup_sets where setname='ancestry'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 0 as value, 'Start' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 1 as value, 'End' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 2 as value, 'Pause' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 3 as value, 'Resume' as title from ehr_lookups.lookup_sets where setname='adoption_status'; \ No newline at end of file diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx new file mode 100644 index 000000000..b1e65af40 --- /dev/null +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -0,0 +1,254 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + + +import * as React from 'react'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { + DataGrid, + GridAutosizeOptions, + GridColDef, + GridRenderCellParams, + GridRowModel, + useGridApiRef, + useGridApiContext, + GridRenderEditCellParams, GridCellParams +} from '@mui/x-data-grid'; +import { Autocomplete, Box, Button, TextField } from '@mui/material'; +import dayjs from 'dayjs'; +import { AdoptionData, AdoptionResult, AdoptionStatus } from '../../types/adoptionFormTypes'; +import { dateTimeColumnType } from '../DateTimeGridField'; +import { generateUUID } from '../../utils/helpers'; +import { HousingTransferData } from '../../types/housingFormTypes'; +import { Option } from '@labkey/components'; +import { Query } from '@labkey/api'; +import { labkeyActionSelectWithPromise } from '../../api/labkeyActions'; +import { AutoCompleteEditCell } from '../AutoCompleteEditCell'; + +interface AdoptionFormProps {} + +export const AdoptionForm: FC = (props) => { + const [animals, setAnimals] = useState([]); + const apiRef = useGridApiRef(); + const [autoSizeOptions] = useState({ + includeHeaders: true, + includeOutliers: true, + expand: true, + outliersFactor: 1.5, + }); + + const handleAddAnimal = useCallback(() => { + const newAnimal: AdoptionData = { + objectid: generateUUID(), + id: '', + date: dayjs(), + dam: '', + sire: '', + type: AdoptionStatus.Start, + result: null + }; + setAnimals(prev => [...prev, newAnimal]); + }, []); + + const processRowUpdate = useCallback((newRow: GridRowModel, oldRow: GridRowModel) => { + if (newRow.type !== AdoptionStatus.End) { + newRow.result = null; + } + setAnimals(prev => prev.map(row => (row.objectid === newRow.objectid ? newRow : row))); + return newRow; + }, []); + + const handleCellClick = useCallback((params: GridCellParams) => { + if (params.isEditable && params.cellMode === 'view') { + apiRef.current.startCellEditMode({ id: params.id, field: params.field }); + } + }, [apiRef]); + + const adoptionStatusOptions = useMemo(() => { + return Object.keys(AdoptionStatus) + .filter(key => isNaN(Number(key))) + .map(key => ({ + label: key, + value: AdoptionStatus[key as keyof typeof AdoptionStatus] + })); + }, []); + + const adoptionResultOptions = useMemo(() => { + return Object.keys(AdoptionResult) + .filter(key => isNaN(Number(key))) + .map(key => ({ + label: key, + value: AdoptionResult[key as keyof typeof AdoptionResult] + })); + }, []); + + const columns: GridColDef[] = useMemo(() => [ + { + field: 'id', + headerName: 'Infant Id', + minWidth: 100, + editable: true, + display: 'flex' + }, + { + field: 'date', + headerName: 'Date', + ...dateTimeColumnType, + minWidth: 180, + editable: true, + display: 'flex' + }, + { + field: 'dam', + headerName: 'Foster Dam', + minWidth: 120, + editable: true, + display: 'flex', + renderEditCell: (params) => ( + params.api.setEditCellValue({ id: params.id, field: params.field, value: e.target.value })} + error={!params.value} + required + autoFocus + /> + ) + }, + { + field: 'type', + headerName: 'Type', + minWidth: 120, + editable: true, + display: 'flex', + renderEditCell: (params) => ( + + ), + valueFormatter: (value) => { + const val = (value as any)?.value !== undefined ? (value as any).value : value; + if (val === undefined || val === null) return ''; + return AdoptionStatus[val as number] || ''; + } + }, + { + field: 'result', + headerName: 'Result', + minWidth: 120, + editable: true, + display: 'flex', + renderEditCell: (params) => ( + + ), + valueFormatter: (value) => { + const val = (value as any)?.value !== undefined ? (value as any).value : value; + if (val === undefined || val === null) return ''; + return AdoptionResult[val as number] || ''; + }, + isCellEditable: (params) => params.row.type === AdoptionStatus.End + } + ], [adoptionStatusOptions, adoptionResultOptions]); + + const getCellClassName = useCallback((params: GridCellParams) => { + const { field, value, row } = params; + + const isRequired = + field === 'date' || + field === 'dam' || + field === 'type' || + (field === 'result' && row.type === AdoptionStatus.End); + + if (isRequired && (value === null || value === undefined || value === '' || (typeof value === 'object' && (value as any).value === null))) { + return 'required-field-error'; + } + + return ''; + }, []); + + return ( + + + + + + row.objectid} + disableRowSelectionOnClick + autosizeOptions={autoSizeOptions} + autosizeOnMount + sx={{ + '& .required-field-error': { + backgroundColor: '#ffebee', // Light red background + '&:hover': { + backgroundColor: '#ffcdd2', + }, + }, + '& .MuiDataGrid-cell': { + display: 'flex', + alignItems: 'center', + padding: '8px', + whiteSpace: 'normal', + wordBreak: 'break-word', + }, + '& .MuiDataGrid-cellContent': { + width: '100%', + display: 'flex', + alignItems: 'center', + }, + '& .MuiInputBase-root': { + height: 'auto', + minHeight: '100%', + }, + '& .MuiOutlinedInput-root': { + height: 'auto', + }, + '& .MuiAutocomplete-root': { + width: '100%', + }, + '& .MuiTextField-root': { + width: '100%', + }, + '& .MuiDateTimePicker': { + height: '100%', + }, + '& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows': { + margin: 0, + }, + }} + /> + + + ); +}; + diff --git a/CageUI/src/client/types/adoptionFormTypes.ts b/CageUI/src/client/types/adoptionFormTypes.ts new file mode 100644 index 000000000..1d4fa557b --- /dev/null +++ b/CageUI/src/client/types/adoptionFormTypes.ts @@ -0,0 +1,41 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import { Dayjs } from 'dayjs'; + +export interface AdoptionData { + objectid: string; + id: string; + date: Dayjs; + dam: string; + sire: string; + type: AdoptionStatus; + result?: AdoptionResult; +} + +export enum AdoptionStatus { + Start, + End, + Pause, + Resume +} + +export enum AdoptionResult { + Success, + Failure +} \ No newline at end of file From c58676879cb6e4cef292b82d9b310fe0f0d78bd5 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Mon, 6 Jul 2026 12:40:42 -0500 Subject: [PATCH 04/21] Create adoption form --- .../adoptionDataEntry/AdoptionForm.tsx | 145 +++--------------- CageUI/src/client/entryPoints.js | 9 ++ .../adoptionDataEntry/AdoptionDataEntry.tsx | 53 +++++++ .../client/pages/adoptionDataEntry/app.tsx | 30 ++++ .../client/pages/adoptionDataEntry/dev.tsx | 29 ++++ CageUI/src/client/types/adoptionFormTypes.ts | 17 +- 6 files changed, 144 insertions(+), 139 deletions(-) create mode 100644 CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx create mode 100644 CageUI/src/client/pages/adoptionDataEntry/app.tsx create mode 100644 CageUI/src/client/pages/adoptionDataEntry/dev.tsx diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx index b1e65af40..cc42c0746 100644 --- a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -18,27 +18,13 @@ import * as React from 'react'; -import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { - DataGrid, - GridAutosizeOptions, - GridColDef, - GridRenderCellParams, - GridRowModel, - useGridApiRef, - useGridApiContext, - GridRenderEditCellParams, GridCellParams -} from '@mui/x-data-grid'; -import { Autocomplete, Box, Button, TextField } from '@mui/material'; -import dayjs from 'dayjs'; -import { AdoptionData, AdoptionResult, AdoptionStatus } from '../../types/adoptionFormTypes'; +import { FC, useState, useCallback, useMemo } from 'react'; +import { DataGrid, GridAutosizeOptions, GridColDef, GridRowModel, useGridApiRef } from '@mui/x-data-grid'; +import { Box, Button } from '@mui/material'; +import * as dayjs from 'dayjs'; +import { AdoptionData } from '../../types/adoptionFormTypes'; import { dateTimeColumnType } from '../DateTimeGridField'; import { generateUUID } from '../../utils/helpers'; -import { HousingTransferData } from '../../types/housingFormTypes'; -import { Option } from '@labkey/components'; -import { Query } from '@labkey/api'; -import { labkeyActionSelectWithPromise } from '../../api/labkeyActions'; -import { AutoCompleteEditCell } from '../AutoCompleteEditCell'; interface AdoptionFormProps {} @@ -54,53 +40,25 @@ export const AdoptionForm: FC = (props) => { const handleAddAnimal = useCallback(() => { const newAnimal: AdoptionData = { - objectid: generateUUID(), + uuid: generateUUID(), id: '', date: dayjs(), dam: '', sire: '', - type: AdoptionStatus.Start, - result: null + type: '' }; setAnimals(prev => [...prev, newAnimal]); }, []); - const processRowUpdate = useCallback((newRow: GridRowModel, oldRow: GridRowModel) => { - if (newRow.type !== AdoptionStatus.End) { - newRow.result = null; - } - setAnimals(prev => prev.map(row => (row.objectid === newRow.objectid ? newRow : row))); + const processRowUpdate = useCallback((newRow: GridRowModel) => { + setAnimals(prev => prev.map(row => (row.uuid === newRow.uuid ? newRow : row))); return newRow; }, []); - const handleCellClick = useCallback((params: GridCellParams) => { - if (params.isEditable && params.cellMode === 'view') { - apiRef.current.startCellEditMode({ id: params.id, field: params.field }); - } - }, [apiRef]); - - const adoptionStatusOptions = useMemo(() => { - return Object.keys(AdoptionStatus) - .filter(key => isNaN(Number(key))) - .map(key => ({ - label: key, - value: AdoptionStatus[key as keyof typeof AdoptionStatus] - })); - }, []); - - const adoptionResultOptions = useMemo(() => { - return Object.keys(AdoptionResult) - .filter(key => isNaN(Number(key))) - .map(key => ({ - label: key, - value: AdoptionResult[key as keyof typeof AdoptionResult] - })); - }, []); - const columns: GridColDef[] = useMemo(() => [ { field: 'id', - headerName: 'Infant Id', + headerName: 'ID', minWidth: 100, editable: true, display: 'flex' @@ -115,78 +73,26 @@ export const AdoptionForm: FC = (props) => { }, { field: 'dam', - headerName: 'Foster Dam', + headerName: 'Dam', minWidth: 120, editable: true, - display: 'flex', - renderEditCell: (params) => ( - params.api.setEditCellValue({ id: params.id, field: params.field, value: e.target.value })} - error={!params.value} - required - autoFocus - /> - ) + display: 'flex' }, { - field: 'type', - headerName: 'Type', + field: 'sire', + headerName: 'Sire', minWidth: 120, editable: true, - display: 'flex', - renderEditCell: (params) => ( - - ), - valueFormatter: (value) => { - const val = (value as any)?.value !== undefined ? (value as any).value : value; - if (val === undefined || val === null) return ''; - return AdoptionStatus[val as number] || ''; - } + display: 'flex' }, { - field: 'result', - headerName: 'Result', + field: 'type', + headerName: 'Type', minWidth: 120, editable: true, - display: 'flex', - renderEditCell: (params) => ( - - ), - valueFormatter: (value) => { - const val = (value as any)?.value !== undefined ? (value as any).value : value; - if (val === undefined || val === null) return ''; - return AdoptionResult[val as number] || ''; - }, - isCellEditable: (params) => params.row.type === AdoptionStatus.End - } - ], [adoptionStatusOptions, adoptionResultOptions]); - - const getCellClassName = useCallback((params: GridCellParams) => { - const { field, value, row } = params; - - const isRequired = - field === 'date' || - field === 'dam' || - field === 'type' || - (field === 'result' && row.type === AdoptionStatus.End); - - if (isRequired && (value === null || value === undefined || value === '' || (typeof value === 'object' && (value as any).value === null))) { - return 'required-field-error'; + display: 'flex' } - - return ''; - }, []); + ], []); return ( @@ -200,20 +106,12 @@ export const AdoptionForm: FC = (props) => { rows={animals} columns={columns} apiRef={apiRef} - onCellClick={handleCellClick} processRowUpdate={processRowUpdate} - getCellClassName={getCellClassName} - getRowId={(row) => row.objectid} + getRowId={(row) => row.uuid} disableRowSelectionOnClick autosizeOptions={autoSizeOptions} autosizeOnMount sx={{ - '& .required-field-error': { - backgroundColor: '#ffebee', // Light red background - '&:hover': { - backgroundColor: '#ffcdd2', - }, - }, '& .MuiDataGrid-cell': { display: 'flex', alignItems: 'center', @@ -250,5 +148,4 @@ export const AdoptionForm: FC = (props) => { ); -}; - +}; \ No newline at end of file diff --git a/CageUI/src/client/entryPoints.js b/CageUI/src/client/entryPoints.js index 59fa2c8f2..4ce03bfbc 100644 --- a/CageUI/src/client/entryPoints.js +++ b/CageUI/src/client/entryPoints.js @@ -40,6 +40,15 @@ module.exports = { 'org.labkey.api.security.permissions.ReadPermission', ], path: './src/client/pages/updateRackStatus' + }, + { + name: "adoptionDataEntry", + title: "Adoption Form", + permissionClasses: [ + 'org.labkey.api.security.permissions.ReadPermission', + 'org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission' + ], + path: './src/client/pages/adoptionDataEntry' } ] }; diff --git a/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx new file mode 100644 index 000000000..6d2a9dc54 --- /dev/null +++ b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx @@ -0,0 +1,53 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { FC, useEffect, useState } from 'react'; +import '../../cageui.scss'; +import { RoomList } from '../../components/home/RoomList'; +import { RoomNavbar } from '../../components/home/RoomNavbar'; +import { RoomContent } from '../../components/home/RoomContent'; +import { HomeNavigationContextProvider, useHomeNavigationContext } from '../../context/HomeNavigationContextManager'; +import { RoomContextProvider } from '../../context/RoomContextManager'; +import { labkeyGetUserPermissions } from '../../api/labkeyActions'; +import { GetUserPermissionsResponse } from '@labkey/api/dist/labkey/security/Permission'; +import { AdoptionForm } from '../../components/adoptionDataEntry/AdoptionForm'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers'; + + +export const AdoptionDataEntry: FC = () => { + /*const [user, setUser] = useState(null); + + useEffect(() => { + const userProfile = labkeyGetUserPermissions(); + userProfile.then((profile: GetUserPermissionsResponse) => { + if (profile.user) { + setUser(profile); + } + }).catch((e) => { + console.error(e); + }); + }, []);*/ + + return( + + + + ) +}; \ No newline at end of file diff --git a/CageUI/src/client/pages/adoptionDataEntry/app.tsx b/CageUI/src/client/pages/adoptionDataEntry/app.tsx new file mode 100644 index 000000000..ce896bd1d --- /dev/null +++ b/CageUI/src/client/pages/adoptionDataEntry/app.tsx @@ -0,0 +1,30 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { AdoptionDataEntry } from './AdoptionDataEntry'; + + +// Need to wait for container element to be available in labkey wrapper before render +window.addEventListener('DOMContentLoaded', (event) => { + + createRoot(document.getElementById('app')).render( + + ); +}); \ No newline at end of file diff --git a/CageUI/src/client/pages/adoptionDataEntry/dev.tsx b/CageUI/src/client/pages/adoptionDataEntry/dev.tsx new file mode 100644 index 000000000..b449ef975 --- /dev/null +++ b/CageUI/src/client/pages/adoptionDataEntry/dev.tsx @@ -0,0 +1,29 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { AdoptionDataEntry } from './AdoptionDataEntry'; + +const render = () => { + createRoot(document.getElementById('app')).render( + + ); +}; + +render(); \ No newline at end of file diff --git a/CageUI/src/client/types/adoptionFormTypes.ts b/CageUI/src/client/types/adoptionFormTypes.ts index 1d4fa557b..6b13ccf8d 100644 --- a/CageUI/src/client/types/adoptionFormTypes.ts +++ b/CageUI/src/client/types/adoptionFormTypes.ts @@ -19,23 +19,10 @@ import { Dayjs } from 'dayjs'; export interface AdoptionData { - objectid: string; + uuid: string; id: string; date: Dayjs; dam: string; sire: string; - type: AdoptionStatus; - result?: AdoptionResult; + type: string; } - -export enum AdoptionStatus { - Start, - End, - Pause, - Resume -} - -export enum AdoptionResult { - Success, - Failure -} \ No newline at end of file From a636c225d42e347051af4a8daf01d4b707797773 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Fri, 17 Jul 2026 12:14:55 -0500 Subject: [PATCH 05/21] Fix results click ability and error display --- .../adoptionDataEntry/AdoptionForm.tsx | 158 +++++++++++++++--- CageUI/src/client/types/adoptionFormTypes.ts | 18 +- 2 files changed, 151 insertions(+), 25 deletions(-) diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx index cc42c0746..af1da5ab7 100644 --- a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -18,13 +18,27 @@ import * as React from 'react'; -import { FC, useState, useCallback, useMemo } from 'react'; -import { DataGrid, GridAutosizeOptions, GridColDef, GridRowModel, useGridApiRef } from '@mui/x-data-grid'; -import { Box, Button } from '@mui/material'; -import * as dayjs from 'dayjs'; -import { AdoptionData } from '../../types/adoptionFormTypes'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { + DataGrid, + GridAutosizeOptions, + GridColDef, + GridRenderCellParams, + GridRowModel, + useGridApiRef, + useGridApiContext, + GridRenderEditCellParams, GridCellParams +} from '@mui/x-data-grid'; +import { Autocomplete, Box, Button, TextField } from '@mui/material'; +import dayjs from 'dayjs'; +import { AdoptionData, AdoptionResult, AdoptionStatus } from '../../types/adoptionFormTypes'; import { dateTimeColumnType } from '../DateTimeGridField'; import { generateUUID } from '../../utils/helpers'; +import { HousingTransferData } from '../../types/housingFormTypes'; +import { Option } from '@labkey/components'; +import { Query } from '@labkey/api'; +import { labkeyActionSelectWithPromise } from '../../api/labkeyActions'; +import { AutoCompleteEditCell } from '../AutoCompleteEditCell'; interface AdoptionFormProps {} @@ -38,27 +52,61 @@ export const AdoptionForm: FC = (props) => { outliersFactor: 1.5, }); + useEffect(() => { + console.log("Data: ", animals) + }, [animals]); + const handleAddAnimal = useCallback(() => { const newAnimal: AdoptionData = { - uuid: generateUUID(), + objectid: generateUUID(), id: '', date: dayjs(), dam: '', - sire: '', - type: '' + type: { + label: AdoptionStatus[AdoptionStatus.Start] as keyof typeof AdoptionStatus, + value: AdoptionStatus.Start + }, + result: null }; setAnimals(prev => [...prev, newAnimal]); }, []); - const processRowUpdate = useCallback((newRow: GridRowModel) => { - setAnimals(prev => prev.map(row => (row.uuid === newRow.uuid ? newRow : row))); + const processRowUpdate = useCallback((newRow: GridRowModel, oldRow: GridRowModel) => { + if (newRow.type.value !== AdoptionStatus.End) { + newRow.result = null; + } + setAnimals(prev => prev.map(row => (row.objectid === newRow.objectid ? newRow : row))); return newRow; }, []); + const handleCellClick = useCallback((params: GridCellParams) => { + if (params.isEditable && params.cellMode === 'view') { + apiRef.current.startCellEditMode({ id: params.id, field: params.field }); + } + }, [apiRef]); + + const adoptionStatusOptions = useMemo(() => { + return Object.keys(AdoptionStatus) + .filter(key => isNaN(Number(key))) + .map(key => ({ + label: key, + value: AdoptionStatus[key as keyof typeof AdoptionStatus] + })); + }, []); + + const adoptionResultOptions = useMemo(() => { + return Object.keys(AdoptionResult) + .filter(key => isNaN(Number(key))) + .map(key => ({ + label: key, + value: AdoptionResult[key as keyof typeof AdoptionResult] + })); + }, []); + const columns: GridColDef[] = useMemo(() => [ { field: 'id', - headerName: 'ID', + headerName: 'Infant Id', minWidth: 100, editable: true, display: 'flex' @@ -73,26 +121,83 @@ export const AdoptionForm: FC = (props) => { }, { field: 'dam', - headerName: 'Dam', + headerName: 'Foster Dam', minWidth: 120, editable: true, - display: 'flex' + display: 'flex', + renderEditCell: (params) => ( + params.api.setEditCellValue({ id: params.id, field: params.field, value: e.target.value })} + error={!params.value} + required + autoFocus + /> + ) }, { - field: 'sire', - headerName: 'Sire', + field: 'type', + headerName: 'Type', minWidth: 120, editable: true, - display: 'flex' + display: 'flex', + renderEditCell: (params) => ( + + ), + valueFormatter: (value) => { + const val = (value as any)?.value !== undefined ? (value as any).value : value; + if (val === undefined || val === null) return ''; + return AdoptionStatus[val as number] || ''; + } }, { - field: 'type', - headerName: 'Type', + field: 'result', + headerName: 'Result', minWidth: 120, editable: true, - display: 'flex' + display: 'flex', + renderEditCell: (params) => { + if(params.row.type.value !== AdoptionStatus.End){ + return; + } + return( + + ); + }, + valueFormatter: (value) => { + const val = (value as any)?.value !== undefined ? (value as any).value : value; + if (val === undefined || val === null) return ''; + return AdoptionResult[val as number] || ''; + }, + isCellEditable: (params) => params.row.type.value === AdoptionStatus.End + } + ], [adoptionStatusOptions, adoptionResultOptions]); + + const getCellClassName = useCallback((params: GridCellParams) => { + const { field, value, row } = params; + + const isRequired = + field === 'date' || + field === 'dam' || + field === 'type' || + (field === 'result' && row.type.value === AdoptionStatus.End); + + if (isRequired && (value === null || value === undefined || value === '' || (typeof value === 'object' && (value as any).value === null))) { + return 'required-field-error'; } - ], []); + + return ''; + }, []); return ( @@ -106,12 +211,20 @@ export const AdoptionForm: FC = (props) => { rows={animals} columns={columns} apiRef={apiRef} + onCellClick={handleCellClick} processRowUpdate={processRowUpdate} - getRowId={(row) => row.uuid} + getCellClassName={getCellClassName} + getRowId={(row) => row.objectid} disableRowSelectionOnClick autosizeOptions={autoSizeOptions} autosizeOnMount sx={{ + '& .required-field-error': { + backgroundColor: '#ffebee', // Light red background + '&:hover': { + backgroundColor: '#ffcdd2', + }, + }, '& .MuiDataGrid-cell': { display: 'flex', alignItems: 'center', @@ -148,4 +261,5 @@ export const AdoptionForm: FC = (props) => { ); -}; \ No newline at end of file +}; + diff --git a/CageUI/src/client/types/adoptionFormTypes.ts b/CageUI/src/client/types/adoptionFormTypes.ts index 6b13ccf8d..5648d32a3 100644 --- a/CageUI/src/client/types/adoptionFormTypes.ts +++ b/CageUI/src/client/types/adoptionFormTypes.ts @@ -19,10 +19,22 @@ import { Dayjs } from 'dayjs'; export interface AdoptionData { - uuid: string; + objectid: string; id: string; date: Dayjs; dam: string; - sire: string; - type: string; + type: {label: keyof typeof AdoptionStatus, value: AdoptionStatus}; + result?: {label: keyof typeof AdoptionResult, value: AdoptionResult}; } + +export enum AdoptionStatus { + Start, + End, + Pause, + Resume +} + +export enum AdoptionResult { + Success, + Failure +} \ No newline at end of file From 1a0f9d9111e1fb628aabbc8b9d8e3ff317bd7cb0 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Fri, 17 Jul 2026 14:46:55 -0500 Subject: [PATCH 06/21] Add adoption form submission --- .../postgresql/cageui-26.002-26.003.sql | 16 ++- CageUI/src/client/api/labkeyActions.ts | 17 +++ .../adoptionDataEntry/AdoptionForm.tsx | 30 +++++- .../org/labkey/cageui/CageUIController.java | 100 ++++++++++++++++++ .../cageui/action/AdoptionDataForm.java | 94 ++++++++++++++++ .../org/labkey/cageui/model/AdoptionData.java | 94 ++++++++++++++++ .../queries/study/adoptions.query.xml | 13 +++ 7 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java create mode 100644 CageUI/src/org/labkey/cageui/model/AdoptionData.java diff --git a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql index b837c2fe0..c73234473 100644 --- a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql +++ b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql @@ -58,4 +58,18 @@ insert into ehr_lookups.lookups (set_name,container,value, title) select setname, container, 2 as value, 'Pause' as title from ehr_lookups.lookup_sets where setname='adoption_status'; insert into ehr_lookups.lookups (set_name,container,value, title) -select setname, container, 3 as value, 'Resume' as title from ehr_lookups.lookup_sets where setname='adoption_status'; \ No newline at end of file +select setname, container, 3 as value, 'Resume' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + + +INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) +select 'adoption_results' as setname, + 'Adoption Result Field Values' as label, + 'List of possible adoption results' as description, + 'value' as keyField, + container from ehr_lookups.lookup_sets where setname='ancestry'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 0 as value, 'Success' as title from ehr_lookups.lookup_sets where setname='adoption_results'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 1 as value, 'Failure' as title from ehr_lookups.lookup_sets where setname='adoption_results'; diff --git a/CageUI/src/client/api/labkeyActions.ts b/CageUI/src/client/api/labkeyActions.ts index 7a9451d59..5e6fbbf39 100644 --- a/CageUI/src/client/api/labkeyActions.ts +++ b/CageUI/src/client/api/labkeyActions.ts @@ -19,6 +19,7 @@ import { ActionURL, Ajax, Query, Security, Utils } from '@labkey/api'; import { CageMods, Rack, RackConditionOption, Room, SessionLog } from '../types/typings'; import { buildURL } from '@labkey/components'; import { RackSwitchOption } from '../types/homeTypes'; +import { AdoptionData } from '../types/adoptionFormTypes'; export function labkeyActionSelectWithPromise( options: Query.SelectRowsOptions, @@ -193,4 +194,20 @@ export function updateRackConditionStatus(rack: RackSwitchOption, condition: Rac jsonData: {rack: rack.value.objectId, condition: condition.value}, }); }); +} + +// This function is for submitting a adoption form. +export function startAdoptionSubmission(animals: AdoptionData[]): Promise<{ + success: boolean, + errors: any[] +}> { + return new Promise((resolve, reject) => { + Ajax.request({ + url: buildURL('cageui', 'submitAdoptionForm.api'), + method: 'POST', + success: (res) => resolve(JSON.parse(res.response)), + failure: Utils.getCallbackWrapper((error) => reject(error)), + jsonData: {adoptionData: animals}, + }); + }); } \ No newline at end of file diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx index af1da5ab7..be55d5c24 100644 --- a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -37,13 +37,14 @@ import { generateUUID } from '../../utils/helpers'; import { HousingTransferData } from '../../types/housingFormTypes'; import { Option } from '@labkey/components'; import { Query } from '@labkey/api'; -import { labkeyActionSelectWithPromise } from '../../api/labkeyActions'; +import { labkeyActionSelectWithPromise, startAdoptionSubmission, startHousingTransfer } from '../../api/labkeyActions'; import { AutoCompleteEditCell } from '../AutoCompleteEditCell'; interface AdoptionFormProps {} export const AdoptionForm: FC = (props) => { const [animals, setAnimals] = useState([]); + const [isSaving, setIsSaving] = useState(false); const apiRef = useGridApiRef(); const [autoSizeOptions] = useState({ includeHeaders: true, @@ -199,6 +200,22 @@ export const AdoptionForm: FC = (props) => { return ''; }, []); + const handleSubmit = useCallback(() => { + console.log('Submitting form...', animals); + startAdoptionSubmission(animals).then((res) => { + if(res.success){ + // Housing transfer complete + alert('Adoption Form Submission Complete'); + }else{ + alert('Adoption Form Submission Error'); + } + setIsSaving(false); + }).catch(err => { + alert(`Error saving form: ${err}`); + setIsSaving(false); + }); + }, [animals]); + return ( @@ -259,6 +276,17 @@ export const AdoptionForm: FC = (props) => { }} /> + {animals.length > 0 && ( +
+ +
+ )}
); }; diff --git a/CageUI/src/org/labkey/cageui/CageUIController.java b/CageUI/src/org/labkey/cageui/CageUIController.java index 88a1deb5e..9c64613a9 100644 --- a/CageUI/src/org/labkey/cageui/CageUIController.java +++ b/CageUI/src/org/labkey/cageui/CageUIController.java @@ -54,10 +54,12 @@ import org.labkey.api.view.HtmlView; import org.labkey.api.view.JspView; import org.labkey.api.view.NavTree; +import org.labkey.cageui.action.AdoptionDataForm; import org.labkey.cageui.action.BundledForms; import org.labkey.cageui.action.CagesForm; import org.labkey.cageui.action.RackTypesForm; import org.labkey.cageui.action.RacksForm; +import org.labkey.cageui.model.AdoptionData; import org.labkey.cageui.model.Cage; import org.labkey.cageui.model.Manufacturer; import org.labkey.cageui.model.ModData; @@ -69,6 +71,7 @@ import org.labkey.cageui.model.RackTypes; import org.labkey.cageui.model.Room; import org.labkey.cageui.model.SessionLog; +import org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission; import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; import org.labkey.cageui.security.permissions.CageUIModificationEditorPermission; import org.labkey.cageui.security.permissions.CageUIRoomCreatorPermission; @@ -80,12 +83,15 @@ import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; public class CageUIController extends SpringActionController { @@ -115,6 +121,100 @@ public void addNavTrail(NavTree root) } } + @RequiresPermission(CageUIAnimalEditorPermission.class) + public static class SubmitAdoptionFormAction extends MutatingApiAction + { + ArrayList _adoptionData; + + public ArrayList getAdoptionData() + { + return _adoptionData; + } + + public void setAdoptionData(ArrayList adoptionData) + { + _adoptionData = adoptionData; + } + + + @Override + public void validateForm(SimpleApiJsonForm form, Errors errors) + { + JSONObject json = form.getJsonObject(); + if (json == null) + { + errors.reject(ERROR_MSG, "Missing json parameter."); + return; + } + + JSONArray jsonTransferData = json.getJSONArray("adoptionData"); + ObjectMapper mapper = JsonUtil.createDefaultMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try + { + TypeReference> typeRef = new TypeReference>() + { + }; + ArrayList adoptionDataList = mapper.readValue(jsonTransferData.toString(), typeRef); + setAdoptionData(adoptionDataList); + }catch (JsonProcessingException e) + { + errors.reject(ERROR_MSG, e.getMessage()); + } + } + + @Override + public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception + { + BatchValidationException batchErrors = new BatchValidationException(); + ApiSimpleResponse response = new ApiSimpleResponse(); + UserSchema studySchema = QueryService.get().getUserSchema(getUser(), getContainer(), "study"); + ArrayList finalForm = new ArrayList(); + + for(AdoptionData row : getAdoptionData()){ + AdoptionDataForm finalRow = new AdoptionDataForm(); + finalRow.setId(row.getId()); + finalRow.setObjectid(row.getObjectid()); + finalRow.setDate(row.getDate()); + finalRow.setDam(row.getDam()); + finalRow.setType(row.getType().getValue()); + if(row.getResult() != null){ + finalRow.setResult(row.getResult().getValue()); + } + finalForm.add(finalRow); + } + + TableInfo studyAdoptionsTable = studySchema.getTable("adoptions"); + QueryUpdateService studyAdoptionsQus = studyAdoptionsTable.getUpdateService(); + if (studyAdoptionsQus == null) + { + throw new IllegalStateException(studyAdoptionsTable.getName() + " query update service"); + } + + try (DbScope.Transaction tx = CageUISchema.getInstance().getSchema().getScope().ensureTransaction()) + { + List> adoptionMapList = CageUIManager.get().convertToMapList(finalForm); + + studyAdoptionsQus.insertRows(getUser(), getContainer(), adoptionMapList, batchErrors, null, null); + + if (batchErrors.hasErrors()) + { + response.put("success", false); + response.put("errors", batchErrors); + return response; + } + tx.commit(); + response.put("success", true); + } + catch (QueryUpdateServiceException | BatchValidationException | DuplicateKeyException | RuntimeException | + SQLException e) + { + throw new ValidationException(e.getMessage()); + } + return response; + } + } + @RequiresPermission(CageUIRoomModifierPermission.class) public static class UpdateRackConditionStatusAction extends MutatingApiAction { diff --git a/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java b/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java new file mode 100644 index 000000000..8cb6b1d71 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java @@ -0,0 +1,94 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.action; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.util.Date; + +public class AdoptionDataForm +{ + private String id; + private String objectid; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + private Date date; + private String dam; + private Integer type; + private Integer result; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getObjectid() + { + return objectid; + } + + public void setObjectid(String objectid) + { + this.objectid = objectid; + } + + public Date getDate() + { + return date; + } + + public void setDate(Date date) + { + this.date = date; + } + + public String getDam() + { + return dam; + } + + public void setDam(String dam) + { + this.dam = dam; + } + + public Integer getType() + { + return type; + } + + public void setType(Integer type) + { + this.type = type; + } + + public Integer getResult() + { + return result; + } + + public void setResult(Integer result) + { + this.result = result; + } +} diff --git a/CageUI/src/org/labkey/cageui/model/AdoptionData.java b/CageUI/src/org/labkey/cageui/model/AdoptionData.java new file mode 100644 index 000000000..862ac766c --- /dev/null +++ b/CageUI/src/org/labkey/cageui/model/AdoptionData.java @@ -0,0 +1,94 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.model; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.util.Date; + +public class AdoptionData +{ + private String id; + private String objectid; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + private Date date; + private String dam; + private Option type; + private Option result; + + public String getId() + { + return this.id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getObjectid() + { + return this.objectid; + } + + public void setObjectid(String objectid) + { + this.objectid = objectid; + } + + public Date getDate() + { + return this.date; + } + + public void setDate(Date date) + { + this.date = date; + } + + public String getDam() + { + return this.dam; + } + + public void setDam(String dam) + { + this.dam = dam; + } + + public Option getType() + { + return this.type; + } + + public void setType(Option type) + { + this.type = type; + } + + public Option getResult() + { + return this.result; + } + + public void setResult(Option result) + { + this.result = result; + } +} diff --git a/WNPRC_EHR/resources/queries/study/adoptions.query.xml b/WNPRC_EHR/resources/queries/study/adoptions.query.xml index 77f200580..5a6c9e7ae 100644 --- a/WNPRC_EHR/resources/queries/study/adoptions.query.xml +++ b/WNPRC_EHR/resources/queries/study/adoptions.query.xml @@ -34,11 +34,24 @@ Date + + ehr_lookups + adoption_status + value + title + + + ehr_lookups + adoption_results + value + title + Foster Dam + /ehr/participantView.view?participantId=${dam} From 9f83b0b517f23b38c350e033afac98fab760a8f7 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Mon, 20 Jul 2026 11:57:25 -0500 Subject: [PATCH 07/21] Fix styling for mui grid --- CageUI/src/client/cageui.scss | 175 +++++++++++++++--- .../adoptionDataEntry/AdoptionForm.tsx | 93 ++++++++-- 2 files changed, 225 insertions(+), 43 deletions(-) diff --git a/CageUI/src/client/cageui.scss b/CageUI/src/client/cageui.scss index a3b6bc49a..da7818d0b 100644 --- a/CageUI/src/client/cageui.scss +++ b/CageUI/src/client/cageui.scss @@ -1711,26 +1711,12 @@ margin-top: 0px; box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2); } - - - -.modification-editor { - -} - -.modification-editor-title { +.mod-table-title { padding-bottom: 5px; padding-top: 5px; border-bottom: lightgrey 5px solid; } -.modification-editor-input { - width: 100%; - padding: 8px 12px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; -} .modification-editor-content { margin-bottom: 20px; display: flex; @@ -1738,9 +1724,6 @@ margin-top: 0px; flex-direction: row; } - - - @keyframes fadeIn { from { opacity: 0; @@ -1752,9 +1735,8 @@ margin-top: 0px; } } - -.animal-editor{ - +.animal-editor { + padding: 1rem 0 1rem 0; } .animal-editor-title { @@ -1762,6 +1744,81 @@ margin-top: 0px; border-bottom: lightgrey 5px solid; } +.animal-editor-list ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +/* Each list item row */ +.animal-editor-list li { + display: flex; + align-items: center; + background-color: #f9f9f9; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 0.5rem 1rem; + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.animal-editor-list li.selected { + display: flex; + align-items: center; + background-color: lightblue; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 0.5rem 1rem; + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +/* Hover effect for better interactivity */ +.animal-editor-list li:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transform: translateY(-1px); +} + +/* Animal ID (start of row) */ +.animal-editor-list li div:first-child { + flex: 0 0 auto; /* don't grow/shrink */ + font-size: 1.25rem; + font-weight: 600; + color: #2d3748; + margin-right: 1rem; +} + +/* Transfer button (end of row, full height) */ +.animal-editor button { + margin-top: 1rem; + flex: 0 0 auto; + align-self: stretch; /* ensures it fills row height */ + margin-left: auto; /* pushes to right edge */ + padding: 0.6rem 1.25rem; + font-size: 1rem; + font-weight: 600; + color: white; + background-color: cornflowerblue; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease, box-shadow 0.2s ease; + white-space: nowrap; +} + +/* Button hover state */ +.animal-editor button:hover { + background-color: #4338ca; + box-shadow: 0 2px 6px rgba(79, 70, 229, 0.3); +} + +/* Optional: active/click effect */ +.animal-editor button:active { + transform: scale(0.98); +} + + /* Multi Dropdown Css @@ -2186,10 +2243,6 @@ Multi Dropdown Css overflow: auto; } - .modification-editor-content{ - flex-direction: column-reverse; - } - .room-layout{ grid-template-areas: "room-layout-toolbar room-layout-toolbar" @@ -2217,3 +2270,75 @@ Multi Dropdown Css height: 100% !important; overflow: hidden !important; } + + +.housing-transfer-page { + background-color: #f5f5f5; + min-height: 100vh; + display: flex; + flex-direction: column; + //grid-template-columns: minmax(0, 1fr); + +} + +.housing-transfer-header { + font-size: 24px; + font-weight: bold; + margin-bottom: 20px; + color: #333; +} + +.MuiDataGrid-form-container { + display: grid; + grid-template-columns: minmax(0, 1fr); + position: relative; + width: 100%; + gap: 10px; + + .MuiDataGrid-root { + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + width: 100%; + overflow: hidden; // Ensure DataGrid handles its own internal overflow + + .MuiDataGrid-main { + min-width: 0; + } + + .MuiDataGrid-cell:focus-within { + outline: none; + } + } +} + +.add-animal-controls { + display: flex; + gap: 10px; + background: white; + padding: 15px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + align-items: center; + + .animal-id-input { + width: 200px; + } +} + + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 15px; + padding: 20px; + background: white; + border-radius: 8px; + box-shadow: 0 -2px 10px rgba(0,0,0,0.05); + position: sticky; + bottom: 0; +} + +.data-grid-parent { + width: 100%; + overflow: hidden; +} \ No newline at end of file diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx index be55d5c24..e996434b4 100644 --- a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -44,6 +44,7 @@ interface AdoptionFormProps {} export const AdoptionForm: FC = (props) => { const [animals, setAnimals] = useState([]); + const [centerAnimals, setCenterAnimals] = useState([]); const [isSaving, setIsSaving] = useState(false); const apiRef = useGridApiRef(); const [autoSizeOptions] = useState({ @@ -53,10 +54,40 @@ export const AdoptionForm: FC = (props) => { outliersFactor: 1.5, }); + useEffect(() => { + if (apiRef.current) { + const timeout = setTimeout(() => { + apiRef.current?.autosizeColumns(autoSizeOptions); + }, 250); + return () => clearTimeout(timeout); + } + }, [apiRef, animals, autoSizeOptions]); + useEffect(() => { console.log("Data: ", animals) }, [animals]); + useEffect(() => { + const config: Query.SelectRowsOptions = { + schemaName: 'study', + queryName: 'demographics', + viewName: 'Alive, at Center', + columns: ['Id'] + }; + + labkeyActionSelectWithPromise(config).then(result => { + if (result.rows.length !== 0) { + const rowOptions: string[] = []; + result.rows.forEach(row => { + rowOptions.push(row.Id); + }); + setCenterAnimals(rowOptions); + } + }).catch(err => { + console.error('Error fetching alive at center animals', err); + }); + }, []); + const handleAddAnimal = useCallback(() => { const newAnimal: AdoptionData = { objectid: generateUUID(), @@ -104,46 +135,70 @@ export const AdoptionForm: FC = (props) => { })); }, []); + const centerAnimalsOptions = useMemo(() => { + return centerAnimals.map(animalId => ({ + label: animalId, + value: animalId + })); + }, [centerAnimals]); + const columns: GridColDef[] = useMemo(() => [ { field: 'id', headerName: 'Infant Id', minWidth: 100, + flex: 1, + display: 'flex', editable: true, - display: 'flex' + renderEditCell: (params) => ( + + ), + valueFormatter: (value) => { + const val = (value as any)?.value !== undefined ? (value as any).value : value; + if (val === undefined || val === null) return ''; + return val; + } }, { field: 'date', headerName: 'Date', ...dateTimeColumnType, minWidth: 180, - editable: true, - display: 'flex' + flex: 1, + display: 'flex', + editable: true }, { field: 'dam', headerName: 'Foster Dam', minWidth: 120, - editable: true, + flex: 1, display: 'flex', + editable: true, renderEditCell: (params) => ( - params.api.setEditCellValue({ id: params.id, field: params.field, value: e.target.value })} - error={!params.value} - required - autoFocus + - ) + ), + valueFormatter: (value) => { + const val = (value as any)?.value !== undefined ? (value as any).value : value; + if (val === undefined || val === null) return ''; + return val; + } }, { field: 'type', headerName: 'Type', minWidth: 120, - editable: true, + flex: 1, display: 'flex', + editable: true, renderEditCell: (params) => ( = (props) => { field: 'result', headerName: 'Result', minWidth: 120, - editable: true, + flex: 1, display: 'flex', + editable: true, renderEditCell: (params) => { if(params.row.type.value !== AdoptionStatus.End){ return; @@ -182,7 +238,7 @@ export const AdoptionForm: FC = (props) => { }, isCellEditable: (params) => params.row.type.value === AdoptionStatus.End } - ], [adoptionStatusOptions, adoptionResultOptions]); + ], [adoptionStatusOptions, adoptionResultOptions, centerAnimalsOptions]); const getCellClassName = useCallback((params: GridCellParams) => { const { field, value, row } = params; @@ -217,13 +273,13 @@ export const AdoptionForm: FC = (props) => { }, [animals]); return ( - + - + = (props) => { processRowUpdate={processRowUpdate} getCellClassName={getCellClassName} getRowId={(row) => row.objectid} + getRowHeight={() => 'auto'} disableRowSelectionOnClick autosizeOptions={autoSizeOptions} autosizeOnMount From 4ef48889146cfb5db5778d8840c5bc8cad19b67d Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Tue, 21 Jul 2026 13:58:14 -0500 Subject: [PATCH 08/21] Finish adoption form with validation --- CageUI/src/client/cageui.scss | 8 +- .../components/AutoCompleteEditCell.tsx | 13 ++- .../adoptionDataEntry/AdoptionForm.tsx | 88 +++++++++++++++---- .../adoptionDataEntry/AdoptionDataEntry.tsx | 56 ++++++++++-- .../org/labkey/cageui/CageUIController.java | 83 ++++++++++++++++- .../src/org/labkey/cageui/CageUIManager.java | 19 ++++ .../cageui/action/AdoptionDataForm.java | 15 +--- .../org/labkey/cageui/model/AdoptionType.java | 51 +++++++++++ .../queries/study/adoptionsOngoing.sql | 33 +++++++ 9 files changed, 316 insertions(+), 50 deletions(-) create mode 100644 CageUI/src/org/labkey/cageui/model/AdoptionType.java create mode 100644 WNPRC_EHR/resources/queries/study/adoptionsOngoing.sql diff --git a/CageUI/src/client/cageui.scss b/CageUI/src/client/cageui.scss index da7818d0b..8dc39c54d 100644 --- a/CageUI/src/client/cageui.scss +++ b/CageUI/src/client/cageui.scss @@ -673,7 +673,7 @@ } .loading-overlay { - position: absolute; + position: fixed; top: 0; left: 0; width: 100%; @@ -681,22 +681,18 @@ background-color: rgba(0, 0, 0, 0.30); display: flex; justify-content: center; - align-items: start; + align-items: center; border-radius: 8px; z-index: 9999; backdrop-filter: blur(5px); } .loading-content { - position: sticky; - top: 25%; - left: 50%; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 20px; - padding-top: 20px; } .spinner { diff --git a/CageUI/src/client/components/AutoCompleteEditCell.tsx b/CageUI/src/client/components/AutoCompleteEditCell.tsx index ebb154caa..1608fe743 100644 --- a/CageUI/src/client/components/AutoCompleteEditCell.tsx +++ b/CageUI/src/client/components/AutoCompleteEditCell.tsx @@ -25,15 +25,17 @@ interface AutoCompleteEditCellParams { required: boolean; multiple?: boolean; disableClearable?: boolean; + returnValueOnly?: boolean; } export const AutoCompleteEditCell = (props: GridRenderEditCellParams & AutoCompleteEditCellParams) => { - const { id, field, value, options, required, multiple, disableClearable } = props; + const { id, field, value, options, required, multiple, disableClearable, returnValueOnly } = props; const apiRef = useGridApiContext(); const [open, setOpen] = useState(true); const handleChange = (event: any, newValue: any) => { - apiRef.current.setEditCellValue({ id, field, value: newValue }); + const val = returnValueOnly && newValue ? newValue.value : newValue; + apiRef.current.setEditCellValue({ id, field, value: val }); if (!multiple && (newValue || newValue === null)) { apiRef.current.stopCellEditMode({ id, field }); } @@ -46,7 +48,7 @@ export const AutoCompleteEditCell = (props: GridRenderEditCellParams & AutoCompl }; const isError = required && (value === null || value === undefined || (Array.isArray(value) && value.length === 0) || value === ''); - const selectedOption = multiple ? (value || []) : (options.find(opt => opt.value === value || opt === value) || null); + const selectedOption = multiple ? (value || []) : (options.find(opt => opt.value === value || opt === value || (typeof value === 'object' && value !== null && opt.value === value.value)) || null); return ( option.value === value.value} + isOptionEqualToValue={(option, value) => { + const val = (value && typeof value === 'object' && 'value' in value) ? value.value : value; + return option.value === val; + }} renderInput={(params) => ( = (props) => { - const [animals, setAnimals] = useState([]); + const {prevForm} = props; + const [animals, setAnimals] = useState(prevForm ? [prevForm] : []); const [centerAnimals, setCenterAnimals] = useState([]); + const [errorMsg, setErrorMsg] = useState([]); const [isSaving, setIsSaving] = useState(false); const apiRef = useGridApiRef(); const [autoSizeOptions] = useState({ @@ -104,12 +110,31 @@ export const AdoptionForm: FC = (props) => { }, []); const processRowUpdate = useCallback((newRow: GridRowModel, oldRow: GridRowModel) => { - if (newRow.type.value !== AdoptionStatus.End) { + if (newRow.type && newRow.type.value !== AdoptionStatus.End) { newRow.result = null; } + + if (!prevForm && newRow.id && newRow.id !== oldRow.id) { + const config: Query.SelectRowsOptions = { + schemaName: 'study', + queryName: 'adoptionsOngoing', + filterArray: [Filter.create('Id', newRow.id, Filter.Types.EQUAL)], + columns: ['dam'] + }; + + labkeyActionSelectWithPromise(config).then(result => { + if (result.rows.length !== 0) { + const damId = result.rows[0].dam; + setAnimals(prev => prev.map(row => (row.objectid === newRow.objectid ? { ...newRow, dam: damId } : row))); + } + }).catch(err => { + console.error('Error fetching ongoing adoption for dam ID', err); + }); + } + setAnimals(prev => prev.map(row => (row.objectid === newRow.objectid ? newRow : row))); return newRow; - }, []); + }, [prevForm]); const handleCellClick = useCallback((params: GridCellParams) => { if (params.isEditable && params.cellMode === 'view') { @@ -155,12 +180,12 @@ export const AdoptionForm: FC = (props) => { {...params} required={true} options={centerAnimalsOptions} + returnValueOnly={true} /> ), valueFormatter: (value) => { - const val = (value as any)?.value !== undefined ? (value as any).value : value; - if (val === undefined || val === null) return ''; - return val; + if (value === undefined || value === null) return ''; + return value; } }, { @@ -184,12 +209,12 @@ export const AdoptionForm: FC = (props) => { {...params} required={true} options={centerAnimalsOptions} + returnValueOnly={true} /> ), valueFormatter: (value) => { - const val = (value as any)?.value !== undefined ? (value as any).value : value; - if (val === undefined || val === null) return ''; - return val; + if (value === undefined || value === null) return ''; + return value; } }, { @@ -220,13 +245,13 @@ export const AdoptionForm: FC = (props) => { display: 'flex', editable: true, renderEditCell: (params) => { - if(params.row.type.value !== AdoptionStatus.End){ + if(params.row.type?.value !== AdoptionStatus.End){ return; } return( ); @@ -236,20 +261,38 @@ export const AdoptionForm: FC = (props) => { if (val === undefined || val === null) return ''; return AdoptionResult[val as number] || ''; }, - isCellEditable: (params) => params.row.type.value === AdoptionStatus.End + isCellEditable: (params) => params.row.type?.value === AdoptionStatus.End } ], [adoptionStatusOptions, adoptionResultOptions, centerAnimalsOptions]); + const isRowValid = useCallback((row: AdoptionData) => { + const { id, date, dam, type, result } = row; + const isTypeEnd = type?.value === AdoptionStatus.End; + + return ( + id !== '' && id !== null && id !== undefined && + date !== null && date !== undefined && + dam !== '' && dam !== null && dam !== undefined && + type !== null && type !== undefined && + (!isTypeEnd || (result !== null && result !== undefined)) + ); + }, []); + + const isFormValid = useMemo(() => { + return animals.length > 0 && animals.every(isRowValid); + }, [animals, isRowValid]); + const getCellClassName = useCallback((params: GridCellParams) => { const { field, value, row } = params; const isRequired = + field === 'id' || field === 'date' || field === 'dam' || field === 'type' || - (field === 'result' && row.type.value === AdoptionStatus.End); + (field === 'result' && row.type?.value === AdoptionStatus.End); - if (isRequired && (value === null || value === undefined || value === '' || (typeof value === 'object' && (value as any).value === null))) { + if (isRequired && (value === null || value === undefined || value === '')) { return 'required-field-error'; } @@ -267,13 +310,19 @@ export const AdoptionForm: FC = (props) => { } setIsSaving(false); }).catch(err => { - alert(`Error saving form: ${err}`); + console.log(err) + setErrorMsg(err.errors.map(e => e.msg)); setIsSaving(false); }); }, [animals]); return ( + )} + {errorMsg.length > 0 && } ); }; diff --git a/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx index 6d2a9dc54..7c76f5346 100644 --- a/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx +++ b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx @@ -19,19 +19,56 @@ import * as React from 'react'; import { FC, useEffect, useState } from 'react'; import '../../cageui.scss'; -import { RoomList } from '../../components/home/RoomList'; -import { RoomNavbar } from '../../components/home/RoomNavbar'; -import { RoomContent } from '../../components/home/RoomContent'; -import { HomeNavigationContextProvider, useHomeNavigationContext } from '../../context/HomeNavigationContextManager'; -import { RoomContextProvider } from '../../context/RoomContextManager'; -import { labkeyGetUserPermissions } from '../../api/labkeyActions'; -import { GetUserPermissionsResponse } from '@labkey/api/dist/labkey/security/Permission'; +import { labkeyActionSelectWithPromise } from '../../api/labkeyActions'; import { AdoptionForm } from '../../components/adoptionDataEntry/AdoptionForm'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers'; +import { ActionURL, Filter, Query } from '@labkey/api'; +import { AdoptionData, AdoptionResult, AdoptionStatus } from '../../types/adoptionFormTypes'; +import dayjs from 'dayjs'; export const AdoptionDataEntry: FC = () => { + const prevFormLsid = ActionURL.getParameter('lsid'); + const [prevFormData, setPrevFormData] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const config: Query.SelectRowsOptions = { + schemaName: 'study', + queryName: 'adoptions', + columns: ['id', 'objectid', 'date', 'dam', 'result/value', 'result/title', 'type/value', 'type/title'], + filterArray: [Filter.create('lsid', prevFormLsid, Filter.Types.EQUAL)] + }; + + labkeyActionSelectWithPromise(config).then(result => { + if (result.rowCount === 1) { + const res = result.rows[0]; + const adoptionData: AdoptionData = { + dam: res.dam, + date: dayjs(res.date), + id: res.Id, + objectid: res.objectid, + result: { + label: res['result/title'] as keyof typeof AdoptionResult, + value: parseInt(res['result/value']) + }, + type: { + label: res['type/title'] as keyof typeof AdoptionStatus, + value: parseInt(res['type/value']) + } + }; + setPrevFormData(adoptionData); + setIsLoading(false); + }else{ + setIsLoading(false); + } + }).catch(err => { + console.error('Error fetching alive at center animals', err); + setIsLoading(false); + }); + }, []); + /*const [user, setUser] = useState(null); useEffect(() => { @@ -46,8 +83,11 @@ export const AdoptionDataEntry: FC = () => { }, []);*/ return( + !isLoading && - +
+ +
) }; \ No newline at end of file diff --git a/CageUI/src/org/labkey/cageui/CageUIController.java b/CageUI/src/org/labkey/cageui/CageUIController.java index 9c64613a9..91e442e03 100644 --- a/CageUI/src/org/labkey/cageui/CageUIController.java +++ b/CageUI/src/org/labkey/cageui/CageUIController.java @@ -54,12 +54,16 @@ import org.labkey.api.view.HtmlView; import org.labkey.api.view.JspView; import org.labkey.api.view.NavTree; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.UnauthorizedException; import org.labkey.cageui.action.AdoptionDataForm; import org.labkey.cageui.action.BundledForms; import org.labkey.cageui.action.CagesForm; import org.labkey.cageui.action.RackTypesForm; import org.labkey.cageui.action.RacksForm; import org.labkey.cageui.model.AdoptionData; +import org.labkey.cageui.model.AdoptionType; import org.labkey.cageui.model.Cage; import org.labkey.cageui.model.Manufacturer; import org.labkey.cageui.model.ModData; @@ -85,6 +89,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -95,6 +100,7 @@ public class CageUIController extends SpringActionController { + private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(CageUIController.class); public static final String NAME = "cageui"; @@ -161,6 +167,82 @@ public void validateForm(SimpleApiJsonForm form, Errors errors) { errors.reject(ERROR_MSG, e.getMessage()); } + + Map> dataById = getAdoptionData().stream() + .collect(Collectors.groupingBy(AdoptionData::getId)); + + for (Map.Entry> entry : dataById.entrySet()) + { + String id = entry.getKey(); + List newAdoptions = entry.getValue(); + newAdoptions.sort(Comparator.comparing(AdoptionData::getDate)); + + List existingAdoptions = CageUIManager.getAdoptionsForId(id, getUser(), getContainer()); + existingAdoptions.sort(Comparator.comparing(AdoptionDataForm::getDate)); + + AdoptionDataForm lastAdoption = existingAdoptions.isEmpty() ? null : existingAdoptions.get(existingAdoptions.size() - 1); + String expectedDam = lastAdoption != null ? lastAdoption.getDam() : null; + + for (AdoptionData newAdoption : newAdoptions) + { + AdoptionType newType = AdoptionType.fromInt(newAdoption.getType().getValue()); + AdoptionType lastType = lastAdoption != null ? AdoptionType.fromInt(lastAdoption.getType()) : null; + + // Type validation + if (newType == AdoptionType.START) + { + if (lastType != null && lastType != AdoptionType.END) + { + errors.reject(ERROR_MSG, "Animal " + id + " already has an ongoing adoption. Must end previous adoption before starting a new one."); + } + } + else if (newType == AdoptionType.PAUSE) + { + if (lastType != AdoptionType.START && lastType != AdoptionType.RESUME) + { + errors.reject(ERROR_MSG, "Animal " + id + " can only be paused if it is currently started or resumed."); + } + } + else if (newType == AdoptionType.RESUME) + { + if (lastType != AdoptionType.PAUSE) + { + errors.reject(ERROR_MSG, "Animal " + id + " can only be resumed if it is currently paused."); + } + } + else if (newType == AdoptionType.END) + { + if (lastType == AdoptionType.END) + { + errors.reject(ERROR_MSG, "Animal " + id + " adoption has already ended."); + } + } + + // Dam validation + if (expectedDam == null) + { + expectedDam = newAdoption.getDam(); + } + else if (!expectedDam.equals(newAdoption.getDam())) + { + errors.reject(ERROR_MSG, "Dam ID for animal " + id + " must be consistent across adoptions. Expected: " + expectedDam + ", Found: " + newAdoption.getDam()); + } + + // Date validation + if (lastAdoption != null && !newAdoption.getDate().after(lastAdoption.getDate())) + { + errors.reject(ERROR_MSG, "Date for animal " + id + " must be after the previous adoption entry's date."); + } + + // Update last adoption for next iteration + AdoptionDataForm currentAsForm = new AdoptionDataForm(); + currentAsForm.setId(newAdoption.getId()); + currentAsForm.setType(newAdoption.getType().getValue()); + currentAsForm.setDate(newAdoption.getDate()); + currentAsForm.setDam(newAdoption.getDam()); + lastAdoption = currentAsForm; + } + } } @Override @@ -174,7 +256,6 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep for(AdoptionData row : getAdoptionData()){ AdoptionDataForm finalRow = new AdoptionDataForm(); finalRow.setId(row.getId()); - finalRow.setObjectid(row.getObjectid()); finalRow.setDate(row.getDate()); finalRow.setDam(row.getDam()); finalRow.setType(row.getType().getValue()); diff --git a/CageUI/src/org/labkey/cageui/CageUIManager.java b/CageUI/src/org/labkey/cageui/CageUIManager.java index bb4d80cd5..020d59ecd 100644 --- a/CageUI/src/org/labkey/cageui/CageUIManager.java +++ b/CageUI/src/org/labkey/cageui/CageUIManager.java @@ -42,6 +42,7 @@ import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; import org.labkey.api.util.JsonUtil; +import org.labkey.cageui.action.AdoptionDataForm; import org.labkey.cageui.action.AllHistoryForm; import org.labkey.cageui.action.BundledForms; import org.labkey.cageui.action.CageModificationHistoryForm; @@ -510,6 +511,24 @@ public static ArrayList getRacksInRoom(String room) return racksForm; } + public static ArrayList getAdoptionsForId(String id, User user, Container container) + { + + //TableInfo table = getRealTableForDataset(container, "adoptions"); + UserSchema studySchema = QueryService.get().getUserSchema(user, container, "study"); + TableInfo table = studySchema.getTable("adoptions"); + + SimpleFilter filter = new SimpleFilter(); + filter.addCondition(FieldKey.fromString("Id"), id, CompareType.EQUAL); + TableSelector selector = new TableSelector(table, filter, null); + + ObjectMapper mapper = JsonUtil.createDefaultMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + TypeReference> typeRef = new TypeReference>() {}; + ArrayList form = mapper.convertValue(selector.getMapArray(), typeRef); + return form; + } + // ends an all history row public static AllHistoryForm endPreviousAllHistory(String room, Date endDate) diff --git a/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java b/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java index 8cb6b1d71..4bce301cd 100644 --- a/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java +++ b/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java @@ -18,15 +18,16 @@ package org.labkey.cageui.action; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonFormat; import java.util.Date; public class AdoptionDataForm { + @JsonAlias({"id", "Id"}) private String id; - private String objectid; - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") private Date date; private String dam; private Integer type; @@ -42,16 +43,6 @@ public void setId(String id) this.id = id; } - public String getObjectid() - { - return objectid; - } - - public void setObjectid(String objectid) - { - this.objectid = objectid; - } - public Date getDate() { return date; diff --git a/CageUI/src/org/labkey/cageui/model/AdoptionType.java b/CageUI/src/org/labkey/cageui/model/AdoptionType.java new file mode 100644 index 000000000..9bf4163bf --- /dev/null +++ b/CageUI/src/org/labkey/cageui/model/AdoptionType.java @@ -0,0 +1,51 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.model; + +public enum AdoptionType +{ + START(0), + END(1), + PAUSE(2), + RESUME(3); + + private final int _value; + + AdoptionType(int value) + { + _value = value; + } + + public int getValue() + { + return _value; + } + + public static AdoptionType fromInt(int value) + { + for (AdoptionType type : AdoptionType.values()) + { + if (type.getValue() == value) + { + return type; + } + } + return null; + } +} diff --git a/WNPRC_EHR/resources/queries/study/adoptionsOngoing.sql b/WNPRC_EHR/resources/queries/study/adoptionsOngoing.sql new file mode 100644 index 000000000..de5c5eda5 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/adoptionsOngoing.sql @@ -0,0 +1,33 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +SELECT + a.Id, + a.date, + a.type, + a.result, + a.dam +FROM study.adoptions a +WHERE (a.type = '0') -- start + AND NOT EXISTS ( + SELECT 1 + FROM study.adoptions a2 + WHERE a.Id = a2.Id + AND a2.date > a.date + AND (a2.type = '1') -- end + ) From c35db6cfd149f1910663159a1ac7fce4f5cd469b Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Tue, 21 Jul 2026 14:38:25 -0500 Subject: [PATCH 09/21] add delete row column that is not available in prev forms --- .../adoptionDataEntry/AdoptionForm.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx index b4fe281a2..96f6055a8 100644 --- a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -29,7 +29,8 @@ import { useGridApiContext, GridRenderEditCellParams, GridCellParams } from '@mui/x-data-grid'; -import { Autocomplete, Box, Button, TextField } from '@mui/material'; +import { Autocomplete, Box, Button, IconButton, TextField } from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; import dayjs from 'dayjs'; import { AdoptionData, AdoptionResult, AdoptionStatus } from '../../types/adoptionFormTypes'; import { dateTimeColumnType } from '../DateTimeGridField'; @@ -69,10 +70,6 @@ export const AdoptionForm: FC = (props) => { } }, [apiRef, animals, autoSizeOptions]); - useEffect(() => { - console.log("Data: ", animals) - }, [animals]); - useEffect(() => { const config: Query.SelectRowsOptions = { schemaName: 'study', @@ -109,6 +106,10 @@ export const AdoptionForm: FC = (props) => { setAnimals(prev => [...prev, newAnimal]); }, []); + const handleDeleteRow = useCallback((objectid: string) => { + setAnimals(prev => prev.filter(animal => animal.objectid !== objectid)); + }, []); + const processRowUpdate = useCallback((newRow: GridRowModel, oldRow: GridRowModel) => { if (newRow.type && newRow.type.value !== AdoptionStatus.End) { newRow.result = null; @@ -262,8 +263,20 @@ export const AdoptionForm: FC = (props) => { return AdoptionResult[val as number] || ''; }, isCellEditable: (params) => params.row.type?.value === AdoptionStatus.End + }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + minWidth: 80, + display: 'flex', + renderCell: (params: GridRenderCellParams) => ( + handleDeleteRow(params.row.objectid)} color="error"> + + + ), } - ], [adoptionStatusOptions, adoptionResultOptions, centerAnimalsOptions]); + ], [adoptionStatusOptions, adoptionResultOptions, centerAnimalsOptions, handleDeleteRow]); const isRowValid = useCallback((row: AdoptionData) => { const { id, date, dam, type, result } = row; @@ -323,11 +336,14 @@ export const AdoptionForm: FC = (props) => { message={"Saving Form..."} targetElement={document.getElementById("adoption-form-root")} /> - - - + {!prevForm && + + + + } + = (props) => { getRowHeight={() => 'auto'} disableRowSelectionOnClick autosizeOptions={autoSizeOptions} + columnVisibilityModel={{ + actions: !prevForm, + }} autosizeOnMount sx={{ '& .required-field-error': { From 221dd32f22c0ec081585166ea520c519967e7a58 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Tue, 21 Jul 2026 18:15:37 -0500 Subject: [PATCH 10/21] Write query for adoption successes --- .../queries/study/adoptionsSuccess.sql | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 WNPRC_EHR/resources/queries/study/adoptionsSuccess.sql diff --git a/WNPRC_EHR/resources/queries/study/adoptionsSuccess.sql b/WNPRC_EHR/resources/queries/study/adoptionsSuccess.sql new file mode 100644 index 000000000..1ca424e84 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/adoptionsSuccess.sql @@ -0,0 +1,40 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +SELECT + a_start.dam, + a_start.Id, + a_start.date AS start_date, + a_end.date AS end_date, + timestampdiff('SQL_TSI_DAY', a_start.date, a_end.date) AS days_adopted, + (SELECT COUNT(*) FROM study.adoptions a_sub WHERE a_sub.dam = a_start.dam AND a_sub.type = '1' AND a_sub.result = '0' AND a_sub.date <= a_end.date) AS total_adoptions_for_dam +FROM study.adoptions a_start +JOIN study.adoptions a_end ON a_start.Id = a_end.Id AND a_start.dam = a_end.dam +WHERE a_start.type = '0' + AND a_end.type = '1' + AND a_end.result = '0' + AND a_end.date = ( + SELECT MIN(a2.date) + FROM study.adoptions a2 + WHERE a2.Id = a_start.Id + AND a2.dam = a_start.dam + AND a2.type = '1' + AND a2.date > a_start.date + ) + + From 2887915d75a45571e7f24f356ac7f5eaf82dff3c Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Fri, 24 Jul 2026 10:18:40 -0500 Subject: [PATCH 11/21] Add sire and update adoptions table edit url --- .../pages/adoptionDataEntry/AdoptionDataEntry.tsx | 10 +++++++--- WNPRC_EHR/resources/queries/study/adoptions.query.xml | 8 ++++++-- WNPRC_EHR/resources/queries/study/adoptions/.qview.xml | 1 + 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx index 7c76f5346..a67ba3000 100644 --- a/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx +++ b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx @@ -29,16 +29,20 @@ import dayjs from 'dayjs'; export const AdoptionDataEntry: FC = () => { - const prevFormLsid = ActionURL.getParameter('lsid'); + const prevFormObjId = ActionURL.getParameter('objectid'); const [prevFormData, setPrevFormData] = useState(); const [isLoading, setIsLoading] = useState(true); useEffect(() => { + if(!prevFormData) { + setIsLoading(false); + return; + } const config: Query.SelectRowsOptions = { schemaName: 'study', queryName: 'adoptions', - columns: ['id', 'objectid', 'date', 'dam', 'result/value', 'result/title', 'type/value', 'type/title'], - filterArray: [Filter.create('lsid', prevFormLsid, Filter.Types.EQUAL)] + columns: ['Id', 'objectid', 'date', 'dam', 'sire', 'result/value', 'result/title', 'type/value', 'type/title'], + filterArray: [Filter.create('objectid', prevFormObjId, Filter.Types.EQUAL)] }; labkeyActionSelectWithPromise(config).then(result => { diff --git a/WNPRC_EHR/resources/queries/study/adoptions.query.xml b/WNPRC_EHR/resources/queries/study/adoptions.query.xml index 5a6c9e7ae..67300b7a5 100644 --- a/WNPRC_EHR/resources/queries/study/adoptions.query.xml +++ b/WNPRC_EHR/resources/queries/study/adoptions.query.xml @@ -21,9 +21,9 @@ - + /WNPRC/EHR/cageui-adoptionDataEntryDev.view&objectid=${objectid} - + /WNPRC/EHR/cageui-adoptionDataEntryDev.view&objectid=${objectid} @@ -53,6 +53,10 @@ Foster Dam /ehr/participantView.view?participantId=${dam} + + Foster Sire + /ehr/participantView.view?participantId=${dam} +
diff --git a/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml b/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml index 4aa4af1da..d17db41a9 100644 --- a/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml +++ b/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml @@ -23,5 +23,6 @@ + \ No newline at end of file From 0029601154bdcd888ac3317e1c19917db1e9671c Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 13 Aug 2026 12:45:36 -0500 Subject: [PATCH 12/21] Update queries xml for housing test and adoptions --- WNPRC_EHR/resources/queries/study/adoptions.query.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WNPRC_EHR/resources/queries/study/adoptions.query.xml b/WNPRC_EHR/resources/queries/study/adoptions.query.xml index 67300b7a5..c149f6e78 100644 --- a/WNPRC_EHR/resources/queries/study/adoptions.query.xml +++ b/WNPRC_EHR/resources/queries/study/adoptions.query.xml @@ -21,9 +21,9 @@ - /WNPRC/EHR/cageui-adoptionDataEntryDev.view&objectid=${objectid} + /cageui/adoptionDataEntry.view?objectid=${objectid} - /WNPRC/EHR/cageui-adoptionDataEntryDev.view&objectid=${objectid} + /cageui/adoptionDataEntry.view?objectid=${objectid} From b19a11b3ba905b79b7935abde306b23a95a81bd6 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Wed, 19 Aug 2026 12:38:51 -0500 Subject: [PATCH 13/21] Small fixes after cherry pick --- CageUI/package-lock.json | 688 +++++++++++++++++- CageUI/package.json | 7 + .../postgresql/cageui-26.001-26.002.sql | 33 + .../postgresql/cageui-26.002-26.003.sql | 75 -- .../adoptionDataEntry/AdoptionForm.tsx | 4 +- .../org/labkey/cageui/model/AdoptionData.java | 1 + 6 files changed, 707 insertions(+), 101 deletions(-) delete mode 100644 CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql diff --git a/CageUI/package-lock.json b/CageUI/package-lock.json index 8d9e2834d..5faf07a69 100644 --- a/CageUI/package-lock.json +++ b/CageUI/package-lock.json @@ -9,13 +9,20 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@labkey/api": "1.51.5", "@labkey/components": "7.43.0", + "@mui/icons-material": "^9.0.1", + "@mui/material": "^9.0.1", + "@mui/x-data-grid": "^8.28.6", + "@mui/x-date-pickers": "^9.2.0", "d3": "^7.9.0", "dayjs": "^1.11.21", "react": "~18.3.1", "react-bootstrap": "~2.10.10", "react-dom": "~18.3.1", + "react-is": "^18.3.1", "react-select": "^5.10.2", "react-svg": "^16.4.2" }, @@ -1727,6 +1734,28 @@ "node": ">=6.9.0" } }, + "node_modules/@base-ui/utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.2.tgz", + "integrity": "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@floating-ui/utils": "^0.2.12", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", @@ -1929,20 +1958,14 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", "dependencies": { - "@emotion/memoize": "0.7.4" + "@emotion/memoize": "^0.9.0" } }, - "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "license": "MIT" - }, "node_modules/@emotion/memoize": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", @@ -1993,17 +2016,26 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz", - "integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==", + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", "dependencies": { - "@emotion/styled-base": "^10.3.0", - "babel-plugin-emotion": "^10.0.27" + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" }, "peerDependencies": { - "@emotion/core": "^10.0.27", - "react": ">=16.3.0" + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@emotion/styled-base": { @@ -2028,6 +2060,15 @@ "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "license": "MIT" }, + "node_modules/@emotion/styled-base/node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, "node_modules/@emotion/styled-base/node_modules/@emotion/memoize": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", @@ -2153,9 +2194,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, "node_modules/@hello-pangea/dnd": { @@ -2782,6 +2823,581 @@ "dev": true, "license": "MIT" }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.3.1.tgz", + "integrity": "sha512-IAyAFNQbT7hysJ9HXphiOmWJF7G1OglzHanqCgvQgH9LA2ydxtmaTBDbcBqw6euZesyShiwvpvbnYO1GY1AyXQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.3.1.tgz", + "integrity": "sha512-rZj5ccG7vkpV38o/l4ys+chfE9GFypmvZr9dSNBoYVCNBD9yC6KfKq1TYpEMshWbjic7GKQ8MA1LQlcpGgcq9Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^9.3.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.3.1.tgz", + "integrity": "sha512-NahAEGIXqS1K0bA4th1jeFxBguS59NOcLbMA0vU+fSaPWKjtwGBGGHeTwlc9PSmzMjOZKceeFWESB8fVHr31hA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/core-downloads-tracker": "^9.3.1", + "@mui/system": "^9.3.0", + "@mui/types": "^9.3.0", + "@mui/utils": "^9.3.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.8", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.3.0.tgz", + "integrity": "sha512-ERvqk5pejf9aRnQcDILSWGtFmsEMiVxlQ4+xsVCjsEvmK0fV9BiVP/cQwAF5dwyDFbve4lTrlcgeFEecVzTNiA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/utils": "^9.3.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.3.0.tgz", + "integrity": "sha512-x9+KYxhjoHYZ4nioxdKnvQWdw0RScbhoZfQ4tv3Db742683U3wlYSp6zV6Us78dMFnKnyTrPTkQKyutp14gnKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.3.0.tgz", + "integrity": "sha512-0l4LqHJxZj65xSrioniGsxm7VNoGXonPo203oZjhBUvIDPeBqRTb7Mqc45Qxs6sO6WGR7WE/9cJ6lb4lkvHkjg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/private-theming": "^9.3.0", + "@mui/styled-engine": "^9.3.0", + "@mui/types": "^9.3.0", + "@mui/utils": "^9.3.0", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.3.0.tgz", + "integrity": "sha512-2JSxyfpEFWNUB2vKs/T1BvkfyNisMHWph8bLMj8T0uHwmLl/0qfAwQkfwMT6kxLXN9uIum9AEbECXU8er3amIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.3.0.tgz", + "integrity": "sha512-2HZdHwWJ6eB+7lVGSOHsByGw8jeRulT4g0NZ608Wb8Q57DE2jbNqrWPFuJsvkQQiBiTmlpvQL3i+/62zsiPrkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/types": "^9.3.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/x-data-grid": { + "version": "8.29.2", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.29.2.tgz", + "integrity": "sha512-AnH3yQJZXUB+Dv2CtSx8J1XBU1y4GTbq3MCC+r0CCTtglB032K4UhbyeMXG6FLFqWDn1nKofcBNYVoNv5ELR4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "@mui/x-internals": "8.29.2", + "@mui/x-virtualizer": "0.4.1", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/utils": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/x-date-pickers": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-9.11.0.tgz", + "integrity": "sha512-3vLmkn1wG+hNBaGobnk/9R8APCRhOiAXBTC78m7wZ2OWZ2NoMqi+O09HwTGlIgK6mScZZCeCQTSj5phNLrK1hw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/utils": "^9.3.0", + "@mui/x-internals": "^9.11.0", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^7.3.0 || ^9.0.0", + "@mui/system": "^7.3.0 || ^9.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/x-internals": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.11.0.tgz", + "integrity": "sha512-JjKe9k1+gVWNPwMTZLNSc92eVmoZqP5Xq3Ui6mJkstotUrbWIZC0o9+4AfTR1lYqWwnKmqm+VtLLVCa7MEBXWQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-internals": { + "version": "8.29.2", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.29.2.tgz", + "integrity": "sha512-TLILyHia5NHh3MGErFDh0bXZ4V6iS45hdStofsV5wklF6dpgZjduo65oJB0h81mI5mexNlhHANEVfw0KV6BxBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-internals/node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals/node_modules/@mui/utils": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/x-virtualizer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.4.1.tgz", + "integrity": "sha512-EM4lCW9MFRHxAgdqBG6nRQXlrhctlHqOjPm95usht15JBXtuWwtuVdA5q8ta+EwXE9zF0BcKyQsGs8R/vWzmFw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "@mui/x-internals": "8.29.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-virtualizer/node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-virtualizer/node_modules/@mui/utils": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-virtualizer/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", @@ -8929,6 +9545,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -9199,9 +9821,9 @@ } }, "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "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==", "license": "MIT" }, "node_modules/react-lifecycles-compat": { @@ -9365,6 +9987,20 @@ "react-dom": ">=16.7.0" } }, + "node_modules/react-treebeard/node_modules/@emotion/styled": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz", + "integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==", + "license": "MIT", + "dependencies": { + "@emotion/styled-base": "^10.3.0", + "babel-plugin-emotion": "^10.0.27" + }, + "peerDependencies": { + "@emotion/core": "^10.0.27", + "react": ">=16.3.0" + } + }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -9568,6 +10204,12 @@ "dev": true, "license": "MIT" }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", diff --git a/CageUI/package.json b/CageUI/package.json index 28708475a..a9a490598 100644 --- a/CageUI/package.json +++ b/CageUI/package.json @@ -18,13 +18,20 @@ "author": "Board of Regents of the University of Wisconsin System", "license": "Apache-2.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@labkey/api": "1.51.5", "@labkey/components": "7.43.0", + "@mui/icons-material": "^9.0.1", + "@mui/material": "^9.0.1", + "@mui/x-data-grid": "^8.28.6", + "@mui/x-date-pickers": "^9.2.0", "d3": "^7.9.0", "dayjs": "^1.11.21", "react": "~18.3.1", "react-bootstrap": "~2.10.10", "react-dom": "~18.3.1", + "react-is": "^18.3.1", "react-select": "^5.10.2", "react-svg": "^16.4.2" }, diff --git a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.001-26.002.sql b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.001-26.002.sql index d20ac7610..0e58c0893 100644 --- a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.001-26.002.sql +++ b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.001-26.002.sql @@ -40,3 +40,36 @@ select setname, container, 8 as value, 'Caging' as category, 'Ghost Cage' as tit insert into ehr_lookups.lookups (set_name,container,value, title) select setname, container, 'ghostCage' as value, '/cageui/static/cage.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) +select 'adoption_status' as setname, + 'Adoption Status Field Values' as label, + 'List of possible adoption progress statuses' as description, + 'value' as keyField, + container from ehr_lookups.lookup_sets where setname='ancestry'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 0 as value, 'Start' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 1 as value, 'End' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 2 as value, 'Pause' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 3 as value, 'Resume' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + + +INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) +select 'adoption_results' as setname, + 'Adoption Result Field Values' as label, + 'List of possible adoption results' as description, + 'value' as keyField, + container from ehr_lookups.lookup_sets where setname='ancestry'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 0 as value, 'Success' as title from ehr_lookups.lookup_sets where setname='adoption_results'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 1 as value, 'Failure' as title from ehr_lookups.lookup_sets where setname='adoption_results'; \ No newline at end of file diff --git a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql deleted file mode 100644 index c73234473..000000000 --- a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.002-26.003.sql +++ /dev/null @@ -1,75 +0,0 @@ -/* - * - * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * - */ - -DROP TABLE IF EXISTS cageui.ghost_cages; -CREATE TABLE cageui.ghost_cages -( - rowid SERIAL NOT NULL, - cage_objectid VARCHAR NOT NULL, - positionid INTEGER, - rack_group INTEGER NOT NULL, - rack_objectid VARCHAR NOT NULL, - group_rotation INTEGER NOT NULL, - cage INTEGER NOT NULL, - container entityid NOT NULL, - createdby userid, - created TIMESTAMP, - modifiedby userid, - modified TIMESTAMP, - CONSTRAINT PK_ghost_cages PRIMARY KEY (rowid), - CONSTRAINT FK_ghost_cages_container FOREIGN KEY (container) REFERENCES core.Containers (EntityId) -); - -insert into ehr_lookups.lookups (set_name,container,value, category, title, description) -select setname, container, 8 as value, 'Caging' as category, 'Ghost Cage' as title, 4 as description from ehr_lookups.lookup_sets where setname='cageui_item_types'; - -insert into ehr_lookups.lookups (set_name,container,value, title) -select setname, container, 'ghostCage' as value, '/cageui/static/cage.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; - -INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) -select 'adoption_status' as setname, - 'Adoption Status Field Values' as label, - 'List of possible adoption progress statuses' as description, - 'value' as keyField, - container from ehr_lookups.lookup_sets where setname='ancestry'; - -insert into ehr_lookups.lookups (set_name,container,value, title) -select setname, container, 0 as value, 'Start' as title from ehr_lookups.lookup_sets where setname='adoption_status'; - -insert into ehr_lookups.lookups (set_name,container,value, title) -select setname, container, 1 as value, 'End' as title from ehr_lookups.lookup_sets where setname='adoption_status'; - -insert into ehr_lookups.lookups (set_name,container,value, title) -select setname, container, 2 as value, 'Pause' as title from ehr_lookups.lookup_sets where setname='adoption_status'; - -insert into ehr_lookups.lookups (set_name,container,value, title) -select setname, container, 3 as value, 'Resume' as title from ehr_lookups.lookup_sets where setname='adoption_status'; - - -INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) -select 'adoption_results' as setname, - 'Adoption Result Field Values' as label, - 'List of possible adoption results' as description, - 'value' as keyField, - container from ehr_lookups.lookup_sets where setname='ancestry'; - -insert into ehr_lookups.lookups (set_name,container,value, title) -select setname, container, 0 as value, 'Success' as title from ehr_lookups.lookup_sets where setname='adoption_results'; - -insert into ehr_lookups.lookups (set_name,container,value, title) -select setname, container, 1 as value, 'Failure' as title from ehr_lookups.lookup_sets where setname='adoption_results'; diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx index 96f6055a8..0358eebe4 100644 --- a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -35,10 +35,8 @@ import dayjs from 'dayjs'; import { AdoptionData, AdoptionResult, AdoptionStatus } from '../../types/adoptionFormTypes'; import { dateTimeColumnType } from '../DateTimeGridField'; import { generateUUID } from '../../utils/helpers'; -import { HousingTransferData } from '../../types/housingFormTypes'; -import { Option } from '@labkey/components'; import { Filter, Query } from '@labkey/api'; -import { labkeyActionSelectWithPromise, startAdoptionSubmission, startHousingTransfer } from '../../api/labkeyActions'; +import { labkeyActionSelectWithPromise, startAdoptionSubmission } from '../../api/labkeyActions'; import { AutoCompleteEditCell } from '../AutoCompleteEditCell'; import { LoadingScreen } from '../LoadingScreen'; import { LayoutErrors } from '../LayoutErrors'; diff --git a/CageUI/src/org/labkey/cageui/model/AdoptionData.java b/CageUI/src/org/labkey/cageui/model/AdoptionData.java index 862ac766c..fdddb8909 100644 --- a/CageUI/src/org/labkey/cageui/model/AdoptionData.java +++ b/CageUI/src/org/labkey/cageui/model/AdoptionData.java @@ -19,6 +19,7 @@ package org.labkey.cageui.model; import com.fasterxml.jackson.annotation.JsonFormat; +import org.labkey.api.formSchema.Option; import java.util.Date; From a1edccca391b6d0ad3ac417475d001d9baa12a46 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Wed, 19 Aug 2026 16:52:11 -0500 Subject: [PATCH 14/21] Additional validation and redirect on success --- .../adoptionDataEntry/AdoptionForm.tsx | 41 ++++++++-- .../adoptionDataEntry/AdoptionDataEntry.tsx | 16 +--- CageUI/src/client/types/adoptionFormTypes.ts | 1 + .../org/labkey/cageui/CageUIController.java | 21 +++++ .../org/labkey/cageui/model/AdoptionData.java | 12 ++- .../src/org/labkey/cageui/model/Option.java | 78 +++++++++++++++++++ 6 files changed, 146 insertions(+), 23 deletions(-) create mode 100644 CageUI/src/org/labkey/cageui/model/Option.java diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx index 0358eebe4..38e5bbf05 100644 --- a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -35,7 +35,7 @@ import dayjs from 'dayjs'; import { AdoptionData, AdoptionResult, AdoptionStatus } from '../../types/adoptionFormTypes'; import { dateTimeColumnType } from '../DateTimeGridField'; import { generateUUID } from '../../utils/helpers'; -import { Filter, Query } from '@labkey/api'; +import { ActionURL, Filter, Query } from '@labkey/api'; import { labkeyActionSelectWithPromise, startAdoptionSubmission } from '../../api/labkeyActions'; import { AutoCompleteEditCell } from '../AutoCompleteEditCell'; import { LoadingScreen } from '../LoadingScreen'; @@ -94,7 +94,8 @@ export const AdoptionForm: FC = (props) => { objectid: generateUUID(), id: '', date: dayjs(), - dam: '', + dam: null, + sire: null, type: { label: AdoptionStatus[AdoptionStatus.Start] as keyof typeof AdoptionStatus, value: AdoptionStatus.Start @@ -206,7 +207,27 @@ export const AdoptionForm: FC = (props) => { renderEditCell: (params) => ( + ), + valueFormatter: (value) => { + if (value === undefined || value === null) return ''; + return value; + } + }, + { + field: 'sire', + headerName: 'Foster Sire', + minWidth: 120, + flex: 1, + display: 'flex', + editable: true, + renderEditCell: (params) => ( + @@ -277,13 +298,12 @@ export const AdoptionForm: FC = (props) => { ], [adoptionStatusOptions, adoptionResultOptions, centerAnimalsOptions, handleDeleteRow]); const isRowValid = useCallback((row: AdoptionData) => { - const { id, date, dam, type, result } = row; + const { id, date, type, result } = row; const isTypeEnd = type?.value === AdoptionStatus.End; return ( id !== '' && id !== null && id !== undefined && date !== null && date !== undefined && - dam !== '' && dam !== null && dam !== undefined && type !== null && type !== undefined && (!isTypeEnd || (result !== null && result !== undefined)) ); @@ -299,7 +319,6 @@ export const AdoptionForm: FC = (props) => { const isRequired = field === 'id' || field === 'date' || - field === 'dam' || field === 'type' || (field === 'result' && row.type?.value === AdoptionStatus.End); @@ -315,9 +334,15 @@ export const AdoptionForm: FC = (props) => { startAdoptionSubmission(animals).then((res) => { if(res.success){ // Housing transfer complete - alert('Adoption Form Submission Complete'); + window.location.href = ActionURL.buildURL( + "query", + 'executeQuery', + ActionURL.getContainer(), + {schemaName: "study", queryName: "adoptions"}); }else{ - alert('Adoption Form Submission Error'); + // If this happens, the issue is likely related to a faulty submission in the java portion that didn't throw + // an error correctly. Otherwise, it would have gotten caught in the catch below. + setErrorMsg(["Unknown Error Occurred"]); } setIsSaving(false); }).catch(err => { diff --git a/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx index a67ba3000..d232d6326 100644 --- a/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx +++ b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx @@ -34,7 +34,7 @@ export const AdoptionDataEntry: FC = () => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { - if(!prevFormData) { + if(!prevFormObjId) { setIsLoading(false); return; } @@ -50,6 +50,7 @@ export const AdoptionDataEntry: FC = () => { const res = result.rows[0]; const adoptionData: AdoptionData = { dam: res.dam, + sire: res.sire, date: dayjs(res.date), id: res.Id, objectid: res.objectid, @@ -73,19 +74,6 @@ export const AdoptionDataEntry: FC = () => { }); }, []); - /*const [user, setUser] = useState(null); - - useEffect(() => { - const userProfile = labkeyGetUserPermissions(); - userProfile.then((profile: GetUserPermissionsResponse) => { - if (profile.user) { - setUser(profile); - } - }).catch((e) => { - console.error(e); - }); - }, []);*/ - return( !isLoading && diff --git a/CageUI/src/client/types/adoptionFormTypes.ts b/CageUI/src/client/types/adoptionFormTypes.ts index 5648d32a3..f201a3329 100644 --- a/CageUI/src/client/types/adoptionFormTypes.ts +++ b/CageUI/src/client/types/adoptionFormTypes.ts @@ -23,6 +23,7 @@ export interface AdoptionData { id: string; date: Dayjs; dam: string; + sire: string; type: {label: keyof typeof AdoptionStatus, value: AdoptionStatus}; result?: {label: keyof typeof AdoptionResult, value: AdoptionResult}; } diff --git a/CageUI/src/org/labkey/cageui/CageUIController.java b/CageUI/src/org/labkey/cageui/CageUIController.java index 91e442e03..d00a5e639 100644 --- a/CageUI/src/org/labkey/cageui/CageUIController.java +++ b/CageUI/src/org/labkey/cageui/CageUIController.java @@ -168,12 +168,33 @@ public void validateForm(SimpleApiJsonForm form, Errors errors) errors.reject(ERROR_MSG, e.getMessage()); } + // Validation on each individual row + for (AdoptionData row : getAdoptionData()){ + + if(row.getDam() == null && row.getSire() == null){ + errors.reject(ERROR_MSG, "Animal " + row.getId() + " must have a Sire or Dam."); + } + if(row.getSire() != null && row.getId().equals(row.getSire())){ + errors.reject(ERROR_MSG, "Infant cannot be the Sire."); + } + if(row.getDam() != null && row.getId().equals(row.getDam())) { + errors.reject(ERROR_MSG, "Infant cannot be the Dam."); + } + if(row.getSire() != null && row.getDam() != null && row.getSire().equals(row.getDam())){ + errors.reject(ERROR_MSG, "Sire and Dam cannot be the same."); + } + } + + + Map> dataById = getAdoptionData().stream() .collect(Collectors.groupingBy(AdoptionData::getId)); + // validation cross referencing other rows for (Map.Entry> entry : dataById.entrySet()) { String id = entry.getKey(); + List newAdoptions = entry.getValue(); newAdoptions.sort(Comparator.comparing(AdoptionData::getDate)); diff --git a/CageUI/src/org/labkey/cageui/model/AdoptionData.java b/CageUI/src/org/labkey/cageui/model/AdoptionData.java index fdddb8909..87a92b309 100644 --- a/CageUI/src/org/labkey/cageui/model/AdoptionData.java +++ b/CageUI/src/org/labkey/cageui/model/AdoptionData.java @@ -19,7 +19,6 @@ package org.labkey.cageui.model; import com.fasterxml.jackson.annotation.JsonFormat; -import org.labkey.api.formSchema.Option; import java.util.Date; @@ -30,6 +29,7 @@ public class AdoptionData @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") private Date date; private String dam; + private String sire; private Option type; private Option result; @@ -92,4 +92,14 @@ public void setResult(Option result) { this.result = result; } + + public String getSire() + { + return sire; + } + + public void setSire(String sire) + { + this.sire = sire; + } } diff --git a/CageUI/src/org/labkey/cageui/model/Option.java b/CageUI/src/org/labkey/cageui/model/Option.java new file mode 100644 index 000000000..aebc4768a --- /dev/null +++ b/CageUI/src/org/labkey/cageui/model/Option.java @@ -0,0 +1,78 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.model; + +import java.io.Serializable; +import java.util.Objects; + +public class Option implements Serializable { + private String label; + private T value; + + // Constructor + public Option() {} + + public Option(String label, T value) { + this.label = label; + this.value = value; + } + + // Getters and setters + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + + @Override + public String toString() { + return "Option{" + + "label='" + label + '\'' + + ", value=" + value + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Option that = (Option) o; + + if (!Objects.equals(label, that.label)) return false; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + int result = label != null ? label.hashCode() : 0; + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } +} \ No newline at end of file From fddb301d3cb9891f46f7aa1361fa27a287afecac Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 20 Aug 2026 15:04:32 -0500 Subject: [PATCH 15/21] Fix url for insert --- WNPRC_EHR/resources/queries/study/adoptions.query.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WNPRC_EHR/resources/queries/study/adoptions.query.xml b/WNPRC_EHR/resources/queries/study/adoptions.query.xml index c149f6e78..2eb6e48a8 100644 --- a/WNPRC_EHR/resources/queries/study/adoptions.query.xml +++ b/WNPRC_EHR/resources/queries/study/adoptions.query.xml @@ -21,7 +21,7 @@
- /cageui/adoptionDataEntry.view?objectid=${objectid} + /cageui/adoptionDataEntry.view /cageui/adoptionDataEntry.view?objectid=${objectid} From ac37b2f7f45b2487cdc61aaf3b2a56ea757eaa89 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 20 Aug 2026 15:23:49 -0500 Subject: [PATCH 16/21] Add adoptions permission groups --- CageUI/src/client/entryPoints.js | 2 +- .../CageUIAdoptionsPermission.java | 30 ++++++++++++++ .../security/roles/CageUIAdoptionsRole.java | 41 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 CageUI/src/org/labkey/cageui/security/permissions/CageUIAdoptionsPermission.java create mode 100644 CageUI/src/org/labkey/cageui/security/roles/CageUIAdoptionsRole.java diff --git a/CageUI/src/client/entryPoints.js b/CageUI/src/client/entryPoints.js index 4ce03bfbc..6e27e447f 100644 --- a/CageUI/src/client/entryPoints.js +++ b/CageUI/src/client/entryPoints.js @@ -46,7 +46,7 @@ module.exports = { title: "Adoption Form", permissionClasses: [ 'org.labkey.api.security.permissions.ReadPermission', - 'org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission' + 'org.labkey.cageui.security.permissions.CageUIAdoptionsPermission' ], path: './src/client/pages/adoptionDataEntry' } diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUIAdoptionsPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUIAdoptionsPermission.java new file mode 100644 index 000000000..e61a826a7 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUIAdoptionsPermission.java @@ -0,0 +1,30 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUIAdoptionsPermission extends AbstractPermission +{ + public CageUIAdoptionsPermission() + { + super("Cage UI Adoptions", + "This permission allows the user access to adoptions table and submitted/editing data"); + } +} \ No newline at end of file diff --git a/CageUI/src/org/labkey/cageui/security/roles/CageUIAdoptionsRole.java b/CageUI/src/org/labkey/cageui/security/roles/CageUIAdoptionsRole.java new file mode 100644 index 000000000..1ee1683ca --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/roles/CageUIAdoptionsRole.java @@ -0,0 +1,41 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.roles; + +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.roles.AbstractRole; +import org.labkey.cageui.CageUIModule; +import org.labkey.cageui.security.permissions.CageUIAdoptionsPermission; + +public class CageUIAdoptionsRole extends AbstractRole +{ + + public CageUIAdoptionsRole() + { + this("Cage UI Adoptions", + "Adoptions role for Cage UI", + CageUIAdoptionsPermission.class + ); + } + + protected CageUIAdoptionsRole(String name, String description, Class... perms) + { + super(name, description, CageUIModule.class, perms); + } +} \ No newline at end of file From 0b749ac1b5ce73c02876a4ae56e839e68ceab65d Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 20 Aug 2026 15:40:27 -0500 Subject: [PATCH 17/21] Remove css that shouldn't have been carried over with cherrypick --- CageUI/src/client/cageui.scss | 122 +++------------------------------- 1 file changed, 10 insertions(+), 112 deletions(-) diff --git a/CageUI/src/client/cageui.scss b/CageUI/src/client/cageui.scss index 8dc39c54d..9db3e427d 100644 --- a/CageUI/src/client/cageui.scss +++ b/CageUI/src/client/cageui.scss @@ -1707,7 +1707,11 @@ margin-top: 0px; box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2); } -.mod-table-title { +.modification-editor { + +} + +.modification-editor-title { padding-bottom: 5px; padding-top: 5px; border-bottom: lightgrey 5px solid; @@ -1732,7 +1736,7 @@ margin-top: 0px; } .animal-editor { - padding: 1rem 0 1rem 0; + } .animal-editor-title { @@ -1740,80 +1744,6 @@ margin-top: 0px; border-bottom: lightgrey 5px solid; } -.animal-editor-list ul { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -/* Each list item row */ -.animal-editor-list li { - display: flex; - align-items: center; - background-color: #f9f9f9; - border: 1px solid #e0e0e0; - border-radius: 8px; - padding: 0.5rem 1rem; - transition: box-shadow 0.2s ease, transform 0.2s ease; -} - -.animal-editor-list li.selected { - display: flex; - align-items: center; - background-color: lightblue; - border: 1px solid #e0e0e0; - border-radius: 8px; - padding: 0.5rem 1rem; - transition: box-shadow 0.2s ease, transform 0.2s ease; -} - -/* Hover effect for better interactivity */ -.animal-editor-list li:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - transform: translateY(-1px); -} - -/* Animal ID (start of row) */ -.animal-editor-list li div:first-child { - flex: 0 0 auto; /* don't grow/shrink */ - font-size: 1.25rem; - font-weight: 600; - color: #2d3748; - margin-right: 1rem; -} - -/* Transfer button (end of row, full height) */ -.animal-editor button { - margin-top: 1rem; - flex: 0 0 auto; - align-self: stretch; /* ensures it fills row height */ - margin-left: auto; /* pushes to right edge */ - padding: 0.6rem 1.25rem; - font-size: 1rem; - font-weight: 600; - color: white; - background-color: cornflowerblue; - border: none; - border-radius: 6px; - cursor: pointer; - transition: background-color 0.2s ease, box-shadow 0.2s ease; - white-space: nowrap; -} - -/* Button hover state */ -.animal-editor button:hover { - background-color: #4338ca; - box-shadow: 0 2px 6px rgba(79, 70, 229, 0.3); -} - -/* Optional: active/click effect */ -.animal-editor button:active { - transform: scale(0.98); -} - /* @@ -2239,6 +2169,10 @@ Multi Dropdown Css overflow: auto; } + .modification-editor-content{ + flex-direction: column-reverse; + } + .room-layout{ grid-template-areas: "room-layout-toolbar room-layout-toolbar" @@ -2268,22 +2202,6 @@ Multi Dropdown Css } -.housing-transfer-page { - background-color: #f5f5f5; - min-height: 100vh; - display: flex; - flex-direction: column; - //grid-template-columns: minmax(0, 1fr); - -} - -.housing-transfer-header { - font-size: 24px; - font-weight: bold; - margin-bottom: 20px; - color: #333; -} - .MuiDataGrid-form-container { display: grid; grid-template-columns: minmax(0, 1fr); @@ -2307,21 +2225,6 @@ Multi Dropdown Css } } -.add-animal-controls { - display: flex; - gap: 10px; - background: white; - padding: 15px; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - align-items: center; - - .animal-id-input { - width: 200px; - } -} - - .form-actions { display: flex; justify-content: flex-end; @@ -2332,9 +2235,4 @@ Multi Dropdown Css box-shadow: 0 -2px 10px rgba(0,0,0,0.05); position: sticky; bottom: 0; -} - -.data-grid-parent { - width: 100%; - overflow: hidden; } \ No newline at end of file From 86b9513905a120693105183935e5d5a15568f040 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 20 Aug 2026 15:53:47 -0500 Subject: [PATCH 18/21] Update submission to use new adoption permission --- CageUI/src/org/labkey/cageui/CageUIController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CageUI/src/org/labkey/cageui/CageUIController.java b/CageUI/src/org/labkey/cageui/CageUIController.java index d00a5e639..97d5ea89a 100644 --- a/CageUI/src/org/labkey/cageui/CageUIController.java +++ b/CageUI/src/org/labkey/cageui/CageUIController.java @@ -75,6 +75,7 @@ import org.labkey.cageui.model.RackTypes; import org.labkey.cageui.model.Room; import org.labkey.cageui.model.SessionLog; +import org.labkey.cageui.security.permissions.CageUIAdoptionsPermission; import org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission; import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; import org.labkey.cageui.security.permissions.CageUIModificationEditorPermission; @@ -127,7 +128,7 @@ public void addNavTrail(NavTree root) } } - @RequiresPermission(CageUIAnimalEditorPermission.class) + @RequiresPermission(CageUIAdoptionsPermission.class) public static class SubmitAdoptionFormAction extends MutatingApiAction { ArrayList _adoptionData; From 0f1062477268c85a4743a328aa0c550f1f2f1ab5 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 20 Aug 2026 17:21:56 -0500 Subject: [PATCH 19/21] Update query.xml with correct sire --- WNPRC_EHR/resources/queries/study/adoptions.query.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WNPRC_EHR/resources/queries/study/adoptions.query.xml b/WNPRC_EHR/resources/queries/study/adoptions.query.xml index 2eb6e48a8..2ee43afab 100644 --- a/WNPRC_EHR/resources/queries/study/adoptions.query.xml +++ b/WNPRC_EHR/resources/queries/study/adoptions.query.xml @@ -55,7 +55,7 @@ Foster Sire - /ehr/participantView.view?participantId=${dam} + /ehr/participantView.view?participantId=${sire}
From 67fab920f0384bfd56895288c802131aee02dcc2 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 20 Aug 2026 17:22:08 -0500 Subject: [PATCH 20/21] update adoption form to handle unknown error --- .../client/components/adoptionDataEntry/AdoptionForm.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx index 38e5bbf05..0e5a1f865 100644 --- a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -346,8 +346,11 @@ export const AdoptionForm: FC = (props) => { } setIsSaving(false); }).catch(err => { - console.log(err) - setErrorMsg(err.errors.map(e => e.msg)); + if(err.errors){ + setErrorMsg(err.errors.map(e => e.msg)); + }else{ + setErrorMsg(err); + } setIsSaving(false); }); }, [animals]); From d601463536ab515edca722a8d62f2c92e546a194 Mon Sep 17 00:00:00 2001 From: LeviCameron1 Date: Thu, 20 Aug 2026 17:24:01 -0500 Subject: [PATCH 21/21] Make it clear that dam/sire might be null --- CageUI/src/client/types/adoptionFormTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CageUI/src/client/types/adoptionFormTypes.ts b/CageUI/src/client/types/adoptionFormTypes.ts index f201a3329..914311ed6 100644 --- a/CageUI/src/client/types/adoptionFormTypes.ts +++ b/CageUI/src/client/types/adoptionFormTypes.ts @@ -22,8 +22,8 @@ export interface AdoptionData { objectid: string; id: string; date: Dayjs; - dam: string; - sire: string; + dam: string | null; + sire: string | null; type: {label: keyof typeof AdoptionStatus, value: AdoptionStatus}; result?: {label: keyof typeof AdoptionResult, value: AdoptionResult}; }