diff --git a/.gitignore b/.gitignore index c3abc985..5ed08681 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ package.json.lock docs package-lock.json .claude + +storybook-static diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 00000000..348be07d --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,26 @@ +const styleRule = (test, extraLoader, { modules = false, exclude } = {}) => ({ + test, + ...(exclude ? { exclude } : {}), + use: [ + "style-loader", + { loader: "css-loader", options: { modules, sourceMap: false } }, + ...(extraLoader ? [{ loader: extraLoader, options: { sourceMap: false } }] : []) + ] +}); + +module.exports = { + framework: "@storybook/react-webpack5", + // the webpack5 builder ships no JS compiler; without this addon nothing transpiles JSX + addons: ["@storybook/addon-docs", "@storybook/addon-webpack5-compiler-babel"], + stories: ["../stories/**/*.stories.@(js|jsx)"], + webpackFinal: (config) => { + // the builder ships plain css only; src imports less/scss in both module and global form + config.module.rules.push( + styleRule(/\.module\.less$/, "less-loader", { modules: true }), + styleRule(/\.module\.scss$/, "sass-loader", { modules: true }), + styleRule(/\.less$/, "less-loader", { exclude: /\.module\.less$/ }), + styleRule(/\.scss$/, "sass-loader", { exclude: /\.module\.scss$/ }) + ); + return config; + } +}; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 00000000..68ad7c91 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,10 @@ + + diff --git a/.storybook/preview.jsx b/.storybook/preview.jsx new file mode 100644 index 00000000..bf2ccbfc --- /dev/null +++ b/.storybook/preview.jsx @@ -0,0 +1,44 @@ +import { Provider } from "react-redux"; +import { + legacy_createStore as createStore, + combineReducers, + applyMiddleware +} from "redux"; +import thunk from "redux-thunk"; +import { ThemeProvider, createTheme } from "@mui/material"; +// Legacy (non-MUI) components render `fa fa-*` icons and bootstrap classes. The +// consuming apps supply both: summit-admin imports this same font-awesome css in +// src/index.js, and links bootstrap 3.3.7 from its index.ejs — mirrored for the +// preview iframe in .storybook/preview-head.html. +import "font-awesome/css/font-awesome.css"; +import "../src/i18n/i18n"; // side-effect: T.setTexts, else components render raw keys +import { genericReducers } from "../src/utils/reducers"; +import allFiltersReducer from "../src/components/mui/GridFilter/reducers/all-filters-reducer"; +import { MuiBaseCustomTheme } from "../src/components/mui/MuiBaseCustomTheme"; + +const theme = createTheme(MuiBaseCustomTheme); + +// GridFilter reads allGridFiltersState; SnackbarNotification reads baseState and +// dispatches clearSnackbarMessage, which is a thunk — hence the middleware. +export const store = createStore( + combineReducers({ + baseState: genericReducers, + allGridFiltersState: allFiltersReducer + }), + applyMiddleware(thunk) +); + +export const decorators = [ + (Story) => ( + + + + + + ) +]; + +export const parameters = { controls: { expanded: true } }; + +// gives every component a generated Docs page (props table + live controls) +export const tags = ["autodocs"]; diff --git a/package.json b/package.json index 0932c260..50a6363e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "clean": "rm -Rf node_modules & rm -Rf lib & yarn install", "build-dev": "./node_modules/.bin/webpack --config webpack.dev.js", "build": "./node_modules/.bin/webpack --config webpack.prod.js", - "test": "jest" + "test": "jest", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "license": "APACHE 2.0", "dependencies": { @@ -34,6 +36,9 @@ "@react-pdf/renderer": "^4.4.1", "@sentry/react": "^8.54.0", "@sentry/webpack-plugin": "^3.1.2", + "@storybook/addon-docs": "^10", + "@storybook/addon-webpack5-compiler-babel": "^4", + "@storybook/react-webpack5": "^10", "@stripe/react-stripe-js": "^5.4.1", "@stripe/stripe-js": "^8.5.3", "@testing-library/jest-dom": "5.17.0", @@ -99,6 +104,7 @@ "sass": "^1.77.0", "sass-loader": "^14.2.1", "spark-md5": "^3.0.2", + "storybook": "^10", "style-loader": "^3.3.1", "superagent": "8.0.9", "sweetalert2": "^8.15.2", 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; diff --git a/stories/AddonTypeSelect.stories.jsx b/stories/AddonTypeSelect.stories.jsx new file mode 100644 index 00000000..0e2a6eb1 --- /dev/null +++ b/stories/AddonTypeSelect.stories.jsx @@ -0,0 +1,13 @@ +import AddonTypeSelect from "../src/components/mui/addon-type-select"; +import { NEEDS_API } from "./_helpers"; + +export default { + title: "MUI/API-backed/AddonTypeSelect", + component: AddonTypeSelect, + argTypes: { onChange: { action: "changed" } }, + parameters: { docs: { description: { component: NEEDS_API } } } +}; + +// Options come from querySummitAddons and are keyed by add-on name, so `value` +// is a name rather than an id. No summit scoping — the query is global. +export const Default = { args: { value: "", placeholder: "Select an add-on" } }; diff --git a/stories/AlertButton.stories.jsx b/stories/AlertButton.stories.jsx new file mode 100644 index 00000000..e524100c --- /dev/null +++ b/stories/AlertButton.stories.jsx @@ -0,0 +1,10 @@ +import AlertButton from "../src/components/mui/AlertButton"; + +export default { + title: "MUI/Buttons/AlertButton", + component: AlertButton, + argTypes: { onClick: { action: "clicked" } } +}; + +export const Default = { args: { label: "Resolve conflict" } }; + diff --git a/stories/AlertModal.stories.jsx b/stories/AlertModal.stories.jsx new file mode 100644 index 00000000..9677f1c3 --- /dev/null +++ b/stories/AlertModal.stories.jsx @@ -0,0 +1,16 @@ +import AlertModal from "../src/components/mui/AlertModal"; + +export default { + title: "MUI/Dialogs/AlertModal", + component: AlertModal, + argTypes: { onClose: { action: "closed" } } +}; + +export const Default = { + args: { + open: true, + title: "Payment declined", + message: "The card ending 4242 was declined. Try another payment method." + } +}; + diff --git a/stories/AuthButton.stories.jsx b/stories/AuthButton.stories.jsx new file mode 100644 index 00000000..59fd9a49 --- /dev/null +++ b/stories/AuthButton.stories.jsx @@ -0,0 +1,17 @@ +import AuthButton from "../src/components/mui/AuthButton"; + +export default { + title: "MUI/Buttons/AuthButton", + component: AuthButton, + argTypes: { doLogin: { action: "login" }, initLogOut: { action: "logout" } } +}; + +export const LoggedOut = { args: { isLoggedUser: false } }; +export const LoggedIn = { + args: { + isLoggedUser: true, + profileName: "Casey Locker", + profileEmail: "casey@example.com" + } +}; + diff --git a/stories/BulkEditTable.stories.jsx b/stories/BulkEditTable.stories.jsx new file mode 100644 index 00000000..bef3f033 --- /dev/null +++ b/stories/BulkEditTable.stories.jsx @@ -0,0 +1,21 @@ +import BulkEditTable from "../src/components/mui/BulkEditTable"; +import { sampleRows } from "./_helpers"; + +export default { + title: "MUI/Tables/BulkEditTable", + component: BulkEditTable, + argTypes: { onSort: { action: "sort" }, onUpdate: { action: "update" } } +}; + +export const Default = { + args: { + options: { sortCol: "name", sortDir: 1 }, + columns: [ + { columnKey: "name", header: "Item", sortable: true }, + { columnKey: "quantity", header: "Qty", align: "right", editable: true }, + { columnKey: "price", header: "Price", align: "right", editable: true } + ], + data: sampleRows + } +}; + diff --git a/stories/CartButton.stories.jsx b/stories/CartButton.stories.jsx new file mode 100644 index 00000000..74800b68 --- /dev/null +++ b/stories/CartButton.stories.jsx @@ -0,0 +1,12 @@ +import CartButton from "../src/components/mui/CartButton"; + +export default { + title: "MUI/Buttons/CartButton", + component: CartButton, + argTypes: { onClick: { action: "clicked" } } +}; + +export const Empty = { args: { itemCount: 0 } }; +export const WithItems = { args: { itemCount: 3 } }; +export const Disabled = { args: { itemCount: 3, disabled: true } }; + diff --git a/stories/CheckboxList.stories.jsx b/stories/CheckboxList.stories.jsx new file mode 100644 index 00000000..9a23a13f --- /dev/null +++ b/stories/CheckboxList.stories.jsx @@ -0,0 +1,22 @@ +import CheckboxList from "../src/components/mui/checkbox-list"; + +export default { + title: "MUI/Inputs/CheckboxList", + component: CheckboxList, + argTypes: { onChange: { action: "changed" } } +}; + +export const Default = { + args: { + items: [ + { id: 1, name: "Keynote Hall", checked: true }, + { id: 2, name: "Breakout A", checked: false }, + { id: 3, name: "Breakout B", checked: false }, + { id: 4, name: "Expo Floor", checked: true } + ], + boxHeight: "220px" + } +}; + +export const Empty = { args: { items: [], boxHeight: "220px" } }; + diff --git a/stories/ChipList.stories.jsx b/stories/ChipList.stories.jsx new file mode 100644 index 00000000..d84a037d --- /dev/null +++ b/stories/ChipList.stories.jsx @@ -0,0 +1,9 @@ +import ChipList from "../src/components/mui/chip-list"; + +export default { title: "MUI/Data display/ChipList", component: ChipList }; + +const chips = ["Diamond", "Gold", "Silver", "Bronze", "In-kind", "Media"]; + +export const Default = { args: { chips } }; +export const Truncated = { args: { chips, maxLength: 3 } }; + diff --git a/stories/ChipNotify.stories.jsx b/stories/ChipNotify.stories.jsx new file mode 100644 index 00000000..069237d9 --- /dev/null +++ b/stories/ChipNotify.stories.jsx @@ -0,0 +1,8 @@ +import ChipNotify from "../src/components/mui/chip-notify"; + +export default { title: "MUI/Data display/ChipNotify", component: ChipNotify }; + +export const Warning = { args: { label: "3 items need review" } }; +export const Success = { args: { label: "All synced", color: "success" } }; +export const Error = { args: { label: "Payment failed", color: "error" } }; + diff --git a/stories/ChipSelectInput.stories.jsx b/stories/ChipSelectInput.stories.jsx new file mode 100644 index 00000000..c0638fcd --- /dev/null +++ b/stories/ChipSelectInput.stories.jsx @@ -0,0 +1,17 @@ +import ChipSelectInput from "../src/components/mui/chip-select-input"; + +export default { title: "MUI/Inputs/ChipSelectInput", component: ChipSelectInput }; + +export const Default = { + args: { + inputLabel: "Badge features", + availableOptions: [ + { id: 1, name: "Expo Access" }, + { id: 2, name: "Keynote Access" }, + { id: 3, name: "Workshop Access" } + ], + canAdd: true, + canEdit: true + } +}; + diff --git a/stories/ConfirmDeleteDialog.stories.jsx b/stories/ConfirmDeleteDialog.stories.jsx new file mode 100644 index 00000000..77342402 --- /dev/null +++ b/stories/ConfirmDeleteDialog.stories.jsx @@ -0,0 +1,12 @@ +import ConfirmDeleteDialog from "../src/components/mui/ConfirmDeleteDialog"; + +export default { + title: "MUI/Dialogs/ConfirmDeleteDialog", + component: ConfirmDeleteDialog, + argTypes: { onClose: { action: "closed" }, onConfirm: { action: "confirmed" } } +}; + +export const Default = { + args: { open: true, message: "Delete the Diamond sponsorship package?" } +}; + diff --git a/stories/ConfirmDialog.stories.jsx b/stories/ConfirmDialog.stories.jsx new file mode 100644 index 00000000..44e9db98 --- /dev/null +++ b/stories/ConfirmDialog.stories.jsx @@ -0,0 +1,25 @@ +import ConfirmDialog from "../src/components/mui/confirm-dialog"; + +export default { + title: "MUI/Dialogs/ConfirmDialog", + component: ConfirmDialog, + argTypes: { onConfirm: { action: "confirmed" }, onCancel: { action: "cancelled" } } +}; + +export const Default = { + args: { + open: true, + title: "Publish this order?", + text: "The sponsor will be emailed a copy of the invoice." + } +}; + +export const Warning = { + args: { + ...Default.args, + iconType: "warning", + confirmButtonText: "Publish anyway", + confirmButtonColor: "warning" + } +}; + diff --git a/stories/CustomAlert.stories.jsx b/stories/CustomAlert.stories.jsx new file mode 100644 index 00000000..559fb298 --- /dev/null +++ b/stories/CustomAlert.stories.jsx @@ -0,0 +1,9 @@ +import CustomAlert from "../src/components/mui/CustomAlert"; + +export default { title: "MUI/Feedback/CustomAlert", component: CustomAlert }; + +export const Info = { args: { severity: "info", message: "Rates refresh nightly." } }; +export const Warning = { args: { severity: "warning", message: "This show closes in 2 days." } }; +export const Error = { args: { severity: "error", message: "Could not reach the payment provider." } }; +export const NoIcon = { args: { severity: "info", message: "Compact variant.", hideIcon: true } }; + diff --git a/stories/CustomTablePagination.stories.jsx b/stories/CustomTablePagination.stories.jsx new file mode 100644 index 00000000..2999d364 --- /dev/null +++ b/stories/CustomTablePagination.stories.jsx @@ -0,0 +1,11 @@ +import CustomTablePagination from "../src/components/mui/table/CustomTablePagination"; + +export default { + title: "MUI/Tables/CustomTablePagination", + component: CustomTablePagination, + argTypes: { onPageChange: { action: "page-change" }, onPerPageChange: { action: "per-page-change" } } +}; + +export const Default = { args: { totalRows: 137, perPage: 10, currentPage: 3 } }; +export const SinglePage = { args: { totalRows: 4, perPage: 10, currentPage: 1 } }; + diff --git a/stories/DndList.stories.jsx b/stories/DndList.stories.jsx new file mode 100644 index 00000000..bbc5cfef --- /dev/null +++ b/stories/DndList.stories.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import DndList from "../src/components/mui/dnd-list"; + +export default { + title: "MUI/Drag and drop/DndList", + component: DndList, + argTypes: { onReorder: { action: "reorder" } }, + parameters: { + docs: { description: { component: "react-beautiful-dnd based (the older list). Prefer DragNDropList for new work." } } + } +}; + +export const Default = { + args: { + droppableId: "story-dnd-list", + items: [ + { id: 1, order: 1, name: "Keynote mention" }, + { id: 2, order: 2, name: "Booth 10x10" }, + { id: 3, order: 3, name: "Lanyard branding" } + ], + renderItem: (item) => {item.name} + } +}; + diff --git a/stories/DownloadBtn.stories.jsx b/stories/DownloadBtn.stories.jsx new file mode 100644 index 00000000..d312029a --- /dev/null +++ b/stories/DownloadBtn.stories.jsx @@ -0,0 +1,6 @@ +import DownloadBtn from "../src/components/mui/DownloadBtn"; + +export default { title: "MUI/Buttons/DownloadBtn", component: DownloadBtn }; + +export const Default = { args: { url: "https://example.com/invoice.pdf" } }; + diff --git a/stories/DragNDropList.stories.jsx b/stories/DragNDropList.stories.jsx new file mode 100644 index 00000000..3b733cea --- /dev/null +++ b/stories/DragNDropList.stories.jsx @@ -0,0 +1,23 @@ +import React from "react"; +import DragNDropList from "../src/components/mui/DragNDropList"; + +export default { + title: "MUI/Drag and drop/DragNDropList", + component: DragNDropList, + argTypes: { onReorder: { action: "reorder" } }, + parameters: { + docs: { description: { component: "dnd-kit based. Optional peer deps: @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities." } } + } +}; + +export const Default = { + args: { + items: [ + { id: 1, order: 1, name: "Keynote mention" }, + { id: 2, order: 2, name: "Booth 10x10" }, + { id: 3, order: 3, name: "Lanyard branding" } + ], + renderItem: (item) => {item.name} + } +}; + diff --git a/stories/Dropdown.stories.jsx b/stories/Dropdown.stories.jsx new file mode 100644 index 00000000..37a4017e --- /dev/null +++ b/stories/Dropdown.stories.jsx @@ -0,0 +1,22 @@ +import Dropdown from "../src/components/mui/Dropdown"; + +export default { + title: "MUI/Inputs/Dropdown", + component: Dropdown, + argTypes: { onChange: { action: "changed" } } +}; + +const options = [ + { value: "diamond", label: "Diamond" }, + { value: "gold", label: "Gold" }, + { value: "silver", label: "Silver" }, + { value: "legacy", label: "Legacy (retired)", disabled: true } +]; + +export const Default = { + args: { id: "tier", label: "Sponsorship tier", options, placeholder: "Select a tier" } +}; + +export const WithValue = { args: { ...Default.args, value: "gold" } }; +export const Multiple = { args: { ...Default.args, multiple: true, value: ["gold", "silver"] } }; + diff --git a/stories/DropdownCheckbox.stories.jsx b/stories/DropdownCheckbox.stories.jsx new file mode 100644 index 00000000..3b2349dc --- /dev/null +++ b/stories/DropdownCheckbox.stories.jsx @@ -0,0 +1,22 @@ +import DropdownCheckbox from "../src/components/mui/dropdown-checkbox"; + +export default { + title: "MUI/Inputs/DropdownCheckbox", + component: DropdownCheckbox, + argTypes: { onChange: { action: "changed" } } +}; + +export const Default = { + args: { + name: "tracks", + label: "Tracks", + allLabel: "All tracks", + value: ["ai"], + options: [ + { value: "ai", label: "AI / ML" }, + { value: "infra", label: "Infrastructure" }, + { value: "security", label: "Security" } + ] + } +}; + diff --git a/stories/EditableTable.stories.jsx b/stories/EditableTable.stories.jsx new file mode 100644 index 00000000..a9a7c663 --- /dev/null +++ b/stories/EditableTable.stories.jsx @@ -0,0 +1,20 @@ +import EditableTable from "../src/components/mui/editable-table/mui-table-editable"; +import { sampleRows } from "./_helpers"; + +export default { + title: "MUI/Tables/EditableTable", + component: EditableTable, + argTypes: { onSort: { action: "sort" }, onPageChange: { action: "page-change" } } +}; + +export const Default = { + args: { + columns: [ + { columnKey: "name", header: "Item", sortable: true }, + { columnKey: "quantity", header: "Qty", align: "right", editable: true }, + { columnKey: "price", header: "Price", align: "right", editable: true } + ], + data: sampleRows + } +}; + diff --git a/stories/ExtraRows.stories.jsx b/stories/ExtraRows.stories.jsx new file mode 100644 index 00000000..f0612650 --- /dev/null +++ b/stories/ExtraRows.stories.jsx @@ -0,0 +1,30 @@ +import React from "react"; +import { + TotalRow, + NotesRow, + FeeRow, + PaymentRow, + RefundRow, + DiscountRow +} from "../src/components/mui/table/extra-rows"; +import { inTable } from "./_helpers"; + +export default { + title: "MUI/Tables/Extra rows", + decorators: [inTable], + parameters: { + docs: { + description: { + component: "Summary rows appended to MuiTable. Each has to render inside a table body." + } + } + } +}; + +export const Total = { render: () => }; +export const Discount = { render: () => }; +export const Fee = { render: () => }; +export const Payment = { render: () => }; +export const Refund = { render: () => }; +export const Notes = { render: () => }; + diff --git a/stories/FormItemTable.stories.jsx b/stories/FormItemTable.stories.jsx new file mode 100644 index 00000000..bcd584aa --- /dev/null +++ b/stories/FormItemTable.stories.jsx @@ -0,0 +1,54 @@ +import React from "react"; +import { FormikProvider, useFormik } from "formik"; +import FormItemTable from "../src/components/mui/FormItemTable"; +import { MOCK_FORM } from "./_form-item-fixture"; + +// FormItemTable reads values/touched/errors off a live formik instance and +// needs discount_type/discount_amount seeded, so it takes the real useFormik +// host rather than the generic withFormik decorator. +const Host = (props) => { + const formik = useFormik({ + initialValues: { discount_type: "AMOUNT", discount_amount: 0 }, + onSubmit: () => {} + }); + return ( + + + + ); +}; + +export default { + title: "MUI/Forms/FormItemTable", + component: FormItemTable, + render: (args) => , + parameters: { + docs: { + description: { + component: + "Data fixture is lifted from the component's own test suite. Rows whose quantity is driven by a Form-level Quantity field default to expanded." + } + } + } +}; + +export const EarlyBird = { + args: { + data: MOCK_FORM.items, + currentApplicableRate: "early_bird", + timeZone: "America/New_York" + } +}; + +export const Standard = { + args: { ...EarlyBird.args, currentApplicableRate: "standard" } +}; + +export const Onsite = { + args: { ...EarlyBird.args, currentApplicableRate: "onsite" } +}; diff --git a/stories/FormikAdditionalInput.stories.jsx b/stories/FormikAdditionalInput.stories.jsx new file mode 100644 index 00000000..bb10e047 --- /dev/null +++ b/stories/FormikAdditionalInput.stories.jsx @@ -0,0 +1,25 @@ +import AdditionalInput from "../src/components/mui/formik-inputs/additional-input/additional-input"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/AdditionalInput", + component: AdditionalInput, + decorators: [ + withFormik({ + meta_fields: [ + { id: 1, name: "shirt_size", type: "ComboBox", values: [{ id: 1, value: "M" }] } + ] + }) + ], + argTypes: { onAdd: { action: "add" }, onDelete: { action: "delete" }, onDeleteValue: { action: "delete-value" } } +}; + +export const Default = { + args: { + baseName: "meta_fields", + itemIdx: 0, + entityId: 1, + item: { id: 1, name: "shirt_size", type: "ComboBox", values: [{ id: 1, value: "M" }] } + } +}; + diff --git a/stories/FormikAdditionalInputList.stories.jsx b/stories/FormikAdditionalInputList.stories.jsx new file mode 100644 index 00000000..9e8e5019 --- /dev/null +++ b/stories/FormikAdditionalInputList.stories.jsx @@ -0,0 +1,19 @@ +import AdditionalInputList from "../src/components/mui/formik-inputs/additional-input/additional-input-list"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/AdditionalInputList", + component: AdditionalInputList, + decorators: [ + withFormik({ + meta_fields: [ + { id: 1, name: "shirt_size", type: "ComboBox", values: [{ id: 1, value: "M" }] }, + { id: 2, name: "dietary", type: "Text", values: [] } + ] + }) + ], + argTypes: { onDelete: { action: "delete" }, onDeleteValue: { action: "delete-value" } } +}; + +export const Default = { args: { name: "meta_fields", entityId: 1 } }; + diff --git a/stories/FormikAddonTypeSelect.stories.jsx b/stories/FormikAddonTypeSelect.stories.jsx new file mode 100644 index 00000000..85653211 --- /dev/null +++ b/stories/FormikAddonTypeSelect.stories.jsx @@ -0,0 +1,11 @@ +import FormikAddonTypeSelect from "../src/components/mui/formik-inputs/mui-formik-addon-type-select"; +import { withFormik, NEEDS_API } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/AddonTypeSelect", + component: FormikAddonTypeSelect, + decorators: [withFormik({ addon: "" })], + parameters: { docs: { description: { component: NEEDS_API } } } +}; + +export const Default = { args: { name: "addon", placeholder: "Select an add-on" } }; diff --git a/stories/FormikAsyncSelect.stories.jsx b/stories/FormikAsyncSelect.stories.jsx new file mode 100644 index 00000000..9032a0ab --- /dev/null +++ b/stories/FormikAsyncSelect.stories.jsx @@ -0,0 +1,24 @@ +import FormikAsyncSelect from "../src/components/mui/formik-inputs/mui-formik-async-select"; +import { withFormik } from "./_helpers"; + +const queryFunction = (term) => + Promise.resolve( + [ + { id: 1, name: "Acme Corp" }, + { id: 2, name: "Globex" }, + { id: 3, name: "Initech" } + ].filter((c) => c.name.toLowerCase().includes((term || "").toLowerCase())) + ); + +export default { + title: "MUI/Formik inputs/AsyncSelect", + component: FormikAsyncSelect, + decorators: [withFormik({ company: null })], + parameters: { + docs: { description: { component: "queryFunction is stubbed here with a local promise; in an app it hits the API." } } + } +}; + +export const Default = { args: { name: "company", queryFunction, placeholder: "Search companies..." } }; +export const Multiple = { args: { ...Default.args, isMulti: true, multiple: true } }; + diff --git a/stories/FormikCheckbox.stories.jsx b/stories/FormikCheckbox.stories.jsx new file mode 100644 index 00000000..05655fc8 --- /dev/null +++ b/stories/FormikCheckbox.stories.jsx @@ -0,0 +1,11 @@ +import FormikCheckbox from "../src/components/mui/formik-inputs/mui-formik-checkbox"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/Checkbox", + component: FormikCheckbox, + decorators: [withFormik({ is_active: true })] +}; + +export const Default = { args: { name: "is_active", label: "Active" } }; + diff --git a/stories/FormikCheckboxGroup.stories.jsx b/stories/FormikCheckboxGroup.stories.jsx new file mode 100644 index 00000000..7b96db29 --- /dev/null +++ b/stories/FormikCheckboxGroup.stories.jsx @@ -0,0 +1,19 @@ +import FormikCheckboxGroup from "../src/components/mui/formik-inputs/mui-formik-checkbox-group"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/CheckboxGroup", + component: FormikCheckboxGroup, + decorators: [withFormik({ tracks: ["ai"] })] +}; + +export const Default = { args: { + name: "tracks", + label: "Tracks", + options: [ + { value: "ai", label: "AI / ML" }, + { value: "infra", label: "Infrastructure" }, + { value: "security", label: "Security" } + ] + } }; + diff --git a/stories/FormikCompanyInput.stories.jsx b/stories/FormikCompanyInput.stories.jsx new file mode 100644 index 00000000..f6277770 --- /dev/null +++ b/stories/FormikCompanyInput.stories.jsx @@ -0,0 +1,12 @@ +import FormikCompanyInput from "../src/components/mui/formik-inputs/company-input-mui"; +import { withFormik, NEEDS_API } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/CompanyInput", + component: FormikCompanyInput, + decorators: [withFormik({ company: null })], + parameters: { docs: { description: { component: NEEDS_API } } } +}; + +export const Default = { args: { id: "company", name: "company", placeholder: "Search companies...", allowCreate: true } }; + diff --git a/stories/FormikDatepicker.stories.jsx b/stories/FormikDatepicker.stories.jsx new file mode 100644 index 00000000..ca66809c --- /dev/null +++ b/stories/FormikDatepicker.stories.jsx @@ -0,0 +1,11 @@ +import FormikDatepicker from "../src/components/mui/formik-inputs/mui-formik-datepicker"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/Datepicker", + component: FormikDatepicker, + decorators: [withFormik({ starts_at: null })] +}; + +export const Default = { args: { name: "starts_at", label: "Starts at" } }; + diff --git a/stories/FormikDiscountField.stories.jsx b/stories/FormikDiscountField.stories.jsx new file mode 100644 index 00000000..2dd98359 --- /dev/null +++ b/stories/FormikDiscountField.stories.jsx @@ -0,0 +1,11 @@ +import FormikDiscountField from "../src/components/mui/formik-inputs/mui-formik-discountfield"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/DiscountField", + component: FormikDiscountField, + decorators: [withFormik({ discount: 10 })] +}; + +export const Default = { args: { name: "discount", label: "Discount", discountType: "percentage" } }; + diff --git a/stories/FormikDropdownCheckbox.stories.jsx b/stories/FormikDropdownCheckbox.stories.jsx new file mode 100644 index 00000000..067c9700 --- /dev/null +++ b/stories/FormikDropdownCheckbox.stories.jsx @@ -0,0 +1,19 @@ +import FormikDropdownCheckbox from "../src/components/mui/formik-inputs/mui-formik-dropdown-checkbox"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/DropdownCheckbox", + component: FormikDropdownCheckbox, + decorators: [withFormik({ tracks: ["ai"] })] +}; + +export const Default = { args: { + name: "tracks", + label: "Tracks", + placeholder: "Select tracks", + options: [ + { value: "ai", label: "AI / ML" }, + { value: "infra", label: "Infrastructure" } + ] + } }; + diff --git a/stories/FormikDropdownRadio.stories.jsx b/stories/FormikDropdownRadio.stories.jsx new file mode 100644 index 00000000..00b10312 --- /dev/null +++ b/stories/FormikDropdownRadio.stories.jsx @@ -0,0 +1,19 @@ +import FormikDropdownRadio from "../src/components/mui/formik-inputs/mui-formik-dropdown-radio"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/DropdownRadio", + component: FormikDropdownRadio, + decorators: [withFormik({ tier: "gold" })] +}; + +export const Default = { args: { + name: "tier", + label: "Tier", + placeholder: "Select a tier", + options: [ + { value: "gold", label: "Gold" }, + { value: "silver", label: "Silver" } + ] + } }; + diff --git a/stories/FormikFileSizeField.stories.jsx b/stories/FormikFileSizeField.stories.jsx new file mode 100644 index 00000000..d8f61e84 --- /dev/null +++ b/stories/FormikFileSizeField.stories.jsx @@ -0,0 +1,11 @@ +import FormikFileSizeField from "../src/components/mui/formik-inputs/mui-formik-file-size-field"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/FileSizeField", + component: FormikFileSizeField, + decorators: [withFormik({ max_size: 5242880 })] +}; + +export const Default = { args: { name: "max_size", label: "Max file size", displayUnit: "MB", valueUnit: "B" } }; + diff --git a/stories/FormikItemPriceTiers.stories.jsx b/stories/FormikItemPriceTiers.stories.jsx new file mode 100644 index 00000000..c3b62497 --- /dev/null +++ b/stories/FormikItemPriceTiers.stories.jsx @@ -0,0 +1,11 @@ +import FormikItemPriceTiers from "../src/components/mui/formik-inputs/item-price-tiers"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/ItemPriceTiers", + component: FormikItemPriceTiers, + decorators: [withFormik({ price_tiers: [{ id: 1, name: "Early bird", price: 7000 }] })] +}; + +export const Default = { args: { readOnly: false } }; + diff --git a/stories/FormikPriceField.stories.jsx b/stories/FormikPriceField.stories.jsx new file mode 100644 index 00000000..43149415 --- /dev/null +++ b/stories/FormikPriceField.stories.jsx @@ -0,0 +1,11 @@ +import FormikPriceField from "../src/components/mui/formik-inputs/mui-formik-pricefield"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/PriceField", + component: FormikPriceField, + decorators: [withFormik({ price: 8000 })] +}; + +export const Default = { args: { name: "price", label: "Price" } }; + diff --git a/stories/FormikQuantityField.stories.jsx b/stories/FormikQuantityField.stories.jsx new file mode 100644 index 00000000..d34e4e1e --- /dev/null +++ b/stories/FormikQuantityField.stories.jsx @@ -0,0 +1,11 @@ +import FormikQuantityField from "../src/components/mui/formik-inputs/mui-formik-quantity-field"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/QuantityField", + component: FormikQuantityField, + decorators: [withFormik({ quantity: 2 })] +}; + +export const Default = { args: { name: "quantity", label: "Quantity", min: 0, max: 99 } }; + diff --git a/stories/FormikRadioGroup.stories.jsx b/stories/FormikRadioGroup.stories.jsx new file mode 100644 index 00000000..c5ab6e46 --- /dev/null +++ b/stories/FormikRadioGroup.stories.jsx @@ -0,0 +1,18 @@ +import FormikRadioGroup from "../src/components/mui/formik-inputs/mui-formik-radio-group"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/RadioGroup", + component: FormikRadioGroup, + decorators: [withFormik({ visibility: "public" })] +}; + +export const Default = { args: { + name: "visibility", + label: "Visibility", + options: [ + { value: "public", label: "Public" }, + { value: "private", label: "Private" } + ] + } }; + diff --git a/stories/FormikSelect.stories.jsx b/stories/FormikSelect.stories.jsx new file mode 100644 index 00000000..5a863b89 --- /dev/null +++ b/stories/FormikSelect.stories.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import { MenuItem } from "@mui/material"; +import FormikSelect from "../src/components/mui/formik-inputs/mui-formik-select"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/Select", + component: FormikSelect, + decorators: [withFormik({ tier: "gold" })] +}; + +export const Default = { + args: { + name: "tier", + label: "Tier", + placeholder: "Select a tier", + isClearable: true, + children: [ + Diamond, + Gold, + Silver + ] + } +}; + diff --git a/stories/FormikSelectGroup.stories.jsx b/stories/FormikSelectGroup.stories.jsx new file mode 100644 index 00000000..ffc994c5 --- /dev/null +++ b/stories/FormikSelectGroup.stories.jsx @@ -0,0 +1,27 @@ +import FormikSelectGroup from "../src/components/mui/formik-inputs/mui-formik-select-group"; +import { withFormik } from "./_helpers"; + +const queryFunction = () => + Promise.resolve([ + { id: 1, name: "Keynote Hall", group_id: 1, group_name: "Main venue" }, + { id: 2, name: "Breakout A", group_id: 1, group_name: "Main venue" }, + { id: 3, name: "Expo Floor", group_id: 2, group_name: "Expo" } + ]); + +export default { + title: "MUI/Formik inputs/SelectGroup", + component: FormikSelectGroup, + decorators: [withFormik({ rooms: [] })] +}; + +export const Default = { + args: { + name: "rooms", + queryFunction, + placeholder: "Select rooms", + showSelectAll: true, + getGroupId: (i) => i.group_id, + getGroupLabel: (i) => i.group_name + } +}; + diff --git a/stories/FormikSelectV2.stories.jsx b/stories/FormikSelectV2.stories.jsx new file mode 100644 index 00000000..7b5be334 --- /dev/null +++ b/stories/FormikSelectV2.stories.jsx @@ -0,0 +1,20 @@ +import FormikSelectV2 from "../src/components/mui/formik-inputs/mui-formik-select-v2"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/Select v2", + component: FormikSelectV2, + decorators: [withFormik({ tier: "gold" })] +}; + +export const Default = { args: { + name: "tier", + label: "Tier", + placeholder: "Select a tier", + options: [ + { value: "diamond", label: "Diamond" }, + { value: "gold", label: "Gold" }, + { value: "silver", label: "Silver" } + ] + } }; + diff --git a/stories/FormikSponsorInput.stories.jsx b/stories/FormikSponsorInput.stories.jsx new file mode 100644 index 00000000..13a9df3e --- /dev/null +++ b/stories/FormikSponsorInput.stories.jsx @@ -0,0 +1,12 @@ +import FormikSponsorInput from "../src/components/mui/formik-inputs/mui-sponsor-input"; +import { withFormik, NEEDS_API } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/SponsorInput", + component: FormikSponsorInput, + decorators: [withFormik({ sponsor: null })], + parameters: { docs: { description: { component: NEEDS_API } } } +}; + +export const Default = { args: { id: "sponsor", name: "sponsor", summitId: 1, placeholder: "Search sponsors..." } }; + diff --git a/stories/FormikSponsorshipInput.stories.jsx b/stories/FormikSponsorshipInput.stories.jsx new file mode 100644 index 00000000..5343ca30 --- /dev/null +++ b/stories/FormikSponsorshipInput.stories.jsx @@ -0,0 +1,12 @@ +import FormikSponsorshipInput from "../src/components/mui/formik-inputs/sponsorship-input-mui"; +import { withFormik, NEEDS_API } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/SponsorshipInput", + component: FormikSponsorshipInput, + decorators: [withFormik({ sponsorship: null })], + parameters: { docs: { description: { component: NEEDS_API } } } +}; + +export const Default = { args: { id: "sponsorship", name: "sponsorship", placeholder: "Search sponsorships..." } }; + diff --git a/stories/FormikSponsorshipSummitSelect.stories.jsx b/stories/FormikSponsorshipSummitSelect.stories.jsx new file mode 100644 index 00000000..e756e7fb --- /dev/null +++ b/stories/FormikSponsorshipSummitSelect.stories.jsx @@ -0,0 +1,12 @@ +import FormikSponsorshipSummitSelect from "../src/components/mui/formik-inputs/sponsorship-summit-select-mui"; +import { withFormik, NEEDS_API } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/SponsorshipSummitSelect", + component: FormikSponsorshipSummitSelect, + decorators: [withFormik({ sponsorship: null })], + parameters: { docs: { description: { component: NEEDS_API } } } +}; + +export const Default = { args: { name: "sponsorship", summitId: 1, placeholder: "Select a sponsorship" } }; + diff --git a/stories/FormikSwitch.stories.jsx b/stories/FormikSwitch.stories.jsx new file mode 100644 index 00000000..1742ef55 --- /dev/null +++ b/stories/FormikSwitch.stories.jsx @@ -0,0 +1,11 @@ +import FormikSwitch from "../src/components/mui/formik-inputs/mui-formik-switch"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/Switch", + component: FormikSwitch, + decorators: [withFormik({ notify: false })] +}; + +export const Default = { args: { name: "notify", label: "Email the sponsor on change" } }; + diff --git a/stories/FormikTextEditor.stories.jsx b/stories/FormikTextEditor.stories.jsx new file mode 100644 index 00000000..5a9c257d --- /dev/null +++ b/stories/FormikTextEditor.stories.jsx @@ -0,0 +1,11 @@ +import FormikTextEditor from "../src/components/mui/formik-inputs/mui-formik-text-editor"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/TextEditor", + component: FormikTextEditor, + decorators: [withFormik({ description: "

Sponsor description

" })] +}; + +export const Default = { args: { name: "description", label: "Description" } }; + diff --git a/stories/FormikTextField.stories.jsx b/stories/FormikTextField.stories.jsx new file mode 100644 index 00000000..62c69b08 --- /dev/null +++ b/stories/FormikTextField.stories.jsx @@ -0,0 +1,11 @@ +import FormikTextField from "../src/components/mui/formik-inputs/mui-formik-textfield"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/TextField", + component: FormikTextField, + decorators: [withFormik({ company_name: "Acme Corp" })] +}; + +export const Default = { args: { name: "company_name", label: "Company name", maxLength: 80 } }; + diff --git a/stories/FormikTimepicker.stories.jsx b/stories/FormikTimepicker.stories.jsx new file mode 100644 index 00000000..717cf394 --- /dev/null +++ b/stories/FormikTimepicker.stories.jsx @@ -0,0 +1,11 @@ +import FormikTimepicker from "../src/components/mui/formik-inputs/mui-formik-timepicker"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/Timepicker", + component: FormikTimepicker, + decorators: [withFormik({ starts_at: null })] +}; + +export const Default = { args: { name: "starts_at", label: "Starts at", timeZone: "America/Chicago" } }; + diff --git a/stories/FormikUpload.stories.jsx b/stories/FormikUpload.stories.jsx new file mode 100644 index 00000000..2f225c24 --- /dev/null +++ b/stories/FormikUpload.stories.jsx @@ -0,0 +1,11 @@ +import FormikUpload from "../src/components/mui/formik-inputs/mui-formik-upload"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Formik inputs/Upload", + component: FormikUpload, + decorators: [withFormik({ images: [] })] +}; + +export const Default = { args: { id: "images", name: "images", maxFiles: 3, allowedExtensions: ["png", "jpg"] } }; + diff --git a/stories/GridFilter.stories.jsx b/stories/GridFilter.stories.jsx new file mode 100644 index 00000000..cebea7a5 --- /dev/null +++ b/stories/GridFilter.stories.jsx @@ -0,0 +1,58 @@ +import React from "react"; +import GridFilter from "../src/components/mui/GridFilter/GridFilter"; +import { OPERATORS } from "../src/components/mui/GridFilter/utils"; + +// criterias lifted from src/components/mui/GridFilter/readme.md +const criterias = [ + { + key: "tracks", + label: "Tracks", + operators: [OPERATORS.IS, OPERATORS.LIKE], + values: { + type: "select", + props: { + options: [ + { value: 1, label: "OpenStack" }, + { value: 2, label: "FnTech" } + ], + multiple: true, + placeholder: "Select Tracks" + } + } + }, + { + key: "selection_status", + label: "Selection Status", + operators: [OPERATORS.IS], + values: { + type: "select", + props: { + options: [ + { value: "accepted", label: "Accepted" }, + { value: "rejected", label: "Rejected" }, + { value: "alternate", label: "Alternate" } + ], + placeholder: "Filter by Selection Status" + } + } + } +]; + +export default { + title: "MUI/GridFilter", + component: GridFilter, + args: { criterias, hideJoinOperators: false }, + argTypes: { onApply: { action: "applied" } }, + parameters: { docs: { description: { component: "See src/components/mui/GridFilter/readme.md for the full criteria contract." } } } +}; + +// each story gets its own id — filter state is keyed by id in a store shared across stories +export const Default = { args: { id: "story-default" } }; + +export const JoinOperatorsHidden = { + args: { id: "story-hidden-join", hideJoinOperators: true } +}; + +export const SingleCriteria = { + args: { id: "story-single", criterias: [criterias[0]] } +}; diff --git a/stories/InfiniteTable.stories.jsx b/stories/InfiniteTable.stories.jsx new file mode 100644 index 00000000..0b98b975 --- /dev/null +++ b/stories/InfiniteTable.stories.jsx @@ -0,0 +1,15 @@ +import InfiniteTable from "../src/components/mui/infinite-table"; +import { sampleRows, sampleColumns } from "./_helpers"; + +export default { + title: "MUI/Tables/InfiniteTable", + component: InfiniteTable, + argTypes: { onSort: { action: "sort" }, loadMoreData: { action: "load-more" }, onRowEdit: { action: "row-edit" } } +}; + +export const Default = { + args: { columns: sampleColumns, data: sampleRows, boxHeight: "300px" } +}; + +export const Empty = { args: { columns: sampleColumns, data: [], boxHeight: "300px" } }; + diff --git a/stories/InfoNote.stories.jsx b/stories/InfoNote.stories.jsx new file mode 100644 index 00000000..0783af41 --- /dev/null +++ b/stories/InfoNote.stories.jsx @@ -0,0 +1,8 @@ +import InfoNote from "../src/components/mui/InfoNote"; + +export default { title: "MUI/Feedback/InfoNote", component: InfoNote }; + +export const Default = { + args: { message: "Prices shown exclude tax. Tax is applied at checkout." } +}; + diff --git a/stories/InlineCard.stories.jsx b/stories/InlineCard.stories.jsx new file mode 100644 index 00000000..9bd15a17 --- /dev/null +++ b/stories/InlineCard.stories.jsx @@ -0,0 +1,15 @@ +import InlineCard from "../src/components/mui/cards/InlineCard"; + +export default { title: "MUI/Cards/InlineCard", component: InlineCard }; + +export const Default = { + args: { + title: "Sponsor", + rows: [ + { label: "Company", value: "Acme Corp" }, + { label: "Tier", value: "Diamond" }, + { label: "Contact", value: "casey@example.com" } + ] + } +}; + diff --git a/stories/ItemSettingsModal.stories.jsx b/stories/ItemSettingsModal.stories.jsx new file mode 100644 index 00000000..888da967 --- /dev/null +++ b/stories/ItemSettingsModal.stories.jsx @@ -0,0 +1,16 @@ +import ItemSettingsModal from "../src/components/mui/ItemSettingsModal"; + +export default { + title: "MUI/Dialogs/ItemSettingsModal", + component: ItemSettingsModal, + argTypes: { onClose: { action: "closed" } } +}; + +export const Default = { + args: { + open: true, + timeZone: "America/Chicago", + item: { id: 1, name: "Booth 10x10", quantity: 1, price: 8000, meta_fields: [] } + } +}; + diff --git a/stories/ListCard.stories.jsx b/stories/ListCard.stories.jsx new file mode 100644 index 00000000..90bfa7d9 --- /dev/null +++ b/stories/ListCard.stories.jsx @@ -0,0 +1,15 @@ +import ListCard from "../src/components/mui/cards/ListCard"; + +export default { title: "MUI/Cards/ListCard", component: ListCard }; + +export const Default = { + args: { + title: "Included add-ons", + rows: [ + { label: "Booth 10x10", value: "1" }, + { label: "Lanyard branding", value: "4" }, + { label: "Keynote mention", value: "1" } + ] + } +}; + diff --git a/stories/LoadingOverlay.stories.jsx b/stories/LoadingOverlay.stories.jsx new file mode 100644 index 00000000..0c80ceee --- /dev/null +++ b/stories/LoadingOverlay.stories.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import LoadingOverlay from "../src/components/mui/LoadingOverlay"; + +export default { title: "MUI/Feedback/LoadingOverlay", component: LoadingOverlay }; + +export const Loading = { + render: (args) => ( +
+ +
+ ), + args: { loading: true } +}; + +export const Idle = { ...Loading, args: { loading: false } }; + diff --git a/stories/MenuButton.stories.jsx b/stories/MenuButton.stories.jsx new file mode 100644 index 00000000..3b3bde64 --- /dev/null +++ b/stories/MenuButton.stories.jsx @@ -0,0 +1,19 @@ +import MenuButton from "../src/components/mui/menu-button"; + +export default { title: "MUI/Buttons/MenuButton", component: MenuButton }; + +export const Default = { + args: { + buttonId: "story-menu-button", + menuId: "story-menu", + children: "Actions", + menuItems: [ + { label: "Edit", onClick: () => {} }, + { label: "Duplicate", onClick: () => {} }, + { label: "Delete", onClick: () => {} } + ] + } +}; + +export const WithBadge = { args: { ...Default.args, hasBadge: true } }; + diff --git a/stories/NavBar.stories.jsx b/stories/NavBar.stories.jsx new file mode 100644 index 00000000..d9b4bc6d --- /dev/null +++ b/stories/NavBar.stories.jsx @@ -0,0 +1,20 @@ +import NavBar from "../src/components/mui/NavBar"; + +export default { + title: "MUI/Layout/NavBar", + component: NavBar, + argTypes: { onClickLogin: { action: "login" }, initLogOut: { action: "logout" } }, + parameters: { layout: "fullscreen" } +}; + +export const LoggedOut = { args: { title: "Sponsor Portal", isLoggedUser: false } }; + +export const LoggedIn = { + args: { + title: "Sponsor Portal", + isLoggedUser: true, + profileName: "Casey Locker", + profileEmail: "casey@example.com" + } +}; + diff --git a/stories/NotesModal.stories.jsx b/stories/NotesModal.stories.jsx new file mode 100644 index 00000000..8c2b0ae0 --- /dev/null +++ b/stories/NotesModal.stories.jsx @@ -0,0 +1,20 @@ +import NotesModal from "../src/components/mui/NotesModal"; +import { withFormik } from "./_helpers"; + +export default { + title: "MUI/Dialogs/NotesModal", + component: NotesModal, + argTypes: { onClose: { action: "closed" } }, + decorators: [withFormik({ notes: "" })] +}; + +export const Default = { + args: { + open: true, + id: 42, + title: "Internal notes", + label: "Note", + placeholder: "Add a note for the ops team..." + } +}; + diff --git a/stories/OrderSummary.stories.jsx b/stories/OrderSummary.stories.jsx new file mode 100644 index 00000000..bf005f87 --- /dev/null +++ b/stories/OrderSummary.stories.jsx @@ -0,0 +1,13 @@ +import OrderSummary from "../src/components/mui/OrderSummary"; + +export default { title: "MUI/Commerce/OrderSummary", component: OrderSummary }; + +export const Default = { + args: { + amount: 3350000, + dueDate: "2026-09-30", + toName: "Acme Corp", + fromName: "Foxtrot November Events" + } +}; + diff --git a/stories/RoundButton.stories.jsx b/stories/RoundButton.stories.jsx new file mode 100644 index 00000000..225e7c9a --- /dev/null +++ b/stories/RoundButton.stories.jsx @@ -0,0 +1,12 @@ +import RoundButton from "../src/components/mui/RoundButton"; + +export default { + title: "MUI/Buttons/RoundButton", + component: RoundButton, + argTypes: { onClick: { action: "clicked" } } +}; + +export const Default = { args: { children: "Save changes" } }; +export const Outlined = { args: { children: "Cancel", variant: "outlined" } }; +export const Disabled = { args: { children: "Save changes", disabled: true } }; + diff --git a/stories/SearchInput.stories.jsx b/stories/SearchInput.stories.jsx new file mode 100644 index 00000000..fe3480d8 --- /dev/null +++ b/stories/SearchInput.stories.jsx @@ -0,0 +1,12 @@ +import SearchInput from "../src/components/mui/search-input"; + +export default { + title: "MUI/Inputs/SearchInput", + component: SearchInput, + argTypes: { onSearch: { action: "search" } } +}; + +export const Default = { args: { placeholder: "Search sponsors..." } }; +export const WithTerm = { args: { term: "diamond", placeholder: "Search sponsors..." } }; +export const Debounced = { args: { placeholder: "Type to search...", debounced: true } }; + diff --git a/stories/ShowConfirmDialog.stories.jsx b/stories/ShowConfirmDialog.stories.jsx new file mode 100644 index 00000000..b4a7d552 --- /dev/null +++ b/stories/ShowConfirmDialog.stories.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import { Button } from "@mui/material"; +import showConfirmDialog, { GlobalConfirmDialog } from "../src/components/mui/showConfirmDialog"; + +export default { + title: "MUI/Dialogs/ShowConfirmDialog", + component: GlobalConfirmDialog, + parameters: { + docs: { + description: { + component: + "Imperative API. Mount GlobalConfirmDialog once at the app root, then call showConfirmDialog() from anywhere; it resolves with the user's choice." + } + } + } +}; + +export const Imperative = { + render: () => ( + <> + + + + ) +}; + diff --git a/stories/SnackbarNotification.stories.jsx b/stories/SnackbarNotification.stories.jsx new file mode 100644 index 00000000..fdf4b35f --- /dev/null +++ b/stories/SnackbarNotification.stories.jsx @@ -0,0 +1,65 @@ +import React from "react"; +import { useDispatch } from "react-redux"; +import { Button } from "@mui/material"; +import SnackbarNotification, { useSnackbarMessage } from "../src/components/mui/SnackbarNotification"; +import { setSnackbarMessage } from "../src/utils/actions"; + +export default { + title: "MUI/SnackbarNotification", + component: SnackbarNotification, + parameters: { + docs: { + description: { + component: + "Wrapper component — renders nothing until a message arrives. Two trigger paths: the useSnackbarMessage hook, or a snackbarMessage in the redux base reducer." + } + } + } +}; + +// path 1: the useSnackbarMessage hook, for on-demand messaging +const HookTrigger = () => { + const { successMessage, errorMessage } = useSnackbarMessage(); + return ( + <> + + + + ); +}; + +export const ViaHook = { + render: () => ( + + + + ) +}; + +// path 2: baseState.snackbarMessage, set by snackbarSuccessHandler/snackbarErrorHandler +const DispatchTrigger = () => { + const dispatch = useDispatch(); + return ( + + ); +}; + +export const ViaReduxState = { + render: () => ( + + + + ) +}; diff --git a/stories/SortableTable.stories.jsx b/stories/SortableTable.stories.jsx new file mode 100644 index 00000000..51fa01fa --- /dev/null +++ b/stories/SortableTable.stories.jsx @@ -0,0 +1,24 @@ +import SortableTable from "../src/components/mui/sortable-table/mui-table-sortable"; +import { sampleRows, sampleColumns } from "./_helpers"; + +export default { + title: "MUI/Tables/SortableTable", + component: SortableTable, + argTypes: { + onSort: { action: "sort" }, + onReorder: { action: "reorder" }, + onEdit: { action: "edit" }, + onDelete: { action: "delete" } + }, + parameters: { + docs: { description: { component: "Drag-to-reorder table. Pulls react-beautiful-dnd as an optional peer." } } + } +}; + +export const Default = { + args: { + columns: sampleColumns, + data: sampleRows.map((r, i) => ({ ...r, order: i + 1 })) + } +}; + diff --git a/stories/SponsorAddonSelect.stories.jsx b/stories/SponsorAddonSelect.stories.jsx new file mode 100644 index 00000000..9f94edc7 --- /dev/null +++ b/stories/SponsorAddonSelect.stories.jsx @@ -0,0 +1,18 @@ +import SponsorAddonSelect from "../src/components/mui/sponsor-addon-select"; +import { NEEDS_API } from "./_helpers"; + +export default { + title: "MUI/API-backed/SponsorAddonSelect", + component: SponsorAddonSelect, + argTypes: { onChange: { action: "changed" } }, + parameters: { docs: { description: { component: NEEDS_API } } } +}; + +export const Default = { + args: { summitId: 1, sponsor: { + id: 7, + company: { name: "Acme Corp" }, + sponsorships: [{ id: 1, name: "Diamond" }] + }, placeholder: "Select an add-on" } +}; + diff --git a/stories/SponsorOrderGrid.stories.jsx b/stories/SponsorOrderGrid.stories.jsx new file mode 100644 index 00000000..f74f6d17 --- /dev/null +++ b/stories/SponsorOrderGrid.stories.jsx @@ -0,0 +1,44 @@ +import SponsorOrderGrid from "../src/components/mui/SponsorOrderGrid"; + +const order = { + id: 9001, + status: "Paid", + currency: "USD", + currency_symbol: "$", + sub_total: 3300000, + taxes_amount: 50000, + discount_amount: 0, + total: 3350000, + purchased_amount: 3350000, + refunded_amount: 0, + lines: [ + { + id: 1, + name: "Diamond Sponsorship", + description: "Top tier package", + qty: 1, + subtotal: 2500000, + total: 2500000, + early_bird_discount: 0 + }, + { + id: 2, + name: "Booth 10x10", + description: "Expo floor booth", + qty: 1, + subtotal: 800000, + total: 800000, + early_bird_discount: 0 + } + ] +}; + +export default { + title: "MUI/Commerce/SponsorOrderGrid", + component: SponsorOrderGrid, + argTypes: { onCancelForm: { action: "cancel" }, onUndoCancelForm: { action: "undo-cancel" } } +}; + +export const Default = { args: { order } }; +export const WithReconciliation = { args: { order, withReconciliation: true } }; + diff --git a/stories/StatusChip.stories.jsx b/stories/StatusChip.stories.jsx new file mode 100644 index 00000000..318f5ce0 --- /dev/null +++ b/stories/StatusChip.stories.jsx @@ -0,0 +1,9 @@ +import StatusChip from "../src/components/mui/StatusChip"; + +export default { title: "MUI/Data display/StatusChip", component: StatusChip }; + +export const Paid = { args: { status: "Paid" } }; +export const Pending = { args: { status: "Pending" } }; +export const Cancelled = { args: { status: "Cancelled" } }; +export const Refunded = { args: { status: "Refunded" } }; + diff --git a/stories/StripePayment.stories.jsx b/stories/StripePayment.stories.jsx new file mode 100644 index 00000000..19bdd489 --- /dev/null +++ b/stories/StripePayment.stories.jsx @@ -0,0 +1,35 @@ +import React from "react"; +import { Alert } from "@mui/material"; +import StripePayment from "../src/components/mui/StripePayment"; + +export default { + title: "MUI/Commerce/StripePayment", + component: StripePayment, + argTypes: { onPaymentSuccess: { action: "success" }, onPaymentError: { action: "error" } }, + parameters: { + docs: { + description: { + component: + "The only component here that cannot render standalone: Stripe Elements needs a real publishable key plus a client_secret from a server-created payment intent. Supply both via args to see the live card form." + } + } + } +}; + +export const NeedsStripeCredentials = { + render: (args) => ( + <> + + Renders blank without a real Stripe publishable key and client_secret. Set + both in the Controls panel to load the live card form. + + + + ), + args: { + stripeFormTitle: "Card details", + paymentProfile: { publishable_key: "" }, + paymentIntent: { client_secret: "" }, + paymentOptions: { currency: "USD", amount: 3350000 } + } +}; diff --git a/stories/SummitsDropdown.stories.jsx b/stories/SummitsDropdown.stories.jsx new file mode 100644 index 00000000..23dd04ac --- /dev/null +++ b/stories/SummitsDropdown.stories.jsx @@ -0,0 +1,28 @@ +import SummitsDropdown from "../src/components/mui/summits-dropdown"; + +export default { + title: "MUI/API-backed/SummitsDropdown", + component: SummitsDropdown, + argTypes: { onChange: { action: "changed" } }, + parameters: { + docs: { + description: { + component: + "`summits` has no default and is read during the first render, so it must always be passed even though propTypes does not mark it required — omitting it throws before the useEffect fetch ever runs. Pass [] to opt into the fetch path, or a populated array to skip it." + } + } + } +}; + +// [] opts into the fetch path; no API is reachable from Storybook, so it stays empty +export const FetchesOnMount = { args: { summits: [], label: "Search by show" } }; + +export const WithSummitsProvided = { + args: { + label: "Search by show", + summits: [ + { id: 1, name: "FN Summit 2026" }, + { id: 2, name: "FN Summit 2025" } + ] + } +}; diff --git a/stories/Table.stories.jsx b/stories/Table.stories.jsx new file mode 100644 index 00000000..6cd7b495 --- /dev/null +++ b/stories/Table.stories.jsx @@ -0,0 +1,37 @@ +import Table from "../src/components/mui/table/mui-table"; +import { sampleRows, sampleColumns } from "./_helpers"; + +export default { + title: "MUI/Tables/Table", + component: Table, + argTypes: { + onSort: { action: "sort" }, + onEdit: { action: "edit" }, + onDelete: { action: "delete" }, + onPageChange: { action: "page-change" }, + onPerPageChange: { action: "per-page-change" } + } +}; + +export const Default = { args: { columns: sampleColumns, data: sampleRows } }; + +export const Paginated = { + args: { + columns: sampleColumns, + data: sampleRows, + totalRows: 42, + perPage: 3, + currentPage: 1 + } +}; + +export const Sorted = { + args: { + columns: sampleColumns, + data: sampleRows, + options: { sortCol: "name", sortDir: 1 } + } +}; + +export const Empty = { args: { columns: sampleColumns, data: [] } }; + diff --git a/stories/TableCard.stories.jsx b/stories/TableCard.stories.jsx new file mode 100644 index 00000000..e39e057d --- /dev/null +++ b/stories/TableCard.stories.jsx @@ -0,0 +1,19 @@ +import TableCard from "../src/components/mui/cards/TableCard"; + +export default { title: "MUI/Cards/TableCard", component: TableCard }; + +export const Default = { + args: { + title: "Order lines", + columns: [ + { columnKey: "name", header: "Item" }, + { columnKey: "quantity", header: "Qty", align: "right" }, + { columnKey: "price", header: "Price", align: "right" } + ], + rows: [ + { id: 1, name: "Diamond Sponsorship", quantity: 2, price: "$25,000" }, + { id: 2, name: "Booth 10x10", quantity: 1, price: "$8,000" } + ] + } +}; + diff --git a/stories/ToggleButtons.stories.jsx b/stories/ToggleButtons.stories.jsx new file mode 100644 index 00000000..2913efc5 --- /dev/null +++ b/stories/ToggleButtons.stories.jsx @@ -0,0 +1,16 @@ +import ToggleButtons from "../src/components/mui/ToggleButtons"; + +export default { + title: "MUI/Inputs/ToggleButtons", + component: ToggleButtons, + argTypes: { onChange: { action: "changed" } } +}; + +export const Default = { + args: { options: ["ALL", "ANY"], value: "ALL" } +}; + +export const Secondary = { + args: { options: ["Day", "Week", "Month"], value: "Week", color: "secondary" } +}; + diff --git a/stories/UploadBtn.stories.jsx b/stories/UploadBtn.stories.jsx new file mode 100644 index 00000000..e88cc8e1 --- /dev/null +++ b/stories/UploadBtn.stories.jsx @@ -0,0 +1,11 @@ +import UploadBtn from "../src/components/mui/UploadBtn"; + +export default { + title: "MUI/Buttons/UploadBtn", + component: UploadBtn, + argTypes: { onClick: { action: "clicked" } } +}; + +export const Default = { args: { disabled: false } }; +export const Disabled = { args: { disabled: true } }; + diff --git a/stories/UploadDialog.stories.jsx b/stories/UploadDialog.stories.jsx new file mode 100644 index 00000000..57cdcd4c --- /dev/null +++ b/stories/UploadDialog.stories.jsx @@ -0,0 +1,23 @@ +import UploadDialog from "../src/components/mui/UploadDialog"; + +export default { + title: "MUI/Dialogs/UploadDialog", + component: UploadDialog, + argTypes: { onClose: { action: "closed" }, onUpload: { action: "uploaded" } } +}; + +export const Default = { + args: { + open: true, + name: "logo", + value: [], + maxFiles: 1, + fileMeta: { + name: "Sponsor logo", + description: "PNG or SVG, 5 MB max.", + max_file_size: 5242880, + allowed_extensions: "png,jpg,svg" + } + } +}; + diff --git a/stories/_form-item-fixture.js b/stories/_form-item-fixture.js new file mode 100644 index 00000000..a7e28b43 --- /dev/null +++ b/stories/_form-item-fixture.js @@ -0,0 +1,102 @@ +// lifted verbatim from src/components/mui/FormItemTable/__tests__/FormItemTable.test.js +export const MOCK_FORM = { + items: [ + { + form_item_id: 1, + code: "INST", + name: "Installation", + rates: { + early_bird: 15000, + standard: 18800, + onsite: 22400 + }, + meta_fields: [ + { + type_id: 1, + class_field: "Form", + name: "Qty of People", + type: "Quantity", + minimum_quantity: 1, + maximum_quantity: 4 + }, + { + type_id: 2, + class_field: "Form", + name: "Hour x Person", + type: "Quantity", + minimum_quantity: 1, + maximum_quantity: 8 + }, + { + type_id: 3, + class_field: "Form", + name: "Arrival Time", + type: "Time" + }, + { + type_id: 4, + class_field: "Item", + name: "Special Instructions", + type: "Text", + is_required: true + } + ] + }, + { + form_item_id: 2, + code: "DISMANTLE", + name: "Dismantle", + rates: { + early_bird: 15000, + standard: 18800, + onsite: 22400 + }, + meta_fields: [ + { + type_id: 1, + class_field: "Form", + name: "Qty of People", + type: "Quantity", + minimum_quantity: 1, + maximum_quantity: 4 + }, + { + type_id: 2, + class_field: "Form", + name: "Hour x Person", + type: "Quantity", + minimum_quantity: 1, + maximum_quantity: 8 + }, + { + type_id: 3, + class_field: "Form", + name: "Arrival Time", + type: "Time" + } + ] + }, + { + form_item_id: 3, + code: "INST-MAN", + name: "Installation Manpower", + rates: { + early_bird: 15000, + standard: 18800, + onsite: 22400 + }, + meta_fields: [] + }, + { + form_item_id: 4, + code: "DIS-MAN", + name: "Dismantle Manpower", + rates: { + early_bird: 15000, + standard: 18800, + onsite: 22400 + }, + meta_fields: [] + } + ] +}; diff --git a/stories/_helpers.jsx b/stories/_helpers.jsx new file mode 100644 index 00000000..50a5755f --- /dev/null +++ b/stories/_helpers.jsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Formik, Form } from "formik"; + +/** + * Formik-bound inputs read from context, so every formik-inputs story needs a + * host form. Usage: decorators: [withFormik({ myField: "value" })] + */ +export const withFormik = + (initialValues = {}, formProps = {}) => + (Story) => ( + {}} {...formProps}> +
+ + +
+ ); + +/** Table rows have to live inside a table to render at all. */ +export const inTable = (Story) => ( + + + + +
+); + +/** Components that fetch on mount have no backend here — see docs note on each. */ +export const NEEDS_API = + "Fetches on mount via utils/query-actions. With no API reachable from Storybook it renders its empty state; wire msw to populate it."; + +export const sampleRows = [ + { id: 1, name: "Diamond Sponsorship", quantity: 2, price: 25000 }, + { id: 2, name: "Booth 10x10", quantity: 1, price: 8000 }, + { id: 3, name: "Lanyard Branding", quantity: 4, price: 1500 } +]; + +export const sampleColumns = [ + { columnKey: "name", header: "Item", sortable: true }, + { columnKey: "quantity", header: "Qty", align: "right" }, + { columnKey: "price", header: "Price", align: "right" } +]; diff --git a/stories/core/ActionDropdown.stories.jsx b/stories/core/ActionDropdown.stories.jsx new file mode 100644 index 00000000..2bc7fc9d --- /dev/null +++ b/stories/core/ActionDropdown.stories.jsx @@ -0,0 +1,16 @@ +import ActionDropdown from "../../src/components/inputs/action-dropdown"; + +export default { + title: "Core/Inputs/ActionDropdown", + component: ActionDropdown, + argTypes: { onClick: { action: "action" } } +}; + +const options = [ + { value: "export_csv", label: "Export CSV" }, + { value: "send_email", label: "Send Email" }, + { value: "archive", label: "Archive" } +]; + +export const Default = { args: { options, actionLabel: "Go", placeholder: "Bulk action..." } }; +export const Small = { args: { ...Default.args, small: true } }; diff --git a/stories/core/AjaxLoader.stories.jsx b/stories/core/AjaxLoader.stories.jsx new file mode 100644 index 00000000..5527fc71 --- /dev/null +++ b/stories/core/AjaxLoader.stories.jsx @@ -0,0 +1,17 @@ +import AjaxLoader from "../../src/components/ajaxloader"; + +export default { + title: "Core/Feedback/AjaxLoader", + component: AjaxLoader, + // fixed-position overlay; relative + a positioned box keeps it inside the story frame + decorators: [ + (Story) => ( +
+ +
+ ) + ] +}; + +export const Default = { args: { show: true, relative: true, size: 40 } }; +export const Hidden = { args: { show: false, relative: true, size: 40 } }; diff --git a/stories/core/AsyncEntityInputs.stories.jsx b/stories/core/AsyncEntityInputs.stories.jsx new file mode 100644 index 00000000..fd1fb311 --- /dev/null +++ b/stories/core/AsyncEntityInputs.stories.jsx @@ -0,0 +1,59 @@ +import CompanyInput from "../../src/components/inputs/company-input"; +import PromocodeInput from "../../src/components/inputs/promocode-input"; +import SponsorInput from "../../src/components/inputs/sponsor-input"; +import OrganizationInput from "../../src/components/inputs/organization-input"; +import EventInput from "../../src/components/inputs/event-input"; +import GroupInput from "../../src/components/inputs/group-input"; +import MemberInput from "../../src/components/inputs/member-input"; +import AttendeeInput from "../../src/components/inputs/attendee-input"; +import SummitInput from "../../src/components/inputs/summit-input"; +import SpeakerInput from "../../src/components/inputs/speaker-input"; +import OperatorInput from "../../src/components/inputs/operator-input"; +import TagInput from "../../src/components/inputs/tag-input"; +import AccessLevelsInput from "../../src/components/inputs/access-levels-input"; +import RegistrationCompanyInput from "../../src/components/inputs/registration-company-input"; +import TicketTypesInput from "../../src/components/inputs/ticket-types-input.js"; +import SponsoredProjectInput from "../../src/components/inputs/sponsored-project-input.js"; +import CountryInput from "../../src/components/inputs/country-input"; +import LanguageInput from "../../src/components/inputs/language-input"; +import CountryDropdown from "../../src/components/inputs/country-dropdown"; +import { NEEDS_API } from "../_helpers"; + +/** + * Every input here is an async select over utils/query-actions — one story each + * would be 19 near-identical files, so they share this one. NEEDS_API applies + * to all of them: they render and open, but option lists stay empty offline. + */ +export default { + title: "Core/Inputs/AsyncEntityInputs", + parameters: { docs: { description: { component: NEEDS_API } } }, + argTypes: { onChange: { action: "changed" } } +}; + +const base = { value: null, placeholder: "Type to search..." }; + +export const Company = { render: (args) => }; +export const Promocode = { render: (args) => }; +export const Sponsor = { render: (args) =>