From e2fd2d694fb25f1331c715ea4b2abb6f641e2b3f Mon Sep 17 00:00:00 2001 From: Casey Locker Date: Fri, 14 Aug 2026 09:30:19 -0500 Subject: [PATCH 1/4] docs(components): declare propTypes across the Core UI components Adds propTypes with per-prop JSDoc descriptions to the components exported by the library. Dev-time only; no rendering behaviour changes. Co-Authored-By: Claude --- src/components/ajaxloader/index.js | 14 ++++++ src/components/bulk-actions-selector/index.js | 15 ++++++ src/components/clock.js | 11 +++++ src/components/exclusive-wrapper.js | 7 +++ src/components/forms/rsvp-form.js | 18 +++++++ src/components/forms/simple-form.js | 17 +++++++ src/components/inputs/access-levels-input.js | 21 ++++++++ .../inputs/action-dropdown/index.js | 17 +++++++ src/components/inputs/attendee-input.js | 21 ++++++++ src/components/inputs/company-input.js | 24 ++++++++++ src/components/inputs/country-dropdown.js | 16 +++++++ src/components/inputs/country-input.js | 14 ++++++ src/components/inputs/datetimepicker/index.js | 20 ++++++++ src/components/inputs/dropdown.js | 27 +++++++++++ src/components/inputs/editor-input/index.js | 14 ++++++ src/components/inputs/event-input.js | 17 +++++++ .../inputs/free-multi-text-input.js | 11 +++++ src/components/inputs/group-input.js | 13 +++++ .../inputs/grouped-dropdown/index.js | 22 ++++++++- src/components/inputs/language-input.js | 16 +++++++ src/components/inputs/member-input.js | 17 +++++++ src/components/inputs/operator-input.js | 29 ++++++++++- src/components/inputs/organization-input.js | 17 ++++++- src/components/inputs/speaker-input.js | 21 ++++++++ src/components/inputs/sponsor-input.js | 17 +++++++ .../inputs/sponsored-project-input.js | 14 ++++++ .../inputs/stepped-select/index.jsx | 17 ++++++- src/components/inputs/summit-days-select.js | 14 ++++++ src/components/inputs/summit-input.js | 13 +++++ src/components/inputs/summit-venues-select.js | 21 ++++++++ src/components/inputs/text-input.js | 15 ++++++ src/components/inputs/textarea-input.js | 14 ++++++ .../inputs/upload-input-v2/index.js | 32 +++++++++++++ .../inputs/upload-input-v3/index.js | 35 ++++++++++++++ src/components/inputs/upload-input/index.js | 13 +++++ src/components/raw-html/index.js | 9 ++++ src/components/schedule-builder-view/index.js | 39 +++++++++++++++ src/components/sections/panel.js | 15 +++++- src/components/summit-dropdown/index.js | 16 +++++++ .../table-editable/EditableTable.js | 25 ++++++++++ .../table-selectable/SelectableTable.js | 47 ++++++++++++++++++ src/components/table/Table.js | 48 +++++++++++++++++++ src/components/video-stream.js | 6 +++ 43 files changed, 823 insertions(+), 6 deletions(-) diff --git a/src/components/ajaxloader/index.js b/src/components/ajaxloader/index.js index cde24b0c..1ee9e0b9 100644 --- a/src/components/ajaxloader/index.js +++ b/src/components/ajaxloader/index.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; const AjaxLoader = ({ show, @@ -75,4 +76,17 @@ const AjaxLoader = ({ ); }; +AjaxLoader.propTypes = { + /** Toggles display; the overlay stays mounted either way. */ + show: PropTypes.bool, + /** Positions absolute inside the nearest positioned ancestor instead of fixed to the viewport. */ + relative: PropTypes.bool, + /** Background colour of the dimming layer behind the spinner. */ + color: PropTypes.string, + /** Spinner font-size in px. */ + size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** Optional caption rendered under the spinner. */ + children: PropTypes.node +}; + export default AjaxLoader; diff --git a/src/components/bulk-actions-selector/index.js b/src/components/bulk-actions-selector/index.js index ad670a76..3ff509f8 100644 --- a/src/components/bulk-actions-selector/index.js +++ b/src/components/bulk-actions-selector/index.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import T from "i18n-react/dist/i18n-react"; import './styles.less'; class ScheduleAdminsBulkActionsSelector extends React.Component { @@ -51,4 +52,18 @@ class ScheduleAdminsBulkActionsSelector extends React.Component { } } +ScheduleAdminsBulkActionsSelector.propTypes = { + /** Renders nothing when false; the container element is always present. */ + show: PropTypes.bool, + /** Actions offered alongside the built-in default option. */ + bulkOptions: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + })).isRequired, + /** Click handler for the select-all checkbox; the component tracks no selection itself. */ + onSelectAll: PropTypes.func, + /** Receives the chosen action value on Go. Not called while the default option is selected. */ + onSelectedBulkAction: PropTypes.func.isRequired +}; + export default ScheduleAdminsBulkActionsSelector; diff --git a/src/components/clock.js b/src/components/clock.js index e5fc4fa7..091f3009 100644 --- a/src/components/clock.js +++ b/src/components/clock.js @@ -11,6 +11,7 @@ * limitations under the License. **/ import React from 'react'; +import PropTypes from 'prop-types'; import moment from "moment-timezone"; import FragmentParser from "./fragment-parser"; import {getTimeServiceUrl} from '../utils/methods'; @@ -155,4 +156,14 @@ class Clock extends React.Component { } +Clock.propTypes = { + /** Renders nothing until true and a timestamp has been resolved. */ + display: PropTypes.bool, + /** IANA zone used to format the clock. */ + timezone: PropTypes.string, + /** Called on each tick with the current epoch seconds. */ + onTick: PropTypes.func, + /** Overrides the resolved time; otherwise the summit time service is queried. */ + now: PropTypes.number +}; export default Clock; diff --git a/src/components/exclusive-wrapper.js b/src/components/exclusive-wrapper.js index 5b3a4b7b..6d0b2469 100644 --- a/src/components/exclusive-wrapper.js +++ b/src/components/exclusive-wrapper.js @@ -12,6 +12,7 @@ **/ import React from 'react' +import PropTypes from 'prop-types' export default class Exclusive extends React.Component { @@ -40,3 +41,9 @@ export default class Exclusive extends React.Component { } } + +Exclusive.propTypes = { + /** Children render only if window.EXCLUSIVE_SECTIONS includes this name. */ + name: PropTypes.string.isRequired, + children: PropTypes.node +}; diff --git a/src/components/forms/rsvp-form.js b/src/components/forms/rsvp-form.js index 53d9d71f..3372c687 100644 --- a/src/components/forms/rsvp-form.js +++ b/src/components/forms/rsvp-form.js @@ -12,6 +12,7 @@ **/ import React from 'react' +import PropTypes from 'prop-types'; import 'awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css' import Input from '../inputs/text-input' import Dropdown from '../inputs/dropdown' @@ -164,4 +165,21 @@ class RsvpForm extends React.Component { } } +RsvpForm.propTypes = { + /** Rendered by class_name, e.g. RSVPTextBoxQuestionTemplate or RSVPCheckBoxListQuestionTemplate. */ + questions: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + /** Selects the widget; unknown values render nothing. */ + class_name: PropTypes.string.isRequired, + name: PropTypes.string, + /** Injected as raw HTML. */ + label: PropTypes.string, + is_mandatory: PropTypes.bool, + values: PropTypes.array + })).isRequired, + /** Receives the collected answers array on submit. */ + onSubmit: PropTypes.func.isRequired, + /** Keyed by question id. Read once into state at mount. */ + errors: PropTypes.object +}; export default RsvpForm; diff --git a/src/components/forms/simple-form.js b/src/components/forms/simple-form.js index 779bcbbf..6df2b900 100644 --- a/src/components/forms/simple-form.js +++ b/src/components/forms/simple-form.js @@ -12,6 +12,7 @@ **/ import React from 'react' +import PropTypes from 'prop-types'; import T from 'i18n-react/dist/i18n-react' import 'awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css' import Input from '../inputs/text-input' @@ -147,4 +148,20 @@ class SimpleForm extends React.Component { } } +SimpleForm.propTypes = { + /** Field descriptors rendered in order. */ + fields: PropTypes.arrayOf(PropTypes.shape({ + /** Matches a key on entity. */ + name: PropTypes.string.isRequired, + /** One of 'text', 'textarea', 'checkbox'. */ + type: PropTypes.string.isRequired, + label: PropTypes.node + })).isRequired, + /** Seeds the form. Copied into local state and re-synced when it changes. */ + entity: PropTypes.object.isRequired, + /** Keyed by field name. */ + errors: PropTypes.object, + /** Receives the edited entity. */ + onSubmit: PropTypes.func.isRequired +}; export default SimpleForm; diff --git a/src/components/inputs/access-levels-input.js b/src/components/inputs/access-levels-input.js index 8ae84b61..cc741f1e 100644 --- a/src/components/inputs/access-levels-input.js +++ b/src/components/inputs/access-levels-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import {queryAccessLevels} from '../../utils/query-actions'; @@ -92,3 +93,23 @@ export default class AccessLevelsInput extends React.Component { } } +AccessLevelsInput.propTypes = { + /** Selected access level(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Scopes the lookup. Required for results to return. */ + summitId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** Shown before the user types. */ + defaultOptions: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), + /** (item) => value. Defaults to item.id. */ + getOptionValue: PropTypes.func, + /** (item) => label. */ + getOptionLabel: PropTypes.func +}; diff --git a/src/components/inputs/action-dropdown/index.js b/src/components/inputs/action-dropdown/index.js index 848b6364..dec3662c 100644 --- a/src/components/inputs/action-dropdown/index.js +++ b/src/components/inputs/action-dropdown/index.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import './action-dropdown.less'; import Select from 'react-select'; @@ -66,3 +67,19 @@ export default class ActionDropdown extends React.Component { } } + +ActionDropdown.propTypes = { + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.any.isRequired, + label: PropTypes.string.isRequired + })).isRequired, + /** Label on the trigger button next to the select. */ + actionLabel: PropTypes.node, + placeholder: PropTypes.string, + /** Fires only on button click, with the selected option's value. Throws if nothing is selected. */ + onClick: PropTypes.func.isRequired, + /** Seeds the initial selection only; later changes are held in local state. */ + value: PropTypes.any, + /** Gated on the prop being present, so small={false} still applies the small styling. */ + small: PropTypes.bool +}; diff --git a/src/components/inputs/attendee-input.js b/src/components/inputs/attendee-input.js index 67e2815a..2fa156a8 100644 --- a/src/components/inputs/attendee-input.js +++ b/src/components/inputs/attendee-input.js @@ -12,6 +12,7 @@ **/ import React, {useState} from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import {queryAttendees} from '../../utils/query-actions'; @@ -71,5 +72,25 @@ const AttendeeInput = ({id, value, summitId, error, multi, onChange, getOptionVa ); } +AttendeeInput.propTypes = { + /** Selected attendee(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Scopes the lookup. Required for results to return. */ + summitId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** Overrides the default queryAttendees lookup. */ + queryFunction: PropTypes.func, + /** (attendee) => value. Defaults to attendee.id. */ + getOptionValue: PropTypes.func, + /** (attendee) => label. */ + getOptionLabel: PropTypes.func +}; export default AttendeeInput; diff --git a/src/components/inputs/company-input.js b/src/components/inputs/company-input.js index c64766f7..57536642 100644 --- a/src/components/inputs/company-input.js +++ b/src/components/inputs/company-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import {queryCompanies} from '../../utils/query-actions'; import AsyncCreatableSelect from "react-select/lib/AsyncCreatable"; @@ -115,3 +116,26 @@ export default class CompanyInput extends React.Component { } } + +CompanyInput.propTypes = { + /** Selected company or companies, as { id, name }. */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Alias for multi; either being present enables multi-select. */ + isMulti: PropTypes.bool, + /** Presence turns this into a creatable select. */ + allowCreate: PropTypes.bool, + /** Called with the typed text when a new company is created. */ + onCreate: PropTypes.func, + /** Overrides the default queryCompanies lookup. */ + queryFunction: PropTypes.func, + /** Appended to the fetched option list. */ + extraOptions: PropTypes.array +}; diff --git a/src/components/inputs/country-dropdown.js b/src/components/inputs/country-dropdown.js index bbab5f92..5502482d 100644 --- a/src/components/inputs/country-dropdown.js +++ b/src/components/inputs/country-dropdown.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import Dropdown from './dropdown'; import {getCountryList} from '../../utils/query-actions'; @@ -67,3 +68,18 @@ export default class CountryDropdown extends React.Component { } } + +CountryDropdown.propTypes = { + /** Selected ISO country code. */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Fetches the country list on mount; renders empty without a reachable API. */ + placeholder: PropTypes.string +}; diff --git a/src/components/inputs/country-input.js b/src/components/inputs/country-input.js index e348f72e..3037bc43 100644 --- a/src/components/inputs/country-input.js +++ b/src/components/inputs/country-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import Select from 'react-select'; import {getCountryList} from '../../utils/query-actions'; @@ -90,3 +91,16 @@ export default class CountryInput extends React.Component { ); } } + +CountryInput.propTypes = { + /** Selected ISO country code(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, +}; diff --git a/src/components/inputs/datetimepicker/index.js b/src/components/inputs/datetimepicker/index.js index 4ee0e6e4..3079c6b1 100644 --- a/src/components/inputs/datetimepicker/index.js +++ b/src/components/inputs/datetimepicker/index.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import './datetimepicker.less'; import Datetime from 'react-datetime'; import moment from 'moment-timezone'; @@ -113,3 +114,22 @@ export default class DateTimePicker extends React.Component { ); } } + +DateTimePicker.propTypes = { + id: PropTypes.string.isRequired, + /** A moment instance in the given timezone. */ + value: PropTypes.object, + /** Receives a synthetic { target: { id, value, type } } carrying a moment. */ + onChange: PropTypes.func.isRequired, + /** IANA zone; the displayed value is converted into it. */ + timezone: PropTypes.string, + /** { date, time } moment format strings. Pass time: false for a date-only picker. */ + format: PropTypes.object, + /** Constrains selectable dates, e.g. { after, before }. */ + validation: PropTypes.object, + /** Forwarded to the underlying input. */ + inputProps: PropTypes.object, + disabled: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string +}; diff --git a/src/components/inputs/dropdown.js b/src/components/inputs/dropdown.js index 083d8880..a796dc5f 100644 --- a/src/components/inputs/dropdown.js +++ b/src/components/inputs/dropdown.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import Select from 'react-select'; export default class Dropdown extends React.Component { @@ -84,6 +85,32 @@ export default class Dropdown extends React.Component { } } +Dropdown.propTypes = { + /** Echoed back as ev.target.id on change. */ + id: PropTypes.string.isRequired, + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.any.isRequired, + /** Injected as raw HTML into the option label. */ + label: PropTypes.string.isRequired + })).isRequired, + /** An array of option values when isMulti, otherwise a single value or option object. */ + value: PropTypes.oneOfType([ + PropTypes.array, PropTypes.object, PropTypes.string, PropTypes.number + ]), + /** Receives a synthetic { target: { id, value, type: 'dropdown' } }. */ + onChange: PropTypes.func.isRequired, + isMulti: PropTypes.bool, + className: PropTypes.string, + /** Non-empty renders an .error-label and adds the error class. */ + error: PropTypes.string, + ariaLabelledBy: PropTypes.string, + /** Set true to keep className as-is instead of prefixing 'dropdown'. */ + overrideCSS: PropTypes.bool, + disabled: PropTypes.bool, + /** Gated on the prop being present, so clearable={false} still enables clearing. */ + clearable: PropTypes.bool +}; + Dropdown.defaultProps = { ariaLabelledBy : null, } diff --git a/src/components/inputs/editor-input/index.js b/src/components/inputs/editor-input/index.js index 5eca9155..b3512f72 100644 --- a/src/components/inputs/editor-input/index.js +++ b/src/components/inputs/editor-input/index.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import './editor-input.less'; @@ -128,3 +129,16 @@ export default class TextEditor extends React.Component { } } + +TextEditor.propTypes = { + id: PropTypes.string, + /** HTML string. Loaded into the editor and re-synced when it changes externally. */ + value: PropTypes.string, + /** Receives a synthetic { target: { id, value, type } } carrying an HTML string. */ + onChange: PropTypes.func.isRequired, + /** Shows a remaining-characters counter. */ + maxLength: PropTypes.number, + className: PropTypes.string, + /** Non-empty renders an .error-label. */ + error: PropTypes.string +}; diff --git a/src/components/inputs/event-input.js b/src/components/inputs/event-input.js index acf070c3..91c58bd6 100644 --- a/src/components/inputs/event-input.js +++ b/src/components/inputs/event-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import {queryEvents} from '../../utils/query-actions'; @@ -64,3 +65,19 @@ export default class EventInput extends React.Component { } } +EventInput.propTypes = { + /** Selected event(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Scopes the lookup; summit.id is what is actually read. */ + summit: PropTypes.shape({ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) }).isRequired, + /** Restricts results to published events. */ + onlyPublished: PropTypes.bool +}; diff --git a/src/components/inputs/free-multi-text-input.js b/src/components/inputs/free-multi-text-input.js index 4a779d1f..67bcd723 100644 --- a/src/components/inputs/free-multi-text-input.js +++ b/src/components/inputs/free-multi-text-input.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import CreatableSelect from 'react-select/lib/Creatable'; import T from 'i18n-react/dist/i18n-react'; @@ -76,3 +77,13 @@ export default class FreeMultiTextInput extends React.Component { ); } } + +FreeMultiTextInput.propTypes = { + id: PropTypes.string.isRequired, + /** Current tags as react-select options. */ + value: PropTypes.array, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Maximum number of entries; further input is rejected once reached. */ + limit: PropTypes.number +}; diff --git a/src/components/inputs/group-input.js b/src/components/inputs/group-input.js index d153d9d4..1d051ffc 100644 --- a/src/components/inputs/group-input.js +++ b/src/components/inputs/group-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import {queryGroups} from '../../utils/query-actions'; @@ -62,3 +63,15 @@ export default class GroupInput extends React.Component { } } +GroupInput.propTypes = { + /** Selected group(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, +}; diff --git a/src/components/inputs/grouped-dropdown/index.js b/src/components/inputs/grouped-dropdown/index.js index 63fd9e9f..d27ca2d6 100644 --- a/src/components/inputs/grouped-dropdown/index.js +++ b/src/components/inputs/grouped-dropdown/index.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import { OptionGroup } from './OptionGroup'; import './optiongroup.less'; @@ -70,4 +71,23 @@ export default class GroupedDropdown extends React.Component { ); } -} \ No newline at end of file +} + +GroupedDropdown.propTypes = { + id: PropTypes.string, + /** An entry with a nested `options` array renders as an optgroup; otherwise a plain option. */ + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.any, + label: PropTypes.string, + options: PropTypes.array + })).isRequired, + /** Native select value. Mirrored into local state and re-synced when the prop changes. */ + value: PropTypes.any, + /** Receives the raw DOM change event. */ + onChange: PropTypes.func.isRequired, + /** Rendered as a disabled first option. */ + placeholder: PropTypes.string, + className: PropTypes.string, + /** Non-empty renders an .error-label and adds the error class. */ + error: PropTypes.string +}; \ No newline at end of file diff --git a/src/components/inputs/language-input.js b/src/components/inputs/language-input.js index 368f1dac..fe4ad35f 100644 --- a/src/components/inputs/language-input.js +++ b/src/components/inputs/language-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import Select from 'react-select'; import {getLanguageList} from '../../utils/query-actions'; @@ -92,3 +93,18 @@ export default class LanguageInput extends React.Component { } } + +LanguageInput.propTypes = { + /** Selected language(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Presence switches option values from ISO code to numeric id. */ + shouldUseId: PropTypes.bool +}; diff --git a/src/components/inputs/member-input.js b/src/components/inputs/member-input.js index d0b9d04f..d2d47543 100644 --- a/src/components/inputs/member-input.js +++ b/src/components/inputs/member-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import {queryMembers} from '../../utils/query-actions'; @@ -89,3 +90,19 @@ export default class MemberInput extends React.Component { } } +MemberInput.propTypes = { + /** Selected member(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** (member) => value. Defaults to member.id. */ + getOptionValue: PropTypes.func, + /** (member) => label. Defaults to the member's name and email. */ + getOptionLabel: PropTypes.func +}; diff --git a/src/components/inputs/operator-input.js b/src/components/inputs/operator-input.js index 45841e5b..25e8cbbf 100644 --- a/src/components/inputs/operator-input.js +++ b/src/components/inputs/operator-input.js @@ -12,6 +12,7 @@ **/ import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; import Select from 'react-select'; const OperatorInput = ({ error, label, value, onChange, id, multi, isMulti, className, isDisabled, isClearable, options, selectStyles, customStyle, ...rest }) => { @@ -138,4 +139,30 @@ OperatorInput.defaultProps = { { value: '==', label: 'Equal' }, { value: 'between', label: 'Between' }, ], -}; \ No newline at end of file +}; + +OperatorInput.propTypes = { + /** Either a scalar like '>10', or a two-element array for the 'between' operator. */ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Operator choices; 'between' switches the control to two inputs. */ + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string, + label: PropTypes.string + })).isRequired, + label: PropTypes.node, + className: PropTypes.string, + isMulti: PropTypes.bool, + isDisabled: PropTypes.bool, + isClearable: PropTypes.bool, + /** Merged into the operator select's react-select styles. */ + selectStyles: PropTypes.object, + customStyle: PropTypes.object +}; diff --git a/src/components/inputs/organization-input.js b/src/components/inputs/organization-input.js index 3a4269ea..9440177d 100644 --- a/src/components/inputs/organization-input.js +++ b/src/components/inputs/organization-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import AsyncCreatableSelect from 'react-select/lib/AsyncCreatable'; import {queryOrganizations} from '../../utils/query-actions'; @@ -97,5 +98,17 @@ export default class OrganizationInput extends React.Component { } } - - +OrganizationInput.propTypes = { + /** Selected organization. */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Called with the typed text when a new organization is created. */ + onCreate: PropTypes.func +}; diff --git a/src/components/inputs/speaker-input.js b/src/components/inputs/speaker-input.js index 39480087..2d072350 100644 --- a/src/components/inputs/speaker-input.js +++ b/src/components/inputs/speaker-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import { components } from 'react-select/lib/components' import { querySpeakers } from '../../utils/query-actions'; @@ -104,3 +105,23 @@ export default class SpeakerInput extends React.Component { } } +SpeakerInput.propTypes = { + /** Selected speaker(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Scopes the lookup to a summit; omit to search all speakers. */ + summitId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** Router history, used to link out to a speaker. */ + history: PropTypes.object, + /** (speaker) => value. Defaults to speaker.id. */ + getOptionValue: PropTypes.func, + /** (speaker) => label. */ + getOptionLabel: PropTypes.func +}; diff --git a/src/components/inputs/sponsor-input.js b/src/components/inputs/sponsor-input.js index f7454619..a3a1def1 100644 --- a/src/components/inputs/sponsor-input.js +++ b/src/components/inputs/sponsor-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import { querySponsors } from '../../utils/query-actions'; @@ -58,4 +59,20 @@ const SponsorInput = ({ id, summitId, value, error, multi, onChange, queryFuncti ); } +SponsorInput.propTypes = { + /** Selected sponsor(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, + /** Scopes the lookup. Required for results to return. */ + summitId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** Overrides the default querySponsors lookup. */ + queryFunction: PropTypes.func +}; export default SponsorInput; diff --git a/src/components/inputs/sponsored-project-input.js b/src/components/inputs/sponsored-project-input.js index 24c8d95c..117d2d35 100644 --- a/src/components/inputs/sponsored-project-input.js +++ b/src/components/inputs/sponsored-project-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import { querySponsoredProjects } from '../../utils/query-actions'; @@ -69,3 +70,16 @@ export default class SponsoredProjectInput extends React.Component { } } + +SponsoredProjectInput.propTypes = { + /** Selected sponsored project(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, +}; diff --git a/src/components/inputs/stepped-select/index.jsx b/src/components/inputs/stepped-select/index.jsx index e3f7888a..0eba2098 100644 --- a/src/components/inputs/stepped-select/index.jsx +++ b/src/components/inputs/stepped-select/index.jsx @@ -1,7 +1,8 @@ import React from 'react'; +import PropTypes from 'prop-types'; import styles from './index.module.less'; -export default ({value, options, onChange, ...rest}) => { +const SteppedSelect = ({value, options, onChange, ...rest}) => { const currentOptionKey = options.findIndex(op => op.value === value); @@ -31,3 +32,17 @@ export default ({value, options, onChange, ...rest}) => { ); }; + +SteppedSelect.propTypes = { + /** Must match one option's value — an unmatched value throws while reading its label. */ + value: PropTypes.any.isRequired, + /** Order defines the step sequence; the +/- buttons move one position at a time. */ + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.any.isRequired, + label: PropTypes.node.isRequired + })).isRequired, + /** Receives the neighbouring option's value. Not called at either end of the list. */ + onChange: PropTypes.func.isRequired +}; + +export default SteppedSelect; diff --git a/src/components/inputs/summit-days-select.js b/src/components/inputs/summit-days-select.js index 6c913b8f..145af8b9 100644 --- a/src/components/inputs/summit-days-select.js +++ b/src/components/inputs/summit-days-select.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import Select from 'react-select'; const SummitDaysSelect = ({ days, currentValue, placeholder, onDayChanged }) => { @@ -33,4 +34,17 @@ const SummitDaysSelect = ({ days, currentValue, placeholder, onDayChanged }) => ); } +SummitDaysSelect.propTypes = { + /** Doubles as the option list, so each entry needs both value and label. */ + days: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + })).isRequired, + /** Matched against day.value. Anything unmatched shows the placeholder. */ + currentValue: PropTypes.string, + placeholder: PropTypes.string, + /** Receives the selected day value, or null when cleared. */ + onDayChanged: PropTypes.func.isRequired +}; + export default SummitDaysSelect; diff --git a/src/components/inputs/summit-input.js b/src/components/inputs/summit-input.js index 3b66be8f..dab5407b 100644 --- a/src/components/inputs/summit-input.js +++ b/src/components/inputs/summit-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; import {querySummits} from '../../utils/query-actions'; @@ -73,3 +74,15 @@ export default class SummitInput extends React.Component { } } +SummitInput.propTypes = { + /** Selected summit(s). */ + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Echoed back as ev.target.id on the synthetic change event. */ + id: PropTypes.string.isRequired, + /** Receives a synthetic { target: { id, value, type } }. */ + onChange: PropTypes.func.isRequired, + /** Gated on the prop being present, so multi={false} still enables multi-select. */ + multi: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string, +}; diff --git a/src/components/inputs/summit-venues-select.js b/src/components/inputs/summit-venues-select.js index 8c1b82c1..87289846 100644 --- a/src/components/inputs/summit-venues-select.js +++ b/src/components/inputs/summit-venues-select.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import Select from 'react-select'; const SummitVenuesSelect = ({venues, currentValue, placeholder, onVenueChanged, ...rest}) => { @@ -41,4 +42,24 @@ const SummitVenuesSelect = ({venues, currentValue, placeholder, onVenueChanged, ); } +SummitVenuesSelect.propTypes = { + /** Flat list of venues and their rooms; rooms are indented in the option list. */ + venues: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + name: PropTypes.string, + /** 'SummitVenue' renders as a venue; anything else renders as a room. */ + class_name: PropTypes.string + }).isRequired + })).isRequired, + /** Matched by id against venues[].value.id, not by identity. */ + currentValue: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + }), + placeholder: PropTypes.string, + /** Receives the selected location object, or null when cleared. */ + onVenueChanged: PropTypes.func.isRequired +}; + export default SummitVenuesSelect; diff --git a/src/components/inputs/text-input.js b/src/components/inputs/text-input.js index 281a31e1..15d47aa9 100644 --- a/src/components/inputs/text-input.js +++ b/src/components/inputs/text-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; export default class Input extends React.Component { @@ -56,6 +57,20 @@ export default class Input extends React.Component { } } +Input.propTypes = { + /** Applied as defaultValue — the input is uncontrolled and only re-synced when this prop changes. */ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** Receives the raw DOM change event. */ + onChange: PropTypes.func.isRequired, + /** Replaces the default 'form-control' class on the input. */ + className: PropTypes.string, + /** Wrapper class; defaults to 'container-form-control'. */ + containerClassName: PropTypes.string, + /** Non-empty renders an .error-label and adds the error class. */ + error: PropTypes.string, + ariaLabelledBy: PropTypes.string +}; + Input.defaultProps = { ariaLabelledBy : null, } \ No newline at end of file diff --git a/src/components/inputs/textarea-input.js b/src/components/inputs/textarea-input.js index 49909d8c..bde8d470 100644 --- a/src/components/inputs/textarea-input.js +++ b/src/components/inputs/textarea-input.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; const TextArea = ({ onChange, value, className, error, maxLength, ...rest }) => { const has_error = error && error !== ''; @@ -47,6 +48,19 @@ const TextArea = ({ onChange, value, className, error, maxLength, ...rest }) => ); } +TextArea.propTypes = { + /** Textarea contents. Its length drives the character counter. */ + value: PropTypes.string, + /** Receives the raw change event. Suppressed once maxLength is reached, except on delete. */ + onChange: PropTypes.func.isRequired, + /** Replaces the default 'form-control' class. */ + className: PropTypes.string, + /** Non-empty renders an .error-label and marks the field. */ + error: PropTypes.string, + /** Omit to hide the "characters left" counter. */ + maxLength: PropTypes.number +}; + TextArea.defaultProps = { value: "" }; diff --git a/src/components/inputs/upload-input-v2/index.js b/src/components/inputs/upload-input-v2/index.js index dc2c6b19..f7c46366 100644 --- a/src/components/inputs/upload-input-v2/index.js +++ b/src/components/inputs/upload-input-v2/index.js @@ -12,6 +12,7 @@ **/ import React from 'react' +import PropTypes from 'prop-types'; import DropzoneJS from '../dropzone' import './index.less'; import file_icon from '../upload-input/file.png'; @@ -177,3 +178,34 @@ export default class UploadInputV2 extends React.Component { ); } } + +UploadInputV2.propTypes = { + id: PropTypes.string, + /** Already-uploaded files. */ + value: PropTypes.array, + /** Endpoint the dropzone POSTs to. */ + postUrl: PropTypes.string, + /** Drives allowed extensions and max size unless the getters below override them. */ + mediaType: PropTypes.shape({ + max_size: PropTypes.number, + type: PropTypes.shape({ allowed_extensions: PropTypes.array }) + }), + /** Upload is blocked once value reaches this count. */ + maxFiles: PropTypes.number, + canAdd: PropTypes.bool, + onRemove: PropTypes.func, + onUploadComplete: PropTypes.func, + onError: PropTypes.func, + /** Extra Dropzone config, merged last. */ + djsConfig: PropTypes.object, + timeOut: PropTypes.number, + parallelChunkUploads: PropTypes.bool, + maxConcurrentChunks: PropTypes.number, + /** Returns a comma-separated extension list, overriding mediaType. */ + getAllowedExtensions: PropTypes.func, + /** Returns max size in MB, overriding mediaType. */ + getMaxSize: PropTypes.func, + canDelete: PropTypes.bool, + /** Non-empty renders an .error-label. */ + error: PropTypes.string +}; diff --git a/src/components/inputs/upload-input-v3/index.js b/src/components/inputs/upload-input-v3/index.js index 46a7766c..90dba55c 100644 --- a/src/components/inputs/upload-input-v3/index.js +++ b/src/components/inputs/upload-input-v3/index.js @@ -12,6 +12,7 @@ **/ import React, { useState, useRef, useMemo, useCallback, useLayoutEffect, useEffect } from 'react'; +import PropTypes from 'prop-types'; import T from "i18n-react/dist/i18n-react"; import { Box, @@ -501,4 +502,38 @@ const UploadInputV3 = ({ ); }; +UploadInputV3.propTypes = { + id: PropTypes.string, + /** Already-uploaded files. */ + value: PropTypes.array, + /** Endpoint the dropzone POSTs to. */ + postUrl: PropTypes.string, + /** Drives allowed extensions and max size unless the getters below override them. */ + mediaType: PropTypes.shape({ + max_size: PropTypes.number, + type: PropTypes.shape({ allowed_extensions: PropTypes.array }) + }), + /** Upload is blocked once value reaches this count. */ + maxFiles: PropTypes.number, + canAdd: PropTypes.bool, + onRemove: PropTypes.func, + onUploadComplete: PropTypes.func, + onError: PropTypes.func, + /** Extra Dropzone config, merged last. */ + djsConfig: PropTypes.object, + timeOut: PropTypes.number, + parallelChunkUploads: PropTypes.bool, + maxConcurrentChunks: PropTypes.number, + /** Returns a comma-separated extension list, overriding mediaType. */ + getAllowedExtensions: PropTypes.func, + /** Returns max size in MB, overriding mediaType. */ + getMaxSize: PropTypes.func, + canDelete: PropTypes.bool, + /** Fired when an upload begins. */ + onUploadStart: PropTypes.func, + label: PropTypes.node, + helpText: PropTypes.node, + /** Non-empty renders an .error-label. */ + error: PropTypes.string +}; export default UploadInputV3; diff --git a/src/components/inputs/upload-input/index.js b/src/components/inputs/upload-input/index.js index c552952a..5cd9eb5e 100644 --- a/src/components/inputs/upload-input/index.js +++ b/src/components/inputs/upload-input/index.js @@ -12,6 +12,7 @@ **/ import React, {useEffect, useState} from 'react'; +import PropTypes from 'prop-types'; import Dropzone from 'react-dropzone'; import T from 'i18n-react/dist/i18n-react'; import './upload.less'; @@ -120,4 +121,16 @@ const UploadInput = ({value, error, handleRemove, handleUpload, handleError, ... ) } +UploadInput.propTypes = { + /** Existing file URL. An image renders as a preview, anything else as a file icon. */ + value: PropTypes.string, + /** Called with the accepted file(s). */ + handleUpload: PropTypes.func, + /** Called when the existing file is cleared. */ + handleRemove: PropTypes.func, + /** Called with rejected files, e.g. wrong type or too large. */ + handleError: PropTypes.func, + /** Non-empty renders an .error-label. */ + error: PropTypes.string +}; export default UploadInput; diff --git a/src/components/raw-html/index.js b/src/components/raw-html/index.js index 1cb2fa15..1d787696 100644 --- a/src/components/raw-html/index.js +++ b/src/components/raw-html/index.js @@ -1,7 +1,16 @@ import React from 'react'; +import PropTypes from 'prop-types'; const RawHTML = ({children, replaceNewLine = false, className = "", ...rest}) => ') : children}} {...rest}/> +RawHTML.propTypes = { + /** HTML string, injected unescaped via dangerouslySetInnerHTML. Never pass untrusted input. */ + children: PropTypes.string, + /** Converts newlines to
before injecting. */ + replaceNewLine: PropTypes.bool, + className: PropTypes.string +}; + export default RawHTML; \ No newline at end of file diff --git a/src/components/schedule-builder-view/index.js b/src/components/schedule-builder-view/index.js index 89875f08..284100a5 100644 --- a/src/components/schedule-builder-view/index.js +++ b/src/components/schedule-builder-view/index.js @@ -1,4 +1,5 @@ import React, {useEffect, useMemo} from 'react'; +import PropTypes from 'prop-types'; import SummitDaysSelect from "../inputs/summit-days-select"; import SummitVenuesSelect from "../inputs/summit-venues-select"; import SteppedSelect from "../inputs/stepped-select/index.jsx"; @@ -176,4 +177,42 @@ const ScheduleBuilderView = ({ ); } +ScheduleBuilderView.propTypes = { + /** Provides the day range, locations and timezone for the grid. */ + summit: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + start_date: PropTypes.number.isRequired, + end_date: PropTypes.number.isRequired, + time_zone_id: PropTypes.string, + /** Required: schedule-event-list reads time_zone.name. */ + time_zone: PropTypes.shape({ name: PropTypes.string }), + locations: PropTypes.array.isRequired + }).isRequired, + /** Restricts selectable days and venues per track. Null allows everything. */ + trackSpaceTime: PropTypes.array, + /** Events placed on the grid. */ + scheduleEvents: PropTypes.array.isRequired, + selectedEvents: PropTypes.array, + /** YYYY-MM-DD. Reset via onDayChanged when the venue no longer allows it. */ + currentDay: PropTypes.string, + /** The location object, matched by id. */ + currentVenue: PropTypes.object, + /** Slot height in minutes. */ + slotSize: PropTypes.number, + hideBulkSelect: PropTypes.bool, + allowResize: PropTypes.bool, + allowDrag: PropTypes.bool, + /** Renders the print button when provided. */ + showPrint: PropTypes.bool, + onDayChanged: PropTypes.func, + onVenueChanged: PropTypes.func, + onSlotSizeChange: PropTypes.func, + onScheduleEvent: PropTypes.func, + onUnPublishEvent: PropTypes.func, + onEditEvent: PropTypes.func, + onClickSelected: PropTypes.func, + onMoveSingleEvent: PropTypes.func, + onSelectAll: PropTypes.func, + onSelectedBulkAction: PropTypes.func +}; export default ScheduleBuilderView; diff --git a/src/components/sections/panel.js b/src/components/sections/panel.js index d30c8818..205fea6e 100644 --- a/src/components/sections/panel.js +++ b/src/components/sections/panel.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; export default class Panel extends React.Component { @@ -44,4 +45,16 @@ export default class Panel extends React.Component { ); } -} \ No newline at end of file +} + +Panel.propTypes = { + /** Heading text. Also seeds the fallback DOM id when `id` is omitted. */ + title: PropTypes.node, + /** Body is only mounted while true; the heading stays visible either way. */ + show: PropTypes.bool, + /** Click handler on the heading. The component holds no open/closed state itself. */ + handleClick: PropTypes.func, + children: PropTypes.node, + className: PropTypes.string, + id: PropTypes.string +}; \ No newline at end of file diff --git a/src/components/summit-dropdown/index.js b/src/components/summit-dropdown/index.js index 7a2a4f44..775a93a6 100644 --- a/src/components/summit-dropdown/index.js +++ b/src/components/summit-dropdown/index.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import './summit-dropdown.less'; import Select from 'react-select'; import T from 'i18n-react/dist/i18n-react'; @@ -73,3 +74,18 @@ export default class SummitDropdown extends React.Component { } } + +SummitDropdown.propTypes = { + /** Sorted by start_date descending before display. */ + summits: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + name: PropTypes.string.isRequired, + start_date: PropTypes.number + })).isRequired, + /** Fires with the selected summit id. The button stays disabled until a choice is made. */ + onClick: PropTypes.func.isRequired, + actionLabel: PropTypes.node, + actionClass: PropTypes.string, + /** Gated on presence, so big={false} still applies the large styling. */ + big: PropTypes.bool +}; diff --git a/src/components/table-editable/EditableTable.js b/src/components/table-editable/EditableTable.js index 11af19ae..e25c4b01 100644 --- a/src/components/table-editable/EditableTable.js +++ b/src/components/table-editable/EditableTable.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import EditableTableHeading from './EditableTableHeading'; import EditableTableCell from './EditableTableCell'; import EditableActionsTableCell from './EditableActionsTableCell'; @@ -237,3 +238,27 @@ export default class EditableTable extends React.Component { ); } }; + +EditableTable.propTypes = { + /** Rows are copied into local state; each needs an `id`. Re-synced when this prop changes. */ + data: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.shape({ + /** Key used to read and write the cell on each row. */ + columnKey: PropTypes.string.isRequired, + value: PropTypes.node, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + })).isRequired, + /** Renders textareas instead of inputs. Gated on presence, so textArea={false} still enables it. */ + textArea: PropTypes.bool, + options: PropTypes.shape({ + className: PropTypes.string, + /** Skips the sweetalert confirmation on delete. Gated on presence. */ + noAlert: PropTypes.bool, + actions: PropTypes.shape({ + /** Called with the whole edited row once the user commits it. */ + save: PropTypes.shape({ onClick: PropTypes.func.isRequired }), + /** Called with the row id, after confirmation unless noAlert is set. */ + delete: PropTypes.shape({ onClick: PropTypes.func.isRequired }) + }) + }).isRequired +}; diff --git a/src/components/table-selectable/SelectableTable.js b/src/components/table-selectable/SelectableTable.js index 0f727fe7..cb13a5cd 100644 --- a/src/components/table-selectable/SelectableTable.js +++ b/src/components/table-selectable/SelectableTable.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import SelectableTableHeading from './SelectableTableHeading'; import SelectableTableCell from './SelectableTableCell'; import SelectableTableRow from './SelectableTableRow'; @@ -182,4 +183,50 @@ class SelectableTable extends React.Component { } } +SelectableTable.propTypes = { + columns: PropTypes.arrayOf(PropTypes.shape({ + columnKey: PropTypes.string.isRequired, + value: PropTypes.node, + sortable: PropTypes.bool, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** (row, cellValue) => node. Overrides default cell rendering. */ + render: PropTypes.func + })).isRequired, + /** Each row's own `checked` flag drives its checkbox; selection state lives with the caller. */ + data: PropTypes.arrayOf(PropTypes.object).isRequired, + options: PropTypes.shape({ + className: PropTypes.string, + /** Hides the header select-all checkbox. */ + disableSelectAll: PropTypes.bool, + /** Controlled checked state of the select-all checkbox. */ + selectedAll: PropTypes.bool, + sortCol: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + sortDir: PropTypes.number, + sortFunc: PropTypes.func, + actionsHeader: PropTypes.node, + actions: PropTypes.shape({ + edit: PropTypes.shape({ + /** Row click. Checkbox clicks are excluded. */ + onClick: PropTypes.func, + /** (id, checked) for a single row. */ + onSelected: PropTypes.func, + /** Change handler for the header select-all checkbox. */ + onSelectedAll: PropTypes.func, + display: PropTypes.func + }), + delete: PropTypes.shape({ + onClick: PropTypes.func.isRequired, + display: PropTypes.func + }), + custom: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + icon: PropTypes.node, + tooltip: PropTypes.string, + onClick: PropTypes.func.isRequired, + display: PropTypes.func + })) + }) + }).isRequired +}; + export default SelectableTable; diff --git a/src/components/table/Table.js b/src/components/table/Table.js index 50ae9cf3..6677ad86 100644 --- a/src/components/table/Table.js +++ b/src/components/table/Table.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import TableHeading from './TableHeading'; import TableCell from './TableCell'; import TableRow from './TableRow'; @@ -114,4 +115,51 @@ const Table = (props) => { ); }; +const actionShape = PropTypes.shape({ + onClick: PropTypes.func.isRequired, + /** (id) => bool — return false to hide this action for a given row. */ + display: PropTypes.func +}); + +Table.propTypes = { + columns: PropTypes.arrayOf(PropTypes.shape({ + /** Key used to read the cell out of each row: row[columnKey]. */ + columnKey: PropTypes.string.isRequired, + /** Heading content. */ + value: PropTypes.node, + sortable: PropTypes.bool, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** Presence adds a title attribute to the cell, set to the raw value. */ + title: PropTypes.any, + /** (row, cellValue) => node. Overrides default cell rendering. */ + render: PropTypes.func, + styles: PropTypes.object + })).isRequired, + /** Row objects keyed by columnKey. Each needs an `id` when actions are used. */ + data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.object, PropTypes.array])).isRequired, + /** Called by sortable headings. */ + onSort: PropTypes.func, + options: PropTypes.shape({ + className: PropTypes.string, + /** Matched against either a column's columnKey or its numeric index. */ + sortCol: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + sortDir: PropTypes.number, + sortFunc: PropTypes.func, + /** Heading for the actions column. */ + actionsHeader: PropTypes.node, + actions: PropTypes.shape({ + /** Makes whole rows clickable and adds the table-hover class. */ + edit: actionShape, + delete: actionShape, + custom: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + icon: PropTypes.node, + tooltip: PropTypes.string, + onClick: PropTypes.func.isRequired, + display: PropTypes.func + })) + }) + }).isRequired +}; + export default Table; diff --git a/src/components/video-stream.js b/src/components/video-stream.js index 4d4205ef..aa0c4ab2 100644 --- a/src/components/video-stream.js +++ b/src/components/video-stream.js @@ -12,6 +12,7 @@ **/ import React from 'react'; +import PropTypes from 'prop-types'; import videojs from 'video.js' import 'video.js/dist/video-js.css' @@ -82,4 +83,9 @@ const VideoStream = ({ url }) => { return layout; }; +VideoStream.propTypes = { + /** A .m3u8 URL plays through video.js as a live stream; anything else is embedded in an iframe. Omit to render the "No video URL Provided" placeholder. */ + url: PropTypes.string +}; + export default VideoStream; From 09a13abe24fe2bd001f27c5777d7a71a9eaa7ecc Mon Sep 17 00:00:00 2001 From: Casey Locker Date: Fri, 14 Aug 2026 10:10:34 -0500 Subject: [PATCH 2/4] docs(components): correct propTypes that contradicted component behaviour Several propTypes declarations described props as optional that the components in fact require, causing a crash on omission rather than a console warning. Two others described behaviour the component does not have (a multi/isMulti prop that is inert due to how it is consumed). Co-Authored-By: Claude --- src/components/forms/rsvp-form.js | 18 +++++++++++++----- src/components/inputs/attendee-input.js | 2 +- src/components/inputs/datetimepicker/index.js | 7 +++++-- src/components/inputs/operator-input.js | 4 ++-- src/components/inputs/organization-input.js | 11 ++++++++--- src/components/inputs/upload-input-v2/index.js | 4 ++-- src/components/inputs/upload-input/index.js | 4 ++-- src/components/schedule-builder-view/index.js | 8 ++++++-- 8 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/components/forms/rsvp-form.js b/src/components/forms/rsvp-form.js index 3372c687..faff1fdf 100644 --- a/src/components/forms/rsvp-form.js +++ b/src/components/forms/rsvp-form.js @@ -175,11 +175,19 @@ RsvpForm.propTypes = { /** Injected as raw HTML. */ label: PropTypes.string, is_mandatory: PropTypes.bool, - values: PropTypes.array + values: PropTypes.array, + /** RSVPLiteralContentQuestionTemplate: raw HTML content rendered via RawHTML. */ + value: PropTypes.string, + /** RSVPDropDownQuestionTemplate: when true, values are remapped to {value: id, label: value}. */ + is_country_selector: PropTypes.bool, + /** RSVPDropDownQuestionTemplate: passed through to the Dropdown as isMulti. */ + is_multiselect: PropTypes.bool, + /** RSVPDropDownQuestionTemplate: used as the Dropdown's placeholder. */ + empty_string: PropTypes.string })).isRequired, - /** Receives the collected answers array on submit. */ - onSubmit: PropTypes.func.isRequired, - /** Keyed by question id. Read once into state at mount. */ - errors: PropTypes.object + /** Receives the collected answers array on submit. Omit to render a display-only form with no submit button. */ + onSubmit: PropTypes.func, + /** Keyed by question id. Read into state at mount with no fallback; hasErrors() throws if omitted. */ + errors: PropTypes.object.isRequired }; export default RsvpForm; diff --git a/src/components/inputs/attendee-input.js b/src/components/inputs/attendee-input.js index 2fa156a8..0c16bec8 100644 --- a/src/components/inputs/attendee-input.js +++ b/src/components/inputs/attendee-input.js @@ -79,7 +79,7 @@ AttendeeInput.propTypes = { id: PropTypes.string.isRequired, /** Receives a synthetic { target: { id, value, type } }. */ onChange: PropTypes.func.isRequired, - /** Enables multi-select. */ + /** No effect on this component: it is destructured out of props so it never reaches ...rest, and isMulti is never computed or passed to AsyncSelect. Consumers still pass it; it does nothing here. */ multi: PropTypes.bool, /** Non-empty renders an .error-label. */ error: PropTypes.string, diff --git a/src/components/inputs/datetimepicker/index.js b/src/components/inputs/datetimepicker/index.js index 3079c6b1..5ab56c8c 100644 --- a/src/components/inputs/datetimepicker/index.js +++ b/src/components/inputs/datetimepicker/index.js @@ -123,8 +123,11 @@ DateTimePicker.propTypes = { onChange: PropTypes.func.isRequired, /** IANA zone; the displayed value is converted into it. */ timezone: PropTypes.string, - /** { date, time } moment format strings. Pass time: false for a date-only picker. */ - format: PropTypes.object, + /** { date, time } moment format strings. Pass time: false for a date-only picker. Dereferenced with no default; required. */ + format: PropTypes.shape({ + date: PropTypes.string.isRequired, + time: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]) + }).isRequired, /** Constrains selectable dates, e.g. { after, before }. */ validation: PropTypes.object, /** Forwarded to the underlying input. */ diff --git a/src/components/inputs/operator-input.js b/src/components/inputs/operator-input.js index 25e8cbbf..0c514513 100644 --- a/src/components/inputs/operator-input.js +++ b/src/components/inputs/operator-input.js @@ -152,11 +152,11 @@ OperatorInput.propTypes = { multi: PropTypes.bool, /** Non-empty renders an .error-label. */ error: PropTypes.string, - /** Operator choices; 'between' switches the control to two inputs. */ + /** Operator choices; 'between' switches the control to two inputs. Has a defaultProps fallback above. */ options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, label: PropTypes.string - })).isRequired, + })), label: PropTypes.node, className: PropTypes.string, isMulti: PropTypes.bool, diff --git a/src/components/inputs/organization-input.js b/src/components/inputs/organization-input.js index 9440177d..936ee37a 100644 --- a/src/components/inputs/organization-input.js +++ b/src/components/inputs/organization-input.js @@ -99,16 +99,21 @@ export default class OrganizationInput extends React.Component { } OrganizationInput.propTypes = { - /** Selected organization. */ - value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Selected organization, read as value.id / value.name. */ + value: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + name: PropTypes.string + }), /** Echoed back as ev.target.id on the synthetic change event. */ id: PropTypes.string.isRequired, /** Receives a synthetic { target: { id, value, type } }. */ onChange: PropTypes.func.isRequired, - /** Gated on the prop being present, so multi={false} still enables multi-select. */ + /** No effect on this component: it flows through ...rest into AsyncSelect as multi, but react-select ^2.4.3 renamed that prop to isMulti. Consumers still pass it; it does nothing here. */ multi: PropTypes.bool, /** Non-empty renders an .error-label. */ error: PropTypes.string, + /** Presence-gated: switches to AsyncCreatableSelect regardless of value. */ + allowCreate: PropTypes.bool, /** Called with the typed text when a new organization is created. */ onCreate: PropTypes.func }; diff --git a/src/components/inputs/upload-input-v2/index.js b/src/components/inputs/upload-input-v2/index.js index f7c46366..2ee2dfb0 100644 --- a/src/components/inputs/upload-input-v2/index.js +++ b/src/components/inputs/upload-input-v2/index.js @@ -181,8 +181,8 @@ export default class UploadInputV2 extends React.Component { UploadInputV2.propTypes = { id: PropTypes.string, - /** Already-uploaded files. */ - value: PropTypes.array, + /** Already-uploaded files. Destructured with no default and read as value.length; required. */ + value: PropTypes.array.isRequired, /** Endpoint the dropzone POSTs to. */ postUrl: PropTypes.string, /** Drives allowed extensions and max size unless the getters below override them. */ diff --git a/src/components/inputs/upload-input/index.js b/src/components/inputs/upload-input/index.js index 5cd9eb5e..0b22a654 100644 --- a/src/components/inputs/upload-input/index.js +++ b/src/components/inputs/upload-input/index.js @@ -124,8 +124,8 @@ const UploadInput = ({value, error, handleRemove, handleUpload, handleError, ... UploadInput.propTypes = { /** Existing file URL. An image renders as a preview, anything else as a file icon. */ value: PropTypes.string, - /** Called with the accepted file(s). */ - handleUpload: PropTypes.func, + /** Called with the accepted file(s). Called unconditionally on drop, unlike handleError below; required. */ + handleUpload: PropTypes.func.isRequired, /** Called when the existing file is cleared. */ handleRemove: PropTypes.func, /** Called with rejected files, e.g. wrong type or too large. */ diff --git a/src/components/schedule-builder-view/index.js b/src/components/schedule-builder-view/index.js index 284100a5..7ef49b3c 100644 --- a/src/components/schedule-builder-view/index.js +++ b/src/components/schedule-builder-view/index.js @@ -185,7 +185,7 @@ ScheduleBuilderView.propTypes = { end_date: PropTypes.number.isRequired, time_zone_id: PropTypes.string, /** Required: schedule-event-list reads time_zone.name. */ - time_zone: PropTypes.shape({ name: PropTypes.string }), + time_zone: PropTypes.shape({ name: PropTypes.string.isRequired }).isRequired, locations: PropTypes.array.isRequired }).isRequired, /** Restricts selectable days and venues per track. Null allows everything. */ @@ -213,6 +213,10 @@ ScheduleBuilderView.propTypes = { onClickSelected: PropTypes.func, onMoveSingleEvent: PropTypes.func, onSelectAll: PropTypes.func, - onSelectedBulkAction: PropTypes.func + onSelectedBulkAction: PropTypes.func, + /** Overrides the default bulkOptions passed to BulkActionsSelector. */ + customBulkOptions: PropTypes.array, + /** Passed through to ScheduleEventList to gate drop targets. */ + canDropEvent: PropTypes.func }; export default ScheduleBuilderView; From 936698d41eff6ab4ce879043b05964dff3a5e865 Mon Sep 17 00:00:00 2001 From: Casey Locker Date: Fri, 14 Aug 2026 10:15:36 -0500 Subject: [PATCH 3/4] docs(components): correct the remaining inert multi-select prop docs operator-input.js destructures both multi and isMulti out of props, so neither reaches the underlying Select. multi's JSDoc claimed it enables multi-select and isMulti had no description; both now say plainly that they have no effect on this component. Co-Authored-By: Claude --- src/components/inputs/operator-input.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/inputs/operator-input.js b/src/components/inputs/operator-input.js index 0c514513..a14f1224 100644 --- a/src/components/inputs/operator-input.js +++ b/src/components/inputs/operator-input.js @@ -148,7 +148,7 @@ OperatorInput.propTypes = { id: PropTypes.string.isRequired, /** Receives a synthetic { target: { id, value, type } }. */ onChange: PropTypes.func.isRequired, - /** Enables multi-select. */ + /** No effect on this component: destructured out of props alongside isMulti, so neither is passed to the underlying Select. Consumers still pass it; it does nothing here. */ multi: PropTypes.bool, /** Non-empty renders an .error-label. */ error: PropTypes.string, @@ -159,6 +159,7 @@ OperatorInput.propTypes = { })), label: PropTypes.node, className: PropTypes.string, + /** No effect on this component: destructured out of props alongside multi, so neither is passed to the underlying Select. Consumers still pass it; it does nothing here. */ isMulti: PropTypes.bool, isDisabled: PropTypes.bool, isClearable: PropTypes.bool, From 1a63093bf80cfb427777c737e89f468e756fe812 Mon Sep 17 00:00:00 2001 From: Casey Locker Date: Fri, 14 Aug 2026 13:47:43 -0500 Subject: [PATCH 4/4] docs(components): correct propTypes flagged by review across the component set These declarations described props as optional that the components require, omitted props the components read, declared types the components cannot handle, and asserted behaviour some components do not have. Co-Authored-By: Claude --- src/components/forms/rsvp-form.js | 3 ++- src/components/forms/simple-form.js | 4 ++-- src/components/inputs/company-input.js | 15 ++++++++++++--- src/components/inputs/country-dropdown.js | 4 ++-- src/components/inputs/event-input.js | 2 +- src/components/inputs/free-multi-text-input.js | 4 ++-- src/components/inputs/group-input.js | 2 +- src/components/inputs/language-input.js | 2 +- src/components/inputs/member-input.js | 2 +- src/components/inputs/organization-input.js | 2 +- src/components/inputs/speaker-input.js | 2 +- src/components/inputs/sponsored-project-input.js | 2 ++ src/components/inputs/upload-input-v3/index.js | 2 +- src/components/inputs/upload-input/index.js | 4 ++-- src/components/schedule-builder-view/index.js | 3 ++- src/components/table-editable/EditableTable.js | 4 ++-- .../table-selectable/SelectableTable.js | 4 ++-- 17 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/components/forms/rsvp-form.js b/src/components/forms/rsvp-form.js index faff1fdf..cec65dd0 100644 --- a/src/components/forms/rsvp-form.js +++ b/src/components/forms/rsvp-form.js @@ -168,7 +168,8 @@ class RsvpForm extends React.Component { RsvpForm.propTypes = { /** Rendered by class_name, e.g. RSVPTextBoxQuestionTemplate or RSVPCheckBoxListQuestionTemplate. */ questions: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + /** Must be a number: handleChange compares answer.question_id to parseInt(id) with strict equality, which never matches a string id. */ + id: PropTypes.number.isRequired, /** Selects the widget; unknown values render nothing. */ class_name: PropTypes.string.isRequired, name: PropTypes.string, diff --git a/src/components/forms/simple-form.js b/src/components/forms/simple-form.js index 6df2b900..f7e9bb3f 100644 --- a/src/components/forms/simple-form.js +++ b/src/components/forms/simple-form.js @@ -153,8 +153,8 @@ SimpleForm.propTypes = { fields: PropTypes.arrayOf(PropTypes.shape({ /** Matches a key on entity. */ name: PropTypes.string.isRequired, - /** One of 'text', 'textarea', 'checkbox'. */ - type: PropTypes.string.isRequired, + /** Anything else renders nothing. */ + type: PropTypes.oneOf(['text', 'textarea', 'checkbox']).isRequired, label: PropTypes.node })).isRequired, /** Seeds the form. Copied into local state and re-synced when it changes. */ diff --git a/src/components/inputs/company-input.js b/src/components/inputs/company-input.js index 57536642..d5b14a8d 100644 --- a/src/components/inputs/company-input.js +++ b/src/components/inputs/company-input.js @@ -118,8 +118,17 @@ export default class CompanyInput extends React.Component { } CompanyInput.propTypes = { - /** Selected company or companies, as { id, name }. */ - value: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string, PropTypes.number]), + /** Selected company or companies, read as value.id / value.name (or value.map(...) in multi mode). */ + value: PropTypes.oneOfType([ + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + name: PropTypes.string + }), + PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + name: PropTypes.string + })) + ]), /** Echoed back as ev.target.id on the synthetic change event. */ id: PropTypes.string.isRequired, /** Receives a synthetic { target: { id, value, type } }. */ @@ -132,7 +141,7 @@ CompanyInput.propTypes = { isMulti: PropTypes.bool, /** Presence turns this into a creatable select. */ allowCreate: PropTypes.bool, - /** Called with the typed text when a new company is created. */ + /** Called with the typed text when a new company is created. Required whenever allowCreate is present: AsyncCreatableSelect's create action calls it unconditionally. */ onCreate: PropTypes.func, /** Overrides the default queryCompanies lookup. */ queryFunction: PropTypes.func, diff --git a/src/components/inputs/country-dropdown.js b/src/components/inputs/country-dropdown.js index 5502482d..9e43c93b 100644 --- a/src/components/inputs/country-dropdown.js +++ b/src/components/inputs/country-dropdown.js @@ -76,10 +76,10 @@ CountryDropdown.propTypes = { id: PropTypes.string.isRequired, /** Receives a synthetic { target: { id, value, type } }. */ onChange: PropTypes.func.isRequired, - /** Gated on the prop being present, so multi={false} still enables multi-select. */ + /** No effect: forwarded via {...this.props} to Dropdown, which reads isMulti, not multi. */ multi: PropTypes.bool, /** Non-empty renders an .error-label. */ error: PropTypes.string, - /** Fetches the country list on mount; renders empty without a reachable API. */ + /** Forwarded via {...this.props} to Dropdown, then to react-select's Select as its placeholder text. */ placeholder: PropTypes.string }; diff --git a/src/components/inputs/event-input.js b/src/components/inputs/event-input.js index 91c58bd6..f58aa712 100644 --- a/src/components/inputs/event-input.js +++ b/src/components/inputs/event-input.js @@ -74,7 +74,7 @@ EventInput.propTypes = { onChange: PropTypes.func.isRequired, /** Gated on the prop being present, so multi={false} still enables multi-select. */ multi: PropTypes.bool, - /** Non-empty renders an .error-label. */ + /** Not read by this component; not destructured, so it flows through ...rest into AsyncSelect, which does not render it. */ error: PropTypes.string, /** Scopes the lookup; summit.id is what is actually read. */ summit: PropTypes.shape({ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) }).isRequired, diff --git a/src/components/inputs/free-multi-text-input.js b/src/components/inputs/free-multi-text-input.js index 67bcd723..1f0f98ef 100644 --- a/src/components/inputs/free-multi-text-input.js +++ b/src/components/inputs/free-multi-text-input.js @@ -80,8 +80,8 @@ export default class FreeMultiTextInput extends React.Component { FreeMultiTextInput.propTypes = { id: PropTypes.string.isRequired, - /** Current tags as react-select options. */ - value: PropTypes.array, + /** Current tags as react-select options. Destructured with no default and spread on Enter/Tab; required. */ + value: PropTypes.array.isRequired, /** Receives a synthetic { target: { id, value, type } }. */ onChange: PropTypes.func.isRequired, /** Maximum number of entries; further input is rejected once reached. */ diff --git a/src/components/inputs/group-input.js b/src/components/inputs/group-input.js index 1d051ffc..6efa0493 100644 --- a/src/components/inputs/group-input.js +++ b/src/components/inputs/group-input.js @@ -72,6 +72,6 @@ GroupInput.propTypes = { onChange: PropTypes.func.isRequired, /** Gated on the prop being present, so multi={false} still enables multi-select. */ multi: PropTypes.bool, - /** Non-empty renders an .error-label. */ + /** Not read by this component; not destructured, so it flows through ...rest into AsyncSelect, which does not render it. */ error: PropTypes.string, }; diff --git a/src/components/inputs/language-input.js b/src/components/inputs/language-input.js index fe4ad35f..6dc6ae27 100644 --- a/src/components/inputs/language-input.js +++ b/src/components/inputs/language-input.js @@ -103,7 +103,7 @@ LanguageInput.propTypes = { onChange: PropTypes.func.isRequired, /** Gated on the prop being present, so multi={false} still enables multi-select. */ multi: PropTypes.bool, - /** Non-empty renders an .error-label. */ + /** Not read by this component; not destructured, so it flows through ...rest into Select, which does not render it. */ error: PropTypes.string, /** Presence switches option values from ISO code to numeric id. */ shouldUseId: PropTypes.bool diff --git a/src/components/inputs/member-input.js b/src/components/inputs/member-input.js index d2d47543..1b39f953 100644 --- a/src/components/inputs/member-input.js +++ b/src/components/inputs/member-input.js @@ -103,6 +103,6 @@ MemberInput.propTypes = { error: PropTypes.string, /** (member) => value. Defaults to member.id. */ getOptionValue: PropTypes.func, - /** (member) => label. Defaults to the member's name and email. */ + /** (member) => label. Defaults to the member's name and id: `${first_name} ${last_name} (${id})`. */ getOptionLabel: PropTypes.func }; diff --git a/src/components/inputs/organization-input.js b/src/components/inputs/organization-input.js index 936ee37a..00600a4e 100644 --- a/src/components/inputs/organization-input.js +++ b/src/components/inputs/organization-input.js @@ -114,6 +114,6 @@ OrganizationInput.propTypes = { error: PropTypes.string, /** Presence-gated: switches to AsyncCreatableSelect regardless of value. */ allowCreate: PropTypes.bool, - /** Called with the typed text when a new organization is created. */ + /** Called with the typed text when a new organization is created. Required whenever allowCreate is present: AsyncCreatableSelect's create action calls it unconditionally. */ onCreate: PropTypes.func }; diff --git a/src/components/inputs/speaker-input.js b/src/components/inputs/speaker-input.js index 2d072350..3c9a33bf 100644 --- a/src/components/inputs/speaker-input.js +++ b/src/components/inputs/speaker-input.js @@ -118,7 +118,7 @@ SpeakerInput.propTypes = { error: PropTypes.string, /** Scopes the lookup to a summit; omit to search all speakers. */ summitId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - /** Router history, used to link out to a speaker. */ + /** Router history, used to link out to a speaker. Required whenever multi is present: the multi-value label's click handler calls history.push with no guard. */ history: PropTypes.object, /** (speaker) => value. Defaults to speaker.id. */ getOptionValue: PropTypes.func, diff --git a/src/components/inputs/sponsored-project-input.js b/src/components/inputs/sponsored-project-input.js index 117d2d35..ed0b9170 100644 --- a/src/components/inputs/sponsored-project-input.js +++ b/src/components/inputs/sponsored-project-input.js @@ -82,4 +82,6 @@ SponsoredProjectInput.propTypes = { multi: PropTypes.bool, /** Non-empty renders an .error-label. */ error: PropTypes.string, + /** Presence-gated: sets isClearable regardless of value. */ + clearable: PropTypes.bool, }; diff --git a/src/components/inputs/upload-input-v3/index.js b/src/components/inputs/upload-input-v3/index.js index 90dba55c..6e361263 100644 --- a/src/components/inputs/upload-input-v3/index.js +++ b/src/components/inputs/upload-input-v3/index.js @@ -533,7 +533,7 @@ UploadInputV3.propTypes = { onUploadStart: PropTypes.func, label: PropTypes.node, helpText: PropTypes.node, - /** Non-empty renders an .error-label. */ + /** Non-empty renders an MUI Alert (severity="error"), not an .error-label. */ error: PropTypes.string }; export default UploadInputV3; diff --git a/src/components/inputs/upload-input/index.js b/src/components/inputs/upload-input/index.js index 0b22a654..597286c0 100644 --- a/src/components/inputs/upload-input/index.js +++ b/src/components/inputs/upload-input/index.js @@ -126,8 +126,8 @@ UploadInput.propTypes = { value: PropTypes.string, /** Called with the accepted file(s). Called unconditionally on drop, unlike handleError below; required. */ handleUpload: PropTypes.func.isRequired, - /** Called when the existing file is cleared. */ - handleRemove: PropTypes.func, + /** Called when the existing file is cleared. Called unconditionally on remove-icon click, with no guard; required. */ + handleRemove: PropTypes.func.isRequired, /** Called with rejected files, e.g. wrong type or too large. */ handleError: PropTypes.func, /** Non-empty renders an .error-label. */ diff --git a/src/components/schedule-builder-view/index.js b/src/components/schedule-builder-view/index.js index 7ef49b3c..0e7f332c 100644 --- a/src/components/schedule-builder-view/index.js +++ b/src/components/schedule-builder-view/index.js @@ -204,7 +204,8 @@ ScheduleBuilderView.propTypes = { allowDrag: PropTypes.bool, /** Renders the print button when provided. */ showPrint: PropTypes.bool, - onDayChanged: PropTypes.func, + /** Called unconditionally (no guard) when the venue change invalidates currentDay; required. */ + onDayChanged: PropTypes.func.isRequired, onVenueChanged: PropTypes.func, onSlotSizeChange: PropTypes.func, onScheduleEvent: PropTypes.func, diff --git a/src/components/table-editable/EditableTable.js b/src/components/table-editable/EditableTable.js index e25c4b01..39925b42 100644 --- a/src/components/table-editable/EditableTable.js +++ b/src/components/table-editable/EditableTable.js @@ -240,8 +240,8 @@ export default class EditableTable extends React.Component { }; EditableTable.propTypes = { - /** Rows are copied into local state; each needs an `id`. Re-synced when this prop changes. */ - data: PropTypes.arrayOf(PropTypes.object).isRequired, + /** Rows are copied into local state; each needs an `id`. Re-synced when this prop changes. `:220` explicitly handles array rows too. */ + data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.object, PropTypes.array])).isRequired, columns: PropTypes.arrayOf(PropTypes.shape({ /** Key used to read and write the cell on each row. */ columnKey: PropTypes.string.isRequired, diff --git a/src/components/table-selectable/SelectableTable.js b/src/components/table-selectable/SelectableTable.js index cb13a5cd..63d40cf7 100644 --- a/src/components/table-selectable/SelectableTable.js +++ b/src/components/table-selectable/SelectableTable.js @@ -192,8 +192,8 @@ SelectableTable.propTypes = { /** (row, cellValue) => node. Overrides default cell rendering. */ render: PropTypes.func })).isRequired, - /** Each row's own `checked` flag drives its checkbox; selection state lives with the caller. */ - data: PropTypes.arrayOf(PropTypes.object).isRequired, + /** Each row's own `checked` flag drives its checkbox; selection state lives with the caller. `:152` explicitly handles array rows too. */ + data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.object, PropTypes.array])).isRequired, options: PropTypes.shape({ className: PropTypes.string, /** Hides the header select-all checkbox. */