diff --git a/src/components/app.jsx b/src/components/app.jsx index a5541182..b5fb1c76 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -58,7 +58,7 @@ export default function App() { guideRef.current.startGuideFunc()} /> - + !guideRef.current.isGuideRunning()} /> {reloadSplasher} diff --git a/src/components/cube-column-chooser.jsx b/src/components/cube-column-chooser.jsx index 562bd8fb..8f3929cb 100644 --- a/src/components/cube-column-chooser.jsx +++ b/src/components/cube-column-chooser.jsx @@ -17,14 +17,15 @@ function findAttribute(event, attrName) { } export default function CubeColumnChooser({ - alignTo, arrowStyle, chooseColumn, selectableColumns, + alignTo, arrowStyle, chooseColumn, selectableColumns, closeOnClickOutside, }) { const [filter, setFilter] = useState(''); const inputRef = useRef(null); const selfRef = useRef(null); - - useClickOutside(selfRef, true, () => { - chooseColumn(null); + useClickOutside(selfRef, closeOnClickOutside(), () => { + if (closeOnClickOutside()) { + chooseColumn(null); + } }); const filteredColumnsOptions = selectableColumns.filter(item => item.title.toLowerCase().indexOf(filter) >= 0); @@ -75,8 +76,10 @@ CubeColumnChooser.propTypes = { arrowStyle: PropTypes.string, chooseColumn: PropTypes.func.isRequired, selectableColumns: PropTypes.arrayOf(PropTypes.object).isRequired, + closeOnClickOutside: PropTypes.func, }; CubeColumnChooser.defaultProps = { arrowStyle: '', + closeOnClickOutside: () => true, }; diff --git a/src/components/cube.jsx b/src/components/cube.jsx index 3062e32d..6597615e 100644 --- a/src/components/cube.jsx +++ b/src/components/cube.jsx @@ -7,7 +7,7 @@ import HypercubeTable from './hypercube-table'; import './cube.pcss'; -export default function Cube({ app, tableData: { initialColumns } }) { +export default function Cube({ app, tableData: { initialColumns }, closeOnClickOutside }) { const selectableColumns = useColumnOptions(app); const [columns, setColumns] = useState(initialColumns); const currentHeader = useRef(null); @@ -55,8 +55,7 @@ export default function Cube({ app, tableData: { initialColumns } }) { toggleAdd(event); } - const popup = addOpen.current ? : null; - + const popup = addOpen.current ? : null; const measures = columns.filter(column => column.type === 'measure'); const dimensions = columns.filter(column => column.type === 'dimension' || column.type === 'field'); @@ -78,7 +77,12 @@ export default function Cube({ app, tableData: { initialColumns } }) { ); } +Cube.defaultProps = { + closeOnClickOutside: () => true, +}; + Cube.propTypes = { app: PropTypes.object.isRequired, tableData: PropTypes.object.isRequired, + closeOnClickOutside: PropTypes.func, }; diff --git a/src/components/cubes.jsx b/src/components/cubes.jsx index 8f433b5f..9d5306b9 100644 --- a/src/components/cubes.jsx +++ b/src/components/cubes.jsx @@ -9,7 +9,7 @@ import CubeColumnChooser from './cube-column-chooser'; import './cubes.pcss'; -export function Cubes({ app }) { +export function Cubes({ app, closeOnClickOutside }) { const [count, setCount] = useState(1); const [cubeList, setCubeList] = useState([]); const [, forceUpdate] = useState(null); @@ -42,14 +42,14 @@ export function Cubes({ app }) { forceUpdate(); } - const popup = addOpen.current ? addCube(column)} /> : null; + const popup = addOpen.current ? addCube(column)} closeOnClickOutside={closeOnClickOutside} /> : null; const cubeDivs = cubeList.map(cube => (
HYPERCUBE
removeCube(cube.id)} />
- +
)); return ( @@ -67,6 +67,7 @@ export function Cubes({ app }) { Cubes.propTypes = { app: PropTypes.object.isRequired, + closeOnClickOutside: PropTypes.func.isRequired, }; export default Cubes; diff --git a/src/components/guide-steps.jsx b/src/components/guide-steps.jsx index 90b2bf0d..0a014b48 100644 --- a/src/components/guide-steps.jsx +++ b/src/components/guide-steps.jsx @@ -182,8 +182,7 @@ const steps = [ ), spotlightClicks: true, - disableOverlayClose: true, - hideFooter: true, + placement: 'left-end', target: '.add-button', title: 'Hypercube builder', }, @@ -195,8 +194,6 @@ const steps = [

Click on an entity to select it.

), - disableOverlayClose: true, - hideFooter: true, spotlightClicks: true, target: '.cube-column-chooser', title: 'Hypercube builder', @@ -213,26 +210,25 @@ const steps = [

Add another column by clicking the plus button.

), - disableOverlayClose: true, - hideFooter: true, spotlightClicks: true, - target: '.card', + hideBackButton: true, + target: '.card:last-child', title: 'Hypercube builder', }, { step: 'selectAnotherEntity', content: 'Click an entity to add it as a column in the cube.', placement: 'left', - disableOverlayClose: true, - hideFooter: true, spotlightClicks: true, + hideBackButton: true, target: '.cube-column-chooser', title: 'Hypercube builder', }, { step: 'cubeFinished', content: 'More columns can be added to the cube. To close the cube, just click the button in the upper corner.', - target: '.card', + hideBackButton: true, + target: '.card:last-child', title: 'Hypercube builder', }, { diff --git a/src/components/guide.jsx b/src/components/guide.jsx index e3dbb51e..ac72e33d 100644 --- a/src/components/guide.jsx +++ b/src/components/guide.jsx @@ -24,8 +24,23 @@ const Guide = forwardRef((props, ref) => { setRunGuide(true); } }, + isGuideRunning() { + return runGuide; + }, })); + const getNbrOfColumnsInCube = () => { + let nbrOfColumns = 0; + const table = document.getElementsByClassName('hypercube-table'); + if (table.length > 0) { + const virtTable = table[table.length - 1].getElementsByClassName('ReactVirtualized__Table'); + if (virtTable.length > 0) { + nbrOfColumns = virtTable[0].getAttribute('aria-colcount'); + } + } + return nbrOfColumns; + }; + const setStep = (stepName) => { setRunGuide(false); setStepIndex(steps.findIndex(s => s.step === stepName)); @@ -49,14 +64,7 @@ const Guide = forwardRef((props, ref) => { setStep('selectEntity'); } else if (parentElemName === 'expression' || parentElemName === 'expression-list') { // a click on an expression in the hypercube builder. - let nbrOfColumns = 0; - const table = document.getElementsByClassName('hypercube-table'); - if (table.length > 0) { - const virtTable = table[0].getElementsByClassName('ReactVirtualized__Table'); - if (virtTable.length > 0) { - nbrOfColumns = virtTable[0].getAttribute('aria-colcount'); - } - } + const nbrOfColumns = getNbrOfColumnsInCube(); if (nbrOfColumns > 0) { setStep('cubeFinished'); } else { @@ -75,19 +83,61 @@ const Guide = forwardRef((props, ref) => { document.removeEventListener('mouseup', onClick); }; + const readyToProceed = (newStepIndex) => { + if (newStepIndex < steps.length) { + const stepName = steps[newStepIndex].step; + if (stepName === 'addAnotherColumn') { + // we need to have a hypercube with a column in order for the step to be valid. + const nbrOfColumns = getNbrOfColumnsInCube(); + if (nbrOfColumns <= 0) { + return false; + } + } else if (stepName === 'selectAnotherEntity') { + const overlay = document.getElementsByClassName('cube-column-chooser'); + if (!overlay.length > 0) { + return false; + } + } else if (stepName === 'cubeFinished') { + const nbrOfColumns = getNbrOfColumnsInCube(); + if (nbrOfColumns > 0) { + return false; + } + } + } + // we are ready to proceed with this step. + return true; + }; + const handleJoyrideCallback = (data) => { const { action, index, type, status, } = data; if ([EVENTS.TOUR_START].includes(type)) { document.addEventListener('mouseup', onClick); + } else if ([ACTIONS.START].includes(action) && index === 0) { + // if the guide is restarted there will be no EVENTS.TOUR_START, the EventListener must + // be added on ACTION.START and step 0. + document.addEventListener('mouseup', onClick); } else if ([ACTIONS.CLOSE].includes(action) || [STATUS.FINISHED, STATUS.SKIPPED].includes(status)) { if (runGuide) { endGuide(); } - } else if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type)) { + } else if ([EVENTS.STEP_AFTER].includes(type)) { const newStepIndex = index + (action === ACTIONS.PREV ? -1 : 1); - // Update state to advance the guide + if (action !== ACTIONS.PREV && !readyToProceed(newStepIndex)) { + // we are not ready to proceed to the next step. Reset the current step. + // The stop/start of the guide is needed to reset the Joyride events, or it will + // be somewhere in between steps, statewise. + setRunGuide(false); + setStepIndex(index); + setTimeout(() => setRunGuide(true), 100); + } else { + // Update state to advance the guide + setStepIndex(newStepIndex); + } + } else if ([EVENTS.TARGET_NOT_FOUND].includes(type)) { + // The target could not be found. Go to previous step. + const newStepIndex = index - 1; setStepIndex(newStepIndex); } };