Work on settings user table

This commit is contained in:
viktorstrate
2021-06-17 17:49:51 +02:00
parent f80ef5b54c
commit 51d2ac96c1
12 changed files with 223 additions and 145 deletions

View File

@@ -32,7 +32,6 @@ const ScannerSection = () => {
)}
</InputLabelDescription>
<Button
className="my-2"
onClick={() => {
startScanner()
}}

View File

@@ -1,7 +1,9 @@
import { gql, useMutation } from '@apollo/client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Checkbox, Input, Table } from 'semantic-ui-react'
import Checkbox from '../../../primitives/form/Checkbox'
import { TextField, Button, ButtonGroup } from '../../../primitives/form/Input'
import { TableRow, TableCell } from '../../../primitives/Table'
const CREATE_USER_MUTATION = gql`
mutation createUser($username: String!, $admin: Boolean!) {
@@ -88,16 +90,16 @@ const AddUserRow = ({ setShow, show, onUserAdded }: AddUserRowProps) => {
}
return (
<Table.Row>
<Table.Cell>
<Input
<TableRow>
<TableCell>
<TextField
placeholder={t('login_page.field.username', 'Username')}
value={state.username}
onChange={e => updateInput(e, 'username')}
/>
</Table.Cell>
<Table.Cell>
<Input
</TableCell>
<TableCell>
<TextField
placeholder={t(
'login_page.initial_setup.field.photo_path.placeholder',
'/path/to/photos'
@@ -105,29 +107,28 @@ const AddUserRow = ({ setShow, show, onUserAdded }: AddUserRowProps) => {
value={state.rootPath}
onChange={e => updateInput(e, 'rootPath')}
/>
</Table.Cell>
<Table.Cell>
</TableCell>
<TableCell>
<Checkbox
toggle
label="Admin"
checked={state.admin}
onChange={(e, data) => {
onChange={e => {
setState({
...state,
admin: data.checked || false,
admin: e.target.checked || false,
})
}}
/>
</Table.Cell>
<Table.Cell>
<Button.Group>
<Button negative onClick={() => setShow(false)}>
</TableCell>
<TableCell>
<ButtonGroup>
<Button variant="negative" onClick={() => setShow(false)}>
{t('general.action.cancel', 'Cancel')}
</Button>
<Button
type="submit"
loading={loading}
disabled={loading}
positive
variant="positive"
onClick={() => {
createUser({
variables: {
@@ -139,9 +140,9 @@ const AddUserRow = ({ setShow, show, onUserAdded }: AddUserRowProps) => {
>
{t('settings.users.add_user.submit', 'Add user')}
</Button>
</Button.Group>
</Table.Cell>
</Table.Row>
</ButtonGroup>
</TableCell>
</TableRow>
)
}

View File

@@ -1,8 +1,11 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Checkbox, Input, Table } from 'semantic-ui-react'
import { EditRootPaths } from './EditUserRowRootPaths'
import { UserRowChildProps } from './UserRow'
import { TableRow, TableCell } from '../../../primitives/Table'
import { TextField } from '../../../primitives/form/Input'
import { Button, ButtonGroup } from '../../../primitives/form/Input'
import Checkbox from '../../../primitives/form/Checkbox'
const EditUserRow = ({
user,
@@ -24,34 +27,34 @@ const EditUserRow = ({
}
return (
<Table.Row>
<Table.Cell>
<Input
<TableRow>
<TableCell>
<TextField
style={{ width: '100%' }}
placeholder={user.username}
value={state.username}
onChange={e => updateInput(e, 'username')}
/>
</Table.Cell>
<Table.Cell>
</TableCell>
<TableCell>
<EditRootPaths user={user} />
</Table.Cell>
<Table.Cell>
</TableCell>
<TableCell>
<Checkbox
toggle
label="Admin"
checked={state.admin}
onChange={(_, data) => {
onChange={e => {
setState(state => ({
...state,
admin: data.checked || false,
admin: e.target.checked || false,
}))
}}
/>
</Table.Cell>
<Table.Cell>
<Button.Group>
</TableCell>
<TableCell>
<ButtonGroup>
<Button
negative
variant="negative"
onClick={() =>
setState(state => ({
...state,
@@ -62,9 +65,8 @@ const EditUserRow = ({
{t('general.action.cancel', 'Cancel')}
</Button>
<Button
loading={updateUserLoading}
disabled={updateUserLoading}
positive
variant="positive"
onClick={() =>
updateUser({
variables: {
@@ -77,9 +79,9 @@ const EditUserRow = ({
>
{t('general.action.save', 'Save')}
</Button>
</Button.Group>
</Table.Cell>
</Table.Row>
</ButtonGroup>
</TableCell>
</TableRow>
)
}

View File

@@ -1,7 +1,5 @@
import React, { useState } from 'react'
import { gql, useMutation } from '@apollo/client'
import { Button, Icon, Input } from 'semantic-ui-react'
import styled from 'styled-components'
import { USERS_QUERY } from './UsersTable'
import { useTranslation } from 'react-i18next'
import { USER_ADD_ROOT_PATH_MUTATION } from './AddUserRow'
@@ -14,6 +12,7 @@ import {
settingsUsersQuery_user_rootAlbums,
} from './__generated__/settingsUsersQuery'
import { userAddRootPath } from './__generated__/userAddRootPath'
import { Button, TextField } from '../../../primitives/form/Input'
const USER_REMOVE_ALBUM_PATH_MUTATION = gql`
mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) {
@@ -23,12 +22,6 @@ const USER_REMOVE_ALBUM_PATH_MUTATION = gql`
}
`
const RootPathListItem = styled.li`
display: flex;
justify-content: space-between;
align-items: center;
`
type EditRootPathProps = {
album: settingsUsersQuery_user_rootAlbums
user: settingsUsersQuery_user
@@ -48,10 +41,10 @@ const EditRootPath = ({ album, user }: EditRootPathProps) => {
})
return (
<RootPathListItem>
<li className="flex justify-between">
<span>{album.filePath}</span>
<Button
negative
variant="negative"
disabled={loading}
onClick={() =>
removeAlbumPath({
@@ -62,18 +55,12 @@ const EditRootPath = ({ album, user }: EditRootPathProps) => {
})
}
>
<Icon name="remove" />
{t('general.action.remove', 'Remove')}
</Button>
</RootPathListItem>
</li>
)
}
const NewRootPathInput = styled(Input)`
width: 100%;
margin-top: 24px;
`
type EditNewRootPathProps = {
userID: string
}
@@ -93,39 +80,33 @@ const EditNewRootPath = ({ userID }: EditNewRootPathProps) => {
)
return (
<li>
<NewRootPathInput
style={{ width: '100%' }}
<li className="flex gap-1 mt-2">
<TextField
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValue(e.target.value)
}
disabled={loading}
action={{
positive: true,
icon: 'add',
content: t('general.action.add', 'Add'),
onClick: () => {
setValue('')
addRootPath({
variables: {
id: userID,
rootPath: value,
},
})
},
}}
/>
<Button
variant="positive"
disabled={loading}
onClick={() => {
setValue('')
addRootPath({
variables: {
id: userID,
rootPath: value,
},
})
}}
>
{t('general.action.add', 'Add')}
</Button>
</li>
)
}
const RootPathList = styled.ul`
margin: 0;
padding: 0;
list-style: none;
`
type EditRootPathsProps = {
user: settingsUsersQuery_user
}
@@ -136,9 +117,9 @@ export const EditRootPaths = ({ user }: EditRootPathsProps) => {
))
return (
<RootPathList>
<ul>
{editRows}
<EditNewRootPath userID={user.id} />
</RootPathList>
</ul>
)
}

