Skip to content

Commit 67ecb15

Browse files
committed
test changes, UI changes
1 parent 78437af commit 67ecb15

File tree

222 files changed

+4304
-207
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

222 files changed

+4304
-207
lines changed

public/cwl/fsl/feat.cwl

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env cwl-runner
2+
3+
# https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FEAT
4+
# Complete fMRI analysis pipeline: preprocessing, statistics, and post-stats
5+
# The .fsf design file encodes all analysis parameters.
6+
# input_data must be provided separately so cwltool can stage it
7+
# inside the container; the .fsf paths are rewritten at runtime.
8+
9+
cwlVersion: v1.2
10+
class: CommandLineTool
11+
12+
requirements:
13+
InlineJavascriptRequirement: {}
14+
InitialWorkDirRequirement:
15+
listing:
16+
- entry: $(inputs.design_file)
17+
entryname: design.fsf
18+
writable: true
19+
- entry: $(inputs.input_data)
20+
- entryname: run_feat.sh
21+
entry: |
22+
#!/bin/bash
23+
set -e
24+
export USER=\${USER:-cwluser}
25+
WD=`pwd`
26+
sed -i "s|^set feat_files(1).*|set feat_files(1) \"\${WD}/$(inputs.input_data.basename)\"|" design.fsf
27+
sed -i "s|^set fmri(outputdir).*|set fmri(outputdir) \"\${WD}/feat_output\"|" design.fsf
28+
feat design.fsf
29+
30+
hints:
31+
DockerRequirement:
32+
dockerPull: brainlife/fsl:latest
33+
34+
stdout: feat.log
35+
stderr: feat.log
36+
37+
inputs:
38+
design_file:
39+
type: File
40+
label: FEAT design file (.fsf) containing all analysis parameters
41+
input_data:
42+
type: File
43+
label: 4D BOLD input data referenced by the design file
44+
45+
baseCommand: [bash, run_feat.sh]
46+
47+
outputs:
48+
feat_directory:
49+
type: Directory
50+
outputBinding:
51+
glob: "*.feat"
52+
log:
53+
type: File
54+
outputBinding:
55+
glob: feat.log

public/cwl/fsl/film_gls.cwl

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ inputs:
3131
inputBinding:
3232
prefix: --pd=
3333
separate: false
34+
contrast_file:
35+
type: ['null', File]
36+
label: Contrast definition file (.con)
37+
inputBinding:
38+
prefix: --con=
39+
separate: false
3440
threshold:
3541
type: ['null', double]
3642
label: Threshold for FILM estimation (default 1000)
@@ -145,6 +151,42 @@ outputs:
145151
glob:
146152
- $(inputs.results_dir || 'results')/threshac1.nii.gz
147153
- $(inputs.results_dir || 'results')/threshac1.nii
154+
cope:
155+
type:
156+
- 'null'
157+
- type: array
158+
items: File
159+
outputBinding:
160+
glob:
161+
- $(inputs.results_dir || 'results')/cope*.nii.gz
162+
- $(inputs.results_dir || 'results')/cope*.nii
163+
varcope:
164+
type:
165+
- 'null'
166+
- type: array
167+
items: File
168+
outputBinding:
169+
glob:
170+
- $(inputs.results_dir || 'results')/varcope*.nii.gz
171+
- $(inputs.results_dir || 'results')/varcope*.nii
172+
tstat:
173+
type:
174+
- 'null'
175+
- type: array
176+
items: File
177+
outputBinding:
178+
glob:
179+
- $(inputs.results_dir || 'results')/tstat*.nii.gz
180+
- $(inputs.results_dir || 'results')/tstat*.nii
181+
zstat:
182+
type:
183+
- 'null'
184+
- type: array
185+
items: File
186+
outputBinding:
187+
glob:
188+
- $(inputs.results_dir || 'results')/zstat*.nii.gz
189+
- $(inputs.results_dir || 'results')/zstat*.nii
148190
log:
149191
type: File
150192
outputBinding:

