From b3b55de644b6bbd23d0884c52c783603665223d7 Mon Sep 17 00:00:00 2001 From: dleadbetter Date: Fri, 9 Aug 2024 14:44:51 -0400 Subject: [PATCH] RC #291 - Adding FacetTimeline component; Adding "actions" prop to FacetSlider component; Moving ModalContext to shared package (breaking) --- packages/core-data/package.json | 3 +- .../core-data/src/components/EventDetails.js | 11 +- .../core-data/src/components/EventsList.js | 99 ++++++++ .../core-data/src/components/FacetSlider.js | 81 +++++- .../core-data/src/components/FacetTimeline.js | 234 ++++++++++++++++++ packages/core-data/src/components/Modal.js | 141 +++++++++++ .../core-data/src/components/RelatedEvents.js | 61 +---- packages/core-data/src/index.css | 1 + .../core-data/src/services/BaseService.js | 27 +- .../core-data/src/styles/FacetTimeline.css | 3 + packages/core-data/src/utils/Api.js | 11 +- .../src/components/AccordionSelector.js | 3 +- .../semantic-ui/src/components/AudioPlayer.js | 2 +- .../src/components/ColorPickerModal.js | 2 +- .../semantic-ui/src/components/DataView.js | 2 +- .../src/components/FileUploadModal.js | 2 +- .../semantic-ui/src/components/FuzzyDate.js | 3 +- .../semantic-ui/src/components/ListFilters.js | 3 +- .../semantic-ui/src/components/LoginModal.js | 2 +- .../semantic-ui/src/components/PhotoViewer.js | 4 +- .../semantic-ui/src/components/Selectize.js | 2 +- .../semantic-ui/src/components/TabbedModal.js | 3 +- .../src/components/VideoFrameSelector.js | 3 +- .../semantic-ui/src/components/VideoPlayer.js | 4 +- .../semantic-ui/src/components/ViewXML.js | 2 +- packages/semantic-ui/src/index.js | 3 - .../src/context/ModalContext.js | 0 packages/shared/src/index.js | 3 + .../.storybook/api/core-data/Base.js | 10 +- .../.storybook/api/core-data/Events.js | 23 +- packages/storybook/.storybook/preview.js | 2 +- .../storybook/.storybook/routes/CoreData.js | 8 +- .../src/core-data/FacetTimeline.stories.js | 121 +++++++++ yarn.lock | 89 +++++-- 34 files changed, 827 insertions(+), 141 deletions(-) create mode 100644 packages/core-data/src/components/EventsList.js create mode 100644 packages/core-data/src/components/FacetTimeline.js create mode 100644 packages/core-data/src/components/Modal.js create mode 100644 packages/core-data/src/styles/FacetTimeline.css rename packages/{semantic-ui => shared}/src/context/ModalContext.js (100%) create mode 100644 packages/storybook/src/core-data/FacetTimeline.stories.js diff --git a/packages/core-data/package.json b/packages/core-data/package.json index f264cb40..82aee51b 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -22,8 +22,9 @@ "dependencies": { "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-slider": "^1.2.0", "@radix-ui/react-tooltip": "^1.1.2", "@samvera/clover-iiif": "^2.3.2", diff --git a/packages/core-data/src/components/EventDetails.js b/packages/core-data/src/components/EventDetails.js index 5662d949..51b512c5 100644 --- a/packages/core-data/src/components/EventDetails.js +++ b/packages/core-data/src/components/EventDetails.js @@ -6,6 +6,11 @@ import LoadAnimation from './LoadAnimation'; import { useEventsService, useLoader } from '../hooks/CoreData'; type Props = { + /** + * (Optional) class name to apply to the root element. + */ + className?: string, + /** * Identifier for the event to fetch. */ @@ -25,7 +30,7 @@ const EventDetails = (props: Props) => { */ const onLoad = useCallback(() => EventsService.fetchOne(props.id), [props.id]); - const { data: { event } = {}, loading } = useLoader(onLoad); + const { data: { event } = {}, loading } = useLoader(onLoad, {}, [props.id]); if (loading) { return ( @@ -38,7 +43,9 @@ const EventDetails = (props: Props) => { } return ( -
+

