Skip to content

Commit

Permalink
membership-request [inveniosoftware#855]: implement wait for decision…
Browse files Browse the repository at this point in the history
… flow
  • Loading branch information
fenekku committed Jul 22, 2024
1 parent ed95004 commit 67a28b4
Show file tree
Hide file tree
Showing 39 changed files with 1,264 additions and 96 deletions.
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 @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Button, Header, Icon, Segment } from "semantic-ui-react";
import { withState } from "react-searchkit";
import { i18next } from "@translations/invenio_communities/i18next";

class MembershipRequestsEmptyResultsCmp extends Component {
render() {
const { resetQuery, extraContent, queryString } = this.props;

return (
<Segment.Group>
<Segment placeholder textAlign="center">
<Header icon>
<Icon name="search" />
{i18next.t("No matching members found.")}
</Header>
{queryString && (
<p>
<em>
{i18next.t("Current search")} "{queryString}"
</em>
</p>
)}
<Button primary onClick={() => resetQuery()}>
{i18next.t("Clear query")}
</Button>
{extraContent}
</Segment>
</Segment.Group>
);
}
}

MembershipRequestsEmptyResultsCmp.propTypes = {
resetQuery: PropTypes.func.isRequired,
queryString: PropTypes.string.isRequired,
extraContent: PropTypes.node,
};

MembershipRequestsEmptyResultsCmp.defaultProps = {
extraContent: null,
};

export const MembershipRequestsEmptyResults = withState(
MembershipRequestsEmptyResultsCmp
);
Loading

0 comments on commit 67a28b4

Please sign in to comment.