src/components/BIDSDataModal.jsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,6 @@ const BIDSDataModal = ({ show, onClose, bidsStructure }) => {
268268
});
269269
}, []);
270270

271-
const toggleAllSubjects = useCallback(() => {
272-
setSelectedSubjects(prev =>
273-
prev.size === allSubjectIds.length ? new Set() : new Set(allSubjectIds)
274-
);
275-
}, [allSubjectIds]);
276271

277272
// --- Data type handlers ---
278273
const toggleDataType = useCallback((dt) => {
@@ -398,9 +393,14 @@ const BIDSDataModal = ({ show, onClose, bidsStructure }) => {
398393
{/* ---- Level 1: Subject Panel ---- */}
399394
<div className="bids-subject-panel">
400395
<div className="bids-subject-header">
401-
<button className="bids-select-all-btn" onClick={toggleAllSubjects}>
402-
{selectedSubjects.size === allSubjectIds.length ? 'Deselect all' : 'Select all'}
403-
</button>
396+
<div className="bids-select-btns">
397+
<button className="bids-select-all-btn" onClick={() => setSelectedSubjects(new Set(allSubjectIds))}>
398+
Select all
399+
</button>
400+
<button className="bids-select-all-btn" onClick={() => setSelectedSubjects(new Set())}>
401+
Deselect all
402+
</button>
403+
</div>
404404
<span className="bids-subject-count">
405405
{selectedSubjects.size}/{allSubjectIds.length}
406406
</span>

src/components/CWLPreviewPanel.jsx

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ function CWLPreviewPanel({ getWorkflowData }) {
3434
const [activeTab, setActiveTab] = useState('workflow');
3535
const [showFullscreen, setShowFullscreen] = useState(false);
3636
const [copied, setCopied] = useState(false);
37+
const [copiedPane, setCopiedPane] = useState(null);
3738
const debounceRef = useRef(null);
3839
const copiedTimerRef = useRef(null);
40+
const copiedPaneTimerRef = useRef(null);
3941

4042
const [isCollapsed, setIsCollapsed] = useState(() => {
4143
try {
@@ -76,9 +78,9 @@ function CWLPreviewPanel({ getWorkflowData }) {
7678
return;
7779
}
7880
setShowPlaceholder(false);
79-
const { wf, jobDefaults } = buildCWLWorkflowObject(graph);
81+
const { wf, jobDefaults, cwlDefaultKeys } = buildCWLWorkflowObject(graph);
8082
setCwlOutput(SHEBANG + YAML.dump(wf, { noRefs: true }));
81-
setJobOutput(buildJobTemplate(wf, jobDefaults));
83+
setJobOutput(buildJobTemplate(wf, jobDefaults, cwlDefaultKeys));
8284
setError(null);
8385
} catch (err) {
8486
setShowPlaceholder(false);
@@ -91,8 +93,11 @@ function CWLPreviewPanel({ getWorkflowData }) {
9193

9294
const activeContent = activeTab === 'workflow' ? cwlOutput : jobOutput;
9395

94-
// Clean up copied-reset timer on unmount
95-
useEffect(() => () => { if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current); }, []);
96+
// Clean up copied-reset timers on unmount
97+
useEffect(() => () => {
98+
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
99+
if (copiedPaneTimerRef.current) clearTimeout(copiedPaneTimerRef.current);
100+
}, []);
96101

97102
const handleCopy = useCallback(() => {
98103
navigator.clipboard.writeText(activeContent).then(() => {
@@ -103,6 +108,16 @@ function CWLPreviewPanel({ getWorkflowData }) {
103108
}, [activeContent]);
104109

105110
const highlightedHtml = useMemo(() => activeContent ? highlightYaml(activeContent) : '', [activeContent]);
111+
const highlightedCwl = useMemo(() => cwlOutput ? highlightYaml(cwlOutput) : '', [cwlOutput]);
112+
const highlightedJob = useMemo(() => jobOutput ? highlightYaml(jobOutput) : '', [jobOutput]);
113+
114+
const handleCopyPane = useCallback((content, pane) => {
115+
navigator.clipboard.writeText(content).then(() => {
116+
setCopiedPane(pane);
117+
if (copiedPaneTimerRef.current) clearTimeout(copiedPaneTimerRef.current);
118+
copiedPaneTimerRef.current = setTimeout(() => setCopiedPane(null), 1500);
119+
}).catch(() => showWarning('Copy to clipboard failed'));
120+
}, []);
106121

107122
return (
108123
<>
@@ -186,22 +201,43 @@ function CWLPreviewPanel({ getWorkflowData }) {
186201
className="cwl-fullscreen-modal"
187202
>
188203
<Modal.Header>
189-
<Modal.Title>
190-
{activeTab === 'workflow' ? 'CWL Workflow Preview' : 'Job Template Preview'}
191-
</Modal.Title>
192-
<button
193-
className="cwl-action-btn"
194-
onClick={handleCopy}
195-
disabled={!activeContent}
196-
>
197-
{copied ? 'Copied!' : 'Copy'}
198-
</button>
204+
<Modal.Title>CWL Preview</Modal.Title>
199205
</Modal.Header>
200206
<Modal.Body>
201-
<pre
202-
className="cwl-code cwl-code-fullscreen"
203-
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
204-
/>
207+
<div className="cwl-split-container">
208+
<div className="cwl-split-pane">
209+
<div className="cwl-split-pane-header">
210+
<span className="cwl-split-pane-label">.cwl</span>
211+
<button
212+
className="cwl-action-btn"
213+
onClick={() => handleCopyPane(cwlOutput, 'cwl')}
214+
disabled={!cwlOutput}
215+
>
216+
{copiedPane === 'cwl' ? 'Copied!' : 'Copy'}
217+
</button>
218+
</div>
219+
<pre
220+
className="cwl-code cwl-code-fullscreen"
221+
dangerouslySetInnerHTML={{ __html: highlightedCwl }}
222+
/>
223+
</div>
224+
<div className="cwl-split-pane">
225+
<div className="cwl-split-pane-header">
226+
<span className="cwl-split-pane-label">.yml</span>
227+
<button
228+
className="cwl-action-btn"
229+
onClick={() => handleCopyPane(jobOutput, 'job')}
230+
disabled={!jobOutput}
231+
>
232+
{copiedPane === 'job' ? 'Copied!' : 'Copy'}
233+
</button>
234+
</div>
235+
<pre
236+
className="cwl-code cwl-code-fullscreen"
237+
dangerouslySetInnerHTML={{ __html: highlightedJob }}
238+
/>
239+
</div>
240+
</div>
205241
</Modal.Body>
206242
</Modal>
207243
</>

src/components/CustomWorkflowParamModal.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@ const CustomWorkflowParamModal = ({ show, onClose, workflowName, internalNodes,
644644
</div>
645645
)}
646646
</div>
647+
647648
</Form>
648649
</Modal.Body>
649650
</Modal>

src/components/EdgeMappingModal.jsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ const getBaseType = (type) => {
1515

1616
const isArrayType = (type) => type?.includes('[]') || false;
1717

18+
/** When the source node is scattered, scalar File/Directory outputs are effectively arrays. */
19+
const getEffectiveOutputType = (type, sourceIsScattered) => {
20+
if (!sourceIsScattered) return type;
21+
const base = getBaseType(type);
22+
if (!isArrayType(type) && (base === 'File' || base === 'Directory')) return `${base}[]`;
23+
return type;
24+
};
25+
1826
const checkTypeCompatibility = (outputType, inputType, outputExtensions = null, inputAcceptedExtensions = null) => {
1927
if (!outputType || !inputType) return { compatible: true, warning: true, reason: 'Type information unavailable' };
2028

@@ -221,6 +229,7 @@ const EdgeMappingModal = ({
221229
targetNode,
222230
existingMappings = [],
223231
adjacencyWarning = null,
232+
sourceIsScattered = false,
224233
}) => {
225234
const { showWarning } = useToast();
226235
const [mappings, setMappings] = useState([]);
@@ -437,8 +446,9 @@ const EdgeMappingModal = ({
437446
const getMappingCompatibility = (outputName, inputName) => {
438447
const output = sourceIO.outputs.find(o => o.name === outputName);
439448
const input = targetIO.inputs.find(i => i.name === inputName);
449+
const effectiveOutputType = getEffectiveOutputType(output?.type, sourceIsScattered);
440450
return checkTypeCompatibility(
441-
output?.type,
451+
effectiveOutputType,
442452
input?.type,
443453
output?.extensions,
444454
input?.acceptedExtensions
@@ -516,7 +526,7 @@ const EdgeMappingModal = ({
516526
>
517527
<div className="io-item-main">
518528
<span className="io-name">{output.label}</span>
519-
<span className="io-type">{output.type}</span>
529+
<span className="io-type">{getEffectiveOutputType(output.type, sourceIsScattered)}</span>
520530
{!compatibility.compatible && <span className="warning-icon" title={compatibility.reason}>⚠️</span>}
521531
</div>
522532
{output.extensions?.length > 0 && (

src/components/IONodeModal.jsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useState, useEffect } from 'react';
2+
import { Modal, Form, Button } from 'react-bootstrap';
3+
import '../styles/ioNodeModal.css';
4+
5+
function IONodeModal({ show, onHide, label, notes, onSave }) {
6+
const [nameValue, setNameValue] = useState(label || '');
7+
const [notesValue, setNotesValue] = useState(notes || '');
8+
9+
// Sync state when modal opens with new data
10+
useEffect(() => {
11+
if (show) {
12+
setNameValue(label || '');
13+
setNotesValue(notes || '');
14+
}
15+
}, [show, label, notes]);
16+
17+
const handleSave = () => {
18+
onSave({
19+
label: nameValue.trim() || label,
20+
notes: notesValue,
21+
});
22+
onHide();
23+
};
24+
25+
return (
26+
<Modal
27+
show={show}
28+
onHide={onHide}
29+
centered
30+
size="sm"
31+
className="io-node-modal"
32+
>
33+
<Modal.Header>
34+
<Modal.Title>Edit I/O Node</Modal.Title>
35+
</Modal.Header>
36+
<Modal.Body onClick={(e) => e.stopPropagation()}>
37+
<Form>
38+
<Form.Group className="io-modal-field">
39+
<Form.Label className="io-modal-label">Name</Form.Label>
40+
<Form.Control
41+
type="text"
42+
value={nameValue}
43+
onChange={(e) => setNameValue(e.target.value)}
44+
className="io-modal-input"
45+
placeholder="Node name"
46+
autoFocus
47+
/>
48+
</Form.Group>
49+
<Form.Group className="io-modal-field">
50+
<Form.Label className="io-modal-label">Notes</Form.Label>
51+
<Form.Control
52+
as="textarea"
53+
rows={4}
54+
value={notesValue}
55+
onChange={(e) => setNotesValue(e.target.value)}
56+
className="io-modal-textarea"
57+
placeholder="Add notes about this node..."
58+
/>
59+
</Form.Group>
60+
</Form>
61+
</Modal.Body>
62+
<Modal.Footer>
63+
<Button variant="secondary" onClick={onHide}>Cancel</Button>
64+
<Button variant="primary" onClick={handleSave}>Save</Button>
65+
</Modal.Footer>
66+
</Modal>
67+
);
68+
}
69+
70+
export default IONodeModal;

0 commit comments

Comments
 (0)