Skip to content

Commit 863af9c

Browse files
committed
members: [inveniosoftware#855] 5) implement 'wait for decision' flow in Members tab[+]
- Most significant contribution here is the proper link(s) serialization for Members. This affects invitations and membership requests.
1 parent dbf1ce4 commit 863af9c

25 files changed

+971
-15
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+
};

Diff for: 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
@@ -0,0 +1,59 @@
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 { Input } from "semantic-ui-react";
12+
import { i18next } from "@translations/invenio_communities/i18next";
13+
import PropTypes from "prop-types";
14+
15+
export const MemberRequestsSearchBarElement = ({
16+
onBtnSearchClick,
17+
onInputChange,
18+
onKeyPress,
19+
queryString,
20+
uiProps,
21+
className,
22+
placeholder,
23+
}) => {
24+
return (
25+
<Input
26+
className={className}
27+
action={{
28+
icon: "search",
29+
onClick: onBtnSearchClick,
30+
className: "search",
31+
title: i18next.t("Search"),
32+
}}
33+
fluid
34+
placeholder={placeholder}
35+
onChange={(_, { value }) => {
36+
onInputChange(value);
37+
}}
38+
value={queryString}
39+
onKeyPress={onKeyPress}
40+
{...uiProps}
41+
/>
42+
);
43+
};
44+
45+
MemberRequestsSearchBarElement.propTypes = {
46+
onBtnSearchClick: PropTypes.func.isRequired,
47+
onInputChange: PropTypes.func.isRequired,
48+
onKeyPress: PropTypes.func.isRequired,
49+
queryString: PropTypes.string.isRequired,
50+
uiProps: PropTypes.object,
51+
className: PropTypes.string,
52+
placeholder: PropTypes.string,
53+
};
54+
55+
MemberRequestsSearchBarElement.defaultProps = {
56+
uiProps: null,
57+
className: "",
58+
placeholder: "",
59+
};
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+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 { RequestActionController } from "@js/invenio_requests/request/actions/RequestActionController";
11+
import RequestStatus from "@js/invenio_requests/request/RequestStatus";
12+
import { i18next } from "@translations/invenio_communities/i18next";
13+
import { DateTime } from "luxon";
14+
import PropTypes from "prop-types";
15+
import React, { Component } from "react";
16+
import { Image } from "react-invenio-forms";
17+
import { Grid, Item, Table } from "semantic-ui-react";
18+
19+
import { RoleDropdown } from "../components/dropdowns";
20+
21+
const formattedTime = (expiresAt) =>
22+
DateTime.fromISO(expiresAt).setLocale(i18next.language).toRelative();
23+
24+
export class MembershipRequestsResultItem extends Component {
25+
constructor(props) {
26+
super(props);
27+
const { result } = this.props;
28+
this.state = { membershipRequest: result };
29+
}
30+
31+
update = (data, value) => {
32+
const { membershipRequest } = this.state;
33+
this.setState({ membershipRequest: { ...membershipRequest, ...{ role: value } } });
34+
};
35+
36+
actionSuccessCallback = () => undefined;
37+
38+
render() {
39+
const {
40+
config: { rolesCanAssign },
41+
community,
42+
} = this.props;
43+
44+
const {
45+
membershipRequest: { member, request },
46+
membershipRequest,
47+
} = this.state;
48+
// TODO: Decision flow
49+
// const { api: membershipRequestsApi } = this.context;
50+
const rolesCanAssignByType = rolesCanAssign[member.type];
51+
const membershipRequestExpiration = formattedTime(request.expires_at);
52+
return (
53+
<Table.Row className="community-member-item">
54+
<Table.Cell>
55+
<Grid textAlign="left" verticalAlign="middle">
56+
<Grid.Column>
57+
<Item className="flex align-items-center" key={membershipRequest.id}>
58+
<Image src={member.avatar} avatar circular className="mr-10" />
59+
<Item.Content>
60+
<Item.Header size="small" as="b">
61+
<a href={`/communities/${community.slug}/requests/${request.id}`}>
62+
{member.name}
63+
</a>
64+
</Item.Header>
65+
{member.description && (
66+
<Item.Meta>
67+
<div className="truncate-lines-1">{member.description}</div>
68+
</Item.Meta>
69+
)}
70+
</Item.Content>
71+
</Item>
72+
</Grid.Column>
73+
</Grid>
74+
</Table.Cell>
75+
<Table.Cell data-label={i18next.t("Status")}>
76+
<RequestStatus status={request.status} />
77+
</Table.Cell>
78+
<Table.Cell
79+
aria-label={i18next.t("Expires") + " " + membershipRequestExpiration}
80+
data-label={i18next.t("Expires")}
81+
>
82+
{membershipRequestExpiration}
83+
</Table.Cell>
84+
<Table.Cell data-label={i18next.t("Role")}>
85+
<RoleDropdown
86+
roles={rolesCanAssignByType}
87+
successCallback={this.update}
88+
// TODO: Decision flow
89+
// action={membershipRequestsApi.updateRole}
90+
disabled={!membershipRequest.permissions.can_update_role}
91+
currentValue={membershipRequest.role}
92+
resource={membershipRequest}
93+
label={i18next.t("Role") + " " + membershipRequest.role}
94+
/>
95+
</Table.Cell>
96+
<Table.Cell data-label={i18next.t("Actions")}>
97+
<RequestActionController
98+
request={membershipRequest}
99+
// TODO: Decision flow
100+
actionSuccessCallback={() => console.log("actionSuccessCallback called")}
101+
/>
102+
</Table.Cell>
103+
</Table.Row>
104+
);
105+
}
106+
}
107+
108+
MembershipRequestsResultItem.propTypes = {
109+
result: PropTypes.object.isRequired,
110+
config: PropTypes.object.isRequired,
111+
community: PropTypes.object.isRequired,
112+
};
113+
114+
MembershipRequestsResultItem.defaultProps = {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 { i18next } from "@translations/invenio_communities/i18next";
11+
import PropTypes from "prop-types";
12+
import React from "react";
13+
import { Table } from "semantic-ui-react";
14+
15+
export const MembershipRequestsResultsContainer = ({ results }) => {
16+
return (
17+
<Table>
18+
<Table.Header>
19+
<Table.Row>
20+
<Table.HeaderCell width={5}>{i18next.t("Name")}</Table.HeaderCell>
21+
<Table.HeaderCell width={2}>{i18next.t("Status")}</Table.HeaderCell>
22+
<Table.HeaderCell width={3}>{i18next.t("Expires")}</Table.HeaderCell>
23+
<Table.HeaderCell width={3}>{i18next.t("Role")}</Table.HeaderCell>
24+
<Table.HeaderCell width={3}>{i18next.t("Actions")}</Table.HeaderCell>
25+
</Table.Row>
26+
</Table.Header>
27+
<Table.Body>{results}</Table.Body>
28+
</Table>
29+
);
30+
};
31+
32+
MembershipRequestsResultsContainer.propTypes = {
33+
results: PropTypes.array.isRequired,
34+
};
35+
36+
MembershipRequestsResultsContainer.defaultProps = {};

0 commit comments

Comments
 (0)