Skip to content

Commit 67a28b4

Browse files
committed
membership-request [inveniosoftware#855]: implement wait for decision flow
1 parent ed95004 commit 67a28b4

File tree

39 files changed

+1264
-96
lines changed

39 files changed

+1264
-96
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// This file is part of Invenio-communities
2+
// Copyright (C) 2022 CERN.
3+
// Copyright (C) 2024 Northwestern University.
4+
//
5+
// Invenio-communities is free software; you can redistribute it and/or modify it
6+
// under the terms of the MIT License; see LICENSE file for more details.
7+
8+
import { CommunityMembershipRequestsApi } from "./api";
9+
import React, { Component } from "react";
10+
import PropTypes from "prop-types";
11+
12+
export const MembershipRequestsContext = React.createContext({ api: undefined });
13+
14+
export class MembershipRequestsContextProvider extends Component {
15+
constructor(props) {
16+
super(props);
17+
const { community } = props;
18+
this.apiClient = new CommunityMembershipRequestsApi(community);
19+
}
20+
render() {
21+
const { children } = this.props;
22+
return (
23+
<MembershipRequestsContext.Provider value={{ api: this.apiClient }}>
24+
{children}
25+
</MembershipRequestsContext.Provider>
26+
);
27+
}
28+
}
29+
30+
MembershipRequestsContextProvider.propTypes = {
31+
community: PropTypes.object.isRequired,
32+
children: PropTypes.node.isRequired,
33+
};

invenio_communities/assets/semantic-ui/js/invenio_communities/members/Filters.js

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ export class Filters {
4949
return { ...rolesFilters, ...statusFilters };
5050
}
5151

52+
getMembershipRequestFilters() {
53+
const statusFilters = this.getStatus();
54+
const rolesFilters = this.getRoles();
55+
return { ...rolesFilters, ...statusFilters };
56+
}
57+
5258
getMembersFilters() {
5359
const visibilityFilters = this.getVisibility();
5460
const rolesFilters = this.getRoles();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* This file is part of Invenio.
3+
* Copyright (C) 2022 CERN.
4+
* Copyright (C) 2024 Northwestern University.
5+
*
6+
* Invenio is free software; you can redistribute it and/or modify it
7+
* under the terms of the MIT License; see LICENSE file for more details.
8+
*/
9+
10+
import React from "react";
11+
import { Grid } from "semantic-ui-react";
12+
import { ResultsPerPage, Pagination, ResultsList } from "react-searchkit";
13+
import PropTypes from "prop-types";
14+
import { Trans } from "react-i18next";
15+
16+
export const MemberRequestsResults = ({ paginationOptions, currentResultsState }) => {
17+
const { total } = currentResultsState.data;
18+
return (
19+
total && (
20+
<Grid>
21+
<Grid.Row>
22+
<Grid.Column width={16}>
23+
<ResultsList />
24+
</Grid.Column>
25+
</Grid.Row>
26+
<Grid.Row verticalAlign="middle">
27+
<Grid.Column width={8} textAlign="right">
28+
<Pagination
29+
options={{
30+
size: "mini",
31+
showFirst: false,
32+
showLast: false,
33+
}}
34+
/>
35+
</Grid.Column>
36+
<Grid.Column textAlign="right" width={8}>
37+
<ResultsPerPage
38+
values={paginationOptions.resultsPerPage}
39+
label={(cmp) => (
40+
// kept key for translation purposes - it should be
41+
// the same across members, invitations, membership requests
42+
// and beyond
43+
<Trans key="communitiesInvitationsResult" count={cmp}>
44+
{cmp} results per page
45+
</Trans>
46+
)}
47+
/>
48+
</Grid.Column>
49+
</Grid.Row>
50+
</Grid>
51+
)
52+
);
53+
};
54+
55+
MemberRequestsResults.propTypes = {
56+
paginationOptions: PropTypes.object.isRequired,
57+
currentResultsState: PropTypes.object.isRequired,
58+
};
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* This file is part of Invenio.
33
* Copyright (C) 2022 CERN.
4+
* Copyright (C) 2024 Northwestern University.
45
*
56
* Invenio is free software; you can redistribute it and/or modify it
67
* under the terms of the MIT License; see LICENSE file for more details.
@@ -11,24 +12,26 @@ import { Input } from "semantic-ui-react";
1112
import { i18next } from "@translations/invenio_communities/i18next";
1213
import PropTypes from "prop-types";
1314

14-
export const InvitationsSearchBarElement = ({
15+
export const MemberRequestsSearchBarElement = ({
1516
onBtnSearchClick,
1617
onInputChange,
1718
onKeyPress,
1819
queryString,
1920
uiProps,
21+
className,
22+
placeholder,
2023
}) => {
2124
return (
2225
<Input
23-
className="invitation-searchbar"
26+
className={className}
2427
action={{
2528
icon: "search",
2629
onClick: onBtnSearchClick,
2730
className: "search",
2831
title: i18next.t("Search"),
2932
}}
3033
fluid
31-
placeholder={i18next.t("Search in invitations...")}
34+
placeholder={placeholder}
3235
onChange={(_, { value }) => {
3336
onInputChange(value);
3437
}}
@@ -39,14 +42,18 @@ export const InvitationsSearchBarElement = ({
3942
);
4043
};
4144

42-
InvitationsSearchBarElement.propTypes = {
45+
MemberRequestsSearchBarElement.propTypes = {
4346
onBtnSearchClick: PropTypes.func.isRequired,
4447
onInputChange: PropTypes.func.isRequired,
4548
onKeyPress: PropTypes.func.isRequired,
4649
queryString: PropTypes.string.isRequired,
4750
uiProps: PropTypes.object,
51+
className: PropTypes.string,
52+
placeholder: PropTypes.string,
4853
};
4954

50-
InvitationsSearchBarElement.defaultProps = {
55+
MemberRequestsSearchBarElement.defaultProps = {
5156
uiProps: null,
57+
className: "",
58+
placeholder: "",
5259
};

invenio_communities/assets/semantic-ui/js/invenio_communities/members/invitations/InvitationResultItem.js

+14-15
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
/*
22
* This file is part of Invenio.
33
* Copyright (C) 2022 CERN.
4+
* Copyright (C) 2024 Northwestern University.
45
*
56
* Invenio is free software; you can redistribute it and/or modify it
67
* under the terms of the MIT License; see LICENSE file for more details.
78
*/
89

10+
import { RequestActionController } from "@js/invenio_requests/request/actions/RequestActionController";
911
import { i18next } from "@translations/invenio_communities/i18next";
10-
import { DateTime } from "luxon";
1112
import PropTypes from "prop-types";
1213
import React, { Component } from "react";
1314
import { Image } from "react-invenio-forms";
1415
import { Container, Grid, Item, Table } from "semantic-ui-react";
1516
import { InvitationsContext } from "../../api/invitations/InvitationsContextProvider";
1617
import { RoleDropdown } from "../components/dropdowns";
18+
import { buildRequest, formattedTime } from "../utils";
1719
import RequestStatus from "@js/invenio_requests/request/RequestStatus";
1820

19-
const formattedTime = (expiresAt) =>
20-
DateTime.fromISO(expiresAt).setLocale(i18next.language).toRelative();
21-
2221
export class InvitationResultItem extends Component {
2322
constructor(props) {
2423
super(props);
@@ -39,12 +38,13 @@ export class InvitationResultItem extends Component {
3938
community,
4039
} = this.props;
4140
const {
42-
invitation: { member, request },
41+
invitation: { member },
4342
invitation,
4443
} = this.state;
44+
const request = buildRequest(invitation, ["cancel"]);
4545
const { api: invitationsApi } = this.context;
4646
const rolesCanInviteByType = rolesCanInvite[member.type];
47-
const memberInvitationExpiration = formattedTime(request.expires_at);
47+
const expiration = formattedTime(request.expires_at);
4848
return (
4949
<Table.Row className="community-member-item">
5050
<Table.Cell>
@@ -72,10 +72,10 @@ export class InvitationResultItem extends Component {
7272
<RequestStatus status={request.status} />
7373
</Table.Cell>
7474
<Table.Cell
75-
aria-label={i18next.t("Expires") + " " + memberInvitationExpiration}
75+
aria-label={i18next.t("Expires") + " " + expiration}
7676
data-label={i18next.t("Expires")}
7777
>
78-
{memberInvitationExpiration}
78+
{expiration}
7979
</Table.Cell>
8080
<Table.Cell data-label={i18next.t("Role")}>
8181
<RoleDropdown
@@ -90,13 +90,12 @@ export class InvitationResultItem extends Component {
9090
</Table.Cell>
9191
<Table.Cell>
9292
<Container fluid textAlign="right">
93-
{/* TODO uncomment when links available in the request resource subschema */}
94-
{/*<RequestActionController*/}
95-
{/* request={request }*/}
96-
{/* actionSuccessCallback={this.updateInvitation}*/}
97-
{/*>*/}
98-
{/*<ActionButtons request={invitation} />*/}
99-
{/*</RequestActionController>*/}
93+
<RequestActionController
94+
request={request}
95+
actionSuccessCallback={() => {
96+
window.location.reload();
97+
}}
98+
/>
10099
</Container>
101100
</Table.Cell>
102101
</Table.Row>

invenio_communities/assets/semantic-ui/js/invenio_communities/members/invitations/index.js

+36-13
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
11
/*
22
* This file is part of Invenio.
33
* Copyright (C) 2022 CERN.
4+
* Copyright (C) 2024 Northwestern University.
45
*
56
* Invenio is free software; you can redistribute it and/or modify it
67
* under the terms of the MIT License; see LICENSE file for more details.
78
*/
89

9-
import { createSearchAppInit } from "@js/invenio_search_ui";
10-
import { parametrize, overrideStore } from "react-overridable";
11-
import { DropdownSort } from "@js/invenio_search_ui/components";
12-
import { InvitationsContextProvider as ContextProvider } from "../../api/invitations/InvitationsContextProvider";
13-
import { InvitationResultItem } from "./InvitationResultItem";
14-
import { InvitationsResults } from "./InvitationsResults";
15-
import { InvitationsResultsContainer } from "./InvitationsResultsContainer";
16-
import { InvitationsSearchBarElement } from "./InvitationsSearchBarElement";
17-
import { InvitationsSearchLayout } from "./InvitationsSearchLayout";
18-
import { InvitationsEmptyResults } from "./InvitationsEmptyResults";
10+
import { RequestCancelButton } from "@js/invenio_requests/components/Buttons";
11+
import { RequestCancelModalTrigger } from "@js/invenio_requests/components/ModalTriggers";
1912
import {
2013
SubmitStatus,
2114
DeleteStatus,
@@ -24,6 +17,20 @@ import {
2417
CancelStatus,
2518
ExpireStatus,
2619
} from "@js/invenio_requests/request";
20+
import { createSearchAppInit } from "@js/invenio_search_ui";
21+
import { DropdownSort } from "@js/invenio_search_ui/components";
22+
import { i18next } from "@translations/invenio_communities/i18next";
23+
import React from "react";
24+
import { Trans } from "react-i18next";
25+
import { parametrize, overrideStore } from "react-overridable";
26+
27+
import { InvitationsContextProvider as ContextProvider } from "../../api/invitations/InvitationsContextProvider";
28+
import { MemberRequestsSearchBarElement } from "../MemberRequestsSearchBarElement";
29+
import { InvitationsEmptyResults } from "./InvitationsEmptyResults";
30+
import { InvitationResultItem } from "./InvitationResultItem";
31+
import { InvitationsResults } from "./InvitationsResults";
32+
import { InvitationsResultsContainer } from "./InvitationsResultsContainer";
33+
import { InvitationsSearchLayout } from "./InvitationsSearchLayout";
2734

2835
const dataAttr = document.getElementById("community-invitations-search-root").dataset;
2936
const community = JSON.parse(dataAttr.community);
@@ -49,6 +56,11 @@ const InvitationsSearchLayoutWithConfig = parametrize(InvitationsSearchLayout, {
4956
appName: appName,
5057
});
5158

59+
const InvitationsSearchBarElement = parametrize(MemberRequestsSearchBarElement, {
60+
className: "invitation-searchbar",
61+
placeholder: i18next.t("Search in invitations..."),
62+
});
63+
5264
const InvitationsContextProvider = parametrize(ContextProvider, {
5365
community: community,
5466
});
@@ -65,14 +77,25 @@ const InvitationsEmptyResultsWithCommunity = parametrize(InvitationsEmptyResults
6577
rolesCanInvite: communitiesRolesCanInvite,
6678
});
6779

80+
const InvitationsRequestCancelButton = parametrize(RequestCancelButton, {
81+
content: i18next.t("Cancel invitation"),
82+
});
83+
84+
const InvitationsRequestActionModalCancelTitle = (props) => {
85+
return <Trans defaults="{{action}} invitation" values={{ action: "cancel" }} />;
86+
};
87+
6888
const defaultComponents = {
6989
[`${appName}.EmptyResults.element`]: InvitationsEmptyResultsWithCommunity,
70-
[`${appName}.ResultsList.item`]: InvitationResultItemWithConfig,
7190
[`${appName}.SearchApp.layout`]: InvitationsSearchLayoutWithConfig,
7291
[`${appName}.SearchBar.element`]: InvitationsSearchBarElement,
73-
[`${appName}.SearchApp.results`]: InvitationsResults,
74-
[`${appName}.ResultsList.container`]: InvitationsResultsContainerWithConfig,
7592
[`${appName}.Sort.element`]: DropdownSort,
93+
[`${appName}.ResultsList.container`]: InvitationsResultsContainerWithConfig,
94+
[`${appName}.SearchApp.results`]: InvitationsResults,
95+
[`${appName}.ResultsList.item`]: InvitationResultItemWithConfig,
96+
"RequestActionModalTrigger.cancel": RequestCancelModalTrigger,
97+
"RequestActionModal.title.cancel": InvitationsRequestActionModalCancelTitle,
98+
"RequestActionButton.cancel": InvitationsRequestCancelButton,
7699
[`RequestStatus.layout.submitted`]: SubmitStatus,
77100
[`RequestStatus.layout.deleted`]: DeleteStatus,
78101
[`RequestStatus.layout.accepted`]: AcceptStatus,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React, { Component } from "react";
2+
import PropTypes from "prop-types";
3+
import { Button, Header, Icon, Segment } from "semantic-ui-react";
4+
import { withState } from "react-searchkit";
5+
import { i18next } from "@translations/invenio_communities/i18next";
6+
7+
class MembershipRequestsEmptyResultsCmp extends Component {
8+
render() {
9+
const { resetQuery, extraContent, queryString } = this.props;
10+
11+
return (
12+
<Segment.Group>
13+
<Segment placeholder textAlign="center">
14+
<Header icon>
15+
<Icon name="search" />
16+
{i18next.t("No matching members found.")}
17+
</Header>
18+
{queryString && (
19+
<p>
20+
<em>
21+
{i18next.t("Current search")} "{queryString}"
22+
</em>
23+
</p>
24+
)}
25+
<Button primary onClick={() => resetQuery()}>
26+
{i18next.t("Clear query")}
27+
</Button>
28+
{extraContent}
29+
</Segment>
30+
</Segment.Group>
31+
);
32+
}
33+
}
34+
35+
MembershipRequestsEmptyResultsCmp.propTypes = {
36+
resetQuery: PropTypes.func.isRequired,
37+
queryString: PropTypes.string.isRequired,
38+
extraContent: PropTypes.node,
39+
};
40+
41+
MembershipRequestsEmptyResultsCmp.defaultProps = {
42+
extraContent: null,
43+
};
44+
45+
export const MembershipRequestsEmptyResults = withState(
46+
MembershipRequestsEmptyResultsCmp
47+
);

0 commit comments

Comments
 (0)