diff --git a/lib/components/Button/SplitButton.d.ts b/lib/components/Button/SplitButton.d.ts new file mode 100644 index 00000000..43243a7d --- /dev/null +++ b/lib/components/Button/SplitButton.d.ts @@ -0,0 +1,2 @@ +declare const WrappedSplitButton: any; +export default WrappedSplitButton; diff --git a/lib/components/Button/SplitButton.js b/lib/components/Button/SplitButton.js new file mode 100644 index 00000000..81f2ad19 --- /dev/null +++ b/lib/components/Button/SplitButton.js @@ -0,0 +1,194 @@ +"use strict"; + +function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _react = _interopRequireWildcard(require("react")); + +var _Grid = _interopRequireDefault(require("@material-ui/core/Grid")); + +var _Button = _interopRequireDefault(require("@material-ui/core/Button")); + +var _ButtonGroup = _interopRequireDefault(require("@material-ui/core/ButtonGroup")); + +var _ClickAwayListener = _interopRequireDefault(require("@material-ui/core/ClickAwayListener")); + +var _Grow = _interopRequireDefault(require("@material-ui/core/Grow")); + +var _Paper = _interopRequireDefault(require("@material-ui/core/Paper")); + +var _Popper = _interopRequireDefault(require("@material-ui/core/Popper")); + +var _MenuItem = _interopRequireDefault(require("@material-ui/core/MenuItem")); + +var _MenuList = _interopRequireDefault(require("@material-ui/core/MenuList")); + +var _ArrowDropDown = _interopRequireDefault(require("@material-ui/icons/ArrowDropDown")); + +var _Theme = _interopRequireDefault(require("../Theme/Theme")); + +var _typeUtil = require("../../util/typeUtil"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } + +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { var _i = arr && (typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]); if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + +var SplitButton = function SplitButton(props) { + var name = props.name, + options = props.options, + selectedOption = props.selectedOption, + selectedOptionDisplayCallback = props.selectedOptionDisplayCallback, + onClick = props.onClick, + onChange = props.onChange, + buttonGroupProps = props.buttonGroupProps, + buttonMenuProps = props.buttonMenuProps, + buttonProps = props.buttonProps; + + var _useState = (0, _react.useState)(false), + _useState2 = _slicedToArray(_useState, 2), + open = _useState2[0], + setOpen = _useState2[1]; + + var _useState3 = (0, _react.useState)(selectedOption), + _useState4 = _slicedToArray(_useState3, 2), + stateSelectedOption = _useState4[0], + setStateSelectedOption = _useState4[1]; + + var anchorRef = (0, _react.useRef)(null); + var appliedButtonGroupProps = { + variant: 'outlined', + color: 'primary' + }; + + if ((0, _typeUtil.exists)(buttonGroupProps)) { + appliedButtonGroupProps = buttonGroupProps; + } + + var appliedButtonProps = { + color: 'primary', + size: 'small' + }; + + if ((0, _typeUtil.exists)(buttonProps)) { + appliedButtonProps = buttonProps; + } + + var appliedButtonMenuProps = { + color: 'primary', + size: 'small' + }; + + if ((0, _typeUtil.exists)(buttonMenuProps)) { + appliedButtonMenuProps = buttonMenuProps; + } + + (0, _react.useEffect)(function () { + if (selectedOption === stateSelectedOption) return; + setStateSelectedOption(selectedOption); + }, [selectedOption, stateSelectedOption]); + + var handleClick = function handleClick() { + onClick(stateSelectedOption); + }; + + var handleMenuItemClick = function handleMenuItemClick(event, index) { + setStateSelectedOption(options[index]); + onChange(options[index]); + setOpen(false); + }; + + var handleToggle = function handleToggle() { + setOpen(function (prevOpen) { + return !prevOpen; + }); + }; + + var handleClose = function handleClose(event) { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + + setOpen(false); + }; + + var renderSelectedOption = function renderSelectedOption() { + if ((0, _typeUtil.exists)(selectedOptionDisplayCallback)) { + // eslint-disable-next-line max-len + return selectedOptionDisplayCallback(stateSelectedOption); + } + + return stateSelectedOption; + }; + + return /*#__PURE__*/_react.default.createElement(_Grid.default, { + container: true, + direction: "column", + alignItems: "center" + }, /*#__PURE__*/_react.default.createElement(_Grid.default, { + item: true, + xs: 12 + }, /*#__PURE__*/_react.default.createElement(_ButtonGroup.default, _extends({ + "aria-label": "".concat(name, "-split-button") + }, appliedButtonGroupProps, { + ref: anchorRef + }), /*#__PURE__*/_react.default.createElement(_Button.default, _extends({}, appliedButtonProps, { + onClick: handleClick + }), renderSelectedOption()), /*#__PURE__*/_react.default.createElement(_Button.default, _extends({ + "aria-controls": open ? "".concat(name, "-split-button-menu") : undefined, + "aria-expanded": open ? 'true' : undefined, + "aria-label": "".concat(name, "-split-button-select"), + "aria-haspopup": "menu" + }, appliedButtonMenuProps, { + onClick: handleToggle + }), /*#__PURE__*/_react.default.createElement(_ArrowDropDown.default, null))), /*#__PURE__*/_react.default.createElement(_Popper.default, { + transition: true, + anchorEl: anchorRef.current, + open: open, + role: undefined + }, function (_ref) { + var TransitionProps = _ref.TransitionProps, + placement = _ref.placement; + return /*#__PURE__*/_react.default.createElement(_Grow.default, _extends({}, TransitionProps, { + style: { + transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' + } + }), /*#__PURE__*/_react.default.createElement(_Paper.default, null, /*#__PURE__*/_react.default.createElement(_ClickAwayListener.default, { + onClickAway: handleClose + }, /*#__PURE__*/_react.default.createElement(_MenuList.default, { + id: "".concat(name, "-split-button-menu") + }, options.map(function (option, index) { + return /*#__PURE__*/_react.default.createElement(_MenuItem.default, { + key: option, + selected: option === stateSelectedOption, + onClick: function onClick(event) { + return handleMenuItemClick(event, index); + } + }, option); + }))))); + }))); +}; + +var WrappedSplitButton = _Theme.default.getWrappedComponent(SplitButton); + +var _default = WrappedSplitButton; +exports.default = _default; \ No newline at end of file diff --git a/lib/components/Button/index.d.ts b/lib/components/Button/index.d.ts new file mode 100644 index 00000000..3220a80f --- /dev/null +++ b/lib/components/Button/index.d.ts @@ -0,0 +1 @@ +export { default } from "./SplitButton"; diff --git a/lib/components/Button/index.js b/lib/components/Button/index.js new file mode 100644 index 00000000..451c5e72 --- /dev/null +++ b/lib/components/Button/index.js @@ -0,0 +1,15 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "default", { + enumerable: true, + get: function get() { + return _SplitButton.default; + } +}); + +var _SplitButton = _interopRequireDefault(require("./SplitButton")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } \ No newline at end of file diff --git a/lib/components/Button/package.json b/lib/components/Button/package.json new file mode 100644 index 00000000..7ae51b6e --- /dev/null +++ b/lib/components/Button/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "name": "neon-buttons", + "main": "./SplitButton.tsx", + "module": "./SplitButton.tsx" +} diff --git a/lib/components/Documents/DocumentList.d.ts b/lib/components/Documents/DocumentList.d.ts new file mode 100644 index 00000000..9e3f1fb1 --- /dev/null +++ b/lib/components/Documents/DocumentList.d.ts @@ -0,0 +1,11 @@ +import { DocumentListItemModel } from './DocumentListItem'; +import { Nullable } from '../../types/core'; +export interface DocumentListProps { + documents: DocumentListItemModel[]; + makeDownloadableLink: Nullable; + enableDownloadButton: Nullable; + fetchVariants: Nullable; + enableVariantChips: Nullable; +} +declare const WrappedDocumentList: any; +export default WrappedDocumentList; diff --git a/lib/components/Documents/DocumentList.js b/lib/components/Documents/DocumentList.js new file mode 100644 index 00000000..b9dc8470 --- /dev/null +++ b/lib/components/Documents/DocumentList.js @@ -0,0 +1,74 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _react = _interopRequireDefault(require("react")); + +var _List = _interopRequireDefault(require("@material-ui/core/List")); + +var _styles = require("@material-ui/core/styles"); + +var _DocumentListItem = _interopRequireDefault(require("./DocumentListItem")); + +var _Theme = _interopRequireDefault(require("../Theme/Theme")); + +var _WarningCard = _interopRequireDefault(require("../Card/WarningCard")); + +var _typeUtil = require("../../util/typeUtil"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var useStyles = (0, _styles.makeStyles)(function (muiTheme) { + return (// eslint-disable-next-line implicit-arrow-linebreak + (0, _styles.createStyles)({ + list: { + paddingTop: muiTheme.spacing(0) + } + }) + ); +}); + +var DocumentList = function DocumentList(props) { + var classes = useStyles(_Theme.default); + var documents = props.documents, + makeDownloadableLink = props.makeDownloadableLink, + enableDownloadButton = props.enableDownloadButton, + fetchVariants = props.fetchVariants, + enableVariantChips = props.enableVariantChips; + + if (!(0, _typeUtil.existsNonEmpty)(documents)) { + return /*#__PURE__*/_react.default.createElement("div", { + className: classes.container + }, /*#__PURE__*/_react.default.createElement(_WarningCard.default, { + title: "No Documents", + message: "No documents available to display" + })); + } + + var renderDocuments = function renderDocuments() { + return documents.map(function (document, index) { + return /*#__PURE__*/_react.default.createElement(_DocumentListItem.default, { + key: document.name, + id: index, + document: document, + makeDownloadableLink: makeDownloadableLink === true, + enableDownloadButton: enableDownloadButton, + fetchVariants: fetchVariants, + enableVariantChips: enableVariantChips + }); + }); + }; + + return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(_List.default, { + dense: true, + className: classes.list + }, renderDocuments())); +}; + +var WrappedDocumentList = _Theme.default.getWrappedComponent(DocumentList); + +var _default = WrappedDocumentList; +exports.default = _default; \ No newline at end of file diff --git a/lib/components/Documents/DocumentListItem.d.ts b/lib/components/Documents/DocumentListItem.d.ts new file mode 100644 index 00000000..11c07fd7 --- /dev/null +++ b/lib/components/Documents/DocumentListItem.d.ts @@ -0,0 +1,17 @@ +import React from 'react'; +import { Nullable, Undef } from '../../types/core'; +import { NeonDocument } from '../../types/neonApi'; +export interface DocumentListItemModel extends NeonDocument { + variants: NeonDocument[]; +} +export interface DocumentListItemProps { + id: number; + document: DocumentListItemModel; + makeDownloadableLink: boolean; + enableDownloadButton: Nullable; + fetchVariants: Nullable; + enableVariantChips: Nullable; + containerComponent: Undef>>; +} +declare const WrappedDocumentListItem: any; +export default WrappedDocumentListItem; diff --git a/lib/components/Documents/DocumentListItem.js b/lib/components/Documents/DocumentListItem.js new file mode 100644 index 00000000..28be846b --- /dev/null +++ b/lib/components/Documents/DocumentListItem.js @@ -0,0 +1,672 @@ +"use strict"; + +function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _react = _interopRequireWildcard(require("react")); + +var _rxjs = require("rxjs"); + +var _operators = require("rxjs/operators"); + +var _cloneDeep = _interopRequireDefault(require("lodash/cloneDeep")); + +var _Button = _interopRequireDefault(require("@material-ui/core/Button")); + +var _Chip = _interopRequireDefault(require("@material-ui/core/Chip")); + +var _CircularProgress = _interopRequireDefault(require("@material-ui/core/CircularProgress")); + +var _IconButton = _interopRequireDefault(require("@material-ui/core/IconButton")); + +var _ListItem = _interopRequireDefault(require("@material-ui/core/ListItem")); + +var _ListItemIcon = _interopRequireDefault(require("@material-ui/core/ListItemIcon")); + +var _ListItemText = _interopRequireDefault(require("@material-ui/core/ListItemText")); + +var _ListItemSecondaryAction = _interopRequireDefault(require("@material-ui/core/ListItemSecondaryAction")); + +var _Tooltip = _interopRequireDefault(require("@material-ui/core/Tooltip")); + +var _Typography = _interopRequireDefault(require("@material-ui/core/Typography")); + +var _styles = require("@material-ui/core/styles"); + +var _SaveAlt = _interopRequireDefault(require("@material-ui/icons/SaveAlt")); + +var _NeonApi = _interopRequireDefault(require("../NeonApi")); + +var _SplitButton = _interopRequireDefault(require("../Button/SplitButton")); + +var _Theme = _interopRequireDefault(require("../Theme/Theme")); + +var _WarningCard = _interopRequireDefault(require("../Card/WarningCard")); + +var _DocumentParser = _interopRequireDefault(require("../../parser/DocumentParser")); + +var _DocumentService = _interopRequireDefault(require("../../service/DocumentService")); + +var _typeUtil = require("../../util/typeUtil"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { var _i = arr && (typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]); if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + +function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } + +var COMPONENT_XS_UPPER = 480; +var COMPONENT_SM_UPPER = 805; +var useStyles = (0, _styles.makeStyles)(function (muiTheme) { + return (0, _styles.createStyles)({ + listItemContainer: { + display: 'flex' + }, + listItem: { + display: 'flex', + wordBreak: 'break-word', + paddingLeft: muiTheme.spacing(1), + '& p': { + marginTop: muiTheme.spacing(0.5), + '& > span > span': { + whiteSpace: 'nowrap' + } + } + }, + listItemSecondarySpacer: { + margin: muiTheme.spacing(0, 2), + color: muiTheme.palette.grey[200] + }, + listItemIcon: { + minWidth: muiTheme.spacing(4), + marginRight: muiTheme.spacing(1) + }, + fileTypeChip: { + marginRight: '5px', + '&:last-child': { + marginRight: '0px' + } + }, + fileTypeChipSelected: { + marginRight: '5px', + fontWeight: 500 + }, + variantFetchingLabel: { + lineHeight: '24px' + }, + variantFetchingProgress: { + marginRight: '36px', + marginLeft: '36px' + }, + downloadErrorContainer: { + marginTop: muiTheme.spacing(2) + } + }); +}); +var useListItemSecondaryActionStyles = (0, _styles.makeStyles)(function (muiTheme) { + return (// eslint-disable-next-line implicit-arrow-linebreak + (0, _styles.createStyles)({ + root: { + display: 'flex', + alignItems: 'center', + position: 'unset', + transform: 'unset', + top: 'unset', + right: 'unset', + whiteSpace: 'nowrap' + } + }) + ); +}); +var ActionTypes; + +(function (ActionTypes) { + ActionTypes["FETCH_VARIANTS_STARTED"] = "FETCH_VARIANTS_STARTED"; + ActionTypes["FETCH_VARIANTS_FAILED"] = "FETCH_VARIANTS_FAILED"; + ActionTypes["FETCH_VARIANTS_SUCCEEDED"] = "FETCH_VARIANTS_SUCCEEDED"; + ActionTypes["SET_SELECTED_VARIANT"] = "SET_SELECTED_VARIANT"; + ActionTypes["DOWNLOAD_IDLE"] = "DOWNLOAD_IDLE"; + ActionTypes["DOWNLOAD_STARTED"] = "DOWNLOAD_STARTED"; + ActionTypes["DOWNLOAD_FAILED"] = "DOWNLOAD_FAILED"; +})(ActionTypes || (ActionTypes = {})); + +var ActionCreator = { + fetchVariantsStarted: function fetchVariantsStarted() { + return { + type: ActionTypes.FETCH_VARIANTS_STARTED + }; + }, + fetchVariantsFailed: function fetchVariantsFailed(error) { + return { + type: ActionTypes.FETCH_VARIANTS_FAILED, + error: error + }; + }, + fetchVariantsSucceeded: function fetchVariantsSucceeded(variants) { + return { + type: ActionTypes.FETCH_VARIANTS_SUCCEEDED, + variants: variants + }; + }, + setSelectedVariant: function setSelectedVariant(variant) { + return { + type: ActionTypes.SET_SELECTED_VARIANT, + variant: variant + }; + }, + downloadIdle: function downloadIdle() { + return { + type: ActionTypes.DOWNLOAD_IDLE + }; + }, + downloadStarted: function downloadStarted() { + return { + type: ActionTypes.DOWNLOAD_STARTED + }; + }, + downloadFailed: function downloadFailed() { + return { + type: ActionTypes.DOWNLOAD_FAILED + }; + } +}; +var FetchStatus; + +(function (FetchStatus) { + FetchStatus["AWAITING_CALL"] = "AWAITING_CALL"; + FetchStatus["FETCHING"] = "FETCHING"; + FetchStatus["ERROR"] = "ERROR"; + FetchStatus["SUCCESS"] = "SUCCESS"; + FetchStatus["IDLE"] = "IDLE"; +})(FetchStatus || (FetchStatus = {})); + +var DEFAULT_STATE = { + fetchVariants: { + status: FetchStatus.IDLE, + error: null + }, + variants: [], + selectedVariant: null, + downloadStatus: FetchStatus.IDLE +}; + +var documentListItemReducer = function documentListItemReducer(state, action) { + var newState = _extends({}, state); + + var fetchVariantFailedAction; + var fetchVariantSucceededAction; + var setSelectedVariantAction; + + switch (action.type) { + case ActionTypes.FETCH_VARIANTS_STARTED: + newState.fetchVariants.status = FetchStatus.FETCHING; + return newState; + + case ActionTypes.FETCH_VARIANTS_FAILED: + fetchVariantFailedAction = action; + newState.fetchVariants.status = FetchStatus.ERROR; + newState.fetchVariants.error = fetchVariantFailedAction.error; + return newState; + + case ActionTypes.FETCH_VARIANTS_SUCCEEDED: + fetchVariantSucceededAction = action; + newState.fetchVariants.status = FetchStatus.SUCCESS; + newState.variants = fetchVariantSucceededAction.variants; + + if (!(0, _typeUtil.exists)(newState.selectedVariant) && (0, _typeUtil.existsNonEmpty)(newState.variants)) { + // eslint-disable-next-line prefer-destructuring + newState.selectedVariant = newState.variants[0]; + } + + return newState; + + case ActionTypes.SET_SELECTED_VARIANT: + setSelectedVariantAction = action; + newState.selectedVariant = setSelectedVariantAction.variant; + return newState; + + case ActionTypes.DOWNLOAD_IDLE: + newState.downloadStatus = FetchStatus.IDLE; + return newState; + + case ActionTypes.DOWNLOAD_STARTED: + newState.downloadStatus = FetchStatus.FETCHING; + return newState; + + case ActionTypes.DOWNLOAD_FAILED: + newState.downloadStatus = FetchStatus.ERROR; + return newState; + + default: + return newState; + } +}; + +var DocumentListItem = function DocumentListItem(props) { + var id = props.id, + document = props.document, + makeDownloadableLink = props.makeDownloadableLink, + enableDownloadButton = props.enableDownloadButton, + fetchVariants = props.fetchVariants, + enableVariantChips = props.enableVariantChips, + containerComponent = props.containerComponent; + var classes = useStyles(_Theme.default); + var listItemSecondaryActionClasses = useListItemSecondaryActionStyles(_Theme.default); + var containerRef = (0, _react.useRef)(); + + var _useState = (0, _react.useState)(0), + _useState2 = _slicedToArray(_useState, 2), + componentWidth = _useState2[0], + setComponentWidth = _useState2[1]; + + var atComponentXs = false; + var atComponentSm = false; + + if (componentWidth > 0) { + atComponentXs = componentWidth <= COMPONENT_XS_UPPER; + atComponentSm = componentWidth >= COMPONENT_XS_UPPER && componentWidth < COMPONENT_SM_UPPER; + } + + var _useReducer = (0, _react.useReducer)(documentListItemReducer, (0, _cloneDeep.default)(DEFAULT_STATE)), + _useReducer2 = _slicedToArray(_useReducer, 2), + state = _useReducer2[0], + dispatch = _useReducer2[1]; + + var fetchVariantStatus = state.fetchVariants.status, + stateVariants = state.variants, + stateSelectedVariant = state.selectedVariant, + downloadStatus = state.downloadStatus; + var hasDocument = (0, _typeUtil.exists)(document); + var hasProvidedVariants = hasDocument && (0, _typeUtil.existsNonEmpty)(document.variants); + + var isQsg = _DocumentService.default.isQuickStartGuide(document); + + var appliedFetchVariants = !hasProvidedVariants && fetchVariants === true; + var requireVariantFetch = isQsg && appliedFetchVariants && fetchVariantStatus === FetchStatus.IDLE; + (0, _react.useEffect)(function () { + if (!hasDocument || !requireVariantFetch) { + return; + } + + var qsgParsedName = _DocumentService.default.parseQuickStartGuideName(document.name); + + if (!(0, _typeUtil.exists)(qsgParsedName)) { + return; + } + + var coercedParsedName = qsgParsedName; + + var variantObs = _NeonApi.default.getQuickStartGuideDetailObservable(coercedParsedName.matchedName, coercedParsedName.matchedVersion).pipe((0, _operators.map)(function (response) { + if (!(0, _typeUtil.exists)(response) || !(0, _typeUtil.exists)(response.data)) { + dispatch(ActionCreator.fetchVariantsFailed('Failed to fetch variants')); + return (0, _rxjs.of)(false); + } + + var qsgResponse = _DocumentParser.default.parseQuickStartGuideVersionResponse(response); + + if (!(0, _typeUtil.exists)(qsgResponse)) { + dispatch(ActionCreator.fetchVariantsFailed('Failed to fetch variants')); + return (0, _rxjs.of)(false); + } + + var coercedQsgDocumentResponse = qsgResponse; + var qsgDocuments = coercedQsgDocumentResponse.documents; + + var variantDocuments = _DocumentService.default.transformQuickStartGuideDocuments(qsgDocuments); + + if (!(0, _typeUtil.existsNonEmpty)(variantDocuments)) { + dispatch(ActionCreator.fetchVariantsFailed('Failed to fetch variants')); + return (0, _rxjs.of)(false); + } + + dispatch(ActionCreator.fetchVariantsSucceeded(variantDocuments)); + return (0, _rxjs.of)(true); + }), (0, _operators.catchError)(function (error) { + dispatch(ActionCreator.fetchVariantsFailed(error)); + return (0, _rxjs.of)(false); + })); + + dispatch(ActionCreator.fetchVariantsStarted()); + variantObs.subscribe(); + }, [hasDocument, requireVariantFetch, document]); + var handleSelectedVariantChanged = (0, _react.useCallback)(function (selectedVariantCb) { + dispatch(ActionCreator.setSelectedVariant(selectedVariantCb)); + }, [dispatch]); + var handleDownloadIdle = (0, _react.useCallback)(function () { + dispatch(ActionCreator.downloadIdle()); + }, [dispatch]); + var handleDownloadStarted = (0, _react.useCallback)(function () { + dispatch(ActionCreator.downloadStarted()); + }, [dispatch]); + var handleDownloadFailed = (0, _react.useCallback)(function () { + dispatch(ActionCreator.downloadFailed()); + }, [dispatch]); + var handleResizeCb = (0, _react.useCallback)(function () { + var container = containerRef.current; + + if (!container) { + return; + } + + if (container.clientWidth === componentWidth) { + return; + } + + setComponentWidth(container.clientWidth); + }, [containerRef, componentWidth, setComponentWidth]); + (0, _react.useLayoutEffect)(function () { + var element = containerRef.current; + + if (!element) { + return function () {}; + } + + handleResizeCb(); + + if (typeof ResizeObserver !== 'function') { + window.addEventListener('resize', handleResizeCb); + return function () { + window.removeEventListener('resize', handleResizeCb); + }; + } + + var resizeObserver = new ResizeObserver(handleResizeCb); + resizeObserver.observe(element); + return function () { + if (!resizeObserver) { + return; + } + + resizeObserver.disconnect(); + resizeObserver = null; + }; + }, [containerRef, handleResizeCb]); + + if (!hasDocument) { + return null; + } + + var isFetchingVariants = fetchVariantStatus === FetchStatus.FETCHING; + var appliedDocument = (0, _typeUtil.exists)(stateSelectedVariant) ? stateSelectedVariant : document; + var appliedVariants = appliedFetchVariants ? stateVariants : document.variants; + var hasAppliedVariants = (0, _typeUtil.existsNonEmpty)(appliedVariants); + var isDownloading = downloadStatus === FetchStatus.FETCHING; + var isDownloadError = downloadStatus === FetchStatus.ERROR; + + var documentType = _DocumentService.default.resolveDocumentType(appliedDocument); + + var typeTitle = documentType.title, + TypeIcon = documentType.Icon; + var typeTitleString = typeTitle(appliedDocument.type); + var primary = (0, _typeUtil.isStringNonEmpty)(appliedDocument.description) ? appliedDocument.description : /*#__PURE__*/_react.default.createElement("i", null, "No description"); + + var spacer = /*#__PURE__*/_react.default.createElement("span", { + className: classes.listItemSecondarySpacer + }, "|"); + + var renderTypes = function renderTypes() { + if (!(enableVariantChips === true)) { + return /*#__PURE__*/_react.default.createElement("span", { + title: "file type: ".concat(typeTitleString) + }, typeTitleString); + } + + if (isFetchingVariants) { + return /*#__PURE__*/_react.default.createElement(_Typography.default, { + variant: "caption", + className: classes.variantFetchingLabel + }, "Determining variants..."); + } + + if (!hasAppliedVariants) { + return /*#__PURE__*/_react.default.createElement(_Chip.default, { + key: appliedDocument.name, + variant: undefined, + className: classes.fileTypeChipSelected, + style: { + marginRight: '0px' + }, + component: "span", + size: "small", + label: typeTitleString + }); + } + + return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, appliedVariants.map(function (variant, index) { + var variantTypeTitleString = _DocumentService.default.getDocumentTypeTitle(variant); + + var isSelected = appliedDocument.name === variant.name; + var isLast = index === appliedVariants.length - 1; + return /*#__PURE__*/_react.default.createElement(_Chip.default, { + key: variant.name, + variant: isSelected ? undefined : 'outlined', + className: isSelected ? classes.fileTypeChipSelected : classes.fileTypeChip, + style: { + marginRight: isLast ? '0px' : undefined + }, + component: "span", + size: "small", + label: variantTypeTitleString, + disabled: isDownloading || isDownloadError, + onClick: function onClick(event) { + handleSelectedVariantChanged(variant); + } + }); + })); + }; + + var renderSecondaryItem = function renderSecondaryItem() { + var sizeDisplay = /*#__PURE__*/_react.default.createElement("span", null, /*#__PURE__*/_react.default.createElement("i", null, "n/a")); + + if (appliedDocument.size) { + sizeDisplay = /*#__PURE__*/_react.default.createElement("span", { + title: "file size: ".concat(_DocumentService.default.formatBytes(appliedDocument.size)) + }, _DocumentService.default.formatBytes(appliedDocument.size)); + } + + var fileNumber = /*#__PURE__*/_react.default.createElement("span", { + style: { + whiteSpace: 'break-spaces' + }, + title: "file number: ".concat(appliedDocument.name) + }, appliedDocument.name); + + if (atComponentSm || atComponentXs) { + if (hasAppliedVariants) { + return /*#__PURE__*/_react.default.createElement("span", null, fileNumber, /*#__PURE__*/_react.default.createElement("span", { + style: { + height: '5px', + display: 'block' + } + }), renderTypes(), /*#__PURE__*/_react.default.createElement("span", { + style: { + height: '5px', + display: 'block' + } + }), sizeDisplay); + } + + return /*#__PURE__*/_react.default.createElement("span", null, fileNumber, /*#__PURE__*/_react.default.createElement("span", { + style: { + height: '5px', + display: 'block' + } + }), renderTypes(), spacer, sizeDisplay); + } + + return /*#__PURE__*/_react.default.createElement("span", null, renderTypes(), spacer, sizeDisplay, spacer, fileNumber); + }; + + var renderAction = function renderAction() { + if (!(enableDownloadButton === true)) return null; + + if (isFetchingVariants) { + return /*#__PURE__*/_react.default.createElement(_ListItemSecondaryAction.default, { + classes: listItemSecondaryActionClasses + }, /*#__PURE__*/_react.default.createElement(_CircularProgress.default, { + size: 36, + className: classes.variantFetchingProgress + })); + } + + if (atComponentXs) { + return /*#__PURE__*/_react.default.createElement(_ListItemSecondaryAction.default, { + classes: listItemSecondaryActionClasses + }, /*#__PURE__*/_react.default.createElement(_Tooltip.default, { + placement: "top", + title: "Download ".concat(appliedDocument.name) + }, /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(_IconButton.default, { + color: "primary", + disabled: isDownloading || isDownloadError, + onClick: function onClick() { + handleDownloadStarted(); + + _DocumentService.default.downloadDocument(appliedDocument, function (downloadDoc) { + return handleDownloadIdle(); + }, function (downloadDoc) { + return handleDownloadFailed(); + }); + } + }, isDownloading ? /*#__PURE__*/_react.default.createElement(_CircularProgress.default, { + size: 18 + }) : /*#__PURE__*/_react.default.createElement(_SaveAlt.default, { + fontSize: "small" + }))))); + } + + var button = !hasAppliedVariants ? /*#__PURE__*/_react.default.createElement(_Button.default, { + variant: "outlined", + disabled: isDownloading || isDownloadError, + startIcon: isDownloading ? /*#__PURE__*/_react.default.createElement(_CircularProgress.default, { + size: 18 + }) : /*#__PURE__*/_react.default.createElement(_SaveAlt.default, { + fontSize: "small" + }), + onClick: function onClick() { + handleDownloadStarted(); + + _DocumentService.default.downloadDocument(appliedDocument, function (downloadDoc) { + return handleDownloadIdle(); + }, function (downloadDoc) { + return handleDownloadFailed(); + }); + } + }, "Download") : /*#__PURE__*/_react.default.createElement(_SplitButton.default, { + name: "".concat(appliedDocument.name, "-document-list-item-download-split-button"), + selectedOption: "".concat(typeTitleString), + selectedOptionDisplayCallback: function selectedOptionDisplayCallback(selectedOption) { + return "Download ".concat(selectedOption); + }, + options: appliedVariants.map(function (variant) { + return _DocumentService.default.getDocumentTypeTitle(variant); + }), + onClick: function onClick(option) { + var nextSelectedVariant = _DocumentService.default.findFirstByDocumentTypeTitle(appliedVariants, option); + + if ((0, _typeUtil.exists)(nextSelectedVariant)) { + var coercedDownloadVariant = nextSelectedVariant; + handleDownloadStarted(); + + _DocumentService.default.downloadDocument(coercedDownloadVariant, function (downloadDoc) { + return handleDownloadIdle(); + }, function (downloadDoc) { + return handleDownloadFailed(); + }); + } + }, + onChange: function onChange(option) { + var nextSelectedVariant = _DocumentService.default.findFirstByDocumentTypeTitle(appliedVariants, option); + + if ((0, _typeUtil.exists)(nextSelectedVariant)) { + handleSelectedVariantChanged(nextSelectedVariant); + } + }, + buttonGroupProps: { + size: 'small', + variant: 'outlined', + color: 'primary' + }, + buttonMenuProps: { + size: 'small', + color: 'primary', + disabled: isDownloading || isDownloadError + }, + buttonProps: { + size: 'small', + color: 'primary', + disabled: isDownloading || isDownloadError, + startIcon: isDownloading ? /*#__PURE__*/_react.default.createElement(_CircularProgress.default, { + size: 18 + }) : /*#__PURE__*/_react.default.createElement(_SaveAlt.default, { + fontSize: "small" + }) + } + }); + return /*#__PURE__*/_react.default.createElement(_ListItemSecondaryAction.default, { + classes: listItemSecondaryActionClasses + }, button); + }; + + var renderDownloadError = function renderDownloadError() { + if (!isDownloadError) return null; + return /*#__PURE__*/_react.default.createElement("div", { + className: classes.downloadErrorContainer + }, /*#__PURE__*/_react.default.createElement(_WarningCard.default, { + title: "Document download is currently unavailable for \"".concat(appliedDocument.name, ".\""), + onActionClick: function onActionClick() { + return handleDownloadIdle(); + } + })); + }; + + return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_ListItem.default, { + ref: containerRef, + key: id, + className: classes.listItem, + component: "div", + ContainerComponent: containerComponent, + ContainerProps: { + className: classes.listItemContainer + }, + title: makeDownloadableLink ? "Click to download ".concat(document.name) : undefined // @ts-ignore + , + button: makeDownloadableLink, + onClick: !makeDownloadableLink ? undefined : function () { + handleDownloadStarted(); + + _DocumentService.default.downloadDocument(appliedDocument, function (downloadDoc) { + return handleDownloadIdle(); + }, function (downloadDoc) { + return handleDownloadFailed(); + }); + } + }, /*#__PURE__*/_react.default.createElement(_ListItemIcon.default, { + className: classes.listItemIcon + }, /*#__PURE__*/_react.default.createElement(TypeIcon, null)), /*#__PURE__*/_react.default.createElement(_ListItemText.default, { + primary: primary, + secondary: renderSecondaryItem() + }), renderAction()), renderDownloadError()); +}; + +var WrappedDocumentListItem = _Theme.default.getWrappedComponent(DocumentListItem); + +var _default = WrappedDocumentListItem; +exports.default = _default; \ No newline at end of file diff --git a/lib/components/Documents/DocumentSelect.d.ts b/lib/components/Documents/DocumentSelect.d.ts new file mode 100644 index 00000000..586621ff --- /dev/null +++ b/lib/components/Documents/DocumentSelect.d.ts @@ -0,0 +1,6 @@ +import { NeonDocument } from '../../types/neonApi'; +export interface DocumentSelectProps { + documents: NeonDocument[]; +} +declare const WrappedDocumentSelect: any; +export default WrappedDocumentSelect; diff --git a/lib/components/Documents/DocumentSelect.js b/lib/components/Documents/DocumentSelect.js new file mode 100644 index 00000000..a6f86a88 --- /dev/null +++ b/lib/components/Documents/DocumentSelect.js @@ -0,0 +1,168 @@ +"use strict"; + +function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _react = _interopRequireWildcard(require("react")); + +var _FormControl = _interopRequireDefault(require("@material-ui/core/FormControl")); + +var _Grid = _interopRequireDefault(require("@material-ui/core/Grid")); + +var _InputLabel = _interopRequireDefault(require("@material-ui/core/InputLabel")); + +var _Select = _interopRequireDefault(require("@material-ui/core/Select")); + +var _MenuItem = _interopRequireDefault(require("@material-ui/core/MenuItem")); + +var _styles = require("@material-ui/core/styles"); + +var _DocumentListItem = _interopRequireDefault(require("./DocumentListItem")); + +var _DocumentService = _interopRequireDefault(require("../../service/DocumentService")); + +var _DocumentViewer = _interopRequireDefault(require("./DocumentViewer")); + +var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment")); + +var _Theme = _interopRequireDefault(require("../Theme/Theme")); + +var _WarningCard = _interopRequireDefault(require("../Card/WarningCard")); + +var _typeUtil = require("../../util/typeUtil"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { var _i = arr && (typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]); if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + +var useStyles = (0, _styles.makeStyles)(function (muiTheme) { + return (// eslint-disable-next-line implicit-arrow-linebreak + (0, _styles.createStyles)({ + container: { + width: '100%' + }, + selectContainer: { + width: '100%', + padding: muiTheme.spacing(3, 0) + }, + selectFormControl: { + width: '100%' + } + }) + ); +}); + +var DocumentSelect = function DocumentSelect(props) { + var classes = useStyles(_Theme.default); + var documents = props.documents; + var hasDocuments = (0, _typeUtil.existsNonEmpty)(documents); + var initialValue = ''; + + if (hasDocuments) { + initialValue = documents[0].name; + } + + var _useState = (0, _react.useState)(initialValue), + _useState2 = _slicedToArray(_useState, 2), + selectedDoc = _useState2[0], + setSelectedDoc = _useState2[1]; + + if (!hasDocuments) { + return /*#__PURE__*/_react.default.createElement("div", { + className: classes.container + }, /*#__PURE__*/_react.default.createElement(_WarningCard.default, { + title: "No Documents", + message: "No documents available to display" + })); + } + + var renderSelectedDocument = function renderSelectedDocument() { + var matchedDoc = documents.find(function (doc) { + return (0, _typeUtil.exists)(doc) && (0, _typeUtil.isStringNonEmpty)(doc.name) && doc.name.localeCompare(selectedDoc) === 0; + }); + + if (!(0, _typeUtil.exists)(matchedDoc)) { + return /*#__PURE__*/_react.default.createElement(_WarningCard.default, { + title: "Document Not Found", + message: "Document could not be found" + }); + } + + var coercedMatchedDoc = matchedDoc; + var fullUrlPath = _DocumentService.default.isQuickStartGuide(coercedMatchedDoc) ? "".concat(_NeonEnvironment.default.getFullApiPath('quickStartGuides')) : "".concat(_NeonEnvironment.default.getFullApiPath('documents')); + return /*#__PURE__*/_react.default.createElement(_DocumentViewer.default, { + document: coercedMatchedDoc, + width: 600, + fullUrlPath: fullUrlPath + }); + }; + + return /*#__PURE__*/_react.default.createElement("div", { + className: classes.container + }, /*#__PURE__*/_react.default.createElement(_Grid.default, { + container: true, + spacing: 3 + }, /*#__PURE__*/_react.default.createElement(_Grid.default, { + item: true, + xs: 12 + }, /*#__PURE__*/_react.default.createElement(_FormControl.default, { + variant: "outlined", + className: classes.selectFormControl + }, /*#__PURE__*/_react.default.createElement(_InputLabel.default, { + htmlFor: "document-select-input" + }, "Select Document to View"), /*#__PURE__*/_react.default.createElement(_Select.default, { + fullWidth: true, + id: "document-select", + inputProps: { + name: 'Select Document to View', + id: 'document-select-input' + }, + label: "Select Document to View", + "aria-labelledby": "document-select-label", + value: selectedDoc, + onChange: function onChange(event) { + var nextDoc = documents.find(function (doc) { + return doc.name.localeCompare(event.target.value) === 0; + }); + + if ((0, _typeUtil.exists)(nextDoc)) { + setSelectedDoc(nextDoc.name); + } + } + }, documents.map(function (doc, index) { + return /*#__PURE__*/_react.default.createElement(_MenuItem.default, { + key: doc.name, + value: doc.name + }, /*#__PURE__*/_react.default.createElement(_DocumentListItem.default, { + id: index, + document: doc, + makeDownloadableLink: false + })); + })))), /*#__PURE__*/_react.default.createElement(_Grid.default, { + item: true, + xs: 12 + }, renderSelectedDocument()))); +}; + +var WrappedDocumentSelect = _Theme.default.getWrappedComponent(DocumentSelect); + +var _default = WrappedDocumentSelect; +exports.default = _default; \ No newline at end of file diff --git a/lib/components/Documents/DocumentTabs.d.ts b/lib/components/Documents/DocumentTabs.d.ts new file mode 100644 index 00000000..6283d64e --- /dev/null +++ b/lib/components/Documents/DocumentTabs.d.ts @@ -0,0 +1,6 @@ +import { NeonDocument } from '../../types/neonApi'; +export interface DocumentTabsProps { + documents: NeonDocument[]; +} +declare const WrappedDocumentTabs: any; +export default WrappedDocumentTabs; diff --git a/lib/components/Documents/DocumentTabs.js b/lib/components/Documents/DocumentTabs.js new file mode 100644 index 00000000..fcd68d3f --- /dev/null +++ b/lib/components/Documents/DocumentTabs.js @@ -0,0 +1,205 @@ +"use strict"; + +function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _react = _interopRequireWildcard(require("react")); + +var _Tab = _interopRequireDefault(require("@material-ui/core/Tab")); + +var _Tabs = _interopRequireDefault(require("@material-ui/core/Tabs")); + +var _styles = require("@material-ui/core/styles"); + +var _DocumentListItem = _interopRequireDefault(require("./DocumentListItem")); + +var _DocumentService = _interopRequireDefault(require("../../service/DocumentService")); + +var _DocumentViewer = _interopRequireDefault(require("./DocumentViewer")); + +var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment")); + +var _Theme = _interopRequireDefault(require("../Theme/Theme")); + +var _WarningCard = _interopRequireDefault(require("../Card/WarningCard")); + +var _typeUtil = require("../../util/typeUtil"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { var _i = arr && (typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]); if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + +var useStyles = (0, _styles.makeStyles)(function (muiTheme) { + return (// eslint-disable-next-line implicit-arrow-linebreak + (0, _styles.createStyles)({ + container: { + width: '100%', + display: 'flex', + margin: muiTheme.spacing(0, -0.5, -0.5, -0.5), + flexDirection: 'column' + }, + tabPanels: { + width: '100%', + backgroundColor: '#fff' + }, + tabContentContainer: { + width: '100%', + padding: muiTheme.spacing(3, 3, 3, 3) + } + }) + ); +}); +var useTabsStyles = (0, _styles.makeStyles)(function (muiTheme) { + return (// eslint-disable-next-line implicit-arrow-linebreak + (0, _styles.createStyles)({ + scroller: { + backgroundColor: muiTheme.palette.grey[200] + }, + scrollButtons: { + '&.Mui-disabled': { + opacity: 0.6 + } + } + }) + ); +}); +var useTabStyles = (0, _styles.makeStyles)(function (muiTheme) { + return (// eslint-disable-next-line implicit-arrow-linebreak + (0, _styles.createStyles)({ + root: { + textTransform: 'none', + opacity: 1, + maxWidth: 464 + }, + wrapper: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + '& svg': { + margin: "".concat(muiTheme.spacing(0, 1, 0, 0), " !important") + } + }, + selected: { + borderBottom: 'none' + } + }) + ); +}); + +var DocumentTabs = function DocumentTabs(props) { + var classes = useStyles(_Theme.default); + var tabClasses = useTabStyles(_Theme.default); + var tabsClasses = useTabsStyles(_Theme.default); + var documents = props.documents; + var initialTabIdx = 0; + + var _useState = (0, _react.useState)(initialTabIdx), + _useState2 = _slicedToArray(_useState, 2), + selectedTab = _useState2[0], + setSelectedTab = _useState2[1]; + + if (!(0, _typeUtil.existsNonEmpty)(documents)) { + return /*#__PURE__*/_react.default.createElement("div", { + className: classes.container + }, /*#__PURE__*/_react.default.createElement(_WarningCard.default, { + title: "No Documents", + message: "No documents available to display" + })); + } + + var docTabs = documents.map(function (doc, index) { + return { + document: doc, + index: index + }; + }); + + var renderTabs = function renderTabs() { + return /*#__PURE__*/_react.default.createElement(_Tabs.default, { + orientation: "horizontal", + scrollButtons: "on", + variant: "scrollable", + value: selectedTab, + "aria-label": "Document Tabs", + classes: tabsClasses, + onChange: function onChange(event, newTab) { + setSelectedTab(newTab); + }, + TabIndicatorProps: { + style: { + display: 'none' + } + } + }, docTabs.map(function (docTab) { + return /*#__PURE__*/_react.default.createElement(_Tab.default, { + key: docTab.index, + value: docTab.index, + label: /*#__PURE__*/_react.default.createElement(_DocumentListItem.default, { + id: docTab.index, + document: docTab.document, + makeDownloadableLink: false + }), + "aria-label": docTab.document.name, + classes: tabClasses, + id: "document-tabs-tab-".concat(docTab.index), + "aria-controls": "document-tabs-tabpanel-".concat(docTab.index) + }); + })); + }; + + var renderTabContent = function renderTabContent(documentTab) { + var document = documentTab.document, + index = documentTab.index; + var fullUrlPath = _DocumentService.default.isQuickStartGuide(document) ? "".concat(_NeonEnvironment.default.getFullApiPath('quickStartGuides')) : "".concat(_NeonEnvironment.default.getFullApiPath('documents')); + return /*#__PURE__*/_react.default.createElement("div", { + key: index, + role: "tabpanel", + id: "document-tabs-tabpanel-".concat(index), + "aria-labelledby": "document-tabs-tab-".concat(index), + style: { + display: selectedTab === index ? 'block' : 'none' + }, + className: classes.tabContentContainer + }, /*#__PURE__*/_react.default.createElement(_DocumentViewer.default, { + document: document, + width: 600, + fullUrlPath: fullUrlPath + })); + }; + + var renderTabPanels = function renderTabPanels() { + return /*#__PURE__*/_react.default.createElement("div", { + className: classes.tabPanels + }, docTabs.map(function (docTab) { + return renderTabContent(docTab); + })); + }; + + return /*#__PURE__*/_react.default.createElement("div", { + className: classes.container + }, renderTabs(), renderTabPanels()); +}; + +var WrappedDocumentTabs = _Theme.default.getWrappedComponent(DocumentTabs); + +var _default = WrappedDocumentTabs; +exports.default = _default; \ No newline at end of file diff --git a/lib/components/DocumentViewer/DocumentViewer.d.ts b/lib/components/Documents/DocumentViewer.d.ts similarity index 90% rename from lib/components/DocumentViewer/DocumentViewer.d.ts rename to lib/components/Documents/DocumentViewer.d.ts index 41667356..158dcb96 100644 --- a/lib/components/DocumentViewer/DocumentViewer.d.ts +++ b/lib/components/Documents/DocumentViewer.d.ts @@ -3,6 +3,7 @@ import { NeonDocument } from '../../types/neonApi'; export interface DocumentViewerProps { document: NeonDocument; width: number; + fullUrlPath?: string; } declare const DocumentViewer: React.FC; export default DocumentViewer; diff --git a/lib/components/DocumentViewer/DocumentViewer.js b/lib/components/Documents/DocumentViewer.js similarity index 76% rename from lib/components/DocumentViewer/DocumentViewer.js rename to lib/components/Documents/DocumentViewer.js index f7aeaee4..f1091d39 100644 --- a/lib/components/DocumentViewer/DocumentViewer.js +++ b/lib/components/Documents/DocumentViewer.js @@ -11,10 +11,16 @@ var _react = _interopRequireWildcard(require("react")); var _styles = require("@material-ui/core/styles"); +var _DocumentService = _interopRequireDefault(require("../../service/DocumentService")); + +var _ErrorCard = _interopRequireDefault(require("../Card/ErrorCard")); + var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment")); var _Theme = _interopRequireDefault(require("../Theme/Theme")); +var _typeUtil = require("../../util/typeUtil"); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } @@ -37,8 +43,10 @@ var useStyles = (0, _styles.makeStyles)(function (muiTheme) { return (// eslint-disable-next-line implicit-arrow-linebreak (0, _styles.createStyles)({ container: { - width: '100%', - margin: muiTheme.spacing(3, 3, 3, 3) + width: '100%' + }, + iframe: { + border: 'none' } }) ); @@ -47,7 +55,7 @@ var useStyles = (0, _styles.makeStyles)(function (muiTheme) { var noop = function noop() {}; var breakpoints = [0, 675, 900, 1200]; -var ratios = ['8:11', '3:4', '3:4', '3:4']; +var ratios = ['8:11', '3:4', '4:4', '4:3']; var calcAutoHeight = function calcAutoHeight(width) { var breakIdx = breakpoints.reduce(function (acc, breakpoint, idx) { @@ -66,9 +74,12 @@ var calcAutoHeight = function calcAutoHeight(width) { var DocumentViewer = function DocumentViewer(props) { var classes = useStyles(_Theme.default); var document = props.document, - width = props.width; + width = props.width, + fullUrlPath = props.fullUrlPath; + var appliedUrlPath = (0, _typeUtil.isStringNonEmpty)(fullUrlPath) ? fullUrlPath : _NeonEnvironment.default.getFullApiPath('documents'); + var dataUrl = "".concat(appliedUrlPath, "/").concat(document.name, "?inline=true&fallback=html"); var containerRef = (0, _react.useRef)(); - var embedRef = (0, _react.useRef)(); + var iframeRef = (0, _react.useRef)(); var _useState = (0, _react.useState)(width), _useState2 = _slicedToArray(_useState, 2), @@ -77,15 +88,15 @@ var DocumentViewer = function DocumentViewer(props) { var handleResizeCb = (0, _react.useCallback)(function () { var container = containerRef.current; - var embed = embedRef.current; // Do nothing if either container or viz references fail ot point to a DOM node + var iframeElement = iframeRef.current; // Do nothing if either container or viz references fail ot point to a DOM node - if (!container || !embed) { + if (!container || !iframeElement) { return; } // Do nothing if either refs have no offset parent // (meaning they're hidden from rendering anyway) - if (container.offsetParent === null || embed.offsetParent === null) { + if (container.offsetParent === null || iframeElement.offsetParent === null) { return; } // Do nothing if container and viz have the same width // (resize event fired but no actual resize necessary) @@ -97,11 +108,11 @@ var DocumentViewer = function DocumentViewer(props) { var newWidth = container.clientWidth; setViewerWidth(newWidth); - embed.setAttribute('width', "".concat(newWidth)); - embed.setAttribute('height', "".concat(calcAutoHeight(newWidth))); - }, [containerRef, embedRef, viewerWidth, setViewerWidth]); + iframeElement.setAttribute('width', "".concat(newWidth)); + iframeElement.setAttribute('height', "".concat(calcAutoHeight(newWidth))); + }, [containerRef, iframeRef, viewerWidth, setViewerWidth]); (0, _react.useLayoutEffect)(function () { - var element = embedRef.current; + var element = iframeRef.current; if (!element) { return noop; @@ -132,18 +143,31 @@ var DocumentViewer = function DocumentViewer(props) { resizeObserver.disconnect(); resizeObserver = null; }; - }, [embedRef, handleResizeCb]); + }, [iframeRef, handleResizeCb]); + + var renderObject = function renderObject() { + if (!_DocumentService.default.isViewerSupported(document)) { + return /*#__PURE__*/_react.default.createElement(_ErrorCard.default, { + title: "Document Error", + message: "This document type is not supported or could not be displayed" + }); + } + + return /*#__PURE__*/_react.default.createElement("iframe", { + ref: iframeRef, + src: dataUrl, + "aria-label": document.description, + title: document.description, + width: viewerWidth, + height: calcAutoHeight(viewerWidth), + className: classes.iframe + }); + }; + return /*#__PURE__*/_react.default.createElement("div", { ref: containerRef, className: classes.container - }, /*#__PURE__*/_react.default.createElement("embed", { - ref: embedRef, - type: document.type, - src: "".concat(_NeonEnvironment.default.getFullApiPath('documents'), "/").concat(document.name, "?inline=true"), - title: document.description, - width: viewerWidth, - height: calcAutoHeight(viewerWidth) - })); + }, renderObject()); }; var _default = DocumentViewer; diff --git a/lib/components/DocumentViewer/index.d.ts b/lib/components/Documents/index.d.ts similarity index 100% rename from lib/components/DocumentViewer/index.d.ts rename to lib/components/Documents/index.d.ts diff --git a/lib/components/DocumentViewer/index.js b/lib/components/Documents/index.js similarity index 100% rename from lib/components/DocumentViewer/index.js rename to lib/components/Documents/index.js diff --git a/lib/components/DocumentViewer/package.json b/lib/components/Documents/package.json similarity index 76% rename from lib/components/DocumentViewer/package.json rename to lib/components/Documents/package.json index e6d1de5a..5e07b0f0 100644 --- a/lib/components/DocumentViewer/package.json +++ b/lib/components/Documents/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "document-viewer", + "name": "documents", "main": "./DocumentViewer.tsx", "module": "./DocumentViewer.tsx" } diff --git a/lib/components/NeonApi/NeonApi.d.ts b/lib/components/NeonApi/NeonApi.d.ts index 54b90e28..083f1229 100644 --- a/lib/components/NeonApi/NeonApi.d.ts +++ b/lib/components/NeonApi/NeonApi.d.ts @@ -12,6 +12,7 @@ declare namespace NeonApi { function getApiTokenHeader(headers?: Object | undefined): Object; function getJsonObservable(url: string, headers?: Object | undefined, includeToken?: boolean): import("rxjs").Observable; function postJsonObservable(url: string, body: any, headers?: Object | undefined, includeToken?: boolean): import("rxjs").Observable | import("rxjs").Observable; + function headJsonObservable(url: string, headers?: Object | undefined, includeToken?: boolean): import("rxjs").Observable | import("rxjs").Observable; function getJson(url: string, callback: any, errorCallback: any, cancellationSubject$: any, headers?: Object | undefined): import("rxjs").Subscription; function getProductsObservable(): import("rxjs").Observable; function getProductObservable(productCode: string, release?: string): import("rxjs").Observable; @@ -27,4 +28,7 @@ declare namespace NeonApi { function getSiteLocationHierarchyObservable(siteCode: string): import("rxjs").Observable; function getLocationObservable(location: string): import("rxjs").Observable; function getArcgisAssetObservable(feature: string, siteCode: string): import("rxjs").Observable; + function headDocumentObservable(name: string): import("rxjs").Observable | import("rxjs").Observable; + function getQuickStartGuideDetailObservable(name: string, version: string): import("rxjs").Observable; + function headQuickStartGuideObservable(name: string): import("rxjs").Observable | import("rxjs").Observable; } diff --git a/lib/components/NeonApi/NeonApi.js b/lib/components/NeonApi/NeonApi.js index 3e788497..7d1d048f 100644 --- a/lib/components/NeonApi/NeonApi.js +++ b/lib/components/NeonApi/NeonApi.js @@ -120,6 +120,59 @@ var _getJsonObservable = function getJsonObservable(url) { var request = getJsonAjaxRequest(url, headers, includeToken, withCredentials); return mapResponse((0, _ajax.ajax)(request)); }; +/** + * Gets the RxJS HEAD AjaxRequest + * @param {string} url The URL to make the API request to + * @param {Object|undefined} headers The headers to add to the request + * @param {boolean} includeToken Option to include the API token in the request + * @param {boolean} withCredentials Option to include credentials with a CORS request + * @return The RxJS HEAD AjaxRequest + */ + + +var headJsonAjaxRequest = function headJsonAjaxRequest(url) { + var headers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; + var includeToken = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; + var withCredentials = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : undefined; + var appliedHeaders = headers || {}; + + if (includeToken) { + appliedHeaders = _getApiTokenHeader(appliedHeaders); + } + + var appliedWithCredentials = getAppliedWithCredentials(withCredentials); + return { + url: url, + method: 'HEAD', + responseType: 'json', + crossDomain: true, + withCredentials: appliedWithCredentials, + headers: _extends({}, appliedHeaders) + }; +}; +/** + * Gets the RxJS observable for making an API request to the specified URL + * with optional headers. + * @param {string} url The URL to make the API request to + * @param {Object|undefined} headers The headers to add to the request + * @param {boolean} includeToken Option to include the API token in the request + * @param {boolean} withCredentials Option to include credentials with a CORS request + * @return The RxJS Ajax Observable + */ + + +var _headJsonObservable = function headJsonObservable(url) { + var headers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; + var includeToken = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; + var withCredentials = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : undefined; + + if (typeof url !== 'string' || !url.length) { + return (0, _rxjs.of)(null); + } + + var request = headJsonAjaxRequest(url, headers, includeToken, withCredentials); + return (0, _ajax.ajax)(request); +}; /** * Gets the RxJS observable for making a POST API request to the specified URL * with body and optional headers. @@ -205,6 +258,20 @@ var NeonApi = { return _postJsonObservable(url, body, headers, includeToken); }, + /** + * Gets the RxJS observable for making a HEAD API request to the specified URL + * with optional headers. + * @param {string} url The URL to make the API request to + * @param {Object|undefined} headers The headers to add to the request + * @param {boolean} includeToken Option to include the API token in the request + * @return The RxJS Ajax Observable + */ + headJsonObservable: function headJsonObservable(url) { + var headers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; + var includeToken = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; + return _headJsonObservable(url, headers, includeToken); + }, + /** * Convenience method for utiliizing the rxUtil => getJson function. * This will automatically add the API token header to the request. @@ -336,6 +403,34 @@ var NeonApi = { */ getArcgisAssetObservable: function getArcgisAssetObservable(feature, siteCode) { return _getJsonObservable("".concat(_NeonEnvironment.default.getFullApiPath('arcgisAssets'), "/").concat(feature, "/").concat(siteCode), undefined, true, false); + }, + + /** + * Gets the RxJS Observable for the document HEAD endpoint for a given name + * @param {string} name The document name + * @return The RxJS Ajax Observable + */ + headDocumentObservable: function headDocumentObservable(name) { + return _headJsonObservable("".concat(_NeonEnvironment.default.getFullApiPath('documents'), "/").concat(name)); + }, + + /** + * Gets the RxJS Observable for the quick start guides endpoint for a given name + * @param {string} name The quick start guide name + * @param {string} version The quick start guide version + * @return The RxJS Ajax Observable + */ + getQuickStartGuideDetailObservable: function getQuickStartGuideDetailObservable(name, version) { + return _getJsonObservable("".concat(_NeonEnvironment.default.getFullApiPath('quickStartGuides'), "/details/").concat(name, "/").concat(version)); + }, + + /** + * Gets the RxJS Observable for the quick start guides HEAD endpoint for a given name + * @param {string} name The document name + * @return The RxJS Ajax Observable + */ + headQuickStartGuideObservable: function headQuickStartGuideObservable(name) { + return _headJsonObservable("".concat(_NeonEnvironment.default.getFullApiPath('quickStartGuides'), "/").concat(name)); } }; Object.freeze(NeonApi); diff --git a/lib/components/NeonEnvironment/NeonEnvironment.js b/lib/components/NeonEnvironment/NeonEnvironment.js index 3730abde..f93b3b4c 100644 --- a/lib/components/NeonEnvironment/NeonEnvironment.js +++ b/lib/components/NeonEnvironment/NeonEnvironment.js @@ -80,6 +80,9 @@ var NeonEnvironment = { documents: function documents() { return '/documents'; }, + quickStartGuides: function quickStartGuides() { + return '/documents/quick-start-guides'; + }, products: function products() { return '/products'; }, diff --git a/lib/parser/DocumentParser.d.ts b/lib/parser/DocumentParser.d.ts new file mode 100644 index 00000000..ce7ea134 --- /dev/null +++ b/lib/parser/DocumentParser.d.ts @@ -0,0 +1,9 @@ +import { QuickStartGuideDocument, QuickStartGuideVersion } from '../types/neonApi'; +import { Nullable, UnknownRecord } from '../types/core'; +export interface IDocumentParser { + parseQuickStartGuideVersionResponse: (response: UnknownRecord) => Nullable; + parseQuickStartGuideDocuments: (documents: UnknownRecord[]) => QuickStartGuideDocument[]; + parseQuickStartGuideDocument: (document: UnknownRecord) => QuickStartGuideDocument; +} +declare const DocumentParser: IDocumentParser; +export default DocumentParser; diff --git a/lib/parser/DocumentParser.js b/lib/parser/DocumentParser.js new file mode 100644 index 00000000..60adae73 --- /dev/null +++ b/lib/parser/DocumentParser.js @@ -0,0 +1,52 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _typeUtil = require("../util/typeUtil"); + +var DocumentParser = { + parseQuickStartGuideVersionResponse: function parseQuickStartGuideVersionResponse(response) { + if (!(0, _typeUtil.exists)(response)) { + return null; + } + + var data = (0, _typeUtil.resolveAny)(response, 'data'); + + if (!(0, _typeUtil.exists)(data)) { + return null; + } + + return { + name: data.name, + version: data.version, + publishedDate: data.publishedDate, + documents: DocumentParser.parseQuickStartGuideDocuments(data.documents) + }; + }, + parseQuickStartGuideDocuments: function parseQuickStartGuideDocuments(documents) { + if (!Array.isArray(documents)) { + return []; + } + + return documents.map(function (document) { + return DocumentParser.parseQuickStartGuideDocument(document); + }); + }, + parseQuickStartGuideDocument: function parseQuickStartGuideDocument(document) { + return { + name: document.name, + description: document.description, + type: document.type, + size: document.size, + md5: document.md5, + generationDate: document.generationDate, + url: document.url + }; + } +}; +Object.freeze(DocumentParser); +var _default = DocumentParser; +exports.default = _default; \ No newline at end of file diff --git a/lib/remoteAssets/drupal-header.html.d.ts b/lib/remoteAssets/drupal-header.html.d.ts index 03daa7c6..9f348f44 100644 --- a/lib/remoteAssets/drupal-header.html.d.ts +++ b/lib/remoteAssets/drupal-header.html.d.ts @@ -1,2 +1,2 @@ -declare var _default: "\n
\n
\n \n \n \n
\n Sign In\n
\n
\n
\n
\n \n \n \n\n\n \n\n
\n
\n \n
\n
\n

Search

\n \n\n
\n \n \n
\n
\n \n\n \n
\n
\n\n\n
\n\n
\n\n
\n\n
\n \n\n
\n
\n
\n
\n
\n\n\n"; +declare var _default: "\n
\n
\n \n \n \n
\n Sign In\n
\n
\n
\n
\n \n \n \n\n\n \n\n
\n
\n \n
\n
\n

Search

\n \n\n
\n \n \n
\n
\n \n\n \n
\n
\n\n\n
\n\n
\n\n
\n\n
\n \n\n
\n
\n
\n
\n
\n\n\n"; export default _default; diff --git a/lib/remoteAssets/drupal-header.html.js b/lib/remoteAssets/drupal-header.html.js index 8ce5ebc5..288cca4d 100644 --- a/lib/remoteAssets/drupal-header.html.js +++ b/lib/remoteAssets/drupal-header.html.js @@ -6,6 +6,6 @@ Object.defineProperty(exports, "__esModule", { exports.default = void 0; var html; -var _default = html = "\n
\n
\n \n \n \n
\n Sign In\n
\n
\n
\n
\n \n \n \n\n\n \n\n
\n
\n \n
\n
\n

Search

\n \n\n
\n \n \n
\n
\n \n\n \n
\n
\n\n\n
\n\n
\n\n
\n\n
\n \n\n
\n
\n
\n
\n
\n\n\n"; +var _default = html = "\n
\n
\n \n \n \n
\n Sign In\n
\n
\n
\n
\n \n \n \n\n\n \n\n
\n
\n \n
\n
\n

Search

\n \n\n
\n \n \n
\n
\n \n\n \n
\n
\n\n\n
\n\n
\n\n
\n\n
\n \n\n
\n
\n
\n
\n
\n\n\n"; exports.default = _default; \ No newline at end of file diff --git a/lib/service/DocumentService.d.ts b/lib/service/DocumentService.d.ts new file mode 100644 index 00000000..ce70cde8 --- /dev/null +++ b/lib/service/DocumentService.d.ts @@ -0,0 +1,49 @@ +/// +import { DataProductSpec, NeonDocument, QuickStartGuideDocument } from '../types/neonApi'; +import { Nullable } from '../types/core'; +export interface DocumentTypeListItemDef { + match: (type: string) => boolean; + title: (type?: string) => string; + Icon: React.ReactNode; +} +export interface ParsedQsgNameResult { + name: string; + matchedName: string; + matchedVersion: string; + matchedExtension: string; + parsedVersion: number; +} +export declare const VIEWER_SUPPORTED_DOC_TYPES: string[]; +export declare type DocumentCallback = (document: NeonDocument) => void; +export interface IDocumentService { + formatBytes: (bytes: number) => string; + resolveDocumentType: (document: NeonDocument) => DocumentTypeListItemDef; + getDocumentTypeTitle: (document: NeonDocument) => string; + findFirstByDocumentTypeTitle: (documents: NeonDocument[], typeTitle: string) => Nullable; + getDocumentTypeListItemDefs: () => Record; + getDocumentTypeListItemDefKeys: () => string[]; + getDefaultDocumentTypeListItemDef: () => DocumentTypeListItemDef; + isQuickStartGuide: (doc: NeonDocument) => boolean; + isQuickStartGuideName: (name: Nullable) => boolean; + getQuickStartGuideNameRegex: () => RegExp; + parseQuickStartGuideName: (name: string) => Nullable; + isViewerSupported: (doc: NeonDocument) => boolean; + transformSpecs: (specs: DataProductSpec[]) => NeonDocument[]; + transformSpec: (spec: DataProductSpec) => NeonDocument; + transformQuickStartGuideDocuments: (documents: QuickStartGuideDocument[]) => NeonDocument[]; + transformQuickStartGuideDocument: (document: QuickStartGuideDocument) => NeonDocument; + applyDisplaySort: (documents: NeonDocument[], reverse?: boolean, qsgPrecedence?: boolean) => NeonDocument[]; + downloadDocument: (document: NeonDocument, onSuccessCb?: DocumentCallback, onErrorCb?: DocumentCallback) => void; + /** + * Utilize save as APIs to trigger a document download. + * EXPERIMENTAL! Note that this utilizes not-yet-standard web APIs + * that will not work across all browsers. + * @param document + * @param onSuccessCb + * @param onErrorCb + * @return + */ + saveDocument: (document: NeonDocument, onSuccessCb?: DocumentCallback, onErrorCb?: DocumentCallback) => void; +} +declare const DocumentService: IDocumentService; +export default DocumentService; diff --git a/lib/service/DocumentService.js b/lib/service/DocumentService.js new file mode 100644 index 00000000..09f5cfa4 --- /dev/null +++ b/lib/service/DocumentService.js @@ -0,0 +1,513 @@ +"use strict"; + +function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = exports.VIEWER_SUPPORTED_DOC_TYPES = void 0; + +var _Archive = _interopRequireDefault(require("@material-ui/icons/Archive")); + +var _Code = _interopRequireDefault(require("@material-ui/icons/Code")); + +var _DescriptionOutlined = _interopRequireDefault(require("@material-ui/icons/DescriptionOutlined")); + +var _InsertDriveFile = _interopRequireDefault(require("@material-ui/icons/InsertDriveFile")); + +var _Photo = _interopRequireDefault(require("@material-ui/icons/Photo")); + +var _Tv = _interopRequireDefault(require("@material-ui/icons/Tv")); + +var _GridOn = _interopRequireDefault(require("@material-ui/icons/GridOn")); + +var _NeonEnvironment = _interopRequireDefault(require("../components/NeonEnvironment/NeonEnvironment")); + +var _typeUtil = require("../util/typeUtil"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } + +function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } + +function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _wrapRegExp() { _wrapRegExp = function _wrapRegExp(re, groups) { return new BabelRegExp(re, undefined, groups); }; var _super = RegExp.prototype; var _groups = new WeakMap(); function BabelRegExp(re, flags, groups) { var _this = new RegExp(re, flags); _groups.set(_this, groups || _groups.get(re)); return _setPrototypeOf(_this, BabelRegExp.prototype); } _inherits(BabelRegExp, RegExp); BabelRegExp.prototype.exec = function (str) { var result = _super.exec.call(this, str); if (result) result.groups = buildGroups(result, this); return result; }; BabelRegExp.prototype[Symbol.replace] = function (str, substitution) { if (typeof substitution === "string") { var groups = _groups.get(this); return _super[Symbol.replace].call(this, str, substitution.replace(/\$<([^>]+)>/g, function (_, name) { return "$" + groups[name]; })); } else if (typeof substitution === "function") { var _this = this; return _super[Symbol.replace].call(this, str, function () { var args = arguments; if (_typeof(args[args.length - 1]) !== "object") { args = [].slice.call(args); args.push(buildGroups(args, _this)); } return substitution.apply(this, args); }); } else { return _super[Symbol.replace].call(this, str, substitution); } }; function buildGroups(result, re) { var g = _groups.get(re); return Object.keys(g).reduce(function (groups, name) { groups[name] = result[g[name]]; return groups; }, Object.create(null)); } return _wrapRegExp.apply(this, arguments); } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } + +function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } + +var VIEWER_SUPPORTED_DOC_TYPES = ['application/pdf', 'text/html', 'text/markdown', 'text/plain']; +exports.VIEWER_SUPPORTED_DOC_TYPES = VIEWER_SUPPORTED_DOC_TYPES; +var documentTypes = { + pdf: { + match: function match(type) { + return type === 'application/pdf' || type.includes('pdf'); + }, + title: function title(type) { + return 'PDF'; + }, + Icon: _DescriptionOutlined.default + }, + markdown: { + match: function match(type) { + return type === 'text/markdown'; + }, + title: function title(type) { + return 'Markdown'; + }, + Icon: _DescriptionOutlined.default + }, + image: { + match: function match(type) { + return ['image/gif', 'image/png', 'image/jpeg'].includes(type) || type.startsWith('image'); + }, + title: function title(type) { + return (0, _typeUtil.isStringNonEmpty)(type) ? "Image (".concat((type.match(/\/(.*)$/) || [])[1] || 'unknown type', ")") : 'unknown type'; + }, + Icon: _Photo.default + }, + csv: { + match: function match(type) { + return type === 'text/csv' || type.includes('csv'); + }, + title: function title(type) { + return 'CSV'; + }, + Icon: _GridOn.default + }, + text: { + match: function match(type) { + return type === 'text/plain'; + }, + title: function title(type) { + return 'Plain text file'; + }, + Icon: _DescriptionOutlined.default + }, + document: { + match: function match(type) { + return ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'].includes(type); + }, + title: function title(type) { + return 'Document'; + }, + Icon: _DescriptionOutlined.default + }, + spreadsheet: { + match: function match(type) { + return type.includes('spreadsheet') || type.includes('excel'); + }, + title: function title(type) { + return 'Spreadsheet'; + }, + Icon: _GridOn.default + }, + presentation: { + match: function match(type) { + return type.includes('presentation') || type.includes('powerpoint'); + }, + title: function title(type) { + return 'Presentation'; + }, + Icon: _Tv.default + }, + archive: { + match: function match(type) { + return type.includes('zip'); + }, + title: function title(type) { + return 'ZIP archive'; + }, + Icon: _Archive.default + }, + binary: { + match: function match(type) { + return type === 'application/octet-stream'; + }, + title: function title(type) { + return 'Raw binary data'; + }, + Icon: _InsertDriveFile.default + }, + xml: { + match: function match(type) { + return type === 'application/xml'; + }, + title: function title(type) { + return 'XML'; + }, + Icon: _Code.default + }, + html: { + match: function match(type) { + return type === 'text/html'; + }, + title: function title(type) { + return 'HTML'; + }, + Icon: _Code.default + } +}; +var documentTypeKeys = Object.keys(documentTypes); +var defaultDocumentType = { + match: function match(type) { + return true; + }, + title: function title(type) { + return 'File type unavailable'; + }, + Icon: _InsertDriveFile.default +}; + +var getFilenameFromContentDisposition = function getFilenameFromContentDisposition(response) { + var filename = null; + var contentDisposition = response.headers.get('content-disposition'); + + if ((0, _typeUtil.isStringNonEmpty)(contentDisposition)) { + var filenameSplit = contentDisposition === null || contentDisposition === void 0 ? void 0 : contentDisposition.split('filename='); + var splitLength = filenameSplit ? filenameSplit.length : -1; + + if ((0, _typeUtil.existsNonEmpty)(filenameSplit) && splitLength >= 2) { + var quotedFilename = filenameSplit[1]; + filename = quotedFilename.replaceAll('"', ''); + } + } + + return filename; +}; + +var DocumentService = { + formatBytes: function formatBytes(bytes) { + if (!Number.isInteger(bytes) || bytes < 0) { + return '0.000 B'; + } + + var scales = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + var log = Math.log(bytes) / Math.log(1024); + var scale = Math.floor(log); + var precision = Math.floor(3 - (log - scale) * 3); + return "".concat((bytes / Math.pow(1024, scale)).toFixed(precision), " ").concat(scales[scale]); + }, + resolveDocumentType: function resolveDocumentType(document) { + var documentType = DocumentService.getDefaultDocumentTypeListItemDef(); + + if (typeof document.type === 'string') { + var matchKey = DocumentService.getDocumentTypeListItemDefKeys().find(function (key) { + return DocumentService.getDocumentTypeListItemDefs()[key].match(document.type); + }); + + if (matchKey) { + documentType = DocumentService.getDocumentTypeListItemDefs()[matchKey]; + } + } + + return documentType; + }, + getDocumentTypeTitle: function getDocumentTypeTitle(document) { + var documentType = DocumentService.resolveDocumentType(document); + var typeTitle = documentType.title; + return typeTitle(document.type); + }, + findFirstByDocumentTypeTitle: function findFirstByDocumentTypeTitle(documents, typeTitle) { + if (!(0, _typeUtil.existsNonEmpty)(documents) || !(0, _typeUtil.isStringNonEmpty)(typeTitle)) { + return null; + } + + return documents.find(function (document) { + var typeTitleString = DocumentService.getDocumentTypeTitle(document); + return typeTitle.localeCompare(typeTitleString) === 0; + }); + }, + getDocumentTypeListItemDefs: function getDocumentTypeListItemDefs() { + return documentTypes; + }, + getDocumentTypeListItemDefKeys: function getDocumentTypeListItemDefKeys() { + return documentTypeKeys; + }, + getDefaultDocumentTypeListItemDef: function getDefaultDocumentTypeListItemDef() { + return defaultDocumentType; + }, + isQuickStartGuide: function isQuickStartGuide(doc) { + return (0, _typeUtil.exists)(doc) && DocumentService.isQuickStartGuideName(doc.name); + }, + isQuickStartGuideName: function isQuickStartGuideName(name) { + return (0, _typeUtil.isStringNonEmpty)(name) && name.startsWith('NEON.QSG.'); + }, + getQuickStartGuideNameRegex: function getQuickStartGuideNameRegex() { + return new RegExp( /*#__PURE__*/_wrapRegExp(/^(NEON\.QSG\.DP[0-9]{1}\.[0-9]{5}\.[0-9]{3})(v([0-9]+))*(\.([a-z]+))*$/, { + name: 1, + version: 2, + versionNumber: 3, + extension: 4, + extensionName: 5 + })); + }, + parseQuickStartGuideName: function parseQuickStartGuideName(name) { + var regex = DocumentService.getQuickStartGuideNameRegex(); + if (!regex) return null; + var matches = regex.exec(name); + if (!matches) return null; + if (matches.length <= 0) return null; + return { + name: name, + matchedName: matches[1], + matchedVersion: matches[2], + matchedExtension: matches[4], + parsedVersion: (0, _typeUtil.isStringNonEmpty)(matches[3]) ? parseInt(matches[3], 10) : -1 + }; + }, + isViewerSupported: function isViewerSupported(doc) { + return (0, _typeUtil.exists)(doc) && (0, _typeUtil.isStringNonEmpty)(doc.type) && VIEWER_SUPPORTED_DOC_TYPES.includes(doc.type); + }, + transformSpecs: function transformSpecs(specs) { + if (!(0, _typeUtil.existsNonEmpty)(specs)) { + return []; + } + + return specs.map(function (spec) { + return DocumentService.transformSpec(spec); + }); + }, + transformSpec: function transformSpec(spec) { + return { + name: spec.specNumber, + type: spec.specType, + size: spec.specSize, + description: spec.specDescription + }; + }, + transformQuickStartGuideDocuments: function transformQuickStartGuideDocuments(documents) { + if (!(0, _typeUtil.existsNonEmpty)(documents)) { + return []; + } + + return documents.map(function (document) { + return DocumentService.transformQuickStartGuideDocument(document); + }); + }, + transformQuickStartGuideDocument: function transformQuickStartGuideDocument(document) { + return { + name: document.name, + type: document.type, + size: document.size, + description: document.description + }; + }, + applyDisplaySort: function applyDisplaySort(documents, reverse, qsgPrecedence) { + if (!(0, _typeUtil.existsNonEmpty)(documents)) { + return []; + } + + var appliedReverse = reverse === true; + var appliedQsgPrecedence = qsgPrecedence === true; + + var sortedDocs = _toConsumableArray(documents); + + sortedDocs.sort(function (a, b) { + if (!(0, _typeUtil.exists)(a) && !(0, _typeUtil.exists)(b)) { + return 0; + } + + if (!(0, _typeUtil.exists)(a)) { + return appliedReverse ? -1 : 1; + } + + if (!(0, _typeUtil.exists)(b)) { + return appliedReverse ? 1 : -1; + } + + if (appliedQsgPrecedence) { + var aQsg = DocumentService.isQuickStartGuide(a); + var bQsg = DocumentService.isQuickStartGuide(b); + + if (!aQsg || !bQsg) { + if (aQsg) { + return appliedReverse ? 1 : -1; + } + + if (bQsg) { + return appliedReverse ? -1 : 1; + } + } + } + + if (!(0, _typeUtil.isStringNonEmpty)(a.description) && !(0, _typeUtil.isStringNonEmpty)(b.description)) { + return 0; + } + + if (!(0, _typeUtil.isStringNonEmpty)(a.description)) { + return appliedReverse ? -1 : 1; + } + + if (!(0, _typeUtil.isStringNonEmpty)(b.description)) { + return appliedReverse ? 1 : -1; + } + + var descriptionCompare = a.description.localeCompare(b.description); + + if (descriptionCompare === 0) { + if (!(0, _typeUtil.isStringNonEmpty)(a.name) && !(0, _typeUtil.isStringNonEmpty)(b.name)) { + return 0; + } + + if (!(0, _typeUtil.isStringNonEmpty)(a.name)) { + return appliedReverse ? -1 : 1; + } + + if (!(0, _typeUtil.isStringNonEmpty)(b.name)) { + return appliedReverse ? 1 : -1; + } + + return a.name.localeCompare(b.name); + } + + return descriptionCompare; + }); + return sortedDocs; + }, + downloadDocument: function downloadDocument(document, onSuccessCb, onErrorCb) { + var apiPath = DocumentService.isQuickStartGuide(document) ? "".concat(_NeonEnvironment.default.getFullApiPath('quickStartGuides'), "/").concat(document.name) : "".concat(_NeonEnvironment.default.getFullApiPath('documents'), "/").concat(document.name); + fetch(apiPath, { + method: 'HEAD' + }).then(function (response) { + if (!response.ok) { + throw new Error('Invalid HEAD response'); + } + + var filename = getFilenameFromContentDisposition(response); + + if (!(0, _typeUtil.isStringNonEmpty)(filename)) { + filename = document.name; + } + + fetch(apiPath).then(function (downloadResponse) { + if (!downloadResponse.ok || !downloadResponse.body) { + throw new Error('Invalid download response'); + } + + return downloadResponse.blob(); + }).then(function (blob) { + try { + var link = window.document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.setAttribute('download', filename); + window.document.body.appendChild(link); + link.click(); + window.document.body.removeChild(link); + + if (onSuccessCb) { + onSuccessCb(document); + } + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + + if (onErrorCb) { + onErrorCb(document); + } + } + }).catch(function (err) { + // eslint-disable-next-line no-console + console.error(err); + + if (onErrorCb) { + onErrorCb(document); + } + }); + }).catch(function (err) { + // eslint-disable-next-line no-console + console.error(err); + + if (onErrorCb) { + onErrorCb(document); + } + }); + }, + + /** + * Utilize save as APIs to trigger a document download. + * EXPERIMENTAL! Note that this utilizes not-yet-standard web APIs + * that will not work across all browsers. + * @param document + * @param onSuccessCb + * @param onErrorCb + * @return + */ + saveDocument: function saveDocument(document, onSuccessCb, onErrorCb) { + if (typeof window.showSaveFilePicker !== 'function') { + // eslint-disable-next-line no-console + console.error('Operation not supported'); + + if (onErrorCb) { + onErrorCb(document); + } + } + + var apiPath = DocumentService.isQuickStartGuide(document) ? "".concat(_NeonEnvironment.default.getFullApiPath('quickStartGuides'), "/").concat(document.name) : "".concat(_NeonEnvironment.default.getFullApiPath('documents'), "/").concat(document.name); + fetch(apiPath, { + method: 'HEAD' + }).then(function (response) { + if (!response.ok) { + throw new Error('Invalid HEAD response'); + } + + var filename = getFilenameFromContentDisposition(response); + + if (!(0, _typeUtil.isStringNonEmpty)(filename)) { + filename = document.name; + } + + var saveOpts = { + suggestedName: filename + }; + window.showSaveFilePicker(saveOpts).then(function (fileHandle) { + return fileHandle.createWritable(); + }).then(function (writable) { + fetch(apiPath).then(function (downloadResponse) { + if (!downloadResponse.ok || !downloadResponse.body) { + throw new Error('Invalid download response'); + } + + return downloadResponse.body.pipeTo(writable); + }).then(function (value) { + if (onSuccessCb) { + onSuccessCb(document); + } + }).catch(function (err) { + // eslint-disable-next-line no-console + console.error(err); + + if (onErrorCb) { + onErrorCb(document); + } + }); + }).catch(function (err) { + // eslint-disable-next-line no-console + console.error(err); + + if (onErrorCb) { + onErrorCb(document); + } + }); + }).catch(function (err) { + // eslint-disable-next-line no-console + console.error(err); + + if (onErrorCb) { + onErrorCb(document); + } + }); + } +}; +Object.freeze(DocumentService); +var _default = DocumentService; +exports.default = _default; \ No newline at end of file diff --git a/lib/types/neon.d.ts b/lib/types/neon.d.ts deleted file mode 100644 index ef5c51b7..00000000 --- a/lib/types/neon.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface NeonDocument { - name: string; - type: string; - size: number; - description: string; -} diff --git a/lib/types/neon.js b/lib/types/neon.js deleted file mode 100644 index 9a390c31..00000000 --- a/lib/types/neon.js +++ /dev/null @@ -1 +0,0 @@ -"use strict"; \ No newline at end of file diff --git a/lib/types/neonApi.d.ts b/lib/types/neonApi.d.ts index e1335a0e..eb5e6027 100644 --- a/lib/types/neonApi.d.ts +++ b/lib/types/neonApi.d.ts @@ -19,6 +19,13 @@ export interface ReleaseDataProductBundles { export interface Release extends IReleaseLike { dataProducts: UnknownRecord[]; } +export interface DataProductSpec { + specId: string; + specNumber: string; + specType: string; + specSize: number; + specDescription: string; +} export interface NeonDocument { name: string; type: string; @@ -33,3 +40,18 @@ export interface DataProductRelease extends IReleaseLike { url: string; productDoi: DataProductReleaseDoi; } +export interface QuickStartGuideDocument { + name: string; + description: string; + type: string; + size: number; + md5: string; + generationDate: string; + url: string; +} +export interface QuickStartGuideVersion { + name: string; + version: number; + publishedDate: string; + documents: QuickStartGuideDocument[]; +} diff --git a/package-lock.json b/package-lock.json index d15a80a2..dc7b0f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "portal-core-components", - "version": "1.12.1", + "version": "1.13.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "portal-core-components", - "version": "1.12.1", + "version": "1.13.0", "dependencies": { "@date-io/moment": "^1.3.9", "@material-ui/core": "^4.11.3", @@ -22,6 +22,7 @@ "@types/react-router": "^5.1.8", "@types/react-router-dom": "^5.1.5", "@types/sockjs-client": "^1.1.1", + "@types/wicg-file-system-access": "^2020.9.5", "clsx": "^1.1.0", "core-js": "^3.2.1", "d3-drag": "^1.2.3", @@ -35,7 +36,7 @@ "html-react-parser": "^0.10.3", "leaflet": "^1.6.0", "lodash": "^4.17.15", - "material-table": "^1.50.0", + "material-table": "^1.69.3", "moment": "^2.24.0", "node-lzw": "^0.3.1", "papaparse": "^5.1.1", @@ -4135,6 +4136,11 @@ "node": ">=0.10.0" } }, + "node_modules/@types/wicg-file-system-access": { + "version": "2020.9.5", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", + "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==" + }, "node_modules/@types/yargs": { "version": "15.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", @@ -4745,6 +4751,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-html": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", @@ -6536,14 +6554,20 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001274", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001274.tgz", - "integrity": "sha512-+Nkvv0fHyhISkiMIjnyjmf5YJcQ1IQHZN6U9TLUMroWR38FNwpsC51Gb68yueafX1V6ifOisInSgP9WJFS13ew==", + "version": "1.0.30001373", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001373.tgz", + "integrity": "sha512-pJYArGHrPp3TUqQzFYRmP/lwJlj8RCbVe3Gd3eJQkAV8SAC6b19XS9BjMvRdvaS8RMkaTN8ZhoHP6S1y8zzwEQ==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] }, "node_modules/canvg": { "version": "3.0.7", @@ -23886,10 +23910,12 @@ } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -29145,6 +29171,11 @@ } } }, + "@types/wicg-file-system-access": { + "version": "2020.9.5", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", + "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==" + }, "@types/yargs": { "version": "15.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", @@ -29621,6 +29652,14 @@ "dev": true, "requires": { "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } } }, "ansi-html": { @@ -31076,9 +31115,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001274", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001274.tgz", - "integrity": "sha512-+Nkvv0fHyhISkiMIjnyjmf5YJcQ1IQHZN6U9TLUMroWR38FNwpsC51Gb68yueafX1V6ifOisInSgP9WJFS13ew==", + "version": "1.0.30001373", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001373.tgz", + "integrity": "sha512-pJYArGHrPp3TUqQzFYRmP/lwJlj8RCbVe3Gd3eJQkAV8SAC6b19XS9BjMvRdvaS8RMkaTN8ZhoHP6S1y8zzwEQ==", "dev": true }, "canvg": { @@ -44832,10 +44871,12 @@ "dev": true }, "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "optional": true, + "peer": true }, "type-is": { "version": "1.6.18", diff --git a/package.json b/package.json index 3ca61aa9..64693475 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "portal-core-components", - "version": "1.12.1", + "version": "1.13.0", "main": "./lib/index.js", "private": true, "homepage": "http://localhost:3010/core-components", @@ -19,6 +19,7 @@ "@types/react-router": "^5.1.8", "@types/react-router-dom": "^5.1.5", "@types/sockjs-client": "^1.1.1", + "@types/wicg-file-system-access": "^2020.9.5", "clsx": "^1.1.0", "core-js": "^3.2.1", "d3-drag": "^1.2.3", @@ -32,7 +33,7 @@ "html-react-parser": "^0.10.3", "leaflet": "^1.6.0", "lodash": "^4.17.15", - "material-table": "^1.50.0", + "material-table": "^1.69.3", "moment": "^2.24.0", "node-lzw": "^0.3.1", "papaparse": "^5.1.1", diff --git a/src/App.jsx b/src/App.jsx index 05d340fa..3a7f7ec3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,7 +12,7 @@ import AopDataViewerStyleGuide from './lib_components/components/AopDataViewer/S import CitationsStyleGuide from './lib_components/components/Citation/StyleGuide'; import DataProductAvailabilityStyleGuide from './lib_components/components/DataProductAvailability/StyleGuide'; import DataThemeIconStyleGuide from './lib_components/components/DataThemeIcon/StyleGuide'; -import DocumentViewerStyleGuide from './lib_components/components/DocumentViewer/StyleGuide'; +import DocumentsStyleGuide from './lib_components/components/Documents/StyleGuide'; import DownloadDataButtonStyleGuide from './lib_components/components/DownloadDataButton/StyleGuide'; import DownloadDataContextStyleGuide from './lib_components/components/DownloadDataContext/StyleGuide'; import ExternalHostInfoStyleGuide from './lib_components/components/ExternalHostInfo/StyleGuide'; @@ -64,9 +64,9 @@ const sidebarLinks = [ component: DataThemeIconStyleGuide, }, { - name: 'Document Viewer', - hash: '#DocumentViewer', - component: DocumentViewerStyleGuide, + name: 'Documents', + hash: '#Documents', + component: DocumentsStyleGuide, }, { name: 'Download Data Button', diff --git a/src/components/BasicComponents.jsx b/src/components/BasicComponents.jsx index ff2014cf..fd54e973 100644 --- a/src/components/BasicComponents.jsx +++ b/src/components/BasicComponents.jsx @@ -34,6 +34,7 @@ import RightIcon from '@material-ui/icons/ChevronRight'; import DocBlock from './DocBlock'; +import SplitButton from '../lib_components/components/Button/SplitButton'; import Theme from '../lib_components/components/Theme/Theme'; const useStyles = makeStyles((theme) => ({ @@ -271,6 +272,82 @@ export default function BasicComponents() { + {/* Split Button */} + + SplitButton +
+
+ console.log(`clicked ${option}`)} + // eslint-disable-next-line no-console + onChange={(option) => console.log(`selected ${option}`)} + buttonGroupProps={{ size: 'small', variant: 'outlined', color: 'primary' }} + buttonProps={{ size: 'small', color: 'primary' }} + /> + console.log(`clicked ${option}`)} + // eslint-disable-next-line no-console + onChange={(option) => console.log(`selected ${option}`)} + buttonGroupProps={{ variant: 'outlined', color: 'primary' }} + buttonProps={{ color: 'primary' }} + /> + console.log(`clicked ${option}`)} + // eslint-disable-next-line no-console + onChange={(option) => console.log(`selected ${option}`)} + buttonGroupProps={{ size: 'large', variant: 'outlined', color: 'primary' }} + buttonProps={{ size: 'large', color: 'primary' }} + /> +
+
+ console.log(`clicked ${option}`)} + // eslint-disable-next-line no-console + onChange={(option) => console.log(`selected ${option}`)} + buttonGroupProps={{ size: 'small', variant: 'contained', color: 'primary' }} + buttonProps={{ size: 'small', color: 'primary', variant: 'contained' }} + /> + console.log(`clicked ${option}`)} + // eslint-disable-next-line no-console + onChange={(option) => console.log(`selected ${option}`)} + buttonGroupProps={{ variant: 'contained', color: 'primary' }} + buttonProps={{ color: 'primary', variant: 'contained' }} + /> + console.log(`clicked ${option}`)} + // eslint-disable-next-line no-console + onChange={(option) => console.log(`selected ${option}`)} + buttonGroupProps={{ size: 'large', variant: 'contained', color: 'primary' }} + buttonProps={{ size: 'large', color: 'primary', variant: 'contained' }} + /> +
+
+ {/* ButtonGroup */} ButtonGroup diff --git a/src/lib_components/components/Button/SplitButton.tsx b/src/lib_components/components/Button/SplitButton.tsx new file mode 100644 index 00000000..136fd668 --- /dev/null +++ b/src/lib_components/components/Button/SplitButton.tsx @@ -0,0 +1,167 @@ +import React, { useState, useRef, useEffect } from 'react'; + +import Grid from '@material-ui/core/Grid'; +import Button, { ButtonProps } from '@material-ui/core/Button'; +import ButtonGroup, { ButtonGroupProps } from '@material-ui/core/ButtonGroup'; +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import Grow from '@material-ui/core/Grow'; +import Paper from '@material-ui/core/Paper'; +import Popper from '@material-ui/core/Popper'; +import MenuItem from '@material-ui/core/MenuItem'; +import MenuList from '@material-ui/core/MenuList'; + +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; + +import Theme from '../Theme/Theme'; +import { Nullable } from '../../types/core'; +import { exists } from '../../util/typeUtil'; + +interface SplitButtonProps { + name: string; + options: string[]; + selectedOption: string; + onClick: (selectedOption: string) => void; + onChange: (selectedOption: string) => void; + buttonGroupProps: Nullable; + buttonMenuProps: Nullable; + buttonProps: Nullable; + selectedOptionDisplayCallback: Nullable<(selectedOption: string) => string>; +} + +const SplitButton: React.FC = (props: SplitButtonProps): JSX.Element => { + const { + name, + options, + selectedOption, + selectedOptionDisplayCallback, + onClick, + onChange, + buttonGroupProps, + buttonMenuProps, + buttonProps, + }: SplitButtonProps = props; + const [open, setOpen] = useState(false); + const [stateSelectedOption, setStateSelectedOption] = useState(selectedOption); + const anchorRef = useRef(null); + let appliedButtonGroupProps: ButtonGroupProps = { + variant: 'outlined', + color: 'primary', + }; + if (exists(buttonGroupProps)) { + appliedButtonGroupProps = buttonGroupProps as ButtonGroupProps; + } + let appliedButtonProps: ButtonProps = { + color: 'primary', + size: 'small', + }; + if (exists(buttonProps)) { + appliedButtonProps = buttonProps as ButtonProps; + } + let appliedButtonMenuProps: ButtonProps = { + color: 'primary', + size: 'small', + }; + if (exists(buttonMenuProps)) { + appliedButtonMenuProps = buttonMenuProps as ButtonProps; + } + + useEffect(() => { + if (selectedOption === stateSelectedOption) return; + setStateSelectedOption(selectedOption); + }, [selectedOption, stateSelectedOption]); + + const handleClick = (): void => { + onClick(stateSelectedOption); + }; + const handleMenuItemClick = ( + event: React.MouseEvent, + index: number, + ): void => { + setStateSelectedOption(options[index]); + onChange(options[index]); + setOpen(false); + }; + const handleToggle = (): void => { + setOpen((prevOpen) => !prevOpen); + }; + const handleClose = (event: React.MouseEvent): void => { + if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) { + return; + } + setOpen(false); + }; + + const renderSelectedOption = (): string => { + if (exists(selectedOptionDisplayCallback)) { + // eslint-disable-next-line max-len + return (selectedOptionDisplayCallback as (selectedOption: string) => string)(stateSelectedOption); + } + return stateSelectedOption; + }; + + return ( + + + + + + + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option: string, index: number): JSX.Element => (( + handleMenuItemClick(event, index)} + > + {option} + + )))} + + + + + )} + + + + ); +}; + +const WrappedSplitButton = (Theme as any).getWrappedComponent(SplitButton); + +export default WrappedSplitButton; diff --git a/src/lib_components/components/Button/index.d.ts b/src/lib_components/components/Button/index.d.ts new file mode 100644 index 00000000..7d06dc9d --- /dev/null +++ b/src/lib_components/components/Button/index.d.ts @@ -0,0 +1 @@ +export { default } from './SplitButton'; diff --git a/src/lib_components/components/Button/index.js b/src/lib_components/components/Button/index.js new file mode 100644 index 00000000..7d06dc9d --- /dev/null +++ b/src/lib_components/components/Button/index.js @@ -0,0 +1 @@ +export { default } from './SplitButton'; diff --git a/src/lib_components/components/Button/package.json b/src/lib_components/components/Button/package.json new file mode 100644 index 00000000..7ae51b6e --- /dev/null +++ b/src/lib_components/components/Button/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "name": "neon-buttons", + "main": "./SplitButton.tsx", + "module": "./SplitButton.tsx" +} diff --git a/src/lib_components/components/DocumentViewer/StyleGuide.tsx b/src/lib_components/components/DocumentViewer/StyleGuide.tsx deleted file mode 100644 index 304e0251..00000000 --- a/src/lib_components/components/DocumentViewer/StyleGuide.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; - -import Divider from '@material-ui/core/Divider'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; - -import CodeBlock from '../../../components/CodeBlock'; -import DocBlock from '../../../components/DocBlock'; -import ExampleBlock from '../../../components/ExampleBlock'; - -import DocumentViewer from './DocumentViewer'; -import Theme from '../Theme/Theme'; -import { NeonDocument } from '../../types/neonApi'; - -const useStyles = makeStyles((theme) => ({ - divider: { - margin: theme.spacing(3, 0), - }, -})); - -export default function StyleGuide() { - const classes = useStyles(Theme); - const exampleDoc: NeonDocument = { - name: 'NEON.DOC.000780vB.pdf', - type: 'application/pdf', - size: 993762, - description: 'NEON Algorithm Theoretical Basis Document (ATBD) – 2D Wind Speed and Direction', - }; - return ( - <> - - A module for displaying documents inline. - - - {` -import DocumentViewer from 'portal-core-components/lib/components/DocumentViewer'; - `} - - - - Example Document Viewer - - Displays a single embedded document. - - - {` -import DocumentViewer from 'portal-core-components/lib/components/DocumentViewer'; - -const exampleDoc: NeonDocument = { - name: 'NEON.DOC.000780vB.pdf', - type: 'application/pdf', - size: 993762, - description: 'NEON Algorithm Theoretical Basis Document (ATBD) – 2D Wind Speed and Direction', -}; - - - `} - - - - - - ); -} diff --git a/src/lib_components/components/Documents/DocumentList.tsx b/src/lib_components/components/Documents/DocumentList.tsx new file mode 100644 index 00000000..fe42ec3a --- /dev/null +++ b/src/lib_components/components/Documents/DocumentList.tsx @@ -0,0 +1,77 @@ +import React from 'react'; + +import List from '@material-ui/core/List'; +import { + makeStyles, + createStyles, + Theme as MuiTheme, +} from '@material-ui/core/styles'; + +import DocumentListItem, { DocumentListItemModel } from './DocumentListItem'; +import Theme from '../Theme/Theme'; +import WarningCard from '../Card/WarningCard'; + +import { StylesHook } from '../../types/muiTypes'; +import { existsNonEmpty } from '../../util/typeUtil'; +import { Nullable } from '../../types/core'; + +const useStyles: StylesHook = makeStyles((muiTheme: MuiTheme) => + // eslint-disable-next-line implicit-arrow-linebreak + createStyles({ + list: { + paddingTop: muiTheme.spacing(0), + }, + })) as StylesHook; + +export interface DocumentListProps { + documents: DocumentListItemModel[]; + makeDownloadableLink: Nullable; + enableDownloadButton: Nullable; + fetchVariants: Nullable; + enableVariantChips: Nullable; +} + +const DocumentList: React.FC = (props: DocumentListProps): JSX.Element => { + const classes = useStyles(Theme); + const { + documents, + makeDownloadableLink, + enableDownloadButton, + fetchVariants, + enableVariantChips, + }: DocumentListProps = props; + if (!existsNonEmpty(documents)) { + return ( +
+ +
+ ); + } + const renderDocuments = (): JSX.Element[] => ( + documents.map((document: DocumentListItemModel, index: number): JSX.Element => (( + + ))) + ); + return ( +
+ + {renderDocuments()} + +
+ ); +}; + +const WrappedDocumentList = (Theme as any).getWrappedComponent(DocumentList); + +export default WrappedDocumentList; diff --git a/src/lib_components/components/Documents/DocumentListItem.tsx b/src/lib_components/components/Documents/DocumentListItem.tsx new file mode 100644 index 00000000..35d23373 --- /dev/null +++ b/src/lib_components/components/Documents/DocumentListItem.tsx @@ -0,0 +1,700 @@ +import React, { + useReducer, + useEffect, + useCallback, + useRef, + useLayoutEffect, + useState, + Dispatch, +} from 'react'; + +import { of, Observable } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; + +import cloneDeep from 'lodash/cloneDeep'; + +import Button from '@material-ui/core/Button'; +import Chip from '@material-ui/core/Chip'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import IconButton from '@material-ui/core/IconButton'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; +import Tooltip from '@material-ui/core/Tooltip'; +import Typography from '@material-ui/core/Typography'; + +import { + makeStyles, + createStyles, + Theme as MuiTheme, +} from '@material-ui/core/styles'; + +import DownloadIcon from '@material-ui/icons/SaveAlt'; + +import NeonApi from '../NeonApi'; +import SplitButton from '../Button/SplitButton'; +import Theme from '../Theme/Theme'; +import WarningCard from '../Card/WarningCard'; + +import DocumentParser from '../../parser/DocumentParser'; +import DocumentService, { + DocumentTypeListItemDef, + ParsedQsgNameResult, +} from '../../service/DocumentService'; +import { StylesHook } from '../../types/muiTypes'; +import { exists, existsNonEmpty, isStringNonEmpty } from '../../util/typeUtil'; +import { + AnyAction, + Nullable, + Undef, + UnknownRecord, +} from '../../types/core'; +import { + NeonDocument, + QuickStartGuideDocument, + QuickStartGuideVersion, +} from '../../types/neonApi'; + +const COMPONENT_XS_UPPER = 480; +const COMPONENT_SM_UPPER = 805; + +const useStyles = makeStyles((muiTheme: MuiTheme) => createStyles({ + listItemContainer: { + display: 'flex', + }, + listItem: { + display: 'flex', + wordBreak: 'break-word', + paddingLeft: muiTheme.spacing(1), + '& p': { + marginTop: muiTheme.spacing(0.5), + '& > span > span': { + whiteSpace: 'nowrap', + }, + }, + }, + listItemSecondarySpacer: { + margin: muiTheme.spacing(0, 2), + color: muiTheme.palette.grey[200], + }, + listItemIcon: { + minWidth: muiTheme.spacing(4), + marginRight: muiTheme.spacing(1), + }, + fileTypeChip: { + marginRight: '5px', + '&:last-child': { + marginRight: '0px', + }, + }, + fileTypeChipSelected: { + marginRight: '5px', + fontWeight: 500, + }, + variantFetchingLabel: { + lineHeight: '24px', + }, + variantFetchingProgress: { + marginRight: '36px', + marginLeft: '36px', + }, + downloadErrorContainer: { + marginTop: muiTheme.spacing(2), + }, +})) as StylesHook; + +const useListItemSecondaryActionStyles = makeStyles((muiTheme: MuiTheme) => + // eslint-disable-next-line implicit-arrow-linebreak + createStyles({ + root: { + display: 'flex', + alignItems: 'center', + position: 'unset', + transform: 'unset', + top: 'unset', + right: 'unset', + whiteSpace: 'nowrap', + }, + })) as StylesHook; + +enum ActionTypes { + FETCH_VARIANTS_STARTED = 'FETCH_VARIANTS_STARTED', + FETCH_VARIANTS_FAILED = 'FETCH_VARIANTS_FAILED', + FETCH_VARIANTS_SUCCEEDED = 'FETCH_VARIANTS_SUCCEEDED', + + SET_SELECTED_VARIANT = 'SET_SELECTED_VARIANT', + + DOWNLOAD_IDLE = 'DOWNLOAD_IDLE', + DOWNLOAD_STARTED = 'DOWNLOAD_STARTED', + DOWNLOAD_FAILED = 'DOWNLOAD_FAILED', +} + +interface FetchVariantsStartedAction extends AnyAction { + type: typeof ActionTypes.FETCH_VARIANTS_STARTED; +} +interface FetchVariantsFailedAction extends AnyAction { + type: typeof ActionTypes.FETCH_VARIANTS_FAILED; + error: Nullable; +} +interface FetchVariantsSucceededAction extends AnyAction { + type: typeof ActionTypes.FETCH_VARIANTS_SUCCEEDED; + variants: NeonDocument[]; +} +interface SetSelectedVariantAction extends AnyAction { + type: typeof ActionTypes.SET_SELECTED_VARIANT; + variant: NeonDocument; +} +interface DownloadIdleAction extends AnyAction { + type: typeof ActionTypes.DOWNLOAD_IDLE; +} +interface DownloadStartedAction extends AnyAction { + type: typeof ActionTypes.DOWNLOAD_STARTED; +} +interface DownloadFailedAction extends AnyAction { + type: typeof ActionTypes.DOWNLOAD_FAILED; +} + +type DocumentListItemActionTypes = ( + FetchVariantsStartedAction + | FetchVariantsFailedAction + | FetchVariantsSucceededAction + | SetSelectedVariantAction + | DownloadIdleAction + | DownloadStartedAction + | DownloadFailedAction +); + +const ActionCreator = { + fetchVariantsStarted: (): FetchVariantsStartedAction => ({ + type: ActionTypes.FETCH_VARIANTS_STARTED, + }), + fetchVariantsFailed: (error: Nullable): FetchVariantsFailedAction => ({ + type: ActionTypes.FETCH_VARIANTS_FAILED, + error, + }), + fetchVariantsSucceeded: (variants: NeonDocument[]): FetchVariantsSucceededAction => ({ + type: ActionTypes.FETCH_VARIANTS_SUCCEEDED, + variants, + }), + setSelectedVariant: (variant: NeonDocument): SetSelectedVariantAction => ({ + type: ActionTypes.SET_SELECTED_VARIANT, + variant, + }), + downloadIdle: (): DownloadIdleAction => ({ + type: ActionTypes.DOWNLOAD_IDLE, + }), + downloadStarted: (): DownloadStartedAction => ({ + type: ActionTypes.DOWNLOAD_STARTED, + }), + downloadFailed: (): DownloadFailedAction => ({ + type: ActionTypes.DOWNLOAD_FAILED, + }), +}; + +enum FetchStatus { + AWAITING_CALL = 'AWAITING_CALL', + FETCHING = 'FETCHING', + ERROR = 'ERROR', + SUCCESS = 'SUCCESS', + IDLE = 'IDLE', +} + +interface FetchStatusState { + status: FetchStatus, + error?: Nullable, +} + +interface DocumentListItemState { + fetchVariants: FetchStatusState; + variants: NeonDocument[]; + selectedVariant: Nullable; + downloadStatus: FetchStatus; +} + +const DEFAULT_STATE: DocumentListItemState = { + fetchVariants: { + status: FetchStatus.IDLE, + error: null, + }, + variants: [], + selectedVariant: null, + downloadStatus: FetchStatus.IDLE, +}; + +const documentListItemReducer = ( + state: DocumentListItemState, + action: DocumentListItemActionTypes, +): DocumentListItemState => { + const newState: DocumentListItemState = { ...state }; + let fetchVariantFailedAction: FetchVariantsFailedAction; + let fetchVariantSucceededAction: FetchVariantsSucceededAction; + let setSelectedVariantAction: SetSelectedVariantAction; + switch (action.type) { + case ActionTypes.FETCH_VARIANTS_STARTED: + newState.fetchVariants.status = FetchStatus.FETCHING; + return newState; + case ActionTypes.FETCH_VARIANTS_FAILED: + fetchVariantFailedAction = (action as FetchVariantsFailedAction); + newState.fetchVariants.status = FetchStatus.ERROR; + newState.fetchVariants.error = fetchVariantFailedAction.error; + return newState; + case ActionTypes.FETCH_VARIANTS_SUCCEEDED: + fetchVariantSucceededAction = (action as FetchVariantsSucceededAction); + newState.fetchVariants.status = FetchStatus.SUCCESS; + newState.variants = fetchVariantSucceededAction.variants; + if (!exists(newState.selectedVariant) && existsNonEmpty(newState.variants)) { + // eslint-disable-next-line prefer-destructuring + newState.selectedVariant = newState.variants[0]; + } + return newState; + case ActionTypes.SET_SELECTED_VARIANT: + setSelectedVariantAction = (action as SetSelectedVariantAction); + newState.selectedVariant = setSelectedVariantAction.variant; + return newState; + case ActionTypes.DOWNLOAD_IDLE: + newState.downloadStatus = FetchStatus.IDLE; + return newState; + case ActionTypes.DOWNLOAD_STARTED: + newState.downloadStatus = FetchStatus.FETCHING; + return newState; + case ActionTypes.DOWNLOAD_FAILED: + newState.downloadStatus = FetchStatus.ERROR; + return newState; + default: + return newState; + } +}; + +export interface DocumentListItemModel extends NeonDocument { + variants: NeonDocument[]; +} + +export interface DocumentListItemProps { + id: number; + document: DocumentListItemModel; + makeDownloadableLink: boolean; + enableDownloadButton: Nullable; + fetchVariants: Nullable; + enableVariantChips: Nullable; + containerComponent: Undef>>; +} + +const DocumentListItem: React.FC = ( + props: DocumentListItemProps, +): JSX.Element|null => { + const { + id, + document, + makeDownloadableLink, + enableDownloadButton, + fetchVariants, + enableVariantChips, + containerComponent, + }: DocumentListItemProps = props; + const classes = useStyles(Theme); + const listItemSecondaryActionClasses = useListItemSecondaryActionStyles(Theme); + const containerRef: React.MutableRefObject = useRef(); + const [ + componentWidth, + setComponentWidth, + ]: [number, React.Dispatch>] = useState(0); + let atComponentXs = false; + let atComponentSm = false; + if (componentWidth > 0) { + atComponentXs = (componentWidth <= COMPONENT_XS_UPPER); + atComponentSm = (componentWidth >= COMPONENT_XS_UPPER) && (componentWidth < COMPONENT_SM_UPPER); + } + const [ + state, + dispatch, + ]: [DocumentListItemState, Dispatch] = useReducer( + documentListItemReducer, + cloneDeep(DEFAULT_STATE), + ); + const { + fetchVariants: { + status: fetchVariantStatus, + }, + variants: stateVariants, + selectedVariant: stateSelectedVariant, + downloadStatus, + }: DocumentListItemState = state; + const hasDocument = exists(document); + const hasProvidedVariants = hasDocument && existsNonEmpty(document.variants); + const isQsg = DocumentService.isQuickStartGuide(document); + const appliedFetchVariants = !hasProvidedVariants && (fetchVariants === true); + const requireVariantFetch = isQsg + && appliedFetchVariants + && (fetchVariantStatus === FetchStatus.IDLE); + useEffect(() => { + if (!hasDocument || !requireVariantFetch) { + return; + } + const qsgParsedName: Nullable = DocumentService.parseQuickStartGuideName( + document.name, + ); + if (!exists(qsgParsedName)) { + return; + } + const coercedParsedName: ParsedQsgNameResult = (qsgParsedName as ParsedQsgNameResult); + const variantObs = (NeonApi.getQuickStartGuideDetailObservable( + coercedParsedName.matchedName, + coercedParsedName.matchedVersion, + ) as Observable).pipe( + map((response: UnknownRecord): Observable => { + if (!exists(response) || !exists(response.data)) { + dispatch(ActionCreator.fetchVariantsFailed('Failed to fetch variants')); + return of(false); + } + const qsgResponse: Nullable = DocumentParser + .parseQuickStartGuideVersionResponse(response); + if (!exists(qsgResponse)) { + dispatch(ActionCreator.fetchVariantsFailed('Failed to fetch variants')); + return of(false); + } + const coercedQsgDocumentResponse = (qsgResponse as QuickStartGuideVersion); + const qsgDocuments: QuickStartGuideDocument[] = coercedQsgDocumentResponse.documents; + const variantDocuments: NeonDocument[] = DocumentService.transformQuickStartGuideDocuments( + qsgDocuments, + ); + if (!existsNonEmpty(variantDocuments)) { + dispatch(ActionCreator.fetchVariantsFailed('Failed to fetch variants')); + return of(false); + } + dispatch(ActionCreator.fetchVariantsSucceeded(variantDocuments)); + return of(true); + }), + catchError((error: UnknownRecord|string): Observable => { + dispatch(ActionCreator.fetchVariantsFailed(error)); + return of(false); + }), + ); + dispatch(ActionCreator.fetchVariantsStarted()); + variantObs.subscribe(); + }, [hasDocument, requireVariantFetch, document]); + + const handleSelectedVariantChanged = useCallback((selectedVariantCb: NeonDocument): void => { + dispatch(ActionCreator.setSelectedVariant(selectedVariantCb)); + }, [dispatch]); + const handleDownloadIdle = useCallback((): void => { + dispatch(ActionCreator.downloadIdle()); + }, [dispatch]); + const handleDownloadStarted = useCallback((): void => { + dispatch(ActionCreator.downloadStarted()); + }, [dispatch]); + const handleDownloadFailed = useCallback((): void => { + dispatch(ActionCreator.downloadFailed()); + }, [dispatch]); + + const handleResizeCb = useCallback((): void => { + const container: HTMLDivElement|HTMLAnchorElement|undefined = containerRef.current; + if (!container) { return; } + if (container.clientWidth === componentWidth) { return; } + setComponentWidth(container.clientWidth); + }, [containerRef, componentWidth, setComponentWidth]); + + useLayoutEffect(() => { + const element = containerRef.current; + if (!element) { return () => {}; } + handleResizeCb(); + if (typeof ResizeObserver !== 'function') { + window.addEventListener('resize', handleResizeCb); + return () => { + window.removeEventListener('resize', handleResizeCb); + }; + } + let resizeObserver: ResizeObserver|null = new ResizeObserver(handleResizeCb); + resizeObserver.observe(element); + return () => { + if (!resizeObserver) { return; } + resizeObserver.disconnect(); + resizeObserver = null; + }; + }, [containerRef, handleResizeCb]); + + if (!hasDocument) { + return null; + } + const isFetchingVariants = (fetchVariantStatus === FetchStatus.FETCHING); + const appliedDocument: NeonDocument = exists(stateSelectedVariant) + ? stateSelectedVariant as NeonDocument + : document; + const appliedVariants: NeonDocument[] = appliedFetchVariants + ? stateVariants + : document.variants; + const hasAppliedVariants = existsNonEmpty(appliedVariants); + + const isDownloading = (downloadStatus === FetchStatus.FETCHING); + const isDownloadError = (downloadStatus === FetchStatus.ERROR); + + const documentType: DocumentTypeListItemDef = DocumentService.resolveDocumentType( + appliedDocument, + ); + const { title: typeTitle, Icon: TypeIcon }: DocumentTypeListItemDef = documentType; + const typeTitleString = typeTitle(appliedDocument.type); + const primary = isStringNonEmpty(appliedDocument.description) + ? appliedDocument.description + : No description; + const spacer = |; + const renderTypes = (): JSX.Element => { + if (!(enableVariantChips === true)) { + return ({typeTitleString}); + } + if (isFetchingVariants) { + return ( + + Determining variants... + + ); + } + if (!hasAppliedVariants) { + return ( + + ); + } + return ( + <> + {appliedVariants.map((variant: NeonDocument, index: number): JSX.Element => { + const variantTypeTitleString = DocumentService.getDocumentTypeTitle(variant); + const isSelected = (appliedDocument.name === variant.name); + const isLast = (index === (appliedVariants.length - 1)); + return ( + ) => { + handleSelectedVariantChanged(variant); + }} + /> + ); + })} + + ); + }; + const renderSecondaryItem = (): JSX.Element => { + let sizeDisplay = (n/a); + if (appliedDocument.size) { + sizeDisplay = ( + + {DocumentService.formatBytes(appliedDocument.size)} + + ); + } + const fileNumber = ( + + {appliedDocument.name} + + ); + if (atComponentSm || atComponentXs) { + if (hasAppliedVariants) { + return ( + + {fileNumber} + + {renderTypes()} + + {sizeDisplay} + + ); + } + return ( + + {fileNumber} + + {renderTypes()} + {spacer} + {sizeDisplay} + + ); + } + return ( + + {renderTypes()} + {spacer} + {sizeDisplay} + {spacer} + {fileNumber} + + ); + }; + const renderAction = (): JSX.Element|null => { + if (!(enableDownloadButton === true)) return null; + if (isFetchingVariants) { + return ( + + + + ); + } + if (atComponentXs) { + return ( + + +
+ { + handleDownloadStarted(); + DocumentService.downloadDocument( + appliedDocument, + (downloadDoc: NeonDocument): void => handleDownloadIdle(), + (downloadDoc: NeonDocument): void => handleDownloadFailed(), + ); + }} + > + {isDownloading + ? + : } + +
+
+
+ ); + } + const button = !hasAppliedVariants + ? ( + + ) : ( + ( + `Download ${selectedOption}` + )} + options={appliedVariants.map((variant: NeonDocument): string => ( + DocumentService.getDocumentTypeTitle(variant) + ))} + onClick={(option: string) => { + const nextSelectedVariant: Nullable = DocumentService + .findFirstByDocumentTypeTitle(appliedVariants, option); + if (exists(nextSelectedVariant)) { + const coercedDownloadVariant = nextSelectedVariant as NeonDocument; + handleDownloadStarted(); + DocumentService.downloadDocument( + coercedDownloadVariant, + (downloadDoc: NeonDocument): void => handleDownloadIdle(), + (downloadDoc: NeonDocument): void => handleDownloadFailed(), + ); + } + }} + onChange={(option: string) => { + const nextSelectedVariant: Nullable = DocumentService + .findFirstByDocumentTypeTitle(appliedVariants, option); + if (exists(nextSelectedVariant)) { + handleSelectedVariantChanged(nextSelectedVariant as NeonDocument); + } + }} + buttonGroupProps={{ + size: 'small', + variant: 'outlined', + color: 'primary', + }} + buttonMenuProps={{ + size: 'small', + color: 'primary', + disabled: isDownloading || isDownloadError, + }} + buttonProps={{ + size: 'small', + color: 'primary', + disabled: isDownloading || isDownloadError, + startIcon: isDownloading + ? + : , + }} + /> + ); + return ( + + {button} + + ); + }; + const renderDownloadError = (): JSX.Element|null => { + if (!isDownloadError) return null; + return ( +
+ handleDownloadIdle()} + /> +
+ ); + }; + return ( + <> + { + handleDownloadStarted(); + DocumentService.downloadDocument( + appliedDocument, + (downloadDoc: NeonDocument): void => handleDownloadIdle(), + (downloadDoc: NeonDocument): void => handleDownloadFailed(), + ); + }} + > + + {/* @ts-ignore */} + + + + {renderAction()} + + {renderDownloadError()} + + ); +}; + +const WrappedDocumentListItem = (Theme as any).getWrappedComponent(DocumentListItem); + +export default WrappedDocumentListItem; diff --git a/src/lib_components/components/Documents/DocumentSelect.tsx b/src/lib_components/components/Documents/DocumentSelect.tsx new file mode 100644 index 00000000..cb616cc6 --- /dev/null +++ b/src/lib_components/components/Documents/DocumentSelect.tsx @@ -0,0 +1,143 @@ +import React, { useState } from 'react'; + +import FormControl from '@material-ui/core/FormControl'; +import Grid from '@material-ui/core/Grid'; +import InputLabel from '@material-ui/core/InputLabel'; +import Select from '@material-ui/core/Select'; +import MenuItem from '@material-ui/core/MenuItem'; +import { + makeStyles, + createStyles, + Theme as MuiTheme, +} from '@material-ui/core/styles'; + +import DocumentListItem from './DocumentListItem'; +import DocumentService from '../../service/DocumentService'; +import DocumentViewer from './DocumentViewer'; +import NeonEnvironment from '../NeonEnvironment'; +import Theme from '../Theme/Theme'; +import WarningCard from '../Card/WarningCard'; +import { Nullable } from '../../types/core'; +import { StylesHook } from '../../types/muiTypes'; +import { NeonDocument } from '../../types/neonApi'; +import { exists, existsNonEmpty, isStringNonEmpty } from '../../util/typeUtil'; + +const useStyles: StylesHook = makeStyles((muiTheme: MuiTheme) => + // eslint-disable-next-line implicit-arrow-linebreak + createStyles({ + container: { + width: '100%', + }, + selectContainer: { + width: '100%', + padding: muiTheme.spacing(3, 0), + }, + selectFormControl: { + width: '100%', + }, + })) as StylesHook; + +export interface DocumentSelectProps { + documents: NeonDocument[]; +} + +const DocumentSelect: React.FC = (props: DocumentSelectProps): JSX.Element => { + const classes = useStyles(Theme); + const { documents }: DocumentSelectProps = props; + + const hasDocuments: boolean = existsNonEmpty(documents); + let initialValue: string = ''; + if (hasDocuments) { + initialValue = documents[0].name; + } + const [ + selectedDoc, + setSelectedDoc, + ]: [string, React.Dispatch>] = useState(initialValue); + + if (!hasDocuments) { + return ( +
+ +
+ ); + } + + const renderSelectedDocument = (): JSX.Element => { + const matchedDoc: Nullable = documents.find((doc: NeonDocument): boolean => ( + exists(doc) + && isStringNonEmpty(doc.name) + && (doc.name.localeCompare(selectedDoc) === 0) + )); + if (!exists(matchedDoc)) { + return ( + + ); + } + const coercedMatchedDoc: NeonDocument = (matchedDoc as NeonDocument); + const fullUrlPath = DocumentService.isQuickStartGuide(coercedMatchedDoc) + ? `${NeonEnvironment.getFullApiPath('quickStartGuides')}` + : `${NeonEnvironment.getFullApiPath('documents')}`; + return ( + + ); + }; + + return ( +
+ + + + + Select Document to View + + + + + + {renderSelectedDocument()} + + +
+ ); +}; + +const WrappedDocumentSelect = (Theme as any).getWrappedComponent(DocumentSelect); + +export default WrappedDocumentSelect; diff --git a/src/lib_components/components/Documents/DocumentTabs.tsx b/src/lib_components/components/Documents/DocumentTabs.tsx new file mode 100644 index 00000000..dd18790b --- /dev/null +++ b/src/lib_components/components/Documents/DocumentTabs.tsx @@ -0,0 +1,180 @@ +import React, { useState } from 'react'; + +import Tab from '@material-ui/core/Tab'; +import Tabs from '@material-ui/core/Tabs'; +import { + makeStyles, + createStyles, + Theme as MuiTheme, +} from '@material-ui/core/styles'; + +import DocumentListItem from './DocumentListItem'; +import DocumentService from '../../service/DocumentService'; +import DocumentViewer from './DocumentViewer'; +import NeonEnvironment from '../NeonEnvironment'; +import Theme from '../Theme/Theme'; +import WarningCard from '../Card/WarningCard'; +import { StylesHook } from '../../types/muiTypes'; +import { NeonDocument } from '../../types/neonApi'; +import { existsNonEmpty } from '../../util/typeUtil'; + +const useStyles: StylesHook = makeStyles((muiTheme: MuiTheme) => + // eslint-disable-next-line implicit-arrow-linebreak + createStyles({ + container: { + width: '100%', + display: 'flex', + margin: muiTheme.spacing(0, -0.5, -0.5, -0.5), + flexDirection: 'column', + }, + tabPanels: { + width: '100%', + backgroundColor: '#fff', + }, + tabContentContainer: { + width: '100%', + padding: muiTheme.spacing(3, 3, 3, 3), + }, + })) as StylesHook; + +const useTabsStyles: StylesHook = makeStyles((muiTheme: MuiTheme) => + // eslint-disable-next-line implicit-arrow-linebreak + createStyles({ + scroller: { + backgroundColor: muiTheme.palette.grey[200], + }, + scrollButtons: { + '&.Mui-disabled': { + opacity: 0.6, + }, + }, + })) as StylesHook; + +const useTabStyles: StylesHook = makeStyles((muiTheme: MuiTheme) => + // eslint-disable-next-line implicit-arrow-linebreak + createStyles({ + root: { + textTransform: 'none', + opacity: 1, + maxWidth: 464, + }, + wrapper: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + '& svg': { + margin: `${muiTheme.spacing(0, 1, 0, 0)} !important`, + }, + }, + selected: { + borderBottom: 'none', + }, + })) as StylesHook; + +interface DocumentTabModel { + index: number; + document: NeonDocument; +} + +export interface DocumentTabsProps { + documents: NeonDocument[]; +} + +const DocumentTabs: React.FC = (props: DocumentTabsProps): JSX.Element => { + const classes = useStyles(Theme); + const tabClasses = useTabStyles(Theme); + const tabsClasses = useTabsStyles(Theme); + const { documents }: DocumentTabsProps = props; + + const initialTabIdx = 0; + const [ + selectedTab, + setSelectedTab, + ]: [number, React.Dispatch>] = useState(initialTabIdx); + + if (!existsNonEmpty(documents)) { + return ( +
+ +
+ ); + } + + const docTabs: DocumentTabModel[] = documents.map( + (doc: NeonDocument, index: number): DocumentTabModel => ({ + document: doc, + index, + }), + ); + + const renderTabs = (): JSX.Element => (( + { setSelectedTab(newTab); }} + TabIndicatorProps={{ style: { display: 'none' } }} + > + {docTabs.map((docTab: DocumentTabModel): JSX.Element => (( + + )} + aria-label={docTab.document.name} + classes={tabClasses} + id={`document-tabs-tab-${docTab.index}`} + aria-controls={`document-tabs-tabpanel-${docTab.index}`} + /> + )))} + + )); + + const renderTabContent = (documentTab: DocumentTabModel): JSX.Element => { + const { document, index }: DocumentTabModel = documentTab; + const fullUrlPath = DocumentService.isQuickStartGuide(document) + ? `${NeonEnvironment.getFullApiPath('quickStartGuides')}` + : `${NeonEnvironment.getFullApiPath('documents')}`; + return ( +
+ +
+ ); + }; + + const renderTabPanels = (): JSX.Element => ( +
+ {docTabs.map((docTab: DocumentTabModel): JSX.Element => renderTabContent(docTab))} +
+ ); + + return ( +
+ {renderTabs()} + {renderTabPanels()} +
+ ); +}; + +const WrappedDocumentTabs = (Theme as any).getWrappedComponent(DocumentTabs); + +export default WrappedDocumentTabs; diff --git a/src/lib_components/components/DocumentViewer/DocumentViewer.tsx b/src/lib_components/components/Documents/DocumentViewer.tsx similarity index 65% rename from src/lib_components/components/DocumentViewer/DocumentViewer.tsx rename to src/lib_components/components/Documents/DocumentViewer.tsx index 0fb88844..41c08d89 100644 --- a/src/lib_components/components/DocumentViewer/DocumentViewer.tsx +++ b/src/lib_components/components/Documents/DocumentViewer.tsx @@ -11,29 +11,35 @@ import { Theme as MuiTheme, } from '@material-ui/core/styles'; +import DocumentService from '../../service/DocumentService'; +import ErrorCard from '../Card/ErrorCard'; import NeonEnvironment from '../NeonEnvironment'; import Theme from '../Theme/Theme'; import { StylesHook } from '../../types/muiTypes'; import { NeonDocument } from '../../types/neonApi'; +import { isStringNonEmpty } from '../../util/typeUtil'; const useStyles: StylesHook = makeStyles((muiTheme: MuiTheme) => // eslint-disable-next-line implicit-arrow-linebreak createStyles({ container: { width: '100%', - margin: muiTheme.spacing(3, 3, 3, 3), + }, + iframe: { + border: 'none', }, })) as StylesHook; export interface DocumentViewerProps { document: NeonDocument; width: number; + fullUrlPath?: string; } const noop = () => {}; const breakpoints: number[] = [0, 675, 900, 1200]; -const ratios: string[] = ['8:11', '3:4', '3:4', '3:4']; +const ratios: string[] = ['8:11', '3:4', '4:4', '4:3']; const calcAutoHeight = (width: number): number => { const breakIdx: number = breakpoints.reduce( @@ -52,10 +58,15 @@ const DocumentViewer: React.FC = (props: DocumentViewerProp const { document, width, + fullUrlPath, }: DocumentViewerProps = props; + const appliedUrlPath = isStringNonEmpty(fullUrlPath) + ? fullUrlPath + : NeonEnvironment.getFullApiPath('documents'); + const dataUrl: string = `${appliedUrlPath}/${document.name}?inline=true&fallback=html`; const containerRef: React.MutableRefObject = useRef(); - const embedRef: React.MutableRefObject = useRef(); + const iframeRef: React.MutableRefObject = useRef(); const [ viewerWidth, setViewerWidth, @@ -63,23 +74,23 @@ const DocumentViewer: React.FC = (props: DocumentViewerProp const handleResizeCb = useCallback((): void => { const container: HTMLDivElement|undefined = containerRef.current; - const embed: HTMLEmbedElement|undefined = embedRef.current; + const iframeElement: HTMLIFrameElement|undefined = iframeRef.current; // Do nothing if either container or viz references fail ot point to a DOM node - if (!container || !embed) { return; } + if (!container || !iframeElement) { return; } // Do nothing if either refs have no offset parent // (meaning they're hidden from rendering anyway) - if ((container.offsetParent === null) || (embed.offsetParent === null)) { return; } + if ((container.offsetParent === null) || (iframeElement.offsetParent === null)) { return; } // Do nothing if container and viz have the same width // (resize event fired but no actual resize necessary) if (container.clientWidth === viewerWidth) { return; } const newWidth: number = container.clientWidth; setViewerWidth(newWidth); - embed.setAttribute('width', `${newWidth}`); - embed.setAttribute('height', `${calcAutoHeight(newWidth)}`); - }, [containerRef, embedRef, viewerWidth, setViewerWidth]); + iframeElement.setAttribute('width', `${newWidth}`); + iframeElement.setAttribute('height', `${calcAutoHeight(newWidth)}`); + }, [containerRef, iframeRef, viewerWidth, setViewerWidth]); useLayoutEffect(() => { - const element = embedRef.current; + const element = iframeRef.current; if (!element) { return noop; } const parent: HTMLElement|null = element.parentElement; if (!parent) { return noop; } @@ -97,21 +108,36 @@ const DocumentViewer: React.FC = (props: DocumentViewerProp resizeObserver.disconnect(); resizeObserver = null; }; - }, [embedRef, handleResizeCb]); + }, [iframeRef, handleResizeCb]); + + const renderObject = (): JSX.Element => { + if (!DocumentService.isViewerSupported(document)) { + return ( + + ); + } + return ( +