Make UI for authentication flow match the rest of the dashboard (#8211)
- Changes all authentication screens to look similar to the rest of the dashboard - Greatly simplifies HTML structure # Important Notes - This is being added before the official design is ready, *but* it should be useful anyway, because it greatly simplifies the HTML, which should make it easier to implement the new design - The auth screens have a larger border-radius than all other elements in the app. This is intentional, to make them look like they continue naturally from the fully rounded submit buttons. - Basic testing done: - Logging in should still work - Signing up should still work - Setting username should still work - Changing password should still work - Forgot password should still work - Password reset should still work
@ -18,9 +18,9 @@ const REGISTRATION_QUERY_PARAMS = {
|
||||
email: 'email',
|
||||
} as const
|
||||
|
||||
// ============================
|
||||
// === Confirm Registration ===
|
||||
// ============================
|
||||
// ===========================
|
||||
// === ConfirmRegistration ===
|
||||
// ===========================
|
||||
|
||||
/** An empty component redirecting users based on the backend response to user registration. */
|
||||
export default function ConfirmRegistration() {
|
||||
|
@ -0,0 +1,101 @@
|
||||
/** @file Styled input element. */
|
||||
import * as React from 'react'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The delay to wait before checking input validity. */
|
||||
const DEBOUNCE_MS = 1000
|
||||
|
||||
// =======================
|
||||
// === ControlledInput ===
|
||||
// =======================
|
||||
|
||||
/** Props for an {@link Input}. */
|
||||
export interface ControlledInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
value: string
|
||||
error?: string
|
||||
validate?: boolean
|
||||
setValue: (value: string) => void
|
||||
shouldReportValidityRef?: React.MutableRefObject<boolean>
|
||||
}
|
||||
|
||||
/** A component for authentication from inputs, with preset styles. */
|
||||
export default function ControlledInput(props: ControlledInputProps) {
|
||||
const {
|
||||
setValue,
|
||||
error,
|
||||
validate = false,
|
||||
shouldReportValidityRef,
|
||||
onChange,
|
||||
onBlur,
|
||||
...passThrough
|
||||
} = props
|
||||
const [reportTimeoutHandle, setReportTimeoutHandle] = React.useState<number | null>(null)
|
||||
const [hasReportedValidity, setHasReportedValidity] = React.useState(false)
|
||||
const [wasJustBlurred, setWasJustBlurred] = React.useState(false)
|
||||
return (
|
||||
<input
|
||||
{...passThrough}
|
||||
onChange={event => {
|
||||
onChange?.(event)
|
||||
setValue(event.target.value)
|
||||
setWasJustBlurred(false)
|
||||
if (validate) {
|
||||
if (reportTimeoutHandle != null) {
|
||||
window.clearTimeout(reportTimeoutHandle)
|
||||
}
|
||||
const currentTarget = event.currentTarget
|
||||
if (error != null) {
|
||||
currentTarget.setCustomValidity('')
|
||||
currentTarget.setCustomValidity(
|
||||
currentTarget.checkValidity() ||
|
||||
shouldReportValidityRef?.current === false
|
||||
? ''
|
||||
: error
|
||||
)
|
||||
}
|
||||
if (hasReportedValidity) {
|
||||
if (
|
||||
shouldReportValidityRef?.current === false ||
|
||||
currentTarget.checkValidity()
|
||||
) {
|
||||
setHasReportedValidity(false)
|
||||
}
|
||||
} else {
|
||||
setReportTimeoutHandle(
|
||||
window.setTimeout(() => {
|
||||
if (
|
||||
shouldReportValidityRef?.current !== false &&
|
||||
!currentTarget.reportValidity()
|
||||
) {
|
||||
setHasReportedValidity(true)
|
||||
}
|
||||
}, DEBOUNCE_MS)
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={
|
||||
validate
|
||||
? event => {
|
||||
onBlur?.(event)
|
||||
if (wasJustBlurred) {
|
||||
setHasReportedValidity(false)
|
||||
} else {
|
||||
const currentTarget = event.currentTarget
|
||||
if (shouldReportValidityRef?.current !== false) {
|
||||
if (!currentTarget.reportValidity()) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
setWasJustBlurred(true)
|
||||
}
|
||||
}
|
||||
: onBlur
|
||||
}
|
||||
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full border w-full py-2"
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
/** @file Registration confirmation page for when a user clicks the confirmation link set to their
|
||||
* email address. */
|
||||
/** @file Page to enter offlin mode and redirect to dashboard. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as app from '../../components/app'
|
||||
import * as authProvider from '../providers/auth'
|
||||
import * as hooks from '../../hooks'
|
||||
|
||||
// ============================
|
||||
// === Confirm Registration ===
|
||||
// ============================
|
||||
// ========================
|
||||
// === EnterOfflineMode ===
|
||||
// ========================
|
||||
|
||||
/** An empty component redirecting users based on the backend response to user registration. */
|
||||
export default function EnterOfflineMode() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
/** @file Styled wrapper around FontAwesome icons. */
|
||||
/** @file A fixed-size container for a {@link fontawesome.FontAwesomeIcon FontAwesomeIcon}. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as fontawesome from '@fortawesome/react-fontawesome'
|
||||
@ -16,13 +16,9 @@ export interface FontAwesomeIconProps {
|
||||
/** A fixed-size container for a {@link fontawesome.FontAwesomeIcon FontAwesomeIcon}. */
|
||||
export default function FontAwesomeIcon(props: FontAwesomeIconProps) {
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
'absolute left-0 top-0 flex items-center justify-center h-full w-10 ' +
|
||||
'text-blue-500'
|
||||
}
|
||||
>
|
||||
<fontawesome.FontAwesomeIcon icon={props.icon} />
|
||||
</span>
|
||||
<fontawesome.FontAwesomeIcon
|
||||
className="absolute left-0 top-0 text-blue-500 px-3 h-full w-4"
|
||||
icon={props.icon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
/** @file Container responsible for rendering and interactions in first half of forgot password
|
||||
* flow. */
|
||||
import * as React from 'react'
|
||||
import * as router from 'react-router-dom'
|
||||
|
||||
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
||||
import AtIcon from 'enso-assets/at.svg'
|
||||
@ -9,10 +8,10 @@ import GoBackIcon from 'enso-assets/go_back.svg'
|
||||
|
||||
import * as app from '../../components/app'
|
||||
import * as auth from '../providers/auth'
|
||||
import SvgMask from './svgMask'
|
||||
|
||||
import Input from './input'
|
||||
import SvgIcon from './svgIcon'
|
||||
import Link from './link'
|
||||
import SubmitButton from './submitButton'
|
||||
|
||||
// ======================
|
||||
// === ForgotPassword ===
|
||||
@ -25,72 +24,29 @@ export default function ForgotPassword() {
|
||||
const [email, setEmail] = React.useState('')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center">
|
||||
<div className="flex flex-col bg-white shadow-md p-8 rounded-md w-full max-w-md">
|
||||
<div className="font-medium self-center text-xl uppercase text-gray-800">
|
||||
Forgot Your Password?
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<form
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
await forgotPassword(email)
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col mb-6">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
E-Mail Address:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={AtIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
placeholder="E-Mail Address"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<button
|
||||
type="submit"
|
||||
className={
|
||||
'flex items-center justify-center focus:outline-none text-white text-sm ' +
|
||||
'bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition ' +
|
||||
'duration-150 ease-in'
|
||||
}
|
||||
>
|
||||
<span className="mr-2 uppercase">Send link</span>
|
||||
<span>
|
||||
<SvgMask src={ArrowRightIcon} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="flex justify-center items-center mt-6">
|
||||
<router.Link
|
||||
to={app.LOGIN_PATH}
|
||||
className={
|
||||
'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 text-xs ' +
|
||||
'text-center'
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<SvgMask src={GoBackIcon} />
|
||||
</span>
|
||||
<span className="ml-2">Go back to login</span>
|
||||
</router.Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
|
||||
<form
|
||||
className="flex flex-col gap-6 bg-frame-selected rounded-2xl shadow-md p-8 w-full max-w-md"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
await forgotPassword(email)
|
||||
}}
|
||||
>
|
||||
<div className="font-medium self-center text-xl">Forgot Your Password?</div>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
label="Email"
|
||||
icon={AtIcon}
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
/>
|
||||
<SubmitButton text="Send link" icon={ArrowRightIcon} />
|
||||
</form>
|
||||
<Link to={app.LOGIN_PATH} icon={GoBackIcon} text="Go back to login" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,101 +1,37 @@
|
||||
/** @file Styled input element. */
|
||||
/** @file A styled input that includes an icon. */
|
||||
import * as React from 'react'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The delay to wait before checking input validity. */
|
||||
const DEBOUNCE_MS = 1000
|
||||
import type * as controlledInput from './controlledInput'
|
||||
import ControlledInput from './controlledInput'
|
||||
import SvgIcon from './svgIcon'
|
||||
|
||||
// =============
|
||||
// === Input ===
|
||||
// =============
|
||||
|
||||
/** Props for an {@link Input}. */
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
value: string
|
||||
error?: string
|
||||
validate?: boolean
|
||||
setValue: (value: string) => void
|
||||
shouldReportValidityRef?: React.MutableRefObject<boolean>
|
||||
/** Props for a {@link Input}. */
|
||||
export interface InputProps extends controlledInput.ControlledInputProps {
|
||||
label: string | null
|
||||
icon: string
|
||||
footer?: React.ReactNode
|
||||
}
|
||||
|
||||
/** A component for authentication from inputs, with preset styles. */
|
||||
/** A styled input that includes an icon. */
|
||||
export default function Input(props: InputProps) {
|
||||
const {
|
||||
setValue,
|
||||
error,
|
||||
validate = false,
|
||||
shouldReportValidityRef,
|
||||
onChange,
|
||||
onBlur,
|
||||
...passThrough
|
||||
} = props
|
||||
const [reportTimeoutHandle, setReportTimeoutHandle] = React.useState<number | null>(null)
|
||||
const [hasReportedValidity, setHasReportedValidity] = React.useState(false)
|
||||
const [wasJustBlurred, setWasJustBlurred] = React.useState(false)
|
||||
return (
|
||||
<input
|
||||
{...passThrough}
|
||||
onChange={event => {
|
||||
onChange?.(event)
|
||||
setValue(event.target.value)
|
||||
setWasJustBlurred(false)
|
||||
if (validate) {
|
||||
if (reportTimeoutHandle != null) {
|
||||
window.clearTimeout(reportTimeoutHandle)
|
||||
}
|
||||
const currentTarget = event.currentTarget
|
||||
if (error != null) {
|
||||
currentTarget.setCustomValidity('')
|
||||
currentTarget.setCustomValidity(
|
||||
currentTarget.checkValidity() ||
|
||||
shouldReportValidityRef?.current === false
|
||||
? ''
|
||||
: error
|
||||
)
|
||||
}
|
||||
if (hasReportedValidity) {
|
||||
if (
|
||||
shouldReportValidityRef?.current === false ||
|
||||
currentTarget.checkValidity()
|
||||
) {
|
||||
setHasReportedValidity(false)
|
||||
}
|
||||
} else {
|
||||
setReportTimeoutHandle(
|
||||
window.setTimeout(() => {
|
||||
if (
|
||||
shouldReportValidityRef?.current !== false &&
|
||||
!currentTarget.reportValidity()
|
||||
) {
|
||||
setHasReportedValidity(true)
|
||||
}
|
||||
}, DEBOUNCE_MS)
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={
|
||||
validate
|
||||
? event => {
|
||||
onBlur?.(event)
|
||||
if (wasJustBlurred) {
|
||||
setHasReportedValidity(false)
|
||||
} else {
|
||||
const currentTarget = event.currentTarget
|
||||
if (shouldReportValidityRef?.current !== false) {
|
||||
if (!currentTarget.reportValidity()) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
setWasJustBlurred(true)
|
||||
}
|
||||
}
|
||||
: onBlur
|
||||
}
|
||||
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-lg border border-gray-400 w-full py-2 focus:outline-none focus:border-blue-400"
|
||||
/>
|
||||
const { label, icon, footer, ...passthrough } = props
|
||||
const input = (
|
||||
<div className="relative">
|
||||
<SvgIcon src={icon} />
|
||||
<ControlledInput {...passthrough} />
|
||||
</div>
|
||||
)
|
||||
return label != null || footer != null ? (
|
||||
<label className="flex flex-col gap-1">
|
||||
{label}
|
||||
{input}
|
||||
{footer}
|
||||
</label>
|
||||
) : (
|
||||
input
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
/** @file A styled colored link with an icon. */
|
||||
import * as React from 'react'
|
||||
import * as router from 'react-router-dom'
|
||||
|
||||
import SvgMask from './svgMask'
|
||||
|
||||
// ============
|
||||
// === Link ===
|
||||
// ============
|
||||
|
||||
/** Props for a {@link Link}. */
|
||||
export interface LinkProps {
|
||||
to: string
|
||||
icon: string
|
||||
text: string
|
||||
}
|
||||
|
||||
/** A styled colored link with an icon. */
|
||||
export default function Link(props: LinkProps) {
|
||||
const { to, icon, text } = props
|
||||
return (
|
||||
<router.Link
|
||||
to={to}
|
||||
className="flex gap-2 items-center font-bold text-blue-500 hover:text-blue-700 text-xs text-center"
|
||||
>
|
||||
<SvgMask src={icon} />
|
||||
{text}
|
||||
</router.Link>
|
||||
)
|
||||
}
|
@ -15,8 +15,8 @@ import * as validation from '../../dashboard/validation'
|
||||
import * as app from '../../components/app'
|
||||
import FontAwesomeIcon from './fontAwesomeIcon'
|
||||
import Input from './input'
|
||||
import SvgIcon from './svgIcon'
|
||||
import SvgMask from './svgMask'
|
||||
import Link from './link'
|
||||
import SubmitButton from './submitButton'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -43,157 +43,99 @@ export default function Login() {
|
||||
const shouldReportValidityRef = React.useRef(true)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center">
|
||||
<div className="flex flex-col bg-white shadow-md p-8 rounded-md w-full max-w-md">
|
||||
<div className="font-medium self-center text-xl uppercase text-gray-800">
|
||||
Login To Your Account
|
||||
</div>
|
||||
<button
|
||||
onMouseDown={() => {
|
||||
shouldReportValidityRef.current = false
|
||||
}}
|
||||
onClick={async event => {
|
||||
event.preventDefault()
|
||||
await signInWithGoogle()
|
||||
}}
|
||||
className="relative mt-6 border rounded-md py-2 text-sm text-gray-800 bg-gray-100 hover:bg-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={fontawesomeIcons.faGoogle} />
|
||||
<span>Sign Up or Login with Google</span>
|
||||
</button>
|
||||
<button
|
||||
onMouseDown={() => {
|
||||
shouldReportValidityRef.current = false
|
||||
}}
|
||||
onClick={async event => {
|
||||
event.preventDefault()
|
||||
await signInWithGitHub()
|
||||
}}
|
||||
className="relative mt-6 border rounded-md py-2 text-sm text-gray-800 bg-gray-100 hover:bg-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={fontawesomeIcons.faGithub} />
|
||||
<span>Sign Up or Login with Github</span>
|
||||
</button>
|
||||
<div className="relative mt-10 h-px bg-gray-300">
|
||||
<div className="absolute left-0 top-0 flex justify-center w-full -mt-2">
|
||||
<span className="bg-white px-4 text-xs text-gray-500 uppercase">
|
||||
Or Login With Email
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<form
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
await signInWithPassword(email, password)
|
||||
shouldReportValidityRef.current = true
|
||||
setIsSubmitting(false)
|
||||
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
|
||||
<div className="flex flex-col gap-6 bg-frame-selected rounded-4xl shadow-md p-8 w-full max-w-md">
|
||||
<div className="font-medium self-center text-xl">Login to your account</div>
|
||||
<div className="flex flex-col gap-6">
|
||||
<button
|
||||
onMouseDown={() => {
|
||||
shouldReportValidityRef.current = false
|
||||
}}
|
||||
onClick={async event => {
|
||||
event.preventDefault()
|
||||
await signInWithGoogle()
|
||||
}}
|
||||
className="relative rounded-full bg-cloud/10 hover:bg-gray-200 py-2"
|
||||
>
|
||||
<div className="flex flex-col mb-6">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
E-Mail Address:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={AtIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
placeholder="E-Mail Address"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
shouldReportValidityRef={shouldReportValidityRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col mb-6">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
Password:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={LockIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={password}
|
||||
setValue={setPassword}
|
||||
shouldReportValidityRef={shouldReportValidityRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center mb-6 -mt-4">
|
||||
<div className="flex ml-auto">
|
||||
<router.Link
|
||||
to={app.FORGOT_PASSWORD_PATH}
|
||||
className="inline-flex text-xs text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
Forgot Your Password?
|
||||
</router.Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className={
|
||||
'flex items-center justify-center focus:outline-none text-white ' +
|
||||
'text-sm bg-blue-600 hover:bg-blue-700 rounded py-2 w-full ' +
|
||||
'transition duration-150 ease-in disabled:opacity-50'
|
||||
}
|
||||
>
|
||||
<span className="mr-2 uppercase">Login</span>
|
||||
<SvgMask src={ArrowRightIcon} />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<FontAwesomeIcon icon={fontawesomeIcons.faGoogle} />
|
||||
Sign up or login with Google
|
||||
</button>
|
||||
<button
|
||||
onMouseDown={() => {
|
||||
shouldReportValidityRef.current = false
|
||||
}}
|
||||
onClick={async event => {
|
||||
event.preventDefault()
|
||||
await signInWithGitHub()
|
||||
}}
|
||||
className="relative rounded-full bg-cloud/10 hover:bg-gray-200 py-2"
|
||||
>
|
||||
<FontAwesomeIcon icon={fontawesomeIcons.faGithub} />
|
||||
Sign up or login with GitHub
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-center items-center mt-6">
|
||||
<router.Link
|
||||
to={app.REGISTRATION_PATH}
|
||||
className={
|
||||
'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 ' +
|
||||
'text-xs text-center'
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grow border-t border-primary/30 h-0" />
|
||||
<span className="text-xs self-center text-primary/60">or login with email</span>
|
||||
<div className="grow border-t border-primary/30 h-0" />
|
||||
</div>
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
await signInWithPassword(email, password)
|
||||
shouldReportValidityRef.current = true
|
||||
setIsSubmitting(false)
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
label="Email"
|
||||
icon={AtIcon}
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
shouldReportValidityRef={shouldReportValidityRef}
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
label="Password"
|
||||
icon={LockIcon}
|
||||
placeholder="Enter your password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={password}
|
||||
setValue={setPassword}
|
||||
shouldReportValidityRef={shouldReportValidityRef}
|
||||
footer={
|
||||
<router.Link
|
||||
to={app.FORGOT_PASSWORD_PATH}
|
||||
className="text-xs text-blue-500 hover:text-blue-700 text-end"
|
||||
>
|
||||
Forgot Your Password?
|
||||
</router.Link>
|
||||
}
|
||||
>
|
||||
<SvgMask src={CreateAccountIcon} />
|
||||
<span className="ml-2">You don't have an account?</span>
|
||||
</router.Link>
|
||||
</div>
|
||||
<div className="flex justify-center items-center mt-6">
|
||||
<router.Link
|
||||
to={app.ENTER_OFFLINE_MODE_PATH}
|
||||
className={
|
||||
'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 ' +
|
||||
'text-xs text-center'
|
||||
}
|
||||
>
|
||||
<SvgMask src={ArrowRightIcon} />
|
||||
<span className="ml-2">Continue without creating an account</span>
|
||||
</router.Link>
|
||||
</div>
|
||||
/>
|
||||
<SubmitButton disabled={isSubmitting} text="Login" icon={ArrowRightIcon} />
|
||||
</form>
|
||||
</div>
|
||||
<Link
|
||||
to={app.REGISTRATION_PATH}
|
||||
icon={CreateAccountIcon}
|
||||
text="Don't have an account?"
|
||||
/>
|
||||
<Link
|
||||
to={app.ENTER_OFFLINE_MODE_PATH}
|
||||
icon={ArrowRightIcon}
|
||||
text="Continue without creating an account"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ import * as validation from '../../dashboard/validation'
|
||||
|
||||
import * as app from '../../components/app'
|
||||
import Input from './input'
|
||||
import SvgIcon from './svgIcon'
|
||||
import SvgMask from './svgMask'
|
||||
import Link from './link'
|
||||
import SubmitButton from './submitButton'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -39,130 +39,57 @@ export default function Registration() {
|
||||
const { organizationId } = parseUrlSearchParams(location.search)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen p-8">
|
||||
<div
|
||||
className={
|
||||
'rounded-md bg-white w-full max-w-sm sm:max-w-md border border-gray-200 ' +
|
||||
'shadow-md px-4 py-6 sm:p-8'
|
||||
}
|
||||
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
|
||||
<form
|
||||
className="flex flex-col gap-6 bg-frame-selected rounded-4xl shadow-md p-8 w-full max-w-md"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
await auth.signUp(email, password, organizationId)
|
||||
setIsSubmitting(false)
|
||||
}}
|
||||
>
|
||||
<div className="font-medium self-center text-xl uppercase text-gray-800">
|
||||
Create new account
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
await auth.signUp(email, password, organizationId)
|
||||
setIsSubmitting(false)
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col mb-4">
|
||||
<label htmlFor="email" className="mb-1 text-xs tracking-wide text-gray-600">
|
||||
E-Mail Address:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={AtIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
placeholder="E-Mail Address"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col mb-4">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
Password:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={LockIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
placeholder="Password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={password}
|
||||
setValue={setPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col mb-4">
|
||||
<label
|
||||
htmlFor="password_confirmation"
|
||||
className="mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
Confirm Password:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={LockIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
name="password_confirmation"
|
||||
autoComplete="new-password"
|
||||
placeholder="Confirm Password"
|
||||
pattern={string.regexEscape(password)}
|
||||
error={validation.CONFIRM_PASSWORD_ERROR}
|
||||
value={confirmPassword}
|
||||
setValue={setConfirmPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full mt-6">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className={
|
||||
'flex items-center justify-center focus:outline-none text-white text-sm ' +
|
||||
'bg-indigo-600 hover:bg-indigo-700 rounded py-2 w-full transition ' +
|
||||
'duration-150 ease-in disabled:opacity-50'
|
||||
}
|
||||
>
|
||||
<span className="mr-2 uppercase">Register</span>
|
||||
<span>
|
||||
<SvgMask src={CreateAccountIcon} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="flex justify-center items-center mt-6">
|
||||
<router.Link
|
||||
to={app.LOGIN_PATH}
|
||||
className={
|
||||
'inline-flex items-center font-bold text-indigo-500 hover:text-indigo-700 ' +
|
||||
'text-sm text-center'
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<SvgMask src={GoBackIcon} />
|
||||
</span>
|
||||
<span className="ml-2">Already have an account?</span>
|
||||
</router.Link>
|
||||
</div>
|
||||
<div className="font-medium self-center text-xl">Create a new account</div>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
label="Email"
|
||||
icon={AtIcon}
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label="Password"
|
||||
icon={LockIcon}
|
||||
placeholder="Enter your password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={password}
|
||||
setValue={setPassword}
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label="Confirm password"
|
||||
icon={LockIcon}
|
||||
placeholder="Confirm your password"
|
||||
pattern={string.regexEscape(password)}
|
||||
error={validation.CONFIRM_PASSWORD_ERROR}
|
||||
value={confirmPassword}
|
||||
setValue={setConfirmPassword}
|
||||
/>
|
||||
<SubmitButton disabled={isSubmitting} text="Register" icon={CreateAccountIcon} />
|
||||
</form>
|
||||
<Link to={app.LOGIN_PATH} icon={GoBackIcon} text="Already have an account?" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ import * as string from '../../string'
|
||||
import * as validation from '../../dashboard/validation'
|
||||
|
||||
import Input from './input'
|
||||
import SvgIcon from './svgIcon'
|
||||
import SvgMask from './svgMask'
|
||||
import Link from './link'
|
||||
import SubmitButton from './submitButton'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -53,148 +53,64 @@ export default function ResetPassword() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center">
|
||||
<div className={'flex flex-col bg-white shadow-md p-8 rounded-md w-full ' + 'max-w-md'}>
|
||||
<div className="font-medium self-center text-xl uppercase text-gray-800">
|
||||
Reset Your Password
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<form
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
await onSubmit()
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col mb-6">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
E-Mail Address:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={AtIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
placeholder="E-Mail Address"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col mb-6">
|
||||
<label
|
||||
htmlFor="code"
|
||||
className="mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
Confirmation Code:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={LockIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
id="code"
|
||||
type="text"
|
||||
name="code"
|
||||
autoComplete="one-time-code"
|
||||
placeholder="Confirmation Code"
|
||||
value={code}
|
||||
setValue={setCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col mb-6">
|
||||
<label
|
||||
htmlFor="new_password"
|
||||
className="mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
New Password:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={LockIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
id="new_password"
|
||||
type="password"
|
||||
name="new_password"
|
||||
autoComplete="new-password"
|
||||
placeholder="New Password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={newPassword}
|
||||
setValue={setNewPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col mb-6">
|
||||
<label
|
||||
htmlFor="new_password_confirm"
|
||||
className="mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
Confirm New Password:
|
||||
</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={LockIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
id="new_password_confirm"
|
||||
type="password"
|
||||
name="new_password_confirm"
|
||||
autoComplete="new-password"
|
||||
placeholder="Confirm New Password"
|
||||
pattern={string.regexEscape(newPassword)}
|
||||
error={validation.CONFIRM_PASSWORD_ERROR}
|
||||
value={newPasswordConfirm}
|
||||
setValue={setNewPasswordConfirm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<button
|
||||
type="submit"
|
||||
className={
|
||||
'flex items-center justify-center focus:outline-none text-white text-sm ' +
|
||||
'bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition ' +
|
||||
'duration-150 ease-in'
|
||||
}
|
||||
>
|
||||
<span className="mr-2 uppercase">Reset</span>
|
||||
<span>
|
||||
<SvgMask src={ArrowRightIcon} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="flex justify-center items-center mt-6">
|
||||
<router.Link
|
||||
to={app.LOGIN_PATH}
|
||||
className={
|
||||
'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 text-xs ' +
|
||||
'text-center'
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<SvgMask src={GoBackIcon} />
|
||||
</span>
|
||||
<span className="ml-2">Go back to login</span>
|
||||
</router.Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
|
||||
<form
|
||||
className="flex flex-col gap-6 bg-frame-selected rounded-4xl shadow-md p-8 w-full max-w-md"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
await onSubmit()
|
||||
}}
|
||||
>
|
||||
<div className="font-medium self-center text-xl">Reset your password</div>
|
||||
<Input
|
||||
required
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
label="Email"
|
||||
icon={AtIcon}
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
type="text"
|
||||
autoComplete="one-time-code"
|
||||
label="Confirmation code"
|
||||
icon={LockIcon}
|
||||
placeholder="Enter the confirmation code"
|
||||
value={code}
|
||||
setValue={setCode}
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label="New password"
|
||||
icon={LockIcon}
|
||||
placeholder="Enter your new password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={newPassword}
|
||||
setValue={setNewPassword}
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label="Confirm new password"
|
||||
icon={LockIcon}
|
||||
placeholder="Confirm your new password"
|
||||
pattern={string.regexEscape(newPassword)}
|
||||
error={validation.CONFIRM_PASSWORD_ERROR}
|
||||
value={newPasswordConfirm}
|
||||
setValue={setNewPasswordConfirm}
|
||||
/>
|
||||
<SubmitButton text="Reset" icon={ArrowRightIcon} />
|
||||
</form>
|
||||
<Link to={app.LOGIN_PATH} icon={GoBackIcon} text="Go back to login" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -7,10 +7,9 @@ import AtIcon from 'enso-assets/at.svg'
|
||||
|
||||
import * as auth from '../providers/auth'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import SvgMask from './svgMask'
|
||||
|
||||
import Input from './input'
|
||||
import SvgIcon from './svgIcon'
|
||||
import SubmitButton from './submitButton'
|
||||
|
||||
// ===================
|
||||
// === SetUsername ===
|
||||
@ -25,59 +24,29 @@ export default function SetUsername() {
|
||||
const [username, setUsername] = React.useState('')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center">
|
||||
<div
|
||||
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
|
||||
<form
|
||||
data-testid="set-username-panel"
|
||||
className={
|
||||
'flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full ' +
|
||||
'max-w-md'
|
||||
}
|
||||
className="flex flex-col gap-6 bg-frame-selected rounded-4xl shadow-md p-8 w-full max-w-md"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
await authSetUsername(backend, username, email)
|
||||
}}
|
||||
>
|
||||
<div className="font-medium self-center text-xl uppercase text-gray-800">
|
||||
Set your username
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<form
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
await authSetUsername(backend, username, email)
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col mb-6">
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={AtIcon} />
|
||||
</SvgIcon>
|
||||
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
autoComplete="off"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
setValue={setUsername}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<button
|
||||
type="submit"
|
||||
className={
|
||||
'flex items-center justify-center focus:outline-none text-white text-sm ' +
|
||||
'bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition ' +
|
||||
'duration-150 ease-in'
|
||||
}
|
||||
>
|
||||
<span className="mr-2 uppercase">Set username</span>
|
||||
<span>
|
||||
<SvgMask src={ArrowRightIcon} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-medium self-center text-xl">Set your username</div>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
autoComplete="off"
|
||||
label={null}
|
||||
icon={AtIcon}
|
||||
placeholder="Enter your username"
|
||||
value={username}
|
||||
setValue={setUsername}
|
||||
/>
|
||||
<SubmitButton text="Set username" icon={ArrowRightIcon} />
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
/** @file A styled submit button. */
|
||||
import * as React from 'react'
|
||||
|
||||
import SvgMask from './svgMask'
|
||||
|
||||
// ====================
|
||||
// === SubmitButton ===
|
||||
// ====================
|
||||
|
||||
/** Props for a {@link SubmitButton}. */
|
||||
export interface SubmitButtonProps {
|
||||
disabled?: boolean
|
||||
text: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
/** A styled submit button. */
|
||||
export default function SubmitButton(props: SubmitButtonProps) {
|
||||
const { disabled = false, text, icon } = props
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
className="flex gap-2 items-center justify-center focus:outline-none text-white bg-blue-600 hover:bg-blue-700 rounded-full py-2 w-full transition duration-150 ease-in disabled:opacity-50"
|
||||
>
|
||||
{text}
|
||||
<SvgMask src={icon} />
|
||||
</button>
|
||||
)
|
||||
}
|
@ -1,20 +1,23 @@
|
||||
/** @file Styled wrapper around SVG images. */
|
||||
import * as React from 'react'
|
||||
|
||||
import SvgMask from './svgMask'
|
||||
|
||||
// ===============
|
||||
// === SvgIcon ===
|
||||
// ===============
|
||||
|
||||
/** Props for a {@link SvgIcon}. */
|
||||
export interface SvgIconProps extends React.PropsWithChildren {}
|
||||
export interface SvgIconProps {
|
||||
src: string
|
||||
}
|
||||
|
||||
/** A fixed-size container for a SVG image. */
|
||||
export default function SvgIcon(props: SvgIconProps) {
|
||||
const { children } = props
|
||||
|
||||
const { src } = props
|
||||
return (
|
||||
<div className="inline-flex items-center justify-center absolute left-0 top-0 h-full w-10 text-gray-400">
|
||||
{children}
|
||||
<SvgMask src={src} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1317,7 +1317,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
<div className="flex flex-col w-min min-w-full h-full">
|
||||
{backend.type !== backendModule.BackendType.local && (
|
||||
<div className="sticky top-0 h-0">
|
||||
<div className="block sticky right-0 ml-auto w-29 p-2 z-1">
|
||||
<div className="block sticky right-0 ml-auto w-29 px-2 pt-2.25 pb-1.75 z-1">
|
||||
<div className="inline-flex gap-3">
|
||||
{columnModule.EXTRA_COLUMNS.map(column => (
|
||||
<Button
|
||||
|
@ -1,4 +1,4 @@
|
||||
/** @file Managing the logic and displaying the UI for the password change function. */
|
||||
/** @file A modal for changing the user's password. */
|
||||
import * as React from 'react'
|
||||
|
||||
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
||||
@ -9,14 +9,13 @@ import * as modalProvider from '../../providers/modal'
|
||||
import * as string from '../../string'
|
||||
import * as validation from '../validation'
|
||||
|
||||
import Input from './input'
|
||||
import Input from '../../authentication/components/input'
|
||||
import Modal from './modal'
|
||||
import SvgIcon from './svgIcon'
|
||||
import SvgMask from '../../authentication/components/svgMask'
|
||||
import SubmitButton from '../../authentication/components/submitButton'
|
||||
|
||||
// ==========================
|
||||
// === ResetPasswordModal ===
|
||||
// ==========================
|
||||
// ===========================
|
||||
// === ChangePasswordModal ===
|
||||
// ===========================
|
||||
|
||||
/** A modal for changing the user's password. */
|
||||
export default function ChangePasswordModal() {
|
||||
@ -30,105 +29,70 @@ export default function ChangePasswordModal() {
|
||||
|
||||
return (
|
||||
<Modal centered className="bg-dim">
|
||||
<div
|
||||
<form
|
||||
data-testid="change-password-modal"
|
||||
className="flex flex-col bg-frame-selected backdrop-blur-3xl rounded-2xl p-8 w-full max-w-md"
|
||||
className="flex flex-col gap-6 bg-frame-selected backdrop-blur-3xl rounded-2xl p-8 w-full max-w-md"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
const success = await changePassword(oldPassword, newPassword)
|
||||
setIsSubmitting(false)
|
||||
if (success) {
|
||||
unsetModal()
|
||||
}
|
||||
}}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div className="self-center text-xl">Change Your Password</div>
|
||||
<div className="mt-10">
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
const success = await changePassword(oldPassword, newPassword)
|
||||
setIsSubmitting(false)
|
||||
if (success) {
|
||||
unsetModal()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="old_password">Old Password:</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={LockIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
autoFocus
|
||||
required
|
||||
validate
|
||||
id="old_password"
|
||||
type="password"
|
||||
name="old_password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Old Password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={oldPassword}
|
||||
setValue={setOldPassword}
|
||||
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-2xl w-full py-2 focus:outline-none focus:border-blue-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="new_password">New Password:</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={LockIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
id="new_password"
|
||||
type="password"
|
||||
name="new_password"
|
||||
autoComplete="new-password"
|
||||
placeholder="New Password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={newPassword}
|
||||
setValue={setNewPassword}
|
||||
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full w-full py-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="confirm_new_password">Confirm New Password:</label>
|
||||
<div className="relative">
|
||||
<SvgIcon>
|
||||
<SvgMask src={LockIcon} />
|
||||
</SvgIcon>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
id="confirm_new_password"
|
||||
type="password"
|
||||
name="confirm_new_password"
|
||||
autoComplete="new-password"
|
||||
placeholder="Confirm New Password"
|
||||
pattern={string.regexEscape(newPassword)}
|
||||
error={validation.CONFIRM_PASSWORD_ERROR}
|
||||
value={confirmNewPassword}
|
||||
setValue={setConfirmNewPassword}
|
||||
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full w-full py-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className="flex items-center justify-center text-white text-sm bg-cloud rounded-full gap-2 h-10 disabled:opacity-50"
|
||||
>
|
||||
Reset
|
||||
<SvgMask src={ArrowRightIcon} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center text-xl">Change your password</div>
|
||||
<Input
|
||||
autoFocus
|
||||
required
|
||||
validate
|
||||
id="old_password"
|
||||
type="password"
|
||||
name="old_password"
|
||||
autoComplete="current-password"
|
||||
label="Old password"
|
||||
icon={LockIcon}
|
||||
placeholder="Enter your old password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={oldPassword}
|
||||
setValue={setOldPassword}
|
||||
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full w-full py-2"
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label="New password"
|
||||
icon={LockIcon}
|
||||
placeholder="Enter your new password"
|
||||
pattern={validation.PASSWORD_PATTERN}
|
||||
error={validation.PASSWORD_ERROR}
|
||||
value={newPassword}
|
||||
setValue={setNewPassword}
|
||||
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full w-full py-2"
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
validate
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label="Confirm new password"
|
||||
icon={LockIcon}
|
||||
placeholder="Confirm your new password"
|
||||
pattern={string.regexEscape(newPassword)}
|
||||
error={validation.CONFIRM_PASSWORD_ERROR}
|
||||
value={confirmNewPassword}
|
||||
setValue={setConfirmNewPassword}
|
||||
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full w-full py-2"
|
||||
/>
|
||||
<SubmitButton disabled={isSubmitting} text="Reset" icon={ArrowRightIcon} />
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -777,71 +777,67 @@ export default function Chat(props: ChatProps) {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="rounded-2xl bg-white p-1 mx-2 my-1">
|
||||
<form onSubmit={sendCurrentMessage}>
|
||||
<div>
|
||||
<textarea
|
||||
ref={messageInputRef}
|
||||
rows={1}
|
||||
autoFocus
|
||||
required
|
||||
placeholder="Type your message ..."
|
||||
className="w-full rounded-lg resize-none p-1"
|
||||
onKeyDown={event => {
|
||||
switch (event.key) {
|
||||
case 'Enter': {
|
||||
// If the shift key is not pressed, submit the form.
|
||||
// If the shift key is pressed, keep the default
|
||||
// behavior of adding a newline.
|
||||
if (!event.shiftKey) {
|
||||
event.preventDefault()
|
||||
event.currentTarget.form?.requestSubmit()
|
||||
}
|
||||
}
|
||||
<form className="rounded-2xl bg-white p-1 mx-2 my-1" onSubmit={sendCurrentMessage}>
|
||||
<textarea
|
||||
ref={messageInputRef}
|
||||
rows={1}
|
||||
autoFocus
|
||||
required
|
||||
placeholder="Type your message ..."
|
||||
className="w-full rounded-lg resize-none p-1"
|
||||
onKeyDown={event => {
|
||||
switch (event.key) {
|
||||
case 'Enter': {
|
||||
// If the shift key is not pressed, submit the form.
|
||||
// If the shift key is pressed, keep the default
|
||||
// behavior of adding a newline.
|
||||
if (!event.shiftKey) {
|
||||
event.preventDefault()
|
||||
event.currentTarget.form?.requestSubmit()
|
||||
}
|
||||
}}
|
||||
onInput={event => {
|
||||
const element = event.currentTarget
|
||||
element.style.height = '0px'
|
||||
element.style.height =
|
||||
`min(${MAX_MESSAGE_INPUT_LINES}lh,` +
|
||||
`${element.scrollHeight + 1}px)`
|
||||
const newIsReplyEnabled = NON_WHITESPACE_CHARACTER_REGEX.test(
|
||||
element.value
|
||||
)
|
||||
if (newIsReplyEnabled !== isReplyEnabled) {
|
||||
setIsReplyEnabled(newIsReplyEnabled)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isReplyEnabled}
|
||||
className={`text-xxs text-white rounded-full grow text-left px-1.5 py-1 ${
|
||||
isReplyEnabled ? 'bg-gray-400' : 'bg-gray-300'
|
||||
}`}
|
||||
onClick={event => {
|
||||
sendCurrentMessage(event, true)
|
||||
}}
|
||||
>
|
||||
New question? Click to start a new thread!
|
||||
</button>
|
||||
{/* Spacing. */}
|
||||
<div className="w-0.5" />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isReplyEnabled}
|
||||
className={`text-white bg-blue-600 rounded-full px-1.5 py-1 ${
|
||||
isReplyEnabled ? '' : 'opacity-50'
|
||||
}`}
|
||||
>
|
||||
Reply!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}}
|
||||
onInput={event => {
|
||||
const element = event.currentTarget
|
||||
element.style.height = '0px'
|
||||
element.style.height =
|
||||
`min(${MAX_MESSAGE_INPUT_LINES}lh,` +
|
||||
`${element.scrollHeight + 1}px)`
|
||||
const newIsReplyEnabled = NON_WHITESPACE_CHARACTER_REGEX.test(
|
||||
element.value
|
||||
)
|
||||
if (newIsReplyEnabled !== isReplyEnabled) {
|
||||
setIsReplyEnabled(newIsReplyEnabled)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isReplyEnabled}
|
||||
className={`text-xxs text-white rounded-full grow text-left px-1.5 py-1 ${
|
||||
isReplyEnabled ? 'bg-gray-400' : 'bg-gray-300'
|
||||
}`}
|
||||
onClick={event => {
|
||||
sendCurrentMessage(event, true)
|
||||
}}
|
||||
>
|
||||
New question? Click to start a new thread!
|
||||
</button>
|
||||
{/* Spacing. */}
|
||||
<div className="w-0.5" />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isReplyEnabled}
|
||||
className={`text-white bg-blue-600 rounded-full px-1.5 py-1 ${
|
||||
isReplyEnabled ? '' : 'opacity-50'
|
||||
}`}
|
||||
>
|
||||
Reply!
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{!isPaidUser && (
|
||||
<button
|
||||
className="text-left leading-5 rounded-2xl bg-call-to-action text-white p-2 mx-2 my-1"
|
||||
|
@ -38,49 +38,45 @@ export default function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
|
||||
|
||||
return (
|
||||
<Modal centered className="bg-dim">
|
||||
<div
|
||||
<form
|
||||
data-testid="confirm-delete-modal"
|
||||
ref={element => {
|
||||
element?.focus()
|
||||
}}
|
||||
tabIndex={-1}
|
||||
className="relative rounded-2xl pointer-events-auto before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
|
||||
className="relative flex flex-col gap-2 rounded-2xl w-96 px-4 p-2 pointer-events-auto before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
|
||||
onKeyDown={event => {
|
||||
if (event.key !== 'Escape') {
|
||||
event.stopPropagation()
|
||||
}
|
||||
}}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onSubmit={event => {
|
||||
event.preventDefault()
|
||||
// Consider not calling `onSubmit()` here to make it harder to accidentally
|
||||
// delete an important asset.
|
||||
onSubmit()
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onSubmit={event => {
|
||||
event.preventDefault()
|
||||
// Consider not calling `onSubmit()` here to make it harder to accidentally
|
||||
// delete an important asset.
|
||||
onSubmit()
|
||||
}}
|
||||
className="relative flex flex-col rounded-2xl gap-2 w-96 px-4 p-2"
|
||||
>
|
||||
<div>Are you sure you want to delete {description}?</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="hover:cursor-pointer inline-block text-white bg-delete rounded-full px-4 py-1"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:cursor-pointer inline-block bg-frame-selected rounded-full px-4 py-1"
|
||||
onClick={unsetModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="relative">Are you sure you want to delete {description}?</div>
|
||||
<div className="relative flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="hover:cursor-pointer inline-block text-white bg-delete rounded-full px-4 py-1"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:cursor-pointer inline-block bg-frame-selected rounded-full px-4 py-1"
|
||||
onClick={unsetModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -1,80 +0,0 @@
|
||||
/** @file Input element with default event handlers. */
|
||||
import * as React from 'react'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The delay to wait before checking input validity. */
|
||||
const DEBOUNCE_MS = 1000
|
||||
|
||||
// =============
|
||||
// === Input ===
|
||||
// =============
|
||||
|
||||
/** Props for an `<input>` HTML element. */
|
||||
type InputAttributes = JSX.IntrinsicElements['input']
|
||||
|
||||
/** Props for an {@link Input}. */
|
||||
export interface InputProps extends InputAttributes {
|
||||
value: string
|
||||
error?: string
|
||||
validate?: boolean
|
||||
setValue: (value: string) => void
|
||||
}
|
||||
|
||||
/** A component for authentication form inputs, with preset styles. */
|
||||
export default function Input(props: InputProps) {
|
||||
const { setValue, error, validate = false, onChange, onBlur, ...passthroughProps } = props
|
||||
const [reportTimeoutHandle, setReportTimeoutHandle] = React.useState<number | null>(null)
|
||||
const [hasReportedValidity, setHasReportedValidity] = React.useState(false)
|
||||
const [wasJustBlurred, setWasJustBlurred] = React.useState(false)
|
||||
return (
|
||||
<input
|
||||
{...passthroughProps}
|
||||
onChange={event => {
|
||||
onChange?.(event)
|
||||
setValue(event.target.value)
|
||||
setWasJustBlurred(false)
|
||||
if (validate) {
|
||||
if (reportTimeoutHandle != null) {
|
||||
window.clearTimeout(reportTimeoutHandle)
|
||||
}
|
||||
const currentTarget = event.currentTarget
|
||||
if (error != null) {
|
||||
currentTarget.setCustomValidity('')
|
||||
currentTarget.setCustomValidity(currentTarget.checkValidity() ? '' : error)
|
||||
}
|
||||
if (hasReportedValidity) {
|
||||
if (currentTarget.checkValidity()) {
|
||||
setHasReportedValidity(false)
|
||||
}
|
||||
} else {
|
||||
setReportTimeoutHandle(
|
||||
window.setTimeout(() => {
|
||||
if (!currentTarget.reportValidity()) {
|
||||
setHasReportedValidity(true)
|
||||
}
|
||||
}, DEBOUNCE_MS)
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={event => {
|
||||
onBlur?.(event)
|
||||
if (validate) {
|
||||
if (wasJustBlurred) {
|
||||
setHasReportedValidity(false)
|
||||
} else {
|
||||
const currentTarget = event.currentTarget
|
||||
setTimeout(() => {
|
||||
currentTarget.reportValidity()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
setWasJustBlurred(true)
|
||||
getSelection()?.empty()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
@ -117,66 +117,60 @@ export default function ManageLabelsModal<
|
||||
}}
|
||||
>
|
||||
<div className="absolute bg-frame-selected backdrop-blur-3xl rounded-2xl h-full w-full" />
|
||||
<div className="relative flex flex-col rounded-2xl gap-2 p-2">
|
||||
<form
|
||||
className="relative flex flex-col gap-1 rounded-2xl gap-2 p-2"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
setLabels(oldLabels => [...oldLabels, backendModule.LabelName(query)])
|
||||
try {
|
||||
if (color != null) {
|
||||
await doCreateLabel(query, color)
|
||||
}
|
||||
} catch (error) {
|
||||
toastAndLog(null, error)
|
||||
setLabels(oldLabels =>
|
||||
oldLabels.filter(oldLabel => oldLabel !== query)
|
||||
)
|
||||
}
|
||||
unsetModal()
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-sm font-bold">Labels</h2>
|
||||
{/* Space reserved for other tabs. */}
|
||||
</div>
|
||||
<form
|
||||
className="flex gap-1"
|
||||
onSubmit={async event => {
|
||||
event.preventDefault()
|
||||
setLabels(oldLabels => [
|
||||
...oldLabels,
|
||||
backendModule.LabelName(query),
|
||||
])
|
||||
try {
|
||||
if (color != null) {
|
||||
await doCreateLabel(query, color)
|
||||
}
|
||||
} catch (error) {
|
||||
toastAndLog(null, error)
|
||||
setLabels(oldLabels =>
|
||||
oldLabels.filter(oldLabel => oldLabel !== query)
|
||||
)
|
||||
}
|
||||
unsetModal()
|
||||
}}
|
||||
<div
|
||||
className={`flex items-center grow rounded-full border border-black-a10 gap-2 px-1 ${
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
canSelectColor && color != null && color.lightness <= 50
|
||||
? 'text-tag-text placeholder-tag-text'
|
||||
: 'text-primary'
|
||||
}`}
|
||||
style={
|
||||
!canSelectColor || color == null
|
||||
? {}
|
||||
: {
|
||||
backgroundColor: backendModule.lChColorToCssColor(color),
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center grow rounded-full border border-black-a10 gap-2 px-1 ${
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
canSelectColor && color != null && color.lightness <= 50
|
||||
? 'text-tag-text placeholder-tag-text'
|
||||
: 'text-primary'
|
||||
}`}
|
||||
style={
|
||||
!canSelectColor || color == null
|
||||
? {}
|
||||
: {
|
||||
backgroundColor:
|
||||
backendModule.lChColorToCssColor(color),
|
||||
}
|
||||
}
|
||||
>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Type labels to search"
|
||||
className="grow bg-transparent leading-170 h-6 px-1 py-px"
|
||||
onChange={event => {
|
||||
setQuery(event.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canCreateNewLabel}
|
||||
className="text-tag-text bg-invite rounded-full px-2 py-1 disabled:opacity-30"
|
||||
>
|
||||
<div className="h-6 py-0.5">Create</div>
|
||||
</button>
|
||||
</form>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Type labels to search"
|
||||
className="grow bg-transparent leading-170 h-6 px-1 py-px"
|
||||
onChange={event => {
|
||||
setQuery(event.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canCreateNewLabel}
|
||||
className="text-tag-text bg-invite rounded-full px-2 py-1 disabled:opacity-30"
|
||||
>
|
||||
<div className="h-6 py-0.5">Create</div>
|
||||
</button>
|
||||
{canSelectColor && (
|
||||
<div className="flex gap-1">
|
||||
<div className="grow flex items-center gap-1">
|
||||
@ -201,7 +195,7 @@ export default function ManageLabelsModal<
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
@ -40,67 +40,63 @@ export default function NewDataConnectorModal(props: NewDataConnectorModalProps)
|
||||
|
||||
return (
|
||||
<Modal centered className="bg-dim">
|
||||
<div
|
||||
<form
|
||||
tabIndex={-1}
|
||||
className="relative rounded-2xl pointer-events-auto before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
|
||||
className="relative flex flex-col gap-2 rounded-2xl w-96 p-4 pt-2 pointer-events-auto before:inset-0 before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
|
||||
onKeyDown={event => {
|
||||
if (event.key !== 'Escape') {
|
||||
event.stopPropagation()
|
||||
}
|
||||
}}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onSubmit={event => {
|
||||
event.preventDefault()
|
||||
// Consider not calling `onSubmit()` here to make it harder to accidentally
|
||||
// delete an important asset.
|
||||
onSubmit()
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onSubmit={event => {
|
||||
event.preventDefault()
|
||||
// Consider not calling `onSubmit()` here to make it harder to accidentally
|
||||
// delete an important asset.
|
||||
onSubmit()
|
||||
}}
|
||||
className="relative flex flex-col rounded-2xl gap-2 w-96 px-4 py-2"
|
||||
>
|
||||
<h1 className="text-sm font-semibold">New Data Connector</h1>
|
||||
<div className="flex">
|
||||
<div className="w-12 h-6 py-1">Name</div>
|
||||
<input
|
||||
autoFocus
|
||||
placeholder="Enter the name of the data connector"
|
||||
className="grow bg-transparent border border-black-a10 rounded-full leading-170 h-6 px-4 py-px"
|
||||
onInput={event => {
|
||||
setName(event.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="w-12 h-6 py-1">Value</div>
|
||||
<input
|
||||
placeholder="Enter the value of the data connector"
|
||||
className="grow bg-transparent border border-black-a10 rounded-full leading-170 h-6 px-4 py-px"
|
||||
onInput={event => {
|
||||
setValue(event.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
disabled={!canSubmit}
|
||||
type="submit"
|
||||
className="hover:cursor-pointer inline-block text-white bg-invite rounded-full px-4 py-1 disabled:opacity-50 disabled:cursor-default"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:cursor-pointer inline-block bg-frame-selected rounded-full px-4 py-1"
|
||||
onClick={unsetModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<h1 className="relative text-sm font-semibold">New Data Connector</h1>
|
||||
<div className="relative flex">
|
||||
<div className="w-12 h-6 py-1">Name</div>
|
||||
<input
|
||||
autoFocus
|
||||
placeholder="Enter the name of the data connector"
|
||||
className="grow bg-transparent border border-black-a10 rounded-full leading-170 h-6 px-4 py-px"
|
||||
onInput={event => {
|
||||
setName(event.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex">
|
||||
<div className="w-12 h-6 py-1">Value</div>
|
||||
<input
|
||||
placeholder="Enter the value of the data connector"
|
||||
className="grow bg-transparent border border-black-a10 rounded-full leading-170 h-6 px-4 py-px"
|
||||
onInput={event => {
|
||||
setValue(event.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex gap-2">
|
||||
<button
|
||||
disabled={!canSubmit}
|
||||
type="submit"
|
||||
className="hover:cursor-pointer inline-block text-white bg-invite rounded-full px-4 py-1 disabled:opacity-50 disabled:cursor-default"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:cursor-pointer inline-block bg-frame-selected rounded-full px-4 py-1"
|
||||
onClick={unsetModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -47,86 +47,81 @@ export default function NewLabelModal(props: NewLabelModalProps) {
|
||||
|
||||
return (
|
||||
<Modal className="absolute bg-dim">
|
||||
<div
|
||||
<form
|
||||
data-testid="new-label-modal"
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
left: position.left + window.scrollX,
|
||||
top: position.top + window.scrollY,
|
||||
}}
|
||||
className="relative rounded-2xl pointer-events-auto w-80"
|
||||
className="relative flex flex-col gap-2 rounded-2xl pointer-events-auto w-80 p-4 pt-2 before:inset-0 before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
|
||||
onKeyDown={event => {
|
||||
if (event.key !== 'Escape') {
|
||||
event.stopPropagation()
|
||||
}
|
||||
}}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onSubmit={event => {
|
||||
event.preventDefault()
|
||||
// Consider not calling `onSubmit()` here to make it harder to accidentally
|
||||
// delete an important asset.
|
||||
onSubmit()
|
||||
}}
|
||||
>
|
||||
<div className="absolute rounded-2xl bg-frame-selected backdrop-blur-3xl w-full h-full" />
|
||||
<form
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onSubmit={event => {
|
||||
event.preventDefault()
|
||||
// Consider not calling `onSubmit()` here to make it harder to accidentally
|
||||
// delete an important asset.
|
||||
onSubmit()
|
||||
}}
|
||||
className="relative flex flex-col rounded-2xl gap-2 w-80 px-4 py-2"
|
||||
>
|
||||
<h1 className="text-sm font-semibold">New Label</h1>
|
||||
<label className="flex">
|
||||
<div className="w-12 h-6 py-1">Name</div>
|
||||
<input
|
||||
autoFocus
|
||||
placeholder="Enter the name of the label"
|
||||
className={`grow bg-transparent border border-black-a10 rounded-full leading-170 h-6 px-4 py-px ${
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
color != null && color.lightness <= 50
|
||||
? 'text-tag-text placeholder-frame-selected'
|
||||
: 'text-primary'
|
||||
}`}
|
||||
style={
|
||||
color == null
|
||||
? {}
|
||||
: {
|
||||
backgroundColor: backend.lChColorToCssColor(color),
|
||||
}
|
||||
}
|
||||
onInput={event => {
|
||||
setName(event.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
className="flex"
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
<h1 className="relative text-sm font-semibold">New Label</h1>
|
||||
<label className="relative flex">
|
||||
<div className="w-12 h-6 py-1">Name</div>
|
||||
<input
|
||||
autoFocus
|
||||
placeholder="Enter the name of the label"
|
||||
className={`grow bg-transparent border border-black-a10 rounded-full leading-170 h-6 px-4 py-px ${
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
color != null && color.lightness <= 50
|
||||
? 'text-tag-text placeholder-frame-selected'
|
||||
: 'text-primary'
|
||||
}`}
|
||||
style={
|
||||
color == null
|
||||
? {}
|
||||
: {
|
||||
backgroundColor: backend.lChColorToCssColor(color),
|
||||
}
|
||||
}
|
||||
onInput={event => {
|
||||
setName(event.currentTarget.value)
|
||||
}}
|
||||
>
|
||||
<div className="w-12 h-6 py-1">Color</div>
|
||||
<div className="grow flex items-center gap-1">
|
||||
<ColorPicker setColor={setColor} />
|
||||
</div>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
disabled={!canSubmit}
|
||||
type="submit"
|
||||
className="hover:cursor-pointer inline-block text-white bg-invite rounded-full px-4 py-1 disabled:opacity-50 disabled:cursor-default"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:cursor-pointer inline-block bg-frame-selected rounded-full px-4 py-1"
|
||||
onClick={unsetModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
className="relative flex"
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div className="w-12 h-6 py-1">Color</div>
|
||||
<div className="grow flex items-center gap-1">
|
||||
<ColorPicker setColor={setColor} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</label>
|
||||
<div className="relative flex gap-2">
|
||||
<button
|
||||
disabled={!canSubmit}
|
||||
type="submit"
|
||||
className="hover:cursor-pointer inline-block text-white bg-invite rounded-full px-4 py-1 disabled:opacity-50 disabled:cursor-default"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:cursor-pointer inline-block bg-frame-selected rounded-full px-4 py-1"
|
||||
onClick={unsetModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
/** @file Styled wrapper around SVG images. */
|
||||
import * as React from 'react'
|
||||
|
||||
// ===============
|
||||
// === SvgIcon ===
|
||||
// ===============
|
||||
|
||||
/** Props for a {@link SvgIcon}. */
|
||||
export interface SvgIconProps extends React.PropsWithChildren {}
|
||||
|
||||
/** A fixed-size container for a SVG image. */
|
||||
export default function SvgIcon(props: SvgIconProps) {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center justify-center absolute left-0 top-0 h-full w-10 text-gray-400">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -69,6 +69,9 @@ export const theme = {
|
||||
xl: '1.1875rem',
|
||||
'4xl': '2.375rem',
|
||||
},
|
||||
borderRadius: {
|
||||
'4xl': '2rem',
|
||||
},
|
||||
lineHeight: {
|
||||
'144.5': '144.5%',
|
||||
'170': '170%',
|
||||
|
@ -20,37 +20,37 @@ export const VALID_EMAIL = 'email@example.com'
|
||||
|
||||
/** Find an email input (if any) on the current page. */
|
||||
export function locateEmailInput(page: test.Locator | test.Page) {
|
||||
return page.getByLabel('E-Mail Address:')
|
||||
return page.getByLabel('Email')
|
||||
}
|
||||
|
||||
/** Find a password input (if any) on the current page. */
|
||||
export function locatePasswordInput(page: test.Locator | test.Page) {
|
||||
return page.getByLabel('Password:', { exact: true })
|
||||
return page.getByPlaceholder('Enter your password')
|
||||
}
|
||||
|
||||
/** Find a "confirm password" input (if any) on the current page. */
|
||||
export function locateConfirmPasswordInput(page: test.Locator | test.Page) {
|
||||
return page.getByLabel('Confirm Password:')
|
||||
return page.getByLabel('Confirm password')
|
||||
}
|
||||
|
||||
/** Find an "old password" input (if any) on the current page. */
|
||||
export function locateOldPasswordInput(page: test.Locator | test.Page) {
|
||||
return page.getByLabel('Old Password:')
|
||||
return page.getByLabel('Old password')
|
||||
}
|
||||
|
||||
/** Find a "new password" input (if any) on the current page. */
|
||||
export function locateNewPasswordInput(page: test.Locator | test.Page) {
|
||||
return page.getByLabel('New Password:', { exact: true })
|
||||
return page.getByPlaceholder('Enter your new password')
|
||||
}
|
||||
|
||||
/** Find a "confirm new password" input (if any) on the current page. */
|
||||
export function locateConfirmNewPasswordInput(page: test.Locator | test.Page) {
|
||||
return page.getByLabel('Confirm New Password:')
|
||||
return page.getByPlaceholder('Confirm your new password')
|
||||
}
|
||||
|
||||
/** Find a "username" input (if any) on the current page. */
|
||||
export function locateUsernameInput(page: test.Locator | test.Page) {
|
||||
return page.getByPlaceholder('Username')
|
||||
return page.getByPlaceholder('Enter your username')
|
||||
}
|
||||
|
||||
/** Find a "name" input for a "new label" modal (if any) on the current page. */
|
||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 9.0 KiB |