• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zooniverse / Panoptes-Front-End / 21962335028

12 Feb 2026 08:04PM UTC coverage: 55.977% (-0.02%) from 55.996%
21962335028

push

github

web-flow
Refactor GeoDrawing editor with color and uncertainty_circle options (#7402)

* Refactor GeoDrawing editor with color and uncertainty_circle options

* Create toolColors constant, update generic-editor and Pages Editor DrawingTool

7202 of 13250 branches covered (54.35%)

Branch coverage included in aggregate %.

3 of 5 new or added lines in 2 files covered. (60.0%)

91 existing lines in 2 files now uncovered.

8265 of 14381 relevant lines covered (57.47%)

100.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.04
/app/classifier/tasks/generic-editor.cjsx
1
React = require 'react'
1✔
2
createReactClass = require 'create-react-class'
3
ChangeListener = require '../../components/change-listener'
1!
4
AutoSave = require '../../components/auto-save'
5
handleInputChange = require('../../lib/handle-input-change').default
1✔
6
drawingTools = require '../drawing-tools'
7
alert = require('../../lib/alert').default
1✔
8
DrawingTaskDetailsEditor = require './drawing-task-details-editor'
9
NextTaskSelector = require './next-task-selector'
1✔
10
{MarkdownEditor, MarkdownHelp} = require 'markdownz'
11
isAdmin = require '../../lib/is-admin'
1✔
12
TOOL_COLOR_OPTIONS = require('../../constants/toolColors').default
13

1✔
14
# `import MinMaxEditor from './drawing/min-max-editor';`
15
MinMaxEditor = require('./drawing/min-max-editor').default
1✔
16
GridEditor = require('./drawing/grid-editor').default
17

1✔
18
module.exports = createReactClass
19
  displayName: 'GenericTaskEditor'
1✔
20

21
  getDefaultProps: ->
1✔
22
    workflow: null
23
    task: null
1✔
24
    taskPrefix: ''
25

1✔
26
  render: ->
27
    handleChange = handleInputChange.bind @props.workflow
1✔
28

29
    [mainTextKey, choicesKey] = switch @props.task.type
1✔
30
      when 'single', 'multiple' then ['question', 'answers']
31
      when 'drawing' then ['instruction', 'tools']
1✔
32
      when 'crop' then ['instruction']
33
      when 'text' then ['instruction']
1✔
34
      when 'slider' then ['instruction']
35
      when 'highlighter' then ['instruction', 'highlighterLabels']
36
      when 'dataVisAnnotation' then ['instruction', 'tools']
1✔
37
      when 'volumetric' then ['instruction']
38
      when 'geoDrawing' then ['instruction', 'tools']
39

40
    isAQuestion = @props.task.type in ['single', 'multiple']
41
    canBeRequired = @props.task.type in ['single', 'multiple', 'text']
42

43
    <div className="workflow-task-editor #{@props.task.type}">
UNCOV
44
      <div>
×
UNCOV
45
        <AutoSave resource={@props.workflow}>
×
UNCOV
46
          <span className="form-label">Main text</span>
×
47
          <br />
48
          <textarea name="#{@props.taskPrefix}.#{mainTextKey}" value={@props.task[mainTextKey]} className="standard-input full" onChange={handleChange} />
UNCOV
49
        </AutoSave>
×
50
        <small className="form-help">Describe the task, or ask the question, in a way that is clear to a non-expert. Markdown can be used only to add images (with alt text), bold and italic text.</small><br />
UNCOV
51
      </div>
×
52
      <br />
UNCOV
53

×
54
      {unless @props.isSubtask
UNCOV
55
        <div>
×
56
          <AutoSave resource={@props.workflow}>
UNCOV
57
            <span className="form-label">Help text</span>
×
58
            <br />
UNCOV
59
            <MarkdownEditor name="#{@props.taskPrefix}.help" onHelp={-> alert <MarkdownHelp/>} value={@props.task.help ? ""} rows="7" className="full" onChange={handleChange} />
×
60
          </AutoSave>
UNCOV
61
          <small className="form-help">Add text and images for a window that pops up when volunteers click “Need some help?” You can use markdown to format this text and add images. The help text can be as long as you need, but you should try to keep it simple and avoid jargon.</small>
×
62
        </div>}
UNCOV
63

×
64
      {if choicesKey?
UNCOV
65
        <div>
×
66
          <hr />
67
          <span className="form-label">Choices</span>
68
        </div>}
×
UNCOV
69
      {' '}
×
UNCOV
70

×
71
      {unless @props.task.type is 'geoDrawing'
72
        <label className="pill-button">
73
          <AutoSave resource={@props.workflow}>
74
            <input type="checkbox" checked={@props.task.enableHidePrevMarks} onChange={@toggleHidePrevMarksEnabled} />{' '}
75
            Allow hiding marks
76
          </AutoSave>
77
        </label>}
78

79
      {if isAQuestion
80
        multipleHelp = 'Multiple Choice: Check this box if more than one answer can be selected.'
81

82
        <span>
83
          <label className="pill-button" title={multipleHelp}>
×
84
            <AutoSave resource={@props.workflow}>
85
              <input type="checkbox" checked={@props.task.type is 'multiple'} onChange={@toggleMultipleChoice} />{' '}
86
              Allow multiple
87
            </AutoSave>
88
          </label>
89
          {' '}
90
        </span>}
×
91

92
      {if canBeRequired
×
93
        requiredHelp = 'Check this box if this question has to be answered before proceeding. If a marking task is Required, the volunteer will not be able to move on until they have made at least 1 mark.'
94
        <span>
95
          <label className="pill-button" title={requiredHelp}>
96
            <AutoSave resource={@props.workflow}>
97
              <input type="checkbox" name="#{@props.taskPrefix}.required" checked={@props.task.required} onChange={handleChange} />{' '}
98
              Required
×
99
            </AutoSave>
100
          </label>
×
101
          {' '}
102
        </span>}
103
      <br />
104

105
      {if choicesKey?
106
        <div className="workflow-task-editor-choices">
107
          {if (@props.task[choicesKey]?.length ? 0) is 0 # Work around the empty-array-becomes-null bug on the back end.
108
            <span className="form-help">No <code>{choicesKey}</code> defined for this task.</span>}
×
109
          {for choice, index in @props.task[choicesKey] ? []
110
            choice._key ?= Math.random()
111
            <div key={choice._key} className="workflow-choice-editor">
112
              <AutoSave resource={@props.workflow}>
113
                <textarea name="#{@props.taskPrefix}.#{choicesKey}.#{index}.label" className="standard-input full" value={choice.label} onChange={handleChange} />
114
              </AutoSave>
115

116
              <div className="workflow-choice-settings">
117
                {switch @props.task.type
×
118
                  when 'single'
119
                    unless @props.isSubtask
120
                      <div className="workflow-choice-setting">
121
                        <AutoSave resource={@props.workflow}>
122
                          Next task{' '}
123
                          <NextTaskSelector task={@props.task} workflow={@props.workflow} name="#{@props.taskPrefix}.#{choicesKey}.#{index}.next" value={choice.next ? ''} onChange={handleChange} />
124
                        </AutoSave>
125
                      </div>
126

127
                  when 'highlighter'
×
128
                    <div className="workflow-choice-setting" >
129
                      <AutoSave resource={@props.workflow} >
×
130
                        Color{' '}
131
                        <select style={{background: choice.color}} name="#{@props.taskPrefix}.#{choicesKey}.#{index}.color" value={choice.color} onChange={handleChange}>
132
                          {for labelOption in TOOL_COLOR_OPTIONS
UNCOV
133
                            <option
×
UNCOV
134
                              key={labelOption.value}
×
UNCOV
135
                              style={{ background: labelOption.value }}
×
UNCOV
136
                              value={labelOption.value}
×
UNCOV
137
                            >
×
UNCOV
138
                              {labelOption.label}
×
139
                            </option>}
UNCOV
140
                        </select>
×
141
                      </AutoSave>
142
                    </div>
143

144
                  when 'dataVisAnnotation'
145
                    <div className="workflow-choice-setting" >
146
                      <AutoSave resource={@props.workflow} >
147
                        Color{' '}
148
                        <select style={{background: choice.color}} name="#{@props.taskPrefix}.#{choicesKey}.#{index}.color" value={choice.color} onChange={handleChange}>
149
                          {for labelOption in TOOL_COLOR_OPTIONS
150
                            <option
151
                              key={labelOption.value}
152
                              style={{ background: labelOption.value }}
153
                              value={labelOption.value}
UNCOV
154
                            >
×
155
                              {labelOption.label}
156
                            </option>}
×
157
                        </select>
×
158
                      </AutoSave>
159
                    </div>
160

161
                  when 'drawing'
162
                    options = drawingTools[choice.type].options ? []
163
                    [<div key="type" className="workflow-choice-setting">
164
                      <AutoSave resource={@props.workflow}>
165
                        Type{' '}
×
166
                        <select name="#{@props.taskPrefix}.#{choicesKey}.#{index}.type" value={choice.type} onChange={handleChange}>
167
                          {for toolKey of drawingTools
168
                            <option key={toolKey} value={toolKey}>{toolKey}</option> unless toolKey in ["grid", "freehandLine", "freehandShape", "freehandSegmentLine", "freehandSegmentShape", "anchoredEllipse", "fan", "transcriptionLine", "temporalPoint", "temporalRotateRectangle"]}
UNCOV
169
                          {if @canUse("grid")
×
170
                            <option key="grid" value="grid">grid</option>}
UNCOV
171
                          {if @canUse("freehandLine")
×
172
                            <option key="freehandLine" value="freehandLine">freehand line</option>}
173
                          {if @canUse("freehandShape")
174
                            <option key="freehandShape" value="freehandShape">freehand shape</option>}
175
                          {if @canUse("freehandSegmentLine")
176
                            <option key="freehandSegmentLine" value="freehandSegmentLine">freehand segment line</option>}
177
                          {if @canUse("freehandSegmentShape")
178
                            <option key="freehandSegmentShape" value="freehandSegmentShape">freehand segment shape</option>}
179
                          {if @canUse("anchoredEllipse")
180
                            <option key="anchoredEllipse" value="anchoredEllipse">anchored ellipse</option>}
181
                          {if @canUse("fan")
182
                            <option key="fan" value="fan">fan tool</option>}
183
                          {if @canUse("temporalPoint")
UNCOV
184
                            <option key="temporalPoint" value="temporalPoint">temporalPoint</option>}
×
UNCOV
185
                          {if @canUse("temporalRotateRectangle")
×
UNCOV
186
                            <option key="temporalRotateRectangle" value="temporalRotateRectangle">temporalRotateRectangle</option>}
×
UNCOV
187
                          {if isAdmin()
×
188
                            <option key="transcriptionLine" value="transcriptionLine">transcription line</option>}
189
                        </select>
190
                      </AutoSave>
191
                    </div>
192

193
                    <div key="color" className="workflow-choice-setting">
194
                      <AutoSave resource={@props.workflow}>
UNCOV
195
                        Color{' '}
×
196
                        <select name="#{@props.taskPrefix}.#{choicesKey}.#{index}.color" value={choice.color} onChange={handleChange}>
197
                          # These are historic PFE task tool colors,
NEW
198
                          # for new development consider using TOOL_COLOR_OPTIONS.
×
199
                          <option value="#ff0000">Red</option>
200
                          <option value="#ffff00">Yellow</option>
201
                          <option value="#00ff00">Green</option>
202
                          <option value="#00ffff">Cyan</option>
203
                          <option value="#0000ff">Blue</option>
204
                          <option value="#ff00ff">Magenta</option>
205
                          <option value="#000000">Black</option>
206
                          <option value="#ffffff">White</option>
207
                        </select>
208
                      </AutoSave>
209
                    </div>
210

211
                    <MinMaxEditor
×
UNCOV
212
                      key='min-max'
×
UNCOV
213
                      workflow={@props.workflow}
×
UNCOV
214
                      name="#{@props.taskPrefix}.#{choicesKey}.#{index}"
×
215
                      choice={choice}
216
                    />
217

218
                    if 'size' in options
219
                      <div key="size" className="workflow-choice-setting">
220
                        <AutoSave resource={@props.workflow}>
221
                          <label>Size{' '}
222
                            <select
×
223
                            name="#{@props.taskPrefix}.#{choicesKey}.#{index}.size"
224
                            value={choice.size}
UNCOV
225
                            onChange={handleChange}
×
UNCOV
226
                            >
×
227
                              <option value="large">Large</option>
228
                              <option value="small">Small</option>
229
                            </select>
230
                          </label>
231
                        </AutoSave>
232
                      </div>
233
                    else
234
                      null
235
                    if 'grid' in options
236
                      <GridEditor
237
                        key="gridoptions"
238
                        workflow={@props.workflow}
×
UNCOV
239
                        name="#{@props.taskPrefix}.#{choicesKey}.#{index}"
×
UNCOV
240
                        choice={choice}
×
UNCOV
241
                      />
×
242
                    else
243
                      null
244

245
                    <div key="details" className="workflow-choice-setting">
246
                      <button type="button" onClick={@editToolDetails.bind this, @props.task, index}>Sub-tasks ({choice.details?.length ? 0})</button>{' '}
×
247
                      <small className="form-help">Ask users a question about what they’ve just drawn.</small>
248
                    </div>]
249

×
250
                  when 'geoDrawing'
×
251
                    [<div
252
                      key="type" 
253
                      className="workflow-choice-setting"
×
254
                    >
255
                      <AutoSave resource={@props.workflow}>
256
                        Geometry type{' '}
×
257
                        <select name="#{@props.taskPrefix}.#{choicesKey}.#{index}.type" value={choice.type} onChange={handleChange}>
258
                          <option key="Point" value="Point">Point</option>
259
                        </select>
×
260
                      </AutoSave>
261
                    </div>
262

×
263
                    <div
264
                      key="color"
265
                      className="workflow-choice-setting"
×
266
                    >
267
                      <AutoSave resource={@props.workflow}>
268
                        Color{' '}
×
269
                        <select
270
                          style={{background: choice.color}} 
271
                          name="#{@props.taskPrefix}.#{choicesKey}.#{index}.color"
×
272
                          value={choice.color}
273
                          onChange={handleChange}
274
                        >
×
275
                          {for labelOption in TOOL_COLOR_OPTIONS
276
                            <option
277
                              key={labelOption.value}
×
278
                              style={{ background: labelOption.value }}
279
                              value={labelOption.value}
280
                            >
281
                              {labelOption.label}
282
                            </option>}
283
                        </select>
284
                      </AutoSave>
285
                    </div>
286
                    
287
                    if choice.type is 'Point'
288
                      <div
289
                        key="uncertainty_circle"
290
                        className="workflow-choice-setting"
291
                      >
292
                        <label>
293
                          <AutoSave resource={@props.workflow}>
294
                            Show uncertainty circle<sup>*</sup>:{' '}
295
                            <input
296
                              type="checkbox"
297
                              name="#{@props.taskPrefix}.#{choicesKey}.#{index}.uncertainty_circle"
298
                              checked={choice.uncertainty_circle}
299
                              onChange={handleChange}
300
                            />{' '}
301
                            <br />
302
                            <small><sup>*</sup>per GeoJSON feature&apos;s <code>properties.uncertainty_radius</code> value (integer).</small>{' '}
303
                          </AutoSave>
304
                        </label>
305
                      </div>
306
                    else
307
                      null
308
                    ]
309
                }
310
              </div>
×
311

312
              <AutoSave resource={@props.workflow}>
313
                <button type="button" className="workflow-choice-remove-button" title="Remove choice" onClick={@removeChoice.bind this, choicesKey, index}>&times;</button>
314
              </AutoSave>
315
            </div>}
316

317
          <AutoSave resource={@props.workflow}>
318
            <button type="button" className="workflow-choice-add-button" title="Add choice" onClick={@addChoice.bind this, choicesKey}>+</button>
319
          </AutoSave>
320
          <br />
321

322
          {switch choicesKey
323
            when 'answers'
×
324
              <div>
325
                <small className="form-help">The answers will be displayed next to each checkbox, so this text is as important as the main text and help text for guiding the volunteers. Keep your answers as minimal as possible -- any more than 5 answers can discourage new users.</small><br />
326
                <small className="form-help">The “Next task” selection describes what task you want the volunteer to perform next after they give a particular answer. You can choose from among the tasks you’ve already defined. If you want to link a task to another you haven’t built yet, you can come back and do it later (don’t forget to save your changes).</small>
327
              </div>
328
            when 'highlighterLabels'
329
              <div>
330
                <small className="form-help"> Add labels for the highlighter tool.</small>
331
              </div>
332
            when 'tools'
333
              if @props.task.type is 'dataVisAnnotation'
334
                <div>
×
335
                  <small className="form-help"> Add labels for the data selection tool.</small>
336
                </div>
337
              if @props.task.type is 'drawing'
338
                <div>
UNCOV
339
                  <small className="form-help">Select which marks you want for this task, and what to call each of them. The tool name will be displayed on the classification page next to each marking option. Use the simplest tool that will give you the results you need for your research.</small><br />
×
340
                  <small className="form-help"><b>bezier:</b> an arbitrary shape made of point-to-point curves. The midpoint of each segment drawn can be dragged to adjust the curvature. </small><br />
341
                  <small className="form-help"><b>circle:</b> a point and a radius.</small><br />
342
                  <small className="form-help"><b>column:</b> a box with full height but variable width; this tool <i>cannot</i> be rotated.</small><br />
343
                  <small className="form-help"><b>ellipse:</b> an oval of any size and axis ratio; this tool <i>can</i> be rotated.</small><br />
344
                  <small className="form-help"><b>line:</b> a straight line at any angle.</small><br />
345
                  <small className="form-help"><b>point:</b> X marks the spot.</small><br />
346
                  <small className="form-help"><b>polygon:</b> an arbitrary shape made of point-to-point lines.</small><br />
347
                  <small className="form-help"><b>rectangle:</b> a box of any size and length-width ratio; this tool <i>cannot</i> be rotated.</small><br />
348
                  <small className="form-help"><b>triangle:</b> an equilateral triangle of any size and vertex distance from the center; this tool <i>can</i> be rotated.</small><br />
349
                  {if @canUse("grid")
350
                    <small className="form-help"><b>grid table</b>: cells which can be made into a table for consecutive annotations.</small>}
351
                  {if @canUse("anchoredEllipse")
352
                    <small className="form-help"><b>anchored ellipse</b>: creates an ellipes in the center of the subject during the first click, and does not allow it to be dragged.</small>}}
353
                </div>}
354
        </div>}
