Skip to content

feat: UX - Initial create form flow #33

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

Closed
wants to merge 45 commits into from

Conversation

bryphe-coder
Copy link
Contributor

@bryphe-coder bryphe-coder commented Jan 20, 2022

This stubs out an initial, very minimal 'Create' flow - based on the placeholder data in #30, since we don't have real data yet:

2022-01-19 21 13 27

This adds the following:

  • Route for create -> both project selection, and then a custom form for that project, w/ dynamic properties
  • Integration of formik and porting over some of our form management utilities

TODO:

  • Merge sign-in change
  • Fix lint issues
  • Remove subform abstraction because it's not needed <-- I thought this would be trivial but ended up being more involved: feat: UX - Initial create form flow #33 (comment)
  • Pivot UX to 'Projects' flow based
  • Inline <FormRow />

Next Steps:

@codecov
Copy link

codecov bot commented Jan 20, 2022

Codecov Report

Merging #33 (8343ef0) into main (dfddaf1) will decrease coverage by 0.81%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #33      +/-   ##
==========================================
- Coverage   71.31%   70.50%   -0.82%     
==========================================
  Files          59       29      -30     
  Lines        2308     2017     -291     
  Branches       30        0      -30     
==========================================
- Hits         1646     1422     -224     
+ Misses        517      455      -62     
+ Partials      145      140       -5     
Flag Coverage Δ
unittest-go-macos-latest 66.63% <ø> (-0.11%) ⬇️
unittest-go-ubuntu-latest 70.00% <ø> (ø)
unittest-go-windows-latest 65.82% <ø> (-0.17%) ⬇️
unittest-js ?
Impacted Files Coverage Δ
peerbroker/dial.go 72.72% <0.00%> (-4.55%) ⬇️
peerbroker/listen.go 80.46% <0.00%> (-2.35%) ⬇️
site/components/Form/FormTextField.tsx
site/components/Navbar/index.tsx
site/components/PageTemplates/Footer.tsx
site/components/index.tsx
site/pages/_app.tsx
site/pages/index.tsx
site/components/Icons/CoderIcon.tsx
site/components/Icons/Logo.tsx
... and 22 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update dfddaf1...8343ef0. Read the comment docs.

@bryphe-coder bryphe-coder self-assigned this Jan 20, 2022
@kylecarbs
Copy link
Member

I think we want the nesting to occur here, rather than a top-level create function.

eg. the primary page is "Projects". The user selected a project, then is presented with the "Create Workspace" button for that project.

@kylecarbs
Copy link
Member

We end up creating multiple flows with different UX, but similar objectives by having the "Create Workspace" flow be global. We'll have a Project selector on the dashboard, and a Project selector in the "Create Workspace" flow.

Copy link
Member

@kylecarbs kylecarbs left a comment

Choose a reason for hiding this comment

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

I like directionally how this is moving. The different routes make me happy to see!

I'm hesitant to bring in large wrapper abstractions like Form, and our Page layer. I'd prefer code duplication right now, but I'm not sure what that looks like either.

Curious for your thoughts!

Comment on lines +3 to +8
/**
* FormikLike is a thin layer of abstraction over 'Formik'
*
* FormikLike is intended to be compatible with Formik (ie, a subset - a drop-in replacement),
* but adds faculty for handling sub-forms.
*/
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I understand the purpose of this concept after reading this comment. Does Formik not support this use-case? I'm hesitant to think we're using it properly if we have to create all these types and a nesting system. Formik is so popular I feel like our use-case isn't unique.

Copy link
Contributor Author

@bryphe-coder bryphe-coder Jan 21, 2022

Choose a reason for hiding this comment

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

It turns out we actually don't need this (at least not yet) - I thought it would be needed for the dynamic-form-parameters, but in this particular case it's not necessary.

Just for some extra context (the comment sure isn't very helpful) - there are actually are two use cases for forms that formik doesn't handle out of the box, that we needed for cdr/m:

A case where we used this is to have the Organization List for workspace providers be a reusable sub-form across all those providers - with the bonus that each of these sub-forms is easily testable.

  • Dynamic Forms - we have use cases where parts of the form need to be changed based on other form fields. formik is great for static forms, but doesn't have any capabilities around dynamically changing parts of the form (and actually, having the nested/subforms is sort of helpful for this too).

An example of a use case where we need this in v1 is in the Create Workspace page - toggling between an EC2 provider vs a Kubernetes provider changes the Advanced Section:

2022-01-20 21 33 19

It's possible to workaround not having dynamism in formik by having a 'fat interface' - having your form represent every possible state (like, having EC2/Kubernetes all on the form) - but it gets janky pretty fast, especially around validation. Before we had that subform - we'd have bugs where Kubernetes validation would run when EC2 provider is selected.

I put a lot more context here in introducing it for the EC2 / Kubernetes provider: https://github.com/coder/m/pull/10250

But anyway our here forms aren't crazy enough yet to need anything like this - so I'll keep it simple and remove for now! If we re-introduce, that means there'll be a clear use case for it.

If we keep our forms simple, we might not need it at all - this may be an extra benefit of the Projects workflow - that we don't need fancy dynamic forms 😎

Copy link
Contributor Author

@bryphe-coder bryphe-coder Jan 22, 2022

Choose a reason for hiding this comment

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

Ah, I tried to remove it but unfortunately it turns out it is necessary to give strong-typing to our forms.

Our utilities - like FormTextField - depend on those utils to implement type-safety via typeof:

export interface FormFieldProps<T> {
  /**
   * form is a reference to a form or subform and is used to compute common
   * states such as error and helper text
   */
  form: FormikLike<T>
  /**
   * formFieldName is a field name associated with the form schema.
   */
  formFieldName: keyof T
}

(in addition to the nesting and dynamic aspects above). I think it'll be simplest to preserve these utilities so it's easy to migrate our other form components built on top of it. Hopefully that's OK with you!

The alternative is we can remove formik, and change all those keyof T fields (which provide the extra layer of type safety) with string (which is no longer type-safe, but will compile), and modify all the Form* components in that way as we migrate them from v1 -> v2

},
}))

