Jaspers Screener: Screens US SEC registrants by fundamentals, holders and insiders, then reads filings against written criteria with cited quotes.
| 1 | import { useState, type ReactElement } from 'react' |
| 2 | import { fieldLabel, groupFields, MAX_COLUMNS, type Field } from './screener' |
| 3 | |
| 4 | // Which fields the table shows besides the ones filtered and sorted on, which are shown whatever is |
| 5 | // picked here. The list is the server's guide, group by group; what is picked goes into the panel's |
| 6 | // state, and from there into the screen's arguments, since the server sends only the columns asked for. |
| 7 | |
| 8 | interface Props { |
| 9 | columns: string[] |
| 10 | fields: Field[] |
| 11 | fieldsError: string | null |
| 12 | onApply: (columns: string[]) => void |
| 13 | onClose: () => void |
| 14 | } |
| 15 | |
| 16 | export function ColumnsDialog({ columns, fields, fieldsError, onApply, onClose }: Props): ReactElement { |
| 17 | const [picked, setPicked] = useState<string[]>(columns) |
| 18 | // The identity columns are the company cell; the table draws them whatever is asked for. |
| 19 | const groups = groupFields(fields.filter((field) => !['cik', 'name', 'sic_description'].includes(field.field))) |
| 20 | |
| 21 | function toggle(name: string, on: boolean): void { |
| 22 | setPicked((previous) => (on ? [...previous, name] : previous.filter((item) => item !== name))) |
| 23 | } |
| 24 | |
| 25 | return ( |
| 26 | <div className="sc-dialog" onMouseDown={onClose}> |
| 27 | <div className="sc-dialog-panel" onMouseDown={(event) => event.stopPropagation()}> |
| 28 | <div className="sc-dialog-head"> |
| 29 | <span>Columns</span> |
| 30 | <button type="button" className="sc-btn" style={{ marginLeft: 'auto' }} onClick={onClose} aria-label="Close"> |
| 31 | × |
| 32 | </button> |
| 33 | </div> |
| 34 | |
| 35 | <div className="sc-dialog-body"> |
| 36 | {fields.length === 0 && <p className="sc-error">{fieldsError ?? 'The server sent no list of fields.'}</p>} |
| 37 | {groups.map(([group, members]) => ( |
| 38 | <div key={group} className="sc-section"> |
| 39 | <span className="sc-label">{group}</span> |
| 40 | <div className="sc-columns"> |
| 41 | {members.map((field) => ( |
| 42 | <label key={field.field} className="sc-check" title={field.description}> |
| 43 | <input type="checkbox" checked={picked.includes(field.field)} onChange={(event) => toggle(field.field, event.target.checked)} /> |
| 44 | <span>{fieldLabel(field.field)}</span> |
| 45 | </label> |
| 46 | ))} |
| 47 | </div> |
| 48 | </div> |
| 49 | ))} |
| 50 | </div> |
| 51 | |
| 52 | <div className="sc-dialog-foot"> |
| 53 | <span className="sc-status" style={{ marginRight: 'auto' }}> |
| 54 | {picked.length} picked{picked.length > MAX_COLUMNS ? `, ${MAX_COLUMNS} at most` : ''} |
| 55 | </span> |
| 56 | <button type="button" className="sc-btn" onClick={() => setPicked([])}> |
| 57 | None |
| 58 | </button> |
| 59 | <button type="button" className="sc-btn" onClick={onClose}> |
| 60 | Cancel |
| 61 | </button> |
| 62 | <button type="button" className="sc-btn" disabled={picked.length > MAX_COLUMNS} onClick={() => onApply(picked)}> |
| 63 | Apply |
| 64 | </button> |
| 65 | </div> |
| 66 | </div> |
| 67 | </div> |
| 68 | ) |
| 69 | } |