From 405384cc1b174892e9fe81c465d98714feb392e6 Mon Sep 17 00:00:00 2001 From: Joseph John Aas Cooper <33054985+cooper-joe@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:35:06 +0200 Subject: [PATCH 1/3] docs: interpretations storybook examples --- .storybook/preview.js | 3 +- src/__demo__/InterpretationModal.stories.js | 333 ++++++++++++ src/__demo__/InterpretationThread.stories.js | 272 ++++++++++ src/__demo__/InterpretationsUnit.stories.js | 521 +++++++++++++++++-- 4 files changed, 1088 insertions(+), 41 deletions(-) create mode 100644 src/__demo__/InterpretationModal.stories.js create mode 100644 src/__demo__/InterpretationThread.stories.js diff --git a/.storybook/preview.js b/.storybook/preview.js index 3fcea95df..82efa2d65 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,10 +1,11 @@ -import { CssReset } from '@dhis2/ui' +import { CssReset, CssVariables } from '@dhis2/ui' import React from 'react' export const decorators = [ (Story) => (
+ -
+ + + + + + + ) } CommentUpdateForm.propTypes = { diff --git a/src/components/Interpretations/InterpretationModal/InterpretationModal.js b/src/components/Interpretations/InterpretationModal/InterpretationModal.js index 73bc13c8c..39aa9156e 100644 --- a/src/components/Interpretations/InterpretationModal/InterpretationModal.js +++ b/src/components/Interpretations/InterpretationModal/InterpretationModal.js @@ -1,3 +1,4 @@ +import { useTimeZoneConversion } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' import { Modal, @@ -12,6 +13,7 @@ import { CircularLoader, } from '@dhis2/ui' import cx from 'classnames' +import moment from 'moment' import PropTypes from 'prop-types' import React, { useMemo } from 'react' import css from 'styled-jsx/css' @@ -59,6 +61,7 @@ const InterpretationModal = ({ }) => { const modalContentWidth = useModalContentWidth() const modalContentCSS = getModalContentCSS(modalContentWidth) + const { fromServerDate } = useTimeZoneConversion() const currentUser = useInterpretationsCurrentUser() const { data: interpretation, @@ -103,17 +106,17 @@ const InterpretationModal = ({ dataTest="interpretation-modal" >

- - {i18n.t( - 'Viewing interpretation: {{- visualisationName}}', - { - visualisationName: - visualization.displayName || - visualization.name, - nsSeparator: '^^', - } - )} - + {interpretation?.created + ? i18n.t( + 'Viewing interpretation data from {{- interpretationDate}}', + { + interpretationDate: moment( + fromServerDate(interpretation.created) + ).format('LL'), + nsSeparator: '^^', + } + ) + : i18n.t('Viewing interpretation data')}

@@ -157,8 +160,8 @@ const InterpretationModal = ({
- {modalCSS.styles} @@ -166,19 +169,11 @@ const InterpretationModal = ({ diff --git a/src/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js b/src/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js index 5fb555899..8d4dba744 100644 --- a/src/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +++ b/src/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js @@ -92,12 +92,11 @@ export const InterpretationsUnit = ({ .container { position: relative; padding: ${spacers.dp16}; - border-bottom: 1px solid ${colors.grey400}; background-color: ${colors.white}; } .expanded { - padding-bottom: ${spacers.dp32}; + padding-block-end: ${spacers.dp24}; } .loader { @@ -111,6 +110,7 @@ export const InterpretationsUnit = ({ display: flex; justify-content: space-between; cursor: pointer; + margin-block-end: ${spacers.dp12}; } .title { diff --git a/src/components/Interpretations/common/Interpretation/Interpretation.js b/src/components/Interpretations/common/Interpretation/Interpretation.js index 347c824e2..531aae9f8 100644 --- a/src/components/Interpretations/common/Interpretation/Interpretation.js +++ b/src/components/Interpretations/common/Interpretation/Interpretation.js @@ -1,6 +1,5 @@ import i18n from '@dhis2/d2-i18n' import { - Button, SharingDialog, IconReply16, IconShare16, @@ -71,6 +70,15 @@ export const Interpretation = ({ created={interpretation.created} username={interpretation.createdBy.displayName} > + {shouldShowButton && ( + onClick(id)} + dataTest="interpretation-view-button" + label={i18n.t('Open…')} + /> + )} {!disabled && ( 0 + ? interpretation.likes + : undefined + } disabled={toggleLikeInProgress} dataTest="interpretation-like-unlike-button" /> @@ -90,7 +102,11 @@ export const Interpretation = ({ tooltipContent={tooltip} iconComponent={IconReply16} onClick={() => onReplyIconClick?.(id)} - count={interpretation.comments.length} + label={ + interpretation.comments.length > 0 + ? interpretation.comments.length + : undefined + } dataTest="interpretation-reply-button" viewOnly={isInThread && !interpretationAccess.comment} /> @@ -146,18 +162,6 @@ export const Interpretation = ({ )} - {shouldShowButton && ( - - )} ) } diff --git a/src/components/Interpretations/common/Interpretation/InterpretationDeleteButton.js b/src/components/Interpretations/common/Interpretation/InterpretationDeleteButton.js index 0cc345a07..4d25e3f29 100644 --- a/src/components/Interpretations/common/Interpretation/InterpretationDeleteButton.js +++ b/src/components/Interpretations/common/Interpretation/InterpretationDeleteButton.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types' import React from 'react' import { useDeleteInterpretation } from '../../InterpretationsProvider/hooks.js' import { MessageIconButton } from '../index.js' +import { useConfirmClick } from '../useConfirmClick.js' const InterpretationDeleteButton = ({ id, onComplete }) => { const { show: showErrorAlert } = useAlert( @@ -16,12 +17,19 @@ const InterpretationDeleteButton = ({ id, onComplete }) => { onComplete, showErrorAlert, }) + const { isConfirming, onClick } = useConfirmClick(remove) return ( diff --git a/src/components/Interpretations/common/Interpretation/InterpretationSharingLink.js b/src/components/Interpretations/common/Interpretation/InterpretationSharingLink.js index f13f6a94a..55de7c7bb 100644 --- a/src/components/Interpretations/common/Interpretation/InterpretationSharingLink.js +++ b/src/components/Interpretations/common/Interpretation/InterpretationSharingLink.js @@ -1,5 +1,5 @@ import i18n from '@dhis2/d2-i18n' -import { SharingDialog, colors, spacers } from '@dhis2/ui' +import { Button, SharingDialog } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useState } from 'react' @@ -8,12 +8,13 @@ const InterpretationSharingLink = ({ type, id }) => { return ( <>
- setShowSharingDialog(true)} > {i18n.t('Manage sharing')} - +
{showSharingDialog && ( { )} diff --git a/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js b/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js index 1d70deb01..a360f6dff 100644 --- a/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js +++ b/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js @@ -1,6 +1,6 @@ import { useAlert } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { Button, spacers, colors } from '@dhis2/ui' +import { Button } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useState } from 'react' import { RichTextEditor } from '../../../RichText/index.js' @@ -38,45 +38,32 @@ export const InterpretationUpdateForm = ({ : '' return ( -
- - + + + + + + {showSharingLink && ( )} - - - - - - -
+ + ) } InterpretationUpdateForm.propTypes = { diff --git a/src/components/Interpretations/common/Message/Message.js b/src/components/Interpretations/common/Message/Message.js index 94bd37b5a..539a98ebd 100644 --- a/src/components/Interpretations/common/Message/Message.js +++ b/src/components/Interpretations/common/Message/Message.js @@ -1,5 +1,5 @@ import { useTimeZoneConversion } from '@dhis2/app-runtime' -import { UserAvatar, spacers, colors } from '@dhis2/ui' +import { spacers, colors, UserAvatar } from '@dhis2/ui' import moment from 'moment' import PropTypes from 'prop-types' import React from 'react' @@ -9,46 +9,68 @@ const Message = ({ children, text, created, username }) => { const { fromServerDate } = useTimeZoneConversion() return (
  • -
    - - {username} - +
    +
    -
    - {text} +
    +
    + {username} + +
    +
    + {text} +
    +
    {children}
    -
    {children}
  • diff --git a/src/components/Interpretations/common/Message/MessageButtonStrip.js b/src/components/Interpretations/common/Message/MessageButtonStrip.js index f2768c968..685e518f2 100644 --- a/src/components/Interpretations/common/Message/MessageButtonStrip.js +++ b/src/components/Interpretations/common/Message/MessageButtonStrip.js @@ -8,8 +8,9 @@ const MessageButtonStrip = ({ children }) => ( diff --git a/src/components/Interpretations/common/Message/MessageEditorContainer.js b/src/components/Interpretations/common/Message/MessageEditorContainer.js index 7f250a649..43eeaa788 100644 --- a/src/components/Interpretations/common/Message/MessageEditorContainer.js +++ b/src/components/Interpretations/common/Message/MessageEditorContainer.js @@ -5,17 +5,17 @@ import React from 'react' const MessageEditorContainer = ({ children, currentUserName, dataTest }) => (
    - +
    {children}
    )} @@ -93,9 +129,10 @@ MessageIconButton.propTypes = { iconComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]) .isRequired, tooltipContent: PropTypes.string.isRequired, - count: PropTypes.number, + confirming: PropTypes.bool, dataTest: PropTypes.string, disabled: PropTypes.bool, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), selected: PropTypes.bool, viewOnly: PropTypes.bool, onClick: PropTypes.func, diff --git a/src/components/Interpretations/common/Message/MessageInput.js b/src/components/Interpretations/common/Message/MessageInput.js index e000f29e2..2e202832a 100644 --- a/src/components/Interpretations/common/Message/MessageInput.js +++ b/src/components/Interpretations/common/Message/MessageInput.js @@ -13,7 +13,8 @@ const MessageInput = forwardRef((props, ref) => ( user-select: text; color: ${colors.grey900}; background-color: white; - padding: 12px 11px 10px; + padding: 7px 8px; + max-height: 32px; outline: 0; border: 1px solid ${colors.grey500}; border-radius: 3px; diff --git a/src/components/Interpretations/common/Message/MessageStatsBar.js b/src/components/Interpretations/common/Message/MessageStatsBar.js index b83e604bd..772f2d3c2 100644 --- a/src/components/Interpretations/common/Message/MessageStatsBar.js +++ b/src/components/Interpretations/common/Message/MessageStatsBar.js @@ -8,7 +8,8 @@ const MessageStatsBar = ({ children }) => (
    diff --git a/src/components/Interpretations/common/useConfirmClick.js b/src/components/Interpretations/common/useConfirmClick.js new file mode 100644 index 000000000..21e98360b --- /dev/null +++ b/src/components/Interpretations/common/useConfirmClick.js @@ -0,0 +1,35 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +const CONFIRM_TIMEOUT_MS = 3000 + +const useConfirmClick = (action) => { + const [isConfirming, setIsConfirming] = useState(false) + const timeoutRef = useRef(null) + + const clearResetTimer = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + }, []) + + useEffect(() => clearResetTimer, [clearResetTimer]) + + const onClick = useCallback(() => { + if (isConfirming) { + clearResetTimer() + setIsConfirming(false) + action() + } else { + setIsConfirming(true) + timeoutRef.current = setTimeout(() => { + timeoutRef.current = null + setIsConfirming(false) + }, CONFIRM_TIMEOUT_MS) + } + }, [isConfirming, action, clearResetTimer]) + + return { isConfirming, onClick } +} + +export { useConfirmClick } diff --git a/src/components/RichText/Editor/Editor.js b/src/components/RichText/Editor/Editor.js index 82ab625df..10bcc0050 100644 --- a/src/components/RichText/Editor/Editor.js +++ b/src/components/RichText/Editor/Editor.js @@ -1,14 +1,13 @@ import i18n from '@dhis2/d2-i18n' import { - Button, Popover, Tooltip, Help, - IconAt24, - IconFaceAdd24, - IconLink24, - IconTextBold24, - IconTextItalic24, + IconAt16, + IconFaceAdd16, + IconLink16, + IconTextBold16, + IconTextItalic16, colors, } from '@dhis2/ui' import cx from 'classnames' @@ -34,6 +33,7 @@ import { toolbarClasses, tooltipAnchorClasses, emojisPopoverClasses, + toolbarButtonClasses, } from './styles/Editor.style.js' const EmojisPopover = ({ onInsertMarkdown, onClose, reference }) => ( @@ -64,7 +64,12 @@ EmojisPopover.propTypes = { const IconButtonWithTooltip = ({ tooltipContent, disabled, icon, onClick }) => ( <> - + {({ ref, onMouseOver, onMouseOut }) => ( ( onMouseOut={onMouseOut} className="tooltip" > - )} + ) @@ -114,32 +121,32 @@ const Toolbar = ({ } + icon={} onClick={() => onInsertMarkdown(BOLD)} /> } + icon={} onClick={() => onInsertMarkdown(ITALIC)} /> } + icon={} onClick={() => onInsertMarkdown(LINK)} /> } + icon={} onClick={() => onInsertMarkdown(MENTION)} /> } + icon={} onClick={() => setEmojisPopoverIsOpen(true)} /> @@ -156,30 +163,31 @@ const Toolbar = ({
    - +
    ) : (
    - +
    )} + ) } @@ -279,6 +287,7 @@ export const Editor = forwardRef( placeholder={inputPlaceholder} disabled={disabled} value={value} + rows={5} onChange={(event) => onChange(event.target.value) } diff --git a/src/components/RichText/Editor/styles/Editor.style.js b/src/components/RichText/Editor/styles/Editor.style.js index 607be4c8d..306864e8b 100644 --- a/src/components/RichText/Editor/styles/Editor.style.js +++ b/src/components/RichText/Editor/styles/Editor.style.js @@ -10,14 +10,21 @@ export const mainClasses = css` } .preview { - padding: ${spacers.dp8} ${spacers.dp12}; + padding: 0 ${spacers.dp8} ${spacers.dp8} ${spacers.dp8}; font-size: 14px; line-height: ${spacers.dp16}; color: ${colors.grey900}; + border: 1px solid ${colors.grey500}; + border-block-start: none; + border-radius: 0 0 3px 3px; overflow-y: auto; scroll-behavior: smooth; } + .preview :global(p:first-of-type) { + margin: 0; + } + .edit { width: 100%; height: 100%; @@ -28,19 +35,20 @@ export const mainClasses = css` width: 100%; height: 100%; box-sizing: border-box; - padding: ${spacers.dp8} 15px; + padding: ${spacers.dp4} ${spacers.dp8} ${spacers.dp8} ${spacers.dp8}; color: ${colors.grey900}; background-color: ${colors.white}; border: 1px solid ${colors.grey500}; - border-radius: 3px; - box-shadow: inset 0 0 0 1px rgba(102, 113, 123, 0.15), - inset 0 1px 2px 0 rgba(102, 113, 123, 0.1); + border-block-start: none; + border-radius: 0 0 3px 3px; + + box-shadow: inset 0 0 1px 0 rgba(48, 54, 60, 0.1); outline: 0; font-size: 14px; - line-height: ${spacers.dp16}; + line-height: 21px; user-select: text; resize: none; } @@ -49,13 +57,8 @@ export const mainClasses = css` resize: vertical; } - .textarea:focus { - outline: none; - box-shadow: 0 0 0 3px ${theme.focus}; - width: calc(100% - 6px); - height: calc(100% - 3px); - padding: ${spacers.dp8} ${spacers.dp12}; - margin-left: 3px; + .textarea::placeholder { + color: ${colors.grey600}; } .textarea:disabled { @@ -68,10 +71,10 @@ export const mainClasses = css` export const toolbarClasses = css` .toolbar { - background: ${colors.grey050}; - border-radius: 3px; - border: 1px solid ${colors.grey300}; - margin-bottom: ${spacers.dp4}; + background: ${colors.white}; + border-radius: 3px 3px 0 0; + border: 1px solid ${colors.grey500}; + border-bottom-color: transparent; } .actionsWrapper { @@ -80,13 +83,13 @@ export const toolbarClasses = css` gap: ${spacers.dp4}; align-items: center; justify-content: space-between; - padding: ${spacers.dp4}; + padding: ${spacers.dp4} ${spacers.dp4} 0 ${spacers.dp4}; } .mainActions { display: flex; - gap: ${spacers.dp4}; - margin-top: ${spacers.dp2}; + gap: 0; + margin-top: 0; } .sideActions { @@ -106,6 +109,41 @@ export const tooltipAnchorClasses = css` } ` +export const toolbarButtonClasses = css` + .toolbarButton { + display: inline-flex; + align-items: center; + justify-content: center; + padding: ${spacers.dp4} 6px; + background: none; + border: none; + border-radius: 3px; + cursor: pointer; + color: ${colors.grey700}; + font-size: 13px; + font-family: inherit; + line-height: 1.4; + } + + .toolbarButton:hover:not(:disabled) { + background-color: ${colors.grey200}; + } + + .toolbarButton:active:not(:disabled) { + background-color: ${colors.grey300}; + } + + .toolbarButton:disabled { + cursor: not-allowed; + color: ${colors.grey500}; + } + + .toolbarButton:focus-visible { + outline: 2px solid ${theme.focus}; + outline-offset: -2px; + } +` + export const emojisPopoverClasses = css` .emojisList { display: flex; From 8bb39aaec62e3eeb4f683301e45075e676b782fe Mon Sep 17 00:00:00 2001 From: Joseph John Aas Cooper <33054985+cooper-joe@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:49:01 +0200 Subject: [PATCH 3/3] chore: change storybook error variable usage --- src/__demo__/InterpretationModal.stories.js | 4 ++-- src/__demo__/InterpretationThread.stories.js | 4 ++-- src/__demo__/InterpretationsUnit.stories.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/__demo__/InterpretationModal.stories.js b/src/__demo__/InterpretationModal.stories.js index 446abd8a0..04789cb74 100644 --- a/src/__demo__/InterpretationModal.stories.js +++ b/src/__demo__/InterpretationModal.stories.js @@ -303,7 +303,7 @@ WithManyComments.story = { name: 'With many comments', } -export const Error = () => ( +export const ErrorState = () => ( Promise.reject( @@ -328,6 +328,6 @@ export const Error = () => ( ) -Error.story = { +ErrorState.story = { name: 'Error', } diff --git a/src/__demo__/InterpretationThread.stories.js b/src/__demo__/InterpretationThread.stories.js index 4ee6c539d..493ed664c 100644 --- a/src/__demo__/InterpretationThread.stories.js +++ b/src/__demo__/InterpretationThread.stories.js @@ -250,7 +250,7 @@ Loading.story = { name: 'Loading', } -export const Error = () => ( +export const ErrorState = () => ( Promise.reject( @@ -267,6 +267,6 @@ export const Error = () => ( ) -Error.story = { +ErrorState.story = { name: 'Error', } diff --git a/src/__demo__/InterpretationsUnit.stories.js b/src/__demo__/InterpretationsUnit.stories.js index c5f392796..5f610cc86 100644 --- a/src/__demo__/InterpretationsUnit.stories.js +++ b/src/__demo__/InterpretationsUnit.stories.js @@ -100,7 +100,7 @@ Loading.story = { name: 'Loading', } -export const Error = () => ( +export const ErrorState = () => ( Promise.reject( @@ -114,7 +114,7 @@ export const Error = () => ( ) -Error.story = { +ErrorState.story = { name: 'Error', }