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

create table technical design #71

Merged
merged 19 commits into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion docs/src/lib/getMDXDirFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const getMDXDirFiles = (routeParams) =>
dirTree(
`../packages/cascara/src/${routeParams.mdx[0]}/${routeParams.mdx[1]}`,
{
extensions: /\.(mdx|fixture.js)$/,
extensions: /\.(mdx)$/,
}
).children;

Expand Down
2 changes: 1 addition & 1 deletion docs/src/lib/getMDXTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import dirTree from 'directory-tree';
const getMDXTree = () =>
// TODO: Memoize this
dirTree('../packages/cascara/src', {
extensions: /\.(mdx|fixture.js)$/,
extensions: /\.(mdx)$/,
}).children;

export default getMDXTree;
7 changes: 6 additions & 1 deletion docs/src/pages/docs/[[...mdx]].js
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,16 @@ export const getStaticPaths = async () => {
};

export const getStaticProps = async ({ params }) => {
const mdxDirFiles = getMDXDirFiles(params);
const mdxDir = getMDXDirFiles(params);

// We need to make sure this is only actual files and not a directory
// so we are filtering it to make sure the size of the file is not zero.
const mdxDirFiles = mdxDir.filter((file) => file.size > 0 && file);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is to make sure we are trying to parse MDX for actual MDX files and not a directory.


// This needs to be async or it will blow up since `next-mdx-remote` is
// asyncrhonously getting all MDX files and rendering them to string.
// Without the Promise.all pattern this will blow up on us.

const mdxDirSource = await Promise.all(
mdxDirFiles.map(async (file) => {
const filePath = path.join(process.cwd(), file.path);
Expand Down
170 changes: 170 additions & 0 deletions packages/cascara/src/ui/Table/TechnicalDesign.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
name: Spec
menu: Table
route: /table-spec
---

# Technical Design - Espressive Table

# Current implementations

As of `goldengirls` release, we use tables in the following applications:

Barista:

- Admin
- Service Department
- Service Teams
- Barista FAQs
- Users
- Locations

Caffeine

- Configuration
- Entities
- Localization
- Training
- Variables
- Status
- Doppio

## Features in tables

The features in those tables vary from one implementation to another, but the general idea is to present the data in a tabular way, ussually also providing the means for data persistance. The aggregated features list is the following:

**Item creation**:
Some tables allow the user to create a new entry in the table. Adding an empty row.

**Inline edition**:
Some tables allow the user to edit the data, replacing the display representation with some JSX that allows the mutation of the data.

**Item deletion**:
Some entities in our API allow item deletion, some others don't. The `delete` functionality must be `opt-in`.

**Custom actions**:
In most implementations, tables add a trailing column to each row. This row can display `action buttons`, which can be mapped to corresponding actions over the entire row. i.e. edit, delete, cancel, etc.

All of the features mentioned above must be `opt-in`.

## Data representation / mutation

The way each cell represents the data for either display or edit purposes not only depends on the data type but also on business logic, in most cases, values are easily coerced to string for display purposes and updates are easily achieved via a controlled input component. The rest of scenarios require a more complex implementation, e.g. the `employee` field in the `Users` table from `Barista Admin` which under the carpet is a single `eid`, but the representation of that cell is an avatar with the user name it points to, although it could be any other representation, specially when editing the value.

Note: The updates made to any row would be notified upstream via `events` so the parent component is able to persist those changes. The means by which the table enables data mutation are to be provided by the developer in the form of `triggers`, the table doesn't provide such means.

# Proposal

Much thought has been put to the proposal described in this section, our goal is to create a table that can support all of our current use cases while at the same time being abstract enough to support future ones. We are not inventing the wheel here, there are great open source libraries like `react-table`. The problem with those libraries is that they are bloated with things we don't need either because we already abstracted such functionality out to other components or we simply don't need it.

## Espressive CRUD Table

Given the nature of our actual tables and the operations we need to perform on the data entities, a CRUD Table starts to make sense. A kind of table that not only displays data in a nice tabular way, but also allow for a richier interaction with the data.

It is true that CRUD operations are needed in less scenarios, so the CRUD feature must be easily enabled and must not interfere with the regular table logic.

## Props

![Table props](assets/props.svg)

### Events

In order to support CRUD operations, we must define a mechanism to communicate the parent any changes to the data.

- onCreate
- onUpdate
- onDelete
- onTrigger

Please note that the names could have been something like `onItemCreate` for semantics, but let's remember that in the future we plan to support selection and custom actions on multiple items. Our proposal is to keep these as abstract as possible.
Copy link
Contributor

Choose a reason for hiding this comment

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

Good stuff. I suspect that this pattern is going to be our bread and butter for a lot of these components. After all, there are all kinds of scenarios on the implementation end where a developer will want to get a snapshot of our component's data when something happens outside of it.

Based on this... we should be thinking about this pattern overall. How do we keep the choices we make here as consistent as possible? Let's take some extra time really focusing on the semantics of the CRUD operations we expose, what their signatures look like, and what parameters they return. They should be consistent across all components when we reach for a onCreate function or onUpdate function. Also... should we have a separate onDelete, onUpdate and onCreate function? What if we just have an onUpdate function that returns a type with it? Would this potentially be a more clean pattern? Are there reasons you can think of where we would NOT want to do that? If so, what would the workarounds be?

  • should we expose multiple functions for state change in the component, or one with types?

I will create a MDX file for us to make some notes about components and CRUD operations in Cascara in general so we can start putting some of our decisions there. That way once we start deciding on how we want some of these things to look and feel, we can keep that up to date.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@brianespinosa the definitions for the rest of the CRUD events are below.


| Event | Signature | Description |
| --------- | ------------------------------------------------- | --------------------------------------------------------------- |
| onCreate | `(Object: newItem) => {}` | Fired when the User saves a new item |
| onUpdate | `(Array[Object]: updatedItems) => {}` | Fired when one or more rows have been updated by the User |
| onDelete | `(Array[String]: selectedItems) => {}` | Fired when one or more rows have been deleted by the User |
| onTrigger | `(String: actionType, Array[String]: data) => {}` | Fired when a custom action has been applied to one or more rows |

### Config

There will be use cases where features like multi-selection, CRUD, custom actions, etc. won't be required at all, they must be disabled by thefault but easily activated. It makes sense to have boolean props that controll such features.

Another important aspect of configuration is the definition of columns, hence a complete section is devoted to it.

| Name | Type | default | Description |
| ------------------ | ------------- | ------- | -------------------------------------------------- |
| Bulk (enhancement) | Boolean | `FALSE` | If true, activates the bulk mode (multi-select) |
| Columns | Array[Object] | `[]` | Holds the definitions for each column in the table |
| Create | Boolean | `FALSE` | If true, activates the creation of new items |
| Delete | Boolean | `FALSE` | If true, activates the deletion of items |
| Update | Boolean | `FALSE` | If true, activates the edit mode |

### Triggers

Custom actions are present in most tables, they are an important aspect of all applications. The nature of the actions depends on the entity they are applied to, hence, an abstract way of defining custom actions is required.

| Attribute | Type | default | Description |
| --------- | -------- | ------------ | ----------------------------------------------------------- |
| id | String | `''` | The id of the action, usually the name. |
| callback | Function | `noop` | The function to be called when the action button is clicked |
| trigger | ANY | `<Button />` | The element that will trigger the action |

### Data

The data passed to the table for rendering purposes.

| Name | Type | default | Description |
| ---- | ------------- | ------- | --------------------------------------------- |
| data | Array[Object] | `[]` | the tabular data to be displayed by the table |
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if there are some consistent things we could at least make sure are present in these data objects. I guess the only thing for sure that we would want to make sure of is eid which could then be used for keys in the table view. Past that, these objects could look like almost anything.

An eid will be super important for data arrays on anything virtualized too.

Not sure if we should necessarily address that here.

Also, like the pattern of CRUD operations, I think that a data prop is also going to be some of our bread and butter in Cascara. We should also consider a section for making notes of how this particular prop should be used too.


### Columns

The definition of columns depends on the data, we must make sure we have a well-defined set of data types to allow display and mutation for each one.

| Attribute | Type | default | Description |
| ---------- | ---------------------------------------------------------------------- | ---------- | -------------------------------------------------------- |
| attribute | String | `null` | The name of the entity attribute that maps to the column |
| isEditable | Boolean | `FALSE` | Specifies if the cells are editable |
| label | String | `''` | Label text to display with this attribute |
| type | Enum[`boolean`, `date`, `image`, `link`, `number`, `string`, `render`] | `'string'` | Specifies the type of data for the column |

### Data types

The data types this table can handle are the following:

For display puposes: `boolean`, `date`, `image`, `link`, `number`, `string` and `render`. The `render` type being a custom JSX.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should figure out how to identify which of these is part of MVP and which is future.

Also, we need to figure out what these little guys are called. They will be used in a reusable manner all over Cascara. Basically they are like display atoms or something. And they will have an editing experience as well.

  • Maybe we just call these guys "atoms". And for all atoms they have an editing or display presentation?

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, atoms is a more descriptive term.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@brianespinosa, I double-checked the apps to see which atoms are to be included in the MVP.
The lists are the following:

Display

  • boolean
  • date
  • link
  • number
  • string

Edit

  • switch
  • input
  • datetime picker
  • select
  • lookup select

The atoms we use fordisplay will be different to the atoms we use for edit, that doubles the effort.

Copy link
Contributor

Choose a reason for hiding this comment

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

For "edit" the experience needs to be more simple for MVP. I think for boolean we can only do a default html checkbox. For date, we should show an input date time field, not our own date picker. Basically the edit experience for MVP is going to be: whatever is available in html and what do browser vendors provide natively. We will absolutely have other options though in the future:

  • switch versus checkbox for booleans
  • custom date picker to be consistent across all browsers (except I think keeping native pickers on mobile)
  • custom select/dropwdown component of our own so we can control the UI/UX (again I think the exception would be mobile)
  • lots of others

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok sounds good! Thanks for reminding me about HTML5, the mobile functionality aspect is very important.

I mentioned the switch instead of checkbox because wee have tables that implement it. At the end of the day we can style an HTML5 checkbox to make it look/behave as a switch. I have some ideas on that matter that I want to explore and run by @CaffeinatedAllie.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep. I think with these atoms, there are also use cases where we will want a checkbox too. Eventually someone could decide which control they want to show for their boolean. Does a checkbox, a switch, an enum select make the most sense?

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, they completely make sense. The enum select will have the true, false values - we could also allow the user to set the labels to support localizaton.

You just uncovered a track we can start to follow in terms of abstraction, looking forward to it!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will start typing these ideas into code soon, these kind of convos make really think about implementation! 🙌


For mutation purposes, the implementation will support `select`, `input`, `boolean switch`, `date / time pickers`, `lookup select`. With the ability to expand to more types.

## Context

All the internal logic will be hosted in a Context. The definition is WIP.
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I can tell this is specific to React context. 👍🏽 Moving forward, any time we are referring to something that is part of React as part of the API, or any other tool as part of the API, we should figure out a consistent way of calling that out. We have a lot of terms in our ecosystem that have special meaning which we want to be clear on.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup, that's React Context. I will make it clear throught the document.


## The complete JSX picture

```
<Table
// config
bulk
create
delete
update

coluns=[{}]

// events
onCreate={this.handleNewItem}
onDelete={this.handleItemDelete}
onUpdate={this.handleItemUpdate}
onTrigger={this.handleTriggers}

// triggers
triggers={[
{ id: 'cancel', ...additionalConfig },
{ id: 'view', ...additionalConfig },
]}

// data
data={[{}]}
/>
```
Loading