Repository: DataBiosphere/duos-ui
Pull request #2874: DT-1678: Remove institution from Library Card creation
Author: login: rushtong
Created: 2025-05-19T13:21:13Z | Merged: 2025-05-19T19:20:15Z

### Addresses
https://broadworkbench.atlassian.net/browse/DT-1678

### Summary
Institutions are no longer part of Library Cards. This PR removes them from the Admin UI. They were already disabled in the Signing Official UI, so this cleans that up so the code doesn't have stray references to institutions where they're not necessary. The SO page was also doing some unnecessary institution id checks to determine if a user had a Library Card or not. This PR fixes that as well.

#### Notes

* When reviewing, I suggest hiding white space to remove the pure format changes.
* We don't have test coverage for these components. Will file a ticket so we can add testing and potentially migrate these to TypeScript, see https://broadworkbench.atlassian.net/browse/DT-1682
* Converting to TS will also address the sonarqube issues related to prop validation.
* There is no longer any action to take with **Update**, see https://broadworkbench.atlassian.net/browse/DT-1683


----
Have you read [Terra's Contributing Guide](https://github.com/DataBiosphere/terra-ui/wiki/Contributor-Guide) lately? If not, do that first.

- Label PR with a Jira ticket number and include a link to the ticket
- Label PR with a security risk modifier [no, low, medium, high]
- PR describes scope of changes
- Get a minimum of one thumbs worth of review, preferably two if enough team members are available
- Get PO sign-off for all non-trivial UI or workflow changes
- Verify all tests go green
- Test this change deployed correctly and works on dev environment after deployment



Diff:
diff --git a/src/components/library_card_table/LibraryCardTable.jsx b/src/components/library_card_table/LibraryCardTable.jsx
index c915f11ebb..a9bcbdd851 100644
--- a/src/components/library_card_table/LibraryCardTable.jsx
+++ b/src/components/library_card_table/LibraryCardTable.jsx
@@ -316,12 +316,9 @@ export default function LibraryCardTable(props) {
   //onClick function, used to create new card on modal based on form data
   const addLibraryCard = async (card) => {
     try {
-      //check if card combination already exits, show error if it does
+      //check if card already exits, show error if it does
       const alreadyExists  = findIndex(
-        (element) =>
-          (isEqual(element.institutionId)(card.institutionId) &&
-            element.userId === card.userId) ||
-          isEqual(element.userEmail)(card.userEmail), libraryCards
+        (element) => isEqual(element.userEmail)(card.userEmail), libraryCards
       );
       if (alreadyExists > -1) {
         Notifications.showError({ text: 'Library Card already exists' });
@@ -329,10 +326,6 @@ export default function LibraryCardTable(props) {
         //add(with sort afterwards) library card to libraryCards (reference list)
       } else {
         const newCard = await LibraryCard.createLibraryCard(card);
-        const institution = find(
-          (institution) => institution.id === newCard.institutionId
-        )(institutions);
-        newCard.institution = institution;
         const updatedList = cloneDeep(libraryCards);
         updatedList.push(newCard);
         updatedList.sort((a, b) => {
@@ -429,7 +422,6 @@ export default function LibraryCardTable(props) {
         updateOnClick={updateListFn}
         createOnClick={addLibraryCard}
         closeModal={() => setShowModal(false)}
-        institutions={institutions}
         users={users}
         card={currentCard}
         modalType={modalType}
diff --git a/src/components/modals/LibraryCardFormModal.jsx b/src/components/modals/LibraryCardFormModal.jsx
index a0c627985a..5eaa0c59bf 100644
--- a/src/components/modals/LibraryCardFormModal.jsx
+++ b/src/components/modals/LibraryCardFormModal.jsx
@@ -3,19 +3,14 @@ import {cloneDeep, includes, isEmpty, isNil, isObject} from 'lodash/fp';
 import {Styles, Theme} from '../../libs/theme';
 import CloseIconComponent from '../CloseIconComponent';
 import Modal from 'react-modal';
-import {SearchSelect} from '../SearchSelect';
 import Creatable from 'react-select/creatable';
 import SimpleButton from '../SimpleButton';
 import {LibraryCardAgreementTermsDownload} from '../LibraryCardAgreementTermsDownload';
 
 const FormFieldRow = (props) => {
-  const { card, dropdownOptions, updateInstitution, updateUser, modalType, setCard } = props;
+  const { card, dropdownOptions, updateUser, modalType, setCard } = props;
 
-  const cardlessOptions = dropdownOptions.filter((option) => {
-    const libraryCards = option.libraryCards || [];
-    const savedCard = libraryCards.find(({institutionId}) => institutionId === card.institutionId);
-    return isNil(savedCard);
-  });
+  const cardlessOptions = dropdownOptions.filter((option) => isEmpty(option.libraryCards));
   const [filteredDropdown, setFilteredDropdown] = useState(cardlessOptions);
 
   let template;
@@ -39,48 +34,33 @@ const FormFieldRow = (props) => {
     }
   };
 
-  if(!isNil(updateInstitution)) {
-    //first template for institution selection
+  //template here is for new card creation
+  if (modalType === 'add') {
     template = <div style={{ marginBottom: '2%', width:'100%' }}>
-      <label>Institution</label>
-      <SearchSelect
-        id={'institution-form-field'}
-        name='Institution'
-        onSelection={(selection) => updateInstitution(selection?.value?.institutionId)}
+      <label>Users</label>
+      <Creatable
+        key='select-user'
+        isClearable={true}
+        onChange={updateUser}
+        createOptionPosition='first'
+        onInputChange={(input, { action }) => userListFilter({ input, card, setCard, action })}
+        getNewOptionData={(input) => { return { email: input }; }}
         options={dropdownOptions}
-        placeholder='Search for institution...'
-        value={card.institutionId}
+        placeholder='Select or type a new user email'
+        isOptionSelected={() => false} //Workaround to prevent odd react-select behavior where all dropdown options are highlighted
+        /* eslint-disable-next-line no-constant-binary-expression */
+        getOptionLabel={(option) => `${option.displayName || 'New User'} (${option.email || 'No email provided'})` || option.email}
       />
     </div>;
   } else {
-    //template here is for new card creation
-    if (modalType === 'add') {
-      template = <div style={{ marginBottom: '2%', width:'100%' }}>
-        <label>Users</label>
-        <Creatable
-          key='select-user'
-          isClearable={true}
-          onChange={updateUser}
-          createOptionPosition='first'
-          onInputChange={(input, { action }) => userListFilter({ input, card, setCard, action })}
-          getNewOptionData={(input) => { return { email: input }; }}
-          options={dropdownOptions}
-          placeholder='Select or type a new user email'
-          isOptionSelected={() => false} //Workaround to prevent odd react-select behavior where all dropdown options are highlighted
-          /* eslint-disable-next-line no-constant-binary-expression */
-          getOptionLabel={(option) => `${option.displayName || 'New User'} (${option.email || 'No email provided'})` || option.email}
-        />
-      </div>;
-    } else {
-      <div>{card.displayName}</div>;
-    }
+    <div>{card.displayName}</div>;
   }
   return <div style={{ display: 'flex' }}>{template}</div>;
 };
 
 export default function LibraryCardFormModal(props) {
   //NOTE: dropdown options need to be passed down from parent component
-  const { showModal, updateOnClick, createOnClick, closeModal, institutions, users, modalType } = props;
+  const { showModal, updateOnClick, createOnClick, closeModal, users, modalType } = props;
 
   const [card, setCard] = useState(props.card);
 
@@ -90,13 +70,6 @@ export default function LibraryCardFormModal(props) {
   }, [props.card]);
 
 
-  //onClick function, updates associated instituion on dropdown select
-  const updateInstitution = (value) => {
-    const updatedCard = cloneDeep(card);
-    updatedCard.institutionId = value;
-    setCard(updatedCard);
-  };
-
   //onChange function, used to change associated user on Creatable dropdown selection
   const updateUser = (value) => {
     let userEmail, userId, userName, eraCommonsId;
@@ -113,14 +86,8 @@ export default function LibraryCardFormModal(props) {
   };
 
   //boolean function, used to determine if submit button should be disabled
-  const isConfirmDisabled = (modalType, card) => {
-    let result;
-    if(modalType === 'add') {
-      result = isNil(card.userEmail) || isNil(card.institutionId);
-    } else {
-      result = isNil(card.institutionId);
-    }
-    return result;
+  const isConfirmDisabled = (card) => {
+    return isNil(card.userEmail);
   };
 
   return (
@@ -151,17 +118,6 @@ export default function LibraryCardFormModal(props) {
           setCard={setCard}
           dropdownOptions={users}
         />
-        {/* institution dropdown */}
-        {
-          !isNil(institutions) && institutions.length > 1 && (
-            <FormFieldRow
-              card={card}
-              modalType={modalType}
-              updateInstitution={updateInstitution}
-              dropdownOptions={institutions}
-            />
-          )
-        }
         <div style={{ display:'inline-block' }}>
           By clicking {modalType === 'add'? "'ADD'" : "'UPDATE'" } you agree to the terms of the agreements above for this user.
         </div>
@@ -175,7 +131,7 @@ export default function LibraryCardFormModal(props) {
             onClick={modalType === 'add' ? () => createOnClick(card) : () => updateOnClick(card)}
             additionalStyle={{ margin: '0%', width: '80px', height: '15px', padding: '20px' }}
             baseColor={Theme.palette.secondary}
-            disabled={isConfirmDisabled(modalType, card)}
+            disabled={isConfirmDisabled(card)}
             label={modalType === 'add' ? 'Add' : 'Update'}
           />
           <SimpleButton
diff --git a/src/pages/signing_official_console/SigningOfficialTable.jsx b/src/pages/signing_official_console/SigningOfficialTable.jsx
index f6d2eefae2..e9a903c034 100644
--- a/src/pages/signing_official_console/SigningOfficialTable.jsx
+++ b/src/pages/signing_official_console/SigningOfficialTable.jsx
@@ -3,6 +3,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
 import { Info } from '@mui/icons-material';
 import { Styles, Theme } from '../../libs/theme';
 import { cloneDeep, find, findIndex, join, map, sortedUniq, sortBy, isEmpty, isNil, flow, filter } from 'lodash/fp';
+import {head} from 'lodash';
 import SimpleTable from '../../components/SimpleTable';
 import SimpleButton from '../../components/SimpleButton';
 import PaginationBar from '../../components/PaginationBar';
@@ -88,7 +89,6 @@ const DeactivateLibraryCardButton = (props) => {
 const IssueLibraryCardButton = (props) => {
   //SO should be able to add library cards to users that are not yet in the system, so userEmail needs to be a possible value to send back
   //username can be confirmed on back-end -> if userId exists pull data from db, otherwise only save email
-  //institution id should be determined from the logged in SO account on the back-end
   const {card, showConfirmationModal} = props;
   const message = (
     <div>
@@ -120,12 +120,9 @@ const researcherFilterFunction = getSearchFilterFunctions().signingOfficialResea
 const LibraryCardCell = ({
   researcher,
   showConfirmationModal,
-  institutionId
 }) => {
   const id = researcher.userId || researcher.email;
-  const card = !isEmpty(researcher.libraryCards)
-    ? find((card) => card.institutionId === institutionId)(researcher.libraryCards)
-    : null;
+  const card = head(researcher.libraryCards);
   const button = !isNil(card)
     ? DeactivateLibraryCardButton({
       card,
@@ -134,8 +131,7 @@ const LibraryCardCell = ({
     : IssueLibraryCardButton({
       card: {
         userId: researcher.userId,
-        userEmail: researcher.email,
-        institutionId: institutionId
+        userEmail: researcher.email
       },
       showConfirmationModal
     });
@@ -300,8 +296,7 @@ export default function SigningOfficialTable(props) {
         emailCell(email, id),
         LibraryCardCell({
           researcher,
-          showConfirmationModal,
-          institutionId: signingOfficial.institutionId
+          showConfirmationModal
         }),
         roleCell(roles, id),
         // activeDarCountCell(count, id)
@@ -440,7 +435,6 @@ export default function SigningOfficialTable(props) {
         closeModal={() => setShowModal(false)}
         card={selectedCard}
         users={filter(onlyResearchersWithoutCardFilter(signingOfficial.institutionId))(researchers)}
-        institutions={[]} //pass in empty array to force modal to hide institution dropdown
         modalType="add" />
       <ConfirmationModal
         showConfirmation={showConfirmation}


Reviews:
user: login: pshapiro4broad
body: Looks OK, might be nice to have unit tests.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T17:48:56Z
user: login: s-rubenstein
body: Pending Phil's comments, this looks good.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T18:35:45Z

Review comments:
diff_hunk: @@ -3,18 +3,16 @@ import {cloneDeep, includes, isEmpty, isNil, isObject} from 'lodash/fp';
 import {Styles, Theme} from '../../libs/theme';
 import CloseIconComponent from '../CloseIconComponent';
 import Modal from 'react-modal';
-import {SearchSelect} from '../SearchSelect';
 import Creatable from 'react-select/creatable';
 import SimpleButton from '../SimpleButton';
 import {LibraryCardAgreementTermsDownload} from '../LibraryCardAgreementTermsDownload';
 
 const FormFieldRow = (props) => {
-  const { card, dropdownOptions, updateInstitution, updateUser, modalType, setCard } = props;
+  const { card, dropdownOptions, updateUser, modalType, setCard } = props;
 
   const cardlessOptions = dropdownOptions.filter((option) => {
     const libraryCards = option.libraryCards || [];
-    const savedCard = libraryCards.find(({institutionId}) => institutionId === card.institutionId);
-    return isNil(savedCard);
+    return isEmpty(libraryCards);
path: src/components/modals/LibraryCardFormModal.jsx
user: login: pshapiro4broad
body: I think this can be simplified. Since `_.isEmpty(undefined)` is `true`, writing `|| []` is redundant
```suggestion
  const cardlessOptions = dropdownOptions.filter((option) => isEmpty(option.libraryCards));
```

created_at: 2025-05-19T17:43:06Z
author_association: MEMBER
diff_hunk: @@ -120,11 +119,10 @@ const researcherFilterFunction = getSearchFilterFunctions().signingOfficialResea
 const LibraryCardCell = ({
   researcher,
   showConfirmationModal,
-  institutionId
 }) => {
   const id = researcher.userId || researcher.email;
   const card = !isEmpty(researcher.libraryCards)
-    ? find((card) => card.institutionId === institutionId)(researcher.libraryCards)
+    ? researcher.libraryCards[0]
     : null;
path: src/pages/signing_official_console/SigningOfficialTable.jsx
user: login: pshapiro4broad
body: Is this the same as lodash `head()`?
```
   const card = _.head(researcher.libraryCards);
```
created_at: 2025-05-19T17:48:17Z
author_association: MEMBER

PR comments:
user: login: rushtong
created_at: 2025-05-19T17:51:43Z
body: > Looks OK, might be nice to have unit tests.

I do want to tackle unit tests, but they will be non-trivial to write especially if we want to convert these to TypeScript. I have a ticket for that effort linked in the PR description. My hope is to get this fix in place first so Admins and SOs are able to work effectively with LCs in the short term.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-19T19:03:11Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DataBiosphere_duos-ui&pullRequest=2874) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [12 New issues](https://sonarcloud.io/project/issues?id=DataBiosphere_duos-ui&pullRequest=2874&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DataBiosphere_duos-ui&pullRequest=2874&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DataBiosphere_duos-ui&pullRequest=2874&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DataBiosphere_duos-ui&pullRequest=2874&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DataBiosphere_duos-ui&pullRequest=2874&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=DataBiosphere_duos-ui&pullRequest=2874)


author_association: NONE