Add custom Modals
This commit is contained in:
parent
51d2ac96c1
commit
a60796e0ca
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
|
@ -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',
|
||||||
|
})``
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in New Issue