-
Notifications
You must be signed in to change notification settings - Fork 443
Port login page to preact #9589
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
base: main
Are you sure you want to change the base?
Conversation
For the files which are re-used unchanged from the group forms app, I would suggest to simply re-import them from there for the moment. One of the frontend devs can then look at moving them to a shared directory and adjusting the tests etc. |
Thank you. I was going to lift them up to |
10962d0
to
da90554
Compare
inputType?: 'text' | 'password'; | ||
|
||
/** Name of the input field. */ | ||
name?: string; |
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 need to make some changes for text properties because we actually submit this React form, not make API calls from here.
@@ -60,6 +74,9 @@ export type TextFieldProps = { | |||
|
|||
/** Additional classes to apply to the input element. */ | |||
classes?: string; | |||
|
|||
/** Optional error message for the field. */ | |||
fieldError?: string; |
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.
And this is field validation error we get from backend login schema.
I've tried to make regular input error work but unfortunately it seems to be designed for in-place submit validation, not when the error is coming from the outside.
{fieldError && ( | ||
<> | ||
<div className="grow" /> | ||
<ErrorNotice message={fieldError} /> |
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.
Conditionally rendering the field error
classes={classes} | ||
autofocus={autofocus} | ||
autocomplete="off" | ||
required={required} | ||
{...(name ? { name: name } : {})} | ||
{...(inputType === 'password' ? { type: 'password' } : {})} |
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 works, but I'm not a fan. Maybe we actually need a separate text field for login/signup form, but the changes seem to be minor, so I'm not sure.
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.
You can set these fields but use the value undefined
and they won't appear in the HTML:
diff --git a/h/static/scripts/group-forms/components/forms/TextField.tsx b/h/static/scripts/group-forms/components/forms/TextField.tsx
index 0925b7177..bea45fcfe 100644
--- a/h/static/scripts/group-forms/components/forms/TextField.tsx
+++ b/h/static/scripts/group-forms/components/forms/TextField.tsx
@@ -78,7 +78,7 @@ export type TextFieldProps = {
*/
export default function TextField({
type = 'input',
- inputType = 'text',
+ inputType,
value,
onChangeValue,
minLength = 0,
@@ -87,7 +87,7 @@ export default function TextField({
required = false,
autofocus = false,
classes = '',
- name = '',
+ name,
fieldError = '',
}: TextFieldProps) {
const id = useId();
@@ -131,8 +131,8 @@ export default function TextField({
autofocus={autofocus}
autocomplete="off"
required={required}
- {...(name ? { name: name } : {})}
- {...(inputType === 'password' ? { type: 'password' } : {})}
+ name={name}
+ type={inputType}
/>
{typeof maxLength === 'number' && (
<CharacterCounter
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.
Perfect, thank you!
<Config.Provider value={config}> | ||
<Router> | ||
<Switch> | ||
<Route path={routes.login}> |
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'm thinking to extend this with sign up route in the switch when we get to that rather than creating a new sign up react app.
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.
Yes, I think that will make sense.
data-testid="form" | ||
className="max-w-[530px] mx-auto flex flex-col gap-y-4" | ||
> | ||
<input type="hidden" name="csrf_token" value={config.csrfToken} /> |
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.
Setting csrf token from js config on the form for pyramid.
da90554
to
64122dc
Compare
</footer> | ||
</div> | ||
</div> | ||
{% endblock %} |
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 mostly follows what we have in group jinja2 html template.
@view_config( | ||
request_method="GET", | ||
request_param="for_oauth", | ||
renderer="h:templates/accounts/login_oauth.html.jinja2", | ||
) | ||
def get(self): | ||
"""Render the login page, including the login form.""" | ||
def get_oauth(self): |
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.
Splitting these two routes for now to limit the scope of this PR.
So, regular login is covered by React app and open authorization login is covered by Jinja2 at this point.
h/views/accounts.py
Outdated
csrf_token = get_csrf_token(self.request) | ||
|
||
return { | ||
"styles": self.request.registry["assets_env"].urls("group_forms_css"), |
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.
Reusing group form styles because basically these are just tailwind styles, which is exactly the same we need for login form.
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.
Something to know about how Tailwind works is that it scans a specified set of files looking for Tailwind classes that are used and only generates those styles in the CSS. The set of files that are scanned includes all of our TSX files under h/static/scripts
, so re-using the CSS here will work. It will probably make sense to rename this bundle to something more general.
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.
Sure, renamed to forms.css
except deform.ValidationFailure as e: | ||
js_config = self._js_config() | ||
js_config["formErrors"] = e.error.asdict() | ||
js_config["formData"] = { |
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 need to pass back both form errors and form data for pre-fill if backend POST validation fails.
Again, this is a separate route to take care of regular POST login for now.
I'm using camel case here because this data is prepared by backend but used by frontend.
res.form["username"] = user.username | ||
res.form["password"] = "pass" # noqa: S105 | ||
res.form.submit() | ||
js_config = json.loads(res.html.find("script", class_="js-config").text) |
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.
Since login form is now rendered by React, we need to make POST request here for login, taking csrf token from HTML page body.
6d33dbe
to
fe871c4
Compare
fe871c4
to
cac877b
Compare
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 looking good overall from an initial glance. I think we should remove the visible "required" markers from the fields in this form, though we will need to keep the required
attribute on the form field. This will require a small change to the TextField
API.
classes={classes} | ||
autofocus={autofocus} | ||
autocomplete="off" | ||
required={required} | ||
{...(name ? { name: name } : {})} | ||
{...(inputType === 'password' ? { type: 'password' } : {})} |
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.
You can set these fields but use the value undefined
and they won't appear in the HTML:
diff --git a/h/static/scripts/group-forms/components/forms/TextField.tsx b/h/static/scripts/group-forms/components/forms/TextField.tsx
index 0925b7177..bea45fcfe 100644
--- a/h/static/scripts/group-forms/components/forms/TextField.tsx
+++ b/h/static/scripts/group-forms/components/forms/TextField.tsx
@@ -78,7 +78,7 @@ export type TextFieldProps = {
*/
export default function TextField({
type = 'input',
- inputType = 'text',
+ inputType,
value,
onChangeValue,
minLength = 0,
@@ -87,7 +87,7 @@ export default function TextField({
required = false,
autofocus = false,
classes = '',
- name = '',
+ name,
fieldError = '',
}: TextFieldProps) {
const id = useId();
@@ -131,8 +131,8 @@ export default function TextField({
autofocus={autofocus}
autocomplete="off"
required={required}
- {...(name ? { name: name } : {})}
- {...(inputType === 'password' ? { type: 'password' } : {})}
+ name={name}
+ type={inputType}
/>
{typeof maxLength === 'number' && (
<CharacterCounter
<Config.Provider value={config}> | ||
<Router> | ||
<Switch> | ||
<Route path={routes.login}> |
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.
Yes, I think that will make sense.
<span> | ||
{/* These are in a child span to avoid a gap between them. */} | ||
<Star /> | ||
Required |
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.
For a login form I think it is sufficiently obvious that the username and password are required that we can omit this visual indicator. We do need to make sure that the required
field is still set on the <input>
though.
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.
Yep, should've made it the same as the original form rather than taking that from group form. Thanks.
}; | ||
|
||
/** Return the frontend config from the page's <script class="js-config">. */ | ||
export function readConfig(): ConfigObject { |
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 it will make sense to re-use the readConfig
function from the group forms module, and make that generic, but this can be done as part of some refactoring after this lands.
|
||
function init() { | ||
const shadowHost = document.querySelector('#login-form')!; | ||
const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); |
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 suspect we can simplify things for ourselves by removing the use of shadow DOM in this form, but that's out of scope for this PR.
h/static/styles/login-forms.css
Outdated
@@ -0,0 +1,6 @@ | |||
/* CSS entry point for the login app. */ |
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 file appears to be unused.
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.
Yep, removed
h/views/accounts.py
Outdated
csrf_token = get_csrf_token(self.request) | ||
|
||
return { | ||
"styles": self.request.registry["assets_env"].urls("group_forms_css"), |
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.
Something to know about how Tailwind works is that it scans a specified set of files looking for Tailwind classes that are used and only generates those styles in the CSS. The set of files that are scanned includes all of our TSX files under h/static/scripts
, so re-using the CSS here will work. It will probably make sense to rename this bundle to something more general.
1544491
to
68ea2f0
Compare
68ea2f0
to
671f84b
Compare
Added separate |
Refs #9588
Here we port login page to react in a hybrid way.
React app renders the form, but we actually submit it. And POST request is handled by the backend.
Login form controller is mostly based on group form controller.
For now it only affects regular login and open authorization popup login still uses Jinja2 for easier porting, which we'll address next.
Reusable components like text field will need to be lifted up from group form app.
Testing
/login
page/login?for_oauth=true
page