mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 11:35:57 +01: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>
|
</h4>
|
||||||
<pre>
|
<pre>
|
||||||
actor Foo1
|
actor Foo1
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ storiesOf("MarkdownView", module)
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h4 style={{ border: "1px dashed lightgray", padding: "2px" }}>
|
<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>
|
</h4>
|
||||||
<pre>{value}</pre>
|
<pre>{value}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,8 +22,9 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { Redirect, useParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { RepositoryRoleCollection } from "@scm-manager/ui-types";
|
||||||
import {
|
import {
|
||||||
CreateButton,
|
CreateButton,
|
||||||
ErrorNotification,
|
ErrorNotification,
|
||||||
@@ -32,11 +33,33 @@ import {
|
|||||||
Notification,
|
Notification,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
Title,
|
Title,
|
||||||
urls
|
urls,
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import PermissionRoleTable from "../components/PermissionRoleTable";
|
import PermissionRoleTable from "../components/PermissionRoleTable";
|
||||||
import { useRepositoryRoles } from "@scm-manager/ui-api";
|
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 = {
|
type Props = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
};
|
};
|
||||||
@@ -44,29 +67,9 @@ type Props = {
|
|||||||
const RepositoryRoles: FC<Props> = ({ baseUrl }) => {
|
const RepositoryRoles: FC<Props> = ({ baseUrl }) => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const page = urls.getPageFromMatch({ params });
|
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 [t] = useTranslation("admin");
|
||||||
const roles = list?._embedded.repositoryRoles;
|
const canAddRoles = !!data?._links.create;
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorNotification error={error} />;
|
return <ErrorNotification error={error} />;
|
||||||
@@ -76,12 +79,18 @@ const RepositoryRoles: FC<Props> = ({ baseUrl }) => {
|
|||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data && data.pageTotal < page && page > 1) {
|
||||||
|
return <Redirect to={`${baseUrl}/${data.pageTotal}`} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title title={t("repositoryRole.title")} />
|
<Title title={t("repositoryRole.title")} />
|
||||||
<Subtitle subtitle={t("repositoryRole.overview.title")} />
|
<Subtitle subtitle={t("repositoryRole.overview.title")} />
|
||||||
{renderPermissionsTable()}
|
<RepositoryRolesPage data={data} page={page} baseUrl={baseUrl} />
|
||||||
{renderCreateButton()}
|
{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
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React, { FC } from "react";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import GroupRow from "./GroupRow";
|
|
||||||
import { Group } from "@scm-manager/ui-types";
|
import { Group } from "@scm-manager/ui-types";
|
||||||
|
import GroupRow from "./GroupRow";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = {
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
};
|
};
|
||||||
|
|
||||||
class GroupTable extends React.Component<Props> {
|
const GroupTable: FC<Props> = ({ groups }) => {
|
||||||
render() {
|
const [t] = useTranslation("groups");
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
import { Redirect, useLocation, useParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
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 {
|
import {
|
||||||
CreateButton,
|
CreateButton,
|
||||||
LinkPaginator,
|
LinkPaginator,
|
||||||
@@ -31,32 +33,44 @@ import {
|
|||||||
OverviewPageActions,
|
OverviewPageActions,
|
||||||
Page,
|
Page,
|
||||||
PageActions,
|
PageActions,
|
||||||
urls
|
urls,
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import { GroupTable } from "./../components/table";
|
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 Groups: FC = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const search = urls.getQueryStringFromLocation(location);
|
const search = urls.getQueryStringFromLocation(location);
|
||||||
const page = urls.getPageFromMatch({ params });
|
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 [t] = useTranslation("groups");
|
||||||
const groups = list?._embedded.groups;
|
const groups = data?._embedded?.groups;
|
||||||
const canCreateGroups = !!list?._links.create;
|
const canCreateGroups = !!data?._links.create;
|
||||||
|
if (data && data.pageTotal < page && page > 1) {
|
||||||
const renderGroupTable = () => {
|
return <Redirect to={`/groups/${data.pageTotal}`} />;
|
||||||
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>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
@@ -65,8 +79,8 @@ const Groups: FC = () => {
|
|||||||
loading={isLoading || !groups}
|
loading={isLoading || !groups}
|
||||||
error={error || undefined}
|
error={error || undefined}
|
||||||
>
|
>
|
||||||
{renderGroupTable()}
|
<GroupPage data={data} groups={groups} page={page} search={search} />
|
||||||
{canCreateGroups ? <CreateButton label={t("groups.createButton")} link="/groups/create" /> : null}
|
{canCreateGroups ? <CreateButton link="/groups/create" label={t("groups.createButton")} /> : null}
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<OverviewPageActions
|
<OverviewPageActions
|
||||||
showCreateButton={canCreateGroups}
|
showCreateButton={canCreateGroups}
|
||||||
|
|||||||
@@ -22,46 +22,34 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { useRouteMatch } from "react-router-dom";
|
import { Redirect, useRouteMatch } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
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 {
|
import {
|
||||||
ChangesetList,
|
ChangesetList,
|
||||||
ErrorNotification,
|
ErrorNotification,
|
||||||
LinkPaginator,
|
LinkPaginator,
|
||||||
Loading,
|
Loading,
|
||||||
Notification,
|
Notification,
|
||||||
urls
|
urls,
|
||||||
} from "@scm-manager/ui-components";
|
} 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 = () => {
|
export const usePage = () => {
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
return urls.getPageFromMatch(match);
|
return urls.getPageFromMatch(match);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Changesets: FC<Props> = ({ repository, branch }) => {
|
type Props = {
|
||||||
const page = usePage();
|
repository: Repository;
|
||||||
|
branch?: Branch;
|
||||||
const { isLoading, error, data } = useChangesets(repository, { branch, page: page - 1 });
|
url: string;
|
||||||
|
|
||||||
return <ChangesetsPanel repository={repository} branch={branch} error={error} isLoading={isLoading} data={data} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChangesetsPanel: FC<ChangesetProps> = ({ repository, error, isLoading, data }) => {
|
const Changesets: FC<Props> = ({ repository, branch, url }) => {
|
||||||
const [t] = useTranslation("repos");
|
|
||||||
const page = usePage();
|
const page = usePage();
|
||||||
|
const { isLoading, error, data } = useChangesets(repository, { branch, page: page - 1 });
|
||||||
|
const [t] = useTranslation("repos");
|
||||||
const changesets = data?._embedded?.changesets;
|
const changesets = data?._embedded?.changesets;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -72,7 +60,11 @@ export const ChangesetsPanel: FC<ChangesetProps> = ({ repository, error, isLoadi
|
|||||||
return <Loading />;
|
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>;
|
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} />
|
<ChangesetList repository={repository} changesets={changesets} />
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-footer">
|
<div className="panel-footer">
|
||||||
<LinkPaginator page={page} collection={data} />
|
<LinkPaginator collection={data} page={page} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ const ChangesetRoot: FC<Props> = ({ repository, baseUrl, branches, selectedBranc
|
|||||||
|
|
||||||
const onSelectBranch = (branch?: Branch) => {
|
const onSelectBranch = (branch?: Branch) => {
|
||||||
if (branch) {
|
if (branch) {
|
||||||
const url = `${baseUrl}/branch/${encodeURIComponent(branch.name)}/changesets/`;
|
history.push(`${baseUrl}/branch/${encodeURIComponent(branch.name)}/changesets/`);
|
||||||
history.push(url);
|
|
||||||
} else {
|
} else {
|
||||||
history.push(`${baseUrl}/changesets/`);
|
history.push(`${baseUrl}/changesets/`);
|
||||||
}
|
}
|
||||||
@@ -75,7 +74,7 @@ const ChangesetRoot: FC<Props> = ({ repository, baseUrl, branches, selectedBranc
|
|||||||
switchViewLink={evaluateSwitchViewLink()}
|
switchViewLink={evaluateSwitchViewLink()}
|
||||||
/>
|
/>
|
||||||
<Route path={`${url}/:page?`}>
|
<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>
|
</Route>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: string;
|
type: string;
|
||||||
hits: HitType[];
|
hits?: HitType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const hitComponents: { [name: string]: FC<HitProps> } = {
|
const hitComponents: { [name: string]: FC<HitProps> } = {
|
||||||
@@ -69,12 +69,12 @@ const HitComponent: FC<HitComponentProps> = ({ hit, type }) => (
|
|||||||
|
|
||||||
const NoHits: FC = () => {
|
const NoHits: FC = () => {
|
||||||
const [t] = useTranslation("commons");
|
const [t] = useTranslation("commons");
|
||||||
return <Notification>{t("search.noHits")}</Notification>;
|
return <Notification type="info">{t("search.noHits")}</Notification>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Hits: FC<Props> = ({ type, hits }) => (
|
const Hits: FC<Props> = ({ type, hits }) => (
|
||||||
<div className="panel-block">
|
<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} />)
|
hits.map((hit, c) => <HitComponent key={`${type}_${c}_${hit.score}`} hit={hit} type={type} />)
|
||||||
) : (
|
) : (
|
||||||
<NoHits />
|
<NoHits />
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import React, { FC } from "react";
|
|||||||
import { QueryResult } from "@scm-manager/ui-types";
|
import { QueryResult } from "@scm-manager/ui-types";
|
||||||
import Hits from "./Hits";
|
import Hits from "./Hits";
|
||||||
import { LinkPaginator } from "@scm-manager/ui-components";
|
import { LinkPaginator } from "@scm-manager/ui-components";
|
||||||
|
import { Redirect, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
result: QueryResult;
|
result: QueryResult;
|
||||||
@@ -35,9 +36,21 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Results: FC<Props> = ({ result, type, page, query }) => {
|
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 (
|
return (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<Hits type={type} hits={result._embedded.hits} />
|
<Hits type={type} hits={hits} />
|
||||||
<div className="panel-footer">
|
<div className="panel-footer">
|
||||||
<LinkPaginator collection={result} page={page} filter={query} />
|
<LinkPaginator collection={result} page={page} filter={query} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,35 +21,34 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React, { FC } from "react";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { User } from "@scm-manager/ui-types";
|
import { User } from "@scm-manager/ui-types";
|
||||||
import UserRow from "./UserRow";
|
import UserRow from "./UserRow";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = {
|
||||||
users: User[];
|
users: User[];
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserTable extends React.Component<Props> {
|
const UserTable: FC<Props> = ({ users }) => {
|
||||||
render() {
|
const [t] = useTranslation("users");
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
import { Redirect, useLocation, useParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
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 {
|
import {
|
||||||
CreateButton,
|
CreateButton,
|
||||||
LinkPaginator,
|
LinkPaginator,
|
||||||
@@ -31,10 +33,31 @@ import {
|
|||||||
OverviewPageActions,
|
OverviewPageActions,
|
||||||
Page,
|
Page,
|
||||||
PageActions,
|
PageActions,
|
||||||
urls
|
urls,
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import { UserTable } from "./../components/table";
|
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 Users: FC = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -42,28 +65,13 @@ const Users: FC = () => {
|
|||||||
const search = urls.getQueryStringFromLocation(location);
|
const search = urls.getQueryStringFromLocation(location);
|
||||||
const page = urls.getPageFromMatch({ params });
|
const page = urls.getPageFromMatch({ params });
|
||||||
const { isLoading, error, data } = useUsers({ page: page - 1, search });
|
const { isLoading, error, data } = useUsers({ page: page - 1, search });
|
||||||
const users = data?._embedded.users;
|
|
||||||
const [t] = useTranslation("users");
|
const [t] = useTranslation("users");
|
||||||
|
const users = data?._embedded?.users;
|
||||||
const canAddUsers = !!data?._links.create;
|
const canAddUsers = !!data?._links.create;
|
||||||
|
|
||||||
const renderUserTable = () => {
|
if (data && data.pageTotal < page && page > 1) {
|
||||||
if (data && users && users.length > 0) {
|
return <Redirect to={`/users/${data.pageTotal}`} />;
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
@@ -72,8 +80,8 @@ const Users: FC = () => {
|
|||||||
loading={isLoading || !users}
|
loading={isLoading || !users}
|
||||||
error={error || undefined}
|
error={error || undefined}
|
||||||
>
|
>
|
||||||
{renderUserTable()}
|
<UserPage data={data} users={users} page={page} search={search} />
|
||||||
{renderCreateButton()}
|
{canAddUsers ? <CreateButton link="/users/create" label={t("users.createButton")} /> : null}
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<OverviewPageActions
|
<OverviewPageActions
|
||||||
showCreateButton={canAddUsers}
|
showCreateButton={canAddUsers}
|
||||||
|
|||||||
Reference in New Issue
Block a user