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..cec65dd0 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,30 @@ class RsvpForm extends React.Component { } } +RsvpForm.propTypes = { + /** Rendered by class_name, e.g. RSVPTextBoxQuestionTemplate or RSVPCheckBoxListQuestionTemplate. */ + questions: PropTypes.arrayOf(PropTypes.shape({ + /** 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, + /** Injected as raw HTML. */ + label: PropTypes.string, + is_mandatory: PropTypes.bool, + 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. 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/forms/simple-form.js b/src/components/forms/simple-form.js index 779bcbbf..f7e9bb3f 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, + /** 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. */ + 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..0c16bec8 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, + /** 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, + /** 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..d5b14a8d 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,35 @@ export default class CompanyInput extends React.Component { } } + +CompanyInput.propTypes = { + /** 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 } }. */ + 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. Required whenever allowCreate is present: AsyncCreatableSelect's create action calls it unconditionally. */ + 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..9e43c93b 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, + /** 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, + /** 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/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..5ab56c8c 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,25 @@ 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. 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. */ + 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..f58aa712 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, + /** 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, + /** 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..1f0f98ef 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. 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. */ + limit: PropTypes.number +}; diff --git a/src/components/inputs/group-input.js b/src/components/inputs/group-input.js index d153d9d4..6efa0493 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, + /** 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/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..6dc6ae27 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, + /** 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 d0b9d04f..1b39f953 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 id: `${first_name} ${last_name} (${id})`. */ + getOptionLabel: PropTypes.func +}; diff --git a/src/components/inputs/operator-input.js b/src/components/inputs/operator-input.js index 45841e5b..a14f1224 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,31 @@ 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, + /** 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, + /** 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 + })), + 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, + /** 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..00600a4e 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,22 @@ export default class OrganizationInput extends React.Component { } } - - +OrganizationInput.propTypes = { + /** 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, + /** 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. 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 39480087..3c9a33bf 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. 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, + /** (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..ed0b9170 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,18 @@ 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, + /** Presence-gated: sets isClearable regardless of value. */ + clearable: PropTypes.bool, +}; 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..2ee2dfb0 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. 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. */ + 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..6e361263 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 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 c552952a..597286c0 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). Called unconditionally on drop, unlike handleError below; required. */ + handleUpload: PropTypes.func.isRequired, + /** 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. */ + 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..0e7f332c 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,47 @@ 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.isRequired }).isRequired, + 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, + /** Called unconditionally (no guard) when the venue change invalidates currentDay; required. */ + onDayChanged: PropTypes.func.isRequired, + 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, + /** Overrides the default bulkOptions passed to BulkActionsSelector. */ + customBulkOptions: PropTypes.array, + /** Passed through to ScheduleEventList to gate drop targets. */ + canDropEvent: 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..39925b42 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. `: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, + 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..63d40cf7 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. `: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. */ + 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;