diff --git a/invenio_communities/administration/communities.py b/invenio_communities/administration/communities.py index 80f73ac61..230db26fe 100644 --- a/invenio_communities/administration/communities.py +++ b/invenio_communities/administration/communities.py @@ -7,11 +7,14 @@ # details. """Invenio administration OAI-PMH view module.""" -from flask import app, current_app, has_app_context +from functools import partial + +from flask import current_app from invenio_administration.views.base import ( AdminResourceDetailView, AdminResourceListView, ) +from invenio_search_ui.searchconfig import search_app_config from invenio_communities.communities.schema import CommunityFeaturedSchema @@ -36,15 +39,12 @@ class CommunityListView(AdminResourceListView): display_edit = False item_field_list = { - "slug": { - "text": "Slug", - "order": 1, - }, - "metadata.title": {"text": "Title", "order": 2}, + "slug": {"text": "Slug", "order": 1, "width": 1}, + "metadata.title": {"text": "Title", "order": 2, "width": 4}, # This field is for display only, it won't work on forms - "ui.type.title_l10n": {"text": "Type", "order": 3}, - "featured": {"text": "Featured", "order": 4}, - "created": {"text": "Created", "order": 5}, + "ui.type.title_l10n": {"text": "Type", "order": 3, "width": 2}, + "featured": {"text": "Featured", "order": 4, "width": 1}, + "created": {"text": "Created", "order": 5, "width": 2}, } actions = { @@ -52,12 +52,41 @@ class CommunityListView(AdminResourceListView): "text": "Feature", "payload_schema": CommunityFeaturedSchema, "order": 1, - } + }, + # custom components in the UI + "delete": { + "text": "Delete", + "payload_schema": None, + "order": 2, + }, + # custom components in the UI + "restore": { + "text": "Restore", + "payload_schema": None, + "order": 2, + }, } search_config_name = "COMMUNITIES_SEARCH" search_facets_config_name = "COMMUNITIES_FACETS" search_sort_config_name = "COMMUNITIES_SORT_OPTIONS" + def init_search_config(self): + """Build search view config.""" + return partial( + search_app_config, + config_name=self.get_search_app_name(), + available_facets=current_app.config.get(self.search_facets_config_name), + sort_options=current_app.config[self.search_sort_config_name], + endpoint=self.get_api_endpoint(), + headers=self.get_search_request_headers(), + initial_filters=[["status", "P"]], + hidden_params=[ + ["include_deleted", "1"], + ], + page=1, + size=30, + ) + @staticmethod def disabled(): """Disable the view on demand.""" diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api.js deleted file mode 100644 index 22842a102..000000000 --- a/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api.js +++ /dev/null @@ -1,9 +0,0 @@ -import { http } from "react-invenio-forms"; - -const getFeatured = async (apiEndpoint) => { - return await http.get(apiEndpoint); -}; - -export const InvenioAdministrationCommunitiesApi = { - getFeatured: getFeatured, -}; diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api/api.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api/api.js new file mode 100644 index 000000000..56953fd71 --- /dev/null +++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api/api.js @@ -0,0 +1,26 @@ +import { APIRoutes } from "./routes"; +import { http } from "react-invenio-forms"; + +const getFeatured = async (apiEndpoint) => { + return await http.get(apiEndpoint); +}; + +const deleteCommunity = async (community, payload) => { + const reason = payload["removal_reason"]; + payload["removal_reason"] = { id: reason }; + // WARNING: Axios does not accept payload without data key + return await http.delete(APIRoutes.delete(community), { + data: { ...payload }, + headers: { ...http.headers, if_match: community.revision_id }, + }); +}; + +const restore = async (record) => { + return await http.post(APIRoutes.restore(record)); +}; + +export const InvenioAdministrationCommunitiesApi = { + getFeatured: getFeatured, + delete: deleteCommunity, + restore: restore, +}; diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api/index.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api/index.js new file mode 100644 index 000000000..d3dde92ae --- /dev/null +++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api/index.js @@ -0,0 +1 @@ +export { InvenioAdministrationCommunitiesApi } from "./api"; diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api/routes.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api/routes.js new file mode 100644 index 000000000..bd53019f5 --- /dev/null +++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/api/routes.js @@ -0,0 +1,14 @@ +import _get from "lodash/get"; + +const APIRoutesGenerators = { + delete: (record, idKeyPath = "id") => { + return `/api/communities/${_get(record, idKeyPath)}`; + }, + restore: (record, idKeyPath = "id") => { + return `/api/communities/${_get(record, idKeyPath)}/restore`; + }, +}; + +export const APIRoutes = { + ...APIRoutesGenerators, +}; diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RecordResourceActions.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RecordResourceActions.js new file mode 100644 index 000000000..685762d5c --- /dev/null +++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RecordResourceActions.js @@ -0,0 +1,165 @@ +/* + * // This file is part of Invenio-App-Rdm + * // Copyright (C) 2023 CERN. + * // + * // Invenio-App-Rdm 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 { RestoreConfirmation } from "./RestoreConfirmation"; +import TombstoneForm from "./TombstoneForm"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Button, Modal, Icon } from "semantic-ui-react"; +import { ActionModal, ActionForm } from "@js/invenio_administration"; +import _isEmpty from "lodash/isEmpty"; +import { i18next } from "@translations/invenio_app_rdm/i18next"; + +export class RecordResourceActions extends Component { + constructor(props) { + super(props); + this.state = { + modalOpen: false, + modalHeader: undefined, + modalBody: undefined, + }; + } + + onModalTriggerClick = (e, { payloadSchema, dataName, dataActionKey }) => { + const { resource } = this.props; + + if (dataActionKey === "delete") { + this.setState({ + modalOpen: true, + modalHeader: i18next.t("Delete community"), + modalBody: ( + + ), + }); + } else if (dataActionKey === "restore") { + this.setState({ + modalOpen: true, + modalHeader: i18next.t("Restore community"), + modalBody: ( + + ), + }); + } else { + this.setState({ + modalOpen: true, + modalHeader: dataName, + modalBody: ( + + ), + }); + } + }; + + closeModal = () => { + this.setState({ + modalOpen: false, + modalHeader: undefined, + modalBody: undefined, + }); + }; + + handleSuccess = () => { + const { successCallback } = this.props; + this.setState({ + modalOpen: false, + modalHeader: undefined, + modalBody: undefined, + }); + successCallback(); + }; + + render() { + const { actions, Element, resource } = this.props; + const { modalOpen, modalHeader, modalBody } = this.state; + return ( + <> + {Object.entries(actions).map(([actionKey, actionConfig]) => { + if (actionKey === "delete" && !resource.deletion_status.is_deleted) { + return ( + + + {actionConfig.text} + + ); + } else if (actionKey === "restore" && resource.deletion_status.is_deleted) { + return ( + + + {actionConfig.text} + + ); + } else if (!resource.deletion_status.is_deleted && actionKey !== "restore") { + return ( + + {actionConfig.text} + + ); + } + })} + + {modalHeader && {modalHeader}} + {!_isEmpty(modalBody) && modalBody} + + + ); + } +} + +RecordResourceActions.propTypes = { + resource: PropTypes.object.isRequired, + successCallback: PropTypes.func.isRequired, + actions: PropTypes.shape({ + text: PropTypes.string.isRequired, + payload_schema: PropTypes.object.isRequired, + order: PropTypes.number.isRequired, + }), + Element: PropTypes.node, +}; + +RecordResourceActions.defaultProps = { + Element: Button, + actions: undefined, +}; diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RemovalReasonsSelect.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RemovalReasonsSelect.js new file mode 100644 index 000000000..3eac9aef4 --- /dev/null +++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RemovalReasonsSelect.js @@ -0,0 +1,85 @@ +/* + * // This file is part of Invenio-App-Rdm + * // Copyright (C) 2023 CERN. + * // + * // Invenio-App-Rdm 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, { Component } from "react"; +import PropTypes from "prop-types"; +import { Dropdown, http, withCancel } from "react-invenio-forms"; +import { Dropdown as SUIDropdown } from "semantic-ui-react"; +import { i18next } from "@translations/invenio_app_rdm/i18next"; + +export default class RemovalReasonsSelect extends Component { + constructor(props) { + super(props); + this.state = { + options: undefined, + loading: false, + defaultOpt: undefined, + }; + } + + componentDidMount() { + this.fetchOptions(); + } + + componentWillUnmount() { + this.cancellableAction && this.cancellableAction.cancel(); + } + + fetchOptions = async () => { + const { setFieldValue } = this.props; + this.setState({ loading: true }); + const url = "/api/vocabularies/removalreasons"; + this.cancellableAction = withCancel( + http.get(url, { + headers: { + Accept: "application/vnd.inveniordm.v1+json", + }, + }) + ); + try { + const response = await this.cancellableAction.promise; + const options = response.data.hits.hits; + + const defaultOpt = options + ? { text: options[0].title_l10n, value: options[0].id, key: options[0].id } + : {}; + this.setState({ + options: options, + loading: false, + defaultOpt: defaultOpt, + }); + setFieldValue("removal_reason", defaultOpt.value); + } catch (e) { + this.setState({ loading: false }); + console.error(e); + } + }; + + render() { + const { loading, options, defaultOpt } = this.state; + + if (loading) { + return ; + } + + return ( + + ); + } +} + +RemovalReasonsSelect.propTypes = { + setFieldValue: PropTypes.func.isRequired, +}; diff --git a/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RestoreConfirmation.js b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RestoreConfirmation.js new file mode 100644 index 000000000..6adaaa2ab --- /dev/null +++ b/invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RestoreConfirmation.js @@ -0,0 +1,110 @@ +/* + * // This file is part of Invenio-Communities + * // Copyright (C) 2023 CERN. + * // + * // Invenio-App-Rdm 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 { InvenioAdministrationCommunitiesApi } from "../api"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { NotificationContext } from "@js/invenio_administration"; +import { withCancel, ErrorMessage } from "react-invenio-forms"; +import { Button, Modal } from "semantic-ui-react"; +import { i18next } from "@translations/invenio_app_rdm/i18next"; + +export class RestoreConfirmation extends Component { + constructor(props) { + super(props); + this.state = { + loading: false, + error: undefined, + }; + } + + componentWillUnmount() { + this.cancellableAction && this.cancellableAction.cancel(); + } + + static contextType = NotificationContext; + + handleSubmit = async (values) => { + const { addNotification } = this.context; + const { resource, actionSuccessCallback } = this.props; + + this.setState({ loading: true }); + + this.cancellableAction = withCancel( + InvenioAdministrationCommunitiesApi.restore(resource) + ); + + try { + await this.cancellableAction.promise; + this.setState({ loading: false, error: undefined }); + addNotification({ + title: i18next.t("Success"), + content: i18next.t("Community {{id}} was restored.", { id: resource.slug }), + type: "success", + }); + actionSuccessCallback(); + } catch (error) { + if (error === "UNMOUNTED") return; + + this.setState({ + error: error?.response?.data?.message || error?.message, + loading: false, + }); + console.error(error); + } + }; + handleModalClose = () => { + const { actionCancelCallback } = this.props; + actionCancelCallback(); + }; + + render() { + const { error, loading } = this.state; + const { resource } = this.props; + return ( + <> + {error && ( + + )} + + {i18next.t("Are you sure you want to restore community #{{id}}", { + id: resource.slug, + })} + "{resource.metadata.title}" ? + + + + + + + {!values.is_visible && isPublic && ( + + + {i18next.t( + "The tombstone is set to hidden but your record is public. Best practice is to provide a public tombstone when deactivating public records." + )} + + )} + {values.is_visible && !isPublic && ( + + + {i18next.t( + "RISK INFORMATION LEAKAGE: The tombstone is set to public but your record is restricted. Please make sure no restricted information is shared in the tombstone below." + )} + + )} +