politics-rewired / Spoke

Politics Rewired's fork of Spoke
GNU General Public License v3.0
35 stars 16 forks source link

Make Message Review filters shareable by url #826

Closed kohlee closed 2 years ago

kohlee commented 3 years ago

Is your feature request related to a problem? Please describe. Admins are unable to share Message Review urls with fellow users and have the filters pre-populate with values that were present when the url was copied.

Describe the solution you'd like Message Review views should be shareable by url.

Describe alternatives you've considered N/A

Additional context This is supported in MoveOn's fork.

hiemanshu commented 2 years ago

@bchrobot For this I was thinking of moving most of the state into a search params of the URL. So the component reads the values from the URL, and any changes in filters cause a change in the URL, this way the URL can just be copy pasted and shared with anyone else.

I've tried to experiment with a few different ideas, and instead of maintaining multiple sources of truth, moving the source to the URL made the most sense.

bchrobot commented 2 years ago

@hiemanshu I 100% agree with this. I have a few ideas about how this could pair with a larger refactor of message review. I will open a PR with a design doc later today.

hiemanshu commented 2 years ago

@bchrobot following up on this, do you have any other ideas on the refactoring?

bchrobot commented 2 years ago

Current State

Message Review currently looks like:

<AdminIncomingMessageList>
  {/* Workaround for fetching data */}
  <PaginatedUsersRetriever />
  <PaginatedUsersRetriever />
  <PaginatedCampaignsRetriever />

  <IncomingMessageFilter>
    <Card>
      <Toggle />
      <Toggle />
      <SelectField />
      <Autocomplete />
      <Autocomplete />
      <TextField />
      <SelectField />
    </Card>
  </IncomingMessageFilter>

  <IncomingMessageActions />

  <IncomingMessageList>
    <DataTable />
    <ConversationPreviewModal />
  </IncomingMessageList>

  <ReassignmentResultDialog />
</AdminIncomingMessageList>

Drawbacks:

Proposal

<AdminIncomingMessageList>
  <IncomingMessageFilter>
    <Grid>
      <MessageStatusSelect />
      <CampaignSelect />
      <TexterSelect />
      <TagSelect />
    </Grid>
  </IncomingMessageFilter>

  <IncomingMessageActions />

  <IncomingMessageList>
    <DataTable muiVersion="v4" />
  </IncomingMessageList>

  <ConversationPreviewModal />
  <ReassignmentResultDialog />
</AdminIncomingMessageList>

Questions I still have about my proposal are:

hiemanshu commented 2 years ago

Extract business logic and state management into a standalone hook useReducer

I don't think we want to use a react hook here, useReducer still helps with managing state, and we don't want to manage state for the most part. We'll have a couple of items that use state, but most everything else will be read from the URLSearchParams. For the little state we want to manage, we should be able to just make do with useState

Break up filter component. Container handles layout, individual components handle filter-specific logic (e.g. resolving multiselect dropdown value into a valid GraphQL query input object)

Sounds good! Splitting into similar types grouped together.

can fetch data sources for its Autocomplete components as well Pull conversation preview modal up a level

Sounds good!

Update incoming message list to a React v16 compatible data table implementation. The one muiv4 provides is likely sufficient

Exploring this, the one in v4 is in the lab, and not in core. DataGrid also has a paid pro version.

Is pushing data source fetching down closer to the Autocomplete and SelectFields that consume them a bad idea?

Not at all, the component that needs the data should be able to call the API to fetch it, making the component more atomic.

Should reassignment actions be handled in the top level business logic hook? Or pushed down to ?

This is an interesting question. Looking at the data flow here, IncomingMessageList tells the parent what messages have been selected, and IncomingMessageActions will need to read the parents state to find what messages it should reassign. I'd say for the simpler data flow (this refactor is going to be large as is), we should let the parent manage it for now. We can come back and revisit this idea at a later time.

bchrobot commented 2 years ago

Extract business logic and state management into a standalone hook useReducer

useReducer still helps with managing state, and we don't want to manage state for the most part. We'll have a couple of items that use state, but most everything else will be read from the URLSearchParams.

Sorry, I should have clarified that I was including URL query params in my bucket of "state" - just swapping useState for useQueryParam. Agree that we shouldn't be adding more React state via useState though.

Update incoming message list to a React v16 compatible data table implementation. The one muiv4 provides is likely sufficient

Exploring this, the one in v4 is in the lab, and not in core. DataGrid also has a paid pro version.

Got it -- looks like DataGrid is the path forward in mui v5 as well. The free version seems fine for what we need. Pro version pricing is per developer and there's no clarification on how that interfaces with open source projects where the number of developers is unknowable.

Is pushing data source fetching down closer to the Autocomplete and SelectFields that consume them a bad idea?

Not at all, the component that needs the data should be able to call the API to fetch it, making the component more atomic.

That's my feeling as well, but the boundary of container and component feels a little murky for elements like this.

Should reassignment actions be handled in the top level business logic hook? Or pushed down to ?

This is an interesting question. Looking at the data flow here, IncomingMessageList tells the parent what messages have been selected, and IncomingMessageActions will need to read the parents state to find what messages it should reassign. I'd say for the simpler data flow (this refactor is going to be large as is), we should let the parent manage it for now. We can come back and revisit this idea at a later time.

Sounds good! If it proves annoying I think it could work to pass selected state down to IncomingMessageActions

hiemanshu commented 2 years ago

Sorry, I should have clarified that I was including URL query params in my bucket of "state" - just swapping useState for useQueryParam. Agree that we shouldn't be adding more React state via useState though.

Yeah. I wonder how userQueryParams will work out.

hiemanshu commented 2 years ago

@kohlee @bchrobot Do you have a preference on what the URL parameters look like, these two below are common options.

Option 1 looks cleaner, but is a bit more work to define it out clearly. Option 2 works more easily, and is less work to implement and use.

Screenshot 2022-06-10 at 10 47 36 AM Screenshot 2022-06-10 at 2 09 10 PM
bchrobot commented 2 years ago

Do you have a preference on what the URL parameters look like, these two below are common options.

Option 1 looks good!

Are these options based on use-query-params's Param Types ?

hiemanshu commented 2 years ago

Are these options based on use-query-params's Param Types ?

Option 1 uses a custom defined param type to convert the params to the correct types. So we can shorten the URL links as well using a variable name to URL name map easily as well.

Here's what a custom Param looks like:

export const ContactsFilterParam = {
    encode: (filter: ContactsFilter | undefined): string | null | undefined => {
        return encodeObject(filter);
    },
    decode: (params: string | null | undefined): ContactsFilter | null | undefined => {
        const decodedObject = decodeObject(params);

        if (!decodedObject) {
            return null;
        }

        const contactsFilter: ContactsFilter = new Object();

        for (const [key, value] of Object.entries(decodedObject)) {
            if (["isOptedOut", "validTimeZone", "includePastDue"].includes(key)) {
                contactsFilter[key] = value === "true";
            } else {
                contactsFilter[key] = value;
            }
        }
        return contactsFilter;
    }
}
hiemanshu commented 2 years ago

@bchrobot looked at the last required part for this, to refactor ConversationPreviewModal, and right now it seems tightly knit with the IncomingMessageList, since IncomingMessageList is the one that pulls in the conversations, and has the conversation data, I think pulling the Modal out to the main component is not ideal since it will add more complexity without much gain. So I'm going to mark this task as complete. Let me know what you think!

bchrobot commented 2 years ago

So I'm going to mark this task as complete. Let me know what you think!

Works for me!