Finish password protection ui
This commit is contained in:
parent
9cb236df41
commit
dc317d4489
|
@ -1,5 +1,11 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { useMutation, useQuery, gql, useLazyQuery } from '@apollo/client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
gql,
|
||||
useLazyQuery,
|
||||
DocumentNode,
|
||||
} from '@apollo/client'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Popover } from '@headlessui/react'
|
||||
|
@ -23,6 +29,7 @@ import {
|
|||
import {
|
||||
sidebarGetAlbumShares,
|
||||
sidebarGetAlbumSharesVariables,
|
||||
sidebarGetAlbumShares_album_shares,
|
||||
} from './__generated__/sidebarGetAlbumShares'
|
||||
import { authToken } from '../../helpers/authentication'
|
||||
import { SidebarSection, SidebarSectionTitle } from './SidebarComponents'
|
||||
|
@ -35,6 +42,10 @@ import { ReactComponent as AddIcon } from './icons/shareAddIcon.svg'
|
|||
import Checkbox from '../../primitives/form/Checkbox'
|
||||
import { TextField } from '../../primitives/form/Input'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
sidebarProtectShare,
|
||||
sidebarProtectShareVariables,
|
||||
} from './__generated__/sidebarProtectShare'
|
||||
|
||||
const SHARE_PHOTO_QUERY = gql`
|
||||
query sidebarGetPhotoShares($id: ID!) {
|
||||
|
@ -78,14 +89,14 @@ const ADD_ALBUM_SHARE_MUTATION = gql`
|
|||
}
|
||||
`
|
||||
|
||||
// const PROTECT_SHARE_MUTATION = gql`
|
||||
// mutation sidebarProtectShare($token: String!, $password: String) {
|
||||
// protectShareToken(token: $token, password: $password) {
|
||||
// token
|
||||
// hasPassword
|
||||
// }
|
||||
// }
|
||||
// `
|
||||
const PROTECT_SHARE_MUTATION = gql`
|
||||
mutation sidebarProtectShare($token: String!, $password: String) {
|
||||
protectShareToken(token: $token, password: $password) {
|
||||
token
|
||||
hasPassword
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const DELETE_SHARE_MUTATION = gql`
|
||||
mutation sidebareDeleteShare($token: String!) {
|
||||
|
@ -95,12 +106,6 @@ const DELETE_SHARE_MUTATION = gql`
|
|||
}
|
||||
`
|
||||
|
||||
// type ShareItemMoreDropdownProps = {
|
||||
// id: string
|
||||
// query: DocumentNode
|
||||
// share: sidbarGetAlbumShares_album_shares
|
||||
// }
|
||||
|
||||
// const ShareItemMoreDropdown = ({
|
||||
// id,
|
||||
// share,
|
||||
|
@ -255,7 +260,121 @@ const ArrowPopoverPanel = styled.div.attrs({
|
|||
}
|
||||
`
|
||||
|
||||
const MorePopover = () => {
|
||||
type MorePopoverSectionPasswordProps = {
|
||||
share: sidebarGetAlbumShares_album_shares
|
||||
query: DocumentNode
|
||||
id: string
|
||||
}
|
||||
|
||||
const MorePopoverSectionPassword = ({
|
||||
share,
|
||||
query,
|
||||
id,
|
||||
}: MorePopoverSectionPasswordProps) => {
|
||||
const [addingPassword, setAddingPassword] = useState(false)
|
||||
const activated = addingPassword || share.hasPassword
|
||||
|
||||
const [passwordInputValue, setPasswordInputValue] = useState(
|
||||
share.hasPassword ? '**********' : ''
|
||||
)
|
||||
const [passwordHidden, setPasswordHidden] = useState(share.hasPassword)
|
||||
|
||||
const [setPassword, { loading: setPasswordLoading }] = useMutation<
|
||||
sidebarProtectShare,
|
||||
sidebarProtectShareVariables
|
||||
>(PROTECT_SHARE_MUTATION, {
|
||||
refetchQueries: [{ query: query, variables: { id } }],
|
||||
onCompleted: data => {
|
||||
hidePassword(data.protectShareToken.hasPassword)
|
||||
},
|
||||
// refetchQueries: [{ query: query, variables: { id } }],
|
||||
variables: {
|
||||
token: share.token,
|
||||
},
|
||||
})
|
||||
|
||||
const hidePassword = (hide: boolean) => {
|
||||
if (hide) {
|
||||
setPasswordInputValue('**********')
|
||||
}
|
||||
|
||||
if (passwordHidden && !hide) {
|
||||
setPasswordInputValue('')
|
||||
}
|
||||
|
||||
setPasswordHidden(hide)
|
||||
}
|
||||
|
||||
const checkboxChange = () => {
|
||||
const enable = !activated
|
||||
setAddingPassword(enable)
|
||||
if (!enable) {
|
||||
setPassword({
|
||||
variables: {
|
||||
token: share.token,
|
||||
password: null,
|
||||
},
|
||||
})
|
||||
setPasswordInputValue('')
|
||||
}
|
||||
}
|
||||
|
||||
const updatePasswordAction = () => {
|
||||
if (!passwordHidden && passwordInputValue != '') {
|
||||
setPassword({
|
||||
variables: {
|
||||
token: share.token,
|
||||
password: passwordInputValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 py-2">
|
||||
<Checkbox
|
||||
label="Password protected"
|
||||
checked={activated}
|
||||
onChange={checkboxChange}
|
||||
/>
|
||||
<TextField
|
||||
disabled={!activated}
|
||||
type={passwordHidden ? 'password' : 'text'}
|
||||
value={passwordInputValue}
|
||||
sizeVariant="small"
|
||||
className="mt-2"
|
||||
onKeyDown={event => {
|
||||
if (
|
||||
event.shiftKey ||
|
||||
event.altKey ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
event.key == 'Enter' ||
|
||||
event.key == 'Tab' ||
|
||||
event.key == 'Escape'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
hidePassword(false)
|
||||
}}
|
||||
onChange={event => {
|
||||
setPasswordInputValue(event.target.value)
|
||||
}}
|
||||
action={updatePasswordAction}
|
||||
loading={setPasswordLoading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type MorePopoverProps = {
|
||||
id: string
|
||||
query: DocumentNode
|
||||
share: sidebarGetAlbumShares_album_shares
|
||||
}
|
||||
|
||||
const MorePopover = ({ id, share, query }: MorePopoverProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
|
@ -269,10 +388,7 @@ const MorePopover = () => {
|
|||
|
||||
<Popover.Panel>
|
||||
<ArrowPopoverPanel>
|
||||
<div className="px-4 py-2">
|
||||
<Checkbox label="Password protected" />
|
||||
<TextField sizeVariant="small" className="mt-2" />
|
||||
</div>
|
||||
<MorePopoverSectionPassword id={id} share={share} query={query} />
|
||||
<div className="px-4 py-2 border-t border-gray-200 mt-2 mb-2">
|
||||
<Checkbox label="Expiration date" />
|
||||
<TextField sizeVariant="small" className="mt-2" />
|
||||
|
@ -439,7 +555,7 @@ const SidebarShare = ({
|
|||
>
|
||||
<DeleteIcon />
|
||||
</button>
|
||||
<MorePopover />
|
||||
<MorePopover share={share} id={id} query={query} />
|
||||
|
||||
{/* <ShareItemMoreDropdown share={share} id={id} isPhoto={isPhoto} /> */}
|
||||
</td>
|
||||
|
|
|
@ -1,18 +1,32 @@
|
|||
import React, { forwardRef } from 'react'
|
||||
import classNames, { Argument as ClassNamesArg } from 'classnames'
|
||||
|
||||
import { ReactComponent as ActionArrowIcon } from './icons/textboxActionArrow.svg'
|
||||
import { ReactComponent as LoadingSpinnerIcon } from './icons/textboxLoadingSpinner.svg'
|
||||
|
||||
type TextFieldProps = {
|
||||
label?: string
|
||||
error?: string
|
||||
className?: ClassNamesArg
|
||||
sizeVariant?: 'default' | 'small'
|
||||
action?: () => void
|
||||
loading?: boolean
|
||||
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className'>
|
||||
|
||||
export const TextField = forwardRef(
|
||||
(
|
||||
{ label, error, className, sizeVariant, ...inputProps }: TextFieldProps,
|
||||
{
|
||||
label,
|
||||
error,
|
||||
className,
|
||||
sizeVariant,
|
||||
action,
|
||||
loading,
|
||||
...inputProps
|
||||
}: TextFieldProps,
|
||||
ref: React.ForwardedRef<HTMLInputElement>
|
||||
) => {
|
||||
const disabled = !!inputProps.disabled
|
||||
sizeVariant = sizeVariant ?? 'default'
|
||||
|
||||
let variant = 'bg-white border-gray-200 focus:border-blue-400'
|
||||
|
@ -20,8 +34,23 @@ export const TextField = forwardRef(
|
|||
variant =
|
||||
'bg-red-50 border-red-200 focus:border-red-400 focus:ring-red-100'
|
||||
|
||||
const input = (
|
||||
if (disabled) variant = 'bg-gray-100'
|
||||
|
||||
let keyUpEvent = undefined
|
||||
if (action) {
|
||||
keyUpEvent = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (inputProps.onKeyUp) inputProps.onKeyUp(event)
|
||||
|
||||
if (event.key == 'Enter') {
|
||||
event.preventDefault()
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let input = (
|
||||
<input
|
||||
onKeyUp={keyUpEvent}
|
||||
className={classNames(
|
||||
'block border rounded-md w-full focus:ring-2 focus:outline-none px-2',
|
||||
variant,
|
||||
|
@ -32,12 +61,46 @@ export const TextField = forwardRef(
|
|||
/>
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
input = (
|
||||
<div className="relative">
|
||||
{input}
|
||||
<LoadingSpinnerIcon
|
||||
aria-label="Loading"
|
||||
className="absolute right-[8px] top-[7px] animate-spin"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
} else if (action) {
|
||||
input = (
|
||||
<div className="relative">
|
||||
{input}
|
||||
<button
|
||||
disabled={disabled}
|
||||
aria-label="Submit"
|
||||
className={classNames(
|
||||
'absolute top-[1px] right-0 p-2',
|
||||
disabled ? 'text-gray-400 cursor-default' : 'text-gray-600'
|
||||
)}
|
||||
onClick={() => action()}
|
||||
>
|
||||
<ActionArrowIcon />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let errorElm = null
|
||||
if (error) errorElm = <div className="text-red-800">{error}</div>
|
||||
|
||||
const wrapperClasses = classNames(
|
||||
className,
|
||||
sizeVariant == 'small' && 'text-sm'
|
||||
)
|
||||
|
||||
if (label) {
|
||||
return (
|
||||
<label className={classNames('block', className)}>
|
||||
<label className={wrapperClasses}>
|
||||
<span className="block text-xs uppercase font-semibold mb-1">
|
||||
{label}
|
||||
</span>
|
||||
|
@ -48,9 +111,7 @@ export const TextField = forwardRef(
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(className, sizeVariant == 'small' && 'text-sm')}
|
||||
>
|
||||
<div className={wrapperClasses}>
|
||||
{input}
|
||||
{errorElm}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="1" y1="5.66666667" x2="10.3333333" y2="5.66666667"></line>
|
||||
<polyline id="Path" points="5.66666667 1 10.3333333 5.66666667 5.66666667 10.3333333"></polyline>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 490 B |
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none">
|
||||
<path d="M8,0 C12.418278,0 16,3.581722 16,8 C16,12.418278 12.418278,16 8,16 C3.581722,16 0,12.418278 0,8 C0,3.581722 3.581722,0 8,0 Z M8,2 C4.6862915,2 2,4.6862915 2,8 C2,11.3137085 4.6862915,14 8,14 C11.3137085,14 14,11.3137085 14,8 C14,4.6862915 11.3137085,2 8,2 Z" id="background" fill="#D8D8D8"></path>
|
||||
<path d="M8,0 C12.418278,0 16,3.581722 16,8 L14,8 L14,8 C14,4.6862915 11.3137085,2 8,2 L8,0 L8,0 Z" id="foreground" fill="#818181"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 683 B |
Loading…
Reference in New Issue