1
Fork 0

Add custom Modals

This commit is contained in:
viktorstrate 2021-06-18 14:48:22 +02:00
parent 51d2ac96c1
commit a60796e0ca
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
15 changed files with 202 additions and 127 deletions

View File

@ -176,7 +176,7 @@
"title": "Slet bruger" "title": "Slet bruger"
}, },
"password_reset": { "password_reset": {
"description": "Ændre adgangskode for <1></1>", "description": "Ændre adgangskode for <1>{{username}}</1>",
"form": { "form": {
"label": "Ny adgangskode", "label": "Ny adgangskode",
"placeholder": "adgangskode", "placeholder": "adgangskode",

View File

@ -176,7 +176,7 @@
"title": "Benutzer löschen" "title": "Benutzer löschen"
}, },
"password_reset": { "password_reset": {
"description": "Passwort von Benutzer <1></1> aktualisieren", "description": "Passwort von Benutzer <1>{{username}}</1> aktualisieren",
"form": { "form": {
"label": "Neues Passwort", "label": "Neues Passwort",
"placeholder": "Passwort", "placeholder": "Passwort",

View File

@ -176,7 +176,7 @@
"title": "Delete user" "title": "Delete user"
}, },
"password_reset": { "password_reset": {
"description": "Change password for <1></1>", "description": "Change password for <1>{{username}}</1>",
"form": { "form": {
"label": "New password", "label": "New password",
"placeholder": "password", "placeholder": "password",

View File

@ -176,7 +176,7 @@
"title": "Eliminar usuario" "title": "Eliminar usuario"
}, },
"password_reset": { "password_reset": {
"description": "Cambiar contraseña a <1></1>", "description": "Cambiar contraseña a <1>{{username}}</1>",
"form": { "form": {
"label": "Nueva contraseña", "label": "Nueva contraseña",
"placeholder": "contraseña", "placeholder": "contraseña",

View File

@ -176,7 +176,7 @@
"title": "Supprimer l'utilisateur" "title": "Supprimer l'utilisateur"
}, },
"password_reset": { "password_reset": {
"description": "Changer le mot de passe pour <1></1>", "description": "Changer le mot de passe pour <1>{{username}}</1>",
"form": { "form": {
"label": "Nouveau mot de passe", "label": "Nouveau mot de passe",
"placeholder": "mot de passe", "placeholder": "mot de passe",

View File

@ -176,7 +176,7 @@
"title": "Cancella utente" "title": "Cancella utente"
}, },
"password_reset": { "password_reset": {
"description": "Cambia password per <1></1>", "description": "Cambia password per <1>{{username}}</1>",
"form": { "form": {
"label": "Nuova password", "label": "Nuova password",
"placeholder": "password", "placeholder": "password",

View File

@ -176,7 +176,7 @@
"title": "Usuń użytkownika" "title": "Usuń użytkownika"
}, },
"password_reset": { "password_reset": {
"description": "Zmiana hasła dla <1></1>", "description": "Zmiana hasła dla <1>{{username}}</1>",
"form": { "form": {
"label": "Nowe hasło", "label": "Nowe hasło",
"placeholder": "hasło", "placeholder": "hasło",

View File

@ -176,7 +176,7 @@
"title": "Radera användare" "title": "Radera användare"
}, },
"password_reset": { "password_reset": {
"description": "Ändra lösenord för <1></1>", "description": "Ändra lösenord för <1>{{username}}</1>",
"form": { "form": {
"label": "Nytt lösenord", "label": "Nytt lösenord",
"placeholder": "lösenord", "placeholder": "lösenord",

View File

@ -4,7 +4,7 @@ import Layout from '../../components/layout/Layout'
import styled from 'styled-components' import styled from 'styled-components'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import SingleFaceGroup from './SingleFaceGroup/SingleFaceGroup' import SingleFaceGroup from './SingleFaceGroup/SingleFaceGroup'
import { Button, Icon, Input } from 'semantic-ui-react' import { Button, TextField } from '../../primitives/form/Input'
import FaceCircleImage from './FaceCircleImage' import FaceCircleImage from './FaceCircleImage'
import useScrollPagination from '../../hooks/useScrollPagination' import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../../components/PaginateLoader' import PaginateLoader from '../../components/PaginateLoader'
@ -91,7 +91,7 @@ export const FaceDetails = ({ group }: FaceDetailsProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [editLabel, setEditLabel] = useState(false) const [editLabel, setEditLabel] = useState(false)
const [inputValue, setInputValue] = useState(group.label ?? '') const [inputValue, setInputValue] = useState(group.label ?? '')
const inputRef = createRef<Input>() const inputRef = createRef<HTMLInputElement>()
const [setGroupLabel, { loading }] = useMutation< const [setGroupLabel, { loading }] = useMutation<
setGroupLabel, setGroupLabel,
@ -117,21 +117,11 @@ export const FaceDetails = ({ group }: FaceDetailsProps) => {
} }
}, [loading]) }, [loading])
const onKeyUp = (e: React.ChangeEvent<HTMLInputElement> & KeyboardEvent) => { const onKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key == 'Escape') { if (e.key == 'Escape') {
resetLabel() resetLabel()
return return
} }
if (e.key == 'Enter') {
setGroupLabel({
variables: {
groupID: group.id,
label: e.target.value == '' ? null : e.target.value,
},
})
return
}
} }
let label let label
@ -145,20 +135,29 @@ export const FaceDetails = ({ group }: FaceDetailsProps) => {
<FaceLabel> <FaceLabel>
{group.label ?? t('people_page.face_group.unlabeled', 'Unlabeled')} {group.label ?? t('people_page.face_group.unlabeled', 'Unlabeled')}
</FaceLabel> </FaceLabel>
<EditIcon name="pencil" /> {/* <EditIcon name="pencil" /> */}
</FaceDetailsButton> </FaceDetailsButton>
) )
} else { } else {
label = ( label = (
<FaceDetailsButton labeled={!!group.label}> <FaceDetailsButton labeled={!!group.label}>
<Input <TextField
className="w-[160px]"
loading={loading} loading={loading}
ref={inputRef} ref={inputRef}
size="mini" // size="mini"
placeholder={t('people_page.face_group.label_placeholder', 'Label')} placeholder={t('people_page.face_group.label_placeholder', 'Label')}
icon="arrow right" // icon="arrow right"
value={inputValue} value={inputValue}
onKeyUp={onKeyUp} action={() =>
setGroupLabel({
variables: {
groupID: group.id,
label: inputValue == '' ? null : inputValue,
},
})
}
onKeyDown={onKeyUp}
onChange={e => setInputValue(e.target.value)} onChange={e => setInputValue(e.target.value)}
onBlur={() => { onBlur={() => {
resetLabel() resetLabel()
@ -180,16 +179,16 @@ const FaceImagesCount = styled.span`
border-radius: 4px; border-radius: 4px;
` `
const EditIcon = styled(Icon)` // const EditIcon = styled(Icon)`
margin-left: 6px !important; // margin-left: 6px !important;
opacity: 0 !important; // opacity: 0 !important;
transition: opacity 100ms; // transition: opacity 100ms;
${FaceDetailsButton}:hover &, ${FaceDetailsButton}:focus-visible & { // ${FaceDetailsButton}:hover &, ${FaceDetailsButton}:focus-visible & {
opacity: 1 !important; // opacity: 1 !important;
} // }
` // `
type FaceGroupProps = { type FaceGroupProps = {
group: myFaces_myFaceGroups group: myFaces_myFaceGroups
@ -251,13 +250,11 @@ const PeopleGallery = () => {
return ( return (
<Layout title={t('title.people', 'People')}> <Layout title={t('title.people', 'People')}>
<Button <Button
loading={recognizeUnlabeledLoading}
disabled={recognizeUnlabeledLoading} disabled={recognizeUnlabeledLoading}
onClick={() => { onClick={() => {
recognizeUnlabeled() recognizeUnlabeled()
}} }}
> >
<Icon name="sync" />
{t( {t(
'people_page.recognize_unlabeled_faces_button', 'people_page.recognize_unlabeled_faces_button',
'Recognize unlabeled faces' 'Recognize unlabeled faces'

View File

@ -1,8 +1,9 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
import { Button, Form, Input, Modal, ModalProps } from 'semantic-ui-react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery' import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery'
import Modal from '../../../primitives/Modal'
import { TextField } from '../../../primitives/form/Input'
const changeUserPasswordMutation = gql` const changeUserPasswordMutation = gql`
mutation changeUserPassword($userId: ID!, $password: String!) { mutation changeUserPassword($userId: ID!, $password: String!) {
@ -12,7 +13,7 @@ const changeUserPasswordMutation = gql`
} }
` `
interface ChangePasswordModalProps extends ModalProps { interface ChangePasswordModalProps {
onClose(): void onClose(): void
open: boolean open: boolean
user: settingsUsersQuery_user user: settingsUsersQuery_user
@ -22,7 +23,6 @@ const ChangePasswordModal = ({
onClose, onClose,
user, user,
open, open,
...props
}: ChangePasswordModalProps) => { }: ChangePasswordModalProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [passwordInput, setPasswordInput] = useState('') const [passwordInput, setPasswordInput] = useState('')
@ -34,22 +34,42 @@ const ChangePasswordModal = ({
}) })
return ( return (
<Modal open={open} {...props}> <Modal
<Modal.Header> open={open}
{t('settings.users.password_reset.title', 'Change password')} onClose={onClose}
</Modal.Header> title={t('settings.users.password_reset.title', 'Change password')}
<Modal.Content> description={
<p>
<Trans t={t} i18nKey="settings.users.password_reset.description"> <Trans t={t} i18nKey="settings.users.password_reset.description">
Change password for <b>{user.username}</b> Change password for <b>{{ username: user.username }}</b>
</Trans> </Trans>
</p> }
<Form> actions={[
<Form.Field> {
<label> key: 'cancel',
{t('settings.users.password_reset.form.label', 'New password')} label: t('general.action.cancel', 'Cancel'),
</label> onClick: () => onClose && onClose(),
<Input },
{
key: 'change_password',
label: t(
'settings.users.password_reset.form.submit',
'Change password'
),
variant: 'positive',
onClick: () => {
changePassword({
variables: {
userId: user.id,
password: passwordInput,
},
})
},
},
]}
>
<div className="w-[360px]">
<TextField
label={t('settings.users.password_reset.form.label', 'New password')}
placeholder={t( placeholder={t(
'settings.users.password_reset.form.placeholder', 'settings.users.password_reset.form.placeholder',
'password' 'password'
@ -57,27 +77,7 @@ const ChangePasswordModal = ({
onChange={e => setPasswordInput(e.target.value)} onChange={e => setPasswordInput(e.target.value)}
type="password" type="password"
/> />
</Form.Field> </div>
</Form>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => onClose && onClose()}>
{t('general.action.cancel', 'Cancel')}
</Button>
<Button
positive
onClick={() => {
changePassword({
variables: {
userId: user.id,
password: passwordInput,
},
})
}}
>
{t('settings.users.password_reset.form.submit', 'Change password')}
</Button>
</Modal.Actions>
</Modal> </Modal>
) )
} }

View File

@ -8,6 +8,7 @@ import {
TableRow, TableRow,
TableBody, TableBody,
TableFooter, TableFooter,
TableScrollWrapper,
} from '../../../primitives/Table' } from '../../../primitives/Table'
import { useQuery, gql } from '@apollo/client' import { useQuery, gql } from '@apollo/client'
import UserRow from './UserRow' import UserRow from './UserRow'
@ -53,6 +54,7 @@ const UsersTable = () => {
<div> <div>
<SectionTitle>{t('settings.users.title', 'Users')}</SectionTitle> <SectionTitle>{t('settings.users.title', 'Users')}</SectionTitle>
<Loader active={loading} /> <Loader active={loading} />
<TableScrollWrapper>
<Table className="w-full max-w-6xl"> <Table className="w-full max-w-6xl">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@ -60,7 +62,10 @@ const UsersTable = () => {
{t('settings.users.table.column_names.username', 'Username')} {t('settings.users.table.column_names.username', 'Username')}
</TableHeaderCell> </TableHeaderCell>
<TableHeaderCell> <TableHeaderCell>
{t('settings.users.table.column_names.photo_path', 'Photo path')} {t(
'settings.users.table.column_names.photo_path',
'Photo path'
)}
</TableHeaderCell> </TableHeaderCell>
<TableHeaderCell> <TableHeaderCell>
{t( {t(
@ -102,6 +107,7 @@ const UsersTable = () => {
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
</TableScrollWrapper>
</div> </div>
) )
} }

View File

@ -202,7 +202,7 @@ const MorePopoverSectionPassword = ({
disabled={!activated} disabled={!activated}
type={passwordHidden ? 'password' : 'text'} type={passwordHidden ? 'password' : 'text'}
value={passwordInputValue} value={passwordInputValue}
className="mt-2" className="mt-2 w-full"
onKeyDown={event => { onKeyDown={event => {
if ( if (
event.shiftKey || event.shiftKey ||
@ -251,7 +251,7 @@ const MorePopover = ({ id, share, query }: MorePopoverProps) => {
<MorePopoverSectionPassword id={id} share={share} query={query} /> <MorePopoverSectionPassword id={id} share={share} query={query} />
<div className="px-4 py-2 border-t border-gray-200 mt-2 mb-2"> <div className="px-4 py-2 border-t border-gray-200 mt-2 mb-2">
<Checkbox label="Expiration date" /> <Checkbox label="Expiration date" />
<TextField className="mt-2" /> <TextField className="mt-2 w-full" />
</div> </div>
</ArrowPopoverPanel> </ArrowPopoverPanel>
</Popover.Panel> </Popover.Panel>

View File

@ -0,0 +1,68 @@
import React from 'react'
import { Dialog } from '@headlessui/react'
import { Button } from './form/Input'
type ModalAction = {
key: string
label: string
variant?: 'negative' | 'positive' | 'default'
onClick(event: React.MouseEvent<HTMLButtonElement>): void
}
type ModalProps = {
title: string
description: React.ReactNode
children: React.ReactNode
actions: ModalAction[]
open: boolean
onClose(): void
}
const Modal = ({
title,
description,
children,
actions,
open,
onClose,
}: ModalProps) => {
const actionElms = actions.map(x => (
<Button
key={x.key}
onClick={e => x.onClick(e)}
variant={x.variant}
className="bg-white"
>
{x.label}
</Button>
))
return (
<Dialog
open={open}
onClose={onClose}
className="fixed z-40 inset-0 overflow-y-auto"
>
<div className="flex items-center justify-center min-h-screen">
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />
<div className="fixed bg-white rounded max-w-[calc(100%-16px)] mx-auto rounded shadow-md border">
<div className="p-2">
<Dialog.Title className="text-xl mb-1">{title}</Dialog.Title>
<Dialog.Description className="text-sm mb-4">
{description}
</Dialog.Description>
{children}
</div>
<div className="bg-gray-50 p-2 flex gap-2 justify-end mt-4">
{actionElms}
</div>
</div>
</div>
</Dialog>
)
}
export default Modal

View File

@ -39,3 +39,7 @@ export const TableCell = styled.td.attrs({
export const TableHeaderCell = styled.th.attrs({ export const TableHeaderCell = styled.th.attrs({
className: 'bg-gray-50 py-2 px-2 align-top font-semibold' as string, className: 'bg-gray-50 py-2 px-2 align-top font-semibold' as string,
})`` })``
export const TableScrollWrapper = styled.div.attrs({
className: 'block overflow-x-auto whitespace-nowrap',
})``

View File

@ -77,7 +77,7 @@ export const TextField = forwardRef(
) )
} else if (action) { } else if (action) {
input = ( input = (
<div className="relative"> <div className="relative inline-block">
{input} {input}
<button <button
disabled={disabled} disabled={disabled}