export const FormRow: React.FC = ({ children }) => {
Copy link
Member

Choose a reason for hiding this comment

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

Do we believe it's safe to say all of our Form instances will follow a similar format? I have lots of hesitancy over a top-level form component. If we even have a single form that doesn't consume it, this top-level component abstraction has broken.

I'm not sure of an alternative though, so maybe this is required!

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 think this is a good point for this component - it's only used in one places so I can bring it inline.

Comment on lines +29 to +51
export const AppPage: React.FC = ({ children }) => {
const styles = useStyles()

const header = (
<div className={styles.header}>
<Navbar />
</div>
)

const footer = (
<div className={styles.footer}>
<Footer />
</div>
)

return (
<div className={styles.root}>
{header}
{children}
{footer}
</div>
)
}
Copy link
Member

Choose a reason for hiding this comment

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

I've abstracted into this general page format before, and I've generally regretted it.

Could we put the styles.header directly on the Navbar component? And same for the footer? Then for root, put those in _app.tsx. Each page can put <Navbar /> and <Footer /> in itself. It's a bit of duplication, but I think that's probably fine!

This comment is a bit confusing, so please ping me if it doesn't make sense!

Copy link
Contributor Author

@bryphe-coder bryphe-coder Jan 21, 2022

Choose a reason for hiding this comment

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

IMO, I think if we pick the right abstraction - these sort of components can reduce cognitive load when thinking about components.

Like, from my point-of-view:

export const ProjectsPage: React.FC = () => {
<AppPage>
  {...stuff to render projects...}
</AppPage>
}

is simpler to reason about than:

export const ProjectsPage: React.FC = () => {
<AuthenticatedRouter>
  <div-with-proper-fullscreen-styles>
  <Navbar>
  {...stuff to render projects...}
  </Footer>
  </div>
</AuthenticatedRouter>
}

In the first case - I can really focus on the 'guts' of the ProjectsPage, whereas in the second, I have to start think about other details - what are the right styles for the top-level component? Does the body need a wrapper div, or can it be at the same level as Navbar / Footer.

This composability of React is part of the power of it - there's the Thinking in React section in the React documentation that talks about some guidelines for thinking in components / breaking things up into components. Generally start with simple components and compose them into more intricate ones (but of course there's a lot of judgement in it).

I've abstracted into this general page format before, and I've generally regretted it.

I've seen some cases too like this - the flip-side is if you pick the wrong abstraction, and the user not only has to think about the abstraction but then twist it to fit their use-case - it ends up increasing cognitive load instead of decreasing it. I think an example of a case we hit in v1 was in the PaperForms - I'm not sure if the product evolved from the original design, I imagine it was very clean to start, but down the road we ended up in a place where the abstraction just didn't quite fit in any of our forms, and we'd have to introduce extra complexity just to make it work. Is that the sort of the thing you've ran into as well, or are you thinking about a different case?

The key to avoiding the case I called out above is to be diligent in monitoring the abstractions and making the right decisions in when to change / refactor and avoid making components too general. Sometimes by avoiding duplication we end up with components that try and handle everything, but don't handle anything cleanly! It feels like an art though sometimes more than a science...

My personal preference would be to keep this as <AppPage /> because I think it'll make my life easier as I add more pages - but if you feel strongly about decomposing it, we can do that and revisit down the road as we have more use cases to inventory across Login / Projects / Create / etc. Ultimately I want you to be happy with what we ship here 😄

Copy link
Member

Choose a reason for hiding this comment

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

A wonderfully elegant explanation of the rationale. I agree with your perspective, but I'm concerned about the ambiguity of an <AppPage /> specifically. Maybe the name rubs me the wrong way.

In my imagination, we'd structure things like:

UserContext that's added in _app.tsx. Every new page load the user would be fetched again. To consume the user context, a parameter is passed in whether to redirect or not. See:
https://github.com/coder/link/blob/main/site/src/contexts/UserContext.tsx#L20-L32

In each page, we'd consume the useUser() function. For authenticated pages, we'd have useUser(true) to redirect to login on failure.

Most of the page logic would be in the pages/projects.tsx page themselves, removing the need to abstract the bulk from _app.tsx -> pages/*.tsx -> components/*.tsx.

If we can accomplish a similar level of elegance with <AppPage />, I'm game for it. Better I just share my objective haha!

@bryphe-coder bryphe-coder mentioned this pull request Jan 21, 2022
8 tasks
@bryphe-coder
Copy link
Contributor Author

This is now handled by #58 and #60

@kylecarbs kylecarbs deleted the bryphe/feat/initial-create-form branch March 23, 2022 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants