mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-01 00:19:44 +02:00
Fix paging for too large page numbers (#2097)
On some pages with pagination, the user is led to believe that no data is available if a page with page number which it too high is accessed. However, since we show the page number to the outside and the user can access it through the URL, we must also provide appropriate handling. The underlying data can change and so can the number of pages. Now, if a bookmark was saved from an older version, the link should still lead to a destination.
This commit is contained in:
2
gradle/changelog/fix_paging.yaml
Normal file
2
gradle/changelog/fix_paging.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: Fix paging for too large page numbers ([#2097](https://github.com/scm-manager/scm-manager/pull/2097))
|
||||
@@ -4996,7 +4996,7 @@ exports[`Storyshots MarkdownView Custom code renderer 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
To render plantuml as images within markdown, please install the scm-markdown-plantuml-plguin
|
||||
To render plantuml as images within markdown, please install the scm-markdown-plantuml-plugin
|
||||
</h4>
|
||||
<pre>
|
||||
actor Foo1
|
||||
|
||||
@@ -101,7 +101,7 @@ storiesOf("MarkdownView", module)
|
||||
return (
|
||||
<div>
|
||||
<h4 style={{ border: "1px dashed lightgray", padding: "2px" }}>
|
||||
To render plantuml as images within markdown, please install the scm-markdown-plantuml-plguin
|
||||
To render plantuml as images within markdown, please install the scm-markdown-plantuml-plugin
|
||||
</h4>
|
||||
<pre>{value}</pre>
|
||||
</div>
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Redirect, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RepositoryRoleCollection } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CreateButton,
|
||||
ErrorNotification,
|
||||
@@ -32,11 +33,33 @@ import {
|
||||
Notification,
|
||||
Subtitle,
|
||||
Title,
|
||||
urls
|
||||
urls,
|
||||
} from "@scm-manager/ui-components";
|
||||
import PermissionRoleTable from "../components/PermissionRoleTable";
|
||||
import { useRepositoryRoles } from "@scm-manager/ui-api";
|
||||
|
||||
type RepositoryRolesPageProps = {
|
||||
data?: RepositoryRoleCollection;
|
||||
page: number;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
const RepositoryRolesPage: FC<RepositoryRolesPageProps> = ({ data, page, baseUrl }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const roles = data?._embedded?.repositoryRoles;
|
||||
|
||||
if (!data || !roles || roles.length === 0) {
|
||||
return <Notification type="info">{t("repositoryRole.overview.noPermissionRoles")}</Notification>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PermissionRoleTable baseUrl={baseUrl} roles={roles} />
|
||||
<LinkPaginator collection={data} page={page} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
baseUrl: string;
|
||||
};
|
||||
@@ -44,29 +67,9 @@ type Props = {
|
||||
const RepositoryRoles: FC<Props> = ({ baseUrl }) => {
|
||||
const params = useParams();
|
||||
const page = urls.getPageFromMatch({ params });
|
||||
const { isLoading: loading, error, data: list } = useRepositoryRoles({ page: page - 1 });
|
||||
const { isLoading: loading, error, data } = useRepositoryRoles({ page: page - 1 });
|
||||
const [t] = useTranslation("admin");
|
||||
const roles = list?._embedded.repositoryRoles;
|
||||
const canAddRoles = !!list?._links.create;
|
||||
|
||||
const renderPermissionsTable = () => {
|
||||
if (list && roles && roles.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<PermissionRoleTable baseUrl={baseUrl} roles={roles} />
|
||||
<LinkPaginator collection={list} page={page} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Notification type="info">{t("repositoryRole.overview.noPermissionRoles")}</Notification>;
|
||||
};
|
||||
|
||||
const renderCreateButton = () => {
|
||||
if (canAddRoles) {
|
||||
return <CreateButton label={t("repositoryRole.overview.createButton")} link={`${baseUrl}/create`} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const canAddRoles = !!data?._links.create;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
@@ -76,12 +79,18 @@ const RepositoryRoles: FC<Props> = ({ baseUrl }) => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (data && data.pageTotal < page && page > 1) {
|
||||
return <Redirect to={`${baseUrl}/${data.pageTotal}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title title={t("repositoryRole.title")} />
|
||||
<Subtitle subtitle={t("repositoryRole.overview.title")} />
|
||||
{renderPermissionsTable()}
|
||||
{renderCreateButton()}
|
||||
<RepositoryRolesPage data={data} page={page} baseUrl={baseUrl} />
|
||||
{canAddRoles ? (
|
||||
<CreateButton label={t("repositoryRole.overview.createButton")} link={`${baseUrl}/create`} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,34 +21,33 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import GroupRow from "./GroupRow";
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Group } from "@scm-manager/ui-types";
|
||||
import GroupRow from "./GroupRow";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
groups: Group[];
|
||||
};
|
||||
|
||||
class GroupTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { groups, t } = this.props;
|
||||
return (
|
||||
<table className="card-table table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("group.name")}</th>
|
||||
<th className="is-hidden-mobile">{t("group.description")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groups.map((group, index) => {
|
||||
return <GroupRow key={index} group={group} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
const GroupTable: FC<Props> = ({ groups }) => {
|
||||
const [t] = useTranslation("groups");
|
||||
|
||||
export default withTranslation("groups")(GroupTable);
|
||||
return (
|
||||
<table className="card-table table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("group.name")}</th>
|
||||
<th className="is-hidden-mobile">{t("group.description")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groups.map((group, index) => {
|
||||
return <GroupRow key={index} group={group} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupTable;
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { Redirect, useLocation, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { useGroups } from "@scm-manager/ui-api";
|
||||
import { Group, GroupCollection } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CreateButton,
|
||||
LinkPaginator,
|
||||
@@ -31,32 +33,44 @@ import {
|
||||
OverviewPageActions,
|
||||
Page,
|
||||
PageActions,
|
||||
urls
|
||||
urls,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { GroupTable } from "./../components/table";
|
||||
import { useGroups } from "@scm-manager/ui-api";
|
||||
|
||||
type GroupPageProps = {
|
||||
data?: GroupCollection;
|
||||
groups?: Group[];
|
||||
page: number;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
const GroupPage: FC<GroupPageProps> = ({ data, groups, page, search }) => {
|
||||
const [t] = useTranslation("groups");
|
||||
|
||||
if (!data || !groups || groups.length === 0) {
|
||||
return <Notification type="info">{t("groups.noGroups")}</Notification>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<GroupTable groups={groups} />
|
||||
<LinkPaginator collection={data} page={page} filter={search} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Groups: FC = () => {
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const search = urls.getQueryStringFromLocation(location);
|
||||
const page = urls.getPageFromMatch({ params });
|
||||
const { isLoading, error, data: list } = useGroups({ search, page: page - 1 });
|
||||
const { isLoading, error, data } = useGroups({ search, page: page - 1 });
|
||||
const [t] = useTranslation("groups");
|
||||
const groups = list?._embedded.groups;
|
||||
const canCreateGroups = !!list?._links.create;
|
||||
|
||||
const renderGroupTable = () => {
|
||||
if (list && groups && groups.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<GroupTable groups={groups} />
|
||||
<LinkPaginator collection={list} page={page} filter={urls.getQueryStringFromLocation(location)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Notification type="info">{t("groups.noGroups")}</Notification>;
|
||||
};
|
||||
const groups = data?._embedded?.groups;
|
||||
const canCreateGroups = !!data?._links.create;
|
||||
if (data && data.pageTotal < page && page > 1) {
|
||||
return <Redirect to={`/groups/${data.pageTotal}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -65,8 +79,8 @@ const Groups: FC = () => {
|
||||
loading={isLoading || !groups}
|
||||
error={error || undefined}
|
||||
>
|
||||
{renderGroupTable()}
|
||||
{canCreateGroups ? <CreateButton label={t("groups.createButton")} link="/groups/create" /> : null}
|
||||
<GroupPage data={data} groups={groups} page={page} search={search} />
|
||||
{canCreateGroups ? <CreateButton link="/groups/create" label={t("groups.createButton")} /> : null}
|
||||
<PageActions>
|
||||
<OverviewPageActions
|
||||
showCreateButton={canCreateGroups}
|
||||
|
||||
@@ -22,46 +22,34 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { useRouteMatch } from "react-router-dom";
|
||||
import { Redirect, useRouteMatch } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Branch, ChangesetCollection, Repository } from "@scm-manager/ui-types";
|
||||
import { useChangesets } from "@scm-manager/ui-api";
|
||||
import { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import {
|
||||
ChangesetList,
|
||||
ErrorNotification,
|
||||
LinkPaginator,
|
||||
Loading,
|
||||
Notification,
|
||||
urls
|
||||
urls,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { useChangesets } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
branch?: Branch;
|
||||
};
|
||||
|
||||
type ChangesetProps = Props & {
|
||||
error: Error | null;
|
||||
isLoading: boolean;
|
||||
data?: ChangesetCollection;
|
||||
};
|
||||
|
||||
export const usePage = () => {
|
||||
const match = useRouteMatch();
|
||||
return urls.getPageFromMatch(match);
|
||||
};
|
||||
|
||||
const Changesets: FC<Props> = ({ repository, branch }) => {
|
||||
const page = usePage();
|
||||
|
||||
const { isLoading, error, data } = useChangesets(repository, { branch, page: page - 1 });
|
||||
|
||||
return <ChangesetsPanel repository={repository} branch={branch} error={error} isLoading={isLoading} data={data} />;
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
branch?: Branch;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export const ChangesetsPanel: FC<ChangesetProps> = ({ repository, error, isLoading, data }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const Changesets: FC<Props> = ({ repository, branch, url }) => {
|
||||
const page = usePage();
|
||||
const { isLoading, error, data } = useChangesets(repository, { branch, page: page - 1 });
|
||||
const [t] = useTranslation("repos");
|
||||
const changesets = data?._embedded?.changesets;
|
||||
|
||||
if (error) {
|
||||
@@ -72,7 +60,11 @@ export const ChangesetsPanel: FC<ChangesetProps> = ({ repository, error, isLoadi
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!changesets || changesets.length === 0) {
|
||||
if (data && data.pageTotal < page && page > 1) {
|
||||
return <Redirect to={`${urls.unescapeUrlForRoute(url)}/${data.pageTotal}`} />;
|
||||
}
|
||||
|
||||
if (!data || !changesets || changesets.length === 0) {
|
||||
return <Notification type="info">{t("changesets.noChangesets")}</Notification>;
|
||||
}
|
||||
|
||||
@@ -82,7 +74,7 @@ export const ChangesetsPanel: FC<ChangesetProps> = ({ repository, error, isLoadi
|
||||
<ChangesetList repository={repository} changesets={changesets} />
|
||||
</div>
|
||||
<div className="panel-footer">
|
||||
<LinkPaginator page={page} collection={data} />
|
||||
<LinkPaginator collection={data} page={page} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -59,8 +59,7 @@ const ChangesetRoot: FC<Props> = ({ repository, baseUrl, branches, selectedBranc
|
||||
|
||||
const onSelectBranch = (branch?: Branch) => {
|
||||
if (branch) {
|
||||
const url = `${baseUrl}/branch/${encodeURIComponent(branch.name)}/changesets/`;
|
||||
history.push(url);
|
||||
history.push(`${baseUrl}/branch/${encodeURIComponent(branch.name)}/changesets/`);
|
||||
} else {
|
||||
history.push(`${baseUrl}/changesets/`);
|
||||
}
|
||||
@@ -75,7 +74,7 @@ const ChangesetRoot: FC<Props> = ({ repository, baseUrl, branches, selectedBranc
|
||||
switchViewLink={evaluateSwitchViewLink()}
|
||||
/>
|
||||
<Route path={`${url}/:page?`}>
|
||||
<Changesets repository={repository} branch={branches?.filter(b => b.name === selectedBranch)[0]} />
|
||||
<Changesets repository={repository} branch={branches?.filter(b => b.name === selectedBranch)[0]} url={url} />
|
||||
</Route>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
|
||||
type Props = {
|
||||
type: string;
|
||||
hits: HitType[];
|
||||
hits?: HitType[];
|
||||
};
|
||||
|
||||
const hitComponents: { [name: string]: FC<HitProps> } = {
|
||||
@@ -69,12 +69,12 @@ const HitComponent: FC<HitComponentProps> = ({ hit, type }) => (
|
||||
|
||||
const NoHits: FC = () => {
|
||||
const [t] = useTranslation("commons");
|
||||
return <Notification>{t("search.noHits")}</Notification>;
|
||||
return <Notification type="info">{t("search.noHits")}</Notification>;
|
||||
};
|
||||
|
||||
const Hits: FC<Props> = ({ type, hits }) => (
|
||||
<div className="panel-block">
|
||||
{hits.length > 0 ? (
|
||||
{hits && hits.length > 0 ? (
|
||||
hits.map((hit, c) => <HitComponent key={`${type}_${c}_${hit.score}`} hit={hit} type={type} />)
|
||||
) : (
|
||||
<NoHits />
|
||||
|
||||
@@ -26,6 +26,7 @@ import React, { FC } from "react";
|
||||
import { QueryResult } from "@scm-manager/ui-types";
|
||||
import Hits from "./Hits";
|
||||
import { LinkPaginator } from "@scm-manager/ui-components";
|
||||
import { Redirect, useLocation } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
result: QueryResult;
|
||||
@@ -35,9 +36,21 @@ type Props = {
|
||||
};
|
||||
|
||||
const Results: FC<Props> = ({ result, type, page, query }) => {
|
||||
const location = useLocation();
|
||||
const hits = result?._embedded?.hits;
|
||||
|
||||
let pathname = location.pathname;
|
||||
if (!pathname.endsWith("/")) {
|
||||
pathname = pathname.substring(0, pathname.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
if (result && result.pageTotal < page && page > 1) {
|
||||
return <Redirect to={`${pathname}${result.pageTotal}${location.search}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
<Hits type={type} hits={result._embedded.hits} />
|
||||
<Hits type={type} hits={hits} />
|
||||
<div className="panel-footer">
|
||||
<LinkPaginator collection={result} page={page} filter={query} />
|
||||
</div>
|
||||
|
||||
@@ -21,35 +21,34 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { User } from "@scm-manager/ui-types";
|
||||
import UserRow from "./UserRow";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
users: User[];
|
||||
};
|
||||
|
||||
class UserTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { users, t } = this.props;
|
||||
return (
|
||||
<table className="card-table table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("user.name")}</th>
|
||||
<th className="is-hidden-mobile">{t("user.displayName")}</th>
|
||||
<th className="is-hidden-mobile">{t("user.mail")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user, index) => {
|
||||
return <UserRow key={index} user={user} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
const UserTable: FC<Props> = ({ users }) => {
|
||||
const [t] = useTranslation("users");
|
||||
|
||||
export default withTranslation("users")(UserTable);
|
||||
return (
|
||||
<table className="card-table table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("user.name")}</th>
|
||||
<th className="is-hidden-mobile">{t("user.displayName")}</th>
|
||||
<th className="is-hidden-mobile">{t("user.mail")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user, index) => {
|
||||
return <UserRow key={index} user={user} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserTable;
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { Redirect, useLocation, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { useUsers } from "@scm-manager/ui-api";
|
||||
import { User, UserCollection } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CreateButton,
|
||||
LinkPaginator,
|
||||
@@ -31,10 +33,31 @@ import {
|
||||
OverviewPageActions,
|
||||
Page,
|
||||
PageActions,
|
||||
urls
|
||||
urls,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { UserTable } from "./../components/table";
|
||||
import { useUsers } from "@scm-manager/ui-api";
|
||||
|
||||
type UserPageProps = {
|
||||
data?: UserCollection;
|
||||
users?: User[];
|
||||
page: number;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
const UserPage: FC<UserPageProps> = ({ data, users, page, search }) => {
|
||||
const [t] = useTranslation("users");
|
||||
|
||||
if (!data || !users || users.length === 0) {
|
||||
return <Notification type="info">{t("users.noUsers")}</Notification>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserTable users={users} />
|
||||
<LinkPaginator collection={data} page={page} filter={search} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Users: FC = () => {
|
||||
const location = useLocation();
|
||||
@@ -42,28 +65,13 @@ const Users: FC = () => {
|
||||
const search = urls.getQueryStringFromLocation(location);
|
||||
const page = urls.getPageFromMatch({ params });
|
||||
const { isLoading, error, data } = useUsers({ page: page - 1, search });
|
||||
const users = data?._embedded.users;
|
||||
const [t] = useTranslation("users");
|
||||
const users = data?._embedded?.users;
|
||||
const canAddUsers = !!data?._links.create;
|
||||
|
||||
const renderUserTable = () => {
|
||||
if (data && users && users.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<UserTable users={users} />
|
||||
<LinkPaginator collection={data} page={page} filter={urls.getQueryStringFromLocation(location)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Notification type="info">{t("users.noUsers")}</Notification>;
|
||||
};
|
||||
|
||||
const renderCreateButton = () => {
|
||||
if (canAddUsers) {
|
||||
return <CreateButton link="/users/create" label={t("users.createButton")} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
if (data && data.pageTotal < page && page > 1) {
|
||||
return <Redirect to={`/users/${data.pageTotal}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -72,8 +80,8 @@ const Users: FC = () => {
|
||||
loading={isLoading || !users}
|
||||
error={error || undefined}
|
||||
>
|
||||
{renderUserTable()}
|
||||
{renderCreateButton()}
|
||||
<UserPage data={data} users={users} page={page} search={search} />
|
||||
{canAddUsers ? <CreateButton link="/users/create" label={t("users.createButton")} /> : null}
|
||||
<PageActions>
|
||||
<OverviewPageActions
|
||||
showCreateButton={canAddUsers}
|
||||
|
||||
Reference in New Issue
Block a user