355

356
      {unless @props.task.type is 'single' or @props.isSubtask
357
        <div>
358
          <AutoSave resource={@props.workflow}>
359
            Next task{' '}
360
            <NextTaskSelector task={@props.task} workflow={@props.workflow} name="#{@props.taskPrefix}.next" value={@props.task.next ? ''} onChange={handleChange} />
361
          </AutoSave>
362
        </div>}
363
    </div>
364

365
  canUse: (tool) ->
UNCOV
366
    tool in @props.project.experimental_tools
×
UNCOV
367

×
UNCOV
368
  toggleHidePrevMarksEnabled: (e) ->
×
UNCOV
369
    enableHidePrevMarks = e.target.checked
×
370
    @props.task.enableHidePrevMarks = enableHidePrevMarks
371
    @props.onChange @props.task
372

373
  toggleMultipleChoice: (e) ->
374
    newType = if e.target.checked
375
      'multiple'
376
    else
UNCOV
377
      'single'
×
378
    @props.task.type = newType
×
379
    @props.onChange @props.task
380

381
  addChoice: (type) ->
382
    switch type
383
      when 'answers' then @addAnswer()
384
      when 'tools' then @addTool()
385
      when 'highlighterLabels' then @addHighlighterLabels()
386

387
  addAnswer: ->
388
    @props.task.answers.push
389
      label: 'Enter an answer'
390
    @props.onChange @props.task
391

392
  addHighlighterLabels: ->
393
    toolLabelColors = TOOL_COLOR_OPTIONS.map((option) => option.value)
394
    taskColors = @props.task.highlighterLabels.map((label) => label.color)
395
    newColor = toolLabelColors.find((color) => taskColors.indexOf(color) == -1) || toolLabelColors[0]
396

397
    @props.task.highlighterLabels.push
398
      color: newColor
399
      label: 'Enter label'
UNCOV
400
    @props.onChange @props.task
×
401

402
  addTool: ->
403
    if @props.task.type is 'drawing'
404
      @props.task.tools.push
405
        type: 'point'
406
        label: 'Tool name'
407
        color: '#00ff00'
408
        details: []
UNCOV
409
    if @props.task.type is 'geoDrawing'
×
410
      toolLabelColors = TOOL_COLOR_OPTIONS.map((option) => option.value)
NEW
411
      taskColors = @props.task.tools.map((tool) => tool.color)
×
412
      newColor = toolLabelColors.find((color) => taskColors.indexOf(color) == -1) || toolLabelColors[0]      
413
      @props.task.tools.push
414
        type: 'Point'
415
        label: 'Tool name',
