Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#855] 5-6) wait, decision, expiration, notification flows #1175

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// This file is part of Invenio-communities
// Copyright (C) 2022 CERN.
// Copyright (C) 2024 Northwestern University.
//
// Invenio-communities is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.

import { CommunityMembershipRequestsApi } from "./api";
import React, { Component } from "react";
import PropTypes from "prop-types";

export const MembershipRequestsContext = React.createContext({ api: undefined });

export class MembershipRequestsContextProvider extends Component {
constructor(props) {
super(props);
const { community } = props;
this.apiClient = new CommunityMembershipRequestsApi(community);
}
render() {
const { children } = this.props;
return (
<MembershipRequestsContext.Provider value={{ api: this.apiClient }}>
{children}
</MembershipRequestsContext.Provider>
);
}
}

MembershipRequestsContextProvider.propTypes = {
community: PropTypes.object.isRequired,
children: PropTypes.node.isRequired,
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
// Invenio-communities is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.

import { CommunityLinksExtractor } from "../CommunityLinksExtractor";
import { http } from "react-invenio-forms";

import { CommunityLinksExtractor } from "../CommunityLinksExtractor";
import { bulkMembersSerializer } from "../serializers";

/**
* API Client for community membership requests.
*
Expand All @@ -21,6 +23,14 @@ export class CommunityMembershipRequestsApi {
}

requestMembership = async (payload) => {
// assigned rather than defiend for ease of passing as callback
return await http.post(this.linksExtractor.url("membership_requests"), payload);
};

updateRole = async (membershipRequest, role) => {
// assigned rather than defiend for ease of passing as callback
const memberSerialized = bulkMembersSerializer([membershipRequest]);
const payload = { members: memberSerialized, role: role };
return await http.put(this.linksExtractor.url("membership_requests"), payload);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export class Filters {
return { ...rolesFilters, ...statusFilters };
}

getMembershipRequestFilters() {
const statusFilters = this.getStatus();
const rolesFilters = this.getRoles();
return { ...rolesFilters, ...statusFilters };
}

getMembersFilters() {
const visibilityFilters = this.getVisibility();
const rolesFilters = this.getRoles();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* This file is part of Invenio.
* Copyright (C) 2022 CERN.
* Copyright (C) 2024 Northwestern University.
*
* Invenio is free software; you can redistribute it and/or modify it
* under the terms of the MIT License; see LICENSE file for more details.
*/

import React from "react";
import { Grid } from "semantic-ui-react";
import { ResultsPerPage, Pagination, ResultsList } from "react-searchkit";
import PropTypes from "prop-types";
import { Trans } from "react-i18next";

export const MemberRequestsResults = ({ paginationOptions, currentResultsState }) => {
const { total } = currentResultsState.data;
return (
total && (
<Grid>
<Grid.Row>
<Grid.Column width={16}>
<ResultsList />
</Grid.Column>
</Grid.Row>
<Grid.Row verticalAlign="middle">
<Grid.Column width={8} textAlign="right">
<Pagination
options={{
size: "mini",
showFirst: false,
showLast: false,
}}
/>
</Grid.Column>
<Grid.Column textAlign="right" width={8}>
<ResultsPerPage
values={paginationOptions.resultsPerPage}
label={(cmp) => (
// kept key for translation purposes - it should be
// the same across members, invitations, membership requests
// and beyond
<Trans key="communitiesInvitationsResult" count={cmp}>
{cmp} results per page
</Trans>
)}
/>
</Grid.Column>
</Grid.Row>
</Grid>
)
);
};

MemberRequestsResults.propTypes = {
paginationOptions: PropTypes.object.isRequired,
currentResultsState: PropTypes.object.isRequired,
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* This file is part of Invenio.
* Copyright (C) 2022 CERN.
* Copyright (C) 2024 Northwestern University.
*
* Invenio is free software; you can redistribute it and/or modify it
* under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -11,24 +12,26 @@ import { Input } from "semantic-ui-react";
import { i18next } from "@translations/invenio_communities/i18next";
import PropTypes from "prop-types";

export const InvitationsSearchBarElement = ({
export const MemberRequestsSearchBarElement = ({
onBtnSearchClick,
onInputChange,
onKeyPress,
queryString,
uiProps,
className,
placeholder,
}) => {
return (
<Input
className="invitation-searchbar"
className={className}
action={{
icon: "search",
onClick: onBtnSearchClick,
className: "search",
title: i18next.t("Search"),
}}
fluid
placeholder={i18next.t("Search in invitations...")}
placeholder={placeholder}
onChange={(_, { value }) => {
onInputChange(value);
}}
Expand All @@ -39,14 +42,18 @@ export const InvitationsSearchBarElement = ({
);
};

InvitationsSearchBarElement.propTypes = {
MemberRequestsSearchBarElement.propTypes = {
onBtnSearchClick: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
queryString: PropTypes.string.isRequired,
uiProps: PropTypes.object,
className: PropTypes.string,
placeholder: PropTypes.string,
};

InvitationsSearchBarElement.defaultProps = {
MemberRequestsSearchBarElement.defaultProps = {
uiProps: null,
className: "",
placeholder: "",
};
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
/*
* This file is part of Invenio.
* Copyright (C) 2022 CERN.
* Copyright (C) 2024 Northwestern University.
*
* Invenio is free software; you can redistribute it and/or modify it
* under the terms of the MIT License; see LICENSE file for more details.
*/

import { RequestActionController } from "@js/invenio_requests/request/actions/RequestActionController";
import { i18next } from "@translations/invenio_communities/i18next";
import { DateTime } from "luxon";
import PropTypes from "prop-types";
import React, { Component } from "react";
import { Image } from "react-invenio-forms";
import { Container, Grid, Item, Table } from "semantic-ui-react";
import { InvitationsContext } from "../../api/invitations/InvitationsContextProvider";
import { RoleDropdown } from "../components/dropdowns";
import { buildRequest, formattedTime } from "../utils";
import RequestStatus from "@js/invenio_requests/request/RequestStatus";

const formattedTime = (expiresAt) =>
DateTime.fromISO(expiresAt).setLocale(i18next.language).toRelative();

export class InvitationResultItem extends Component {
constructor(props) {
super(props);
Expand All @@ -39,12 +38,13 @@ export class InvitationResultItem extends Component {
community,
} = this.props;
const {
invitation: { member, request },
invitation: { member },
invitation,
} = this.state;
const request = buildRequest(invitation, ["cancel"]);
const { api: invitationsApi } = this.context;
const rolesCanInviteByType = rolesCanInvite[member.type];
const memberInvitationExpiration = formattedTime(request.expires_at);
const expiration = formattedTime(request.expires_at);
return (
<Table.Row className="community-member-item">
<Table.Cell>
Expand Down Expand Up @@ -72,10 +72,10 @@ export class InvitationResultItem extends Component {
<RequestStatus status={request.status} />
</Table.Cell>
<Table.Cell
aria-label={i18next.t("Expires") + " " + memberInvitationExpiration}
aria-label={i18next.t("Expires") + " " + expiration}
data-label={i18next.t("Expires")}
>
{memberInvitationExpiration}
{expiration}
</Table.Cell>
<Table.Cell data-label={i18next.t("Role")}>
<RoleDropdown
Expand All @@ -90,13 +90,12 @@ export class InvitationResultItem extends Component {
</Table.Cell>
<Table.Cell>
<Container fluid textAlign="right">
{/* TODO uncomment when links available in the request resource subschema */}
{/*<RequestActionController*/}
{/* request={request }*/}
{/* actionSuccessCallback={this.updateInvitation}*/}
{/*>*/}
{/*<ActionButtons request={invitation} />*/}
{/*</RequestActionController>*/}
<RequestActionController
request={request}
actionSuccessCallback={() => {
window.location.reload();
}}
/>
</Container>
</Table.Cell>
</Table.Row>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
/*
* This file is part of Invenio.
* Copyright (C) 2022 CERN.
* Copyright (C) 2024 Northwestern University.
*
* Invenio is free software; you can redistribute it and/or modify it
* under the terms of the MIT License; see LICENSE file for more details.
*/

import { createSearchAppInit } from "@js/invenio_search_ui";
import { parametrize, overrideStore } from "react-overridable";
import { DropdownSort } from "@js/invenio_search_ui/components";
import { InvitationsContextProvider as ContextProvider } from "../../api/invitations/InvitationsContextProvider";
import { InvitationResultItem } from "./InvitationResultItem";
import { InvitationsResults } from "./InvitationsResults";
import { InvitationsResultsContainer } from "./InvitationsResultsContainer";
import { InvitationsSearchBarElement } from "./InvitationsSearchBarElement";
import { InvitationsSearchLayout } from "./InvitationsSearchLayout";
import { InvitationsEmptyResults } from "./InvitationsEmptyResults";
import { RequestCancelButton } from "@js/invenio_requests/components/Buttons";
import { RequestCancelModalTrigger } from "@js/invenio_requests/components/ModalTriggers";
import {
SubmitStatus,
DeleteStatus,
Expand All @@ -24,6 +17,20 @@ import {
CancelStatus,
ExpireStatus,
} from "@js/invenio_requests/request";
import { createSearchAppInit } from "@js/invenio_search_ui";
import { DropdownSort } from "@js/invenio_search_ui/components";
import { i18next } from "@translations/invenio_communities/i18next";
import React from "react";
import { Trans } from "react-i18next";
import { parametrize, overrideStore } from "react-overridable";

import { InvitationsContextProvider as ContextProvider } from "../../api/invitations/InvitationsContextProvider";
import { MemberRequestsSearchBarElement } from "../MemberRequestsSearchBarElement";
import { InvitationsEmptyResults } from "./InvitationsEmptyResults";
import { InvitationResultItem } from "./InvitationResultItem";
import { InvitationsResults } from "./InvitationsResults";
import { InvitationsResultsContainer } from "./InvitationsResultsContainer";
import { InvitationsSearchLayout } from "./InvitationsSearchLayout";

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

const InvitationsSearchBarElement = parametrize(MemberRequestsSearchBarElement, {
className: "invitation-searchbar",
placeholder: i18next.t("Search in invitations..."),
});

const InvitationsContextProvider = parametrize(ContextProvider, {
community: community,
});
Expand All @@ -65,14 +77,25 @@ const InvitationsEmptyResultsWithCommunity = parametrize(InvitationsEmptyResults
rolesCanInvite: communitiesRolesCanInvite,
});

const InvitationsRequestCancelButton = parametrize(RequestCancelButton, {
content: i18next.t("Cancel invitation"),
});

const InvitationsRequestActionModalCancelTitle = (props) => {
return <Trans defaults="{{action}} invitation" values={{ action: "cancel" }} />;
};

const defaultComponents = {
[`${appName}.EmptyResults.element`]: InvitationsEmptyResultsWithCommunity,
[`${appName}.ResultsList.item`]: InvitationResultItemWithConfig,
[`${appName}.SearchApp.layout`]: InvitationsSearchLayoutWithConfig,
[`${appName}.SearchBar.element`]: InvitationsSearchBarElement,
[`${appName}.SearchApp.results`]: InvitationsResults,
[`${appName}.ResultsList.container`]: InvitationsResultsContainerWithConfig,
[`${appName}.Sort.element`]: DropdownSort,
[`${appName}.ResultsList.container`]: InvitationsResultsContainerWithConfig,
[`${appName}.SearchApp.results`]: InvitationsResults,
[`${appName}.ResultsList.item`]: InvitationResultItemWithConfig,
"RequestActionModalTrigger.cancel": RequestCancelModalTrigger,
"RequestActionModal.title.cancel": InvitationsRequestActionModalCancelTitle,
"RequestActionButton.cancel": InvitationsRequestCancelButton,
[`RequestStatus.layout.submitted`]: SubmitStatus,
[`RequestStatus.layout.deleted`]: DeleteStatus,
[`RequestStatus.layout.accepted`]: AcceptStatus,
Expand Down
Loading
Loading