-
Notifications
You must be signed in to change notification settings - Fork 4
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
Add summary statistics to location details and rework column metadata types #1634
base: nextjs
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for vision-zero-nextjs ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
<LocationMapCard location={location} /> | ||
</Col> | ||
<Col sm={12} md={4} className="mb-3"> | ||
<Col sm={12} md={6} lg={5} className="mb-3"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like making the location map a bit wider than in the old VZE—just trying to strike a balance to prevent the details card form sidescrolling
editColumn?.editable && editColumn?.relationship | ||
? editColumn.relationship | ||
: undefined | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we only need to use the lookup query when a column is editable and it has relationship metadata
// Save the value to the foreign key column, if exists | ||
const saveColumnName = editColumn.relationship?.foreignKey | ||
? editColumn.relationship?.foreignKey | ||
: editColumn.path; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this hints at the awkward dance we need to do between rendering a value from an object relationship. E.g. crash. collsn.label
versus editing that value, which requires us to update the foreign key at crash.fhe_collsn_id
.
This PR is trying to iron this out by moving the necessary metadata into the column relationship
config, whereas before we had some special lookup table logic hardcoded into various functions.
One important thing that these changes do is open up future iterations where we are editing relationships that are not just plain old lookup tables. E.g., if we wanted a user to select an EMS record from a list so that it can be linked to a crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, right—one other wrinkle (not new to this PR) is that in order to be able to edit lookup values our graphql queries need to include both the foreign key column and the related record itself.
For example, in the Crash
query, you can see here that we have
# this is the foreign key to the lookups.collsn table
fhe_collsn_id
# this is the related collision type lookup value
collsn {
id
label
}
If you were to omit the foreign key from the query, the select
input in the UI would never correctly show the currently selected lookup option. So it would be fairly obvious that something was wrong. This is not documented anywhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for pointing this out. i think this part of apps always gets a bit wrinkly, and I don't think that it is too bad to not have this documented and maybe even a bit intuitive? i also have not written any of this code so I could be wrong!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i've mentioned this elsewhere, but it is rather unfortunate that the RelatedRecordTableRow
is almost identical to the DataCard
component except x + y axes are flipped. Would be cool to try to abstract this 🤷
label: "Citation Number", | ||
}, | ||
}; | ||
} satisfies Record<string, ColDataCardDef<Charge>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here are the docs about Typescript satisfies. it seems like it was designed for this exact scenario: it lets TypeScript infer this object's keys while still checking that each column config matches the ColDataCardDef
this change fixes a type bug down in the card/table configurations 👇
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for pointing to the docs for this one, and I agree that it fits this nicely. I played around with this in my editor, and I see the bug replicated when I go back to the previous typing on this config. sad about no pizza 🍕🥲 but happy about the TIL!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I liked this blog post about satisfies: https://fnune.com/typescript/2022/11/15/typescript-series-5-record-and-the-satisfies-operator/
crashesColumns.fhe_collsn_id, | ||
crashesColumns.rpt_city_id, | ||
crashesColumns.collsn, | ||
crashesColumns.city, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
before adding that satisfies
magic to the column indexes (see my other comment), there was no type checking on these column names. You could have added a column to the summary section called crashColumns.pizza
and typescript would not have blinked.
tableSchema: "lookups", | ||
tableName: "collsn", | ||
idColumnName: "id", | ||
labelColumnName: "label", | ||
foreignKey: "fhe_collsn_id", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here is the new Path
type in action, along with relationship
metadata. the very cool thing here is that the path
property is type-checked against the Crash
type. so "collsn.label"
is valid but collsn.pizza
would not be.
so this new Path
type unlocks adding column metadata for nested properties in a typesafe way 🌈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is so slick! love that the path notation feels like lodash .get() notation which i think is a good sign for this being intuitive. 🚀
cr3_crash_count: { | ||
path: "locations_list_view.cr3_crash_count", | ||
label: "CR3 crashes (5 years)", | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, here you can see the path magic in action, so we can now render this nested value on the location details card. note how the relationship
property is absent—we don't need it because this field is not editable.
export type LookupTableOption = { | ||
id: string | number; | ||
label: string | number; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so now we have the generic Relationship
type which covers lookup tables and any other object relationship that a record might have.
and we still have the LookupTableOption
to describe the id
/label
structure of our conventional lookup tables.
app/types/utils.ts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, this new Path
type is definitely typescript hard mode. I link to a 3rd-party lib that does something similar, which I wouldn't mind reaching for but it does have more functionality than we need.
@@ -75,47 +110,45 @@ const renderYesNoString = (value: unknown): string => { | |||
}; | |||
|
|||
/** | |||
* Return the `label` column from a related lookup table | |||
*/ | |||
export const renderLookupLabel = <T extends Record<string, unknown>>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here's one of those functions that was hardcoded to deal with with our lookup tables in the lookups
schema, but not for other types of relationships.
label | ||
${typename}(order_by: {${relationship.labelColumnName}: asc}) { | ||
id: ${relationship.idColumnName} | ||
label: ${relationship.labelColumnName} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
like i mention in the JSdoc for this hook—we are aliasing the id
and label
properties of this query builder, so that any lookup table we load will have the same shape. right now, we're only using lookups in the lookups
schema in our db, which all of the same shape, but if we wanted to get lookup values to an EMS record, we could do that as well and still use the same UI form components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just want to mention this right at the top—i want to replace the word Column, Col, etc everywhere with the term field. that just feels more intuitive and accurate. would it be worth doing that in a follow-up PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
keeping it separated in a different PR would be good, so its not confused with any other enhancements in a PR (basically so I am not confused)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 - i think that a separate PR would be good because this one took some time for me to read through as-is - lots of learning! 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i am also looking for feedback on the naming conventions and file organization around these configs. we have a few different kinds of configs:
- configs that organize column metadata (the
ColDataCardDef<T>
objects) - configs that consume the column configs ☝️ and define which UI components render which columns (cards and related tables)
- the
queryConfig
objects that plug into the graphql table component for our crashes and locations list views
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after looking at this code, part of me wondered what it would look like to organize the config
folder into subfolders. but then we would have to decide on how to organize - by type like table, columns, or cards or by record type like crash, unit, location... but neither feels like a good fit right now.
So, I like the current config organization, and I think the objects themselves + the file names make it straightforward to understand where they are used and how to update them to add/update fields or change behavior. I could imagine someone finding the components by route first and then following that path to the configs. curious about other thoughts too!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think that's a great idea. i added a todo for this 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great, and I learned a lot. I like the new abstraction too! The location table shows the statistics just as expected, but I ran into one question about the light condition field in the crash view that I wasn't sure about. Other than that, it looks 🚢!
// Save the value to the foreign key column, if exists | ||
const saveColumnName = editColumn.relationship?.foreignKey | ||
? editColumn.relationship?.foreignKey | ||
: editColumn.path; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for pointing this out. i think this part of apps always gets a bit wrinkly, and I don't think that it is too bad to not have this documented and maybe even a bit intuitive? i also have not written any of this code so I could be wrong!
label: "Citation Number", | ||
}, | ||
}; | ||
} satisfies Record<string, ColDataCardDef<Charge>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for pointing to the docs for this one, and I agree that it fits this nicely. I played around with this in my editor, and I see the bug replicated when I go back to the previous typing on this config. sad about no pizza 🍕🥲 but happy about the TIL!
tableSchema: "lookups", | ||
tableName: "collsn", | ||
idColumnName: "id", | ||
labelColumnName: "label", | ||
foreignKey: "fhe_collsn_id", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is so slick! love that the path notation feels like lodash .get() notation which i think is a good sign for this being intuitive. 🚀
sortable?: boolean; | ||
valueGetter?: (record: T, column: ColDataCardDef<T>) => any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool to see that this is no longer needed too!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 - i think that a separate PR would be good because this one took some time for me to read through as-is - lots of learning! 🙏
* For example, given a `<Person>` object like `{ info : { name: { first: "john" } } }`, | ||
* `"info.name.first"` is a valid `Path<Person>`. | ||
* | ||
* todo: use a 3rd party solution? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think a third-party could be good down the line if we need to support more complex shapes, but I am appreciating having this code here to learn from right now. 📚 This is pretty heady for me right now, but I think I'm getting there!
* Retrieve a value from an object given a dot-noted path string. | ||
* | ||
* Essentially, it performs optional chaining on an object. It is similar | ||
* to lodash.get(), except array notation is not supported. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah cool! lodash was the inspiration. ✨
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after looking at this code, part of me wondered what it would look like to organize the config
folder into subfolders. but then we would have to decide on how to organize - by type like table, columns, or cards or by record type like crash, unit, location... but neither feels like a good fit right now.
So, I like the current config organization, and I think the objects themselves + the file names make it straightforward to understand where they are used and how to update them to add/update fields or change behavior. I could imagine someone finding the components by route first and then following that path to the configs. curious about other thoughts too!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚢 Lots of interesting Typescript goodness happening and I think I'm following the direction to use "paths" and "relationships" to have more dynamic attribute stringing with dot-notation. Thanks for adding commentary and links. ✨
Associated issues
This PR restructures the typing and config structure for managing editable columns in the app. It makes our config + components more abstract, so that they can better handle arbitrary relationships rather than the hardcoded "lookup table" relationship.
One additional change that I would like to make, but have not in the interest of the code diff, is to stop using the word "column" where we really mean "field". This came up in some PR feedback from Chia and Rose recently.
Thank you for reviewing! Since this is so large, I'd be happy to set up a time to pair or review as a group interactively.
Testing
URL to test: Netlify
Steps to test:
Ship list
main
branch