diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index f2883a687..ff39769ef 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,3 +1,5 @@ +- [News](./news.md) + ### DOCUMENTATION - [Introduction](./introduction.md) @@ -22,7 +24,8 @@ - [Sharing your analysis](./share_analysis.md) - [How to search](./search.md) - [Workflows](./workflows.md) - +- [Collections](./collections.md) +- [Admin panel](./admin.md) ### Tutorials diff --git a/docs/admin.md b/docs/admin.md new file mode 100644 index 000000000..b6d3f3d7c --- /dev/null +++ b/docs/admin.md @@ -0,0 +1,19 @@ +# Admin panel + +The admin panel lets you access existing schemas for your collaboration to either view or edit them, as well as create new schemas or import them from JSON files. It is only accessible by users with admin privileges in the collaboration. + +If you have such privileges, you can access the admin panel at https://analysispreservation.cern.ch/admin or by clicking on the _Admin_ button at the bottom right of the screen in CAP. + +The tools available in the admin section are listed below. + +## Schema viewer + +The schema viewer displays the JSON schemas and lets you do manual changes to certain parts of them. You can use the diff view to see your changes, and you can easily revert or save them. + +## Form builder + +The form builder is a powerful tool which makes it easy to interactively create and edit forms. It includes a **tour** that we encourage you to follow in order to understand the basics of the tool. The tour is automatically run on your first visit to the admin, and can be run manually whenever you needed by clicking on the _Tour_ button in the bottom right corner of the form builder. + +## Notifications + +The notifications tab allows you to define notifications for a given category. You can add as many notifications as you want, each of them with a different content and addressed to different people. The email notifications can contain information directly extracted from the form, using the provided _context options_. diff --git a/docs/collections.md b/docs/collections.md new file mode 100644 index 000000000..7a9f76c33 --- /dev/null +++ b/docs/collections.md @@ -0,0 +1,8 @@ +# Collections + +The collections page is a centralized place from which you can access all drafts and published records for that collection, being able to **filter** by version. + +It contains a **permissions** table showing all permissions assigned to users, which are applied globally to all records in the collection. +Each entry indicates the type of the user (egroup or user), and its permissions for read, create, review, update and admin, for both drafts (D) and published records (P). + +Admins are also able to define a **readme** for the collection, which can be useful to provide some instructions for other members of the collaboration. diff --git a/docs/introduction.md b/docs/introduction.md index 4a78987dc..914c738ff 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -12,6 +12,8 @@ Find our contact information [here](./support.md). ## Overview +- [News](./news.md) + ##### DOCUMENTATION - [Introduction](./introduction.md) @@ -34,6 +36,8 @@ Find our contact information [here](./support.md). - [Sharing your analysis](./share_analysis.md) - [How to search](./search.md) - [Workflows](./workflows.md) +- [Collections](./collections.md) +- [Admin panel](./admin.md) ##### Tutorials @@ -50,6 +54,7 @@ Find our contact information [here](./support.md). --- - [FAQ](./faq.md) +- [Glossary](./GLOSSARY.md) - [Related Projects](./related-projects.md) --- diff --git a/docs/news.md b/docs/news.md new file mode 100644 index 000000000..e61459078 --- /dev/null +++ b/docs/news.md @@ -0,0 +1 @@ +# News diff --git a/ui/cap-react/index.html b/ui/cap-react/index.html index e289942cf..ae5a6be90 100755 --- a/ui/cap-react/index.html +++ b/ui/cap-react/index.html @@ -9,6 +9,8 @@
+ + diff --git a/ui/cap-react/package.json b/ui/cap-react/package.json index 9808de065..e805b1461 100644 --- a/ui/cap-react/package.json +++ b/ui/cap-react/package.json @@ -69,6 +69,7 @@ "react-dnd-html5-backend": "9.3.4", "react-dom": "^18.2.0", "react-input-mask": "3.0.0-alpha.2", + "react-joyride": "^2.5.4", "react-markdown-editor-lite": "1.2.4", "react-redux": "6.0.0", "react-router": "^4.3.1", diff --git a/ui/cap-react/src/antd/App/App.js b/ui/cap-react/src/antd/App/App.js index bde710ac8..a1e1082a2 100644 --- a/ui/cap-react/src/antd/App/App.js +++ b/ui/cap-react/src/antd/App/App.js @@ -23,6 +23,7 @@ import * as Sentry from "@sentry/react"; import useTrackPageViews from "../hooks/useTrackPageViews"; import { lazy } from "react"; import Loading from "../routes/Loading/Loading"; +import MessageBanner from "../partials/MessageBanner"; const AdminPage = lazy(() => import("../admin")); @@ -55,6 +56,7 @@ const App = ({ initCurrentUser, loadingInit, history, roles }) => {
+ }> diff --git a/ui/cap-react/src/antd/Root.js b/ui/cap-react/src/antd/Root.js index 9fa76c205..9e17229ef 100644 --- a/ui/cap-react/src/antd/Root.js +++ b/ui/cap-react/src/antd/Root.js @@ -4,8 +4,7 @@ import { Provider } from "react-redux"; import App from "../antd/App"; import { ConfigProvider } from "antd"; import { MatomoProvider, createInstance } from "@datapunt/matomo-tracker-react"; - -const PRIMARY_COLOR = "#006996"; +import { theme } from "./utils/theme"; export const matomoInstance = process.env.PIWIK_URL && process.env.PIWIK_SITEID @@ -20,18 +19,7 @@ const Root = ({ store, history }) => { - + diff --git a/ui/cap-react/src/antd/admin/components/AdminPanel.js b/ui/cap-react/src/antd/admin/components/AdminPanel.js index 8e1d5ff14..4c2f3ff49 100644 --- a/ui/cap-react/src/antd/admin/components/AdminPanel.js +++ b/ui/cap-react/src/antd/admin/components/AdminPanel.js @@ -5,7 +5,13 @@ import { CMS_NEW } from "../../routes"; import DocumentTitle from "../../partials/DocumentTitle"; import SchemaWizard from "../containers/SchemaWizard"; import Notifications from "../notifications/containers/Notifications"; -import { Layout } from "antd"; +import { FloatButton, Layout } from "antd"; +import Joyride, { STATUS } from "react-joyride"; +import { steps } from "../utils/tour/admin"; +import TourTooltip from "../utils/tour/TourTooltip"; +import { CarOutlined } from "@ant-design/icons"; +import useStickyState from "../../hooks/useStickyState"; +import { PRIMARY_COLOR } from "../../utils/theme"; const AdminPanel = ({ location, match, schema, schemaInit, getSchema }) => { useEffect(() => { @@ -24,16 +30,48 @@ const AdminPanel = ({ location, match, schema, schemaInit, getSchema }) => { const [display, setDisplay] = useState("builder"); + const [tourDone, setTourDone] = useStickyState(false, "tourDone"); + const getPageTitle = () => location.pathname.includes("notifications") ? "Notifications" : "Form Builder"; + return (
+ ( + + )} + run={!tourDone && display != "notifications"} + spotlightClicks + callback={({ status }) => + [STATUS.FINISHED, STATUS.SKIPPED].includes(status) && + setTourDone(true) + } + styles={{ + options: { primaryColor: PRIMARY_COLOR }, + spotlight: { borderRadius: 0 }, + }} + /> {display === "notifications" ? : } + } + type="primary" + shape="square" + description="Tour" + onClick={() => setTourDone(false)} + /> diff --git a/ui/cap-react/src/antd/admin/components/Draggable.js b/ui/cap-react/src/antd/admin/components/Draggable.js index 4d470ba06..febd31c14 100644 --- a/ui/cap-react/src/antd/admin/components/Draggable.js +++ b/ui/cap-react/src/antd/admin/components/Draggable.js @@ -19,7 +19,7 @@ const Draggable = ({ data, children }) => { const opacity = isDragging ? 0.4 : 1; return ( -
+
{children}
); diff --git a/ui/cap-react/src/antd/admin/components/Header.js b/ui/cap-react/src/antd/admin/components/Header.js index 726aaefd2..559d72e18 100644 --- a/ui/cap-react/src/antd/admin/components/Header.js +++ b/ui/cap-react/src/antd/admin/components/Header.js @@ -105,11 +105,16 @@ const Header = ({ return previews[schemaPreviewDisplay]; //? }; - const getMenuItem = (key, label, icon, onClick, type) => ({ + const getMenuItem = (key, label, icon, onClick, className) => ({ key, label: ( - @@ -213,6 +218,7 @@ const Header = ({ key: "notifications", label: "Notifications", icon: , + className: "tour-notifications-tab", }, ]} /> @@ -229,18 +235,22 @@ const Header = ({ getMenuItem("export", "Export Schema", , () => _getSchema() ), - getMenuItem("diff", "Diff", , () => - setDiffModal(true) - ), - getMenuItem("settings", "Settings", , () => - setSettingsModal(true) + getMenuItem( + "diff", + "Diff", + , + () => setDiffModal(true), + "tour-diff" ), getMenuItem( - "save", - "Save updates", - , - () => saveSchemaChanges(), - "primary" + "settings", + "Settings", + , + () => setSettingsModal(true), + "tour-schema-settings" + ), + getMenuItem("save", "Save updates", , () => + saveSchemaChanges() ), ]} /> diff --git a/ui/cap-react/src/antd/admin/components/SchemaPreview.js b/ui/cap-react/src/antd/admin/components/SchemaPreview.js index 72ca266b2..ee9b18818 100644 --- a/ui/cap-react/src/antd/admin/components/SchemaPreview.js +++ b/ui/cap-react/src/antd/admin/components/SchemaPreview.js @@ -30,6 +30,7 @@ const SchemaPreview = ({ schema, selectProperty }) => { shape="circle" icon={} onClick={() => selectProperty({ schema: [], uiSchema: [] })} + className="tour-root-settings" /> diff --git a/ui/cap-react/src/antd/admin/components/SchemaWizard.js b/ui/cap-react/src/antd/admin/components/SchemaWizard.js index f9df228e7..a0051d33d 100644 --- a/ui/cap-react/src/antd/admin/components/SchemaWizard.js +++ b/ui/cap-react/src/antd/admin/components/SchemaWizard.js @@ -25,6 +25,7 @@ const SchemaWizard = ({ field, loader }) => { height: "100%", display: "flex", }} + className="tour-field-types" > {field ? : } @@ -37,6 +38,7 @@ const SchemaWizard = ({ field, loader }) => { padding: "0px 15px", backgroundColor: "#F6F7F8", }} + className="tour-schema-preview" > @@ -44,6 +46,7 @@ const SchemaWizard = ({ field, loader }) => { xs={24} sm={14} style={{ overflowX: "hidden", height: "100%", padding: "0px 15px" }} + className="tour-form-preview" > diff --git a/ui/cap-react/src/antd/admin/components/SelectFieldType.js b/ui/cap-react/src/antd/admin/components/SelectFieldType.js index f2166d8dc..cd6fa8270 100644 --- a/ui/cap-react/src/antd/admin/components/SelectFieldType.js +++ b/ui/cap-react/src/antd/admin/components/SelectFieldType.js @@ -31,6 +31,7 @@ const SelectFieldType = () => { ))} ), + className: type.className, }))} />
diff --git a/ui/cap-react/src/antd/admin/formComponents/HoverBox.js b/ui/cap-react/src/antd/admin/formComponents/HoverBox.js index 8a4a9487e..04420797e 100644 --- a/ui/cap-react/src/antd/admin/formComponents/HoverBox.js +++ b/ui/cap-react/src/antd/admin/formComponents/HoverBox.js @@ -1,6 +1,7 @@ import { useDrop } from "react-dnd"; import PropTypes from "prop-types"; import { connect } from "react-redux"; +import { PRIMARY_COLOR } from "../../utils/theme"; function getStyle(isOverCurrent) { const style = { @@ -10,7 +11,7 @@ function getStyle(isOverCurrent) { return isOverCurrent ? { - outline: "1px solid #006996", + outline: `1px solid ${PRIMARY_COLOR}`, outlineOffset: "-1px", ...style, } diff --git a/ui/cap-react/src/antd/admin/utils/fieldTypes.js b/ui/cap-react/src/antd/admin/utils/fieldTypes.js index 344a463ac..ee17feebf 100644 --- a/ui/cap-react/src/antd/admin/utils/fieldTypes.js +++ b/ui/cap-react/src/antd/admin/utils/fieldTypes.js @@ -89,6 +89,7 @@ const collections = { title: "Object", icon:
{ }
, description: "Data in JSON format, Grouped section", + className: "tour-object-field", child: {}, optionsSchema: { type: "object", @@ -132,6 +133,7 @@ const collections = { icon: , description: "A list of things. List of strings, numbers, objects, references", + className: "tour-list-field", child: {}, optionsSchema: { type: "object", @@ -249,6 +251,7 @@ const simple = { title: "Text", icon: , description: "Titles, names, paragraphs, IDs, list of names", + className: "tour-text-field", child: {}, optionsSchema: { type: "object", @@ -1058,6 +1061,7 @@ const fields = { title: "Collections", description: "", fields: collections, + className: "tour-collections", }, simple: { title: "Fields", diff --git a/ui/cap-react/src/antd/admin/utils/tour/TourTooltip.js b/ui/cap-react/src/antd/admin/utils/tour/TourTooltip.js new file mode 100644 index 000000000..b2fe6a5f3 --- /dev/null +++ b/ui/cap-react/src/antd/admin/utils/tour/TourTooltip.js @@ -0,0 +1,40 @@ +import { Button, Card, Col, Row, Typography } from "antd"; + +const TourTooltip = ({ + index, + step, + backProps, + primaryProps, + tooltipProps, + isLastStep, + skipProps, + total, +}) => { + return ( + + + {step.title && ( + {step.title} + )} + + {step.content} + + + + + + + {index > 0 && } + + + + + + ); +}; + +export default TourTooltip; diff --git a/ui/cap-react/src/antd/admin/utils/tour/admin.js b/ui/cap-react/src/antd/admin/utils/tour/admin.js new file mode 100644 index 000000000..fe1b9cee3 --- /dev/null +++ b/ui/cap-react/src/antd/admin/utils/tour/admin.js @@ -0,0 +1,89 @@ +export const steps = [ + { + target: "body", + placement: "center", + disableBeacon: true, + title: "Welcome to CAP Admin", + content: + "Press next to start the tour and learn how to create and edit forms with CAP's advanced form editor.", + }, + { + target: ".tour-field-types", + title: "Field types", + placement: "right", + placementBeacon: "top", + disableBeacon: true, + content: + "These are the different fields you can use in a form. They're divided into collections (fields that can contain other fields) normal fields and advanced fields with more specific functionality.", + }, + { + target: ".tour-object-field", + placement: "right", + content: + "Object fields are useful to visually group fields of different types in the form.", + }, + { + target: ".tour-list-field", + placement: "right", + content: + "List fields allow users to add several instances of the same field. In order to configure them, you need to drag the list field to the tree and afterwards drag a conventional field, say a text field, inside of it. This will allow users to add multiple (the amount can be configured) text fields.", + }, + { + target: ".tour-schema-preview", + placement: "right", + placementBeacon: "top", + content: + "Here you will see a tree with all the fields of your form, displaying the type, id and title (if any). You will be able to open their settings by clicking on them.", + }, + { + target: ".tour-form-preview", + placement: "left", + placementBeacon: "top", + content: + "This is the form preview. It will display the final form as the users are going to see it, allowing you to test if everything works as you desire.", + }, + { + target: ".tour-text-field", + content: + "You can add elements by dragging and dropping them into the schema tree. Try with a text field.", + }, + { + target: ".tour-schema-preview .__Form__ div div div div", + content: + "Click on a field to view and edit its properties. You will see two tabs: schema settings, which allow you to define the main settings and behavior of the field, and UI schema settings, which contain extra settings, generally related to how a field is displayed (e.g. its width or, in the case of text fields, whether the text should be automatically transformed to uppercase or not).", + }, + { + target: ".tour-root-settings", + content: + "Here you can customize the root of the form, changing its name and description (visible only from the admin page) but, more importantly, the width and alignment of all fields of the form from the UI settings tab.", + }, + { + target: ".tour-diff", + content: + "You can see the JSON schema representation of your form and a diff view of all the current unsaved changes here.", + }, + { + target: ".tour-schema-settings", + content: + "On a new schema, you will have to provide some settings like id, version, name or experiment before being able to save your changes.", + }, + { + target: ".tour-notifications-tab", + content: + "The notifications tab allows you create notification templates and define patterns to send them to the appropriate users when an event takes place.", + }, + { + target: "body", + placement: "center", + content: ( + + You have now learnt the basics of CAP Admin and you are ready to start + creating your own forms! For more in-depth information, please check the{" "} + + CAP Documentation + + . + + ), + }, +]; diff --git a/ui/cap-react/src/antd/hooks/useStickyState.js b/ui/cap-react/src/antd/hooks/useStickyState.js new file mode 100644 index 000000000..fcde5d401 --- /dev/null +++ b/ui/cap-react/src/antd/hooks/useStickyState.js @@ -0,0 +1,16 @@ +import { useEffect, useState } from "react"; + +const useStickyState = (defaultValue, key) => { + const [value, setValue] = useState(() => { + const stickyValue = window.localStorage.getItem(key); + return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue; + }); + + useEffect(() => { + window.localStorage.setItem(key, JSON.stringify(value)); + }, [key, value]); + + return [value, setValue]; +}; + +export default useStickyState; diff --git a/ui/cap-react/src/antd/partials/MessageBanner/MessageBanner.js b/ui/cap-react/src/antd/partials/MessageBanner/MessageBanner.js new file mode 100644 index 000000000..4dae4073c --- /dev/null +++ b/ui/cap-react/src/antd/partials/MessageBanner/MessageBanner.js @@ -0,0 +1,65 @@ +import { message, Button, Space, Typography } from "antd"; + +import { CloseOutlined } from "@ant-design/icons"; +import { useEffect } from "react"; +import useStickyState from "../../hooks/useStickyState"; + +const bannerRaw = import.meta.env.VITE_CAP_BANNER; + +const MessageBanner = () => { + const [messageApi, contextHolder] = message.useMessage(); + + const [bannerDismissed, setBannerDismissed] = useStickyState( + false, + "bannerDismissed" + ); + + useEffect(() => { + if (bannerRaw) { + try { + const banner = JSON.parse(bannerRaw); + if (banner.text != bannerDismissed) + messageApi.open({ + type: banner.type || "info", + duration: 0, + content: ( + + + {banner.text} + + | + {banner.url && ( + + More info + + )} + {!banner.permanent && ( +