From 4a1557526d9a44cf66d84d2d28978ec7362255f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C4=8Corov=C4=8D=C3=A1k?= Date: Tue, 30 Jan 2024 12:57:12 +0100 Subject: [PATCH] Add RecordRequests React project and initial files --- .../oarepo_requests_ui/RecordRequests.jinja | 10 + oarepo_requests/ui/theme/__init__.py | 0 .../ui/theme/assets/semantic-ui/.gitkeep | 0 .../oarepo_requests_ui/custom-components.js | 16 + .../components/CreateRequestButtonGroup.jsx | 37 ++ .../components/CreateRequestModalContent.jsx | 58 ++ .../components/RecordRequests.jsx | 47 ++ .../components/RequestList.jsx | 56 ++ .../components/RequestListContainer.jsx | 62 ++ .../components/RequestModal.jsx | 315 +++++++++ .../components/RequestModalContent.jsx | 202 ++++++ .../components/common/DefaultView.jsx | 6 + .../common/ReadOnlyCustomFields.jsx | 89 +++ .../components/common/index.js | 3 + .../record-requests/components/index.js | 8 + .../contexts/RequestContext.jsx | 11 + .../record-requests/contexts/index.js | 1 + .../record-requests/dummy-record.js | 607 ++++++++++++++++++ .../record-requests/index.js | 17 + .../record-requests/types.d.ts | 106 +++ .../record-requests/utils/loader.js | 87 +++ .../record-requests/utils/objects.js | 8 + .../oarepo_requests/custom-components.less | 8 + .../oarepo_requests/definitions/sample.less | 50 ++ oarepo_requests/ui/theme/webpack.py | 19 + setup.cfg | 3 + 26 files changed, 1826 insertions(+) create mode 100644 oarepo_requests/ui/templates/semantic-ui/oarepo_requests_ui/RecordRequests.jinja create mode 100644 oarepo_requests/ui/theme/__init__.py create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/.gitkeep create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/custom-components.js create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/CreateRequestButtonGroup.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/CreateRequestModalContent.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RecordRequests.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestList.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestListContainer.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestModal.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestModalContent.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/DefaultView.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/ReadOnlyCustomFields.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/index.js create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/index.js create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/contexts/RequestContext.jsx create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/contexts/index.js create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/dummy-record.js create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/index.js create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/types.d.ts create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/utils/loader.js create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/utils/objects.js create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/less/oarepo_requests/custom-components.less create mode 100644 oarepo_requests/ui/theme/assets/semantic-ui/less/oarepo_requests/definitions/sample.less create mode 100644 oarepo_requests/ui/theme/webpack.py diff --git a/oarepo_requests/ui/templates/semantic-ui/oarepo_requests_ui/RecordRequests.jinja b/oarepo_requests/ui/templates/semantic-ui/oarepo_requests_ui/RecordRequests.jinja new file mode 100644 index 00000000..2b8ecd8c --- /dev/null +++ b/oarepo_requests/ui/templates/semantic-ui/oarepo_requests_ui/RecordRequests.jinja @@ -0,0 +1,10 @@ +{#def record #} + +{% set record = record | to_dict %} + +
+ +{%- block javascript %} +{{ super() }} +{{ webpack['oarepo_requests_ui_record_requests.js']}} +{%- endblock %} \ No newline at end of file diff --git a/oarepo_requests/ui/theme/__init__.py b/oarepo_requests/ui/theme/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/.gitkeep b/oarepo_requests/ui/theme/assets/semantic-ui/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/custom-components.js b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/custom-components.js new file mode 100644 index 00000000..9643553c --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/custom-components.js @@ -0,0 +1,16 @@ +/* +This is the registration file for custom components. The components should not be included here, +but only referenced. The sample component below can be used to start up working on your own custom +component. +*/ +/* + +In the sample-component.js you can define your own javascript functions etc for your custom component. +Then import the file here + +import "./sample-component.js" +*/ + +// This file will import the css templates for your custom components + +import "../../less/oarepo_requests_ui/custom-components.less"; \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/CreateRequestButtonGroup.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/CreateRequestButtonGroup.jsx new file mode 100644 index 00000000..dced49ee --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/CreateRequestButtonGroup.jsx @@ -0,0 +1,37 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; + +import { i18next } from "@translations/oarepo_requests_ui/i18next"; + +import Overridable from "react-overridable"; +import { List, Segment, Header, Button } from "semantic-ui-react"; + +import { RequestModal } from "./RequestModal"; + +/** + * @typedef {import("../types").Request} Request + * @typedef {import("../types").RequestType} RequestType + */ + +/** + * @param {{ requestTypes: RequestType[] }} props + */ +export const CreateRequestButtonGroup = ({ requestTypes }) => { + const createRequests = requestTypes.filter(requestType => requestType.links.actions?.create); + + return ( + +
{i18next.t("Create Request")}
+ + {createRequests.map((requestType) => ( + } + /> + ))} + +
+ ); +} \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/CreateRequestModalContent.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/CreateRequestModalContent.jsx new file mode 100644 index 00000000..225a6599 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/CreateRequestModalContent.jsx @@ -0,0 +1,58 @@ +import React, { useState, useRef, useEffect } from "react"; +import PropTypes from "prop-types"; + +import { i18next } from "@translations/oarepo_requests_ui/i18next"; +import { Dimmer, Loader, Segment, Modal, Button, Header, Icon, Grid, Input, List, Container, Message, Form, Divider } from "semantic-ui-react"; +import { useFormikContext } from "formik"; + +import _isFunction from "lodash/isFunction"; + +import { CustomFields } from "react-invenio-forms"; + +/** + * @typedef {import("../types").RequestType} RequestType + * @typedef {import("formik").FormikConfig} FormikConfig + */ + +/** @param {{ requestType: RequestType }} props */ +export const CreateRequestModalContent = ({ requestType, extraPreSubmitEvent }) => { + const payloadUI = requestType?.payload_ui; + + const { isSubmitting, isValid, handleSubmit } = useFormikContext(); + + const customSubmitHandler = (event) => { + _isFunction(extraPreSubmitEvent) && extraPreSubmitEvent(event); + handleSubmit(event); + } + + return ( + <> + {requestType?.description && +

+ {requestType.description} +

+ } + {payloadUI && + +
+ import(`@templates/custom_fields/${widget}.js`), + (widget) => import(`react-invenio-forms`) + ]} + fieldPathPrefix="payload" + /> +
+ } + + ); +} + +CreateRequestModalContent.propTypes = { + requestType: PropTypes.object.isRequired, + extraPreSubmitEvent: PropTypes.func +}; \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RecordRequests.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RecordRequests.jsx new file mode 100644 index 00000000..207ea898 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RecordRequests.jsx @@ -0,0 +1,47 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; + +import Overridable, { + OverridableContext, + overrideStore, +} from "react-overridable"; + +import { i18next } from "@translations/oarepo_requests_ui/i18next"; +import _sortBy from "lodash/sortBy"; + +import { List, Segment, Header, Button } from "semantic-ui-react"; + +import { CreateRequestButtonGroup, RequestListContainer } from "."; +import { RequestContextProvider } from "../contexts"; + +/** + * @typedef {import("../types").Request} Request + * @typedef {import("../types").RequestType} RequestType + */ + +export const RecordRequests = ({ record }) => { + /** @type {RequestType[]} */ + const requestTypes = record.request_types ?? []; + + const requestsState = useState(_sortBy(record.requests, ["status_code"]) ?? []); + + return ( + <> + {/* Context for app - to not reload page after RequestModal submit */} + {/* */} + + {requestTypes && ( + + )} + {record?.requests && ( + + )} + + {/* */} + + ); +} + +RecordRequests.propTypes = { + record: PropTypes.object.isRequired, +}; \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestList.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestList.jsx new file mode 100644 index 00000000..3646b557 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestList.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import { i18next } from "@translations/oarepo_requests_ui/i18next"; +import { List, Segment, SegmentGroup, Header, Button } from "semantic-ui-react"; +import _isEmpty from "lodash/isEmpty"; + +import { RequestModal } from "."; + +/** + * @typedef {import("../types").Request} Request + * @typedef {import("../types").RequestTypeEnum} RequestTypeEnum + */ + +/** + * @param {{ requests: Request[], requestModalType: RequestTypeEnum }} props + */ +export const RequestList = ({ requests, requestTypes, requestModalType }) => { + return ( + + {requests.map((request) => { + let modalType = requestModalType; + if (_isEmpty(requestModalType)) { + if ("submit" in request.links?.actions) { + modalType = "submit"; + } else if ("cancel" in request.links?.actions) { + modalType = "cancel"; + } + } + return ( + + {/* {request?.created && {new Date(request.created)?.toLocaleString("cs-CZ")}} */} + +
{request?.status ?? i18next.t("No status")}
+
{new Date(request.created)?.toLocaleString("cs-CZ")}
+
+ + {request.name} + {request.description} + + + } + /> + ) + })} +
+ ) +}; + +RequestList.propTypes = { + requests: PropTypes.array.isRequired, + requestTypes: PropTypes.array.isRequired, + requestModalType: PropTypes.oneOf(["create", "accept", "submit", "cancel"]) +}; \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestListContainer.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestListContainer.jsx new file mode 100644 index 00000000..0539cbdc --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestListContainer.jsx @@ -0,0 +1,62 @@ +import React, { useContext } from "react"; +import PropTypes from "prop-types"; + +import { i18next } from "@translations/oarepo_requests_ui/i18next"; +import { List, Segment, SegmentGroup, Header, Button } from "semantic-ui-react"; + +import { RequestList } from "."; +import { RequestContext } from "../contexts"; + +/** + * @typedef {import("../types").Request} Request + * @typedef {import("../types").RequestType} RequestType + */ +/** + * @param {{ requests: Request[], requestTypes: RequestType[] }} props + */ +export const RequestListContainer = ({ requestTypes }) => { + const TriggerButtonForRequest = ({ onClick, request }) => ( + + {/* + } /> + */} + + + {request.name} + + + {request.description} + + + + ); + + const [requests, setRequests] = useContext(RequestContext); + + let requestsToApprove = []; + let otherRequests = []; + for (const request of requests) { + if ("accept" in request.links?.actions) { + requestsToApprove.push(request); + } else { + otherRequests.push(request); + } + } + + const SegmentGroupOrEmpty = requestsToApprove.length > 0 && otherRequests.length > 0 ? SegmentGroup : <>; + + return ( + + +
{i18next.t("My Requests")}
+ +
+ {requestsToApprove.length > 0 && ( + +
{i18next.t("Requests to Approve")}
+ +
+ )} +
+ ); +}; diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestModal.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestModal.jsx new file mode 100644 index 00000000..07bcb2e5 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestModal.jsx @@ -0,0 +1,315 @@ +import React, { useState, useEffect, useContext, useRef } from "react"; +import PropTypes from "prop-types"; + +import { i18next } from "@translations/oarepo_requests_ui/i18next"; +import { Dimmer, Loader, Segment, Modal, Button, Header, Icon, Message, Confirm } from "semantic-ui-react"; +import { isEmpty } from "lodash"; + +import { useFormik, FormikContext } from "formik"; +import axios from "axios"; + +import { RequestModalContent, CreateRequestModalContent } from "."; +import { REQUEST_TYPE } from "../utils/objects"; +import { RequestContext } from "../contexts"; + +/** + * @typedef {import("../types").Request} Request + * @typedef {import("../types").RequestType} RequestType + * @typedef {import("../types").RequestTypeEnum} RequestTypeEnum + * @typedef {import("react").ReactElement} ReactElement + * @typedef {import("semantic-ui-react").ConfirmProps} ConfirmProps + */ + +function delay(t, val) { + return new Promise(resolve => setTimeout(resolve, t, val)); +} + +const mapPayloadUiToInitialValues = (payloadUi) => { + const initialValues = { payload: {} }; + payloadUi?.forEach(section => { + section.fields.forEach(field => { + initialValues.payload[field.field] = ""; + }); + }); + return initialValues; +}; + +/** @param {{ request: Request, requestTypes: RequestType[], requestModalType: RequestTypeEnum, isEventModal: boolean, triggerButton: ReactElement }} props */ +export const RequestModal = ({ request, requestTypes, requestModalType, isEventModal = false, triggerButton }) => { + const [modalOpen, setModalOpen] = useState(false); + const [error, setError] = useState(null); + + /** @type {[ConfirmProps, (props: ConfirmProps) => void]} */ + const [confirmDialogProps, setConfirmDialogProps] = useState({ + open: false, + content: i18next.t("Are you sure?"), + cancelButton: i18next.t("Cancel"), + confirmButton: i18next.t("OK"), + onCancel: () => setConfirmDialogProps(props => ({ ...props, open: false })), + onConfirm: () => setConfirmDialogProps(props => ({ ...props, open: false })) + }); + + const errorMessageRef = useRef(null); + + const [requests, setRequests] = useContext(RequestContext); + + const formik = useFormik({ + initialValues: !isEmpty(request?.payload) ? { payload: request.payload } : mapPayloadUiToInitialValues(request?.payload_ui), + onSubmit: (values) => { + console.log(values); + if (requestModalType === REQUEST_TYPE.SUBMIT) { + confirmAction(REQUEST_TYPE.SUBMIT); + } else if (requestModalType === REQUEST_TYPE.CREATE) { + confirmAction(REQUEST_TYPE.CREATE); + } + } + }); + + useEffect(() => { + if (error) { + errorMessageRef.current?.scrollIntoView({ behavior: "smooth" }); + } + }, [error]); + + const fetchRequests = () => { + axios({ + method: 'get', + url: request.links?.self.split('/').slice(0, -1).join('/'), // TODO: link for /api/{record}/requests ?? + headers: { 'Content-Type': 'application/json' } + }) + .then(response => { + console.log(response); + setRequests(response.data); + }) + .catch(error => { + console.log(error); + }); + } + + const callApi = async (url, method, doNotHandleResolve = false) => { + if (doNotHandleResolve) { + // return axios({ + // method: method, + // url: url, + // data: formik.values, + // headers: { 'Content-Type': 'application/json' } + // }); + return delay(1000) + } + // axios({ + // method: method, + // url: url, + // data: formik.values, + // headers: { 'Content-Type': 'application/json' } + // }) + return delay(1000) + .then(response => { + console.log(response); + setModalOpen(false); + requestModalType !== REQUEST_TYPE.CREATE && fetchRequests(); // TODO: Replace with state update + formik.resetForm(); + }) + .catch(error => { + console.log(error); + setError(error); + }) + .finally(() => { + formik.setSubmitting(false); + }); + } + + const createAndSubmitRequest = async () => { + try { + await callApi(request.links.actions?.create, 'post', true); + await callApi(request.links.actions?.submit, 'post', true); + setModalOpen(false); + formik.resetForm(); + } catch (error) { + console.log(error); + setError(error); + } finally { + formik.setSubmitting(false); + } + } + + const sendRequest = async (requestType, createAndSubmit = false) => { + formik.setSubmitting(true); + setError(null); + if (createAndSubmit) { + return createAndSubmitRequest(); + } + if (requestType === REQUEST_TYPE.SAVE) { + return callApi(request.links.self, 'put'); + } + return callApi(!isEventModal ? request.links.actions[requestType] : request.links[requestType], 'post'); + } + + const confirmAction = (requestType, createAndSubmit = false) => { + /** @type {ConfirmProps} */ + let newConfirmDialogProps = { + open: true, + onConfirm: () => { + setConfirmDialogProps(props => ({ ...props, open: false })); + sendRequest(requestType); + }, + onCancel: () => { + setConfirmDialogProps(props => ({ ...props, open: false })); + formik.setSubmitting(false); + } + }; + + switch (requestType) { + case REQUEST_TYPE.CREATE: + newConfirmDialogProps.header = i18next.t("Create request"); + break; + case REQUEST_TYPE.SUBMIT: + newConfirmDialogProps.header = i18next.t("Submit request"); + break; + case REQUEST_TYPE.CANCEL: + newConfirmDialogProps.header = i18next.t("Delete request"); + newConfirmDialogProps.confirmButton = ; + break; + case REQUEST_TYPE.ACCEPT: + newConfirmDialogProps.header = i18next.t("Accept request"); + newConfirmDialogProps.confirmButton = ; + break; + case REQUEST_TYPE.DECLINE: + newConfirmDialogProps.header = i18next.t("Decline request"); + newConfirmDialogProps.confirmButton = ; + break; + default: + break; + } + + if (createAndSubmit) { + console.log("createAndSubmit"); + newConfirmDialogProps = { + ...newConfirmDialogProps, + header: i18next.t("Create and submit request"), + confirmButton: , + onConfirm: () => { + setConfirmDialogProps(props => ({ ...props, open: false })); + sendRequest(REQUEST_TYPE.CREATE, createAndSubmit); + } + } + } + + setConfirmDialogProps(props => ({ ...props, ...newConfirmDialogProps })); + } + + const onClose = () => { + setModalOpen(false); + setError(null); + formik.resetForm(); + } + + const extraPreSubmitEvent = (event) => { + if (event?.nativeEvent?.submitter?.name === "create-and-submit-request") { + confirmAction(REQUEST_TYPE.SUBMIT, true); + } + } + + return ( + <> + setModalOpen(true)} + open={modalOpen} + trigger={triggerButton || + + + + } + {requestModalType === REQUEST_TYPE.CANCEL && + + } + {requestModalType === REQUEST_TYPE.ACCEPT && + <> + + + + } + {requestModalType === REQUEST_TYPE.CREATE && (!isEventModal && + <> + + + || + ) + } + + + + + + ); +}; + +RequestModal.propTypes = { + request: PropTypes.object.isRequired, + requestModalType: PropTypes.oneOf(["create", "submit", "cancel", "accept"]).isRequired, + requestTypes: PropTypes.array, + isEventModal: PropTypes.bool, + triggerButton: PropTypes.element, +}; \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestModalContent.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestModalContent.jsx new file mode 100644 index 00000000..77402780 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/RequestModalContent.jsx @@ -0,0 +1,202 @@ +import React, { useState, useEffect, useContext } from "react"; +import PropTypes from "prop-types"; + +import { i18next } from "@translations/oarepo_requests_ui/i18next"; +import { Dimmer, Loader, Segment, Modal, Button, Header, Icon, Grid, Input, List, Container, Form, Divider, Message, Comment } from "semantic-ui-react"; +import _isEmpty from "lodash/isEmpty"; +import _sortBy from "lodash/sortBy"; +import { useFormikContext } from "formik"; +import axios from "axios"; + +import { CustomFields } from "react-invenio-forms"; + +import { RequestModal } from "."; +import { RequestContext } from "../contexts"; +import { REQUEST_TYPE } from "../utils/objects"; +import ReadOnlyCustomFields from "./common/ReadOnlyCustomFields"; + +/** + * @typedef {import("../types").Request} Request + * @typedef {import("../types").RequestType} RequestType + * @typedef {import("../types").RequestTypeEnum} RequestTypeEnum + * @typedef {import("../types").Event} Event + */ + +/** @param {{ request: Request, requestType: RequestType, isSidebar: boolean }} props */ +const RequestSideInfo = ({ request, requestType, isSidebar = false }) => { + return ( + + + + {i18next.t("Creator")} + {request.created_by?.link && {request.created_by.label} || request.created_by?.label} + + + + + {i18next.t("Receiver")} + {request.receiver?.link && {request.receiver?.label} || request.receiver?.label} + + + + + {i18next.t("Request type")} + {requestType.name} + + + + + {i18next.t("Created")} + {`${Math.ceil(Math.abs(new Date(request?.created) - new Date()) / 3.6e6)} hours ago`} + + + + ) +}; + +/** @param {{ request: Request, requestModalType: RequestTypeEnum, requestTypes: RequestType[] }} props */ +export const RequestModalContent = ({ request, requestTypes, requestModalType }) => { + /** @type {[Request[], (requests: Request[]) => void]} */ + const [requests, setRequests] = useContext(RequestContext); + + useEffect(() => { + axios + .get(request.links?.events, { headers: { 'Content-Type': 'application/json' } }) + .then(response => { + setRequests(requests => requests.map(req => { + if (req.uuid === request.uuid) { + req.events = response.data; + } + return req; + })); + }) + .catch(error => { + console.log(error); + }); + }, []); + + const actualRequest = requests.find(req => req.uuid === request.uuid); + + const requestType = requestTypes.find(requestType => requestType.id === request.type); + const payloadUI = requestType?.payload_ui; + const eventTypes = requestType?.event_types; + + /** @type {Event[]} */ + let events = []; + if (!_isEmpty(request?.events)) { + events = _sortBy(request.events, ['updated']); + } else if (!_isEmpty(actualRequest?.events)) { + events = _sortBy(actualRequest.events, ['updated']); + } + + const renderSubmitForm = requestModalType === REQUEST_TYPE.SUBMIT && payloadUI; + const renderReadOnlyData = (requestModalType === REQUEST_TYPE.ACCEPT || requestModalType === REQUEST_TYPE.CANCEL) && request?.payload; + + const { isSubmitting, isValid, handleSubmit } = useFormikContext(); + + return ( + + + + {request.description} + + + {(renderSubmitForm || renderReadOnlyData) && + + + + + + {renderSubmitForm && +
+ import(`@templates/custom_fields/${widget}.js`), + (widget) => import(`react-invenio-forms`) + ]} + fieldPathPrefix="payload" + /> +
+ + + +
|| + + + + + + } +
+ ); +} + +RequestModalContent.propTypes = { + request: PropTypes.object.isRequired, + requestTypes: PropTypes.array.isRequired, +}; \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/DefaultView.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/DefaultView.jsx new file mode 100644 index 00000000..26e2ec47 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/DefaultView.jsx @@ -0,0 +1,6 @@ +import React from "react"; +import PropTypes from "prop-types"; + +export default function DefaultView({ props, value }) { + return {value} +} diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/ReadOnlyCustomFields.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/ReadOnlyCustomFields.jsx new file mode 100644 index 00000000..972f14b2 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/ReadOnlyCustomFields.jsx @@ -0,0 +1,89 @@ +// This file is part of React-Invenio-Forms +// Copyright (C) 2022 CERN. +// Copyright (C) 2022 Northwestern University. +// +// React-Invenio-Forms 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, { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import { loadWidgetsFromConfig } from "../../utils/loader"; +import _has from "lodash/has"; +import _zip from "lodash/zip"; + +const ReadOnlyCustomFields = ({ + config, + data, + fieldPathPrefix, + templateLoaders, + includesPaths = (fields) => fields.map((field) => field.key) +}) => { + const [sections, setSections] = useState([]); + + const loadCustomFieldsWidgets = async () => { + const sections = []; + for (const sectionCfg of config) { + const usedFields = sectionCfg.fields.filter((field) => _has(data, field.field)); + const Widgets = await loadWidgetsFromConfig({ + templateLoaders: templateLoaders, + fieldPathPrefix: fieldPathPrefix, + fields: usedFields, + }); + const widgetsWithConfig = _zip(Widgets, usedFields); + const filteredFieldsWithData = widgetsWithConfig + .map(([Widget, fieldConfig]) => { + const value = data[fieldConfig.field]; + return ; + }); + sections.push({ ...sectionCfg, fields: filteredFieldsWithData }); + } + return sections; + }; + + useEffect(() => { + loadCustomFieldsWidgets() + .then((sections) => { + sections = sections.map((sectionCfg) => { + const paths = includesPaths(sectionCfg.fields, fieldPathPrefix); + return { ...sectionCfg, paths }; + }); + setSections(sections); + }) + .catch((error) => { + console.error("Couldn't load custom fields widgets.", error); + }); + }, [config, fieldPathPrefix, includesPaths, templateLoaders]); + + return ( + <> + {sections.map(({ section, fields, paths }) => ( + {fields} + ))} + + ); +}; + +ReadOnlyCustomFields.propTypes = { + config: PropTypes.arrayOf( + PropTypes.shape({ + section: PropTypes.string.isRequired, + fields: PropTypes.arrayOf( + PropTypes.shape({ + field: PropTypes.string.isRequired, + view_widget: PropTypes.string, + view_widget_props: PropTypes.object, + }) + ), + }) + ), + data: PropTypes.object.isRequired, // { field1: value1, field2: value2, ...} just like Formik initialValues + templateLoaders: PropTypes.array.isRequired, + fieldPathPrefix: PropTypes.string, + includesPaths: PropTypes.func, +}; + +ReadOnlyCustomFields.defaultProps = { + includesPaths: (fields) => fields.map((field) => field.key), +}; + +export default ReadOnlyCustomFields; diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/index.js b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/index.js new file mode 100644 index 00000000..4e98fdd0 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/common/index.js @@ -0,0 +1,3 @@ +export { default as ReadOnlyCustomFields } from "./ReadOnlyCustomFields"; +export { default as DefaultView } from "./DefaultView"; +export { default as StaticText } from "./StaticText"; \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/index.js b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/index.js new file mode 100644 index 00000000..0880c0d3 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/components/index.js @@ -0,0 +1,8 @@ +export { RequestListContainer } from "./RequestListContainer"; +export { CreateRequestButtonGroup } from "./CreateRequestButtonGroup"; +export { RequestModal } from "./RequestModal"; +export { TriggerButton } from "./TriggerButton"; +export { RecordRequests } from "./RecordRequests"; +export { RequestModalContent } from "./RequestModalContent"; +export { CreateRequestModalContent } from "./CreateRequestModalContent"; +export { RequestList } from "./RequestList"; \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/contexts/RequestContext.jsx b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/contexts/RequestContext.jsx new file mode 100644 index 00000000..ce71376f --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/contexts/RequestContext.jsx @@ -0,0 +1,11 @@ +import React, { createContext } from "react"; + +export const RequestContext = createContext(); + +export const RequestContextProvider = ({ children, requests }) => { + return ( + + {children} + + ); +}; diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/contexts/index.js b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/contexts/index.js new file mode 100644 index 00000000..abf50903 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/contexts/index.js @@ -0,0 +1 @@ +export { RequestContext, RequestContextProvider } from './RequestContext'; \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/dummy-record.js b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/dummy-record.js new file mode 100644 index 00000000..35bd673d --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/dummy-record.js @@ -0,0 +1,607 @@ +export default { + "id": "36s8e-qwz66", + "created": "2024-01-08T09:37:15.726599+00:00", + "updated": "2024-01-08T09:37:15.761771+00:00", + "links": { + "files": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/files", + "self": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66", + "self_html": "https://127.0.0.1:5000/theses/36s8e-qwz66" + }, + "revision_id": 2, + "$schema": "local://uct_theses-1.0.0.json", + "metadata": { + "abstract": [ + { + "lang": "cs", + "value": "Byla připravena série kvarterních flaviniových solí: 5-ethyl-3,7,8,10-\r\n-tetramethylisoalloxazinium-perchlorát (2a), 10-butyl-5-ethyl-3,7,8-\r\n-trimethylisoalloxazinium-perchlorát (2b), 5-ethyl-3,7,8-trimethyl-10-oktyl-isoalloxazinium-perchlorát (2c) a 10-dodecyl-5-ethyl-3,7,8-trimethylisoalloxazinium-\r\n-perchlorát (2d). Připravené isoalloxaziniové soli 2a-d byly studovány jako katalyzátory oxidace sulfidů na sulfoxidy peroxidem vodíku v různých typech micel: v anionických micelách dodecylsulfátu sodného (SDS), v kationických micelách hexadecyltrimethylamonium-chloridu (CTAC) a hexadecyltrimethylamonium-nitrátu (CTANO3) a v neionických micelách polyoxyethylen(23)(dodecyl)etheru (Brij 35). Pro porovnání byl ve vodě rozpustný derivát 2a testován také v homogenním prostředí (ve směsi methanol-voda 1 : 1). Ve všech micelárních prostředích byl studován vliv pH reakčního prostředí a lipofility isoalloxaziniových solí 2 na jejich katalytickou účinnost měřením rychlosti oxidace modelového substrátu, thioanisolu (9), v přítomnosti katalytického množství soli 2. V micelách SDS byla oxidace provedena také v semipreparativním měřítku." + }, + { + "lang": "en", + "value": "Series of quaternary flavinium salts: 5-ethyl-3,7,8,10-tetramethylisoalloxazinium perchlorate (2a), 10-butyl-5-ethyl-3,7,8-trimethylisoalloxazinium perchlorate (2b), \r\n5-ethyl-3,7,8-trimethyl-10-oktyl-isoalloxazinium perchlorate (2c) and 10-dodecyl-5-\r\n-ethyl-3,7,8-trimethylisoalloxazinium perchlorate (2d) was prepared. Flavinium salts 2a-d were studied as catalysts for oxidation of sulfides to sulfoxides with hydrogen peroxide in several micellar systems: in anionic micelles of sodium dodecylsulfate (SDS), in cationic micelles of hexadecyltrimethylammonium chloride (CTAC) and hexadecyltrimethylammonium nitrate (CTANO3), and in non-ionic micelles formed by polyoxyethylene(23)dodecyl ether (Brij 35). For comparison, water-soluble derivative 2a was also tested in homogeneous solution (in the mixture methanol-water 1 : 1). In all micellar systems, the effect of pH of reaction medium and lipophilicity of isoalloxazinium salts on their catalytic efficiency was tested by measuring the rate of model substrate (thioanisole (9)) oxidation in the presence of a catalytic amount of flavinium salt 2. In SDS micelles, sulfoxidation was performed also in semipreparative scale." + } + ], + "accessRights": { + "id": "c_abf2", + "@v": "199a114b-2933-4905-8a76-015ee4cc7f06::1", + "title": { + "cs": "otevřený přístup", + "en": "open access" + } + }, + "accessibility": [ + { + "lang": "cs", + "value": "Text práce je k nahlédnutí na ústavu, kde byla práce vypracována. Další dokumenty jsou uloženy v archivu VŠCHT Praha." + }, + { + "lang": "en", + "value": "The printed version of the thesis is available at the department where the thesis was produced. Defence protocol and reviews are available at UCT Prague archive." + } + ], + "additionalTitles": [ + { + "title": { + "lang": "en", + "value": "Synthesis and investigation of amphiphilic flavinium salts as sulfoxidation catalysts//in micellar solutions" + }, + "titleType": "translatedTitle" + } + ], + "contributors": [ + { + "fullName": "prof. Ing. Radek Cibulka, Ph.D.", + "nameType": "Personal", + "role": { + "id": "advisor", + "@v": "42e623d6-f9f9-4ece-8db9-434a495d91c1::1", + "title": { + "cs": "vedoucí", + "en": "advisor" + } + } + }, + { + "fullName": "prof. Ing. Jan Šmidrkal, CSc.", + "nameType": "Personal", + "role": { + "id": "referee", + "@v": "c5b87187-8d0b-4de8-aec9-ae19047e1bee::1", + "title": { + "cs": "oponent", + "en": "referee" + } + } + } + ], + "creators": [ + { + "fullName": "Ing. Lenka Baxová", + "nameType": "Personal" + } + ], + "dateAvailable": "2007-10-11", + "dateModified": "2007-10-11", + "languages": [ + { + "id": "cs", + "@v": "590f1c0e-d9c6-4179-bf3c-83b31b4b3803::1", + "title": { + "cs": "čeština", + "en": "Czech" + } + } + ], + "resourceType": { + "id": "master", + "@v": "b96ee94b-fef1-49c1-8840-841add31339b::1", + "title": { + "cs": "Diplomová práce", + "en": "Master thesis" + } + }, + "systemIdentifiers": [ + { + "scheme": "catalogueSysNo", + "identifier": "4701" + } + ], + "title": "Syntéza a studium amfifilních flaviniových solí jako katalyzátorů oxidace sulfidů//v micelárních roztocích", + "collection": "22310", + "thesis": { + "dateDefended": "2007-06-01", + "defended": true + }, + "sis": { + "did": 4701, + "timestamp": "2007-10-11T15:11:24" + } + }, + "files": { + "enabled": true + }, + "requests": [ + { + "name": "pridel doi", + "description": "Pridel DOI k objektu 36s8e-qwz66", + "uuid": "ed181799-26c5-4269-b032-4a2a4624eb9d", + "type": "assign_doi", + "links": { + "actions": { + "submit": "https://127.0.0.1:5000/api/uct-theses/requests/ed181799-26c5-4269-b032-4a2a4624eb9d/actions/submit", + "cancel": "https://127.0.0.1:5000/api/uct-theses/requests/ed181799-26c5-4269-b032-4a2a4624eb9d/actions/cancel" + }, + "self": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-4269-b032-4a2a4624eb9d", + "self_html": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-4269-b032-4a2a4624eb9d/html", + "events": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-4269-b032-4a2a4624eb9d/events" // TODO: Shouldn't this be more specific event like "comments"? + }, + "created_by": { + "reference": "6c3f6437-9197-490c-9584-d2daa5143dcf", + "type": "user", + "label": "Ron", + "link": "https://127.0.0.1:5000/api/uct-theses/users/6c3f6437-9197-490c-9584-d2daa5143dcf" + }, + "receiver": { + "reference": { + "id": "f40d6c41-3aa1-42be-a185-f8a0a125c815" + }, + "type": "community", + "label": "cesnet", + "link": "https://127.0.0.1:5000/api/uct-theses/users/f40d6c41-3aa1-42be-a185-f8a0a125c815" + }, + "topic": { + "reference": { + "id": "36s8e-qwz66" + }, + "type": "record", + "label": "title recordu", + "link": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66" + }, + "status_code": "draft", + "status": "Vytvořeno", + "created": "2024-01-08T09:37:15.726599+00:00", + "updated": "2024-01-08T09:37:15.761771+00:00", + "payload": { + "name": "pridel doi", + "description": "Pridel DOI k objektu 36s8e-qwz66", + "request_text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam fringilla tortor nibh, sed sagittis lorem congue ac. Aliquam ante mauris, porttitor eu ornare ut, feugiat sit amet diam. Maecenas ut sem mi. Maecenas elementum, elit mollis vestibulum cursus, nisi ex commodo neque, id ultrices felis ligula eget massa. Integer vel justo et ligula dapibus ultrices. Nullam ultricies sed neque sed lacinia. Interdum et malesuada fames ac ante ipsum primis in faucibus. Praesent sed lectus ut diam sollicitudin malesuada a quis nibh. In tempus nibh ex, tincidunt tempus enim rutrum id. Aliquam laoreet dictum enim in vestibulum. Nam sagittis, turpis at pretium maximus, odio nibh venenatis risus, eu tempus tortor nibh id ante. Vestibulum aliquam, quam ac gravida fringilla, risus leo fermentum nunc, in fringilla magna urna a turpis. Cras sed metus vel metus lacinia auctor. In sed maximus risus, at euismod urna. Donec mi velit, euismod non orci id, pharetra eleifend tortor." + } + }, + { + "name": "pridel doi 2", + "description": "Pridel DOI k objektu 36s8e-qwz66", + "uuid": "ed181799-26c5-4269-b032-4a2a4624eb9e", + "type": "assign_doi", + "links": { + "actions": { + "cancel": "https://127.0.0.1:5000/api/uct-theses/requests/ed181799-26c5-4269-b032-4a2a4624eb9e/actions/cancel" + }, + "self": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-4269-b032-4a2a4624eb9e", + "self_html": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-4269-b032-4a2a4624eb9e/html", + "events": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-4269-b032-4a2a4624eb9e/events" + }, + "created_by": { + "reference": "6c3f6437-9197-490c-9584-d2daa5143dcg", + "type": "user", + "label": "Ron", + "link": "https://127.0.0.1:5000/api/uct-theses/users/6c3f6437-9197-490c-9584-d2daa5143dcg" + }, + "receiver": { + "reference": { + "id": "f40d6c41-3aa1-42be-a185-f8a0a125c816" + }, + "type": "community", + "label": "cesnet", + "link": "https://127.0.0.1:5000/api/uct-theses/users/f40d6c41-3aa1-42be-a185-f8a0a125c816" + }, + "topic": { + "reference": { + "id": "36s8e-qwz66" + }, + "type": "record", + "label": "title recordu", + "link": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66" + }, + "status_code": "submitted", + "status": "Zažádáno", + "created": "2024-01-09T09:37:15.726599+00:00", + "updated": "2024-01-09T09:37:15.761771+00:00" + }, + { + "name": "pridel doi 3", + "description": "Pridel DOI k objektu 36s8e-qwz66", + "uuid": "ed181799-26c5-po69-b032-4a2a4664eb9e", + "type": "assign_doi", + "links": { + "actions": { + "cancel": "https://127.0.0.1:5000/api/uct-theses/requests/ed181799-26c5-po69-b032-4a2a4664eb9e/actions/cancel" + }, + "self": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-po69-b032-4a2a4664eb9e", + "self_html": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-po69-b032-4a2a4664eb9e/html", + "events": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-po69-b032-4a2a4664eb9e/events" + }, + "created_by": { + "reference": "6c3f6437-9197-490c-9584-d2daa5143dcg", + "type": "user", + "label": "Ron", + "link": "https://127.0.0.1:5000/api/uct-theses/users/6c3f6437-9197-490c-9584-d2daa5143dcg" + }, + "receiver": { + "reference": { + "id": "f40d6c41-3aa1-42be-a185-f8a0a125c816" + }, + "type": "community", + "label": "cesnet", + "link": "https://127.0.0.1:5000/api/uct-theses/users/f40d6c41-3aa1-42be-a185-f8a0a125c816" + }, + "topic": { + "reference": { + "id": "36s8e-qwz66" + }, + "type": "record", + "label": "title recordu", + "link": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66" + }, + "status_code": "submitted", + "status": "Zažádáno", + "created": "2024-01-09T09:37:15.726599+00:00", + "updated": "2024-01-09T09:37:15.761771+00:00" + }, + { + "name": "pridel doi 4", + "description": "Pridel DOI k objektu 36s8e-qwz66", + "uuid": "ed181799-26c5-4226-bsf32-4a2a4624eb9d", + "type": "assign_doi", + "links": { + "actions": { + "accept": "https://127.0.0.1:5000/api/uct-theses/requests/ed181799-26c5-4226-bsf32-4a2a4624eb9d/actions/accept", + "decline": "https://127.0.0.1:5000/api/uct-theses/requests/ed181799-26c5-4226-bsf32-4a2a4624eb9d/actions/decline" + }, + "self": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-4226-bsf32-4a2a4624eb9d", + "self_html": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-4226-bsf32-4a2a4624eb9d/html", + "events": "https://9c2a4912-c1a0-48f8-9565-e772482c6e92.mock.pstmn.io/api/uct-theses/36s8e-qwz66/requests/ed181799-26c5-4226-bsf32-4a2a4624eb9d/events" + }, + "created_by": { + "reference": "6c3f6437-9197-490c-9584-d2daa5143dcf", + "type": "user", + "label": "Ron", + "link": "https://127.0.0.1:5000/api/uct-theses/users/6c3f6437-9197-490c-9584-d2daa5143dcf" + }, + "receiver": { + "reference": { + "id": "f40d6c41-3aa1-42be-a185-f8a0a125c815" + }, + "type": "community", + "label": "cesnet", + "link": "https://127.0.0.1:5000/api/uct-theses/users/f40d6c41-3aa1-42be-a185-f8a0a125c815" + }, + "topic": { + "reference": { + "id": "36s8e-qwz66" + }, + "type": "record", + "label": "title recordu", + "link": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66" + }, + "status_code": "submitted", + "status": "Zažádáno", + "created": "2024-01-08T09:37:15.726599+00:00", + "updated": "2024-01-08T09:37:15.761771+00:00", + "payload": { + "name": "pridel doi", + "description": "Pridel DOI k objektu 36s8e-qwz66", + "request_text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam fringilla tortor nibh, sed sagittis lorem congue ac. Aliquam ante mauris, porttitor eu ornare ut, feugiat sit amet diam. Maecenas ut sem mi. Maecenas elementum, elit mollis vestibulum cursus, nisi ex commodo neque, id ultrices felis ligula eget massa. Integer vel justo et ligula dapibus ultrices. Nullam ultricies sed neque sed lacinia. Interdum et malesuada fames ac ante ipsum primis in faucibus. Praesent sed lectus ut diam sollicitudin malesuada a quis nibh. In tempus nibh ex, tincidunt tempus enim rutrum id. Aliquam laoreet dictum enim in vestibulum. Nam sagittis, turpis at pretium maximus, odio nibh venenatis risus, eu tempus tortor nibh id ante. Vestibulum aliquam, quam ac gravida fringilla, risus leo fermentum nunc, in fringilla magna urna a turpis. Cras sed metus vel metus lacinia auctor. In sed maximus risus, at euismod urna. Donec mi velit, euismod non orci id, pharetra eleifend tortor." + }, + // "events": [ + // { + // "payload": { + // "comment": "hello world" + // }, + // "created": "2024-01-08T09:37:15.726599+00:00", + // "created_by": { + // "reference": "6c3f6437-9197-490c-9584-d2daa5143dcf", + // "type": "user", + // "label": "Ron", + // "link": "https://127.0.0.1:5000/api/uct-theses/users/6c3f6437-9197-490c-9584-d2daa5143dcf" + // }, + // "updated": "2024-01-08T09:37:15.726599+00:00", + // "revision_id": 1, + // "id": "53c6a7fd-6a07-41a7-8996-46852267elk0", + // "type_code": "C", + // "type": "Comment", + // "links": { + // "self": "https://127.0.0.1:5000/api/uct-theses/users/6c3f6437-9197-490c-9584-d2daa5143dcf" + // }, + // "permissions": { + // "can_update_comment": true, + // "can_delete_comment": true + // } + // }, + // { + // "payload": { + + // }, + // "created": "2024-01-08T09:37:15.726599+00:00", + // "created_by": { + // "reference": "6c3f6437-9197-490c-9ki9-d2daa5143dcf", + // "type": "user", + // "label": "Ron", + // "link": "https://127.0.0.1:5000/api/uct-theses/users/6c3f6437-9197-490c-9584-d2daa5143dcf" + // }, + // "updated": "2024-01-08T09:37:15.726599+00:00", + // "revision_id": 1, + // "id": "53c6a7fd-6a07-41a7-8996-46852267e940", + // "type_code": "P", + // "type": "Znalecký posudek", + // "links": { + // "self": "https://127.0.0.1:5000/api/uct-theses/users/6c3f6437-9197-490c-9584-d2daa5143dcf" + // }, + // "permissions": { + // "can_update_comment": true, + // "can_delete_comment": true + // } + // } + // ] + }, + { + "name": "pridel doi 6", + "description": "Pridel DOI k objektu 36s8e-qwz66", + "uuid": "ed129799-26c5-4269-b032-4a2a4624lo9d", + "type": "assign_doi", + "links": { + "actions": { + "submit": "https://127.0.0.1:5000/api/uct-theses/requests/ed129799-26c5-4269-b032-4a2a4624lo9d/actions/submit", + "cancel": "https://127.0.0.1:5000/api/uct-theses/requests/ed129799-26c5-4269-b032-4a2a4624lo9d/actions/cancel" + }, + "self": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed129799-26c5-4269-b032-4a2a4624lo9d", // PUT request: override existing record + "self_html": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed129799-26c5-4269-b032-4a2a4624lo9d/html", + "events": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ed129799-26c5-4269-b032-4a2a4624lo9d/events" + }, + "created_by": { + "reference": "6c3f6437-9197-490c-9584-d2daa5143dcf", + "type": "user", + "label": "Ron", + "link": "https://127.0.0.1:5000/api/uct-theses/users/6c3f6437-9197-490c-9584-d2daa5143dcf" + }, + "receiver": { + "reference": { + "id": "f40d6c41-3aa1-42be-a185-f8a0a125c815" + }, + "type": "community", + "label": "cesnet", + "link": "https://127.0.0.1:5000/api/uct-theses/users/f40d6c41-3aa1-42be-a185-f8a0a125c815" + }, + "topic": { + "reference": { + "id": "36s8e-qwz66" + }, + "type": "record", + "label": "title recordu", + "link": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66" + }, + "status_code": "draft", + "status": "Vytvořeno", + "created": "2024-01-08T09:37:15.726599+00:00", + "updated": "2024-01-08T09:37:15.761771+00:00" + } + ], + "request_types": [ + { + "name": "pridel doi", + "description": "Pridel DOI k objektu 36s8e-qwz66", + "id": "assign_doi", + "links": { + "actions": { + "create": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/assign_doi/create" + } + }, + "fast_approve": true, + "payload_ui": [ + { + "section": "General info", + "fields": [ + { + "field": "name", + "ui_widget": "Input", + // "editable": ["requestor"], + "props": { + "readonly": "true", + "label": "name", + "placeholder": "name", + "icon": "text cursor", + "required": true + }, + "view_widget": "StaticText", + "view_widget_props": { + "label": "name", + "icon": "text cursor", + } + }, + { + "field": "description", + "ui_widget": "RichInput", + "props": { + "label": "description", + "placeholder": "description", + "icon": "text height", + "required": true + }, + "view_widget": "StaticText", + "view_widget_props": { + "label": "name", + "icon": "text cursor", + } + }, + { + "field": "request_text", + "ui_widget": "RichInput", + "props": { + "label": "Request Text", + "placeholder": "request text", + "icon": "text height", + "required": true + }, + "view_widget": "StaticText", + "view_widget_props": { + "label": "name", + "icon": "text cursor", + } + }, + // { + // "field": "comment", + // "ui_widget": "RichInput", + // "editable": ["requestor", "approver"], + // "props": { + // "label": "description", + // "placeholder": "description", + // "icon": "text height", + // "required": true + // } + // }, + // { + // "field": "internal_comment", + // "ui_widget": "RichInput", + // "visible": ["approver"], + // "editable": ["approver"], + // "props": { + // "label": "description", + // "placeholder": "description", + // "icon": "text height", + // "required": true + // } + // } + ] + } + ], + "event_types": [ + { + "name": "Comment", + "description": "Comment on request", + "id": "C", + "links": { + "create": "https://127.0.0.1:5000/api/uct-theses/requests/assign_doi/comments" + }, // only if current user can create new event of this type + "payload_ui": [ + { + "section": "Comment", + "fields": [ + { + "field": "comment", + "ui_widget": "RichInput", + "props": { + "label": "description", + "placeholder": "description", + "icon": "text height", + "required": true + }, + "view_widget": "StaticText", + "view_widget_props": { + "label": "name", + "icon": "text cursor", + } + }, + ], + } + ] + }, + { + "name": "Znalecký posudek", + "description": "Znalecký posudek na 36s8e-qwz66", + "id": "P", + "links": { + "create": "https://127.0.0.1:5000/api/uct-theses/requests/assign_doi/expert_opinion" // TODO: How to connect Event to specific request? + }, // only if current user can create new event of this type + "payload_ui": [ + { + "section": "Informace o znaleckém posudku", + "fields": [ + { + "field": "expert_opinion", + "ui_widget": "RichInput", + "props": { + "label": "description", + "placeholder": "description", + "icon": "text height", + "required": true + }, + "view_widget": "FileViewWidget", + "view_widget_props": { + "label": "file", + "icon": "file", + } + }, + ], + } + ] + } + ] + }, + { + "name": "ask permission", + "description": "Ak permission for 36s8e-qwz66", + "id": "ask_permission", + "links": { + "actions": { + "create": "https://127.0.0.1:5000/api/uct-theses/36s8e-qwz66/requests/ask_permission/create" + } + }, + "fast_approve": true, + "payload_ui": [ + { + "section": "General info", + "fields": [ + { + "field": "name", + "ui_widget": "Input", + "editable": ["requestor"], + "props": { + "readonly": "true", + "label": "name", + "placeholder": "name", + "icon": "text cursor", + "required": true + } + }, + { + "field": "description", + "ui_widget": "RichInput", + "props": { + "label": "description", + "placeholder": "description", + "icon": "text height", + "required": true + } + }, + ] + } + ], + "event_types": [ + { + "name": "Comment", + "description": "Comment on request", + "id": "C", + "links": { + "create": "https://127.0.0.1:5000/api/uct-theses/requests/ask_permission/comments" // TODO: How to connect Event to specific request? + }, // only if current user can create new event of this type + "payload_ui": [ + { + "section": "Comment", + "fields": [ + { + "field": "comment", + "ui_widget": "RichInput", + "editable": ["requestor", "approver"], + "props": { + "label": "description", + "placeholder": "description", + "icon": "text height", + "required": true + } + }, + ], + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/index.js b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/index.js new file mode 100644 index 00000000..b5b73c1e --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/index.js @@ -0,0 +1,17 @@ +import React from "react"; +import ReactDOM from "react-dom"; + +import { RecordRequests } from "./components"; + +import dummyRecord from "./dummy-record.js"; + +const recordRequestsAppDiv = document.getElementById("record-requests"); + +let record = JSON.parse(recordRequestsAppDiv.dataset.record)?.requests || dummyRecord; + +ReactDOM.render( + , + recordRequestsAppDiv +); diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/types.d.ts b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/types.d.ts new file mode 100644 index 00000000..94dbd401 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/types.d.ts @@ -0,0 +1,106 @@ +export interface Request { + name: string; + description: string; + uuid: string; + type: string; + links: Links; + created_by: Creator; + receiver: Receiver; + topic: Receiver; + created: string; // Datetime + updated: string; // Datetime + events?: Event[]; +} + +export interface Links { + actions?: RequestActions; + self: string; + self_html?: string; + events?: string; +} + +export interface RequestActions { + create?: string; + submit?: string; + cancel?: string; + accept?: string; + decline?: string; + expire?: string; + delete?: string; +} + +export interface Creator { + reference: string; + type: string; + label: string; + link: string; +} + +export interface Receiver { + reference: Reference; + type: Type; + label: Label; + link: string; +} + +export interface Reference { + id: string; +} + +export interface RequestType { + name: string; + description: string; + id: string; + links: Links; + fast_approve?: boolean; + payload_ui?: PayloadUI[]; + event_types?: EventType[]; +} + +export interface PayloadUI { + section: string; + fields: Field[]; +} + +export interface Field { + field: string; + ui_widget: "Input" | "NumberInput" | "MultiInput" | "RichInput" | "TextArea" | "Dropdown" | "AutocompleteDropdown" | "BooleanCheckbox"; + visible: ("requestor" | "approver")[]; + editable: ("requestor" | "approver")[]; + props: React.ComponentProps; +} + +export interface EventType { + name: string; + description: string; + id: string; + links: Links; + payload_ui?: PayloadUI[]; +} + +export interface Event { + payload: any; + created: string; // Datetime + created_by: Creator; + updated: string; // Datetime + revision_id: number; + id: string; + type_code: string; + type: string; + links: Links; + permissions: Permissions; +} + +export interface Permissions { + can_update_comment: boolean; + can_delete_comment: boolean; +} + +export enum RequestTypeEnum { + CREATE = "create", + SUBMIT = "submit", + CANCEL = "cancel", + ACCEPT = "accept", + DECLINE = "decline", + SAVE = "save", +} diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/utils/loader.js b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/utils/loader.js new file mode 100644 index 00000000..8d827164 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/utils/loader.js @@ -0,0 +1,87 @@ +import React from "react"; +import DefaultView from "../components/common/DefaultView"; + +/** Import function to load widget either from a specific path or local widgets + * + * The user defined path to import widget is of the format `@templates/`. + * Note that the `@template` alias should be a correctly resolving path and it's the + * user of this function that has to ensure that. The value is hardcoded here as the + * dynamic import cannot rely on purely a dynamic path i.e a variable. + */ +export async function importWidget( + templateLoaders, + { view_widget: UIWidget, fieldPath, view_widget_props: props } +) { + let component = undefined; + + // Try import widget from user's defined templateLoaders + for (const loader of templateLoaders) { + try { + const module = await loader(UIWidget); + component = module.default ?? module[UIWidget]; + // Component was found, stop looking. + if (component) { + return component; + } + } catch (error) { + // If the component failed to load from a loader, try other loaders first. + continue; + } + } + + return DefaultView; +} + +/** + * @param config: Configuration to load widgets + * + * Example configuration + * + * { + * fieldPathPrefix: "mynamespace" or empty, + * templateLoader: UIWidget => import(`my_folder/${UIWidget}.js`), + * fields: [{ + * ui_widget: "MyWidget", + * field: "field_id", + * props: { + * label: "My label" + * } + * }] + * } + * + * @returns array fields: resolved react components + * + * Example return + * + * [ + * , + * ... + * ] + * + */ +export async function loadWidgetsFromConfig({ + templateLoaders, + fieldPathPrefix, + fields, +}) { + const importWidgetsFromFolder = (templateFolder, fieldPathPrefix, fieldsConfig) => { + const tplPromises = []; + fieldsConfig.forEach((fieldCfg) => { + tplPromises.push( + importWidget(templateFolder, { + ...fieldCfg, + fieldPath: fieldPathPrefix + ? `${fieldPathPrefix}.${fieldCfg.field}` + : fieldCfg.field, + }) + ); + }); + return Promise.all(tplPromises); + }; + const _fields = await importWidgetsFromFolder( + templateLoaders, + fieldPathPrefix, + fields + ); + return [..._fields]; +} \ No newline at end of file diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/utils/objects.js b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/utils/objects.js new file mode 100644 index 00000000..512d0b08 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/js/oarepo_requests_ui/record-requests/utils/objects.js @@ -0,0 +1,8 @@ +export const REQUEST_TYPE = { + CREATE: 'create', + SUBMIT: 'submit', + CANCEL: 'cancel', + ACCEPT: 'accept', + DECLINE: 'decline', + SAVE: 'save', +} diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/less/oarepo_requests/custom-components.less b/oarepo_requests/ui/theme/assets/semantic-ui/less/oarepo_requests/custom-components.less new file mode 100644 index 00000000..525c082f --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/less/oarepo_requests/custom-components.less @@ -0,0 +1,8 @@ +/* + * In this file you will import the less files for your component. + + The file name of the component is sample.less, note that the extension is not written here. +*/ +/* + & { @import "./definitions/sample"; } +*/ diff --git a/oarepo_requests/ui/theme/assets/semantic-ui/less/oarepo_requests/definitions/sample.less b/oarepo_requests/ui/theme/assets/semantic-ui/less/oarepo_requests/definitions/sample.less new file mode 100644 index 00000000..02603740 --- /dev/null +++ b/oarepo_requests/ui/theme/assets/semantic-ui/less/oarepo_requests/definitions/sample.less @@ -0,0 +1,50 @@ +/******************************* + Theme +*******************************/ + +/* + The type is the type of the component as defined in semantic-ui - element, collection, view, module + For simple component, 'element' is the correct type +*/ +@type : 'element'; + +/* + This is the name of the component - though not required, keep it the same as the filename +*/ +@element : 'sample'; + +/******************************* + + DO NOT FORGET !!! + +Edit *theme.config* in your site +and add (replace 'sample' with the +name of the component from above) + +@sample : 'default'; + + +Edit *theme.less* in your site +and add the imports for your component +library there. See the commented out +chunks for details. + +*******************************/ + + +@import (multiple) '../../theme.config'; + +/* + This is the styling of your component. The class should start with .ui. + + Note: use variables here, not the exact values. Placing the values to another file (sample.variables) + will allow users to override these values. + */ +.ui.sample { + color: @sampleColor; +} + +/* + This will load the overrides, enabling the user of your component to override your css if required + */ +.loadUIOverrides(); \ No newline at end of file diff --git a/oarepo_requests/ui/theme/webpack.py b/oarepo_requests/ui/theme/webpack.py new file mode 100644 index 00000000..f4b964be --- /dev/null +++ b/oarepo_requests/ui/theme/webpack.py @@ -0,0 +1,19 @@ +from invenio_assets.webpack import WebpackThemeBundle + +theme = WebpackThemeBundle( + __name__, + "assets", + default="semantic-ui", + themes={ + "semantic-ui": dict( + entry={ + "oarepo_requests_ui_record_requests": "./js/oarepo_requests_ui/record-requests/index.js", + }, + dependencies={}, + devDependencies={}, + aliases={ + "@translations/oarepo_requests_ui": "translations/oarepo_requests", + }, + ) + }, +) diff --git a/setup.cfg b/setup.cfg index 9a29f03f..cff0d0b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,3 +29,6 @@ tests = oarepo-model-builder-requests oarepo-model-builder-drafts +[options.entry_points] +invenio_assets.webpack = + oarepo_requests_ui_theme = oarepo_requests.ui.theme.webpack:theme