View File

@@ -1,12 +1,21 @@
import React, { useState } from 'react'
import { Table, Loader, Button, Icon } from 'semantic-ui-react'
import { Loader, Icon } from 'semantic-ui-react'
import {
Table,
TableHeader,
TableHeaderCell,
TableRow,
TableBody,
TableFooter,
} from '../../../primitives/Table'
import { useQuery, gql } from '@apollo/client'
import UserRow from './UserRow'
import AddUserRow from './AddUserRow'
import { SectionTitle } from '../SettingsPage'
import { useTranslation } from 'react-i18next'
import { settingsUsersQuery } from './__generated__/settingsUsersQuery'
import { Button } from '../../../primitives/form/Input'
export const USERS_QUERY = gql`
query settingsUsersQuery {
@@ -26,9 +35,8 @@ const UsersTable = () => {
const { t } = useTranslation()
const [showAddUser, setShowAddUser] = useState(false)
const { loading, error, data, refetch } = useQuery<settingsUsersQuery>(
USERS_QUERY
)
const { loading, error, data, refetch } =
useQuery<settingsUsersQuery>(USERS_QUERY)
if (error) {
return <div>{`Users table error: ${error.message}`}</div>
@@ -45,25 +53,28 @@ const UsersTable = () => {
<div>
<SectionTitle>{t('settings.users.title', 'Users')}</SectionTitle>
<Loader active={loading} />
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell>
<Table className="w-full max-w-6xl">
<TableHeader>
<TableRow>
<TableHeaderCell>
{t('settings.users.table.column_names.username', 'Username')}
</Table.HeaderCell>
<Table.HeaderCell>
</TableHeaderCell>
<TableHeaderCell>
{t('settings.users.table.column_names.photo_path', 'Photo path')}
</Table.HeaderCell>
<Table.HeaderCell>
{t('settings.users.table.column_names.admin', 'Admin')}
</Table.HeaderCell>
<Table.HeaderCell>
</TableHeaderCell>
<TableHeaderCell>
{t(
'settings.users.table.column_names.capabilities',
'Capabilities'
)}
</TableHeaderCell>
<TableHeaderCell className="w-0 whitespace-nowrap">
{t('settings.users.table.column_names.action', 'Action')}
</Table.HeaderCell>
</Table.Row>
</Table.Header>
</TableHeaderCell>
</TableRow>
</TableHeader>
<Table.Body>
<TableBody>
{userRows}
<AddUserRow
show={showAddUser}
@@ -73,23 +84,23 @@ const UsersTable = () => {
refetch()
}}
/>
</Table.Body>
</TableBody>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan="4">
<TableFooter>
<TableRow>
<TableHeaderCell colSpan={4} className="text-right">
<Button
positive
className="bg-white"
variant="positive"
disabled={showAddUser}
floated="right"
onClick={() => setShowAddUser(true)}
>
<Icon name="add" />
{t('settings.users.table.new_user', 'New user')}
</Button>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</TableHeaderCell>
</TableRow>
</TableFooter>
</Table>
</div>
)

View File

@@ -1,16 +1,12 @@
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Button, Icon, Table, Modal } from 'semantic-ui-react'
import styled from 'styled-components'
import { Icon, Modal } from 'semantic-ui-react'
import Checkbox from '../../../primitives/form/Checkbox'
import { Button } from '../../../primitives/form/Input'
import { TableCell, TableRow } from '../../../primitives/Table'
import ChangePasswordModal from './UserChangePassword'
import { UserRowChildProps } from './UserRow'
const PathList = styled.ul`
margin: 0;
padding: 0 0 0 12px;
list-style: none;
`
const ViewUserRow = ({
user,
// state,
@@ -25,22 +21,22 @@ const ViewUserRow = ({
}: UserRowChildProps) => {
const { t } = useTranslation()
const paths = (
<PathList>
<ul>
{user.rootAlbums.map(album => (
<li key={album.id}>{album.filePath}</li>
))}
</PathList>
</ul>
)
return (
<Table.Row>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{paths}</Table.Cell>
<Table.Cell>
{user.admin ? <Icon name="checkmark" size="large" /> : null}
</Table.Cell>
<Table.Cell>
<Button.Group>
<TableRow>
<TableCell>{user.username}</TableCell>
<TableCell>{paths}</TableCell>
<TableCell>
<Checkbox label="Admin" disabled checked={user.admin} />
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
onClick={() => {
setState(state => {
@@ -50,7 +46,6 @@ const ViewUserRow = ({
})
}}
>
<Icon name="edit" />
{t('settings.users.table.row.action.edit', 'Edit')}
</Button>
<Button
@@ -73,7 +68,7 @@ const ViewUserRow = ({
onClose={() => setChangePassword(false)}
/>
<Button
negative
variant="negative"
onClick={() => {
setConfirmDelete(true)
}}
@@ -102,7 +97,7 @@ const ViewUserRow = ({
{t('general.action.cancel', 'Cancel')}
</Button>
<Button
negative
variant="negative"
onClick={() => {
setConfirmDelete(false)
deleteUser({
@@ -120,9 +115,9 @@ const ViewUserRow = ({
</Button>
</Modal.Actions>
</Modal>
</Button.Group>
</Table.Cell>
</Table.Row>
</div>
</TableCell>
</TableRow>
)
}

View File

@@ -444,7 +444,7 @@ const SidebarShare = ({
<tr className="text-left border-gray-100 border-b border-t">
<td colSpan={2} className="pl-4 py-2">
<button
className="text-[#4ABF3C] font-bold uppercase text-xs"
className="text-green-500 font-bold uppercase text-xs"
disabled={loading}
onClick={() => {
shareItem({

View File

@@ -39,7 +39,7 @@ export const SidebarProvider = ({ children }: SidebarProviderProps) => {
pinned: boolean
}>({
content: null,
pinned: true,
pinned: false,
})
const updateSidebar = (content: React.ReactNode | null) => {

View File

@@ -0,0 +1,41 @@
import styled from 'styled-components'
export const Table = styled.table.attrs({
className: 'border border-separate rounded' as string,
})`
border-spacing: 0;
& td:not(:last-child),
& th:not(:last-child) {
border-right: 1px solid;
border-color: inherit;
}
& tr:first-child td {
border-top: 1px solid;
border-color: inherit;
}
& td {
border-bottom: 1px solid;
border-color: inherit;
}
`
export const TableHeader = styled.thead.attrs({
className: 'text-left',
})``
export const TableBody = styled.tbody.attrs({ className: '' })``
export const TableFooter = styled.tfoot.attrs({ className: '' })``
export const TableRow = styled.tr.attrs({ className: '' })``
export const TableCell = styled.td.attrs({
className: 'py-2 px-2 align-top',
})``
export const TableHeaderCell = styled.th.attrs({
className: 'bg-gray-50 py-2 px-2 align-top font-semibold' as string,
})``

View File

@@ -29,13 +29,29 @@ const CheckboxLabelWrapper = styled.label`
& input[type='checkbox']:checked + span::before {
background-color: #0072ff;
border-color: #109dff;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='7px' height='4px' viewBox='0 0 7 4' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EPath 5%3C/title%3E%3Cg id='Page-1' stroke='none' stroke-width='1.5' fill='none' fill-rule='evenodd'%3E%3Cpolyline id='Path-5' stroke='%23FFFFFF' points='1 1.93487039 2.77174339 3.70661378 6.47835718 2.27373675e-13'%3E%3C/polyline%3E%3C/g%3E%3C/svg%3E");
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='7px' height='4px' viewBox='0 0 7 4' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EPath 5%3C/title%3E%3Cg stroke='none' stroke-width='1.5' fill='none' fill-rule='evenodd'%3E%3Cpolyline stroke='white' points='1 1.93487039 2.77174339 3.70661378 6.47835718 2.27373675e-13'%3E%3C/polyline%3E%3C/g%3E%3C/svg%3E");
}
& input[type='checkbox']:focus + span::before {
outline: 2px solid #86c6ff;
border-color: #109dff;
}
& input[type='checkbox']:disabled + span::before {
border-color: #ccc;
background-color: #fefefe;
cursor: initial;
}
& input[type='checkbox']:disabled:checked + span::before {
background-color: #eee;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='7px' height='4px' viewBox='0 0 7 4' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EPath 5%3C/title%3E%3Cg stroke='none' stroke-width='1.5' fill='none' fill-rule='evenodd'%3E%3Cpolyline stroke='%23444444' points='1 1.93487039 2.77174339 3.70661378 6.47835718 2.27373675e-13'%3E%3C/polyline%3E%3C/g%3E%3C/svg%3E");
}
& input[type='checkbox']:disabled + span {
color: #555;
cursor: initial;
}
`
type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement> & {

View File

@@ -3,6 +3,7 @@ import classNames, { Argument as ClassNamesArg } from 'classnames'
import { ReactComponent as ActionArrowIcon } from './icons/textboxActionArrow.svg'
import { ReactComponent as LoadingSpinnerIcon } from './icons/textboxLoadingSpinner.svg'
import styled from 'styled-components'
type TextFieldProps = {
label?: string
@@ -121,15 +122,26 @@ export const TextField = forwardRef(
}
)
const buttonStyles =
'bg-gray-50 px-6 py-0.5 rounded border border-gray-200 focus:outline-none focus:border-blue-300 text-[#222] hover:bg-gray-100'
type ButtonProps = {
variant?: 'negative' | 'positive' | 'default'
}
const buttonStyles = ({ variant }: ButtonProps) =>
classNames(
'bg-gray-50 px-6 py-0.5 rounded border border-gray-200 focus:outline-none focus:border-blue-300 text-[#222] hover:bg-gray-100 whitespace-nowrap',
variant == 'negative' &&
'text-red-600 hover:bg-red-600 hover:border-red-700 hover:text-white transition-colors focus:border-red-600 focus:hover:border-red-700',
variant == 'positive' &&
'text-green-600 hover:bg-green-600 hover:border-green-700 hover:text-white transition-colors focus:border-green-600 focus:hover:border-green-700'
)
export const Submit = ({
className,
variant,
...props
}: React.InputHTMLAttributes<HTMLInputElement>) => (
}: ButtonProps & React.ButtonHTMLAttributes<HTMLInputElement>) => (
<input
className={classNames(buttonStyles, className)}
className={classNames(buttonStyles({ variant }), className)}
type="submit"
{...props}
/>
@@ -137,10 +149,16 @@ export const Submit = ({
export const Button = ({
children,
variant,
className,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button className={classNames(buttonStyles, className)} {...props}>
}: ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={classNames(buttonStyles({ variant }), className)}
{...props}
>
{children}
</button>
)
export const ButtonGroup = styled.div.attrs({ className: 'flex gap-1' })``

View File

@@ -10,6 +10,20 @@ module.exports = {
boxShadow: {
separator: '0 0 4px 0 rgba(0, 0, 0, 0.1)',
},
colors: {
green: {
50: '#f0ffee',
100: '#dcffd8',
200: '#b6fdb3',
300: '#7cf587',
400: '#56e263',
500: '#4abf3c',
600: '#30a23e',
700: '#168332',
800: '#006624',
900: '#00541d',
},
},
},
},
variants: {