feat(userlist): add sortable columns to User List (#1615)
This commit is contained in:
@@ -36,7 +36,7 @@ describe('User List', () => {
|
|||||||
cy.get('#email').type(testUser.emailAddress);
|
cy.get('#email').type(testUser.emailAddress);
|
||||||
cy.get('#password').type(testUser.password);
|
cy.get('#password').type(testUser.password);
|
||||||
|
|
||||||
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
|
cy.intercept('/api/v1/user*').as('user');
|
||||||
|
|
||||||
cy.get('[data-testid=modal-ok-button]').click();
|
cy.get('[data-testid=modal-ok-button]').click();
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ describe('User List', () => {
|
|||||||
|
|
||||||
cy.get('[data-testid=modal-title]').should('contain', `Delete User`);
|
cy.get('[data-testid=modal-title]').should('contain', `Delete User`);
|
||||||
|
|
||||||
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
|
cy.intercept('/api/v1/user*').as('user');
|
||||||
|
|
||||||
cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();
|
cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();
|
||||||
|
|
||||||
@@ -67,4 +67,37 @@ describe('User List', () => {
|
|||||||
.contains(testUser.emailAddress)
|
.contains(testUser.emailAddress)
|
||||||
.should('not.exist');
|
.should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sorts by column headers and updates request params and row order', () => {
|
||||||
|
cy.intercept('GET', '/api/v1/user?*').as('userListFetch');
|
||||||
|
|
||||||
|
cy.visit('/users');
|
||||||
|
cy.wait('@userListFetch');
|
||||||
|
|
||||||
|
cy.get('[data-testid=column-header-displayname]').click();
|
||||||
|
cy.wait('@userListFetch').then((interception) => {
|
||||||
|
const url = interception.request.url;
|
||||||
|
expect(url).to.include('sort=displayname');
|
||||||
|
expect(url).to.include('sortDirection=asc');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(
|
||||||
|
'[data-testid=user-list-row] [data-testid=user-list-username-link]'
|
||||||
|
).then(($links) => {
|
||||||
|
const displayNames = $links
|
||||||
|
.toArray()
|
||||||
|
.map((el) => (el as HTMLElement).innerText.trim().toLowerCase());
|
||||||
|
const sortedAsc = [...displayNames].sort((a, b) => a.localeCompare(b));
|
||||||
|
expect(displayNames).to.deep.equal(sortedAsc);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[data-testid=column-header-created]').click();
|
||||||
|
cy.wait('@userListFetch').then((interception) => {
|
||||||
|
const url = interception.request.url;
|
||||||
|
expect(url).to.include('sort=created');
|
||||||
|
expect(url).to.include('sortDirection=desc');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[data-testid=user-list-row]').should('have.length.greaterThan', 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4117,8 +4117,16 @@ paths:
|
|||||||
name: sort
|
name: sort
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [created, updated, requests, displayname]
|
enum: [created, updated, requests, displayname, usertype, role]
|
||||||
default: created
|
default: created
|
||||||
|
- in: query
|
||||||
|
name: sortDirection
|
||||||
|
description: |
|
||||||
|
Sort direction. When omitted, the server chooses the direction per sort
|
||||||
|
field (e.g. displayname defaults to asc, requests/updated to desc).
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [asc, desc]
|
||||||
- in: query
|
- in: query
|
||||||
name: q
|
name: q
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -45,6 +45,35 @@ router.get('/', async (req, res, next) => {
|
|||||||
: Math.max(10, includeIds.length);
|
: Math.max(10, includeIds.length);
|
||||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||||
const q = req.query.q ? req.query.q.toString().toLowerCase() : '';
|
const q = req.query.q ? req.query.q.toString().toLowerCase() : '';
|
||||||
|
const sortParam = req.query.sort ? req.query.sort.toString() : undefined;
|
||||||
|
const sortDirectionQuery = req.query.sortDirection
|
||||||
|
? req.query.sortDirection.toString().toLowerCase()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let sortDirection: 'ASC' | 'DESC';
|
||||||
|
if (sortDirectionQuery === 'asc') {
|
||||||
|
sortDirection = 'ASC';
|
||||||
|
} else if (sortDirectionQuery === 'desc') {
|
||||||
|
sortDirection = 'DESC';
|
||||||
|
} else {
|
||||||
|
switch (sortParam) {
|
||||||
|
case 'displayname':
|
||||||
|
sortDirection = 'ASC';
|
||||||
|
break;
|
||||||
|
case 'requests':
|
||||||
|
case 'updated':
|
||||||
|
sortDirection = 'DESC';
|
||||||
|
break;
|
||||||
|
case 'created':
|
||||||
|
case 'usertype':
|
||||||
|
case 'role':
|
||||||
|
case undefined:
|
||||||
|
default:
|
||||||
|
sortDirection = 'ASC';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let query = getRepository(User).createQueryBuilder('user');
|
let query = getRepository(User).createQueryBuilder('user');
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
@@ -58,29 +87,32 @@ router.get('/', async (req, res, next) => {
|
|||||||
query.andWhereInIds(includeIds);
|
query.andWhereInIds(includeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (req.query.sort) {
|
switch (sortParam) {
|
||||||
|
case 'created':
|
||||||
|
query = query.orderBy('user.createdAt', sortDirection);
|
||||||
|
break;
|
||||||
case 'updated':
|
case 'updated':
|
||||||
query = query.orderBy('user.updatedAt', 'DESC');
|
query = query.orderBy('user.updatedAt', sortDirection);
|
||||||
break;
|
break;
|
||||||
case 'displayname':
|
case 'displayname':
|
||||||
query = query
|
query = query
|
||||||
.addSelect(
|
.addSelect(
|
||||||
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
||||||
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
||||||
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
||||||
"user"."email"
|
"user"."email"
|
||||||
|
ELSE
|
||||||
|
LOWER(user.jellyfinUsername)
|
||||||
|
END)
|
||||||
ELSE
|
ELSE
|
||||||
LOWER(user.jellyfinUsername)
|
LOWER(user.plexUsername)
|
||||||
END)
|
END)
|
||||||
ELSE
|
ELSE
|
||||||
LOWER(user.jellyfinUsername)
|
LOWER(user.username)
|
||||||
END)
|
END`,
|
||||||
ELSE
|
|
||||||
LOWER(user.username)
|
|
||||||
END`,
|
|
||||||
'displayname_sort_key'
|
'displayname_sort_key'
|
||||||
)
|
)
|
||||||
.orderBy('displayname_sort_key', 'ASC');
|
.orderBy('displayname_sort_key', sortDirection);
|
||||||
break;
|
break;
|
||||||
case 'requests':
|
case 'requests':
|
||||||
query = query
|
query = query
|
||||||
@@ -90,10 +122,25 @@ router.get('/', async (req, res, next) => {
|
|||||||
.from(MediaRequest, 'request')
|
.from(MediaRequest, 'request')
|
||||||
.where('request.requestedBy.id = user.id');
|
.where('request.requestedBy.id = user.id');
|
||||||
}, 'request_count')
|
}, 'request_count')
|
||||||
.orderBy('request_count', 'DESC');
|
.orderBy('request_count', sortDirection);
|
||||||
|
break;
|
||||||
|
case 'usertype':
|
||||||
|
query = query.orderBy('user.userType', sortDirection);
|
||||||
|
break;
|
||||||
|
case 'role':
|
||||||
|
query = query
|
||||||
|
.addSelect(
|
||||||
|
`CASE
|
||||||
|
WHEN user.id = 1 THEN 0
|
||||||
|
WHEN (user.permissions & ${Permission.ADMIN}) != 0 THEN 1
|
||||||
|
ELSE 2
|
||||||
|
END`,
|
||||||
|
'role_sort_key'
|
||||||
|
)
|
||||||
|
.orderBy('role_sort_key', sortDirection);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
query = query.orderBy('user.id', 'ASC');
|
query = query.orderBy('user.id', sortDirection);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ import defineMessages from '@app/utils/defineMessages';
|
|||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import {
|
import {
|
||||||
BarsArrowDownIcon,
|
BarsArrowDownIcon,
|
||||||
|
BarsArrowUpIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
InboxArrowDownIcon,
|
InboxArrowDownIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
@@ -78,14 +81,28 @@ const messages = defineMessages('components.UserList', {
|
|||||||
autogeneratepasswordTip: 'Email a server-generated password to the user',
|
autogeneratepasswordTip: 'Email a server-generated password to the user',
|
||||||
validationUsername: 'You must provide an username',
|
validationUsername: 'You must provide an username',
|
||||||
validationEmail: 'Email required',
|
validationEmail: 'Email required',
|
||||||
sortCreated: 'Join Date',
|
sortBy: 'Sort by {field}',
|
||||||
sortDisplayName: 'Display Name',
|
sortByUser: 'Sort by username',
|
||||||
sortRequests: 'Request Count',
|
sortByRequests: 'Sort by number of requests',
|
||||||
|
sortByType: 'Sort by account type',
|
||||||
|
sortByRole: 'Sort by user role',
|
||||||
|
sortByJoined: 'Sort by join date',
|
||||||
|
toggleSortDirection: 'Click again to sort {direction}',
|
||||||
|
toggleSortDirectionAria: 'Toggle sort direction',
|
||||||
|
ascending: 'ascending',
|
||||||
|
descending: 'descending',
|
||||||
localLoginDisabled:
|
localLoginDisabled:
|
||||||
'The <strong>Enable Local Sign-In</strong> setting is currently disabled.',
|
'The <strong>Enable Local Sign-In</strong> setting is currently disabled.',
|
||||||
});
|
});
|
||||||
|
|
||||||
type Sort = 'created' | 'updated' | 'requests' | 'displayname';
|
type Sort =
|
||||||
|
| 'created'
|
||||||
|
| 'updated'
|
||||||
|
| 'requests'
|
||||||
|
| 'displayname'
|
||||||
|
| 'usertype'
|
||||||
|
| 'role';
|
||||||
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
const UserList = () => {
|
const UserList = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -93,13 +110,20 @@ const UserList = () => {
|
|||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
|
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
|
||||||
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
|
const [currentSort, setCurrentSort] = useState<Sort>('created');
|
||||||
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
||||||
|
|
||||||
const page = router.query.page ? Number(router.query.page) : 1;
|
const page = router.query.page ? Number(router.query.page) : 1;
|
||||||
const pageIndex = page - 1;
|
const pageIndex = page - 1;
|
||||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
||||||
|
|
||||||
|
const defaultSortDirection = (sortKey: Sort): SortDirection =>
|
||||||
|
sortKey === 'requests' || sortKey === 'updated' ? 'desc' : 'asc';
|
||||||
|
|
||||||
|
const [sortDirection, setSortDirection] = useState<SortDirection>(() =>
|
||||||
|
defaultSortDirection('created')
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
error,
|
error,
|
||||||
@@ -107,9 +131,19 @@ const UserList = () => {
|
|||||||
} = useSWR<UserResultsResponse>(
|
} = useSWR<UserResultsResponse>(
|
||||||
`/api/v1/user?take=${currentPageSize}&skip=${
|
`/api/v1/user?take=${currentPageSize}&skip=${
|
||||||
pageIndex * currentPageSize
|
pageIndex * currentPageSize
|
||||||
}&sort=${currentSort}`
|
}&sort=${currentSort}&sortDirection=${sortDirection}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSortChange = (sortKey: Sort) => {
|
||||||
|
if (currentSort === sortKey) {
|
||||||
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||||
|
} else {
|
||||||
|
setCurrentSort(sortKey);
|
||||||
|
setSortDirection(defaultSortDirection(sortKey));
|
||||||
|
}
|
||||||
|
updateQueryParams('page', '1');
|
||||||
|
};
|
||||||
|
|
||||||
const [isDeleting, setDeleting] = useState(false);
|
const [isDeleting, setDeleting] = useState(false);
|
||||||
const [showImportModal, setShowImportModal] = useState(false);
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
const [deleteModal, setDeleteModal] = useState<{
|
const [deleteModal, setDeleteModal] = useState<{
|
||||||
@@ -134,6 +168,9 @@ const UserList = () => {
|
|||||||
|
|
||||||
setCurrentSort(filterSettings.currentSort);
|
setCurrentSort(filterSettings.currentSort);
|
||||||
setCurrentPageSize(filterSettings.currentPageSize);
|
setCurrentPageSize(filterSettings.currentPageSize);
|
||||||
|
if (filterSettings.sortDirection) {
|
||||||
|
setSortDirection(filterSettings.sortDirection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -143,9 +180,74 @@ const UserList = () => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
currentSort,
|
currentSort,
|
||||||
currentPageSize,
|
currentPageSize,
|
||||||
|
sortDirection,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, [currentSort, currentPageSize]);
|
}, [currentSort, currentPageSize, sortDirection]);
|
||||||
|
|
||||||
|
const SortableColumnHeader = ({
|
||||||
|
sortKey,
|
||||||
|
currentSort,
|
||||||
|
sortDirection,
|
||||||
|
onSortChange,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
sortKey: Sort;
|
||||||
|
currentSort: Sort;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
onSortChange: (sortKey: Sort) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const getTooltip = () => {
|
||||||
|
if (currentSort === sortKey) {
|
||||||
|
return intl.formatMessage(messages.toggleSortDirection, {
|
||||||
|
direction:
|
||||||
|
sortDirection === 'asc'
|
||||||
|
? intl.formatMessage(messages.descending)
|
||||||
|
: intl.formatMessage(messages.ascending),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sortKey) {
|
||||||
|
case 'displayname':
|
||||||
|
return intl.formatMessage(messages.sortByUser);
|
||||||
|
case 'requests':
|
||||||
|
return intl.formatMessage(messages.sortByRequests);
|
||||||
|
case 'usertype':
|
||||||
|
return intl.formatMessage(messages.sortByType);
|
||||||
|
case 'role':
|
||||||
|
return intl.formatMessage(messages.sortByRole);
|
||||||
|
case 'created':
|
||||||
|
return intl.formatMessage(messages.sortByJoined);
|
||||||
|
default:
|
||||||
|
return intl.formatMessage(messages.sortBy, { field: sortKey });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.TH
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => onSortChange(sortKey)}
|
||||||
|
data-testid={`column-header-${sortKey}`}
|
||||||
|
title={getTooltip()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span>{children}</span>
|
||||||
|
{currentSort === sortKey && (
|
||||||
|
<span className="ml-1">
|
||||||
|
{sortDirection === 'asc' ? (
|
||||||
|
<ChevronUpIcon className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Table.TH>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const isUserPermsEditable = (userId: number) =>
|
const isUserPermsEditable = (userId: number) =>
|
||||||
userId !== 1 && userId !== currentUser?.id;
|
userId !== 1 && userId !== currentUser?.id;
|
||||||
@@ -546,28 +648,47 @@ const UserList = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-2 flex flex-grow lg:mb-0 lg:flex-grow-0">
|
<div className="mb-2 flex flex-grow lg:mb-0 lg:flex-grow-0">
|
||||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
<button
|
||||||
<BarsArrowDownIcon className="h-6 w-6" />
|
type="button"
|
||||||
</span>
|
className="inline-flex cursor-pointer items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100"
|
||||||
|
onClick={() => {
|
||||||
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||||
|
updateQueryParams('page', '1');
|
||||||
|
}}
|
||||||
|
aria-label={intl.formatMessage(messages.toggleSortDirectionAria)}
|
||||||
|
title={
|
||||||
|
sortDirection === 'asc'
|
||||||
|
? intl.formatMessage(messages.descending)
|
||||||
|
: intl.formatMessage(messages.ascending)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{sortDirection === 'asc' ? (
|
||||||
|
<BarsArrowUpIcon className="h-6 w-6" />
|
||||||
|
) : (
|
||||||
|
<BarsArrowDownIcon className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
<select
|
<select
|
||||||
id="sort"
|
id="sort"
|
||||||
name="sort"
|
name="sort"
|
||||||
onChange={(e) => {
|
onChange={(e) => handleSortChange(e.target.value as Sort)}
|
||||||
setCurrentSort(e.target.value as Sort);
|
|
||||||
router.push(router.pathname);
|
|
||||||
}}
|
|
||||||
value={currentSort}
|
value={currentSort}
|
||||||
className="rounded-r-only"
|
className="rounded-r-only"
|
||||||
>
|
>
|
||||||
<option value="created">
|
<option value="displayname">
|
||||||
{intl.formatMessage(messages.sortCreated)}
|
{intl.formatMessage(messages.username)}
|
||||||
</option>
|
</option>
|
||||||
<option value="requests">
|
<option value="requests">
|
||||||
{intl.formatMessage(messages.sortRequests)}
|
{intl.formatMessage(messages.totalrequests)}
|
||||||
</option>
|
</option>
|
||||||
<option value="displayname">
|
<option value="usertype">
|
||||||
{intl.formatMessage(messages.sortDisplayName)}
|
{intl.formatMessage(messages.accounttype)}
|
||||||
|
</option>
|
||||||
|
<option value="role">{intl.formatMessage(messages.role)}</option>
|
||||||
|
<option value="created">
|
||||||
|
{intl.formatMessage(messages.created)}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -589,11 +710,46 @@ const UserList = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Table.TH>
|
</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.user)}</Table.TH>
|
<SortableColumnHeader
|
||||||
<Table.TH>{intl.formatMessage(messages.totalrequests)}</Table.TH>
|
sortKey="displayname"
|
||||||
<Table.TH>{intl.formatMessage(messages.accounttype)}</Table.TH>
|
currentSort={currentSort}
|
||||||
<Table.TH>{intl.formatMessage(messages.role)}</Table.TH>
|
sortDirection={sortDirection}
|
||||||
<Table.TH>{intl.formatMessage(messages.created)}</Table.TH>
|
onSortChange={handleSortChange}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.user)}
|
||||||
|
</SortableColumnHeader>
|
||||||
|
<SortableColumnHeader
|
||||||
|
sortKey="requests"
|
||||||
|
currentSort={currentSort}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortChange={handleSortChange}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.totalrequests)}
|
||||||
|
</SortableColumnHeader>
|
||||||
|
<SortableColumnHeader
|
||||||
|
sortKey="usertype"
|
||||||
|
currentSort={currentSort}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortChange={handleSortChange}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.accounttype)}
|
||||||
|
</SortableColumnHeader>
|
||||||
|
<SortableColumnHeader
|
||||||
|
sortKey="role"
|
||||||
|
currentSort={currentSort}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortChange={handleSortChange}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.role)}
|
||||||
|
</SortableColumnHeader>
|
||||||
|
<SortableColumnHeader
|
||||||
|
sortKey="created"
|
||||||
|
currentSort={currentSort}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortChange={handleSortChange}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.created)}
|
||||||
|
</SortableColumnHeader>
|
||||||
<Table.TH className="text-right">
|
<Table.TH className="text-right">
|
||||||
{(data.results ?? []).length > 1 && (
|
{(data.results ?? []).length > 1 && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1348,6 +1348,7 @@
|
|||||||
"components.TvDetails.watchtrailer": "Watch Trailer",
|
"components.TvDetails.watchtrailer": "Watch Trailer",
|
||||||
"components.UserList.accounttype": "Type",
|
"components.UserList.accounttype": "Type",
|
||||||
"components.UserList.admin": "Admin",
|
"components.UserList.admin": "Admin",
|
||||||
|
"components.UserList.ascending": "ascending",
|
||||||
"components.UserList.autogeneratepassword": "Automatically Generate Password",
|
"components.UserList.autogeneratepassword": "Automatically Generate Password",
|
||||||
"components.UserList.autogeneratepasswordTip": "Email a server-generated password to the user",
|
"components.UserList.autogeneratepasswordTip": "Email a server-generated password to the user",
|
||||||
"components.UserList.bulkedit": "Bulk Edit",
|
"components.UserList.bulkedit": "Bulk Edit",
|
||||||
@@ -1357,6 +1358,7 @@
|
|||||||
"components.UserList.creating": "Creating…",
|
"components.UserList.creating": "Creating…",
|
||||||
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All of their request data will be permanently removed.",
|
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All of their request data will be permanently removed.",
|
||||||
"components.UserList.deleteuser": "Delete User",
|
"components.UserList.deleteuser": "Delete User",
|
||||||
|
"components.UserList.descending": "descending",
|
||||||
"components.UserList.edituser": "Edit User Permissions",
|
"components.UserList.edituser": "Edit User Permissions",
|
||||||
"components.UserList.email": "Email Address",
|
"components.UserList.email": "Email Address",
|
||||||
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!",
|
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!",
|
||||||
@@ -1378,9 +1380,14 @@
|
|||||||
"components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.",
|
"components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.",
|
||||||
"components.UserList.plexuser": "Plex User",
|
"components.UserList.plexuser": "Plex User",
|
||||||
"components.UserList.role": "Role",
|
"components.UserList.role": "Role",
|
||||||
"components.UserList.sortCreated": "Join Date",
|
"components.UserList.sortBy": "Sort by {field}",
|
||||||
"components.UserList.sortDisplayName": "Display Name",
|
"components.UserList.sortByJoined": "Sort by join date",
|
||||||
"components.UserList.sortRequests": "Request Count",
|
"components.UserList.sortByRequests": "Sort by number of requests",
|
||||||
|
"components.UserList.sortByRole": "Sort by user role",
|
||||||
|
"components.UserList.sortByType": "Sort by account type",
|
||||||
|
"components.UserList.sortByUser": "Sort by username",
|
||||||
|
"components.UserList.toggleSortDirection": "Click again to sort {direction}",
|
||||||
|
"components.UserList.toggleSortDirectionAria": "Toggle sort direction",
|
||||||
"components.UserList.totalrequests": "Requests",
|
"components.UserList.totalrequests": "Requests",
|
||||||
"components.UserList.user": "User",
|
"components.UserList.user": "User",
|
||||||
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.",
|
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.",
|
||||||
|
|||||||
Reference in New Issue
Block a user