From 5a45294e1e8bc5e3ea2296632db98dbfa0dc2872 Mon Sep 17 00:00:00 2001 From: Peter Hindes <19994487+PeterHindes@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:01:38 -0600 Subject: [PATCH 1/3] faster rebuilds --- frontend/Dockerfile | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 3660c468..62307038 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,18 +1,36 @@ # Install dependencies only when needed -FROM node:14-alpine AS builder +FROM node:14-alpine AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app +COPY package.json yarn.lock ./ + +# Install all dependencies (including dev) +RUN yarn install --frozen-lockfile + +# Build stage +FROM node:14-alpine AS builder + +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +# Copy deps from deps stage +COPY --from=deps /app/node_modules ./node_modules +COPY package.json yarn.lock ./ + +# Copy source code COPY . . # Build -RUN yarn install --frozen-lockfile RUN yarn build -# Install only production dependencies in a clean directory -RUN rm -rf node_modules +# Production dependencies stage +FROM deps AS prod-deps + +# Install only production dependencies RUN yarn install --production --frozen-lockfile # Production image, copy all the files and run next @@ -28,7 +46,7 @@ RUN adduser -S nextjs -u 1001 COPY --from=builder /app/next.config.js ./ COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next -COPY --from=builder /app/node_modules ./node_modules +COPY --from=prod-deps /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json USER nextjs From a83ec9817f6c6e9bb9753e4138e39a9e0282bc73 Mon Sep 17 00:00:00 2001 From: Peter Hindes <19994487+PeterHindes@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:01:50 -0600 Subject: [PATCH 2/3] fix lighthouse accessibility issues --- .../Submit/ReusableComponents/InputField.js | 1 + frontend/pages/_document.js | 17 +++++++++++++++++ frontend/specialAccess/setup.js | 9 ++++----- 3 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 frontend/pages/_document.js diff --git a/frontend/components/Submit/ReusableComponents/InputField.js b/frontend/components/Submit/ReusableComponents/InputField.js index 408c8088..9ae167e3 100644 --- a/frontend/components/Submit/ReusableComponents/InputField.js +++ b/frontend/components/Submit/ReusableComponents/InputField.js @@ -33,6 +33,7 @@ export default function InputField(properties) { properties.onChange(event)} diff --git a/frontend/pages/_document.js b/frontend/pages/_document.js new file mode 100644 index 00000000..ae702bfe --- /dev/null +++ b/frontend/pages/_document.js @@ -0,0 +1,17 @@ +import Document, { Html, Head, Main, NextScript } from 'next/document'; + +class MyDocument extends Document { + render() { + return ( + + + +
+ + + + ); + } +} + +export default MyDocument; \ No newline at end of file diff --git a/frontend/specialAccess/setup.js b/frontend/specialAccess/setup.js index 4e28cc82..fd8dbaee 100644 --- a/frontend/specialAccess/setup.js +++ b/frontend/specialAccess/setup.js @@ -60,11 +60,10 @@ export default function Setup({ setInSetupMode }) { } publicPage={true}>
- logo
Welcome to SynBioHub!
@@ -138,7 +137,7 @@ export default function Setup({ setInSetupMode }) { labelText="Require Login: Require login for all operations" value={requireLogin} onChange={event => setRequireLogin(event.target.checked)} - inputName="Alternate Home Page" + inputName="Require Login" containerStyling={styles.checkboxinput} style={secondaryTitleStyle} inputStyle={{ accentColor: color }} From 9a1e539910d7c1822327c8c99c0d23ff8c31cebd Mon Sep 17 00:00:00 2001 From: Peter Hindes <19994487+PeterHindes@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:00:18 -0600 Subject: [PATCH 3/3] refactor the submission page to look good --- .../ChooseCollection/ChooseCollection.js | 99 ++++++++++++------- .../NewCollection/NewCollectionButtons.js | 32 +++--- .../Submit/NewCollection/NewCollectionForm.js | 1 + .../components/Submit/OverwriteObjects.js | 42 ++++---- frontend/package.json | 6 +- frontend/pages/submit.js | 85 ++++++++++------ frontend/redux/actions.js | 10 -- frontend/styles/choosecollection.module.css | 20 +++- frontend/styles/submit.module.css | 99 ++++++++++++++++++- package.json | 5 - 10 files changed, 276 insertions(+), 123 deletions(-) delete mode 100644 package.json diff --git a/frontend/components/Submit/ChooseCollection/ChooseCollection.js b/frontend/components/Submit/ChooseCollection/ChooseCollection.js index 38393202..08f176cc 100644 --- a/frontend/components/Submit/ChooseCollection/ChooseCollection.js +++ b/frontend/components/Submit/ChooseCollection/ChooseCollection.js @@ -1,6 +1,6 @@ -import { faInfoCircle, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faInfoCircle, faPlus, faSearch } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { setPromptNewCollection } from '../../../redux/actions'; @@ -24,48 +24,73 @@ export default function ChooseCollection(properties) { state => state.collectionCreate.promptNewCollection ); + // Handle forceNewCollection prop - only update if it doesn't match current state + useEffect(() => { + if (properties.forceNewCollection === true && !promptNewCollection) { + dispatch(setPromptNewCollection(true)); + } else if (properties.forceNewCollection === false && promptNewCollection) { + dispatch(setPromptNewCollection(false)); + } + }, [properties.forceNewCollection, promptNewCollection, dispatch]); + + // Don't show the label if it's empty + const showLabel = properties.label && properties.label.trim() !== ''; + + // Don't show create button if showCreateButton is false + const showCreateButton = properties.showCreateButton !== false; + return (
- -
- setFilter(event.target.value)} + {showLabel && ( + -
{ - setFilter(''); - dispatch(setPromptNewCollection(true)); - }} - > - - {createCollectionButtonText} -
+ )} +
+ {!promptNewCollection && ( +
+ + setFilter(event.target.value)} + /> +
+ )} + {showCreateButton && !promptNewCollection && ( +
{ + setFilter(''); + dispatch(setPromptNewCollection(true)); + }} + > + + {createCollectionButtonText} +
+ )}
{!promptNewCollection ? ( ) : ( - + )}
); diff --git a/frontend/components/Submit/NewCollection/NewCollectionButtons.js b/frontend/components/Submit/NewCollection/NewCollectionButtons.js index bc5d6b91..37cdbee2 100644 --- a/frontend/components/Submit/NewCollection/NewCollectionButtons.js +++ b/frontend/components/Submit/NewCollection/NewCollectionButtons.js @@ -12,21 +12,23 @@ export default function NewCollectionButtons(properties) { const dispatch = useDispatch(); return (
-
{ - dispatch(getCanSubmitTo()); - dispatch(setPromptNewCollection(false)); - }} - > - - Cancel -
+ {!properties.hideCancel && ( +
{ + dispatch(getCanSubmitTo()); + dispatch(setPromptNewCollection(false)); + }} + > + + Cancel +
+ )}
); diff --git a/frontend/components/Submit/OverwriteObjects.js b/frontend/components/Submit/OverwriteObjects.js index 95641bdc..af29b005 100644 --- a/frontend/components/Submit/OverwriteObjects.js +++ b/frontend/components/Submit/OverwriteObjects.js @@ -1,5 +1,3 @@ -import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useEffect, useState } from 'react'; import styles from '../../styles/submit.module.css'; @@ -28,35 +26,37 @@ export default function OverwriteObjects(properties) { } }, []); + const handleContainerClick = () => { + if (!properties.checked && enablePrompt) { + setWarnUser(true); + } else { + properties.setChecked(!properties.checked); + } + }; + return (
-
+
{ + if (e.key === 'Enter' || e.key === ' ') { + handleContainerClick(); + } + }} + > { - if (event.target.checked && enablePrompt) setWarnUser(true); - else properties.setChecked(event.target.checked); - }} + onChange={() => {}} // Controlled by container click + readOnly />
Overwrite Existing Collection
-
- - - -
{warnUser && enablePrompt ? ( ./public/commitHash.txt; next dev -p 3333", - "devNextGen": "git rev-parse HEAD > ./public/commitHash.txt; NODE_OPTIONS=--openssl-legacy-provider next dev -p 3333", + "dev": "git rev-parse HEAD > ./public/commitHash.txt; next dev -p ${PORT:-3333}", + "devNextGen": "git rev-parse HEAD > ./public/commitHash.txt; NODE_OPTIONS=--openssl-legacy-provider next dev -p ${PORT:-3333}", "build": "git rev-parse HEAD > ./public/commitHash.txt; next build", "buildNextGen": "git rev-parse HEAD > ./public/commitHash.txt; NODE_OPTIONS=--openssl-legacy-provider next build", "testbuild": "NODE_OPTIONS=--openssl-legacy-provider next build", - "start": "next start -p 3333", + "start": "next start -p ${PORT:-3333}", "lint": "eslint '*/**/*.{js,jsx}'", "lint.fix": "eslint '*/**/*.{js,jsx}' --fix", "test": "jest" diff --git a/frontend/pages/submit.js b/frontend/pages/submit.js index b48b574d..22577d4d 100644 --- a/frontend/pages/submit.js +++ b/frontend/pages/submit.js @@ -1,17 +1,14 @@ -import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { useDispatch } from 'react-redux'; import ChooseCollection from '../components/Submit/ChooseCollection/ChooseCollection'; import UploadFileSection from '../components/Submit/FileComponents/UploadFileSection'; import OverwriteObjects from '../components/Submit/OverwriteObjects'; -import SubmitHeader from '../components/Submit/ReusableComponents/SubmitHeader'; import SubmissionStatusPanel from '../components/Submit/SubmissionStatusPanel'; import SubmitButton from '../components/Submit/SubmitButton'; import TopLevel from '../components/TopLevel'; -import { getCanSubmitTo } from '../redux/actions'; +import { getCanSubmitTo, setPromptNewCollection } from '../redux/actions'; import styles from '../styles/submit.module.css'; import ConfigureModal from '../components/Viewing/Modals/ConfigureModal'; @@ -22,6 +19,7 @@ function Submit() { const [overwriteCollection, setOverwriteCollection] = useState(false); const [selectedHandler, setSelectedHandler] = useState({value: 'default', label: 'Default Handler'}); const [modal, setModal] = useState(); + const [activeTab, setActiveTab] = useState('select'); // 'select' or 'create' const showSubmitProgress = useSelector( state => state.submit.showSubmitProgress @@ -30,6 +28,15 @@ function Submit() { const dispatch = useDispatch(); dispatch(getCanSubmitTo()); + // Handle tab switching and update Redux state accordingly + useEffect(() => { + if (activeTab === 'select') { + dispatch(setPromptNewCollection(false)); + } else if (activeTab === 'create') { + dispatch(setPromptNewCollection(true)); + } + }, [activeTab, dispatch]); + const handleClick = () => { if(selectedHandler.value === 'configure') { setModal('configure') @@ -52,31 +59,53 @@ function Submit() { ) : null}
- - } - title="Tell us about your submission" - description="SynBioHub organizes your uploads into collections. Parts can be - uploaded into an existing or new collection." - /> + {/* File Upload Section - Top */} - - - + + {/* Tab Layout for Collection Selection */} +
+
+
setActiveTab('select')} + > + Select Existing Collection +
+
setActiveTab('create')} + > + Create New Collection +
+
+ +
+ {activeTab === 'select' ? ( + <> + {/* Overwrite Option - Fixed at the top */} + + {/* Scrollable collection list */} +
+ +
+ + ) : ( + + )} +
+
+
diff --git a/frontend/redux/actions.js b/frontend/redux/actions.js index 6bbca46e..83911968 100644 --- a/frontend/redux/actions.js +++ b/frontend/redux/actions.js @@ -721,16 +721,6 @@ export const setSelectedCollection = collection => dispatch => { export const setPromptNewCollection = promptNewCollection => dispatch => { dispatch({ type: types.PROMPTNEWCOLLECTION, payload: promptNewCollection }); - if (promptNewCollection) - dispatch({ - type: types.CREATINGCOLLECTIONBUTTONTEXT, - payload: 'Tell us about your collection' - }); - else - dispatch({ - type: types.CREATINGCOLLECTIONBUTTONTEXT, - payload: 'New Collection' - }); }; export const resetSubmit = () => dispatch => { diff --git a/frontend/styles/choosecollection.module.css b/frontend/styles/choosecollection.module.css index d945a33b..1411154a 100644 --- a/frontend/styles/choosecollection.module.css +++ b/frontend/styles/choosecollection.module.css @@ -4,6 +4,21 @@ margin-top: 1rem; } +.searchInputWrapper { + position: relative; + flex: 1; + display: flex; + align-items: center; +} + +.searchIcon { + position: absolute; + left: 0.7rem; + color: #575757; + pointer-events: none; + z-index: 1; +} + .createcollectionbuttons { display: flex; margin-top: 1rem; @@ -105,14 +120,13 @@ .collectionfilter { width: 100%; border: none; - padding: 0.7rem; + padding: 0.7rem 0.7rem 0.7rem 2.5rem; font-size: 1rem; background-color: #ECF0F1; color: #171D26; border-radius: 0.4rem; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; - transition: width .35s; } .collectionfilteraactive::placeholder { @@ -129,7 +143,7 @@ margin-top: 1rem; width: 100%; overflow: auto; - max-height: 20rem; + max-height: 25rem; color: #171D26; } diff --git a/frontend/styles/submit.module.css b/frontend/styles/submit.module.css index c36fd035..21ad4848 100644 --- a/frontend/styles/submit.module.css +++ b/frontend/styles/submit.module.css @@ -16,7 +16,8 @@ } .submitpanel { - width: 35rem; + width: 60rem; + max-width: 95vw; padding: 0 1rem; } @@ -160,6 +161,16 @@ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; } + +.submitinput[type="text"] { + resize: vertical; +} + +textarea.submitinput { + resize: vertical; + min-height: 10rem; +} + .submitinput:disabled { background-color: #6b4245; color: #d3d3d3; @@ -389,11 +400,29 @@ display: flex; flex-direction: row; align-items: center; + cursor: pointer; + -webkit-user-select: none; + user-select: none; + flex: 1; + padding: 0.3rem; + border-radius: 0.3rem; + transition: background-color 0.2s ease; +} + +.overwriteinputcontainer:hover { + background-color: #dde4e6; +} + +.overwriteinputcontainer input[type="checkbox"] { + cursor: pointer; + width: 1.2rem; + height: 1.2rem; } .overwritemessage { margin-left: 0.7rem; font-size: 1rem; + cursor: pointer; } @media (max-width: 40rem) { @@ -440,4 +469,72 @@ .modaltoast { text-align: left; +} + +/* Tab Layout Styles */ +.collectionTabsContainer { + margin-top: 2rem; + margin-bottom: 2rem; +} + +.tabs { + display: flex; + border-bottom: 2px solid #e1e6ee; + margin-bottom: 0; +} + +.tab { + flex: 1; + padding: 1rem; + text-align: center; + cursor: pointer; + background-color: #f5f5f5; + border: 2px solid #e1e6ee; + border-bottom: none; + border-radius: 0.4rem 0.4rem 0 0; + margin-right: 0.25rem; + color: #575757; + font-weight: 500; + transition: all 0.3s ease; +} + +.tab:hover { + background-color: #ECF0F1; + color: #171D26; +} + +.tab:last-child { + margin-right: 0; +} + +.activeTab { + background-color: #f9fafb; + color: #00A1E4; + border-color: #00A1E4; + border-bottom: 2px solid #f9fafb; + margin-bottom: -2px; + position: relative; + z-index: 1; +} + +.activeTab:hover { + background-color: #f9fafb; + color: #00A1E4; +} + +.tabContent { + background-color: #f9fafb; + padding: 1.5rem 1rem; + border-radius: 0 0 0.4rem 0.4rem; + border: 2px solid #e1e6ee; + border-top: none; + min-height: 40rem; + display: flex; + flex-direction: column; +} + +.collectionScrollArea { + flex: 1; + overflow-y: auto; + margin-top: 1rem; } \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index aa798ff7..00000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "devDependencies": { - "eslint-plugin-unused-imports": "^4.1.4" - } -}