diff --git a/packages/core-data/src/components/EventsList.js b/packages/core-data/src/components/EventsList.js new file mode 100644 index 00000000..1b01c088 --- /dev/null +++ b/packages/core-data/src/components/EventsList.js @@ -0,0 +1,99 @@ +// @flow + +import clsx from 'clsx'; +import React from 'react'; +import _ from 'underscore'; +import type { Event as EventType } from '../types/Event'; +import EventUtils from '../utils/Event'; + +type Props = { + /** + * (Optional) class name to apply to the root element. + */ + className?: string, + + /** + * If `true`, the event description will be displayed on the card. + */ + description?: boolean, + + /** + * The list of events records to display. + */ + events: Array, + + /** + * Callback that returns `true` if the passed event is selected. + */ + isSelected?: (event: EventType) => boolean, + + /** + * Callback fired when the event row is clicked. + */ + onClick?: (event: EventType) => void +}; + +const EventsList = (props: Props) => ( +
    + { _.map(props.events, (event) => ( +
  • +
    + +
    +
  • + ))} +
+); + +EventsList.defaultProps = { + isSelected: () => false, + onClick: () => {} +}; + +export default EventsList; diff --git a/packages/core-data/src/components/FacetSlider.js b/packages/core-data/src/components/FacetSlider.js index 2389b940..ef67c182 100644 --- a/packages/core-data/src/components/FacetSlider.js +++ b/packages/core-data/src/components/FacetSlider.js @@ -12,6 +12,7 @@ import { ZoomOut } from 'lucide-react'; import React, { useCallback, useEffect, useState } from 'react'; +import _ from 'underscore'; type MarkerProps = { className?: string, @@ -75,7 +76,7 @@ const SliderMarker = (props: MarkerProps) => { sideOffset={5} >
{ props.value }
@@ -88,17 +89,47 @@ const SliderMarker = (props: MarkerProps) => { ); }; +type Action = { + /** + * Class name to apply to the button element. + */ + className?: string, + + /** + * (Optional) icon to render inside the button element. + */ + icon?: JSX.Element, + + /** + * Button label. + */ + label: string, + + /** + * Callback fired when the button is clicked. + */ + onClick: () => void +}; + +type ClassNames = { + button: string, + range: string, + root: string, + thumb: string, + track: string, + zoom: string +}; + type Props = { + /** + * Custom actions to render as buttons. + */ + actions?: Array, + /** * Custom Tailwind CSS class names. */ - classNames: { - button: string, - range: string, - root: string, - thumb: string, - track: string - }, + classNames: ClassNames, /** * The maximum facet value. @@ -113,7 +144,7 @@ type Props = { /** * Callback fired when the range is changed. */ - onChange?: ([number, number]) => void, + onChange?: ([[number, number], [number, number]]) => void, /** * Number of steps to increment the slider. @@ -224,9 +255,9 @@ const FacetSlider = (props: Props) => { */ useEffect(() => { if (props.onChange) { - props.onChange(range); + props.onChange(range, [min, max]); } - }, [range]); + }, [max, min, range]); return ( <> @@ -292,7 +323,10 @@ const FacetSlider = (props: Props) => {

{ props.zoom && (
+ { !_.isEmpty(props.actions) && ( + <> + { _.map(props.actions, (action, index) => ( + + ))} + + )}
)} @@ -330,3 +382,8 @@ FacetSlider.defaultProps = { }; export default FacetSlider; + +export type { + Action, + ClassNames +}; diff --git a/packages/core-data/src/components/FacetTimeline.js b/packages/core-data/src/components/FacetTimeline.js new file mode 100644 index 00000000..2758a811 --- /dev/null +++ b/packages/core-data/src/components/FacetTimeline.js @@ -0,0 +1,234 @@ +// @flow + +import { useTimer } from '@performant-software/shared-components'; +import * as Popover from '@radix-ui/react-popover'; +import * as Slider from '@radix-ui/react-slider'; +import { clsx } from 'clsx'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react'; +import _ from 'underscore'; +import type { Event as EventType } from '../types/Event'; +import EventUtils from '../utils/Event'; +import FacetSlider, { Action as ActionType, ClassNames as ClassNamesType } from './FacetSlider'; +import { useEventsService } from '../hooks/CoreData'; + +type Props = { + /** + * Custom actions to render with the FacetSlider. + */ + actions?: Array, + + /** + * Class name to apply to the root DOM element. + */ + className?: string, + + /** + * Class names to apply to the FacetSlider components. + */ + classNames?: ClassNamesType, + + /** + * Default maximum value. + */ + defaultMax: number, + + /** + * Default minimum value. + */ + defaultMin: number, + + /** + * Callback fired when the event popover is clicked. + */ + onClick?: (event: EventType) => void, + + /** + * Callback fired when the events are loaded. + */ + onLoad?: (events: Array) => void, + + /** + * Zoom level increment. + */ + zoom?: number +}; + +const FacetTimeline = (props: Props) => { + const [events, setEvents] = useState(); + const [max, setMax] = useState(); + const [min, setMin] = useState(); + const [range, setRange] = useState(); + + const EventsService = useEventsService(); + const { clearTimer, setTimer } = useTimer(); + + const ref = useRef(); + + /** + * Sets the new range and min/max values on the state. + * + * @type {(function(*, [*,*]): void)|*} + */ + const onChange = useCallback((newRange, [newMin, newMax]) => { + setRange(newRange); + setMin(newMin); + setMax(newMax); + }, []); + + /** + * Sets the events on the state. + * + * @type {(function(*): void)|*} + */ + const onLoad = useCallback((data) => { + setEvents(_.map(data.events, (event) => ({ + ...event, + year: new Date(event.start_date?.start_date).getFullYear() + }))); + }, []); + + /** + * Memo-izes the slider value. + */ + const value = useMemo(() => _.pluck(events, 'year'), [events]); + + /** + * Loads the list of events when the range or min/max values are changed. + */ + useEffect(() => { + if (!range) { + return; + } + + clearTimer(); + + setTimer(() => ( + EventsService + .fetchAll({ min: range[0], max: range[1] }) + .then(onLoad) + )); + }, [max, min, range]); + + /** + * Calls the onLoad prop when the events are changed. + */ + useEffect(() => { + if (props.onLoad) { + props.onLoad(events); + } + }, [events, props.onLoad]); + + return ( +
+
+ + + { _.map(events, (event) => ( + + + + + + + + + + + ))} + + +
+ +
+ ); +}; + +export default FacetTimeline; diff --git a/packages/core-data/src/components/Modal.js b/packages/core-data/src/components/Modal.js new file mode 100644 index 00000000..6b53c82e --- /dev/null +++ b/packages/core-data/src/components/Modal.js @@ -0,0 +1,141 @@ +// @flow + +import { ModalContext } from '@performant-software/shared-components'; +import * as Dialog from '@radix-ui/react-dialog'; +import clsx from 'clsx'; +import { X } from 'lucide-react'; +import React, { useContext, type Node } from 'react'; + +type Props = { + /** + * If `true` the modal will be centered in the viewport. + */ + centered?: boolean, + + /** + * Modal content. + */ + children?: Node, + + /** + * (Optional) class name to apply to the modal content. + */ + className?: string, + + /** + * (Optional) modal description. + */ + description?: string, + + /** + * Callback fired when the closed icon is clicked. If this prop is not provided, a close + * icon will not be rendered. + */ + onClose: () => void, + + /** + * If `true` the modal will be displayed. + */ + open?: boolean, + + /** + * If `true`, the modal content will scroll instead of the viewport. + */ + scrolling?: boolean, + + /** + * (Optional) modal title. + */ + title?: string +}; + +const Modal = (props: Props) => { + const rootRef = useContext(ModalContext); + + return ( + + + + + { props.onClose && ( + +
+ +
+
+ )} + { props.title && ( + +

+ { props.title } +

+
+ )} + { props.description && ( + + { props.description } + + )} + { props.children } +
+
+
+
+ ); +}; + +export default Modal; diff --git a/packages/core-data/src/components/RelatedEvents.js b/packages/core-data/src/components/RelatedEvents.js index 0292c15c..dbfe9f83 100644 --- a/packages/core-data/src/components/RelatedEvents.js +++ b/packages/core-data/src/components/RelatedEvents.js @@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight } from 'lucide-react'; import React, { useCallback } from 'react'; import _ from 'underscore'; import type { Event as EventType } from '../types/Item'; -import EventUtils from '../utils/Event'; +import EventsList from './EventsList'; import LoadAnimation from './LoadAnimation'; import { useLoader } from '../hooks/CoreData'; @@ -61,59 +61,12 @@ const RelatedEvents = (props: Props) => { return (
-
    - { _.map(events, (event) => ( -
  • -
    - -
    -
  • - ))} -
+ { pages > 1 && (
/ API endpoint. + * + * @param params + * + * @returns {Promise} + */ + fetchAll(params = {}) { + const url = Api.buildUrl(this.baseUrl, this.getRoute(), null, this.projectIds, params); + return fetch(url).then((response) => response.json()); + } + /** * Calls the GET /core_data/public///:id API endpoint. * @@ -163,20 +174,6 @@ class BaseService { getRoute() { // Implemented in sub-classes } - - /** - * Returns the search params for the current request. - * - * @param params - * - * @returns {*&{project_ids}} - */ - getSearchParams(params) { - return { - ...params, - 'project_ids[]': this.projectIds - }; - } } export default BaseService; diff --git a/packages/core-data/src/styles/FacetTimeline.css b/packages/core-data/src/styles/FacetTimeline.css new file mode 100644 index 00000000..e5fc6ce6 --- /dev/null +++ b/packages/core-data/src/styles/FacetTimeline.css @@ -0,0 +1,3 @@ +div[data-radix-popper-content-wrapper]:hover { + z-index: 99 !important; +} \ No newline at end of file diff --git a/packages/core-data/src/utils/Api.js b/packages/core-data/src/utils/Api.js index ba40c572..040d05e5 100644 --- a/packages/core-data/src/utils/Api.js +++ b/packages/core-data/src/utils/Api.js @@ -18,6 +18,7 @@ const buildNestedUrl = (baseUrl, route, id, nested, projectIds = [], searchParam const url = `${baseUrl}/core_data/public/v1/${route}/${id}/${nested}`; const params = new URLSearchParams(searchParams); + // Append project IDs _.each(projectIds, (projectId) => { params.append('project_ids[]', projectId); }); @@ -37,9 +38,17 @@ const buildNestedUrl = (baseUrl, route, id, nested, projectIds = [], searchParam * @returns {`${string}?${string}`} */ const buildUrl = (baseUrl, route, id, projectIds = [], searchParams = {}) => { - const url = `${baseUrl}/core_data/public/v1/${route}/${id}`; + let url = `${baseUrl}/core_data/public/v1/${route}`; + + // Append the ID to the URL if provided + if (id) { + url = `${url}/${id}`; + } + + // Create the search params const params = new URLSearchParams(searchParams); + // Append each project ID _.each(projectIds, (projectId) => { params.append('project_ids[]', projectId); }); diff --git a/packages/semantic-ui/src/components/AccordionSelector.js b/packages/semantic-ui/src/components/AccordionSelector.js index 8191238d..97eefd7a 100644 --- a/packages/semantic-ui/src/components/AccordionSelector.js +++ b/packages/semantic-ui/src/components/AccordionSelector.js @@ -1,6 +1,6 @@ // @flow -import { Timer } from '@performant-software/shared-components'; +import { ModalContext, Timer } from '@performant-software/shared-components'; import React, { Component, type ComponentType, type Element } from 'react'; import { withTranslation } from 'react-i18next'; import { @@ -15,7 +15,6 @@ import { import _ from 'underscore'; import i18n from '../i18n/i18n'; import EditModal from './EditModal'; -import ModalContext from '../context/ModalContext'; import NestedAccordion from './NestedAccordion'; import SelectizeHeader from './SelectizeHeader'; import Toaster from './Toaster'; diff --git a/packages/semantic-ui/src/components/AudioPlayer.js b/packages/semantic-ui/src/components/AudioPlayer.js index e97ba2f5..a1bd342d 100644 --- a/packages/semantic-ui/src/components/AudioPlayer.js +++ b/packages/semantic-ui/src/components/AudioPlayer.js @@ -1,9 +1,9 @@ // @flow +import { ModalContext } from '@performant-software/shared-components'; import React, { useState } from 'react'; import { Button, Message, Modal } from 'semantic-ui-react'; import i18n from '../i18n/i18n'; -import ModalContext from '../context/ModalContext'; import './AudioPlayer.css'; type Props = { diff --git a/packages/semantic-ui/src/components/ColorPickerModal.js b/packages/semantic-ui/src/components/ColorPickerModal.js index 1d4ec126..e3d08d39 100644 --- a/packages/semantic-ui/src/components/ColorPickerModal.js +++ b/packages/semantic-ui/src/components/ColorPickerModal.js @@ -1,10 +1,10 @@ // @flow +import { ModalContext } from '@performant-software/shared-components'; import React, { Component } from 'react'; import { SketchPicker } from 'react-color'; import { Button, Modal } from 'semantic-ui-react'; import i18n from '../i18n/i18n'; -import ModalContext from '../context/ModalContext'; import './ColorPickerModal.css'; type Props = { diff --git a/packages/semantic-ui/src/components/DataView.js b/packages/semantic-ui/src/components/DataView.js index 8482737b..e3562604 100644 --- a/packages/semantic-ui/src/components/DataView.js +++ b/packages/semantic-ui/src/components/DataView.js @@ -1,5 +1,6 @@ // @flow +import { ModalContext } from '@performant-software/shared-components'; import axios from 'axios'; import React, { useCallback, @@ -15,7 +16,6 @@ import DropdownButton from './DropdownButton'; import i18n from '../i18n/i18n'; import MenuBar from './MenuBar'; import MenuSidebar from './MenuSidebar'; -import ModalContext from '../context/ModalContext'; import useDataList, { SORT_ASCENDING, SORT_DESCENDING } from './DataList'; import './DataView.css'; diff --git a/packages/semantic-ui/src/components/FileUploadModal.js b/packages/semantic-ui/src/components/FileUploadModal.js index a71418f2..586d8cb6 100644 --- a/packages/semantic-ui/src/components/FileUploadModal.js +++ b/packages/semantic-ui/src/components/FileUploadModal.js @@ -1,5 +1,6 @@ // @flow +import { ModalContext } from '@performant-software/shared-components'; import React, { useCallback, useMemo, @@ -20,7 +21,6 @@ import FileUpload from './FileUpload'; import FileUploadStatus from './FileUploadStatus'; import FileUploadProgress from './FileUploadProgress'; import i18n from '../i18n/i18n'; -import ModalContext from '../context/ModalContext'; type Props = { /** diff --git a/packages/semantic-ui/src/components/FuzzyDate.js b/packages/semantic-ui/src/components/FuzzyDate.js index 4c0fc763..37f6db1f 100644 --- a/packages/semantic-ui/src/components/FuzzyDate.js +++ b/packages/semantic-ui/src/components/FuzzyDate.js @@ -1,6 +1,6 @@ // @flow -import { Calendar, Browser } from '@performant-software/shared-components'; +import { Calendar, Browser, ModalContext } from '@performant-software/shared-components'; import React, { Component } from 'react'; import { Button, @@ -14,7 +14,6 @@ import { import _ from 'underscore'; import i18n from '../i18n/i18n'; import DateField from './DateInput'; -import ModalContext from '../context/ModalContext'; import './FuzzyDate.css'; type DateInput = { diff --git a/packages/semantic-ui/src/components/ListFilters.js b/packages/semantic-ui/src/components/ListFilters.js index 693c327a..0973037c 100644 --- a/packages/semantic-ui/src/components/ListFilters.js +++ b/packages/semantic-ui/src/components/ListFilters.js @@ -1,6 +1,6 @@ // @flow -import { type EditContainerProps } from '@performant-software/shared-components'; +import { ModalContext, type EditContainerProps } from '@performant-software/shared-components'; import React, { useCallback, useEffect, @@ -22,7 +22,6 @@ import i18n from '../i18n/i18n'; import AssociatedDropdown from './AssociatedDropdown'; import DropdownButton from './DropdownButton'; import FuzzyDate from './FuzzyDate'; -import ModalContext from '../context/ModalContext'; type Option = { key: string | number, diff --git a/packages/semantic-ui/src/components/LoginModal.js b/packages/semantic-ui/src/components/LoginModal.js index ceda6378..feaa2e9a 100644 --- a/packages/semantic-ui/src/components/LoginModal.js +++ b/packages/semantic-ui/src/components/LoginModal.js @@ -1,5 +1,6 @@ // @flow +import { ModalContext } from '@performant-software/shared-components'; import React, { type Element } from 'react'; import { Button, @@ -12,7 +13,6 @@ import { Modal } from 'semantic-ui-react'; import i18n from '../i18n/i18n'; -import ModalContext from '../context/ModalContext'; import './LoginModal.css'; type Props = { diff --git a/packages/semantic-ui/src/components/PhotoViewer.js b/packages/semantic-ui/src/components/PhotoViewer.js index 45527c10..4d92dcb1 100644 --- a/packages/semantic-ui/src/components/PhotoViewer.js +++ b/packages/semantic-ui/src/components/PhotoViewer.js @@ -1,10 +1,10 @@ // @flow +import { ModalContext } from '@performant-software/shared-components'; import React, { useState } from 'react'; import { Image, Message, Modal } from 'semantic-ui-react'; -import ModalContext from '../context/ModalContext'; -import './PhotoViewer.css'; import i18n from '../i18n/i18n'; +import './PhotoViewer.css'; type Props = { alt?: string, diff --git a/packages/semantic-ui/src/components/Selectize.js b/packages/semantic-ui/src/components/Selectize.js index c515a934..2d943db2 100644 --- a/packages/semantic-ui/src/components/Selectize.js +++ b/packages/semantic-ui/src/components/Selectize.js @@ -1,5 +1,6 @@ // @flow +import { ModalContext } from '@performant-software/shared-components'; import React, { useCallback, useEffect, @@ -20,7 +21,6 @@ import { import _ from 'underscore'; import SelectizeHeader from './SelectizeHeader'; import i18n from '../i18n/i18n'; -import ModalContext from '../context/ModalContext'; import useDataList from './DataList'; import useList, { type Props as ListProps } from './List'; import './Selectize.css'; diff --git a/packages/semantic-ui/src/components/TabbedModal.js b/packages/semantic-ui/src/components/TabbedModal.js index 165962da..3445e3dc 100644 --- a/packages/semantic-ui/src/components/TabbedModal.js +++ b/packages/semantic-ui/src/components/TabbedModal.js @@ -1,10 +1,9 @@ // @flow -import { Element } from '@performant-software/shared-components'; +import { Element, ModalContext } from '@performant-software/shared-components'; import React, { Component, type Node } from 'react'; import { Header, Menu, Modal } from 'semantic-ui-react'; import _ from 'underscore'; -import ModalContext from '../context/ModalContext'; import './TabbedModal.css'; type Props = { diff --git a/packages/semantic-ui/src/components/VideoFrameSelector.js b/packages/semantic-ui/src/components/VideoFrameSelector.js index 60442f75..a365dd03 100644 --- a/packages/semantic-ui/src/components/VideoFrameSelector.js +++ b/packages/semantic-ui/src/components/VideoFrameSelector.js @@ -1,6 +1,6 @@ // @flow -import { Browser } from '@performant-software/shared-components'; +import { Browser, ModalContext } from '@performant-software/shared-components'; import React, { useEffect, useRef, useState } from 'react'; import { withTranslation } from 'react-i18next'; import { @@ -12,7 +12,6 @@ import { Segment } from 'semantic-ui-react'; import i18n from '../i18n/i18n'; -import ModalContext from '../context/ModalContext'; import './VideoFrameSelector.css'; type Props = { diff --git a/packages/semantic-ui/src/components/VideoPlayer.js b/packages/semantic-ui/src/components/VideoPlayer.js index b7b4307c..df7d7b76 100644 --- a/packages/semantic-ui/src/components/VideoPlayer.js +++ b/packages/semantic-ui/src/components/VideoPlayer.js @@ -1,5 +1,6 @@ // @flow +import { ModalContext } from '@performant-software/shared-components'; import React, { useEffect, useRef, @@ -12,9 +13,8 @@ import { Modal, Ref } from 'semantic-ui-react'; -import ModalContext from '../context/ModalContext'; -import './VideoPlayer.css'; import i18n from '../i18n/i18n'; +import './VideoPlayer.css'; type Props = { autoPlay?: boolean, diff --git a/packages/semantic-ui/src/components/ViewXML.js b/packages/semantic-ui/src/components/ViewXML.js index 703fcf1f..8eec12d6 100644 --- a/packages/semantic-ui/src/components/ViewXML.js +++ b/packages/semantic-ui/src/components/ViewXML.js @@ -1,10 +1,10 @@ // @flow +import { ModalContext } from '@performant-software/shared-components'; import React, { type ComponentType, useState } from 'react'; import SyntaxHighlighter from 'react-syntax-highlighter'; import { Button, Modal } from 'semantic-ui-react'; import i18n from '../i18n/i18n'; -import ModalContext from '../context/ModalContext'; type Props = { highlighter?: any, diff --git a/packages/semantic-ui/src/index.js b/packages/semantic-ui/src/index.js index 483b8c5b..dee3e814 100644 --- a/packages/semantic-ui/src/index.js +++ b/packages/semantic-ui/src/index.js @@ -101,9 +101,6 @@ export { default as VideoPlayerButton } from './components/VideoPlayerButton'; export { default as ViewPDFButton } from './components/ViewPDFButton'; export { default as ViewXML } from './components/ViewXML'; -// Context -export { default as ModalContext } from './context/ModalContext'; - // Hooks export { default as BatchEdit } from './hooks/BatchEdit'; diff --git a/packages/semantic-ui/src/context/ModalContext.js b/packages/shared/src/context/ModalContext.js similarity index 100% rename from packages/semantic-ui/src/context/ModalContext.js rename to packages/shared/src/context/ModalContext.js diff --git a/packages/shared/src/index.js b/packages/shared/src/index.js index 9b91b0ae..238a80d1 100644 --- a/packages/shared/src/index.js +++ b/packages/shared/src/index.js @@ -9,6 +9,9 @@ export { default as InfiniteScroll } from './components/InfiniteScroll'; export { default as Keyboard } from './components/Keyboard'; export { default as RichTextArea } from './components/RichTextArea'; +// Context +export { default as ModalContext } from './context/ModalContext'; + // Hooks export { default as useCitationStyles } from './hooks/CitationStyles'; export { default as useTimer } from './hooks/Timer'; diff --git a/packages/storybook/.storybook/api/core-data/Base.js b/packages/storybook/.storybook/api/core-data/Base.js index 3f8a0d56..28e9ef40 100644 --- a/packages/storybook/.storybook/api/core-data/Base.js +++ b/packages/storybook/.storybook/api/core-data/Base.js @@ -11,9 +11,9 @@ class Base { /** * Returns a single item. */ - fetchItem() { + fetchItem(params = {}) { return { - [this.getShowAttribute()]: this.buildItem() + [this.getShowAttribute()]: this.buildItem(params) }; } @@ -24,11 +24,11 @@ class Base { * * @returns {{[p: string]: [], list: {pages: number, count, page: number}}} */ - fetchItems(count) { + fetchItems(count, params = {}) { const items = []; _.times(count, () => { - items.push(this.buildItem()); + items.push(this.buildItem(params)); }); return { @@ -43,7 +43,7 @@ class Base { // Protected - buildItem() { + buildItem(params = {}) { // Implemented in sub-classes return {}; } diff --git a/packages/storybook/.storybook/api/core-data/Events.js b/packages/storybook/.storybook/api/core-data/Events.js index bfaa2880..e15452d3 100644 --- a/packages/storybook/.storybook/api/core-data/Events.js +++ b/packages/storybook/.storybook/api/core-data/Events.js @@ -13,9 +13,11 @@ class Events extends Base { /** * Builds a single event item. */ - buildItem() { - const startDate = this.createFuzzyDate(); - const endDate = this.createFuzzyDate(startDate.end_date); + buildItem(params) { + const { min, max } = params; + + const startDate = this.createFuzzyDate(min, max); + // const endDate = this.createFuzzyDate(startDate.end_date); return { relationship_type: 'Events', @@ -23,8 +25,8 @@ class Events extends Base { name: faker.lorem.words({ min: 1, max: 5 }), description: faker.lorem.sentences({ min: 0, max: 3 }), start_date: startDate, - end_date: endDate - } + end_date: null + }; } /** @@ -50,9 +52,16 @@ class Events extends Base { * * @returns {{end_date, accuracy: number, range: number, description: string, start_date: Date}} */ - createFuzzyDate() { + createFuzzyDate(min = null, max = null) { const accuracy = faker.number.int({ min: 0, max: 2 }); - let startDate = faker.date.anytime(); + + let startDate; + + if (min && max) { + startDate = faker.date.between({ from: min, to: max }); + } else { + startDate = faker.date.anytime(); + } let endDate; let range = faker.number.int({ min: 0, max: 1 }); diff --git a/packages/storybook/.storybook/preview.js b/packages/storybook/.storybook/preview.js index 1cd79920..a0002ee0 100644 --- a/packages/storybook/.storybook/preview.js +++ b/packages/storybook/.storybook/preview.js @@ -2,7 +2,7 @@ import { DocsContainer } from '@storybook/addon-docs'; import React, { useRef } from 'react'; -import ModalContext from '../../semantic-ui/src/context/ModalContext'; +import ModalContext from '../../shared/src/context/ModalContext'; // Peripleo styles import '@peripleo/maplibre/peripleo-maplibre.css'; diff --git a/packages/storybook/.storybook/routes/CoreData.js b/packages/storybook/.storybook/routes/CoreData.js index 757db8bb..be987c51 100644 --- a/packages/storybook/.storybook/routes/CoreData.js +++ b/packages/storybook/.storybook/routes/CoreData.js @@ -18,7 +18,6 @@ import Works from '../api/core-data/Works'; * @param router */ const addRoutes = (router) => { - /** * Core Data Public API. */ @@ -57,6 +56,13 @@ const addRoutes = (router) => { response.end(); }); + router.get(`${BASE_URL}/events`, (request, response) => { + const { count = '10', ...params } = request.query; + + response.send(Events.fetchItems(parseInt(count, 10), params)); + response.end(); + }); + router.get(`${BASE_URL}/places/:id`, (request, response) => { response.send(Places.fetchItem()); response.end(); diff --git a/packages/storybook/src/core-data/FacetTimeline.stories.js b/packages/storybook/src/core-data/FacetTimeline.stories.js new file mode 100644 index 00000000..c9f12274 --- /dev/null +++ b/packages/storybook/src/core-data/FacetTimeline.stories.js @@ -0,0 +1,121 @@ +// @flow + +import { List } from 'lucide-react'; +import React, { useCallback, useState } from 'react'; +import EventDetails from '../../../core-data/src/components/EventDetails'; +import EventsList from '../../../core-data/src/components/EventsList'; +import FacetTimeline from '../../../core-data/src/components/FacetTimeline'; +import Modal from '../../../core-data/src/components/Modal'; +import withCoreDataContextProvider from '../hooks/CoreDataContextProvider'; + +export default { + title: 'Components/Core Data/FacetTimeline', + component: FacetTimeline +}; + +export const Default = withCoreDataContextProvider(() => ( + +)); + +export const Styled = withCoreDataContextProvider(() => ( + +)); + +export const EventModal = withCoreDataContextProvider(() => { + const [selectedEvent, setSelectedEvent] = useState(); + + return ( + <> + setSelectedEvent(event)} + zoom={10} + /> + { selectedEvent && ( + setSelectedEvent(null)} + open + > + + + )} + + ); +}); + +export const ListView = withCoreDataContextProvider(() => { + const [events, setEvents] = useState([]); + const [listView, setListView] = useState(false); + const [selectedEvent, setSelectedEvent] = useState(); + + /** + * Returns true if the passed event is currently selected. + * + * @type {unknown} + */ + const isSelected = useCallback((event) => selectedEvent && selectedEvent.uuid === event.uuid, [selectedEvent]); + + return ( + <> + + ), + onClick: () => setListView(true) + }]} + defaultMin={2000} + defaultMax={2024} + onLoad={setEvents} + zoom={10} + /> + { listView && ( + setListView(false)} + open + > +
+
+ setSelectedEvent(event)} + /> +
+ { selectedEvent && ( + + )} +
+
+ )} + + ); +}); diff --git a/yarn.lock b/yarn.lock index 72ec19a4..fdb49ac0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2428,26 +2428,25 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A== -"@radix-ui/react-dialog@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" - integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== +"@radix-ui/react-dialog@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz#4906507f7b4ad31e22d7dad69d9330c87c431d44" + integrity sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg== dependencies: - "@babel/runtime" "^7.13.10" - "@radix-ui/primitive" "1.0.1" - "@radix-ui/react-compose-refs" "1.0.1" - "@radix-ui/react-context" "1.0.1" - "@radix-ui/react-dismissable-layer" "1.0.5" - "@radix-ui/react-focus-guards" "1.0.1" - "@radix-ui/react-focus-scope" "1.0.4" - "@radix-ui/react-id" "1.0.1" - "@radix-ui/react-portal" "1.0.4" - "@radix-ui/react-presence" "1.0.1" - "@radix-ui/react-primitive" "1.0.3" - "@radix-ui/react-slot" "1.0.2" - "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.0" + "@radix-ui/react-focus-guards" "1.1.0" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-portal" "1.1.1" + "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" aria-hidden "^1.1.1" - react-remove-scroll "2.5.5" + react-remove-scroll "2.5.7" "@radix-ui/react-direction@1.0.1": version "1.0.1" @@ -2517,6 +2516,11 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-focus-guards@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13" + integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw== + "@radix-ui/react-focus-scope@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz#9c2e8d4ed1189a1d419ee61edd5c1828726472f9" @@ -2537,6 +2541,15 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-focus-scope@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2" + integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-form@^0.0.3": version "0.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-form/-/react-form-0.0.3.tgz#328e7163e723ccc748459d66a2d685d7b4f85d5a" @@ -2620,6 +2633,27 @@ aria-hidden "^1.1.1" react-remove-scroll "2.5.5" +"@radix-ui/react-popover@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.1.tgz#604b783cdb3494ed4f16a58c17f0e81e61ab7775" + integrity sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.0" + "@radix-ui/react-focus-guards" "1.1.0" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.0" + "@radix-ui/react-portal" "1.1.1" + "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.7" + "@radix-ui/react-popper@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9" @@ -13226,6 +13260,14 @@ react-remove-scroll-bar@^2.3.3: react-style-singleton "^2.2.1" tslib "^2.0.0" +react-remove-scroll-bar@^2.3.4: + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + react-remove-scroll@2.5.5: version "2.5.5" resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" @@ -13237,6 +13279,17 @@ react-remove-scroll@2.5.5: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-remove-scroll@2.5.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz#15a1fd038e8497f65a695bf26a4a57970cac1ccb" + integrity sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA== + dependencies: + react-remove-scroll-bar "^2.3.4" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + react-resizable@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1"