diff --git a/i18n/en.pot b/i18n/en.pot index e756f185..83e93b16 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-03-01T17:52:14.520Z\n" -"PO-Revision-Date: 2024-03-01T17:52:14.520Z\n" +"POT-Creation-Date: 2024-03-05T18:46:53.934Z\n" +"PO-Revision-Date: 2024-03-05T18:46:53.934Z\n" msgid "schemas" msgstr "schemas" @@ -177,6 +177,9 @@ msgstr "Search by name, code or ID" msgid "Public access" msgstr "Public access" +msgid "Show details" +msgstr "Show details" + msgid "At least one column must be selected" msgstr "At least one column must be selected" diff --git a/src/components/LinkButton/LinkButton.module.css b/src/components/LinkButton/LinkButton.module.css new file mode 100644 index 00000000..9635fea8 --- /dev/null +++ b/src/components/LinkButton/LinkButton.module.css @@ -0,0 +1,288 @@ +.linkButton { + display: inline-flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 4px; + font-weight: 400; + letter-spacing: 0.5px; + text-decoration: none; + cursor: pointer; + user-select: none; + color: var(--colors-grey900); + + /*medium*/ + height: 36px; + padding: 0 var(--spacers-dp12); + font-size: 14px; + line-height: 16px; + + /*basic*/ + border: 1px solid var(--colors-grey500); + background-color: #f9fafb; +} + +.linkButton:disabled { + cursor: not-allowed; +} + +.linkButton:focus { + outline: 3px solid blue; + outline-offset: -3px; + text-decoration: underline; +} + +/* Prevent focus styles when mouse clicking */ +.linkButton:focus:not(:focus-visible) { + outline: none; + text-decoration: none; +} + +/* Prevent focus styles on active and disabled buttons */ +.linkButton:active:focus, +.linkButton:disabled:focus { + outline: none; + text-decoration: none; +} + +.linkButton:hover { + border-color: var(--colors-grey500); + background-color: var(--colors-grey200); +} + +.linkButton:active, +.linkButton:active:focus { + border-color: var(--colors-grey500); + background-color: var(--colors-grey200); + box-shadow: 0 0 0 1px rgb(0, 0, 0, 0.1) inset; +} + +.linkButton:focus { + background-color: #f9fafb; +} + +.linkButton:disabled { + border-color: var(--colors-grey400); + background-color: #f9fafb; + box-shadow: none; + color: var(--theme-disabled); + fill: var(--theme-disabled); +} + +.small { + height: 28px; + padding: 0 6px; + font-size: 14px; + line-height: 16px; +} + +.large { + height: 43px; + padding: 0 var(--spacers-dp24); + font-size: 16px; + letter-spacing: 0.57px; + line-height: 19px; +} + +.primary { + border-color: var(--theme-primary800); + background: linear-gradient(180deg, #1565c0 0%, #0650a3 100%); + background-color: #2b61b3; + color: var(--colors-white); + fill: var(--colors-white); + font-weight: 500; +} + +.primary:hover { + border-color: var(--theme-primary800); + background: linear-gradient(180deg, #054fa3 0%, #034793 100%); + background-color: #21539f; +} + +.primary:active, +.primary:active:focus { + background: linear-gradient(180deg, #054fa3 0%, #034793 100%); + background-color: #1c4a90; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18) inset; +} + +.primary:focus { + background: var(--colors-blue800); + border-color: var(--colors-blue900); + outline-offset: -5px; +} + +.primary:disabled { + border-color: #93a6bd; + background: #b3c6de; + box-shadow: none; + color: var(--colors-white); + fill: var(--colors-white); +} + +.secondary { + border-color: rgba(74, 87, 104, 0.25); + background-color: transparent; +} + +.secondary:hover { + border-color: rgba(74, 87, 104, 0.5); + background-color: rgba(160, 173, 186, 0.05); +} + +.secondary:active, +.secondary:active:focus { + background-color: rgba(160, 173, 186, 0.2); + box-shadow: none; +} + +.secondary:focus { + background-color: transparent; +} + +.secondary:disabled { + border-color: rgba(74, 87, 104, 0.25); + background-color: transparent; + box-shadow: none; + color: var(--theme-disabled); + fill: var(--theme-disabled); +} + +.destructive { + border-color: #a10b0b; + background: linear-gradient(180deg, #d32f2f 0%, #b71c1c 100%); + background-color: #b9242b; + color: var(--colors-white); + fill: var(--colors-white); + font-weight: 500; +} + +.destructive:hover { + border-color: #a10b0b; + background: linear-gradient(180deg, #b81c1c 0%, #b80c0b 100%); + background-color: #ac0f1a; +} + +.destructive:active, +.destructive:active:focus { + background: linear-gradient(180deg, #b81c1c 0%, #b80c0b 100%); + background-color: #ac101b; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18) inset; +} + +.destructive:focus { + background: linear-gradient(180deg, #d32f2f 0%, #b71c1c 100%); + background-color: #b72229; +} + +.destructive:disabled { + border-color: #c59898; + background: #d6a8a8; + box-shadow: none; + color: var(--colors-white); + fill: var(--colors-white); +} + +.destructive.secondary { + border-color: rgba(74, 87, 104, 0.25); + background: transparent; + color: var(--colors-red700); + fill: var(--colors-red700); + font-weight: 400; +} + +.destructive.secondary:hover { + border-color: var(--colors-red600); + background: var(--colors-red050); + color: var(--colors-red800); + fill: var(--colors-red800); +} + +.destructive.secondary:active, +.destructive.secondary:active:focus { + background: var(--colors-red100); + border-color: var(--colors-red700); + box-shadow: none; +} + +.destructive.secondary:disabled { + background: transparent; + border-color: rgba(74, 87, 104, 0.25); + color: rgba(183, 28, 28, 0.6); + fill: rgba(183, 28, 28, 0.6); +} + +.icon-only { + padding: 0 0 0 5px; +} + +.button-icon { + margin-right: 6px; + color: inherit; + fill: inherit; + font-size: 26px; + vertical-align: middle; + pointer-events: none; +} + +.icon-only .button-icon { + margin-right: 5px; +} + +.small.icon-only { + padding: 0 4px 0 5px; +} + +.small .button-icon { + margin-right: 2px; +} + +.small.icon-only .button-icon { + margin-right: 1px; +} + +.toggled { + background: var(--colors-grey700); + border: 1px solid var(--colors-grey900); + color: var(--colors-grey050); + fill: var(--colors-grey050); +} + +.toggled:focus { + background: var(--colors-grey800); +} + +.toggled:hover { + background: var(--colors-grey800); + border-color: var(--colors-grey900); +} + +.toggled:active, +.toggled:active:focus { + background: var(--colors-grey900); + border-color: var(--colors-grey900); +} + +.toggled:disabled { + background: var(--colors-grey500); + border-color: var(--colors-grey600); + color: var(--colors-grey050); + fill: var(--colors-grey050); +} + +.loader { + width: 16px; + height: 16px; + margin-right: 8px; +} + +.loader + .button-icon { + display: none; +} + +.icon-only .loader { + margin: 0 8px 0 4px; +} +.small.icon-only .loader { + margin: 0 4px; +} diff --git a/src/components/LinkButton/LinkButton.tsx b/src/components/LinkButton/LinkButton.tsx new file mode 100644 index 00000000..528ac879 --- /dev/null +++ b/src/components/LinkButton/LinkButton.tsx @@ -0,0 +1,87 @@ +import { ButtonProps } from '@dhis2/ui' +import cx from 'classnames' +import React, { AnchorHTMLAttributes } from 'react' +import { useLinkClickHandler, useHref } from 'react-router-dom' +import css from './LinkButton.module.css' + +type UseLinkClickHandlerParameters = Parameters + +type LinkClickHandlerOptions = UseLinkClickHandlerParameters[1] + +type RelevantButtonProps = Pick< + ButtonProps, + | 'disabled' + | 'className' + | 'primary' + | 'secondary' + | 'small' + | 'toggled' + | 'large' + | 'destructive' +> + +type LinkButtonProps = AnchorHTMLAttributes & + LinkClickHandlerOptions & + RelevantButtonProps & { + to: Parameters[0] + } + +/* Wrapping button with anchor-tags are not valid, style anchor as a UI-button */ +export const LinkButton = ({ + onClick, + disabled, + className, + primary, + secondary, + small, + toggled, + large, + destructive, + target, + replace, + state, + preventScrollReset, + relative, + to, + href, + ...anchorProps +}: LinkButtonProps) => { + const resolvedHref = useHref(to, { relative }) + const handleClickInternal = useLinkClickHandler(to, { + replace, + state, + preventScrollReset, + relative, + target, + }) + + const handleClick = ( + event: React.MouseEvent + ) => { + if (onClick) { + onClick(event) + } + if (!event.defaultPrevented) { + handleClickInternal(event) + } + } + + const resolvedClassname = cx(css.linkButton, className, { + [css.disabled]: disabled, + [css.primary]: primary, + [css.secondary]: secondary, + [css.destructive]: destructive, + [css.toggled]: toggled, + [css.large]: large, + [css.small]: small, + }) + return ( + + ) +} diff --git a/src/components/LinkButton/index.ts b/src/components/LinkButton/index.ts new file mode 100644 index 00000000..793960cd --- /dev/null +++ b/src/components/LinkButton/index.ts @@ -0,0 +1 @@ +export * from './LinkButton' diff --git a/src/components/sectionList/SectionListRow.tsx b/src/components/sectionList/SectionListRow.tsx index af55562b..23e2edd8 100644 --- a/src/components/sectionList/SectionListRow.tsx +++ b/src/components/sectionList/SectionListRow.tsx @@ -1,8 +1,6 @@ -import { DataTableRow, DataTableCell, Checkbox, Button } from '@dhis2/ui' -import { IconEdit24, IconMore24 } from '@dhis2/ui-icons' +import { DataTableRow, DataTableCell, Checkbox } from '@dhis2/ui' import cx from 'classnames' import React from 'react' -import { Link } from 'react-router-dom' import { CheckBoxOnChangeObject } from '../../types' import { IdentifiableObject, GistModel } from '../../types/models' import css from './SectionList.module.css' @@ -13,6 +11,7 @@ export type SectionListRowProps = { selectedColumns: SelectedColumns onSelect: (modelId: string, checked: boolean) => void selected: boolean + renderActions: (modelId: string) => React.ReactNode renderColumnValue: (column: SelectedColumn) => React.ReactNode onClick?: (modelData: GistModel | Model) => void active?: boolean @@ -25,6 +24,7 @@ export function SectionListRow({ onSelect, onClick, selected, + renderActions, renderColumnValue, }: SectionListRowProps) { return ( @@ -50,36 +50,7 @@ export function SectionListRow({ {renderColumnValue(selectedColumn)} ))} - - - + {renderActions(modelData.id)} ) } - -const ListActions = ({ modelId }: { modelId: string }) => { - return ( -
- - -
- ) -} - -const ActionEdit = ({ modelId }: { modelId: string }) => { - return ( - - - - ) -} - -const ActionMore = () => { - return ( - - ) -} diff --git a/src/components/sectionList/SectionListWrapper.tsx b/src/components/sectionList/SectionListWrapper.tsx index 1f1c9052..45a3a2c1 100644 --- a/src/components/sectionList/SectionListWrapper.tsx +++ b/src/components/sectionList/SectionListWrapper.tsx @@ -4,6 +4,11 @@ import { useSchemaFromHandle } from '../../lib' import { Pager, ModelCollection } from '../../types/models' import { DetailsPanel, DefaultDetailsPanelContent } from './detailsPanel' import { FilterWrapper } from './filters/FilterWrapper' +import { + ActionEdit, + ActionMore, + ListActions, +} from './listActions/SectionListActions' import { useModelListView } from './listView' import { ModelValue } from './modelValue/ModelValue' import { SectionList } from './SectionList' @@ -54,6 +59,9 @@ export const SectionListWrapper = ({ } } + const handleShowDetails = (id: string) => + setDetailsId((prevDetailsId) => (prevDetailsId === id ? undefined : id)) + const allSelected = useMemo(() => { return data?.length !== 0 && data?.length === selectedModels.size }, [data, selectedModels.size]) @@ -106,6 +114,17 @@ export const SectionListWrapper = ({ /> ) }} + renderActions={(id) => ( + + + + handleShowDetails(id) + } + /> + + )} /> ))} diff --git a/src/components/sectionList/listActions/SectionListActions.module.css b/src/components/sectionList/listActions/SectionListActions.module.css new file mode 100644 index 00000000..8e42de04 --- /dev/null +++ b/src/components/sectionList/listActions/SectionListActions.module.css @@ -0,0 +1,13 @@ +.listActions { + display: flex; + gap: var(--spacers-dp8); +} + +.listActions button, +.listActions a { + padding: 0 2px !important; +} + +.actionMorePopover { + box-shadow: none !important; +} diff --git a/src/components/sectionList/listActions/SectionListActions.tsx b/src/components/sectionList/listActions/SectionListActions.tsx new file mode 100644 index 00000000..8c71ec99 --- /dev/null +++ b/src/components/sectionList/listActions/SectionListActions.tsx @@ -0,0 +1,85 @@ +import i18n from '@dhis2/d2-i18n' +import { + Button, + FlyoutMenu, + IconEdit16, + IconEdit24, + IconMore16, + IconMore24, + MenuItem, + Popover, +} from '@dhis2/ui' +import React, { useRef, useState } from 'react' +import { Link, useHref, useLinkClickHandler } from 'react-router-dom' +import { LinkButton } from '../../LinkButton' +import css from './SectionListActions.module.css' + +export const ListActions = ({ children }: React.PropsWithChildren) => { + return
{children}
+} + +export const ActionEdit = ({ modelId }: { modelId: string }) => { + return ( + + + + ) +} + +type ActionMoreProps = { + modelId: string + onShowDetailsClick: () => void +} +export const ActionMore = ({ + modelId, + onShowDetailsClick, +}: ActionMoreProps) => { + const [open, setOpen] = useState(false) + const ref = useRef(null) + const href = useHref(modelId, { relative: 'path' }) + + const handleEditClick = useLinkClickHandler(modelId) + + return ( +
+ + {open && ( + setOpen(false)} + > + + } + onClick={() => { + onShowDetailsClick() + setOpen(false) + }} + /> + } + onClick={(_, e) => { + handleEditClick(e) + setOpen(false) + }} + target="_blank" + href={href} + > + + + )} +
+ ) +}