columns\n */\n build_columns(result) {\n let columns = []\n let searchterm = this.props.searchterm || \"\";\n for (let name of this.get_column_names()) {\n let value = result[name];\n let highlighted = this.highlight(value, searchterm);\n columns.push(\n
\n );\n }\n let uid = result.uid;\n let used = this.props.uids.indexOf(uid) > -1;\n columns.push(\n
{used && }
\n );\n return columns;\n }\n\n /*\n * Highlight any found match of the searchterm in the text\n *\n * @returns {String} highlighted text\n */\n highlight(text, searchterm) {\n if (searchterm.length == 0) return text;\n try {\n let rx = new RegExp(searchterm, \"gi\");\n text = text.replaceAll(rx, (m) => {\n return \"\"+m+\"\";\n });\n } catch (error) {\n // pass\n }\n return text\n }\n\n /*\n * Build pagination
...
items\n *\n * @returns {Array} Pagination JSX\n */\n build_pages() {\n let pages = [];\n for (let page=1; page <= this.props.pages; page++) {\n let cls = [\"page-item\"];\n if (this.props.page == page) cls.push(\"active\");\n pages.push(\n
\n \n
\n );\n }\n return pages;\n }\n\n /*\n * Build pagination next button\n *\n * @returns {Array} Next button JSX\n */\n build_next_button() {\n let cls = [\"page-item\"]\n if (!this.props.next_url) cls.push(\"disabled\")\n return (\n
\n );\n }\n}\n\nexport default References;\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\n\nimport ReferenceWidgetAPI from \"./api.js\"\nimport ReferenceField from \"./components/ReferenceField.js\"\nimport ReferenceResults from \"./components/ReferenceResults.js\"\nimport References from \"./components/References.js\"\n\n\nclass UIDReferenceWidgetController extends React.Component {\n\n constructor(props) {\n super(props);\n\n // Internal state\n this.state = {\n results: [], // `items` list of search results coming from `senaite.jsonapi`\n searchterm: \"\", // the search term that was entered by the user\n loading: false, // loading flag when searching for results\n count: 0, // count of results (coming from `senaite.jsonapi`)\n page: 1, // current page (coming from `senaite.jsonapi`)\n pages: 1, // number of pages (coming from `senaite.jsonapi`)\n next_url: null, // next page API URL (coming from `senaite.jsonapi`)\n prev_url: null, // previous page API URL (coming from `senaite.jsonapi`)\n b_start: 1, // batch start for pagination (see `senaite.jsonapi.batch`)\n focused: 0, // current result that has the focus\n }\n\n // Root input HTML element\n let el = props.root_el;\n\n // Data keys located at the root element\n // -> initial values are set from the widget class\n const data_keys = [\n \"id\",\n \"name\",\n \"uids\",\n \"api_url\",\n \"records\",\n \"catalog\",\n \"query\",\n \"columns\",\n \"display_template\",\n \"limit\",\n \"multi_valued\",\n \"disabled\",\n \"readonly\",\n ]\n\n // Query data keys and set state with parsed JSON value\n for (let key of data_keys) {\n let value = el.dataset[key];\n this.state[key] = this.parse_json(value);\n }\n\n // Initialize communication API with the API URL\n this.api = new ReferenceWidgetAPI({\n api_url: this.state.api_url,\n });\n\n // Bind callbacks to current context\n this.search = this.search.bind(this);\n this.goto_page = this.goto_page.bind(this);\n this.clear_results = this.clear_results.bind(this);\n this.select = this.select.bind(this);\n this.select_focused = this.select_focused.bind(this);\n this.deselect = this.deselect.bind(this);\n this.navigate_results = this.navigate_results.bind(this);\n this.on_keydown = this.on_keydown.bind(this);\n this.on_click = this.on_click.bind(this);\n\n // dev only\n window.widget = this;\n\n return this\n }\n\n componentDidMount() {\n // Bind event listeners of the document\n document.addEventListener(\"keydown\", this.on_keydown, false);\n document.addEventListener(\"click\", this.on_click, false)\n }\n\n componentWillUnmount() {\n // Remove event listeners of the document\n document.removeEventListener(\"keydown\", this.on_keydown, false);\n document.removeEventListener(\"click\", this.on_click, false);\n }\n\n /*\n * JSON parse the given value\n *\n * @param {String} value: The JSON value to parse\n */\n parse_json(value) {\n try {\n return JSON.parse(value)\n } catch (error) {\n console.error(`Could not parse \"${value}\" to JSON`);\n }\n }\n\n is_disabled() {\n if (this.state.disabled) {\n return true;\n }\n if (this.state.readonly) {\n return true;\n }\n if (!this.state.multi_valued && this.state.uids.length > 0) {\n return true;\n }\n return false;\n }\n\n /*\n * Create a query object for the API\n *\n * This method prepares a query from the current state variables,\n * which can be used to call the `api.search` method.\n *\n * @param {Object} options: Additional options to add to the query\n * @returns {Object} The query object\n */\n make_query(options) {\n options = options || {};\n return Object.assign({\n q: this.state.searchterm,\n limit: this.state.limit,\n complete: 1,\n }, options, this.state.query);\n }\n\n /*\n * Execute a search query and set the results to the state\n *\n * @param {Object} url_params: Additional search params for the API search URL\n * @returns {Promise}\n */\n fetch_results(url_params) {\n url_params = url_params || {};\n // prepare the server request\n let self = this;\n let query = this.make_query();\n this.toggle_loading(true);\n let promise = this.api.search(this.state.catalog, query, url_params);\n promise.then(function(data) {\n console.debug(\"ReferenceWidgetController::fetch_results:GOT DATA: \", data);\n self.set_results_data(data);\n self.toggle_loading(false);\n });\n return promise;\n }\n\n /*\n * Execute a search for the given searchterm\n *\n * @param {String} searchterm: The value entered into the search field\n * @returns {Promise}\n */\n search(searchterm) {\n if (!searchterm && this.state.results.length > 0) {\n this.state.searchterm = \"\";\n return;\n }\n console.debug(\"ReferenceWidgetController::search:searchterm:\", searchterm);\n // set the searchterm directly to avoid re-rendering\n this.state.searchterm = searchterm || \"\";\n return this.fetch_results();\n }\n\n /*\n * Fetch results of a page\n *\n * @param {Integer} page: The page to fetch\n * @returns {Promise}\n */\n goto_page(page) {\n page = parseInt(page);\n let limit = parseInt(this.state.limit)\n // calculate the beginning of the page\n // Note: this is the count of previous items that are excluded\n let b_start = page * limit - limit;\n return this.fetch_results({b_start: b_start});\n }\n\n /*\n * Add the UID of a search result to the state\n *\n * @param {String} uid: The selected UID\n * @returns {Array} uids: current selected UIDs\n */\n select(uid) {\n console.debug(\"ReferenceWidgetController::select:uid:\", uid);\n // create a copy of the selected UIDs\n let uids = [].concat(this.state.uids);\n // Add the new UID if it is not selected yet\n if (uids.indexOf(uid) == -1) {\n uids.push(uid);\n }\n this.setState({uids: uids});\n if (uids.length > 0 && !this.state.multi_valued) {\n this.clear_results();\n }\n return uids;\n }\n\n /*\n * Add/remove the focused result\n *\n */\n select_focused() {\n console.debug(\"ReferenceWidgetController::select_focused\");\n let focused = this.state.focused;\n let result = this.state.results.at(focused);\n if (result) {\n let uid = result.uid;\n if (this.state.uids.indexOf(uid) == -1) {\n this.select(uid);\n } else {\n this.deselect(uid);\n }\n }\n }\n\n /*\n * Remove the UID of a reference from the state\n *\n * @param {String} uid: The selected UID\n * @returns {Array} uids: current selected UIDs\n */\n deselect(uid) {\n console.debug(\"ReferenceWidgetController::deselect:uid:\", uid);\n let uids = [].concat(this.state.uids);\n let pos = uids.indexOf(uid);\n if (pos > -1) {\n uids.splice(pos, 1);\n }\n this.setState({uids: uids});\n return uids;\n }\n\n /*\n * Navigate the results either up or down\n *\n * @param {String} direction: either up or down\n */\n navigate_results(direction) {\n let page = this.state.page;\n let pages = this.state.pages;\n let results = this.state.results;\n let focused = this.state.focused;\n let searchterm = this.state.searchterm;\n\n console.debug(\"ReferenceWidgetController::navigate_results:focused:\", focused);\n\n if (direction == \"up\") {\n if (focused > 0) {\n this.setState({focused: focused - 1});\n } else {\n this.setState({focused: 0});\n if (page > 1) {\n this.goto_page(page - 1);\n }\n }\n }\n\n else if (direction == \"down\") {\n if (this.state.results.length == 0) {\n this.search(searchterm);\n }\n if (focused < results.length - 1) {\n this.setState({focused: focused + 1});\n } else {\n this.setState({focused: 0});\n if (page < pages) {\n this.goto_page(page + 1);\n }\n }\n }\n\n else if (direction == \"left\") {\n this.setState({focused: 0});\n if (page > 0) {\n this.goto_page(page - 1);\n }\n }\n\n else if (direction == \"right\") {\n this.setState({focused: 0});\n if (page < pages) {\n this.goto_page(page + 1);\n }\n }\n }\n\n /*\n * Toggle loading state\n *\n * @param {Boolean} toggle: The loading state to set\n * @returns {Boolean} toggle: The current loading state\n */\n toggle_loading(toggle) {\n if (toggle == null) {\n toggle = false;\n }\n this.setState({\n loading: toggle\n });\n return toggle;\n }\n\n /*\n * Set results data coming from `senaite.jsonapi`\n *\n * @param {Object} data: JSON search result object returned from `senaite.jsonapi`\n */\n set_results_data(data) {\n data = data || {};\n let items = data.items || [];\n\n let records = Object.assign(this.state.records, {})\n // update state records\n for (let item of items) {\n let uid = item.uid;\n records[uid] = item;\n }\n\n this.setState({\n records: records,\n results: items,\n count: data.count || 0,\n page: data.page || 1,\n pages: data.pages || 1,\n next_url: data.next || null,\n prev_url: data.previous || null,\n });\n }\n\n /*\n * Clear results from the state\n */\n clear_results() {\n this.setState({\n results: [],\n count: 0,\n page: 1,\n pages: 1,\n next_url: null,\n prev_url: null,\n });\n }\n\n /*\n * ReactJS event handler for keydown event\n */\n on_keydown(event){\n // clear results when ESC key is pressed\n if(event.keyCode === 27) {\n this.clear_results();\n }\n }\n\n /*\n * ReactJS event handler for click events\n */\n on_click(event) {\n // clear results when clicked outside of the widget\n let widget = this.props.root_el;\n let target = event.target;\n if (!widget.contains(target)) {\n this.clear_results();\n }\n }\n\n render() {\n return (\n
\n );\n }\n}\n\nexport default AddressField;\n","import React from \"react\";\nimport ReactDOM from \"react-dom\";\n\nimport AddressField from \"./AddressField.js\";\n\n\nclass Address extends React.Component {\n\n constructor(props) {\n super(props);\n this.state = {\n country: props.country,\n subdivision1: props.subdivision1,\n subdivision2: props.subdivision2,\n city: props.city,\n zip: props.zip,\n address: props.address,\n address_type: props.address_type,\n }\n\n // Event handlers\n this.on_country_change = this.on_country_change.bind(this);\n this.on_subdivision1_change = this.on_subdivision1_change.bind(this);\n this.on_subdivision2_change = this.on_subdivision2_change.bind(this);\n this.on_city_change = this.on_city_change.bind(this);\n this.on_zip_change = this.on_zip_change.bind(this);\n this.on_address_change = this.on_address_change.bind(this);\n }\n\n force_array(value) {\n if (!Array.isArray(value)) {\n value = [];\n }\n return value;\n }\n\n /**\n * Returns the list of first-level subdivisions of the current country,\n * sorted alphabetically\n */\n get_subdivisions1() {\n let country = this.state.country;\n return this.force_array(this.props.subdivisions1[country]);\n }\n\n /**\n * Returns the list of subdivisions of the current first-level subdivision,\n * sorted sorted alphabetically\n */\n get_subdivisions2() {\n let subdivision1 = this.state.subdivision1;\n return this.force_array(this.props.subdivisions2[subdivision1]);\n }\n\n get_label(key) {\n let country = this.state.country;\n let label = this.props.labels[country];\n if (label != null && label.constructor == Object && key in label) {\n label = label[key];\n } else {\n label = this.props.labels[key];\n }\n return label;\n }\n\n /** Event triggered when the value for Country selector changes. Updates the\n * selector of subdivisions (e.g. states) with the list of top-level\n * subdivisions for the selected country\n */\n on_country_change(event) {\n let value = event.currentTarget.value;\n console.debug(`Address::on_country_change: ${value}`);\n if (this.props.on_country_change) {\n this.props.on_country_change(value);\n }\n this.setState({\n country: value,\n subdivision1: \"\",\n subdivision2: \"\",\n });\n }\n\n /** Event triggered when the value for the Country first-level subdivision\n * (e.g. state) selector changes. Updates the selector of subdivisions (e.g.\n * districts) for the selected subdivision and country\n */\n on_subdivision1_change(event) {\n let value = event.currentTarget.value;\n console.debug(`Address::on_subdivision1_change: ${value}`);\n if (this.props.on_subdivision1_change) {\n let country = this.state.country\n this.props.on_subdivision1_change(country, value);\n }\n this.setState({\n subdivision1: value,\n subdivision2: \"\",\n });\n }\n\n /** Event triggered when the value for the second-level subdivision (e.g.\n * district) selector changes\n */\n on_subdivision2_change(event) {\n let value = event.currentTarget.value;\n console.debug(`Address::on_subdivision2_change: ${value}`);\n if (this.props.on_subdivision2_change) {\n this.props.on_subdivision2_change(value);\n }\n this.setState({subdivision2: value});\n }\n\n /** Event triggered when the value for the address field changes\n */\n on_address_change(event) {\n let value = event.currentTarget.value;\n this.setState({address: value});\n }\n\n /** Event triggered when the value for the zip field changes\n */\n on_zip_change(event) {\n let value = event.currentTarget.value;\n this.setState({zip: value});\n }\n\n /** Event triggered when the value for the city field changes\n */\n on_city_change(event) {\n let value = event.currentTarget.value;\n this.setState({city: value});\n }\n\n get_input_id(subfield) {\n let id = this.props.id;\n let index = this.props.index;\n return `${id}-${index}-${subfield}`\n }\n\n get_input_name(subfield) {\n let name = this.props.name;\n let index = this.props.index;\n return `${name}.${index}.${subfield}`\n }\n\n render() {\n return (\n
\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n
\n );\n }\n\n}\n\nexport default Address;","import React from \"react\"\nimport ReactDOM from \"react-dom\"\n\nimport AddressWidgetAPI from \"./api.js\"\nimport Address from \"./components/Address.js\"\n\n\nclass AddressWidgetController extends React.Component {\n\n constructor(props) {\n super(props);\n\n // Root input HTML element\n let el = props.root_el;\n\n this.state = {};\n\n // Data keys located at the root element\n // -> initial values are set from the widget class\n const data_keys = [\n \"id\",\n \"name\",\n \"items\",\n \"portal_url\",\n \"labels\",\n \"countries\",\n \"subdivisions1\",\n \"subdivisions2\",\n ];\n\n // Query data keys and set state with parsed JSON value\n for (let key of data_keys) {\n let value = el.dataset[key];\n this.state[key] = this.parse_json(value);\n }\n\n // Initialize communication API with the API URL\n this.api = new AddressWidgetAPI({\n portal_url: this.state.portal_url,\n });\n\n // Bind callbacks to current context\n this.on_country_change = this.on_country_change.bind(this);\n this.on_subdivision1_change = this.on_subdivision1_change.bind(this);\n\n return this;\n }\n\n parse_json(value) {\n try {\n return JSON.parse(value);\n } catch (error) {\n console.error(`Could not parse \"${value}\" to JSON`);\n }\n }\n\n /**\n * Event triggered when the user selects a country. Function fetches and\n * updates the geo mapping with the first level subdivisions for the selected\n * country if are not up-to-date yet. It also updates the label for the first\n * level subdivision in accordance.\n */\n on_country_change(country) {\n console.debug(`widget::on_country_change: ${country}`);\n let self = this;\n let promise = this.api.fetch_subdivisions(country);\n promise.then(function(data){\n\n // Update the label with the type of 1st-level subdivisions\n let labels = {...self.state.labels};\n if (data.length > 0) {\n labels[country][\"subdivision1\"] = data[0].type;\n }\n\n // Create a copy instead of modifying the existing dict from state var\n let subdivisions = {...self.state.subdivisions1};\n\n // Only interested in names, sorted alphabetically\n subdivisions[country] = data.map((x) => x.name).sort();\n\n // Update current state with the changes\n self.setState({\n subdivisions1: subdivisions,\n labels: labels,\n });\n });\n return promise;\n }\n\n /**\n * Event triggered when the user selects a first-level subdivision of a\n * country. Function fetches and updates the geo mapping with the second level\n * subdivisions for the selected subdivision if are not up-to-date. It also\n * updates the label for the second level subdivision in accordance.\n */\n on_subdivision1_change(country, subdivision) {\n console.debug(`widget::on_subdivision1_change: ${country}, ${subdivision}`);\n let self = this;\n let promise = this.api.fetch_subdivisions(subdivision);\n promise.then(function(data){\n\n // Update the label with the type of 1st-level subdivisions\n let labels = {...self.state.labels};\n if (data.length > 0) {\n labels[country][\"subdivision2\"] = data[0].type;\n }\n\n // Create a copy instead of modifying the existing dict from state var\n let subdivisions = {...self.state.subdivisions2};\n\n // Only interested in names, sorted alphabetically\n subdivisions[subdivision] = data.map((x) => x.name).sort();\n\n // Update current state with the changes\n self.setState({\n subdivisions2: subdivisions,\n labels: labels,\n });\n });\n return promise;\n }\n\n render_items() {\n let html_items = [];\n let items = this.state.items;\n for (const [index, item] of items.entries()) {\n let section_title = \"\";\n if (items.length > 1) {\n // Only render the title if more than one address\n section_title = (\n {this.state.labels[item.type]}\n )\n }\n\n html_items.push(\n
columns\n */\n build_columns(result) {\n let columns = []\n let searchterm = this.props.searchterm || \"\";\n for (let name of this.get_column_names()) {\n let value = result[name];\n let highlighted = this.highlight(value, searchterm);\n columns.push(\n
\n );\n }\n let uid = result.uid;\n let used = this.props.uids.indexOf(uid) > -1;\n columns.push(\n
{used && }
\n );\n return columns;\n }\n\n /*\n * Highlight any found match of the searchterm in the text\n *\n * @returns {String} highlighted text\n */\n highlight(text, searchterm) {\n if (searchterm.length == 0) return text;\n try {\n let rx = new RegExp(searchterm, \"gi\");\n text = text.replaceAll(rx, (m) => {\n return \"\"+m+\"\";\n });\n } catch (error) {\n // pass\n }\n return text\n }\n\n /*\n * Build pagination
...
items\n *\n * @returns {Array} Pagination JSX\n */\n build_pages() {\n let pages = [];\n for (let page=1; page <= this.props.pages; page++) {\n let cls = [\"page-item\"];\n if (this.props.page == page) cls.push(\"active\");\n pages.push(\n
\n \n
\n );\n }\n return pages;\n }\n\n /*\n * Build pagination next button\n *\n * @returns {Array} Next button JSX\n */\n build_next_button() {\n let cls = [\"page-item\"]\n if (!this.props.next_url) cls.push(\"disabled\")\n return (\n
\n );\n }\n}\n\nexport default References;\n","import React from \"react\"\nimport ReactDOM from \"react-dom\"\n\nimport ReferenceWidgetAPI from \"./api.js\"\nimport ReferenceField from \"./components/ReferenceField.js\"\nimport ReferenceResults from \"./components/ReferenceResults.js\"\nimport References from \"./components/References.js\"\n\n\nclass UIDReferenceWidgetController extends React.Component {\n\n constructor(props) {\n super(props);\n\n // Internal state\n this.state = {\n results: [], // `items` list of search results coming from `senaite.jsonapi`\n searchterm: \"\", // the search term that was entered by the user\n loading: false, // loading flag when searching for results\n count: 0, // count of results (coming from `senaite.jsonapi`)\n page: 1, // current page (coming from `senaite.jsonapi`)\n pages: 1, // number of pages (coming from `senaite.jsonapi`)\n next_url: null, // next page API URL (coming from `senaite.jsonapi`)\n prev_url: null, // previous page API URL (coming from `senaite.jsonapi`)\n b_start: 1, // batch start for pagination (see `senaite.jsonapi.batch`)\n focused: 0, // current result that has the focus\n }\n\n // Root input HTML element\n let el = props.root_el;\n\n // Data keys located at the root element\n // -> initial values are set from the widget class\n const data_keys = [\n \"id\",\n \"name\",\n \"uids\",\n \"api_url\",\n \"records\",\n \"catalog\",\n \"query\",\n \"columns\",\n \"display_template\",\n \"limit\",\n \"multi_valued\",\n \"disabled\",\n \"readonly\",\n ]\n\n // Query data keys and set state with parsed JSON value\n for (let key of data_keys) {\n let value = el.dataset[key];\n this.state[key] = this.parse_json(value);\n }\n\n // Initialize communication API with the API URL\n this.api = new ReferenceWidgetAPI({\n api_url: this.state.api_url,\n });\n\n // Bind callbacks to current context\n this.search = this.search.bind(this);\n this.goto_page = this.goto_page.bind(this);\n this.clear_results = this.clear_results.bind(this);\n this.select = this.select.bind(this);\n this.select_focused = this.select_focused.bind(this);\n this.deselect = this.deselect.bind(this);\n this.navigate_results = this.navigate_results.bind(this);\n this.on_keydown = this.on_keydown.bind(this);\n this.on_click = this.on_click.bind(this);\n\n // dev only\n window.widget = this;\n\n return this\n }\n\n componentDidMount() {\n // Bind event listeners of the document\n document.addEventListener(\"keydown\", this.on_keydown, false);\n document.addEventListener(\"click\", this.on_click, false)\n }\n\n componentWillUnmount() {\n // Remove event listeners of the document\n document.removeEventListener(\"keydown\", this.on_keydown, false);\n document.removeEventListener(\"click\", this.on_click, false);\n }\n\n /*\n * JSON parse the given value\n *\n * @param {String} value: The JSON value to parse\n */\n parse_json(value) {\n try {\n return JSON.parse(value)\n } catch (error) {\n console.error(`Could not parse \"${value}\" to JSON`);\n }\n }\n\n is_disabled() {\n if (this.state.disabled) {\n return true;\n }\n if (this.state.readonly) {\n return true;\n }\n if (!this.state.multi_valued && this.state.uids.length > 0) {\n return true;\n }\n return false;\n }\n\n /*\n * Create a query object for the API\n *\n * This method prepares a query from the current state variables,\n * which can be used to call the `api.search` method.\n *\n * @param {Object} options: Additional options to add to the query\n * @returns {Object} The query object\n */\n make_query(options) {\n options = options || {};\n return Object.assign({\n q: this.state.searchterm,\n limit: this.state.limit,\n complete: 1,\n }, options, this.state.query);\n }\n\n /*\n * Execute a search query and set the results to the state\n *\n * @param {Object} url_params: Additional search params for the API search URL\n * @returns {Promise}\n */\n fetch_results(url_params) {\n url_params = url_params || {};\n // prepare the server request\n let self = this;\n let query = this.make_query();\n this.toggle_loading(true);\n let promise = this.api.search(this.state.catalog, query, url_params);\n promise.then(function(data) {\n console.debug(\"ReferenceWidgetController::fetch_results:GOT DATA: \", data);\n self.set_results_data(data);\n self.toggle_loading(false);\n });\n return promise;\n }\n\n /*\n * Execute a search for the given searchterm\n *\n * @param {String} searchterm: The value entered into the search field\n * @returns {Promise}\n */\n search(searchterm) {\n if (!searchterm && this.state.results.length > 0) {\n this.state.searchterm = \"\";\n return;\n }\n console.debug(\"ReferenceWidgetController::search:searchterm:\", searchterm);\n // set the searchterm directly to avoid re-rendering\n this.state.searchterm = searchterm || \"\";\n return this.fetch_results();\n }\n\n /*\n * Fetch results of a page\n *\n * @param {Integer} page: The page to fetch\n * @returns {Promise}\n */\n goto_page(page) {\n page = parseInt(page);\n let limit = parseInt(this.state.limit)\n // calculate the beginning of the page\n // Note: this is the count of previous items that are excluded\n let b_start = page * limit - limit;\n return this.fetch_results({b_start: b_start});\n }\n\n /*\n * Add the UID of a search result to the state\n *\n * @param {String} uid: The selected UID\n * @returns {Array} uids: current selected UIDs\n */\n select(uid) {\n console.debug(\"ReferenceWidgetController::select:uid:\", uid);\n // create a copy of the selected UIDs\n let uids = [].concat(this.state.uids);\n // Add the new UID if it is not selected yet\n if (uids.indexOf(uid) == -1) {\n uids.push(uid);\n }\n this.setState({uids: uids});\n if (uids.length > 0 && !this.state.multi_valued) {\n this.clear_results();\n }\n return uids;\n }\n\n /*\n * Add/remove the focused result\n *\n */\n select_focused() {\n console.debug(\"ReferenceWidgetController::select_focused\");\n let focused = this.state.focused;\n let result = this.state.results.at(focused);\n if (result) {\n let uid = result.uid;\n if (this.state.uids.indexOf(uid) == -1) {\n this.select(uid);\n } else {\n this.deselect(uid);\n }\n }\n }\n\n /*\n * Remove the UID of a reference from the state\n *\n * @param {String} uid: The selected UID\n * @returns {Array} uids: current selected UIDs\n */\n deselect(uid) {\n console.debug(\"ReferenceWidgetController::deselect:uid:\", uid);\n let uids = [].concat(this.state.uids);\n let pos = uids.indexOf(uid);\n if (pos > -1) {\n uids.splice(pos, 1);\n }\n this.setState({uids: uids});\n return uids;\n }\n\n /*\n * Navigate the results either up or down\n *\n * @param {String} direction: either up or down\n */\n navigate_results(direction) {\n let page = this.state.page;\n let pages = this.state.pages;\n let results = this.state.results;\n let focused = this.state.focused;\n let searchterm = this.state.searchterm;\n\n console.debug(\"ReferenceWidgetController::navigate_results:focused:\", focused);\n\n if (direction == \"up\") {\n if (focused > 0) {\n this.setState({focused: focused - 1});\n } else {\n this.setState({focused: 0});\n if (page > 1) {\n this.goto_page(page - 1);\n }\n }\n }\n\n else if (direction == \"down\") {\n if (this.state.results.length == 0) {\n this.search(searchterm);\n }\n if (focused < results.length - 1) {\n this.setState({focused: focused + 1});\n } else {\n this.setState({focused: 0});\n if (page < pages) {\n this.goto_page(page + 1);\n }\n }\n }\n\n else if (direction == \"left\") {\n this.setState({focused: 0});\n if (page > 0) {\n this.goto_page(page - 1);\n }\n }\n\n else if (direction == \"right\") {\n this.setState({focused: 0});\n if (page < pages) {\n this.goto_page(page + 1);\n }\n }\n }\n\n /*\n * Toggle loading state\n *\n * @param {Boolean} toggle: The loading state to set\n * @returns {Boolean} toggle: The current loading state\n */\n toggle_loading(toggle) {\n if (toggle == null) {\n toggle = false;\n }\n this.setState({\n loading: toggle\n });\n return toggle;\n }\n\n /*\n * Set results data coming from `senaite.jsonapi`\n *\n * @param {Object} data: JSON search result object returned from `senaite.jsonapi`\n */\n set_results_data(data) {\n data = data || {};\n let items = data.items || [];\n\n let records = Object.assign(this.state.records, {})\n // update state records\n for (let item of items) {\n let uid = item.uid;\n records[uid] = item;\n }\n\n this.setState({\n records: records,\n results: items,\n count: data.count || 0,\n page: data.page || 1,\n pages: data.pages || 1,\n next_url: data.next || null,\n prev_url: data.previous || null,\n });\n }\n\n /*\n * Clear results from the state\n */\n clear_results() {\n this.setState({\n results: [],\n count: 0,\n page: 1,\n pages: 1,\n next_url: null,\n prev_url: null,\n });\n }\n\n /*\n * ReactJS event handler for keydown event\n */\n on_keydown(event){\n // clear results when ESC key is pressed\n if(event.keyCode === 27) {\n this.clear_results();\n }\n }\n\n /*\n * ReactJS event handler for click events\n */\n on_click(event) {\n // clear results when clicked outside of the widget\n let widget = this.props.root_el;\n let target = event.target;\n if (!widget.contains(target)) {\n this.clear_results();\n }\n }\n\n render() {\n return (\n
\n );\n }\n}\n\nexport default AddressField;\n","import React from \"react\";\nimport ReactDOM from \"react-dom\";\n\nimport AddressField from \"./AddressField.js\";\n\n\nclass Address extends React.Component {\n\n constructor(props) {\n super(props);\n this.state = {\n country: props.country,\n subdivision1: props.subdivision1,\n subdivision2: props.subdivision2,\n city: props.city,\n zip: props.zip,\n address: props.address,\n address_type: props.address_type,\n }\n\n // Event handlers\n this.on_country_change = this.on_country_change.bind(this);\n this.on_subdivision1_change = this.on_subdivision1_change.bind(this);\n this.on_subdivision2_change = this.on_subdivision2_change.bind(this);\n this.on_city_change = this.on_city_change.bind(this);\n this.on_zip_change = this.on_zip_change.bind(this);\n this.on_address_change = this.on_address_change.bind(this);\n }\n\n force_array(value) {\n if (!Array.isArray(value)) {\n value = [];\n }\n return value;\n }\n\n /**\n * Returns the list of first-level subdivisions of the current country,\n * sorted alphabetically\n */\n get_subdivisions1() {\n let country = this.state.country;\n return this.force_array(this.props.subdivisions1[country]);\n }\n\n /**\n * Returns the list of subdivisions of the current first-level subdivision,\n * sorted sorted alphabetically\n */\n get_subdivisions2() {\n let subdivision1 = this.state.subdivision1;\n return this.force_array(this.props.subdivisions2[subdivision1]);\n }\n\n get_label(key) {\n let country = this.state.country;\n let label = this.props.labels[country];\n if (label != null && label.constructor == Object && key in label) {\n label = label[key];\n } else {\n label = this.props.labels[key];\n }\n return label;\n }\n\n /** Event triggered when the value for Country selector changes. Updates the\n * selector of subdivisions (e.g. states) with the list of top-level\n * subdivisions for the selected country\n */\n on_country_change(event) {\n let value = event.currentTarget.value;\n console.debug(`Address::on_country_change: ${value}`);\n if (this.props.on_country_change) {\n this.props.on_country_change(value);\n }\n this.setState({\n country: value,\n subdivision1: \"\",\n subdivision2: \"\",\n });\n }\n\n /** Event triggered when the value for the Country first-level subdivision\n * (e.g. state) selector changes. Updates the selector of subdivisions (e.g.\n * districts) for the selected subdivision and country\n */\n on_subdivision1_change(event) {\n let value = event.currentTarget.value;\n console.debug(`Address::on_subdivision1_change: ${value}`);\n if (this.props.on_subdivision1_change) {\n let country = this.state.country\n this.props.on_subdivision1_change(country, value);\n }\n this.setState({\n subdivision1: value,\n subdivision2: \"\",\n });\n }\n\n /** Event triggered when the value for the second-level subdivision (e.g.\n * district) selector changes\n */\n on_subdivision2_change(event) {\n let value = event.currentTarget.value;\n console.debug(`Address::on_subdivision2_change: ${value}`);\n if (this.props.on_subdivision2_change) {\n this.props.on_subdivision2_change(value);\n }\n this.setState({subdivision2: value});\n }\n\n /** Event triggered when the value for the address field changes\n */\n on_address_change(event) {\n let value = event.currentTarget.value;\n this.setState({address: value});\n }\n\n /** Event triggered when the value for the zip field changes\n */\n on_zip_change(event) {\n let value = event.currentTarget.value;\n this.setState({zip: value});\n }\n\n /** Event triggered when the value for the city field changes\n */\n on_city_change(event) {\n let value = event.currentTarget.value;\n this.setState({city: value});\n }\n\n get_input_id(subfield) {\n let id = this.props.id;\n let index = this.props.index;\n return `${id}-${index}-${subfield}`\n }\n\n get_input_name(subfield) {\n let name = this.props.name;\n let index = this.props.index;\n return `${name}.${index}.${subfield}`\n }\n\n render() {\n return (\n
\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n
\n );\n }\n\n}\n\nexport default Address;","import React from \"react\"\nimport ReactDOM from \"react-dom\"\n\nimport AddressWidgetAPI from \"./api.js\"\nimport Address from \"./components/Address.js\"\n\n\nclass AddressWidgetController extends React.Component {\n\n constructor(props) {\n super(props);\n\n // Root input HTML element\n let el = props.root_el;\n\n this.state = {};\n\n // Data keys located at the root element\n // -> initial values are set from the widget class\n const data_keys = [\n \"id\",\n \"name\",\n \"items\",\n \"portal_url\",\n \"labels\",\n \"countries\",\n \"subdivisions1\",\n \"subdivisions2\",\n ];\n\n // Query data keys and set state with parsed JSON value\n for (let key of data_keys) {\n let value = el.dataset[key];\n this.state[key] = this.parse_json(value);\n }\n\n // Initialize communication API with the API URL\n this.api = new AddressWidgetAPI({\n portal_url: this.state.portal_url,\n });\n\n // Bind callbacks to current context\n this.on_country_change = this.on_country_change.bind(this);\n this.on_subdivision1_change = this.on_subdivision1_change.bind(this);\n\n return this;\n }\n\n parse_json(value) {\n try {\n return JSON.parse(value);\n } catch (error) {\n console.error(`Could not parse \"${value}\" to JSON`);\n }\n }\n\n /**\n * Event triggered when the user selects a country. Function fetches and\n * updates the geo mapping with the first level subdivisions for the selected\n * country if are not up-to-date yet. It also updates the label for the first\n * level subdivision in accordance.\n */\n on_country_change(country) {\n console.debug(`widget::on_country_change: ${country}`);\n let self = this;\n let promise = this.api.fetch_subdivisions(country);\n promise.then(function(data){\n\n // Update the label with the type of 1st-level subdivisions\n let labels = {...self.state.labels};\n if (data.length > 0) {\n labels[country][\"subdivision1\"] = data[0].type;\n }\n\n // Create a copy instead of modifying the existing dict from state var\n let subdivisions = {...self.state.subdivisions1};\n\n // Only interested in names, sorted alphabetically\n subdivisions[country] = data.map((x) => x.name).sort();\n\n // Update current state with the changes\n self.setState({\n subdivisions1: subdivisions,\n labels: labels,\n });\n });\n return promise;\n }\n\n /**\n * Event triggered when the user selects a first-level subdivision of a\n * country. Function fetches and updates the geo mapping with the second level\n * subdivisions for the selected subdivision if are not up-to-date. It also\n * updates the label for the second level subdivision in accordance.\n */\n on_subdivision1_change(country, subdivision) {\n console.debug(`widget::on_subdivision1_change: ${country}, ${subdivision}`);\n let self = this;\n let promise = this.api.fetch_subdivisions(subdivision);\n promise.then(function(data){\n\n // Update the label with the type of 1st-level subdivisions\n let labels = {...self.state.labels};\n if (data.length > 0) {\n labels[country][\"subdivision2\"] = data[0].type;\n }\n\n // Create a copy instead of modifying the existing dict from state var\n let subdivisions = {...self.state.subdivisions2};\n\n // Only interested in names, sorted alphabetically\n subdivisions[subdivision] = data.map((x) => x.name).sort();\n\n // Update current state with the changes\n self.setState({\n subdivisions2: subdivisions,\n labels: labels,\n });\n });\n return promise;\n }\n\n render_items() {\n let html_items = [];\n let items = this.state.items;\n for (const [index, item] of items.entries()) {\n let section_title = \"\";\n if (items.length > 1) {\n // Only render the title if more than one address\n section_title = (\n {this.state.labels[item.type]}\n )\n }\n\n html_items.push(\n
\n );\n }\n}\n\nexport default AddressWidgetController;\n","import $ from \"jquery\";\nimport I18N from \"./components/i18n.js\";\nimport {i18n, _t, _p} from \"./i18n-wrapper.js\"\nimport EditForm from \"./components/editform.js\"\nimport Site from \"./components/site.js\"\nimport Sidebar from \"./components/sidebar.js\"\nimport UIDReferenceWidgetController from \"./widgets/uidreferencewidget/widget.js\"\nimport AddressWidgetController from \"./widgets/addresswidget/widget.js\"\n\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n console.debug(\"*** SENAITE CORE JS LOADED ***\");\n\n // Initialize i18n message factories\n window.i18n = new I18N();\n window._t = _t;\n window._p = _p;\n\n // BBB: set global `portal_url` variable\n window.portal_url = document.body.dataset.portalUrl\n\n // TinyMCE\n tinymce.init({\n height: 300,\n paste_data_images: true,\n selector: \"textarea.mce_editable,div.ArchetypesRichWidget textarea,textarea[name='form.widgets.IRichTextBehavior.text'],textarea.richTextWidget\",\n plugins: [\"paste\", \"link\", \"fullscreen\", \"table\", \"code\"],\n content_css : \"/++plone++senaite.core.static/bundles/main.css\",\n })\n // /TinyMCE\n\n // Initialize Site\n window.site = new Site();\n\n // Initialize Sidebar\n window.sidebar = new Sidebar({\n \"el\": \"sidebar\",\n });\n\n // Ajax Edit Form Handler\n var form = new EditForm({\n form_selectors: [\n \"form[name='edit_form']\",\n \"form.senaite-ajax-form\",\n ],\n field_selectors: [\n \"input[type='text']\",\n \"input[type='number']\",\n \"input[type='checkbox']\",\n \"input[type='radio']\",\n \"input[type='file']\",\n \"select\",\n \"textarea\",\n ]\n })\n\n // Init Tooltips\n $(function () {\n $(\"[data-toggle='tooltip']\").tooltip();\n $(\"select.selectpicker\").selectpicker();\n });\n\n // Widgets\n window.widgets = {};\n // Referencewidgets\n var ref_widgets = document.getElementsByClassName(\"senaite-uidreference-widget-input\");\n for (let widget of ref_widgets) {\n let id = widget.dataset.id;\n let controller = ReactDOM.render(, widget);\n window.widgets[id] = controller;\n }\n // AddressWidget\n var address_widgets = document.getElementsByClassName(\"senaite-address-widget-input\");\n for (let widget of address_widgets) {\n let id = widget.dataset.id;\n let controller = ReactDOM.render(, widget);\n window.widgets[id] = controller;\n }\n\n // Workflow Menu Update for Ajax Transitions\n // https://github.com/senaite/senaite.app.listing/pull/87\n document.body.addEventListener(\"listing:submit\", (event) => {\n let menu = document.getElementById(\"plone-contentmenu-workflow\");\n // return immediately if no workflow menu is present\n if (menu === null ) {\n return false;\n }\n // get the base url from the `data-base-url` attribute\n let base_url = document.body.dataset.baseUrl;\n if (base_url === undefined) {\n // fallback to the current location URL\n base_url = location.href.split(\"#\")[0].split(\"?\")[0];\n }\n const request = new Request(base_url + \"/menu/workflow_menu\");\n fetch(request)\n .then((response) => {\n // we might get a 404 e.g. for WS /manage_results, but this is actually\n // desired. Otherwise, we would update the WF menu of the WS ...\n if (response.ok) {\n return response.text();\n }\n })\n .then((html) => {\n if (!html) {\n return;\n }\n let parser = new DOMParser();\n let doc = parser.parseFromString(html, \"text/html\");\n let el = doc.body.firstChild;\n menu.replaceWith(el);\n })\n });\n\n});\n"],"names":["module","exports","jQuery","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","n","getter","__esModule","d","a","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","self","this","baseUrl","$","attr","currentLanguage","split","length","toUpperCase","storage","catalogs","ttl","Date","now","valueOf","window","localStorage","JSON","e","configure","config","_setCatalog","domain","language","catalog","_storeCatalog","setItem","stringify","getUrl","loadCatalog","parseInt","getItem","parse","MessageFactory","msgid","keywords","msgstr","regexp","keyword","RegExp","replace","t","_t","i18n","I18N","p","_p","EditForm","assign","hooked_fields","on_mutated","bind","on_modified","on_submit","on_blur","on_click","on_change","init_forms","form_selectors","selector","form","document","querySelector","tagName","setup_form","watch_form","ajax_send","get_form_fields","field","hook_field","observe_mutations","addEventListener","hasAttribute","indexOf","is_button","is_input_button","is_text","is_textarea","is_select","is_radio","is_checkbox","concat","MutationObserver","mutations","event","CustomEvent","detail","dispatchEvent","observe","childList","subtree","mutation","target","added","closest","addedNodes","selectors","removedNodes","field_selectors","is_multiple_select","notify","ELEMENT_NODE","querySelectorAll","toggle","disabled","parent","css_class","classList","add","remove","message","setAttribute","existing_message","parentElement","innerHTML","div","createElement","className","appendChild","removeAttribute","msg","level","options","el","title","charAt","slice","firstElementChild","getElementById","flush","animation","autohide","delay","data","hide","show","readonly","editable","errors","messages","notifications","updates","html","attributes","name","error","record","rest","get_form_field_by_name","set_field_error","remove_field_error","add_statusmessage","add_notification","toast","toggle_field_visibility","set_field_readonly","set_field_editable","value","set_field_value","append","addAttribute","has_field_errors","toggle_submit","exact","fuzzy","FormData","forEach","fields","nodes","values","checked","selected","selectedOptions","Array","map","option","is_single_reference","getAttribute","is_multi_reference","uids","item","old_selected","sort","b","_a","toLowerCase","_b","selectedIndex","event_type","endpoint","get_field_name","get_field_value","view_url","body","dataset","viewUrl","ajax_url","payload","get_form_data","init","method","credentials","headers","token","ajax_request","entries","set","url","loading","request","Request","fetch","then","response","ok","json","Promise","reject","update_form","is_input","type","contains","is_reference","seen","handle_mutation","preventDefault","currentTarget","submitter","toggle_disable","ajax_submit","modified","fn","me","apply","arguments","Site","set_cookie","read_cookie","authenticator","auth","val","URLSearchParams","location","search","c","ca","i","cookie","substring","expires","setTime","getTime","toUTCString","Sidebar","tid","maximize","minimize","on_mouseenter","on_mouseleave","toggle_el","is_toggled","site","cookie_key","clearTimeout","setTimeout","timeout","React","ReactDOM","ReferenceWidgetAPI","props","api_url","on_api_error","get_api_url","get_csrf_token","query","params","isArray","depth","get_json","ReferenceField","state","input_field_ref","on_focus","on_keydown","on_keypress","on_clear_click","on_search_click","current","get_search_value","on_search","which","on_clear","on_arrow_key","on_enter","ref","onKeyDown","onKeyPress","onChange","onFocus","onBlur","placeholder","style","maxWidth","class","onClick","ReferenceResults","on_select","on_page","on_prev_page","on_next_page","on_close","columns","get_columns","column","label","results","get_results","minWidth","width","backgroundColor","zIndex","result","uid","align","push","rows","index","get_result_uid","focused","build_columns","searchterm","get_column_names","highlighted","highlight","dangerouslySetInnerHTML","__html","used","text","rx","replaceAll","m","pages","page","cls","join","next_url","prev_url","has_results","get_style","position","top","right","build_close_button","build_header_columns","build_rows","build_prev_button","build_pages","build_next_button","References","on_deselect","template","context","display_template","records","interpolate","items","get_selected_uids","render_display_template","build_selected_items","UIDReferenceWidgetController","count","b_start","root_el","parse_json","api","goto_page","clear_results","select","select_focused","deselect","navigate_results","widget","removeEventListener","multi_valued","q","limit","complete","url_params","make_query","toggle_loading","promise","set_results_data","fetch_results","setState","at","pos","splice","direction","next","previous","keyCode","is_disabled","AddressWidgetAPI","portal_url","get_url","LocationSelector","locations","id","render_options","AddressField","visible","is_location_selector","is_visible","for","render_element","Address","country","subdivision1","subdivision2","city","zip","address","address_type","on_country_change","on_subdivision1_change","on_subdivision2_change","on_city_change","on_zip_change","on_address_change","force_array","subdivisions1","subdivisions2","labels","constructor","subfield","get_input_id","get_input_name","countries","get_label","get_subdivisions1","get_subdivisions2","AddressWidgetController","fetch_subdivisions","subdivisions","x","subdivision","html_items","section_title","render_items","portalUrl","tinymce","height","paste_data_images","plugins","content_css","sidebar","tooltip","selectpicker","widgets","getElementsByClassName","controller","render","menu","base_url","href","DOMParser","parseFromString","firstChild","replaceWith"],"sourceRoot":""}
\ No newline at end of file
diff --git a/webpack/app/senaite.core.js b/webpack/app/senaite.core.js
index 55db1a37fd..6aa7991e32 100644
--- a/webpack/app/senaite.core.js
+++ b/webpack/app/senaite.core.js
@@ -76,4 +76,39 @@ document.addEventListener("DOMContentLoaded", () => {
let controller = ReactDOM.render(, widget);
window.widgets[id] = controller;
}
+
+ // Workflow Menu Update for Ajax Transitions
+ // https://github.com/senaite/senaite.app.listing/pull/87
+ document.body.addEventListener("listing:submit", (event) => {
+ let menu = document.getElementById("plone-contentmenu-workflow");
+ // return immediately if no workflow menu is present
+ if (menu === null ) {
+ return false;
+ }
+ // get the base url from the `data-base-url` attribute
+ let base_url = document.body.dataset.baseUrl;
+ if (base_url === undefined) {
+ // fallback to the current location URL
+ base_url = location.href.split("#")[0].split("?")[0];
+ }
+ const request = new Request(base_url + "/menu/workflow_menu");
+ fetch(request)
+ .then((response) => {
+ // we might get a 404 when the current page URL ends with a view, e.g.
+ // `WS-ID/manage_results` or `CLIENT-ID/multi_results` etc.
+ if (response.ok) {
+ return response.text();
+ }
+ })
+ .then((html) => {
+ if (!html) {
+ return;
+ }
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(html, "text/html");
+ let el = doc.body.firstChild;
+ menu.replaceWith(el);
+ })
+ });
+
});