Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Canvas][Layout Engine] Persistent grouping #25854

Merged
merged 23 commits into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
75370f2
Canvas element grouping (squashed)
monfera Oct 22, 2018
2f3a51b
lint
monfera Dec 14, 2018
754a022
separate elements and groups in storage
monfera Dec 16, 2018
e7c90b3
renamed `element...` to `node...` (except exported names and action p…
monfera Dec 16, 2018
9778e2e
be able to remove a group
monfera Dec 16, 2018
d1e57e6
fixing group deletion
monfera Dec 16, 2018
8815027
not re-persisting unnecessarily
monfera Dec 16, 2018
c8bda80
persist / unpersist group right on the keyboard action
monfera Dec 16, 2018
cf37642
solving inverted cascading for backward compatibility
monfera Dec 17, 2018
c35088e
fix failing test case
monfera Dec 17, 2018
f427511
page cloning with group trees of arbitrary depth
monfera Dec 17, 2018
b8cf380
extracted out cloneSubgraphs
monfera Dec 17, 2018
f0a3c97
basic copy-paste that handles a grouping tree of arbitrary depth
monfera Dec 17, 2018
b9de698
fix: when legacy dataset doesn't have `groups`, it should avoid an `u…
monfera Dec 17, 2018
175edb4
PR feedback: regularize group IDs
monfera Dec 18, 2018
d0dd92f
lint: curlies
monfera Dec 18, 2018
973e712
schemaVersion bump
monfera Dec 18, 2018
23b7bb5
copy/paste: restore selection and 10px offset of newly pasted element
monfera Dec 18, 2018
a1ddd79
- fix regression with ad hoc groups
monfera Dec 18, 2018
d0a77c8
PR feedback: don't persist node `type` and group `expression`
monfera Dec 18, 2018
20d575d
chore: remove commented out code
w33ble Dec 18, 2018
6ee1beb
chore: switch Object.entries to Object.keys
w33ble Dec 18, 2018
ee0dad3
fix: handle undefined value
w33ble Dec 18, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const handleMouseDown = (commit, e, isEditable) => {

const keyCode = key => (key === 'Meta' ? 'MetaLeft' : 'Key' + key.toUpperCase());

const isNotTextInput = ({ tagName, type }) => {
const isTextInput = ({ tagName, type }) => {
// input types that aren't variations of text input
const nonTextInputs = [
'button',
Expand All @@ -111,11 +111,11 @@ const isNotTextInput = ({ tagName, type }) => {

switch (tagName.toLowerCase()) {
case 'input':
return nonTextInputs.includes(type);
return !nonTextInputs.includes(type);
case 'textarea':
return false;
default:
return true;
default:
return false;
}
};

Expand All @@ -125,7 +125,7 @@ const handleKeyDown = (commit, e, isEditable, remove) => {
const { key, target } = e;

if (isEditable) {
if (isNotTextInput(target) && (key === 'Backspace' || key === 'Delete')) {
if ((key === 'Backspace' || key === 'Delete') && !isTextInput(target)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point, I'd like to see these become constants, perhaps in an enum in a separate module. I'd add a TODO, or an issue perhaps, for all follow-ups?

e.preventDefault();
remove();
} else if (!modifierKey(key)) {
Expand All @@ -137,6 +137,16 @@ const handleKeyDown = (commit, e, isEditable, remove) => {
}
};

const handleKeyPress = (commit, e, isEditable) => {
Copy link
Contributor

@w33ble w33ble Dec 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason you're not handling this in the handleKeyDown handler? My understanding is that both handlers will be called in most cases, with alt, ctrl, shift, and meta being the exception.

MDN: The keydown event is fired when a key is pressed down. Unlike the keypress event, the keydown event is fired for keys that produce a character value and for keys that do not produce a character value.

So pressing G or U is still going to trigger this:

      commit('keyboardEvent', {
        event: 'keyDown',
        code: keyCode(key), // convert to standard event code
      });

Maybe that's intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it could technically work both ways (actually, keyPress will activate on the upward trajectory), it was simply a question of showing intent - one is concerned with signaling intent with a keypress gesture - 'G' or 'U' here - while the other conveys up / down events.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference would be if you wanted to include control, shift, option, etc. Keypress would ignore keys combined with option on a mac, I believe.

Copy link
Contributor

@w33ble w33ble Dec 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, keypress does ignore those keys. My point was that onKeyDown is always going to be called, and that internal event would always be triggered, even though those specific keys are handled differently and in a different handler. Sounds like it shouldn't cause problems though.

const { key, target } = e;
const upcaseKey = key && key.toUpperCase();
if (isEditable && !isTextInput(target) && 'GU'.indexOf(upcaseKey) !== -1) {
commit('actionEvent', {
event: upcaseKey === 'G' ? 'group' : 'ungroup',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to get into the habit of creating constants for strings like this.

});
}
};

const handleKeyUp = (commit, { key }, isEditable) => {
if (isEditable && !modifierKey(key)) {
commit('keyboardEvent', {
Expand All @@ -150,6 +160,7 @@ export const withEventHandlers = withHandlers({
onMouseDown: props => e => handleMouseDown(props.commit, e, props.isEditable),
onMouseMove: props => e => handleMouseMove(props.commit, e, props.isEditable),
onKeyDown: props => e => handleKeyDown(props.commit, e, props.isEditable, props.remove),
onKeyPress: props => e => handleKeyPress(props.commit, e, props.isEditable),
onKeyUp: props => e => handleKeyUp(props.commit, e, props.isEditable),
onWheel: props => e => handleWheel(props.commit, e, props.isEditable),
resetHandler: () => () => resetHandler(),
Expand Down
53 changes: 40 additions & 13 deletions x-pack/plugins/canvas/public/components/workpad_page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@ import { compose, withState, withProps } from 'recompose';
import { notify } from '../../lib/notify';
import { aeroelastic } from '../../lib/aeroelastic_kibana';
import { setClipboardData, getClipboardData } from '../../lib/clipboard';
import { removeElements, duplicateElement } from '../../state/actions/elements';
import { cloneSubgraphs } from '../../lib/clone_subgraphs';
import { removeElements, rawDuplicateElement } from '../../state/actions/elements';
import { getFullscreen, canUserWrite } from '../../state/selectors/app';
import { getElements, isWriteable } from '../../state/selectors/workpad';
import { getNodes, isWriteable } from '../../state/selectors/workpad';
import { flatten } from '../../lib/aeroelastic/functional';
import { withEventHandlers } from './event_handlers';
import { WorkpadPage as Component } from './workpad_page';

const mapStateToProps = (state, ownProps) => {
return {
isEditable: !getFullscreen(state) && isWriteable(state) && canUserWrite(state),
elements: getElements(state, ownProps.page.id),
elements: getNodes(state, ownProps.page.id),
};
};

const mapDispatchToProps = dispatch => {
return {
duplicateElement: pageId => selectedElement =>
dispatch(duplicateElement(selectedElement, pageId)),
rawDuplicateElement: pageId => (selectedElement, root) =>
dispatch(rawDuplicateElement(selectedElement, pageId, root)),
removeElements: pageId => elementIds => dispatch(removeElements(elementIds, pageId)),
};
};
Expand Down Expand Up @@ -78,14 +80,35 @@ export const WorkpadPage = compose(
setUpdateCount,
page,
elements: pageElements,
rawDuplicateElement,
removeElements,
duplicateElement,
}) => {
const { shapes, selectedLeafShapes = [], cursor } = aeroelastic.getStore(
const { shapes, selectedPrimaryShapes = [], cursor } = aeroelastic.getStore(
page.id
).currentScene;
const elementLookup = new Map(pageElements.map(element => [element.id, element]));
const selectedElementIds = selectedLeafShapes;
const recurseGroupTree = shapeId => {
return [
shapeId,
...flatten(
shapes
.filter(s => s.parent === shapeId && s.type !== 'annotation')
.map(s => s.id)
.map(recurseGroupTree)
),
];
};
const selectedPrimaryShapeObjects = selectedPrimaryShapes.map(id =>
shapes.find(s => s.id === id)
);
const selectedPersistentPrimaryShapes = flatten(
selectedPrimaryShapeObjects.map(shape =>
shape.subtype === 'adHocGroup'
? shapes.filter(s => s.parent === shape.id && s.type !== 'annotation').map(s => s.id)
: [shape.id]
)
);
const selectedElementIds = flatten(selectedPersistentPrimaryShapes.map(recurseGroupTree));
const selectedElements = [];
const elements = shapes.map(shape => {
let element = null;
Expand Down Expand Up @@ -114,21 +137,25 @@ export const WorkpadPage = compose(
},
copyElements: () => {
if (selectedElements.length) {
setClipboardData(selectedElements);
setClipboardData({ selectedElements, rootShapes: selectedPrimaryShapes });
notify.success('Copied element to clipboard');
}
},
cutElements: () => {
if (selectedElements.length) {
setClipboardData(selectedElements);
setClipboardData({ selectedElements, rootShapes: selectedPrimaryShapes });
removeElements(page.id)(selectedElementIds);
notify.success('Copied element to clipboard');
}
},
pasteElements: () => {
const elements = JSON.parse(getClipboardData());
if (elements) {
elements.map(element => duplicateElement(page.id)(element));
const { selectedElements, rootShapes } = JSON.parse(getClipboardData());
const indices = rootShapes.map(r => selectedElements.findIndex(s => s.id === r));
const clonedElements = selectedElements && cloneSubgraphs(selectedElements);
if (clonedElements) {
clonedElements.map((element, index) =>
rawDuplicateElement(page.id)(element, indices.indexOf(index) >= 0)
);
}
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class WorkpadPage extends PureComponent {
isEditable,
onDoubleClick,
onKeyDown,
onKeyPress,
onKeyUp,
onMouseDown,
onMouseMove,
Expand Down Expand Up @@ -108,6 +109,7 @@ export class WorkpadPage extends PureComponent {
onMouseUp={onMouseUp}
onMouseDown={onMouseDown}
onKeyDown={onKeyDown}
onKeyPress={onKeyPress}
onKeyUp={onKeyUp}
onDoubleClick={onDoubleClick}
onAnimationEnd={onAnimationEnd}
Expand Down Expand Up @@ -150,7 +152,7 @@ export class WorkpadPage extends PureComponent {
default:
return [];
}
} else if (element.subtype !== 'adHocGroup') {
} else if (element.type !== 'group') {
return <ElementWrapper key={element.id} element={element} />;
}
})
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/canvas/public/lib/aeroelastic/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ const groupName = 'group';
const groupResize = true;
const guideDistance = 3;
const hoverAnnotationName = 'hoverAnnotation';
const hoverLift = 100;
const intraGroupManipulation = false;
const intraGroupSnapOnly = false;
const persistentGroupName = 'persistentGroup';
const resizeAnnotationOffset = 0;
const resizeAnnotationOffsetZ = 0.1; // causes resize markers to be slightly above the shape plane
const resizeAnnotationSize = 10;
const resizeAnnotationConnectorOffset = 0; //resizeAnnotationSize //+ 2
const resizeConnectorName = 'resizeConnector';
const rotateAnnotationOffset = 12;
const rotationEpsilon = 0.001;
const rotationHandleName = 'rotationHandle';
const rotationHandleSize = 14;
const resizeHandleName = 'resizeHandle';
Expand All @@ -43,15 +47,19 @@ module.exports = {
groupResize,
guideDistance,
hoverAnnotationName,
hoverLift,
intraGroupManipulation,
intraGroupSnapOnly,
minimumElementSize,
persistentGroupName,
resizeAnnotationOffset,
resizeAnnotationOffsetZ,
resizeAnnotationSize,
resizeAnnotationConnectorOffset,
resizeConnectorName,
resizeHandleName,
rotateAnnotationOffset,
rotationEpsilon,
rotateSnapInPixels,
rotationHandleName,
rotationHandleSize,
Expand Down
14 changes: 11 additions & 3 deletions x-pack/plugins/canvas/public/lib/aeroelastic/functional.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,21 @@ const not = fun => (...args) => !fun(...args);
const removeDuplicates = (idFun, a) =>
a.filter((d, i) => a.findIndex(s => idFun(s) === idFun(d)) === i);

const epsilon = 1 / 1000;
const applyTolerance = d => Math.round(d / epsilon) * epsilon;
const arrayToMap = a => Object.assign({}, ...a.map(d => ({ [d]: true })));

const subMultitree = (pk, fk, elements, roots) => {
const getSubgraphs = roots => {
const children = flatten(roots.map(r => elements.filter(e => fk(e) === pk(r))));
return [...roots, ...(children.length && getSubgraphs(children, elements))];
};
return getSubgraphs(roots);
};

module.exports = {
applyTolerance,
arrayToMap,
disjunctiveUnion,
flatten,
subMultitree,
identity,
log,
map,
Expand Down
3 changes: 1 addition & 2 deletions x-pack/plugins/canvas/public/lib/aeroelastic/geometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ const shapesAt = (shapes, { x, y }) =>
const getExtremum = (transformMatrix, a, b) =>
matrix.normalize(matrix.mvMultiply(transformMatrix, [a, b, 0, 1]));

const landmarkPoint = ({ localTransformMatrix, a, b }, k, l) =>
getExtremum(localTransformMatrix, k * a, l * b);
const landmarkPoint = (a, b, transformMatrix, k, l) => getExtremum(transformMatrix, k * a, l * b);

module.exports = {
landmarkPoint,
Expand Down
17 changes: 11 additions & 6 deletions x-pack/plugins/canvas/public/lib/aeroelastic/gestures.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ const metaHeld = select(appleKeyboard ? e => e.metaKey : e => e.altKey)(keyFromM
const optionHeld = select(appleKeyboard ? e => e.altKey : e => e.ctrlKey)(keyFromMouse);
const shiftHeld = select(e => e.shiftKey)(keyFromMouse);

// retaining this for now to avoid removing dependent inactive code `keyTransformGesture` from layout.js
// todo remove this, and `keyTransformGesture` from layout.js and do accessibility outside the layout engine
const pressedKeys = () => ({});

const cursorPosition = selectReduce((previous, position) => position || previous, { x: 0, y: 0 })(
rawCursorPosition
);
Expand All @@ -75,7 +71,12 @@ const mouseIsDown = selectReduce(
false
)(mouseButtonEvent);

const gestureEnd = select(next => next && next.event === 'mouseUp')(mouseButtonEvent);
const gestureEnd = select(
action =>
action &&
(action.type === 'actionEvent' ||
(action.type === 'mouseEvent' && action.payload.event === 'mouseUp'))
)(primaryUpdate);

/**
* mouseButtonStateTransitions
Expand Down Expand Up @@ -138,7 +139,12 @@ const dragVector = select(({ buttonState, downX, downY }, { x, y }) => ({
y1: y,
}))(mouseButtonState, cursorPosition);

const actionEvent = select(action => (action.type === 'actionEvent' ? action.payload : null))(
primaryUpdate
);

module.exports = {
actionEvent,
dragging,
dragVector,
cursorPosition,
Expand All @@ -148,6 +154,5 @@ module.exports = {
mouseDowned,
mouseIsDown,
optionHeld,
pressedKeys,
shiftHeld,
};
Loading