416
        color: newColor
UNCOV
417
    if @props.task.type is 'dataVisAnnotation'
×
418
      toolLabelColors = TOOL_COLOR_OPTIONS.map((option) => option.value)
419
      taskColors = @props.task.tools.map((tool) => tool.color)
420
      newColor = toolLabelColors.find((color) => taskColors.indexOf(color) == -1) || toolLabelColors[0]
UNCOV
421
      @props.task.tools.push
×
UNCOV
422
        type: 'graph2dRangeX'
×
423
        label: 'Tool name'
424
        color: newColor
425
    @props.onChange @props.task
UNCOV
426

×
UNCOV
427
  editToolDetails: (task, toolIndex) ->
×
428
    @props.task.tools[toolIndex].details ?= []
429

430
    alert (resolve) =>
431
      <ChangeListener target={@props.workflow}>{=>
432
        <DrawingTaskDetailsEditor
433
          project={@props.project}
434
          workflow={@props.workflow}
435
          task={@props.task}
436
          toolIndex={toolIndex}
437
          details={@props.task.tools[toolIndex].details}
438
          toolPath="#{@props.taskPrefix}.tools.#{toolIndex}"
439
          onClose={resolve}
440
        />
441
      }</ChangeListener>
442

443
  removeChoice: (choicesName, index) ->
444
    @props.task[choicesName].splice index, 1
445
    @props.onChange @props.task
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc