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

Sorting previous/next semantics for weight #338

Closed
Keats opened this Issue Jul 17, 2018 · 31 comments

Comments

Projects
None yet
3 participants
@Keats
Copy link
Collaborator

Keats commented Jul 17, 2018

The current semantic of prev/next on page seem wrong when looking at weights

Given 3 posts with date [2017-01-01, 2018-01-01, 2019-01-01]. The pages will be sorted in a descending order (2019, 2018, 2017) and the pages will have the following next/prev:

  • 2019 => next = None, previous = 2018
  • 2018 => next = 2019, previous = 2017
  • 2017 => next = 2018, previous = None

The next/previous make sense here since we are talking about the next blog post would be the most recent and the previous as the older one.

Enter order and we get what we would expect:

3 posts with order = [3, 2, 1]

Expectations:

  • 3 => next = None, previous = 2
  • 2 => next = 3, previous = 1
  • 1 => next = 2, previous = None

Lastly, weight:

3 posts with weight = [1, 2. 3]

Expectations:

  • 1 => next = 2, previous = None
  • 2 => next = 3, previous = 1
  • 3 => next = None, previous = 2

Currently, the previous and next are reversed for weight which makes sense since both date/order work that way but weight actually works the other way semantically: it's ascending.
I think it would make sense to inverse setting next/previouswhen weight is used to match the expectations.

2 questions:

  • am I getting something wrong with my expectations about the weight ordering?
  • would switching the next/previous for weight make sense to everyone?

cc @codesections

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 17, 2018

Just to make sure I understand both the current situation and the proposed change, is the table below accurate for 3 posts with weight = [1, 2. 3]?

post title current previous current next proposed previous proposed next
1 2 -- -- 2
2 3 1 1 3
3 -- 2 2 --

Assuming that's right, I think that's a good change and would help with the sort of issues that come up in the Book theme: anytime there's a list of content that isn't a blog, it might make sense to talk about the next item in the list as the one below the current item, not the one above it. The proposed change in the way weight defines previous and next would help address this non-blog use case.

More generally, I find the semantics of "order" and "weight" fairly confusing/non-intuative. If I say "go to my blog and look at the third entry", I would typically mean the third from the top, not the third from the bottom. That's even more true if I were talking about a list of products instead of a list of blog posts; I would always think that the "third" product entry was third from the top. So I'm constantly getting order backwards in my head and having to remember to reverse it.

Similarly, I get weight confused. There's part of me that thinks of "weight" as "how important something is", and thus expects whatever is given the most importance/weight to be presented first. So I'm also constantly getting weight backwards, and having to catch myself.

I'm not suggesting that you switch the two—that would not only be extrodinarially confusing for existing users, but would also have its own semantic dificulties (there is a logic to weight "sinking" to the bottom of a list, after all).

But I wonder if the semantics would be improved by using names like assending and decending, and getting away from "order" and "weight" entierly? (Though potentially leaving order and weight for backwards compatability.)

