From 734d47dc89f67202b8c0e729af8098c4244be5cf Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Sun, 22 Oct 2023 12:25:26 +0300 Subject: [PATCH 1/4] feat(list): implement more menu --- src/components/sectionList/SectionListRow.tsx | 58 ++++++++++-------- .../listActions/SectionListActions.module.css | 12 ++++ .../listActions/SectionListActions.tsx | 60 +++++++++++++++++++ 3 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 src/components/sectionList/listActions/SectionListActions.module.css create mode 100644 src/components/sectionList/listActions/SectionListActions.tsx diff --git a/src/components/sectionList/SectionListRow.tsx b/src/components/sectionList/SectionListRow.tsx index af55562b..befc83cb 100644 --- a/src/components/sectionList/SectionListRow.tsx +++ b/src/components/sectionList/SectionListRow.tsx @@ -5,6 +5,11 @@ import React from 'react' import { Link } from 'react-router-dom' import { CheckBoxOnChangeObject } from '../../types' import { IdentifiableObject, GistModel } from '../../types/models' +import { + ListActions, + ActionEdit, + ActionMore, +} from './listActions/SectionListActions' import css from './SectionList.module.css' import { SelectedColumns, SelectedColumn } from './types' @@ -51,35 +56,38 @@ export function SectionListRow({ ))} - + + + + ) } -const ListActions = ({ modelId }: { modelId: string }) => { - return ( -
- - -
- ) -} +// const ListActions = ({ modelId }: { modelId: string }) => { +// return ( +//
+// +// +//
+// ) +// } -const ActionEdit = ({ modelId }: { modelId: string }) => { - return ( - - - - ) -} +// const ActionEdit = ({ modelId }: { modelId: string }) => { +// return ( +// +// +// +// ) +// } -const ActionMore = () => { - return ( - - ) -} +// const ActionMore = () => { +// return ( +// +// ) +// } diff --git a/src/components/sectionList/listActions/SectionListActions.module.css b/src/components/sectionList/listActions/SectionListActions.module.css new file mode 100644 index 00000000..dd09368d --- /dev/null +++ b/src/components/sectionList/listActions/SectionListActions.module.css @@ -0,0 +1,12 @@ +.listActions { + display: flex; + gap: var(--spacers-dp8); +} + +.listActions button { + 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..be2e27f7 --- /dev/null +++ b/src/components/sectionList/listActions/SectionListActions.tsx @@ -0,0 +1,60 @@ +import { + Button, + FlyoutMenu, + IconEdit16, + IconEdit24, + IconMore16, + IconMore24, + MenuItem, + Popover, +} from '@dhis2/ui' +import React, { useRef, useState } from 'react' +import { Link } from 'react-router-dom' +import css from './SectionListActions.module.css' + +export const ListActions = ({ children }: React.PropsWithChildren) => { + return
{children}
+} + +export const ActionEdit = ({ modelId }: { modelId: string }) => { + return ( + + + + ) +} + +export const ActionMore = () => { + const [open, setOpen] = useState(false) + const ref = useRef(null) + return ( +
+ +
+ ) +} From 0ff853d894f263f130f74feda0ecc9c6f8aa63b1 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 23 Oct 2023 15:58:24 +0300 Subject: [PATCH 2/4] feat(list): add more dropdown --- i18n/en.pot | 7 +- src/components/sectionList/SectionListRow.tsx | 9 +-- .../sectionList/SectionListWrapper.tsx | 19 +++++ .../listActions/SectionListActions.tsx | 74 ++++++++++++------- 4 files changed, 75 insertions(+), 34 deletions(-) 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/sectionList/SectionListRow.tsx b/src/components/sectionList/SectionListRow.tsx index befc83cb..8d1503fe 100644 --- a/src/components/sectionList/SectionListRow.tsx +++ b/src/components/sectionList/SectionListRow.tsx @@ -18,6 +18,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 @@ -30,6 +31,7 @@ export function SectionListRow({ onSelect, onClick, selected, + renderActions, renderColumnValue, }: SectionListRowProps) { return ( @@ -55,12 +57,7 @@ export function SectionListRow({ {renderColumnValue(selectedColumn)} ))} - - - - - - + {renderActions(modelData.id)} ) } 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.tsx b/src/components/sectionList/listActions/SectionListActions.tsx index be2e27f7..6689bee8 100644 --- a/src/components/sectionList/listActions/SectionListActions.tsx +++ b/src/components/sectionList/listActions/SectionListActions.tsx @@ -1,3 +1,4 @@ +import i18n from '@dhis2/d2-i18n' import { Button, FlyoutMenu, @@ -9,7 +10,7 @@ import { Popover, } from '@dhis2/ui' import React, { useRef, useState } from 'react' -import { Link } from 'react-router-dom' +import { Link, useHref, useLinkClickHandler } from 'react-router-dom' import css from './SectionListActions.module.css' export const ListActions = ({ children }: React.PropsWithChildren) => { @@ -26,35 +27,56 @@ export const ActionEdit = ({ modelId }: { modelId: string }) => { ) } -export const ActionMore = () => { +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 handleClick = useLinkClickHandler(modelId) + return (
- + + {open && ( + setOpen(false)} + > + + } + onClick={onShowDetailsClick} + /> + } + onClick={(_, e) => { + handleClick(e) + setOpen(false) + }} + target="_blank" + href={href} + > + + + )}
) } From d6f076cb1e9fc588471afca2b933bba85832f9f1 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Tue, 20 Feb 2024 15:24:13 +0100 Subject: [PATCH 3/4] refactor: cleanup --- src/components/sectionList/SectionListRow.tsx | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/src/components/sectionList/SectionListRow.tsx b/src/components/sectionList/SectionListRow.tsx index 8d1503fe..23e2edd8 100644 --- a/src/components/sectionList/SectionListRow.tsx +++ b/src/components/sectionList/SectionListRow.tsx @@ -1,15 +1,8 @@ -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 { - ListActions, - ActionEdit, - ActionMore, -} from './listActions/SectionListActions' import css from './SectionList.module.css' import { SelectedColumns, SelectedColumn } from './types' @@ -61,30 +54,3 @@ export function SectionListRow({ ) } - -// const ListActions = ({ modelId }: { modelId: string }) => { -// return ( -//
-// -// -//
-// ) -// } - -// const ActionEdit = ({ modelId }: { modelId: string }) => { -// return ( -// -// -// -// ) -// } - -// const ActionMore = () => { -// return ( -// -// ) -// } From c19814f6b3b983687bf76d15aa7488fab1165b23 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Tue, 20 Feb 2024 16:54:17 +0100 Subject: [PATCH 4/4] fix: add linkbutton component --- .../LinkButton/LinkButton.module.css | 288 ++++++++++++++++++ src/components/LinkButton/LinkButton.tsx | 87 ++++++ src/components/LinkButton/index.ts | 1 + .../listActions/SectionListActions.module.css | 3 +- .../listActions/SectionListActions.tsx | 19 +- 5 files changed, 389 insertions(+), 9 deletions(-) create mode 100644 src/components/LinkButton/LinkButton.module.css create mode 100644 src/components/LinkButton/LinkButton.tsx create mode 100644 src/components/LinkButton/index.ts 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/listActions/SectionListActions.module.css b/src/components/sectionList/listActions/SectionListActions.module.css index dd09368d..8e42de04 100644 --- a/src/components/sectionList/listActions/SectionListActions.module.css +++ b/src/components/sectionList/listActions/SectionListActions.module.css @@ -3,7 +3,8 @@ gap: var(--spacers-dp8); } -.listActions button { +.listActions button, +.listActions a { padding: 0 2px !important; } diff --git a/src/components/sectionList/listActions/SectionListActions.tsx b/src/components/sectionList/listActions/SectionListActions.tsx index 6689bee8..8c71ec99 100644 --- a/src/components/sectionList/listActions/SectionListActions.tsx +++ b/src/components/sectionList/listActions/SectionListActions.tsx @@ -11,6 +11,7 @@ import { } 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) => { @@ -19,11 +20,9 @@ export const ListActions = ({ children }: React.PropsWithChildren) => { export const ActionEdit = ({ modelId }: { modelId: string }) => { return ( - - - + + + ) } @@ -38,7 +37,8 @@ export const ActionMore = ({ const [open, setOpen] = useState(false) const ref = useRef(null) const href = useHref(modelId, { relative: 'path' }) - const handleClick = useLinkClickHandler(modelId) + + const handleEditClick = useLinkClickHandler(modelId) return (
@@ -61,14 +61,17 @@ export const ActionMore = ({ dense label={i18n.t('Show details')} icon={} - onClick={onShowDetailsClick} + onClick={() => { + onShowDetailsClick() + setOpen(false) + }} /> } onClick={(_, e) => { - handleClick(e) + handleEditClick(e) setOpen(false) }} target="_blank"