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

Add summary statistics to location details and rework column metadata types #1634

Open
wants to merge 14 commits into
base: nextjs
Choose a base branch
from
Open
4 changes: 2 additions & 2 deletions app/app/locations/[location_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ export default function LocationDetailsPage({
<AppBreadCrumb />
<span className="fs-2">{location.description}</span>
<Row>
<Col sm={12} md={8} className="mb-3">
<Col sm={12} md={6} lg={7} className="mb-3">
<LocationMapCard location={location} />
</Col>
<Col sm={12} md={4} className="mb-3">
<Col sm={12} md={6} lg={5} className="mb-3">
Copy link
Member Author

@johnclary johnclary Dec 20, 2024

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

<DataCard
columns={locationCardColumns}
isValidating={false}
Expand Down
27 changes: 20 additions & 7 deletions app/components/DataCard.tsx
Copy link
Member Author

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?

Copy link
Member

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)

Copy link
Contributor

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! 🙏

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
handleFormValueOutput,
} from "@/utils/formHelpers";
import { ColDataCardDef } from "@/types/types";
import { LookupTableOption } from "@/types/lookupTables";
import { LookupTableOption } from "@/types/relationships";

interface DataCardProps<T extends Record<string, unknown>> {
record: T;
Expand All @@ -37,7 +37,12 @@ export default function DataCard<T extends Record<string, unknown>>({
// todo: handling of null/undefined values in select input
const [editColumn, setEditColumn] = useState<ColDataCardDef<T> | null>(null);
const { mutate, loading: isMutating } = useMutation(mutation);
const [query, typename] = useLookupQuery(editColumn?.lookupTable);
const [query, typename] = useLookupQuery(
editColumn?.editable && editColumn?.relationship
? editColumn.relationship
: undefined
);
Copy link
Member Author

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


const { data: selectOptions, isLoading: isLoadingLookups } =
useQuery<LookupTableOption>({
query,
Expand All @@ -47,10 +52,18 @@ export default function DataCard<T extends Record<string, unknown>>({
});

const onSave = async (value: unknown) => {
if (!editColumn) {
// not possible
return;
}
// Save the value to the foreign key column, if exists
const saveColumnName = editColumn.relationship?.foreignKey
? editColumn.relationship?.foreignKey
: editColumn.path;
Copy link
Member Author

@johnclary johnclary Dec 20, 2024

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.

Copy link
Member Author

@johnclary johnclary Dec 20, 2024

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 ☹️

Copy link
Contributor

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!

Copy link
Member

Choose a reason for hiding this comment

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

"fairly obvious" 😳

await mutate({
id: record.id,
updates: {
[String(editColumn?.name)]: value,
[saveColumnName]: value,
},
});
await onSaveCallback();
Expand All @@ -66,10 +79,10 @@ export default function DataCard<T extends Record<string, unknown>>({
<Table responsive hover>
<tbody>
{columns.map((col) => {
const isEditingThisColumn = col.name === editColumn?.name;
const isEditingThisColumn = col.path === editColumn?.path;
return (
<tr
key={String(col.name)}
key={String(col.path)}
style={{
cursor:
col.editable && !isEditingThisColumn ? "pointer" : "auto",
Expand All @@ -95,14 +108,14 @@ export default function DataCard<T extends Record<string, unknown>>({
{!isLoadingLookups && (
<DataCardInput
initialValue={valueToString(
getRecordValue(record, col),
getRecordValue(record, col, true),
col
)}
onSave={(value: string) =>
onSave(
handleFormValueOutput(
value,
!!col.lookupTable,
!!col.relationship,
col.inputType
)
)
Expand Down
2 changes: 1 addition & 1 deletion app/components/DataCardInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState } from "react";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";
import { InputType } from "@/types/types";
import { LookupTableOption } from "@/types/lookupTables";
import { LookupTableOption } from "@/types/relationships";

interface DataCardInputProps {
/**
Expand Down
2 changes: 1 addition & 1 deletion app/components/RelatedRecordTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function RelatedRecordTable<T extends Record<string, unknown>>({
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.name)} style={{ textWrap: "nowrap" }}>
<th key={String(col.path)} style={{ textWrap: "nowrap" }}>
{col.label}
</th>
))}
Expand Down
30 changes: 21 additions & 9 deletions app/components/RelatedRecordTableRow.tsx
Copy link
Member Author

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 🤷

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import Spinner from "react-bootstrap/Spinner";
import CrashDataCardInput from "./DataCardInput";
import DataCardInput from "./DataCardInput";
import { useMutation, useQuery, useLookupQuery } from "@/utils/graphql";
import {
getRecordValue,
Expand All @@ -9,7 +9,7 @@ import {
handleFormValueOutput,
} from "@/utils/formHelpers";
import { ColDataCardDef } from "@/types/types";
import { LookupTableOption } from "@/types/lookupTables";
import { LookupTableOption } from "@/types/relationships";

interface RelatedRecordTableRowProps<T extends Record<string, unknown>> {
record: T;
Expand Down Expand Up @@ -39,7 +39,11 @@ export default function RelatedRecordTableRow<
// todo: handling of null/undefined values in select input
const [editColumn, setEditColumn] = useState<ColDataCardDef<T> | null>(null);
const { mutate, loading: isMutating } = useMutation(mutation);
const [query, typename] = useLookupQuery(editColumn?.lookupTable);
const [query, typename] = useLookupQuery(
editColumn?.editable && editColumn?.relationship
? editColumn.relationship
: undefined
);
const { data: selectOptions, isLoading: isLoadingLookups } =
useQuery<LookupTableOption>({
query,
Expand All @@ -49,10 +53,18 @@ export default function RelatedRecordTableRow<
});

const onSave = async (recordId: number, value: unknown) => {
if (!editColumn) {
// not possible
return;
}
// Save the value to the foreign key column, if exists
const saveColumnName = editColumn.relationship?.foreignKey
? editColumn.relationship?.foreignKey
: editColumn.path;
await mutate({
id: recordId,
updates: {
[String(editColumn?.name)]: value,
[saveColumnName]: value,
},
});
await onSaveCallback();
Expand All @@ -64,10 +76,10 @@ export default function RelatedRecordTableRow<
return (
<tr>
{columns.map((col) => {
const isEditingThisColumn = col.name === editColumn?.name;
const isEditingThisColumn = col.path === editColumn?.path;
return (
<td
key={String(col.name)}
key={String(col.path)}
style={{
cursor: col.editable && !isEditingThisColumn ? "pointer" : "auto",
}}
Expand All @@ -85,17 +97,17 @@ export default function RelatedRecordTableRow<
<>
{isLoadingLookups && <Spinner size="sm" />}
{!isLoadingLookups && (
<CrashDataCardInput
<DataCardInput
initialValue={valueToString(
getRecordValue(record, col),
getRecordValue(record, col, true),
col
)}
onSave={(value: string) =>
onSave(
Number(record.id),
handleFormValueOutput(
value,
!!col.lookupTable,
!!col.relationship,
col.inputType
)
)
Expand Down
10 changes: 5 additions & 5 deletions app/components/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ export default function Table<T extends Record<string, unknown>>({
<tr>
{columns.map((col) => (
<th
key={String(col.name)}
key={String(col.path)}
style={{ cursor: col.sortable ? "pointer" : "auto" }}
onClick={() => {
if (col.sortable) {
const newQueryConfig = { ...queryConfig };
if (col.name === queryConfig.sortColName) {
if (col.path === queryConfig.sortColName) {
// already sorting on this column, so switch order
newQueryConfig.sortAsc = !newQueryConfig.sortAsc;
} else {
// change sort column and leave order as-is
newQueryConfig.sortColName = String(col.name);
newQueryConfig.sortColName = String(col.path);
}
// reset offset/pagination
newQueryConfig.offset = 0;
Expand All @@ -46,7 +46,7 @@ export default function Table<T extends Record<string, unknown>>({
}}
>
{col.label}
{col.name === queryConfig.sortColName && (
{col.path === queryConfig.sortColName && (
<SortIcon className="ms-1 my-1" />
)}
</th>
Expand All @@ -58,7 +58,7 @@ export default function Table<T extends Record<string, unknown>>({
<tr key={i}>
{columns.map((col) => (
// todo: is no-wrap / side-scrolling ok?
<td key={String(col.name)} style={{ whiteSpace: "nowrap" }}>
<td key={String(col.path)} style={{ whiteSpace: "nowrap" }}>
{renderColumnValue(row, col)}
</td>
))}
Expand Down
12 changes: 6 additions & 6 deletions app/configs/chargeColumns.tsx
Copy link
Member Author

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

Copy link
Contributor

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!

Copy link
Member Author

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 🙏

Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { ColDataCardDef } from "@/types/types";
import { Charge } from "@/types/charge";

export const ALL_CHARGE_COLUMNS: { [name: string]: ColDataCardDef<Charge> } = {
export const ALL_CHARGE_COLUMNS = {
unit_nbr: {
name: "unit_nbr",
path: "unit_nbr",
label: "Unit",
},
prsn_nbr: {
name: "prsn_nbr",
path: "prsn_nbr",
label: "Person",
},
charge: {
name: "charge",
path: "charge",
label: "Charge",
},
citation_nbr: {
name: "citation_nbr",
path: "citation_nbr",
label: "Citation Number",
},
};
} satisfies Record<string, ColDataCardDef<Charge>>;
Copy link
Member Author

@johnclary johnclary Dec 20, 2024

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 👇

Copy link
Contributor

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!

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice post 🙏

8 changes: 3 additions & 5 deletions app/configs/crashDataCard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { crashesColumns } from "./crashesColumns";


/** Construct card-specific arrays of columns */
export const crashDataCards = {
summary: [
crashesColumns.case_id,
crashesColumns.crash_timestamp,
crashesColumns.fhe_collsn_id,
crashesColumns.rpt_city_id,
crashesColumns.collsn,
crashesColumns.city,
Copy link
Member Author

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.

],
flags: [
crashesColumns.private_dr_fl,
Expand All @@ -22,7 +20,7 @@ export const crashDataCards = {
other: [
crashesColumns.light_cond_id,
crashesColumns.crash_speed_limit,
crashesColumns.obj_struck_id,
crashesColumns.obj_struck,
crashesColumns.law_enforcement_ytd_fatality_num,
],
address: [
Expand Down
Loading