From 696cb19757f244798abcb8d51a2f0b8748ab93fe Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Sat, 23 Jul 2022 16:55:55 +0100 Subject: [PATCH 01/24] feat: add opengraph tags to website --- website/site/config.toml | 2 +- website/site/layouts/partials/head.html | 37 +++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/website/site/config.toml b/website/site/config.toml index c84b7aeafd3..59eba275247 100644 --- a/website/site/config.toml +++ b/website/site/config.toml @@ -15,7 +15,7 @@ pygmentsStyle = "manni" [params] # Meta author = "" - description = "" + description = "Focalboard is an open source alternative to tools like Asana, Trello, and Notion. Available as a stand-alone application or integrated into the Mattermost platform, Focalboard helps developers stay aligned to complete tasks, reach milestones, and achieve their goals." email = "" ghrepo = "https://github.com/mattermost/focalboard/" diff --git a/website/site/layouts/partials/head.html b/website/site/layouts/partials/head.html index 2dc47c87029..87e09c24d38 100755 --- a/website/site/layouts/partials/head.html +++ b/website/site/layouts/partials/head.html @@ -1,10 +1,37 @@ - {{ .Title }} - {{ with .Site.Params.author }} -{{ end }} {{ with .Site.Params.description }} -{{ end }} {{ with .Site.LanguageCode }} -{{ end }} + +{{ if in .Title "Focalboard" }} +{{ .Title }} +{{ else }} +{{ .Title }} | Focalboard +{{ end }} + + + +{{ with .Site.Params.author }} + +{{ end }} + +{{ with .Site.Params.description }} + + +{{ end }} + +{{ with .Site.LanguageCode }} + + +{{ end }} + + + + + + + + + + {{ if .Params.canonicalUrl }} From 69793bbb85c35a0d459fec1da77cc736ac8b2792 Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Sat, 23 Jul 2022 16:56:29 +0100 Subject: [PATCH 02/24] docs: noteable > notable in contribution guide --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8e3331f499..6a7bb051b49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thank you for your interest in contributing! Please read the [Focalboard Contrib When you submit a pull request, it goes through a code review process outlined [here](https://developers.mattermost.com/contribute/getting-started/code-review/). -After a noteable bug fix or improvement is merged, submit a pull request to the [CHANGELOG](CHANGELOG.md) under the next release section. +After a notable bug fix or improvement is merged, submit a pull request to the [CHANGELOG](CHANGELOG.md) under the next release section. ## Bug Reports From ffd8233420f0e3759991ce6626d4f1fdd51f7787 Mon Sep 17 00:00:00 2001 From: Jesus Murguia Date: Thu, 2 Mar 2023 11:45:05 -0700 Subject: [PATCH 03/24] display confirmation if there is any content --- webapp/src/components/cardDialog.tsx | 2 +- webapp/src/components/kanban/kanbanCard.tsx | 9 +++++++-- webapp/src/components/table/tableRow.tsx | 10 +++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/webapp/src/components/cardDialog.tsx b/webapp/src/components/cardDialog.tsx index da13e035db2..3d1c5668b22 100644 --- a/webapp/src/components/cardDialog.tsx +++ b/webapp/src/components/cardDialog.tsx @@ -109,7 +109,7 @@ const CardDialog = (props: Props): JSX.Element => { // use may be renaming a card title // and accidently delete the card // so adding des - if (card?.title === '' && card?.fields.contentOrder.length === 0) { + if (card?.title === '' && card?.fields.contentOrder.length === 0 && Object.keys(card?.fields?.properties).length === 0 && attachments?.length === 0 && comments?.length === 0) { handleDeleteCard() return } diff --git a/webapp/src/components/kanban/kanbanCard.tsx b/webapp/src/components/kanban/kanbanCard.tsx index a1fc303e700..3d03f408cd4 100644 --- a/webapp/src/components/kanban/kanbanCard.tsx +++ b/webapp/src/components/kanban/kanbanCard.tsx @@ -20,6 +20,9 @@ import OpenCardTourStep from '../onboardingTour/openCard/open_card' import CopyLinkTourStep from '../onboardingTour/copyLink/copy_link' import CardActionsMenu from '../cardActionsMenu/cardActionsMenu' import CardActionsMenuIcon from '../cardActionsMenu/cardActionsMenuIcon' +import {getCardAttachments} from '../../store/attachments' +import {getCardComments} from '../../store/comments' +import {useAppSelector} from '../../store/hooks' export const OnboardingCardClassName = 'onboardingCard' @@ -38,6 +41,8 @@ type Props = { const KanbanCard = (props: Props) => { const {card, board} = props + const comments = useAppSelector(getCardComments(card?.id)) + const attachments = useAppSelector(getCardAttachments(card?.id)) const intl = useIntl() const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly, props.onDrop) const visiblePropertyTemplates = props.visiblePropertyTemplates || [] @@ -72,12 +77,12 @@ const KanbanCard = (props: Props) => { // user trying to delete a card with blank name // but content present cannot be deleted without // confirmation dialog - if (card?.title === '' && card?.fields?.contentOrder?.length === 0) { + if (card?.title === '' && card?.fields?.contentOrder?.length === 0 && Object.keys(card?.fields?.properties).length === 0 && attachments?.length === 0 && comments?.length === 0) { handleDeleteCard() return } setShowConfirmationDialogBox(true) - }, [handleDeleteCard, card.title, card?.fields?.contentOrder?.length]) + }, [handleDeleteCard, card.title, card?.fields?.contentOrder?.length, card?.fields?.properties, attachments?.length, comments?.length]) const handleOnClick = useCallback((e: React.MouseEvent) => { if (props.onClick) { diff --git a/webapp/src/components/table/tableRow.tsx b/webapp/src/components/table/tableRow.tsx index eb570f41c4c..5f9d04da2f9 100644 --- a/webapp/src/components/table/tableRow.tsx +++ b/webapp/src/components/table/tableRow.tsx @@ -10,6 +10,9 @@ import mutator from '../../mutator' import Button from '../../widgets/buttons/button' import Editable from '../../widgets/editable' import {useSortable} from '../../hooks/sortable' +import {useAppSelector} from '../../store/hooks' +import {getCardAttachments} from '../../store/attachments' +import {getCardComments} from '../../store/comments' import {Utils} from '../../utils' @@ -48,7 +51,8 @@ type Props = { const TableRow = (props: Props) => { const intl = useIntl() const {board, card, isManualSort, groupById, visiblePropertyIds, collapsedOptionIds} = props - + const comments = useAppSelector(getCardComments(card?.id)) + const attachments = useAppSelector(getCardAttachments(card?.id)) const titleRef = useRef<{ focus(selectAll?: boolean): void }>(null) const [title, setTitle] = useState(props.card.title || '') const isGrouped = Boolean(groupById) @@ -138,12 +142,12 @@ const TableRow = (props: Props) => { // user trying to delete a card with blank name // but content present cannot be deleted without // confirmation dialog - if (card?.title === '' && card?.fields.contentOrder.length === 0) { + if (card?.title === '' && card?.fields?.contentOrder?.length === 0 && Object.keys(card?.fields?.properties).length === 0 && attachments?.length === 0 && comments?.length === 0) { handleDeleteCard() return } setShowConfirmationDialogBox(true) - }, [card.title, card.fields.contentOrder, handleDeleteCard]) + }, [card.title, card.fields.contentOrder, handleDeleteCard, card?.fields?.properties, attachments?.length, comments?.length]) return (
Date: Thu, 2 Mar 2023 19:46:14 -0700 Subject: [PATCH 04/24] moved isCardEmpty into utils --- webapp/src/components/cardDialog.tsx | 2 +- webapp/src/components/gallery/galleryCard.tsx | 21 ++++++++++++++++++- webapp/src/components/kanban/kanbanCard.tsx | 4 ++-- webapp/src/components/table/tableRow.tsx | 4 ++-- webapp/src/utils.ts | 13 ++++++++++-- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/webapp/src/components/cardDialog.tsx b/webapp/src/components/cardDialog.tsx index 3d1c5668b22..64c1e4eb3bf 100644 --- a/webapp/src/components/cardDialog.tsx +++ b/webapp/src/components/cardDialog.tsx @@ -109,7 +109,7 @@ const CardDialog = (props: Props): JSX.Element => { // use may be renaming a card title // and accidently delete the card // so adding des - if (card?.title === '' && card?.fields.contentOrder.length === 0 && Object.keys(card?.fields?.properties).length === 0 && attachments?.length === 0 && comments?.length === 0) { + if (Utils.isCardEmpty(card, comments, attachments)) { handleDeleteCard() return } diff --git a/webapp/src/components/gallery/galleryCard.tsx b/webapp/src/components/gallery/galleryCard.tsx index 76e295f3824..eb995a057c6 100644 --- a/webapp/src/components/gallery/galleryCard.tsx +++ b/webapp/src/components/gallery/galleryCard.tsx @@ -22,6 +22,9 @@ import CardBadges from '../cardBadges' import CardActionsMenu from '../cardActionsMenu/cardActionsMenu' import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox' import CardActionsMenuIcon from '../cardActionsMenu/cardActionsMenuIcon' +import {Utils} from '../../utils' +import {getCardComments} from '../../store/comments' +import {getCardAttachments} from '../../store/attachments' type Props = { board: Board @@ -39,6 +42,8 @@ type Props = { const GalleryCard = (props: Props) => { const intl = useIntl() const {card, board} = props + const comments = useAppSelector(getCardComments(card?.id)) + const attachments = useAppSelector(getCardAttachments(card?.id)) const [isDragging, isOver, cardRef] = useSortable('card', card, props.isManualSort && !props.readonly, props.onDrop) const contents = useAppSelector(getCardContents(card.id)) const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(false) @@ -46,6 +51,9 @@ const GalleryCard = (props: Props) => { const visiblePropertyTemplates = props.visiblePropertyTemplates || [] const handleDeleteCard = useCallback(() => { + if (!card) { + return + } mutator.deleteBlock(card, 'delete card') }, [card, board.id]) @@ -60,6 +68,17 @@ const GalleryCard = (props: Props) => { } }, [handleDeleteCard]) + const handleDeleteButtonOnClick = useCallback(() => { + // user trying to delete a card with blank name + // but content present cannot be deleted without + // confirmation dialog + if (Utils.isCardEmpty(card, comments, attachments)) { + handleDeleteCard() + return + } + setShowConfirmationDialogBox(true) + }, [card, comments, attachments]) + const image: ContentBlock|undefined = useMemo(() => { for (let i = 0; i < contents.length; ++i) { if (Array.isArray(contents[i])) { @@ -93,7 +112,7 @@ const GalleryCard = (props: Props) => { setShowConfirmationDialogBox(true)} + onClickDelete={handleDeleteButtonOnClick} onClickDuplicate={() => { TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id}) mutator.duplicateCard(card.id, board.id) diff --git a/webapp/src/components/kanban/kanbanCard.tsx b/webapp/src/components/kanban/kanbanCard.tsx index 3d03f408cd4..f9b4fe8a482 100644 --- a/webapp/src/components/kanban/kanbanCard.tsx +++ b/webapp/src/components/kanban/kanbanCard.tsx @@ -77,12 +77,12 @@ const KanbanCard = (props: Props) => { // user trying to delete a card with blank name // but content present cannot be deleted without // confirmation dialog - if (card?.title === '' && card?.fields?.contentOrder?.length === 0 && Object.keys(card?.fields?.properties).length === 0 && attachments?.length === 0 && comments?.length === 0) { + if (Utils.isCardEmpty(card, comments, attachments)) { handleDeleteCard() return } setShowConfirmationDialogBox(true) - }, [handleDeleteCard, card.title, card?.fields?.contentOrder?.length, card?.fields?.properties, attachments?.length, comments?.length]) + }, [handleDeleteCard, card, comments, attachments]) const handleOnClick = useCallback((e: React.MouseEvent) => { if (props.onClick) { diff --git a/webapp/src/components/table/tableRow.tsx b/webapp/src/components/table/tableRow.tsx index 5f9d04da2f9..fb3e1acef1f 100644 --- a/webapp/src/components/table/tableRow.tsx +++ b/webapp/src/components/table/tableRow.tsx @@ -142,12 +142,12 @@ const TableRow = (props: Props) => { // user trying to delete a card with blank name // but content present cannot be deleted without // confirmation dialog - if (card?.title === '' && card?.fields?.contentOrder?.length === 0 && Object.keys(card?.fields?.properties).length === 0 && attachments?.length === 0 && comments?.length === 0) { + if (Utils.isCardEmpty(card, comments, attachments)) { handleDeleteCard() return } setShowConfirmationDialogBox(true) - }, [card.title, card.fields.contentOrder, handleDeleteCard, card?.fields?.properties, attachments?.length, comments?.length]) + }, [handleDeleteCard, card, comments, attachments]) return (
Date: Thu, 2 Mar 2023 21:26:39 -0700 Subject: [PATCH 05/24] changed the function into a redux selector --- webapp/src/components/cardDialog.tsx | 5 +++-- webapp/src/components/gallery/galleryCard.tsx | 11 ++++------- webapp/src/components/kanban/kanbanCard.tsx | 10 ++++------ webapp/src/components/table/tableRow.tsx | 10 ++++------ webapp/src/store/cards.ts | 10 ++++++++++ webapp/src/utils.ts | 13 ++----------- 6 files changed, 27 insertions(+), 32 deletions(-) diff --git a/webapp/src/components/cardDialog.tsx b/webapp/src/components/cardDialog.tsx index 64c1e4eb3bf..249c47632f0 100644 --- a/webapp/src/components/cardDialog.tsx +++ b/webapp/src/components/cardDialog.tsx @@ -8,7 +8,7 @@ import {BoardView} from '../blocks/boardView' import {Card} from '../blocks/card' import octoClient from '../octoClient' import mutator from '../mutator' -import {getCard} from '../store/cards' +import {getCard, isCardEmpty as isCardEmptySelector} from '../store/cards' import {getCardComments} from '../store/comments' import {getCardContents} from '../store/contents' import {useAppDispatch, useAppSelector} from '../store/hooks' @@ -62,6 +62,7 @@ const CardDialog = (props: Props): JSX.Element => { const dispatch = useAppDispatch() const me = useAppSelector(getMe) const isTemplate = card && card.fields.isTemplate + const isCardEmpty = useAppSelector(isCardEmptySelector(props.cardId)) const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(false) const makeTemplateClicked = async () => { @@ -109,7 +110,7 @@ const CardDialog = (props: Props): JSX.Element => { // use may be renaming a card title // and accidently delete the card // so adding des - if (Utils.isCardEmpty(card, comments, attachments)) { + if (isCardEmpty) { handleDeleteCard() return } diff --git a/webapp/src/components/gallery/galleryCard.tsx b/webapp/src/components/gallery/galleryCard.tsx index eb995a057c6..561e7bf2cb4 100644 --- a/webapp/src/components/gallery/galleryCard.tsx +++ b/webapp/src/components/gallery/galleryCard.tsx @@ -22,9 +22,7 @@ import CardBadges from '../cardBadges' import CardActionsMenu from '../cardActionsMenu/cardActionsMenu' import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox' import CardActionsMenuIcon from '../cardActionsMenu/cardActionsMenuIcon' -import {Utils} from '../../utils' -import {getCardComments} from '../../store/comments' -import {getCardAttachments} from '../../store/attachments' +import {isCardEmpty as isCardEmptySelector} from '../../store/cards' type Props = { board: Board @@ -42,8 +40,7 @@ type Props = { const GalleryCard = (props: Props) => { const intl = useIntl() const {card, board} = props - const comments = useAppSelector(getCardComments(card?.id)) - const attachments = useAppSelector(getCardAttachments(card?.id)) + const isCardEmpty = useAppSelector(isCardEmptySelector(card?.id)) const [isDragging, isOver, cardRef] = useSortable('card', card, props.isManualSort && !props.readonly, props.onDrop) const contents = useAppSelector(getCardContents(card.id)) const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(false) @@ -72,12 +69,12 @@ const GalleryCard = (props: Props) => { // user trying to delete a card with blank name // but content present cannot be deleted without // confirmation dialog - if (Utils.isCardEmpty(card, comments, attachments)) { + if (isCardEmpty) { handleDeleteCard() return } setShowConfirmationDialogBox(true) - }, [card, comments, attachments]) + }, [isCardEmpty]) const image: ContentBlock|undefined = useMemo(() => { for (let i = 0; i < contents.length; ++i) { diff --git a/webapp/src/components/kanban/kanbanCard.tsx b/webapp/src/components/kanban/kanbanCard.tsx index f9b4fe8a482..533e64b632d 100644 --- a/webapp/src/components/kanban/kanbanCard.tsx +++ b/webapp/src/components/kanban/kanbanCard.tsx @@ -20,9 +20,8 @@ import OpenCardTourStep from '../onboardingTour/openCard/open_card' import CopyLinkTourStep from '../onboardingTour/copyLink/copy_link' import CardActionsMenu from '../cardActionsMenu/cardActionsMenu' import CardActionsMenuIcon from '../cardActionsMenu/cardActionsMenuIcon' -import {getCardAttachments} from '../../store/attachments' -import {getCardComments} from '../../store/comments' import {useAppSelector} from '../../store/hooks' +import {isCardEmpty as isCardEmptySelector} from '../../store/cards' export const OnboardingCardClassName = 'onboardingCard' @@ -41,8 +40,7 @@ type Props = { const KanbanCard = (props: Props) => { const {card, board} = props - const comments = useAppSelector(getCardComments(card?.id)) - const attachments = useAppSelector(getCardAttachments(card?.id)) + const isCardEmpty = useAppSelector(isCardEmptySelector(card?.id)) const intl = useIntl() const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly, props.onDrop) const visiblePropertyTemplates = props.visiblePropertyTemplates || [] @@ -77,12 +75,12 @@ const KanbanCard = (props: Props) => { // user trying to delete a card with blank name // but content present cannot be deleted without // confirmation dialog - if (Utils.isCardEmpty(card, comments, attachments)) { + if (isCardEmpty) { handleDeleteCard() return } setShowConfirmationDialogBox(true) - }, [handleDeleteCard, card, comments, attachments]) + }, [handleDeleteCard, isCardEmpty]) const handleOnClick = useCallback((e: React.MouseEvent) => { if (props.onClick) { diff --git a/webapp/src/components/table/tableRow.tsx b/webapp/src/components/table/tableRow.tsx index fb3e1acef1f..a2c47a4d05d 100644 --- a/webapp/src/components/table/tableRow.tsx +++ b/webapp/src/components/table/tableRow.tsx @@ -11,8 +11,7 @@ import Button from '../../widgets/buttons/button' import Editable from '../../widgets/editable' import {useSortable} from '../../hooks/sortable' import {useAppSelector} from '../../store/hooks' -import {getCardAttachments} from '../../store/attachments' -import {getCardComments} from '../../store/comments' +import {isCardEmpty as isCardEmptySelector} from '../../store/cards' import {Utils} from '../../utils' @@ -51,8 +50,7 @@ type Props = { const TableRow = (props: Props) => { const intl = useIntl() const {board, card, isManualSort, groupById, visiblePropertyIds, collapsedOptionIds} = props - const comments = useAppSelector(getCardComments(card?.id)) - const attachments = useAppSelector(getCardAttachments(card?.id)) + const isCardEmpty = useAppSelector(isCardEmptySelector(card?.id)) const titleRef = useRef<{ focus(selectAll?: boolean): void }>(null) const [title, setTitle] = useState(props.card.title || '') const isGrouped = Boolean(groupById) @@ -142,12 +140,12 @@ const TableRow = (props: Props) => { // user trying to delete a card with blank name // but content present cannot be deleted without // confirmation dialog - if (Utils.isCardEmpty(card, comments, attachments)) { + if (isCardEmpty) { handleDeleteCard() return } setShowConfirmationDialogBox(true) - }, [handleDeleteCard, card, comments, attachments]) + }, [handleDeleteCard, isCardEmpty]) return (
Card|undefined { } } +export function isCardEmpty(cardId: string): (state: RootState) => boolean { + return (state: RootState): boolean => { + const card = getCards(state)[cardId] + const comments = state.comments.commentsByCard[cardId] + const attachments = state.attachments.attachmentsByCard[cardId] + + return card?.title === '' && card?.fields.contentOrder.length === 0 && Object.keys(card?.fields?.properties).length === 0 && !comments && !attachments + } +} + export const getCurrentBoardCards = createSelector( (state: RootState) => state.boards.current, getCards, diff --git a/webapp/src/utils.ts b/webapp/src/utils.ts index d5f7e67f54d..3a2954d180f 100644 --- a/webapp/src/utils.ts +++ b/webapp/src/utils.ts @@ -13,13 +13,12 @@ import {IUser} from './user' import {Block} from './blocks/block' import {Board as BoardType, BoardMember, createBoard} from './blocks/board' import {createBoardView} from './blocks/boardView' -import {Card as CardBlock, createCard} from './blocks/card' -import {CommentBlock, createCommentBlock} from './blocks/commentBlock' +import {createCard} from './blocks/card' +import {createCommentBlock} from './blocks/commentBlock' import {IAppWindow} from './types' import {ChangeHandlerType, WSMessage} from './wsclient' import {BoardCategoryWebsocketData, Category} from './store/sidebar' import {UserSettings} from './userSettings' -import {AttachmentBlock} from './blocks/attachmentBlock' declare let window: IAppWindow @@ -832,14 +831,6 @@ class Utils { static isAdmin(roles: string): boolean { return Utils.isSystemAdmin(roles) || Utils.isTeamAdmin(roles) } - - static isCardEmpty(card: CardBlock | undefined, comments: CommentBlock[], attachments: AttachmentBlock[]): boolean { - if (!card) { - return true - } - - return card?.title === '' && card?.fields.contentOrder.length === 0 && Object.keys(card?.fields?.properties).length === 0 && attachments?.length === 0 && comments?.length === 0 - } } export {Utils, IDType} From 80d2bac4b112c625ee0e974b34738c95a4ac4e4c Mon Sep 17 00:00:00 2001 From: Jesus Murguia Date: Thu, 2 Mar 2023 22:52:24 -0700 Subject: [PATCH 06/24] fix tests --- webapp/src/store/cards.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/webapp/src/store/cards.ts b/webapp/src/store/cards.ts index d3fd1682bdf..331f02213b9 100644 --- a/webapp/src/store/cards.ts +++ b/webapp/src/store/cards.ts @@ -170,11 +170,7 @@ export function getCard(cardId: string): (state: RootState) => Card|undefined { export function isCardEmpty(cardId: string): (state: RootState) => boolean { return (state: RootState): boolean => { - const card = getCards(state)[cardId] - const comments = state.comments.commentsByCard[cardId] - const attachments = state.attachments.attachmentsByCard[cardId] - - return card?.title === '' && card?.fields.contentOrder.length === 0 && Object.keys(card?.fields?.properties).length === 0 && !comments && !attachments + return getCards(state)[cardId]?.title === '' && getCards(state)[cardId]?.fields.contentOrder.length === 0 && Object.keys(getCards(state)[cardId]?.fields?.properties).length === 0 && !state.comments.commentsByCard[cardId] && !state.attachments.attachmentsByCard[cardId] } } From 8799eb129aa6b925fe0de30c0d0dd2ff27df8d9b Mon Sep 17 00:00:00 2001 From: Jesus Murguia Date: Sat, 4 Mar 2023 17:17:17 -0700 Subject: [PATCH 07/24] changed selector into hook, to use existing selectors --- webapp/src/components/cardDialog.tsx | 5 +++-- webapp/src/components/gallery/galleryCard.tsx | 4 ++-- webapp/src/components/kanban/kanbanCard.tsx | 5 ++--- webapp/src/components/table/tableRow.tsx | 6 ++--- webapp/src/hooks/useIsCardEmpty.ts | 22 +++++++++++++++++++ webapp/src/store/cards.ts | 6 ----- 6 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 webapp/src/hooks/useIsCardEmpty.ts diff --git a/webapp/src/components/cardDialog.tsx b/webapp/src/components/cardDialog.tsx index 249c47632f0..f35c9e08505 100644 --- a/webapp/src/components/cardDialog.tsx +++ b/webapp/src/components/cardDialog.tsx @@ -8,7 +8,8 @@ import {BoardView} from '../blocks/boardView' import {Card} from '../blocks/card' import octoClient from '../octoClient' import mutator from '../mutator' -import {getCard, isCardEmpty as isCardEmptySelector} from '../store/cards' +import {getCard} from '../store/cards' +import {useIsCardEmpty} from '../hooks/useIsCardEmpty' import {getCardComments} from '../store/comments' import {getCardContents} from '../store/contents' import {useAppDispatch, useAppSelector} from '../store/hooks' @@ -62,7 +63,7 @@ const CardDialog = (props: Props): JSX.Element => { const dispatch = useAppDispatch() const me = useAppSelector(getMe) const isTemplate = card && card.fields.isTemplate - const isCardEmpty = useAppSelector(isCardEmptySelector(props.cardId)) + const isCardEmpty = useIsCardEmpty(card) const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(false) const makeTemplateClicked = async () => { diff --git a/webapp/src/components/gallery/galleryCard.tsx b/webapp/src/components/gallery/galleryCard.tsx index 561e7bf2cb4..69b5e22a038 100644 --- a/webapp/src/components/gallery/galleryCard.tsx +++ b/webapp/src/components/gallery/galleryCard.tsx @@ -22,7 +22,7 @@ import CardBadges from '../cardBadges' import CardActionsMenu from '../cardActionsMenu/cardActionsMenu' import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox' import CardActionsMenuIcon from '../cardActionsMenu/cardActionsMenuIcon' -import {isCardEmpty as isCardEmptySelector} from '../../store/cards' +import {useIsCardEmpty} from '../../hooks/useIsCardEmpty' type Props = { board: Board @@ -40,7 +40,7 @@ type Props = { const GalleryCard = (props: Props) => { const intl = useIntl() const {card, board} = props - const isCardEmpty = useAppSelector(isCardEmptySelector(card?.id)) + const isCardEmpty = useIsCardEmpty(card) const [isDragging, isOver, cardRef] = useSortable('card', card, props.isManualSort && !props.readonly, props.onDrop) const contents = useAppSelector(getCardContents(card.id)) const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(false) diff --git a/webapp/src/components/kanban/kanbanCard.tsx b/webapp/src/components/kanban/kanbanCard.tsx index 533e64b632d..4642a8c63c6 100644 --- a/webapp/src/components/kanban/kanbanCard.tsx +++ b/webapp/src/components/kanban/kanbanCard.tsx @@ -7,6 +7,7 @@ import {useIntl} from 'react-intl' import {Board, IPropertyTemplate} from '../../blocks/board' import {Card} from '../../blocks/card' import {useSortable} from '../../hooks/sortable' +import {useIsCardEmpty} from '../../hooks/useIsCardEmpty' import mutator from '../../mutator' import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient' import {Utils} from '../../utils' @@ -20,8 +21,6 @@ import OpenCardTourStep from '../onboardingTour/openCard/open_card' import CopyLinkTourStep from '../onboardingTour/copyLink/copy_link' import CardActionsMenu from '../cardActionsMenu/cardActionsMenu' import CardActionsMenuIcon from '../cardActionsMenu/cardActionsMenuIcon' -import {useAppSelector} from '../../store/hooks' -import {isCardEmpty as isCardEmptySelector} from '../../store/cards' export const OnboardingCardClassName = 'onboardingCard' @@ -40,7 +39,7 @@ type Props = { const KanbanCard = (props: Props) => { const {card, board} = props - const isCardEmpty = useAppSelector(isCardEmptySelector(card?.id)) + const isCardEmpty = useIsCardEmpty(card) const intl = useIntl() const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly, props.onDrop) const visiblePropertyTemplates = props.visiblePropertyTemplates || [] diff --git a/webapp/src/components/table/tableRow.tsx b/webapp/src/components/table/tableRow.tsx index a2c47a4d05d..23bc34c6f5b 100644 --- a/webapp/src/components/table/tableRow.tsx +++ b/webapp/src/components/table/tableRow.tsx @@ -10,9 +10,7 @@ import mutator from '../../mutator' import Button from '../../widgets/buttons/button' import Editable from '../../widgets/editable' import {useSortable} from '../../hooks/sortable' -import {useAppSelector} from '../../store/hooks' -import {isCardEmpty as isCardEmptySelector} from '../../store/cards' - +import {useIsCardEmpty} from '../../hooks/useIsCardEmpty' import {Utils} from '../../utils' import PropertyValueElement from '../propertyValueElement' @@ -50,7 +48,7 @@ type Props = { const TableRow = (props: Props) => { const intl = useIntl() const {board, card, isManualSort, groupById, visiblePropertyIds, collapsedOptionIds} = props - const isCardEmpty = useAppSelector(isCardEmptySelector(card?.id)) + const isCardEmpty = useIsCardEmpty(card) const titleRef = useRef<{ focus(selectAll?: boolean): void }>(null) const [title, setTitle] = useState(props.card.title || '') const isGrouped = Boolean(groupById) diff --git a/webapp/src/hooks/useIsCardEmpty.ts b/webapp/src/hooks/useIsCardEmpty.ts new file mode 100644 index 00000000000..106000cf611 --- /dev/null +++ b/webapp/src/hooks/useIsCardEmpty.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useMemo} from 'react' + +import {useAppSelector} from '../store/hooks' +import {Card} from '../blocks/card' +import {getCardComments} from '../store/comments' +import {getCardAttachments} from '../store/attachments' + +export const useIsCardEmpty = (card: Card | undefined) => { + if (!card) { + return true + } + + const comments = useAppSelector(getCardComments(card.id)) + const attachments = useAppSelector(getCardAttachments(card.id)) + + return useMemo(() => { + return card?.title === '' && card?.fields.contentOrder.length === 0 && Object.values(card?.fields?.properties).every((x) => !x.length) && comments.length === 0 && attachments.length === 0 + }, [card?.title, card?.fields.contentOrder, card?.fields?.properties, comments, attachments]) +} diff --git a/webapp/src/store/cards.ts b/webapp/src/store/cards.ts index 331f02213b9..5f30f6f60a6 100644 --- a/webapp/src/store/cards.ts +++ b/webapp/src/store/cards.ts @@ -168,12 +168,6 @@ export function getCard(cardId: string): (state: RootState) => Card|undefined { } } -export function isCardEmpty(cardId: string): (state: RootState) => boolean { - return (state: RootState): boolean => { - return getCards(state)[cardId]?.title === '' && getCards(state)[cardId]?.fields.contentOrder.length === 0 && Object.keys(getCards(state)[cardId]?.fields?.properties).length === 0 && !state.comments.commentsByCard[cardId] && !state.attachments.attachmentsByCard[cardId] - } -} - export const getCurrentBoardCards = createSelector( (state: RootState) => state.boards.current, getCards, From 293b62a971bc303c0137208b5fa46375324935da Mon Sep 17 00:00:00 2001 From: Jesus Murguia Date: Sat, 4 Mar 2023 17:21:54 -0700 Subject: [PATCH 08/24] created fullCalendarCard --- .../src/components/calendar/fullCalendar.tsx | 97 ++------------ .../components/calendar/fullCalendarCard.tsx | 120 ++++++++++++++++++ 2 files changed, 133 insertions(+), 84 deletions(-) create mode 100644 webapp/src/components/calendar/fullCalendarCard.tsx diff --git a/webapp/src/components/calendar/fullCalendar.tsx b/webapp/src/components/calendar/fullCalendar.tsx index 7c1fccf263d..0f5cfac11eb 100644 --- a/webapp/src/components/calendar/fullCalendar.tsx +++ b/webapp/src/components/calendar/fullCalendar.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useMemo, useState} from 'react' +import React, {useCallback, useMemo} from 'react' import {useIntl} from 'react-intl' import FullCalendar, {EventChangeArg, EventInput, EventContentArg, DayCellContentArg} from '@fullcalendar/react' @@ -18,18 +18,12 @@ import {BoardView} from '../../blocks/boardView' import {Card} from '../../blocks/card' import {DateProperty} from '../../properties/date/date' import propsRegistry from '../../properties' -import Tooltip from '../../widgets/tooltip' -import PropertyValueElement from '../propertyValueElement' import {Constants, Permission} from '../../constants' import {useHasCurrentBoardPermissions} from '../../hooks/permissions' -import CardBadges from '../cardBadges' -import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox' import './fullcalendar.scss' -import MenuWrapper from '../../widgets/menuWrapper' -import CardActionsMenu from '../cardActionsMenu/cardActionsMenu' -import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient' -import CardActionsMenuIcon from '../cardActionsMenu/cardActionsMenuIcon' + +import FullCalendarCard from './fullCalendarCard' const oneDay = 60 * 60 * 24 * 1000 @@ -76,8 +70,6 @@ const CalendarFullView = (props: Props): JSX.Element|null => { const {board, cards, activeView, dateDisplayProperty, readonly} = props const isSelectable = !readonly const canAddCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards]) - const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(false) - const [cardItem, setCardItem] = useState() const visiblePropertyTemplates = useMemo(() => ( board.cardProperties.filter((template: IPropertyTemplate) => activeView.fields.visiblePropertyIds.includes(template.id)) @@ -127,82 +119,20 @@ const CalendarFullView = (props: Props): JSX.Element|null => { const visibleBadges = activeView.fields.visiblePropertyIds.includes(Constants.badgesColumnId) - const openConfirmationDialogBox = (card: Card) => { - setShowConfirmationDialogBox(true) - setCardItem(card) - } - - const handleDeleteCard = useCallback(() => { - if (!cardItem) { - return - } - mutator.deleteBlock(cardItem, 'delete card') - setShowConfirmationDialogBox(false) - }, [cardItem, board.id]) - - const confirmDialogProps: ConfirmationDialogBoxProps = useMemo(() => { - return { - heading: intl.formatMessage({id: 'CardDialog.delete-confirmation-dialog-heading', defaultMessage: 'Confirm card delete!'}), - confirmButtonText: intl.formatMessage({id: 'CardDialog.delete-confirmation-dialog-button-text', defaultMessage: 'Delete'}), - onConfirm: handleDeleteCard, - onClose: () => { - setShowConfirmationDialogBox(false) - }, - } - }, [handleDeleteCard]) - const renderEventContent = (eventProps: EventContentArg): JSX.Element|null => { const {event} = eventProps const card = cards.find((o) => o.id === event.id) || cards[0] - return ( - <> -
props.showCard(event.id)} - > - {!props.readonly && - - - openConfirmationDialogBox(card)} - onClickDuplicate={() => { - TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id}) - mutator.duplicateCard(card.id, board.id) - }} - /> - } -
- { event.extendedProps.icon ?
{event.extendedProps.icon}
: undefined } -
{event.title || intl.formatMessage({id: 'CalendarCard.untitled', defaultMessage: 'Untitled'})}
-
- {visiblePropertyTemplates.map((template) => ( - - - - ))} - {visibleBadges && - } -
- - ) + ) } const eventChange = useCallback((eventProps: EventChangeArg) => { @@ -292,7 +222,6 @@ const CalendarFullView = (props: Props): JSX.Element|null => { selectMirror={true} select={onNewEvent} /> - {showConfirmationDialogBox && }
) } diff --git a/webapp/src/components/calendar/fullCalendarCard.tsx b/webapp/src/components/calendar/fullCalendarCard.tsx new file mode 100644 index 00000000000..cc4653b46aa --- /dev/null +++ b/webapp/src/components/calendar/fullCalendarCard.tsx @@ -0,0 +1,120 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useMemo, useState} from 'react' +import {useIntl} from 'react-intl' + +import {EventApi} from '@fullcalendar/react' + +import {useIsCardEmpty} from '../../hooks/useIsCardEmpty' +import mutator from '../../mutator' + +import {Board, IPropertyTemplate} from '../../blocks/board' +import {Card} from '../../blocks/card' +import Tooltip from '../../widgets/tooltip' +import PropertyValueElement from '../propertyValueElement' +import CardBadges from '../cardBadges' +import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox' + +import './fullcalendar.scss' +import MenuWrapper from '../../widgets/menuWrapper' +import CardActionsMenu from '../cardActionsMenu/cardActionsMenu' +import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient' +import CardActionsMenuIcon from '../cardActionsMenu/cardActionsMenuIcon' + +type Props = { + card: Card + board: Board + event: EventApi + visiblePropertyTemplates: IPropertyTemplate[] + visibleBadges: boolean + readonly: boolean + showCard: (cardId: string) => void +} + +const FullCalendarCard = (props: Props): JSX.Element|null => { + const {card, board, event, visiblePropertyTemplates, visibleBadges} = props + const isCardEmpty = useIsCardEmpty(card) + const intl = useIntl() + const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(false) + + const handleDeleteCard = useCallback(() => { + if (!card) { + return + } + mutator.deleteBlock(card, 'delete card') + setShowConfirmationDialogBox(false) + }, [card, board.id]) + + const handleDeleteButtonOnClick = useCallback(() => { + // user trying to delete a card with blank name + // but content present cannot be deleted without + // confirmation dialog + if (isCardEmpty) { + handleDeleteCard() + return + } + setShowConfirmationDialogBox(true) + }, [handleDeleteCard, isCardEmpty]) + + const confirmDialogProps: ConfirmationDialogBoxProps = useMemo(() => { + return { + heading: intl.formatMessage({id: 'CardDialog.delete-confirmation-dialog-heading', defaultMessage: 'Confirm card delete!'}), + confirmButtonText: intl.formatMessage({id: 'CardDialog.delete-confirmation-dialog-button-text', defaultMessage: 'Delete'}), + onConfirm: handleDeleteCard, + onClose: () => { + setShowConfirmationDialogBox(false) + }, + } + }, [handleDeleteCard]) + + return (<> +
props.showCard(event.id)} + > + {!props.readonly && + + + { + TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id}) + mutator.duplicateCard(card.id, board.id) + }} + /> + } +
+ { event.extendedProps.icon ?
{event.extendedProps.icon}
: undefined } +
{event.title || intl.formatMessage({id: 'CalendarCard.untitled', defaultMessage: 'Untitled'})}
+
+ {visiblePropertyTemplates.map((template) => ( + + + + ))} + {visibleBadges && + } +
+ {showConfirmationDialogBox && } + ) +} + +export default React.memo(FullCalendarCard) From 4d14413bdaa65105ef5fe77c2402090a6945dc59 Mon Sep 17 00:00:00 2001 From: Jesus Murguia Date: Mon, 6 Mar 2023 17:32:21 -0700 Subject: [PATCH 09/24] removed the event prop as it was unnecesary --- webapp/src/components/calendar/fullCalendar.tsx | 1 - webapp/src/components/calendar/fullCalendarCard.tsx | 11 ++++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/webapp/src/components/calendar/fullCalendar.tsx b/webapp/src/components/calendar/fullCalendar.tsx index 0f5cfac11eb..12e851c8598 100644 --- a/webapp/src/components/calendar/fullCalendar.tsx +++ b/webapp/src/components/calendar/fullCalendar.tsx @@ -126,7 +126,6 @@ const CalendarFullView = (props: Props): JSX.Element|null => { { - const {card, board, event, visiblePropertyTemplates, visibleBadges} = props + const {card, board, visiblePropertyTemplates, visibleBadges} = props const isCardEmpty = useIsCardEmpty(card) const intl = useIntl() const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(false) @@ -71,7 +68,7 @@ const FullCalendarCard = (props: Props): JSX.Element|null => { return (<>
props.showCard(event.id)} + onClick={() => props.showCard(card.id)} > {!props.readonly && { /> }
- { event.extendedProps.icon ?
{event.extendedProps.icon}
: undefined } + { card.fields.icon ?
{card.fields.icon}
: undefined }
{event.title || intl.formatMessage({id: 'CalendarCard.untitled', defaultMessage: 'Untitled'})}
+ >{card.title || intl.formatMessage({id: 'CalendarCard.untitled', defaultMessage: 'Untitled'})}
{visiblePropertyTemplates.map((template) => ( Date: Mon, 6 Mar 2023 17:36:33 -0700 Subject: [PATCH 10/24] added unit tests for FullCalendarCard component --- .../fullCalendarCard.test.tsx.snap | 616 ++++++++++++++++++ .../calendar/fullCalendarCard.test.tsx | 186 ++++++ 2 files changed, 802 insertions(+) create mode 100644 webapp/src/components/calendar/__snapshots__/fullCalendarCard.test.tsx.snap create mode 100644 webapp/src/components/calendar/fullCalendarCard.test.tsx diff --git a/webapp/src/components/calendar/__snapshots__/fullCalendarCard.test.tsx.snap b/webapp/src/components/calendar/__snapshots__/fullCalendarCard.test.tsx.snap new file mode 100644 index 00000000000..e461837d6f1 --- /dev/null +++ b/webapp/src/components/calendar/__snapshots__/fullCalendarCard.test.tsx.snap @@ -0,0 +1,616 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/components/calendar/fullCalendarCard return FullCalendarCard and click on copy link menu 1`] = ` +
+
+