In my dream world—and I have no idea if this is feasable and don't know enough Rust to implement it myself—here's how it would work:

  • Users can only set two variables in the .md toml header:
    • order (a number)
    • date (a datestring)
  • Users can then set two variables in the section _index.md toml header:
    • sort_by: either "date" or ""order"
    • sort_direction: "assending" or "decending" (Or just leave this off and let people handle it with Tera's | reverse filter)

If the user selects sort_by = "date", then they would have access to page.date_earlier and a page.date_later objects, and if they select sort_by = "order", then they'd have access to page.order_higher and page.order_lower objects. And then you woudn't need to worry about the semantics around next and previous and the various different use-cases that make those non-intuative.

But, like I said, that's just a dream implimentation, and it isn't something I could do myself right now. So, the solution you proposed looks good to me 👍

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 17, 2018

The table is correct.

I am tempted to remove one of order or weight to be honest since I'm getting confused from them as well.
You can get the other one by calling the reverse filter on the pages anyway.

I know @phil-opp uses order in his blog: https://github.com/phil-opp/blog_os/tree/master/blog/content/second-edition/posts where it makes sense if we are talking about chapters and just increment a counter as we write, while having the latest ones at the top.
I believe weight could be removed entirely from gutenberg page sorting and be replaced in practice by order, unless I'm missing something. weight of section could be renamed to order as well to remove the weight notion from Gutenberg entirely.

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 17, 2018

That seems like a good option. It would still leave the question of what to do with page.previous and page.next. I could see two options:

  • Change nothing in the code, but add a note to the documentation to the effect that page.next always refers to the page with the order senquentally after the current page, and page.previous always refers to the page with the order senquentally below the current page. If we do that, we'd just accept that page.previous and page.next sometimes don't match their intuative "feel" in certain use-cases—but with good docs, that shouldn't be too big of an issue.
  • Change page.next to page.order_higher and change page.previous to page.order_lower. This would make that usage a bit more clear, helping people understand that the | reverse filter doesn't have any effect on the variables they have access to (as next and previous might incorrectly suggest). Even though it uses the word "order", I don't think that the order_lower and order_higher semantics would be confusing for people sorting by date, since dates can be ordered higher and lower as well.
@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 18, 2018

I don't think changing the names previous/next would be wise.

Looking at hugo/jekyll, it seems they actually only use weight so it might be worth looking at how they do things. Using order for the gutenberg documentation for example would require to reverse when wanting to show the list of pages in ascending order like in a menu.

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 19, 2018

I looked into how Hugo does it, and the short version is that it handles weight just like Gutenberg would with your proposed change.

Given all that, I think your suggestion is best: remove order, and change page.previous and page.next when used with sort_by = "weight" to match the expectations you described in your first post. This may mean that page.next sometimes points in a different direction than someone would initially expect—but this would happen after someone uses the | reverse filter. And this behaviour can be documented, and the | reverse filter should present a clue that order might be, well, reversed.

Here are the details about how Hugo does it:

In Hugo, you loop though pages by writing {{ range .Pages.ByWeight }}. This will loop through them, starting with the lowest-weight page, and contenuing on to the highest-weight page. Within each page, there are {{ .Next }} and {{ .Prev }}. {{ .Next }} points to the page with the weight that is just larger than the current page's weight (just as your proposed change would do). Similarly, {{ .Prev }} points to the page with lower weight, again like your proposed change.

Calling {{ range .Pages.ByWeight.Reverse }} reverses the order but, of course, does not change the {{ .Prev }} or {{ .Next }} pages. This means that {{ .Prev }} or {{ .Next }} may point in a non-intuative case sometimes, but it doesn't seem to be a big deal at all.

(By the way, diving back into the Hugo docs/syntax remind me of how much I prefer Tera's syntax and how greatful I am for your work on this project. So thanks so much for that!)

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 19, 2018

I remember having a very similar discussion some time ago and @phil-opp preferred order.
weight makes more sense to me but I definitely think having both is confusing so one should go away.

(By the way, diving back into the Hugo docs/syntax remind me of how much I prefer Tera's syntax and how greatful I am for your work on this project. So thanks so much for that!)

That's the main reason why I started Tera and Gutenberg later ;)

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 19, 2018

[EDIT: I was getting previous and next backwards in this comment, as @phil-opp's helpful comment below pointed out. I retract the rest of this comment. See my follow-up comment below for my view]

The more I think about it, the more I think that the solution you articulated in the first post to this issue is best: just flip the behaviour of next/previous in weight but leave everything else the same. (That is, leave both weight and order in existence.)

This would have a couple of advantages:

  • No need to pick between weight and order. As you point out, some people have a preference for one over the other, and—regardless of which way Gutenberg goes—there would be some people who would prefer the other semantics.
  • (More important:) Better semantics for next and previous. The result of keeping both and flipping the meaning for next/previous is that people can get any set of sematics that they want. Do you want pages to be ordered 1, 2, 3, with 3 next from 2? Ok, then sorty by weight. Do you want them ordered 1, 2, 3 but to have 1 next from 2? Then sort by order but use the | reverse filter. (And so on and so forth). This lets people control exactly where next and previous point to get to the semanitics that make the most sense for their use case.

The (minor) downside to this is that wieght and order can be a bit confusing—the issue that I went into before. But, the more I think about it, the more I beleive that this can be addressed through documentation, which is the area where I can best contribute now.

If that sounds good, I'd be happy to get to work on a pull request to the docs describing this behavior.

@phil-opp

This comment has been minimized.

Copy link
Contributor

phil-opp commented Jul 19, 2018

I think this is so confusing because we try to configure two things:

  1. The semantic order of posts, i.e. which post comes after which other post and how should the next/prev links be populated. From the discussion it seems like the order should always be 1->2->3->4->…, for both order and weight.
  2. The order in which the posts should be printed by default. This differs between the two types: for order the (semantically) first post is printed first and for weight the last post is printed first. It's easy to switch the printing order around using reverse.

So if we reverse the semantic order of weight (i.e. swap next and previous), there is not much sense in keeping both order and weight, since they're almost identical then (one can be replaced by the other with a reverse).

My personal preference is order because it is a name that expresses what it defines: the semantical order of posts. The display order doesn't really matter, as one can always use reverse if one does not like the default.

The problem I see with weight is that it's more associated with the display order, i.e. in which order the posts are displayed in a list. There are use cases for both display orderings, e.g. latest first for a blog and oldest first for a tutorial series, but only one sensible semantic ordering: latest after oldest. So if I want to put a post on the top I give it the lowest weight (or the highest?), or alternatively I give it the highest and use reverse. But what happens to the semantical ordering then? Which post comes before which (from top to bottom or the other way around)? All in all I find it much more confusing to think with weight than with order.

Another argument for keeping order instead of weight is that we minimize breakage that way. If we removed order and change the semantic ordering with weight, we would break users of both types. Otherwise, we only break weight users.

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 19, 2018

Based on @phil-opp's points and thinking more about all the possible use cases, I actually think that everything in the code should stay exactly the way it currently is, but that the documentation should be updated to more clearly explain how it works.

Here is my logic. If we set asside sorting by date, the user can currently make two choices: 1) chose to sort by weight or by order and 2) chose to use the reverse filter or not. By combining these two choices, the user can get whatever set of semantics works best for their use case.

I'll walk through all four options:

The blog sort: Here, the user wants to list posts by the order that they post them (the first-in-time post is 1 (and at the bottom of the list), the second-in-time post is 2, etc. They also want previous to point to the post that was posted before the current post; that is, they want previous for 2 to be 1. To get this behaviour, the user sets sort_by = "order" and does not use the | reverse filter.

The book sort: This user wants to write a book, where chapter 1 comes at the top of the list, followed by 2, etc. Similarly, they want the previous from 2 to be 1. To get this behaviour, they use sort_by = "order" but use the | reverse filter so that 1 will be the first/top page.

The ecommerce sort: This user wants item 1 to come at the top of the list, followed by the previous item, item 2. To get this behaviour, they set sort_by = "weight" and don't use | reverse.

Weight-as-importance sort: This user wants to use weight to evaluate the importance of different items on the list, and then have the most important item shown at the top of the list. previous should then point down the list to the item with the next-most importance. To get this behavior, they set sort_by = "weight" and use reverse.

I know that some of those use cases might be more common than others. But it seems to me that the current code can support all possible use cases and—with a bit of documentation work that I'm happy to take on—can be clear enough that users can tell exactly what combination of order/weight and | reverse they need to use to get the desired behavior.

If no one has an objection, I'll work on a pull request to the docs that clarifies how the current system works. (There will probably be a table!) If nothing else, it would be good to have an explanation firm enough in my own head that I can stop getting confused the way I did earlier in this thread.

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 20, 2018

The issue with the book sort is that for example for the book theme, I can use order without reverse for the sidebar but I end up having to do some weird things semantic wise like https://github.com/Keats/book/pull/3/files#diff-11720902627aeb97a05b6bad68f479edR19

Let's improve the documentation then if it makes more sense that way

@phil-opp

This comment has been minimized.

Copy link
Contributor

phil-opp commented Jul 23, 2018

@Keats I'm not sure what you mean exactly. Do you want the earliest chapter first or the latest chapter?

@codesections I think the sorting is currently implemented as ascending so that we have to use reverse for the blog sort but not for the book sort.


For my blog_os project, the sorting with order feels pretty natural for me, e.g. getting the latest post. Previous/next also works as expected, i.e. 1 is previous from 2 and 5 is next from 4.

For me it makes sense to think of the posts as array, i.e. the post with the lowest order has the lowest array index and the post with the highest order the highest array index. So to print them ascending (lowest order to highest) I iterate over the array front-to-back and for descending I reverse the array and do the same.

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 23, 2018

@phil-opp
Seeing things like {% set last_page = subsection.pages | first %} doesn't make much sense to me, I should do last to get the last page, not first. That's only an issue that happens when doing specific things in templates though, so it isn't a big deal

@phil-opp

This comment has been minimized.

Copy link
Contributor

phil-opp commented Jul 23, 2018

@Keats I took a closer look at your book example and I think the following things lead to confusion:

  • The pages array is not sorted ascending as one would expect, but descending. So my above comment was wrong about this. I just noticed that I'm creating a local variable with reversed order as a workaround for this issue in blog_os. Reversing the order makes section.pages | first and section.pages | last work as expected.
  • Sections can be only ordered by weight, not by order. However, it seems like they're treated like an order (i.e. smallest first). Maybe rename the corresponding key to section_order?
  • Sections are sorted randomly without a weight key. I think there should be at least a warning for any non-deterministic behavior.
@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 23, 2018

Sections can be only ordered by weight, not by order. However, it seems like they're treated like an order (i.e. smallest first). Maybe rename the corresponding key to section_order?

That's how weight is usually treated, isn't it? We all (including me) keep getting this mixed up, but weight lets the "lightest" item rise to the "top" of the list. I don't think there's any difference between how weight works for pages and sections.

Sections are sorted randomly without a weight key. I think there should be at least a warning for any non-deterministic behavior.

I agree with this. I added a warning in the docs (#343) but think it would be better to add a console warning (similar to how Gutenberg warns about pages that aren't being rendered when they lack the relevant front-matter variable). We discussed this a bit in #335.

@phil-opp

This comment has been minimized.

Copy link
Contributor

phil-opp commented Jul 23, 2018

We all (including me) keep getting this mixed up, but weight lets the "lightest" item rise to the "top" of the list. I don't think there's any difference between how weight works for pages and sections.

Could be that I mixed this up :). The question is where the "top" of a list is. The first item would make sense when it's rendered. However it's also common to name the last item the "top" for lists that are used as a stack. Order is much clearer in this regard.

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 24, 2018

In the case of subsections, it's always used for some kind of menu ordering so weight makes more sense (imo) there: 1 at the top, 2 below etc

@phil-opp

This comment has been minimized.

Copy link
Contributor

phil-opp commented Jul 24, 2018

In the case of subsections, it's always used for some kind of menu ordering so weight makes more sense (imo) there: 1 at the top, 2 below etc

Wouldn't it be the same with order?

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 25, 2018

In the case of subsections, it's always used for some kind of menu ordering so weight makes more sense (imo) there: 1 at the top, 2 below etc

Wouldn't it be the same with order?

Nope. With the current implementation, order would put 1 at the bottom. Consider the following psudo code, where Post_1 is given as having weight = order = 1,:

{% set posts = [Post_1, Post_2, Post_3] | sort_by = "order" %}
<ol>
{% for post in posts %}
   <li> post </li>
{% endfor %}
</ol>

That produces

  1. Post_3
  2. Post_2
  3. Post_1

Whereas

{% set posts = [Post_1, Post_2, Post_3] | sort_by = "weight" %}
<ol>
{% for post in posts %}
   <li> post </li>
{% endfor %}
</ol>

yields

  1. Post_1
  2. Post_2
  3. Post_3
@phil-opp

This comment has been minimized.

Copy link
Contributor

phil-opp commented Jul 25, 2018

Oh ok, thanks! And the currently implemented prev/next order of these would be Post_1 -> Post_2 -> Post_3 for order and Post_3 -> Post_2 -> Post_1 for weight? I.e. from bottom to top in both lists? This would explain the reasoning behind the current implementation.

If my assumption is true, it seems like the current semantics for order and weight are based on the assumption that the latest post is always rendered first. I don't think that's a good strategy because it makes book-like sorting very confusing.

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 25, 2018

Oh ok, thanks! And the currently implemented prev/next order of these would be Post_1 -> Post_2 -> Post_3 for order and Post_3 -> Post_2 -> Post_1 for weight? I.e. from bottom to top in both lists? This would explain the reasoning behind the current implementation.

Subsections don't have prev/next, it's a simple sorted array.

For the pages, i'm not sure what to change to solve the confusion.

@phil-opp

This comment has been minimized.

Copy link
Contributor

phil-opp commented Jul 25, 2018

Subsections don't have prev/next, it's a simple sorted array.

Is there a reason that they don't or is it just not implemented? I imagine that it could be useful e.g. for linking to the next section in a book.

For the pages, i'm not sure what to change to solve the confusion.

I really like what @codesections proposed in their first comment:

In my dream world—and I have no idea if this is feasable and don't know enough Rust to implement it myself—here's how it would work:

  • Users can only set two variables in the .md toml header:
    • order (a number)
    • date (a datestring)
  • Users can then set two variables in the section _index.md toml header:
    • sort_by: either "date" or ""order"
    • sort_direction: "assending" or "decending" (Or just leave this off and let people handle it with Tera's | reverse filter)

If the user selects sort_by = "date", then they would have access to page.date_earlier and a page.date_later objects, and if they select sort_by = "order", then they'd have access to page.order_higher and page.order_lower objects. And then you woudn't need to worry about the semantics around next and previous and the various different use-cases that make those non-intuative.

We could choose a differerent name for order (e.g. number) and deprecate both order and weight. This would avoid silent breakage and we could even have a warning period that allows users to transition.

We could even implement the same system for sections with new sort_sections_by and section_sort_direction keys in the _index.md of the parent section. Then we would have an uniform and explicit system that works the same for pages and sections.

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 25, 2018

If my assumption is true, it seems like the current semantics for order and weight are based on the assumption that the latest post is always rendered first. I don't think that's a good strategy because it makes book-like sorting very confusing.

It's not quite as bad for book-like sorting because of the reverse filter in Tera. So you can do something like the following pseudocode (again, pseudocode because not all of these filters exist; instead, variables have to be set in front matter)

{% for section in sections | sort_by = "weight" %}
  <a href="{{ section.permalink }}"> {{ section.title }} </a>
  {% for page in section.pages | sort_by = "order" | reverse }}
    <a href="{{ page.permalink }}"> {{ page.title }} </a>
  {% endfor %}
{% endfor %}

This gets everything in the right order, and gets page.next and page.previous pointing in the "right" direction. I still don't love it, because it requires content creators to switch between giving each section a weight and each page an order. I'd prefer to keep as much complexity in the template/theme, and keep things simple for the end user. But it's not quite as bad as you might have been thinking.

I really like what @codesections proposed in their first comment:

The other advantage of moving in this direction would be that it would potentially improve the semantics for date, which we haven't even touched on. Right now, if pages are sorted by date, then page.next always points to the next older post. But there's actually considerable difference of opinion about the proper next/previous semantics for blog posts—some bloggers use next to point to the chronologically next-in-time post (i.e., the opposite of the way we do it). See, e.g. https://ux.stackexchange.com/questions/1773/which-direction-indicates-newer-older. Explicitly labeling the data as older/newer would avoid this issue as well.

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 26, 2018

I kind of see older/newer instead of next/previous as a good idea for dates now but I think next/previous makes sense when talking about a normal ordering.

I would keep order or weight as a name though, they are used in other SSG and will be more common.
The main "issue" is that for me weight makes more sense: if I want to add a new page, I just create a new .md with a weight higher than the last post and everything is working is working as expected.

For example:

intro.md -> weight 10
chapter1.md -> weight 20
chapter2.md -> weight 30
chapter3.md -> weight 40

If I want to add chapter 4, I can add a page with a weight of 50 and chapter3 will have next pointing at chapter4. By using reverse, we get the current order back.
We could use heavier/lighter instead of next/previous but that is not really explicit imo

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 26, 2018

I like the heavier/lighter idea. That would let someone have a page.html template something like this

{{ page.content | safe}}

<a href="{{ page.heavier.permalink }}> Previous </a>
<a href="{{ page.lighter.permalink }}> Next </a>

That would let pick whichever direction they want for (displayed to user) next/previous links without ever having the odd construction of <a href="{{ page.next.permalink }}"> Previous </a>

And it wouldn't need to break anything; the existing behavior of next/previous and order could stay in, but be deprecated as @phil-opp suggested

Did you have any thoughts regarding @phil-opp's suggestion that "there should be at least a warning for any non-deterministic behavior" from sections that lack a weight?

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 26, 2018

Did you have any thoughts regarding @phil-opp's suggestion that "there should be at least a warning for any non-deterministic behavior" from sections that lack a weight?

You mean if other sections at the same level have a weight, warn about the sections missing one? Sounds ok to me.

I would be tempted to do a clean break as the next version already has some big breaking changes, might as well group them in one release.
@codesections do you want to get your feet wet with the code? The changes would be fairly small

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 26, 2018

I have never programed Rust. I'm very interested in learning and have blocked off January and February for working through The Rust Programing Language book, but I've literally never coded a line of Rust yet. (I'm mostly a JavaScript developer, though I've done CS 101 amounts of C and dabbled a bit with Python).

Given all that, if you think it's manageable I'd be happy to take a swing at it if you'll point me in the right direction.

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 26, 2018

It should be ok!

  1. Wait for @phil-opp feedback

  2. The first bit is to update this enum https://github.com/Keats/gutenberg/blob/next/components/front_matter/src/lib.rs#L30-L39 to remove order (if we decide to go with weight)

  3. Then in https://github.com/Keats/gutenberg/blob/next/components/content/src/page.rs#L240-L273 we remove next/previous when serialising and add older/newer and lighter/heavier. We still need to keep next/previous field of a page struct, it just won't be exposed to the templates.

  4. Next up is fixing the sorting in https://github.com/Keats/gutenberg/blob/next/components/content/src/sorting.rs this is not going to compile at first because of the enum change in 1. The easy way is to fix the code first with cargo check in the content sub-crate and then remove useless tests. Once this is compiling and passing, you can add/tweak tests if needed.

  5. Go to the root and run cargo test --all and fix any compilation errors

  6. Update the documentation

I might have missed a step as it's late here and I'm a bit tired but the compiler should tell you the missing things anyway.

@phil-opp

This comment has been minimized.

Copy link
Contributor

phil-opp commented Jul 27, 2018

I like lighter/heavier too. It fixes my main issue with weight: that it is not clear whether the next/previous-ordering should be front-to-back or back-to-front. With lighter/heavier the direction is clear.

So +1 from me for removing order and adding lighter/heavier.

@codesections

This comment has been minimized.

Copy link
Contributor

codesections commented Jul 29, 2018

This should now be resolved by #352

@Keats

This comment has been minimized.

Copy link
Collaborator Author

Keats commented Jul 31, 2018

Success! I've updated the book template and semantics are now perfect imo

@phil-opp

This comment has been minimized.

Copy link
Contributor

phil-opp commented Jul 31, 2018

Awesome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment