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

[Table] Support for column and row reordering #963

Merged
merged 65 commits into from
Apr 13, 2017

Conversation

cmslewis
Copy link
Contributor

@cmslewis cmslewis commented Apr 7, 2017

Fixes #213

Checklist

  • Include tests
  • Update documentation (will add language in a separate PR if we want it)
  • Add new example
  • Fix interaction regression w/ TruncatedPopover

Changes proposed in this pull request:

Behold: column and row reordering in Table.

Basic points:

  • Reordering works only if selection is also enabled. If this assumption is bad, let me know.
  • Reordering is performed by mousing down within a selected column header, then dragging. If reordering is enabled, it is no longer possible to deselect a column by clicking on its header. LMK how that feels; I don't love it yet.
  • You can enable reordering for columns, rows, or both via Table props.
  • You can reorder a single entity or a region of multiple entities (having FULL_COLUMNS or FULL_ROWS cardinality).
  • You CANNOT reorder anything if there are multiple regions selected. It's too much of a pain for a v1 implementation, plus initial feedback suggested that no user will be able to predict what will happen anyway. We can revisit this later if we want.
  • The Table does not handle the actual reordering! It simply emits (oldIndex, newIndex, length) and lets the caller pass in updated data.

Reviewers should focus on:

  • High-level architecture. Does it look reasonable?
  • One thing that felt a little gross was the introduction of a "reordering cursor overlay" that displays invisibly over the whole table while you're dragging to reorder. I couldn't think of a better way to guarantee that the cursor would appear as grabbing no matter where your mouse moved during the drag. LMK what you think.

Screenshot

2017-04-07 12 38 28

Use it in DragSelectable to prevent DragReorderable from triggering
its handleActivate on the same mousedown event.
@cmslewis
Copy link
Contributor Author

cmslewis commented Apr 12, 2017

I'm also seeing a lot of duplicated functionality between RowHeader and ColumnHeader. We should pull the common code into an AbstractHeader component, e.g. Would prefer to do that in a separate PR.

@blueprint-bot
Copy link

Bugfix: enable reordering right away if selection exists on mount

Preview: documentation | landing | table
Coverage: core | datetime

@blueprint-bot
Copy link

Fix tests and increase coverage to 79% again

Preview: documentation | landing | table
Coverage: core | datetime

country: array[3],
city: array[4],
// tslint:enable:object-literal-sort-keys
}));
Copy link
Contributor

Choose a reason for hiding this comment

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

i mean you might as well go hard...

([letter, fruit, ...]) => ({ letter, fruit, ... })

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

class ReorderableTableExample extends React.Component<{}, IReorderableTableExampleState> {
public state = {
data: REORDERABLE_TABLE_DATA,
} as IReorderableTableExampleState;
Copy link
Contributor

Choose a reason for hiding this comment

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

always better to declare type up front instead of casting. sometimes you lose type safety when casting, which you don't lose if you type the var first.

public state: IReorderableTableExampleState = { ... }

Copy link
Contributor Author

@cmslewis cmslewis Apr 13, 2017

Choose a reason for hiding this comment

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

Done

isResizable: true,
loading: false,
};

public state = {
hasSelectionEnded: false,
} as IColumnHeaderState;
Copy link
Contributor

Choose a reason for hiding this comment

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

again, do it up front. and on one line.
public state: IColumnHeaderState = { hasSelectionEnded: false };

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

return Regions.column(index1, index2);
}

private isColumnTemporarilyReorderable(isColumnSelected: boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this for? what do these five conditions mean? code comment please

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -168,50 +205,98 @@ export class ColumnHeader extends React.Component<IColumnHeaderProps, {}> {
});
const cellLoading = cell.props.loading != null ? cell.props.loading : loading;
const isColumnSelected = Regions.hasFullColumn(selectedRegions, columnIndex);
const cellProps: IColumnHeaderCellProps = { className, isColumnSelected, loading: cellLoading };
const isColumnTemporarilyReorderable = this.isColumnTemporarilyReorderable(isColumnSelected);
Copy link
Contributor

Choose a reason for hiding this comment

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

does it matter that it's "temporary?" i think "currently" might be a better word, cuz it's determined by props and state. but that's just pure react, so perhaps we don't need such a word at all.

usage doesn't suggest any notion of temporary--you immediately rename it to isColumnReorderable, which as a prop is understood to only matter in the now, not next render.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah wasn't sure about this, but I wanted to more strongly disambiguate isReorderable from isColumnReorderable. It's not necessarily clear at a glance what the difference is.

@@ -161,45 +197,93 @@ export class RowHeader extends React.Component<IRowHeaderProps, {}> {
});
const cellLoading = cell.props.loading != null ? cell.props.loading : loading;
const isRowSelected = Regions.hasFullRow(selectedRegions, rowIndex);
const cellProps: IRowHeaderCellProps = { className, isRowSelected, loading: cellLoading };
const isRowTemporarilyReorderable = this.isRowTemporarilyReorderable(isRowSelected);
Copy link
Contributor

Choose a reason for hiding this comment

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

again with the temporary

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

private getDragSelectedRegions(event: MouseEvent, coords: ICoordinateData) {
let region = (this.props.allowMultipleSelection) ?
this.props.locateDrag(event, coords) :
this.props.locateClick(event);
Copy link
Contributor

Choose a reason for hiding this comment

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

condition
    ? true
    : false;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops. Done

@@ -548,11 +541,52 @@ export class Regions {
}
}

private static overlapsRegionInternal(regions: IRegion[], query: IRegion, allowPartialOverlap = true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

👎 why can't this just be overlapsRegion? the third prop is optional with a default anyway.
i don't see the need for both.

private static regionsEqual(regionA: IRegion, regionB: IRegion) {
return Regions.intervalsEqual(regionA.rows, regionB.rows)
&& Regions.intervalsEqual(regionA.cols, regionB.cols);
}

private static regionContains(regionA: IRegion, regionB: IRegion) {
// containsRegion expects an array of regions as the first param
return Regions.overlapsRegionInternal([regionA], regionB, /* allowPartialOverlap */ false);
Copy link
Contributor

Choose a reason for hiding this comment

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

i don't think we need the prop name comment in typescript. i can easily see it by hovering on function name.

@blueprint-bot
Copy link

Update hasSelectionEnded on componentWillReceiveProps

Preview: documentation | landing | table
Coverage: core | datetime

@cmslewis
Copy link
Contributor Author

cmslewis commented Apr 13, 2017

@giladgray - I fixed the stuff in your CR just now.

@blueprint-bot
Copy link

Respond to Gilad CR feedback

Preview: documentation | landing | table
Coverage: core | datetime

* Enables/disables the reordering interaction.
* @default true
*/
isReorderable?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with @themadcreator, this should not be exposed at this level if we don't expect to support it. How about if you make it @internal, does that work? If not, you could try to use component context to allow <Table> to talk to its sub components.

* @param newIndex the new index of the element or set of elements
* @param length the number of contiguous elements that were moved
*/
onReordered: (oldIndex: number, newIndex: number, length: number) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

onReorderEnd might be more intuitive.

Copy link
Contributor Author

@cmslewis cmslewis Apr 13, 2017

Choose a reason for hiding this comment

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

Yeah, I couldn't find great precedent for this in the codebase. Slider does onChange and onRelease, and DragSelectable coincidentally now has onSelection and onSelectionEnd as of a recent commit in this PR. Regardless, do you think onReordering still makes sense if we change onReordered to onReorderEnd?

Copy link
Contributor

Choose a reason for hiding this comment

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

well, wouldn't onReorder work? You have onReordering while it's going then onReorder once it's finished. That kind of matches the other method names

* @param newIndex the new index of the element or set of elements
* @param length the number of contiguous elements that were moved
*/
onReordering: (oldIndex: number, newIndex: number, length: number) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this useful for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updating the position of the vertical and horizontal guides as you drag. See table.tsx.

@blueprint-bot
Copy link

Make headers' isReorderable prop @internal

Preview: documentation | landing | table
Coverage: core | datetime

@themadcreator
Copy link
Contributor

themadcreator commented Apr 13, 2017

@cmslewis if I were you, I'd be itching to merge this but I do think that perf regression is pretty bad. Also, it's probably just a matter of checking the stack of shouldComponentUpdates and making sure we're not creating new function instances for props, etc.

Drop some console logs in the render methods and compare before after your changes. If you can't find anything obvious with a concerted effort, we can move on, but I expect users who upgrade to this will not be happy.

Edit: Upon further investigation, I've noticed the same perf issue in @gscshoyru's auto-resize PR. So either you both broke it in the same way, or some other change caused the issue. Would still like someone to look into it though less pressure to do it in this PR.

@themadcreator
Copy link
Contributor

@cmslewis re: RowHeader vs. ColumnHeader, I also noticed this and other PR reviewers have suggested it as well!

I think I didn't originally do it because there were so many places where we use x/width vs. y/height sprinkled around that it seemed a little annoying to combine. But, If you can reduce those class files by moving at least a few methods into an abstract class, go for it! (not necessary for this PR obvs)

@adidahiya
Copy link
Contributor

Yeah I saw the perf issues on @gscshoyru's PR too. Are you sure it's not a difference caused by using production vs. dev builds of React?

Either way, try using why-did-you-update.

@giladgray
Copy link
Contributor

approving since the code change looks legit, but we def need some serious perf work ASAP.

@cmslewis
Copy link
Contributor Author

@themadcreator @adidahiya @giladgray would love to look into perf stuff ASAP. From chatting offline, sounds like our plan is to merge this PR and then address perf immediately in another one. The next Blueprint release would block on the followup PR merging. Sound good?

@cmslewis
Copy link
Contributor Author

Also @giladgray FYI I just pushed a fix for a minor visual regression (hiding selected regions while resizing) and added tests to verify everything works there as expected.

@blueprint-bot
Copy link

Re-add isGuidesShowing behavior + unit tests

Preview: documentation | landing | table
Coverage: core | datetime

Copy link
Contributor

@themadcreator themadcreator left a comment

Choose a reason for hiding this comment

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

Cool, let's do this! What a sweet feature

@@ -56,6 +56,10 @@ export class ElementHarness {
this.element = element;
}

public exists() {
return this.element != null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@adidahiya
Copy link
Contributor

:shipit:

@adidahiya adidahiya merged commit 2afd5ea into master Apr 13, 2017
@adidahiya adidahiya deleted the cl/table-reorder-cols-rows branch April 13, 2017 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants