Compare commits

...

41 commits

Author SHA1 Message Date
Harshil Sharma
57298943bf Removed keyboard shortcut binding for testing status property modal 2023-02-15 17:08:58 +05:30
Harshil Sharma
9c52c2e5c0 Merge branch 'main' into status-property 2023-02-15 17:04:21 +05:30
Harshil Sharma
c03bf1446d IMported corect menu reference and fixde tests 2023-02-06 17:03:36 +05:30
Harshil Sharma
ecb2d445ed Revert "Added tests"
This reverts commit e5bc177345.
2023-02-06 16:52:21 +05:30
Harshil Sharma
c03539f33c Updated plugin snapshots 2023-02-06 16:24:39 +05:30
Harshil Sharma
b89bb99424 Fixed existing tests 2023-02-06 15:41:13 +05:30
Harshil Sharma
92ca9d9975 Added new snapshots 2023-02-06 15:36:07 +05:30
Harshil Sharma
e5bc177345 Added tests 2023-02-06 15:35:52 +05:30
Harshil Sharma
e2ce4b76c8 Test WIP 2023-02-06 14:33:40 +05:30
Harshil Sharma
b686e80dda Added tooltip text-wrap capability 2023-02-06 12:43:48 +05:30
Harshil Sharma
f9286dc53b Added tooltip text-wrap capability 2023-02-06 12:43:32 +05:30
Harshil Sharma
ec6ea0497b Merge branch 'main' into status-property 2023-02-06 11:17:39 +05:30
Harshil Sharma
b469c9678c Lint fix 2023-01-27 14:40:49 +05:30
Harshil Sharma
cd6db61930 Cleanup 2023-01-27 14:07:02 +05:30
Harshil Sharma
b32757ff6f Fixed issue with deep clone 2023-01-27 11:36:18 +05:30
Harshil Sharma
6fb01601d5 Merge branch 'main' into status-property 2023-01-27 08:27:00 +05:30
Harshil Sharma
f987a6b99e Added value menu 2023-01-25 20:08:28 +05:30
Harshil Sharma
cdf0926090 Merge branch 'main' into status-property 2023-01-25 13:53:59 +05:30
Harshil Sharma
30471005c0 Refined color picker 2023-01-25 13:08:26 +05:30
Harshil Sharma
75639e20fc Separated valuerow inco seperate component to resolve ref issue 2023-01-25 10:58:51 +05:30
Harshil Sharma
1e3d151ba4 Added color picker 2023-01-25 10:58:32 +05:30
Harshil Sharma
69910d8329 Fixed a bug with DND 2023-01-24 16:36:47 +05:30
Harshil Sharma
d009f80d7c DND in progress 2023-01-24 13:57:47 +05:30
Harshil Sharma
4112db4626 Merge branch 'main' into status-property 2023-01-24 07:11:45 +05:30
Harshil Sharma
68986e181a in-=progress add new option 2023-01-23 17:25:06 +05:30
Harshil Sharma
684c1b09a7 in-=progress add new option 2023-01-23 17:24:46 +05:30
Harshil Sharma
1be39c0ff4 Merge branch 'main' into status-property 2023-01-23 08:30:10 +05:30
Harshil Sharma
a7b94e345e Added edit color and text options 2023-01-20 19:15:43 +05:30
Harshil Sharma
9f5db533ac Adding edit value styling 2023-01-20 17:27:59 +05:30
Harshil Sharma
51e2b0b3f4 Adding edit value styling 2023-01-20 17:27:50 +05:30
Harshil Sharma
57b7295f2a Added check icon 2023-01-20 09:48:00 +05:30
Harshil Sharma
6922539159 Added empty states 2023-01-20 09:47:44 +05:30
Harshil Sharma
f3d4477245 Handled overflow 2023-01-20 08:23:24 +05:30
Harshil Sharma
33e2039dda Merge branch 'main' into status-property 2023-01-20 07:36:51 +05:30
Harshil Sharma
ac98daa792 Adding styles for DND 2023-01-19 19:16:25 +05:30
Harshil Sharma
a05ec7d6eb Adding styles for DND 2023-01-19 19:16:12 +05:30
Harshil Sharma
9d5a8ea6c7 Implemented DIalog as footerless ActionDialog 2023-01-19 15:35:17 +05:30
Harshil Sharma
d0bf7cff4a Added ActionDialog for dialog with two default action buttons 2023-01-19 15:11:12 +05:30
Harshil Sharma
adcc541cc2 Added ActionDialog for dialog with two default action buttons 2023-01-19 15:11:02 +05:30
Harshil Sharma
a3bb9247f6 Merge branch 'main' into status-property 2023-01-19 14:27:46 +05:30
Harshil Sharma
18d9f77837 Added base of UI for statndard properties edit dialog 2023-01-19 10:50:06 +05:30
34 changed files with 3241 additions and 163 deletions

View file

@ -6,7 +6,7 @@ exports[`components/boardSelector escape button should unmount the component 1`]
class="focalboard-body"
>
<div
class="Dialog dialog-back BoardSelector size--medium"
class="ActionDialog Dialog dialog-back BoardSelector size--medium"
>
<div
class="backdrop"
@ -15,7 +15,7 @@ exports[`components/boardSelector escape button should unmount the component 1`]
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -111,7 +111,7 @@ exports[`components/boardSelector renders with no results 1`] = `
class="focalboard-body"
>
<div
class="Dialog dialog-back BoardSelector size--medium"
class="ActionDialog Dialog dialog-back BoardSelector size--medium"
>
<div
class="backdrop"
@ -120,7 +120,7 @@ exports[`components/boardSelector renders with no results 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -217,7 +217,7 @@ exports[`components/boardSelector renders with some results 1`] = `
class="focalboard-body"
>
<div
class="Dialog dialog-back BoardSelector size--medium"
class="ActionDialog Dialog dialog-back BoardSelector size--medium"
>
<div
class="backdrop"
@ -226,7 +226,7 @@ exports[`components/boardSelector renders with some results 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -421,7 +421,7 @@ exports[`components/boardSelector renders without start searching 1`] = `
class="focalboard-body"
>
<div
class="Dialog dialog-back BoardSelector size--medium"
class="ActionDialog Dialog dialog-back BoardSelector size--medium"
>
<div
class="backdrop"
@ -430,7 +430,7 @@ exports[`components/boardSelector renders without start searching 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -3,7 +3,7 @@
"rules": {
"indentation": 4,
"selector-class-pattern": "[a-zA-Z_-]+",
"max-nesting-depth": 4,
"max-nesting-depth": null,
"selector-max-compound-selectors": 6,
"selector-max-id": 1,
"selector-no-qualifying-type": null,

View file

@ -448,5 +448,12 @@
"tutorial_tip.got_it": "Got it",
"tutorial_tip.ok": "Next",
"tutorial_tip.out": "Opt out of these tips.",
"tutorial_tip.seen": "Seen this before?"
"tutorial_tip.seen": "Seen this before?",
"statusProperty.configDialog.title": "Edit Statuses",
"statusProperty.configDialog.todo.emptyText": "Drag statuses here to consider tasks with these statuses “Not Started”",
"statusProperty.configDialog.inProgress.emptyText": "Drag statuses here to consider tasks with these statuses “In Progress”",
"statusProperty.configDialog.complete.emptyText": "Drag statuses here to consider tasks with these statuses ”Done”",
"statusProperty.configDialog.subTitle": "Categorise your status values to represent what each value represents",
"generic.cancel": "Cancel",
"generic.save": "Save"
}

View file

@ -3,7 +3,7 @@
exports[`components/cardDialog already following card 1`] = `
<div>
<div
class="Dialog dialog-back cardDialog size--medium"
class="ActionDialog Dialog dialog-back cardDialog size--medium"
>
<div
class="backdrop"
@ -12,7 +12,7 @@ exports[`components/cardDialog already following card 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -215,7 +215,7 @@ exports[`components/cardDialog already following card 1`] = `
exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] = `
<div>
<div
class="Dialog dialog-back cardDialog size--medium"
class="ActionDialog Dialog dialog-back cardDialog size--medium"
>
<div
class="backdrop"
@ -224,7 +224,7 @@ exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] =
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -461,7 +461,7 @@ exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] =
exports[`components/cardDialog return a cardDialog readonly 1`] = `
<div>
<div
class="Dialog dialog-back cardDialog size--medium"
class="ActionDialog Dialog dialog-back cardDialog size--medium"
>
<div
class="backdrop"
@ -470,7 +470,7 @@ exports[`components/cardDialog return a cardDialog readonly 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -592,7 +592,7 @@ exports[`components/cardDialog return a cardDialog readonly 1`] = `
exports[`components/cardDialog return cardDialog menu content 1`] = `
<div>
<div
class="Dialog dialog-back cardDialog size--medium"
class="ActionDialog Dialog dialog-back cardDialog size--medium"
>
<div
class="backdrop"
@ -601,7 +601,7 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -941,7 +941,7 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
exports[`components/cardDialog return cardDialog menu content and cancel delete confirmation do nothing 1`] = `
<div>
<div
class="Dialog dialog-back cardDialog size--medium"
class="ActionDialog Dialog dialog-back cardDialog size--medium"
>
<div
class="backdrop"
@ -950,7 +950,7 @@ exports[`components/cardDialog return cardDialog menu content and cancel delete
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -1153,7 +1153,7 @@ exports[`components/cardDialog return cardDialog menu content and cancel delete
exports[`components/cardDialog should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back cardDialog size--medium"
class="ActionDialog Dialog dialog-back cardDialog size--medium"
>
<div
class="backdrop"
@ -1162,7 +1162,7 @@ exports[`components/cardDialog should match snapshot 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -1365,7 +1365,7 @@ exports[`components/cardDialog should match snapshot 1`] = `
exports[`components/cardDialog should match snapshot without permissions 1`] = `
<div>
<div
class="Dialog dialog-back cardDialog size--medium"
class="ActionDialog Dialog dialog-back cardDialog size--medium"
>
<div
class="backdrop"
@ -1374,7 +1374,7 @@ exports[`components/cardDialog should match snapshot without permissions 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -3,7 +3,7 @@
exports[`/components/confirmAddUserForNotifications should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back confirmation-dialog-box size--small"
class="ActionDialog Dialog dialog-back confirmation-dialog-box size--small"
>
<div
class="backdrop"
@ -12,7 +12,7 @@ exports[`/components/confirmAddUserForNotifications should match snapshot 1`] =
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -3,7 +3,7 @@
exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back confirmation-dialog-box size--small"
class="ActionDialog Dialog dialog-back confirmation-dialog-box size--small"
>
<div
class="backdrop"
@ -12,7 +12,7 @@ exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -84,7 +84,7 @@ exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1
exports[`/components/confirmationDialogBox confirmDialog with Confirm Button Text should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back confirmation-dialog-box size--small"
class="ActionDialog Dialog dialog-back confirmation-dialog-box size--small"
>
<div
class="backdrop"
@ -93,7 +93,7 @@ exports[`/components/confirmationDialogBox confirmDialog with Confirm Button Tex
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -3,7 +3,7 @@
exports[`components/dialog should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back undefined size--medium"
class="ActionDialog Dialog dialog-back undefined size--medium"
>
<div
class="backdrop"
@ -12,7 +12,7 @@ exports[`components/dialog should match snapshot 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -50,7 +50,7 @@ exports[`components/dialog should match snapshot 1`] = `
exports[`components/dialog should return dialog and click on cancel button 1`] = `
<div>
<div
class="Dialog dialog-back undefined size--medium"
class="ActionDialog Dialog dialog-back undefined size--medium"
>
<div
class="backdrop"
@ -59,7 +59,7 @@ exports[`components/dialog should return dialog and click on cancel button 1`] =
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -0,0 +1,135 @@
@import '../../styles/z-index';
.Dialog {
&.dialog-back {
@include z-index(dialog-back);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
&.size--small {
.dialog {
max-width: 512px;
width: 100%;
height: max-content;
}
}
.dialog-title {
margin: 0;
font-weight: 600;
font-size: 22px;
line-height: 28px;
}
.dialog-subtitle {
font-size: 12px;
margin-top: 8px;
}
.backdrop {
@include z-index(dialog-backdrop);
position: fixed;
width: 100%;
height: 100%;
background-color: rgba(var(--center-channel-color-rgb), 0.5);
}
.wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.dialog {
position: relative;
display: flex;
flex-direction: column;
background-color: rgb(var(--center-channel-bg-rgb));
box-shadow: rgba(var(--center-channel-color-rgb), 0.1) 0 0 0 1px,
rgba(var(--center-channel-color-rgb), 0.1) 0 2px 4px;
border-radius: var(--modal-rad);
padding: 0;
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
overflow-y: auto;
&.footerHidden {
padding-bottom: 24px;
}
@media not screen and (max-width: 975px) {
margin: 72px auto;
max-width: 975px;
height: calc(100% - 144px);
.hideOnWidescreen {
/* Hide elements on larger screens */
display: none !important;
}
}
> * {
flex-shrink: 0;
}
> .banner {
background-color: rgba(230, 220, 192, 0.9);
text-align: center;
padding: 10px;
color: #222;
}
> .banner.error {
background-color: rgba(230, 192, 192, 0.9);
}
.IconButton {
color: rgba(var(--center-channel-color-rgb), 0.56);
&:hover {
color: rgba(var(--center-channel-color-rgb), 0.72);
background-color: rgba(var(--center-channel-color-rgb), 0.08);
}
&:active {
background-color: rgba(var(--button-bg-rgb), 0.08);
color: rgba(var(--button-bg-rgb), 1);
}
}
.toolbar {
display: flex;
flex-direction: row;
padding: 24px 0;
justify-content: space-between;
align-items: flex-start;
}
.toolbar--right {
display: flex;
gap: 8px;
align-items: center;
height: 28px;
margin-right: 16px;
}
}
.cardToolbar {
width: 100%;
margin: 0 16px;
}
.footer {
padding: 24px 0;
display: flex;
justify-content: flex-end;
gap: 8px;
}
}

View file

@ -0,0 +1,133 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useRef} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {useHotkeys} from 'react-hotkeys-hook'
import IconButton from '../../widgets/buttons/iconButton'
import CloseIcon from '../../widgets/icons/close'
import OptionsIcon from '../../widgets/icons/options'
import MenuWrapper from '../../widgets/menuWrapper'
import './actionDialog.scss'
import Button from '../../widgets/buttons/button'
type Props = {
children: React.ReactNode
size?: string
toolsMenu?: React.ReactNode // some dialogs may not require a toolmenu
toolbar?: React.ReactNode
hideCloseButton?: boolean
className?: string
title?: JSX.Element
subtitle?: JSX.Element
onClose: () => void
confirmButtonText?: string
onConfirm?: () => void
cancelButtonText?: string
onCancel?: () => void
hideFooter?: boolean
}
const ActionDialog = (props: Props) => {
const {toolsMenu, toolbar, title, subtitle, size} = props
const intl = useIntl()
const closeDialogText = intl.formatMessage({
id: 'Dialog.closeDialog',
defaultMessage: 'Close dialog',
})
useHotkeys('esc', () => props.onClose())
const isBackdropClickedRef = useRef(false)
return (
<div className={`ActionDialog Dialog dialog-back ${props.className} size--${size || 'medium'}`}>
<div className='backdrop'/>
<div
className='wrapper'
onClick={(e) => {
e.stopPropagation()
if (!isBackdropClickedRef.current) {
return
}
isBackdropClickedRef.current = false
props.onClose()
}}
onMouseDown={(e) => {
if (e.target === e.currentTarget) {
isBackdropClickedRef.current = true
}
}}
>
<div
role='dialog'
className={`dialog${props.hideFooter ? ' footerHidden' : ''}`}
>
<div className='toolbar'>
<div>
{<h1 className='dialog-title'>{title || ''}</h1>}
{subtitle && <h5 className='dialog-subtitle'>{subtitle}</h5>}
</div>
<div className='toolbar--right'>
{toolbar && <div className='d-flex'>{toolbar}</div>}
{toolsMenu && <MenuWrapper>
<IconButton
size='medium'
icon={<OptionsIcon/>}
/>
{toolsMenu}
</MenuWrapper>
}
{
!props.hideCloseButton &&
<IconButton
className='dialog__close'
onClick={props.onClose}
icon={<CloseIcon/>}
title={closeDialogText}
size='medium'
/>
}
</div>
</div>
{props.children}
{
!props.hideFooter &&
<div className='footer'>
<Button
onClick={props.onCancel}
emphasis='tertiary'
size='medium'
>
{
props.cancelButtonText ||
<FormattedMessage
id='generic.cancel'
defaultMessage='Cancel'
/>
}
</Button>
<Button
onClick={props.onConfirm}
emphasis='primary'
filled={true}
size='medium'
>
{
props.confirmButtonText ||
<FormattedMessage
id='generic.save'
defaultMessage='Save'
/>
}
</Button>
</div>
}
</div>
</div>
</div>
)
}
export default React.memo(ActionDialog)

View file

@ -143,7 +143,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelectorItem should trigg
</div>
<div>
<div
class="Dialog dialog-back DeleteBoardDialog size--medium"
class="ActionDialog Dialog dialog-back DeleteBoardDialog size--medium"
>
<div
class="backdrop"
@ -152,7 +152,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelectorItem should trigg
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -3,7 +3,7 @@
exports[`component/BoardSwitcherDialog base case 1`] = `
<div>
<div
class="Dialog dialog-back BoardSwitcherDialog size--medium"
class="ActionDialog Dialog dialog-back BoardSwitcherDialog size--medium"
>
<div
class="backdrop"
@ -12,7 +12,7 @@ exports[`component/BoardSwitcherDialog base case 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -3,7 +3,7 @@
exports[`components/createCategory/CreateCategory base case should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back CreateCategoryModal size--medium"
class="ActionDialog Dialog dialog-back CreateCategoryModal size--medium"
>
<div
class="backdrop"
@ -12,7 +12,7 @@ exports[`components/createCategory/CreateCategory base case should match snapsho
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -1,14 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useRef} from 'react'
import {useIntl} from 'react-intl'
import {useHotkeys} from 'react-hotkeys-hook'
import React from 'react'
import IconButton from '../widgets/buttons/iconButton'
import CloseIcon from '../widgets/icons/close'
import OptionsIcon from '../widgets/icons/options'
import MenuWrapper from '../widgets/menuWrapper'
import './dialog.scss'
import ActionDialog from './actionDialog/actionDialog'
type Props = {
children: React.ReactNode
@ -23,72 +18,12 @@ type Props = {
}
const Dialog = (props: Props) => {
const {toolsMenu, toolbar, title, subtitle, size} = props
const intl = useIntl()
const closeDialogText = intl.formatMessage({
id: 'Dialog.closeDialog',
defaultMessage: 'Close dialog',
})
useHotkeys('esc', () => props.onClose())
const isBackdropClickedRef = useRef(false)
// A dialog is just a ActionDialog without any footer action buttons
return (
<div className={`Dialog dialog-back ${props.className} size--${size || 'medium'}`}>
<div className='backdrop'/>
<div
className='wrapper'
onClick={(e) => {
e.stopPropagation()
if (!isBackdropClickedRef.current) {
return
}
isBackdropClickedRef.current = false
props.onClose()
}}
onMouseDown={(e) => {
if (e.target === e.currentTarget) {
isBackdropClickedRef.current = true
}
}}
>
<div
role='dialog'
className='dialog'
>
<div className='toolbar'>
<div>
{<h1 className='dialog-title'>{title || ''}</h1>}
{subtitle && <h5 className='dialog-subtitle'>{subtitle}</h5>}
</div>
<div className='toolbar--right'>
{toolbar && <div className='d-flex'>{toolbar}</div>}
{toolsMenu && <MenuWrapper>
<IconButton
size='medium'
icon={<OptionsIcon/>}
/>
{toolsMenu}
</MenuWrapper>
}
{
!props.hideCloseButton &&
<IconButton
className='dialog__close'
onClick={props.onClose}
icon={<CloseIcon/>}
title={closeDialogText}
size='medium'
/>
}
</div>
</div>
{props.children}
</div>
</div>
</div>
<ActionDialog
hideFooter={true}
{...props}
/>
)
}

View file

@ -905,7 +905,7 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
</div>
</div>
<div
class="Dialog dialog-back confirmation-dialog-box size--small"
class="ActionDialog Dialog dialog-back confirmation-dialog-box size--small"
>
<div
class="backdrop"
@ -914,7 +914,7 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -3,7 +3,7 @@
exports[`src/components/shareBoard/shareBoard confirm unlinking linked channel 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -12,7 +12,7 @@ exports[`src/components/shareBoard/shareBoard confirm unlinking linked channel 1
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -253,7 +253,7 @@ exports[`src/components/shareBoard/shareBoard confirm unlinking linked channel 1
exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -262,7 +262,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -462,7 +462,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 2`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -471,7 +471,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -671,7 +671,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
exports[`src/components/shareBoard/shareBoard return shareBoard and click Regenerate token 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -680,7 +680,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -903,7 +903,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene
exports[`src/components/shareBoard/shareBoard return shareBoard and click Select 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -912,7 +912,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -1139,7 +1139,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
exports[`src/components/shareBoard/shareBoard return shareBoard and click Select 2`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -1148,7 +1148,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -1607,7 +1607,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
exports[`src/components/shareBoard/shareBoard return shareBoard and click Select, non-plugin mode 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -1616,7 +1616,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -1843,7 +1843,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
exports[`src/components/shareBoard/shareBoard return shareBoard and click Select, non-plugin mode 2`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -1852,7 +1852,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -2329,7 +2329,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
exports[`src/components/shareBoard/shareBoard return shareBoard template and click Select 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -2338,7 +2338,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard template and cli
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -2514,7 +2514,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard template and cli
exports[`src/components/shareBoard/shareBoard return shareBoard template and click Select 2`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -2523,7 +2523,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard template and cli
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -2835,7 +2835,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard template and cli
exports[`src/components/shareBoard/shareBoard return shareBoard, and click switch 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -2844,7 +2844,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -3067,7 +3067,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc
exports[`src/components/shareBoard/shareBoard return shareBoardComponent and click Switch without sharing 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -3076,7 +3076,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -3299,7 +3299,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -3308,7 +3308,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -3508,7 +3508,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
exports[`src/components/shareBoard/shareBoard should match snapshot with sharing 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -3517,7 +3517,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -3717,7 +3717,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and subpath 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -3726,7 +3726,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -3926,7 +3926,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and without workspaceId and subpath 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -3935,7 +3935,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -4135,7 +4135,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
exports[`src/components/shareBoard/shareBoard should match snapshot, with template 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog size--medium"
class="ActionDialog Dialog dialog-back ShareBoardDialog size--medium"
>
<div
class="backdrop"
@ -4144,7 +4144,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot, with templa
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -0,0 +1,176 @@
.StatusPropertyConfigrationDialog {
.wrapper {
.dialog {
width: 832px;
height: 540px;
padding: 0 32px;
}
}
.text-75 {
color: rgba(var(--center-channel-color-rgb), 0.64);
}
.toolbar {
padding-bottom: 10px !important;
}
.categorySwimlanes {
flex: 1;
display: flex;
flex-direction: row;
margin-top: 36px;
gap: 10px;
overflow: hidden;
.categorySwimlane {
flex: 1 1 0;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
}
.categorySwimlane_Header {
display: flex;
flex-direction: row;
color: rgba(var(--center-channel-color-rgb), 0.64);
gap: 9px;
.text-heading1 {
text-transform: uppercase;
}
.addBtnWrapper {
margin-left: auto;
cursor: pointer;
border-radius: 4px;
width: 24px;
height: 24px;
text-align: center;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.1);
}
}
.octo-tooltip.tooltip-bottom:hover::after {
width: 180px;
}
}
.categorySwimlane_ValueArea {
flex: 1;
border: 2px dashed rgba(var(--center-channel-color-rgb), 0.25);
border-radius: 8px;
overflow: hidden;
padding: 14px 3px !important;
display: flex;
flex-direction: column;
gap: 8px;
.overflowWrapper {
overflow-y: auto;
min-height: 100%;
}
.Label {
width: max-content;
margin-top: 0;
max-width: 150px;
margin-right: auto;
span {
overflow: hidden;
text-overflow: ellipsis;
}
}
.categorySwimlane_Value {
display: flex;
cursor: pointer;
padding: 4px;
.colorEditor,
.contextMenu {
display: none;
cursor: pointer;
}
.Menu {
position: fixed;
}
.colorEditor {
margin-right: 4px;
margin-left: 4px;
&.editing {
display: block;
}
}
.contextMenu {
width: 24px;
height: 24px;
text-align: center;
border-radius: 4px;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.1);
}
}
&:hover,
&.active {
background: rgba(var(--center-channel-color-rgb), 0.1);
border-radius: 4px;
.colorEditor,
.contextMenu {
display: block;
}
}
.dragHandleWrapper {
width: 24px;
height: 24px;
text-align: center;
}
.colorEditor {
width: 24px;
min-width: 24px;
max-width: 24px;
height: 24px;
border: 1px solid #80808052;
border-radius: 4px;
}
}
.emptyColumnPlaceholder {
margin: 0 16px;
text-align: center;
display: flex;
flex-direction: column;
gap: 8px;
padding: 6px 0;
.icon-wrapper {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 0 auto;
padding-top: 7px;
.CompassIcon {
font-size: 18px;
opacity: 1;
display: block;
height: 18px;
}
}
}
}
}
}

View file

@ -0,0 +1,256 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {render} from '@testing-library/react'
import CheckIcon from '../../../widgets/icons/check'
import ClockOutline from '../../../widgets/icons/clockOutline'
import {wrapIntl, wrapRBDNDContext} from '../../../testUtils'
import BlackCheckboxOutline from '../../../widgets/icons/blackCheckboxOutline'
import EditStatusPropertyDialog, {StatusCategory} from './editStatusDialog'
describe('components/standardProperties/statusProperty/EditStatusPropertyDialog', () => {
test('should match snapshot', () => {
const initialValueCategoryValue: StatusCategory[] = [
{
id: 'category_id_1',
title: 'Not Started',
options: [
{id: 'status_id_1', value: 'Pending Design', color: 'propColorPurple'},
{id: 'status_id_2', value: 'TODO', color: 'propColorYellow'},
{id: 'status_id_3', value: 'Pending Specs', color: 'propColorGray'},
],
emptyState: {
icon: (<BlackCheckboxOutline/>),
color: '--sys-dnd-indicator-rgb',
text: {
id: 'statusProperty.configDialog.todo.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses “Not Started”',
},
},
},
{
id: 'category_id_2',
title: 'In progress',
options: [
{id: 'status_id_4', value: 'In Progress', color: 'propColorBrown'},
{id: 'status_id_5', value: 'In Review', color: 'propColorRed'},
{id: 'status_id_6', value: 'In QA', color: 'propColorPink'},
{id: 'status_id_7', value: 'Awaiting Cherrypick', color: 'propColorOrange'},
],
emptyState: {
icon: (<ClockOutline/>),
color: '--away-indicator-rgb',
text: {
id: 'statusProperty.configDialog.inProgress.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses “in progress”',
},
},
},
{
id: 'category_id_3',
title: 'Completed',
options: [
{id: 'status_id_20', value: 'Done', color: 'propColorPink'},
{id: 'status_id_21', value: 'Branch Cut', color: 'propColorGreen'},
{id: 'status_id_22', value: 'Released', color: 'propColorDefault'},
],
emptyState: {
icon: (<CheckIcon/>),
color: '--online-indicator-rgb',
text: {
id: 'statusProperty.configDialog.complete.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses ”Done”',
},
},
},
]
const component = wrapRBDNDContext(
wrapIntl(
<EditStatusPropertyDialog
valueCategories={initialValueCategoryValue}
onClose={() => {}}
onUpdate={() => {}}
/>,
))
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('no value in any column', () => {
const noValueConfig: StatusCategory[] = [
{
id: 'category_id_1',
title: 'Not Started',
options: [],
emptyState: {
icon: (<BlackCheckboxOutline/>),
color: '--sys-dnd-indicator-rgb',
text: {
id: 'statusProperty.configDialog.todo.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses “Not Started”',
},
},
},
{
id: 'category_id_2',
title: 'In progress',
options: [],
emptyState: {
icon: (<ClockOutline/>),
color: '--away-indicator-rgb',
text: {
id: 'statusProperty.configDialog.inProgress.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses “in progress”',
},
},
},
{
id: 'category_id_3',
title: 'Completed',
options: [],
emptyState: {
icon: (<CheckIcon/>),
color: '--online-indicator-rgb',
text: {
id: 'statusProperty.configDialog.complete.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses ”Done”',
},
},
},
]
const component = wrapRBDNDContext(
wrapIntl(
<EditStatusPropertyDialog
valueCategories={noValueConfig}
onClose={() => {}}
onUpdate={() => {}}
/>,
))
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('5 columns', () => {
const initialValueCategoryValue: StatusCategory[] = [
{
id: 'category_id_1',
title: 'Column 1',
options: [
{id: 'status_id_1', value: 'Pending Design', color: 'propColorPurple'},
{id: 'status_id_2', value: 'TODO', color: 'propColorYellow'},
{id: 'status_id_3', value: 'Pending Specs', color: 'propColorGray'},
],
emptyState: {
icon: (<BlackCheckboxOutline/>),
color: '--sys-dnd-indicator-rgb',
text: {
id: 'statusProperty.configDialog.todo.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses “Not Started”',
},
},
},
{
id: 'category_id_2',
title: 'Column 2',
options: [
{id: 'status_id_4', value: 'In Progress', color: 'propColorBrown'},
{id: 'status_id_5', value: 'In Review', color: 'propColorRed'},
{id: 'status_id_6', value: 'In QA', color: 'propColorPink'},
{id: 'status_id_7', value: 'Awaiting Cherrypick', color: 'propColorOrange'},
],
emptyState: {
icon: (<ClockOutline/>),
color: '--away-indicator-rgb',
text: {
id: 'statusProperty.configDialog.inProgress.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses “in progress”',
},
},
},
{
id: 'category_id_3',
title: 'Column 3',
options: [
{id: 'status_id_20', value: 'Done', color: 'propColorPink'},
{id: 'status_id_21', value: 'Branch Cut', color: 'propColorGreen'},
{id: 'status_id_22', value: 'Released', color: 'propColorDefault'},
],
emptyState: {
icon: (<CheckIcon/>),
color: '--online-indicator-rgb',
text: {
id: 'statusProperty.configDialog.complete.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses ”Done”',
},
},
},
{
id: 'category_id_2',
title: 'Column 4',
options: [
{id: 'status_id_54', value: 'Michael Scott', color: 'propColorOrange'},
],
emptyState: {
icon: (<ClockOutline/>),
color: '--away-indicator-rgb',
text: {
id: 'statusProperty.configDialog.inProgress.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses “in progress”',
},
},
},
{
id: 'category_id_3',
title: 'Column 5',
options: [
{id: 'status_id_22', value: 'Jim Halpert', color: 'propColorDefault'},
],
emptyState: {
icon: (<CheckIcon/>),
color: '--online-indicator-rgb',
text: {
id: 'statusProperty.configDialog.complete.emptyText',
defaultMessage: 'Drag statuses here to consider tasks with these statuses ”Done”',
},
},
},
]
const component = wrapRBDNDContext(
wrapIntl(
<EditStatusPropertyDialog
valueCategories={initialValueCategoryValue}
onClose={() => {}}
onUpdate={() => {}}
/>,
))
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('0 columns', () => {
const initialValueCategoryValue: StatusCategory[] = []
const component = wrapRBDNDContext(
wrapIntl(
<EditStatusPropertyDialog
valueCategories={initialValueCategoryValue}
onClose={() => {}}
onUpdate={() => {}}
/>,
))
const {container} = render(component)
expect(container).toMatchSnapshot()
})
})

View file

@ -0,0 +1,271 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {DragDropContext, Droppable, DropResult} from 'react-beautiful-dnd'
import {cloneDeep} from 'lodash'
import Tooltip from '../../../widgets/tooltip'
import PlusIcon from '../../../widgets/icons/plus'
import InfoIcon from '../../../widgets/icons/info'
import './editStatusDialog.scss'
import ActionDialog from '../../actionDialog/actionDialog'
import {IPropertyOption} from '../../../blocks/board'
import {IDType, Utils} from '../../../utils'
import ValueRow from './valueRow'
export type StatusCategoryEmptyState = {
icon: JSX.Element
color: string
text: {
id: string
defaultMessage: string
}
}
export type EditablePropertyOption = IPropertyOption & {
editing?: boolean
focused?: boolean
}
export type StatusCategory = {
id: string
title: string
options: EditablePropertyOption[]
emptyState: StatusCategoryEmptyState
}
type Props = {
valueCategories: StatusCategory[]
onClose: () => void
onUpdate: (updatedValueCategories: StatusCategory[]) => void
}
const EditStatusPropertyDialog = (props: Props): JSX.Element => {
const [valueCategories, setValueCategories] = useState<StatusCategory[]>([])
const [focusedValueID, setFocusedValueID] = useState<string>()
const intl = useIntl()
useEffect(() => {
// we save a deel copy of props as user actions like
// DND and adding, editing, changing coloror deleting the values
// affect this local copy first and only if the user clicks the
// "Save" button, are the changes propogated to the prop change callback function/
// Direcxtly modifying the props value can cause the value of variable
// from the parent component to be affected as well in some cases
setValueCategories(cloneDeep(props.valueCategories))
}, [props.valueCategories])
const title = (
<FormattedMessage
id='statusProperty.configDialog.title'
defaultMessage='Edit Statuses'
/>
)
const handleUpdateValue = (statusCategoryID: string, newOptionValue: IPropertyOption): void => {
newOptionValue.value = newOptionValue.value.trim()
const categoryIndex = valueCategories.findIndex((valueCategory) => valueCategory.id === statusCategoryID)
if (categoryIndex < 0) {
Utils.logError(`category with ID: ${statusCategoryID} not found`)
return
}
const valueIndex = valueCategories[categoryIndex].options.findIndex((option) => option.id === newOptionValue.id)
if (valueIndex < 0) {
Utils.logError(`Value with ID ${newOptionValue.id} not found`)
return
}
const updatedValueCategories = [...valueCategories]
const oldOptionvalue = updatedValueCategories[categoryIndex].options[valueIndex]
if (oldOptionvalue.value === newOptionValue.value && newOptionValue.value === '') {
// if used add a new value, but then leaves it empty by, for example,
// clicking outside, we delete that value as its the fasted way for the user
// to remove an accidently added value
updatedValueCategories[categoryIndex].options.splice(valueIndex, 1)
} else {
updatedValueCategories[categoryIndex].options[valueIndex] = newOptionValue
}
setFocusedValueID('')
setValueCategories(updatedValueCategories)
}
const handleDeleteValue = (statusCategoryID: string, optionID: string) => {
const categoryIndex = valueCategories.findIndex((valueCategory) => valueCategory.id === statusCategoryID)
if (categoryIndex < 0) {
Utils.logError(`category with ID: ${statusCategoryID} not found`)
return
}
const valueIndex = valueCategories[categoryIndex].options.findIndex((option) => option.id === optionID)
if (valueIndex < 0) {
Utils.logError(`Value with ID ${optionID} not found`)
return
}
const updatedValueCategories = Array.from(valueCategories)
updatedValueCategories[categoryIndex].options.splice(valueIndex, 1)
setValueCategories(updatedValueCategories)
}
const handleAddCategoryValue = (categoryID: string) => {
const categoryIndex = valueCategories.findIndex((valueCategory) => valueCategory.id === categoryID)
if (categoryIndex < 0) {
return
}
const newOption: EditablePropertyOption = {
id: Utils.createGuid(IDType.None),
value: '',
color: 'propColorOrange',
}
const updatedValueCategories = [...valueCategories]
updatedValueCategories[categoryIndex].options.unshift(newOption)
setFocusedValueID(newOption.id)
setValueCategories(updatedValueCategories)
}
const onDragEndHandler = (result: DropResult) => {
const {destination, source, type} = result
if (type !== 'statusCategory' || !destination) {
return
}
const updatedValues = Array.from(valueCategories)
const sourceCategoryIndex = updatedValues.findIndex((valueCategory) => valueCategory.id === source.droppableId)
const destinationCategoryIndex = updatedValues.findIndex((valueCategory) => valueCategory.id === destination.droppableId)
const draggedObject = valueCategories[sourceCategoryIndex].options[source.index]
updatedValues[sourceCategoryIndex].options.splice(source.index, 1)
updatedValues[destinationCategoryIndex].options.splice(destination.index, 0, draggedObject)
setValueCategories(updatedValues)
}
return (
<ActionDialog
onClose={props.onClose}
title={title}
className='StatusPropertyConfigrationDialog'
onConfirm={() => {
props.onUpdate(valueCategories)
props.onClose()
}}
onCancel={props.onClose}
>
<div className='text-heading5'/>
<div className='text-75'>
<FormattedMessage
id='statusProperty.configDialog.subTitle'
defaultMessage='Categorise your status values to represent what each value represents'
/>
</div>
<div className='categorySwimlanes'>
<DragDropContext onDragEnd={onDragEndHandler}>
{valueCategories.map((valueCategory) => {
return (
<div
key={valueCategory.id}
className='categorySwimlane'
>
<div className='categorySwimlane_Header'>
<div className='text-heading1'>
{valueCategory.title}
</div>
<Tooltip
title={intl.formatMessage({
id: valueCategory.emptyState.text.id,
defaultMessage: valueCategory.emptyState.text.defaultMessage,
})}
placement='bottom'
wrap={true}
>
<InfoIcon/>
</Tooltip>
<div
className='addBtnWrapper'
onClick={() => handleAddCategoryValue(valueCategory.id)}
>
<PlusIcon/>
</div>
</div>
<Droppable
type='statusCategory'
droppableId={valueCategory.id}
>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className='categorySwimlane_ValueArea'
>
<div className='overflowWrapper'>
{
valueCategory.options.length === 0 &&
<div className='emptyColumnPlaceholder'>
<div
className='icon-wrapper'
style={{
backgroundColor: `rgba(var(${valueCategory.emptyState.color}), 0.2)`,
color: `rgba(var(${valueCategory.emptyState.color}), 1)`,
}}
>
{valueCategory.emptyState.icon}
</div>
<div className='placeholderText text-75'>
{
<FormattedMessage
id={valueCategory.emptyState.text.id}
defaultMessage={valueCategory.emptyState.text.defaultMessage}
/>
}
</div>
</div>
}
{
valueCategory.options.length > 0 &&
valueCategory.options.map(
(option: EditablePropertyOption, index) =>
(
<ValueRow
key={option.id}
option={option}
index={index}
editing={option.id === focusedValueID}
onUpdate={handleUpdateValue}
onDelete={handleDeleteValue}
valueCategoryID={valueCategory.id}
/>
),
)
}
</div>
{provided.placeholder}
</div>
)}
</Droppable>
</div>
)
})}
</DragDropContext>
</div>
</ActionDialog>
)
}
export default EditStatusPropertyDialog

View file

@ -0,0 +1,10 @@
.EditableLabel {
input {
border: 0;
background: none;
border-bottom: 1px solid;
border-radius: 0;
max-width: 100%;
text-transform: uppercase;
}
}

View file

@ -0,0 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react'
import Label from '../../../../widgets/label'
import {IPropertyOption} from '../../../../blocks/board'
import './editableLabel.scss'
type Props = {
option: IPropertyOption
editing?: boolean
onBlur?: (newOptionValue: IPropertyOption) => void
}
const EditableLabel = (props: Props): JSX.Element => {
const [value, setValue] = useState<string>(props.option.value)
const handleOnBlur = () => {
const newOptionValue = {
...props.option,
value,
}
if (props.onBlur) {
props.onBlur(newOptionValue)
}
}
const displayValue = (<span>{props.option.value}</span>)
const editValue = (
<input
defaultValue={props.option.value}
autoFocus={true}
onChange={(e) => setValue(e.target.value)}
onBlur={handleOnBlur}
/>
)
return (
<Label
key={props.option.id}
className='EditableLabel'
color={props.option.color}
title={props.option.value}
>
{ props.editing ? editValue : displayValue }
</Label>
)
}
export default React.memo(EditableLabel)

View file

@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useRef, useState} from 'react'
import {Draggable} from 'react-beautiful-dnd'
import DeleteIcon from '../../../widgets/icons/delete'
import {Constants} from '../../../constants'
import DragHandle from '../../../widgets/icons/dragHandle'
import EditIcon from '../../../widgets/icons/edit'
import Menu from '../../../widgets/menu'
import MenuWrapper from '../../../widgets/menuWrapper'
import {IPropertyOption} from '../../../blocks/board'
import OptionsIcon from '../../../widgets/icons/options'
import EditableLabel from './editableLabel/editableLabel'
type Props = {
option: IPropertyOption
index: number
editing: boolean
onUpdate: (statusCategoryID: string, newOptionValue: IPropertyOption) => void
onDelete: (statusCategoryID: string, optionID: string) => void
valueCategoryID: string
}
const ValueRow = (props: Props) => {
const colorEditorRef = useRef(null)
const menuRef = useRef(null)
const [isActive, setIsActive] = useState<boolean>(false)
const [isEditing, setIsEditing] = useState<boolean>(false)
const [menuOpen, setMenuOpen] = useState<boolean>(false)
const handleOnColorChange = useCallback((newColor: string) => {
const newValue: IPropertyOption = {
...props.option,
color: newColor,
}
props.onUpdate(props.valueCategoryID, newValue)
}, [props.valueCategoryID, props.option.value, props.option.color])
const handleEditButtonClick = useCallback(() => {
setIsEditing((value) => !value)
}, [])
const handleDeleteButtonClick = useCallback(() => {
props.onDelete(props.valueCategoryID, props.option.id)
}, [props.valueCategoryID, props.option.id])
return (
<Draggable
draggableId={props.option.id}
index={props.index}
key={props.option.id}
>
{(draggableProvided) => (
<div
{...draggableProvided.draggableProps}
ref={draggableProvided.innerRef}
key={props.option.id}
className={`categorySwimlane_Value ${isActive || props.editing || isEditing || menuOpen ? 'active' : ''}`}
onBlur={() => {
setIsActive(false)
setIsEditing(false)
}}
>
<div
{...draggableProvided.dragHandleProps}
className='dragHandleWrapper'
>
<DragHandle/>
</div>
<EditableLabel
option={props.option}
editing={props.editing || isEditing}
onBlur={(newOptionValue: IPropertyOption) => props.onUpdate(props.valueCategoryID, newOptionValue)}
/>
<MenuWrapper
stopPropagationOnToggle={true}
onToggle={(isOpen: boolean) => setIsActive(isOpen)}
>
<div
ref={colorEditorRef}
className={`colorEditor ${props.option.color} ${props.editing ? 'editing' : ''}`}
/>
<Menu
position='auto'
parentRef={colorEditorRef}
menuMargin={30}
fixed={true}
>
{
Object.entries(Constants.menuColors).map(
([key, color]: [string, string]) => (
<Menu.Color
key={key}
id={key}
name={color}
onClick={() => handleOnColorChange(key)}
/>
),
)
}
</Menu>
</MenuWrapper>
<MenuWrapper
stopPropagationOnToggle={true}
onToggle={(open) => setMenuOpen(open)}
>
<div
ref={menuRef}
className='contextMenu'
>
<OptionsIcon/>
</div>
<Menu
position='auto'
parentRef={menuRef}
menuMargin={30}
fixed={true}
>
<Menu.Text
id='editText'
name={'Edit'}
icon={<EditIcon/>}
onClick={handleEditButtonClick}
/>
<Menu.Text
id='deleteOption'
name={'Delete'}
icon={<DeleteIcon/>}
onClick={handleDeleteButtonClick}
className='text-danger'
/>
</Menu>
</MenuWrapper>
</div>
)}
</Draggable>
)
}
export default React.memo(ValueRow)

View file

@ -3,7 +3,7 @@
exports[`components/viewLimitDialog/ViewLiimitDialog show notify upgrade button for non sys admin user 1`] = `
<div>
<div
class="Dialog dialog-back ViewLimitDialog size--medium"
class="ActionDialog Dialog dialog-back ViewLimitDialog size--medium"
>
<div
class="backdrop"
@ -12,7 +12,7 @@ exports[`components/viewLimitDialog/ViewLiimitDialog show notify upgrade button
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -85,7 +85,7 @@ exports[`components/viewLimitDialog/ViewLiimitDialog show notify upgrade button
exports[`components/viewLimitDialog/ViewLiimitDialog show upgrade button for sys admin user 1`] = `
<div>
<div
class="Dialog dialog-back ViewLimitDialog size--medium"
class="ActionDialog Dialog dialog-back ViewLimitDialog size--medium"
>
<div
class="backdrop"
@ -94,7 +94,7 @@ exports[`components/viewLimitDialog/ViewLiimitDialog show upgrade button for sys
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -3,7 +3,7 @@
exports[`components/viewLimitDialog/ViewL]imitDialog show notify admin confirmation msg 1`] = `
<div>
<div
class="Dialog dialog-back ViewLimitDialog size--medium"
class="ActionDialog Dialog dialog-back ViewLimitDialog size--medium"
>
<div
class="backdrop"
@ -12,7 +12,7 @@ exports[`components/viewLimitDialog/ViewL]imitDialog show notify admin confirmat
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div
@ -119,7 +119,7 @@ exports[`components/viewLimitDialog/ViewL]imitDialog show notify admin confirmat
exports[`components/viewLimitDialog/ViewL]imitDialog show view limit dialog 1`] = `
<div>
<div
class="Dialog dialog-back ViewLimitDialog size--medium"
class="ActionDialog Dialog dialog-back ViewLimitDialog size--medium"
>
<div
class="backdrop"
@ -128,7 +128,7 @@ exports[`components/viewLimitDialog/ViewL]imitDialog show view limit dialog 1`]
class="wrapper"
>
<div
class="dialog"
class="dialog footerHidden"
role="dialog"
>
<div

View file

@ -5,6 +5,9 @@ import React from 'react'
import './button.scss'
import {Utils} from '../../utils'
export type ButtonSize = 'xsmall' | 'small' | 'medium' | 'large'
export type Emphasis = 'primary' | 'secondary' | 'tertiary' | 'quaternary' | 'gray' | 'danger' | 'default' | 'link'
type Props = {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
onMouseOver?: (e: React.MouseEvent<HTMLButtonElement>) => void
@ -16,8 +19,8 @@ type Props = {
filled?: boolean
active?: boolean
submit?: boolean
emphasis?: string
size?: string
emphasis?: Emphasis
size?: ButtonSize
danger?: boolean
className?: string
rightIcon?: boolean

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import CompassIcon from './compassIcon'
export default function BlackCheckboxOutline(): JSX.Element {
return (
<CompassIcon
icon='checkbox-blank-outline'
className='BlackCheckboxOutline'
/>
)
}

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import CompassIcon from './compassIcon'
export default function ClockOutline(): JSX.Element {
return (
<CompassIcon
icon='clock-outline'
className='ClockOutline'
/>
)
}

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import CompassIcon from './compassIcon'
export default function DragHandle(): JSX.Element {
return (
<CompassIcon
icon='drag-vertical'
className='DragHandle'
/>
)
}

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import CompassIcon from './compassIcon'
export default function InfoIcon(): JSX.Element {
return (
<CompassIcon
icon='information-outline'
className='InfoIcon'
/>
)
}

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import CompassIcon from './compassIcon'
export default function PlusIcon(): JSX.Element {
return (
<CompassIcon
icon='plus'
className='PlusIcon'
/>
)
}

View file

@ -11,14 +11,15 @@ type Props = {
title?: string
children: React.ReactNode
className?: string
editing?: boolean
}
// Switch is an on-off style switch / checkbox
function Label(props: Props): JSX.Element {
let color = 'empty'
if (props.color && props.color in Constants.menuColors) {
color = props.color
}
return (
<span
className={`Label ${color} ${props.className ? props.className : ''}`}

View file

@ -18,6 +18,7 @@ type Props = {
position?: 'top' | 'bottom' | 'left' | 'right' | 'auto'
fixed?: boolean
parentRef?: React.RefObject<any>
menuMargin?: number
}
export default class Menu extends React.PureComponent<Props> {
@ -48,7 +49,7 @@ export default class Menu extends React.PureComponent<Props> {
let style: CSSProperties = {}
if (this.props.parentRef) {
const forceBottom = position ? ['bottom', 'left', 'right'].includes(position) : false
style = MenuUtil.openUp(this.props.parentRef, forceBottom).style
style = MenuUtil.openUp(this.props.parentRef, forceBottom, this.props.menuMargin).style
}
return (

View file

@ -47,10 +47,19 @@ $tooltop-vertical-offset: 2px;
position: relative;
// Arrow gets added before content
&:hover::before {
@extend %hover-tooltip-arrow; }
@extend %hover-tooltip-arrow;
}
// Tooltip message gets added after content
&:hover::after {
@extend %hover-tooltip-body; }
@extend %hover-tooltip-body;
}
&.wrap {
&:hover::after {
word-break: break-word;
white-space: unset;
}
}
// Top tooltip arrow style
&.tooltip-top:hover::before {

View file

@ -1,5 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import './tooltip.scss'
@ -8,6 +9,7 @@ type Props = {
title: string
children: React.ReactNode
placement?: 'top'|'left'|'right'|'bottom'
wrap?: boolean
}
// Adds tooltip div over children elements, the popup will
@ -15,7 +17,7 @@ type Props = {
// Default position is 'top'
function Tooltip(props: Props): JSX.Element {
const placement = props.placement || 'top'
const className = `octo-tooltip tooltip-${placement}`
const className = `octo-tooltip tooltip-${placement}${props.wrap ? ' wrap' : ''}`
return (
<div
className={className}