mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-21 13:38:56 +02:00
Porting the card limits frontend to main branch (#3174)
* Porting the cards limits to main branch * Fixing Issue 3124 (#3162) * Fixing Issue 3124 * Update webapp/src/components/cardLimitNotification.tsx Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * Some polishing in the card limit notifications components (#3169) * Adding missing selector * Adding some small missing parts * Fixing some tests type checks adding the roles attribute to the users * Feature to show hidden card count (#3094) * Shows "(Deleted User)" instead of UUID when user not found (#2354) (#2465) * Shows "(Deleted User)" instead of long, unreadable UUID in case the user is not found In case a user is not found, at present unreadable and long UUIDs are shown which kill the look and feel of the application. This patch replaces the UUID with a more explanatory string. * Update server/services/store/mattermostauthlayer/mattermostauthlayer.go Co-authored-by: Doug Lauder <wiggin77@warpmail.net> (cherry picked from commit68819185a4
) Co-authored-by: Akshay Vasudeva Rao <51395864+akkivasu@users.noreply.github.com> * Update CHANGELOG.md for v0.15 Added one more merged PR to the list * GH-2212 - Update global link on board (#2492) (#2495) (cherry picked from commit49df41f9b2
) Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> * GH-2387 - Fixing link in comments (#2480) (#2498) (cherry picked from commit5e2cf0b386
) Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> * Addead feature to start product tour on using the welcome template (#2468) * Fixed a bug where images of the welcome board were not copied over. (#2453) * Fixed a buig where images of welcome board were not copied over * Lint fixes * Fixed test * Fixed test * GH-2496 - Updating board title truncation issue (#2497) (#2503) (cherry picked from commitf9cef8e4a0
) Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> * set min-height on empty date to allow click (#2466) (#2504) (cherry picked from commit20fe19a50d
) Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * Fix problem with viewId 0 in the URL (#2474) (#2510) (cherry picked from commit4cb3a0fae4
) Co-authored-by: Jesús Espino <jespinog@gmail.com> * don't display temlate page if readonly and access revoked (#2499) (#2515) (cherry picked from commit61f1a3cc65
) Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * GH-2447 - Updating label overflow (#2479) (#2517) * GH-2447 - Updating label overflow * Updating labels css (cherry picked from commit923437cc57
) Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> * Gh-2437 - Updating share board modal (#2511) (#2522) * Gh-2437 - Updating share board modal * Updating test * Updating card dialog and test * Updating comment list (cherry picked from commit50ded69852
) Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> * updated/synced prior PR (#2509) (#2523) * updated/synced prior PR * add title back for cypress tests * update unit test for cypress fix * move to function Co-authored-by: Mattermod <mattermod@users.noreply.github.com> (cherry picked from commit5b309e8e25
) Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * modify error page redirects (#2518) (#2532) (cherry picked from commit84a3f8f1fb
) Co-authored-by: Doug Lauder <wiggin77@warpmail.net> * Dismiss tour from overlay (#2525) (#2531) * Shows "(Deleted User)" instead of UUID when user not found (#2354) (#2465) * Shows "(Deleted User)" instead of long, unreadable UUID in case the user is not found In case a user is not found, at present unreadable and long UUIDs are shown which kill the look and feel of the application. This patch replaces the UUID with a more explanatory string. * Update server/services/store/mattermostauthlayer/mattermostauthlayer.go Co-authored-by: Doug Lauder <wiggin77@warpmail.net> (cherry picked from commit68819185a4
) Co-authored-by: Akshay Vasudeva Rao <51395864+akkivasu@users.noreply.github.com> * Update CHANGELOG.md for v0.15 Added one more merged PR to the list * Added ability to dismiss tour from overlay * GH-2212 - Update global link on board (#2492) (#2495) (cherry picked from commit49df41f9b2
) Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> * GH-2387 - Fixing link in comments (#2480) (#2498) (cherry picked from commit5e2cf0b386
) Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> * Addead feature to start product tour on using the welcome template (#2468) * Fixed a bug where images of the welcome board were not copied over. (#2453) * Fixed a buig where images of welcome board were not copied over * Lint fixes * Fixed test * Fixed test * Fixed intended behavio * lint fixes * Fixed tests * Fixed tests Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Akshay Vasudeva Rao <51395864+akkivasu@users.noreply.github.com> Co-authored-by: Winson Wu <93531870+wuwinson@users.noreply.github.com> Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> (cherry picked from commita53e947489
) Co-authored-by: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> * Updated Mac what's new for v0.15 * Done with the frontend for limited card count * Updated the code and done with the unit test case * Updated the code according to the review comment and fixed the test cases and ES lints issue * Fixed the import for css file * Changes made according to review comments, Reverted back whatsnew.txt * Minor changes * Updated the code considering review comments Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Akshay Vasudeva Rao <51395864+akkivasu@users.noreply.github.com> Co-authored-by: Winson Wu <93531870+wuwinson@users.noreply.github.com> Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Jesús Espino <jespinog@gmail.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: Chen-I Lim <chenilim@gmail.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Fixing some tests * Fixing tests * Fixing some compilation errors * More types error fixes * Fixing eslint * Fixing a typing problem * Updating snapshots * Fixing two small problems * Removing unneeded scss empty file * fixing jest test Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Rajat Dabade <rajat.dabade@mattermost.com> Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Akshay Vasudeva Rao <51395864+akkivasu@users.noreply.github.com> Co-authored-by: Winson Wu <93531870+wuwinson@users.noreply.github.com> Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: Chen-I Lim <chenilim@gmail.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
parent
70dabf50f7
commit
5979d19e73
@ -303,9 +303,16 @@
|
||||
"error.page.title": "Sorry, something went wrong",
|
||||
"generic.previous": "Previous",
|
||||
"imagePaste.upload-failed": "Some files not uploaded. File size limit reached",
|
||||
"limitedCard.title": "Cards Hidden",
|
||||
"login.log-in-button": "Log in",
|
||||
"login.log-in-title": "Log in",
|
||||
"login.register-button": "or create an account if you don't have one",
|
||||
"notification-box-card-limit-reached.close-tooltip": "Snooze for 10 days",
|
||||
"notification-box-card-limit-reached.link": "upgrade to a paid plan",
|
||||
"notification-box-card-limit-reached.title": "{cards} cards hidden from board",
|
||||
"notification-box-cards-hidden.title": "Your action hidden another card",
|
||||
"notification-box.card-limit-reached.not-admin.text": "To access archived cards, contact your admin to upgrade to a paid plan.",
|
||||
"notification-box.card-limit-reached.text": "Card limit reached, to view older cards, {link}",
|
||||
"register.login-button": "or log in if you already have an account",
|
||||
"register.signup-title": "Sign up for your account",
|
||||
"share-board.publish": "Publish",
|
||||
|
@ -42,14 +42,16 @@ const App = (props: Props): JSX.Element => {
|
||||
// removed when anonymous plugin routes are implemented. This
|
||||
// check is used to detect if we're running inside the plugin but
|
||||
// in a legacy route
|
||||
if (!Utils.isFocalboardLegacy()) {
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!Utils.isFocalboardLegacy()) {
|
||||
wsClient.open()
|
||||
return () => {
|
||||
}
|
||||
return () => {
|
||||
if (!Utils.isFocalboardLegacy()) {
|
||||
wsClient.close()
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (me) {
|
||||
|
@ -40,6 +40,8 @@ interface Block {
|
||||
createAt: number
|
||||
updateAt: number
|
||||
deleteAt: number
|
||||
|
||||
limited?: boolean
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
|
@ -11,7 +11,7 @@ export function groupCardsByOptions(cards: Card[], optionIds: string[], groupByP
|
||||
if (optionId) {
|
||||
const option = groupByProperty?.options.find((o) => o.id === optionId)
|
||||
if (option) {
|
||||
const c = cards.filter((o) => optionId === o.fields.properties[groupByProperty!.id])
|
||||
const c = cards.filter((o) => optionId === o.fields?.properties[groupByProperty!.id])
|
||||
const group: BoardGroup = {
|
||||
option,
|
||||
cards: c,
|
||||
|
11
webapp/src/boardsCloudLimits/index.ts
Normal file
11
webapp/src/boardsCloudLimits/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const LimitUnlimited = 0
|
||||
|
||||
export interface BoardsCloudLimits {
|
||||
cards: number
|
||||
used_cards: number
|
||||
card_limit_timestamp: number
|
||||
views: number
|
||||
}
|
@ -577,6 +577,713 @@ exports[`components/centerPanel return centerPanel and click on card to show car
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/centerPanel return centerPanel and click on new card to edit template 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="BoardComponent"
|
||||
>
|
||||
<div
|
||||
class="top-head"
|
||||
>
|
||||
<div
|
||||
class="TopBar"
|
||||
>
|
||||
<a
|
||||
class="link"
|
||||
href="https://www.focalboard.com/fwlink/feedback-focalboard.html?v=1.0.0"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Give feedback
|
||||
</a>
|
||||
<a
|
||||
href="https://www.focalboard.com/guide/user?utm_source=webapp"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-help-circle-outline HelpIcon"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="mid-head"
|
||||
>
|
||||
<div
|
||||
class="ViewTitle"
|
||||
>
|
||||
<div
|
||||
class="add-buttons add-visible"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-eye-off-outline undefined"
|
||||
/>
|
||||
<span>
|
||||
hide description
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="title"
|
||||
>
|
||||
<div
|
||||
class="IconSelector"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="octo-icon size-m"
|
||||
>
|
||||
<span>
|
||||
i
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="Editable title"
|
||||
placeholder="Untitled board"
|
||||
spellcheck="true"
|
||||
title="board title"
|
||||
value="board title"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
>
|
||||
<div
|
||||
class="MarkdownEditor octo-editor "
|
||||
>
|
||||
<div
|
||||
class="octo-editor-preview"
|
||||
data-testid="preview-element"
|
||||
/>
|
||||
<div
|
||||
class="MarkdownEditorInput MarkdownEditorInput--IsNotEditing"
|
||||
>
|
||||
<div
|
||||
class="DraftEditor-root"
|
||||
>
|
||||
<div
|
||||
class="DraftEditor-editorContainer"
|
||||
>
|
||||
<div
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
class="notranslate public-DraftEditor-content"
|
||||
contenteditable="true"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
|
||||
>
|
||||
<div
|
||||
data-contents="true"
|
||||
>
|
||||
<div
|
||||
class=""
|
||||
data-block="true"
|
||||
data-editor="123"
|
||||
data-offset-key="123-0-0"
|
||||
>
|
||||
<div
|
||||
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
|
||||
data-offset-key="123-0-0"
|
||||
>
|
||||
<span
|
||||
data-offset-key="123-0-0"
|
||||
>
|
||||
<span
|
||||
data-text="true"
|
||||
>
|
||||
description
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="shareButtonWrapper"
|
||||
>
|
||||
<div
|
||||
class="ShareBoardButton"
|
||||
>
|
||||
<button
|
||||
title="Share board"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-lock-outline LockOutlineIcon"
|
||||
/>
|
||||
<span>
|
||||
Share
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ViewHeader"
|
||||
>
|
||||
<div
|
||||
class="viewSelector"
|
||||
>
|
||||
<input
|
||||
class="Editable "
|
||||
placeholder="Untitled View"
|
||||
spellcheck="true"
|
||||
title="view title"
|
||||
value="view title"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
aria-label="View menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
aria-label="Properties menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Properties
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Group by:
|
||||
<span
|
||||
id="groupByLabel"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Filter
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Sort
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="board-search-field"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-magnify board-search-icon"
|
||||
/>
|
||||
<input
|
||||
class="Editable "
|
||||
placeholder="Search cards"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="View header menu"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ButtonWithMenu"
|
||||
>
|
||||
<div
|
||||
class="button-text"
|
||||
>
|
||||
New
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="button-dropdown"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down DropdownIcon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Table"
|
||||
>
|
||||
<div
|
||||
class="octo-table-body"
|
||||
>
|
||||
<div
|
||||
class="octo-table-header TableHeaders"
|
||||
id="mainBoardHeader"
|
||||
>
|
||||
<div
|
||||
class="octo-table-cell header-cell"
|
||||
style="overflow: unset; opacity: 1; width: 100px;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
class="Label empty "
|
||||
>
|
||||
Name
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<div
|
||||
class="HorizontalGrip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="table-row-container"
|
||||
>
|
||||
<div
|
||||
class="octo-table-group"
|
||||
>
|
||||
<div
|
||||
class="octo-group-header-cell expanded"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polygon
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
class="Label empty "
|
||||
title="Items with an empty name property will go here. This column cannot be removed."
|
||||
>
|
||||
No name
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
0
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-group"
|
||||
>
|
||||
<div
|
||||
class="octo-group-header-cell expanded"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polygon
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
class="Label propColorOrange "
|
||||
>
|
||||
<input
|
||||
class="Editable "
|
||||
placeholder="New Select"
|
||||
spellcheck="true"
|
||||
title="Q1"
|
||||
value="Q1"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
2
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="TableRow octo-table-row"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="action-cell octo-table-cell-btn"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
aria-label="MenuBtn"
|
||||
title="MenuBtn"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="GripIcon Icon"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0V0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell title-cell"
|
||||
id="mainBoardHeader"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
class="octo-icontitle"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<input
|
||||
class="Editable "
|
||||
placeholder="Untitled"
|
||||
spellcheck="true"
|
||||
title="card1"
|
||||
value="card1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="open-button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Open
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableRow octo-table-row"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="action-cell octo-table-cell-btn"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
aria-label="MenuBtn"
|
||||
title="MenuBtn"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="GripIcon Icon"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0V0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell title-cell"
|
||||
id="mainBoardHeader"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
class="octo-icontitle"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<input
|
||||
class="Editable "
|
||||
placeholder="Untitled"
|
||||
spellcheck="true"
|
||||
title="card2"
|
||||
value="card2"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="open-button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Open
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-group"
|
||||
>
|
||||
<div
|
||||
class="octo-group-header-cell expanded"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="DisclosureTriangleIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polygon
|
||||
points="37,35 37,65 63,50"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
class="Label propColorBlue "
|
||||
>
|
||||
<input
|
||||
class="Editable "
|
||||
placeholder="New Select"
|
||||
spellcheck="true"
|
||||
title="Q2"
|
||||
value="Q2"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
0
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-footer"
|
||||
/>
|
||||
<div
|
||||
class="CalculationRow octo-table-row"
|
||||
>
|
||||
<div
|
||||
class="Calculation count octo-table-cell "
|
||||
style="width: 100px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="calculationLabel"
|
||||
>
|
||||
Count
|
||||
</span>
|
||||
<span
|
||||
class="calculationValue"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/centerPanel return centerPanel and press touch 1 with readonly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
@ -59,7 +59,8 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
|
||||
props: {},
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
const template1Title = 'Template 1'
|
||||
const globalTemplateTitle = 'Template Global'
|
||||
|
@ -96,13 +96,14 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
|
||||
}
|
||||
|
||||
const me: IUser = {
|
||||
id: 'user-id-1',
|
||||
username: 'username_1',
|
||||
email: '',
|
||||
props: {},
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false
|
||||
id: 'user-id-1',
|
||||
username: 'username_1',
|
||||
email: '',
|
||||
props: {},
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
|
||||
let store:MockStoreEnhanced<unknown, unknown>
|
||||
|
@ -78,6 +78,7 @@ jest.mock('../../octoClient', () => {
|
||||
'group-prop-id': 'test',
|
||||
},
|
||||
},
|
||||
limited: false,
|
||||
},
|
||||
])),
|
||||
}
|
||||
|
@ -105,6 +105,7 @@ const BoardTemplateSelectorPreview = (props: Props) => {
|
||||
onCardClicked={() => null}
|
||||
addCard={() => Promise.resolve()}
|
||||
showCard={() => null}
|
||||
hiddenCardsCount={0}
|
||||
/>}
|
||||
{activeView?.fields.viewType === 'table' &&
|
||||
<Table
|
||||
@ -120,6 +121,7 @@ const BoardTemplateSelectorPreview = (props: Props) => {
|
||||
onCardClicked={() => null}
|
||||
addCard={() => Promise.resolve()}
|
||||
showCard={() => null}
|
||||
hiddenCardsCount={0}
|
||||
/>}
|
||||
{activeView?.fields.viewType === 'gallery' &&
|
||||
<Gallery
|
||||
@ -130,6 +132,7 @@ const BoardTemplateSelectorPreview = (props: Props) => {
|
||||
selectedCardIds={[]}
|
||||
onCardClicked={() => null}
|
||||
addCard={() => Promise.resolve()}
|
||||
hiddenCardsCount={0}
|
||||
/>}
|
||||
{activeView?.fields.viewType === 'calendar' &&
|
||||
<CalendarFullView
|
||||
|
@ -36,8 +36,10 @@ describe('components/cardBadges', () => {
|
||||
const state: Partial<RootState> = {
|
||||
cards: {
|
||||
current: '',
|
||||
limitTimestamp: 0,
|
||||
cards: blocksById([card, emptyCard]),
|
||||
templates: {},
|
||||
cardHiddenWarning: true,
|
||||
},
|
||||
comments: {
|
||||
comments: blocksById(comments),
|
||||
|
148
webapp/src/components/cardLimitNotification.tsx
Normal file
148
webapp/src/components/cardLimitNotification.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback, useEffect, useState} from 'react'
|
||||
import {useIntl, FormattedMessage} from 'react-intl'
|
||||
|
||||
import AlertIcon from '../widgets/icons/alert'
|
||||
|
||||
import {useAppSelector, useAppDispatch} from '../store/hooks'
|
||||
import {IUser, UserConfigPatch} from '../user'
|
||||
import {getMe, patchProps, getCardLimitSnoozeUntil, getCardHiddenWarningSnoozeUntil} from '../store/users'
|
||||
import {getCurrentBoardHiddenCardsCount, getCardHiddenWarning} from '../store/cards'
|
||||
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../telemetry/telemetryClient'
|
||||
import octoClient from '../octoClient'
|
||||
|
||||
import NotificationBox from '../widgets/notification-box'
|
||||
|
||||
const snoozeTime = 1000 * 60 * 60 * 24 * 10
|
||||
const checkSnoozeInterval = 1000 * 60 * 5
|
||||
|
||||
const CardLimitNotification = () => {
|
||||
const intl = useIntl()
|
||||
const [time, setTime] = useState(Date.now())
|
||||
|
||||
const hiddenCards = useAppSelector<number>(getCurrentBoardHiddenCardsCount)
|
||||
const cardHiddenWarning = useAppSelector<boolean>(getCardHiddenWarning)
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const snoozedUntil = useAppSelector<number>(getCardLimitSnoozeUntil)
|
||||
const snoozedCardHiddenWarningUntil = useAppSelector<number>(getCardHiddenWarningSnoozeUntil)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const onCloseHidden = useCallback(async () => {
|
||||
if (me) {
|
||||
const patch: UserConfigPatch = {
|
||||
updatedFields: {
|
||||
focalboard_cardLimitSnoozeUntil: `${Date.now() + snoozeTime}`,
|
||||
},
|
||||
}
|
||||
|
||||
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
|
||||
if (patchedProps) {
|
||||
dispatch(patchProps(patchedProps))
|
||||
}
|
||||
}
|
||||
}, [me])
|
||||
|
||||
const onCloseWarning = useCallback(async () => {
|
||||
if (me) {
|
||||
const patch: UserConfigPatch = {
|
||||
updatedFields: {
|
||||
focalboard_cardHiddenWarningSnoozeUntil: `${Date.now() + snoozeTime}`,
|
||||
},
|
||||
}
|
||||
|
||||
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
|
||||
if (patchedProps) {
|
||||
dispatch(patchProps(patchedProps))
|
||||
}
|
||||
}
|
||||
}, [me])
|
||||
|
||||
let show = false
|
||||
let onClose = onCloseHidden
|
||||
let title = intl.formatMessage(
|
||||
{
|
||||
id: 'notification-box-card-limit-reached.title',
|
||||
defaultMessage: '{cards} cards hidden from board',
|
||||
},
|
||||
{cards: hiddenCards},
|
||||
)
|
||||
|
||||
if (hiddenCards > 0 && time > snoozedUntil) {
|
||||
show = true
|
||||
}
|
||||
|
||||
if (!show && cardHiddenWarning) {
|
||||
show = time > snoozedCardHiddenWarningUntil
|
||||
onClose = onCloseWarning
|
||||
title = intl.formatMessage(
|
||||
{
|
||||
id: 'notification-box-cards-hidden.title',
|
||||
defaultMessage: 'Your action hidden another card',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!show) {
|
||||
const interval = setInterval(() => setTime(Date.now()), checkSnoozeInterval)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}
|
||||
return () => null
|
||||
}, [show])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.LimitCardLimitReached, {})
|
||||
}
|
||||
}, [show])
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
(window as any).openPricingModal()()
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.LimitCardLimitLinkOpen, {})
|
||||
}, [])
|
||||
|
||||
const hasPermissionToUpgrade = me?.roles?.split(' ').indexOf('system_admin') !== -1
|
||||
|
||||
if (!show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationBox
|
||||
icon={<AlertIcon/>}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
closeTooltip={intl.formatMessage({
|
||||
id: 'notification-box-card-limit-reached.close-tooltip',
|
||||
defaultMessage: 'Snooze for 10 days',
|
||||
})}
|
||||
>
|
||||
{hasPermissionToUpgrade &&
|
||||
<FormattedMessage
|
||||
id='notification-box.card-limit-reached.text'
|
||||
defaultMessage='Card limit reached, to view older cards, {link}'
|
||||
values={{
|
||||
link: (
|
||||
<a
|
||||
onClick={onClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='notification-box-card-limit-reached.link'
|
||||
defaultMessage='upgrade to a paid plan'
|
||||
/>
|
||||
</a>),
|
||||
}}
|
||||
/>}
|
||||
{!hasPermissionToUpgrade &&
|
||||
<FormattedMessage
|
||||
id='notification-box.card-limit-reached.not-admin.text'
|
||||
defaultMessage='To access archived cards, contact your admin to upgrade to a paid plan.'
|
||||
/>}
|
||||
</NotificationBox>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(CardLimitNotification)
|
@ -81,4 +81,11 @@
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.NotificationBox {
|
||||
.AlertIcon {
|
||||
color: #ffbc1f;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,6 +105,14 @@ describe('components/centerPanel', () => {
|
||||
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
limits: {
|
||||
cards: 0,
|
||||
used_cards: 0,
|
||||
card_limit_timestamp: 0,
|
||||
views: 0,
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
templates: [card1, card2],
|
||||
cards: [card1, card2],
|
||||
@ -149,6 +157,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -166,6 +175,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -184,6 +194,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -202,6 +213,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -221,6 +233,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -251,6 +264,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -273,6 +287,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -301,6 +316,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -338,6 +354,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -365,6 +382,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -393,6 +411,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={mockedShowCard}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -417,6 +436,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -438,6 +458,7 @@ describe('components/centerPanel', () => {
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -449,63 +470,63 @@ describe('components/centerPanel', () => {
|
||||
expect(mockedMutator.insertBlock).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
// TODO: Fix this
|
||||
// test('click on new card to add card from template', () => {
|
||||
// activeView.fields.viewType = 'table'
|
||||
// activeView.fields.defaultTemplateId = '1'
|
||||
// const {container} = render(wrapDNDIntl(
|
||||
// <ReduxProvider store={store}>
|
||||
// <CenterPanel
|
||||
// cards={[card1, card2]}
|
||||
// views={[activeView]}
|
||||
// board={board}
|
||||
// activeView={activeView}
|
||||
// readonly={false}
|
||||
// showCard={jest.fn()}
|
||||
// showShared={true}
|
||||
// groupByProperty={groupProperty}
|
||||
// shownCardId={card1.id}
|
||||
// />
|
||||
// </ReduxProvider>,
|
||||
// ))
|
||||
// const elementMenuWrapper = container.querySelector('.ButtonWithMenu > div.MenuWrapper')
|
||||
// expect(elementMenuWrapper).not.toBeNull()
|
||||
// userEvent.click(elementMenuWrapper!)
|
||||
// const elementCard1 = within(elementMenuWrapper!.parentElement!).getByRole('button', {name: 'card1'})
|
||||
// expect(elementCard1).not.toBeNull()
|
||||
// userEvent.click(elementCard1)
|
||||
// expect(mockedMutator.performAsUndoGroup).toBeCalledTimes(1)
|
||||
// })
|
||||
// test('click on new card to edit template', () => {
|
||||
// activeView.fields.viewType = 'table'
|
||||
// activeView.fields.defaultTemplateId = '1'
|
||||
// const {container} = render(wrapDNDIntl(
|
||||
// <ReduxProvider store={store}>
|
||||
// <CenterPanel
|
||||
// cards={[card1, card2]}
|
||||
// views={[activeView]}
|
||||
// board={board}
|
||||
// activeView={activeView}
|
||||
// readonly={false}
|
||||
// showCard={jest.fn()}
|
||||
// showShared={true}
|
||||
// groupByProperty={groupProperty}
|
||||
// shownCardId={card1.id}
|
||||
// />
|
||||
// </ReduxProvider>,
|
||||
// ))
|
||||
// const elementMenuWrapper = container.querySelector('.ButtonWithMenu > div.MenuWrapper')
|
||||
// expect(elementMenuWrapper).not.toBeNull()
|
||||
// userEvent.click(elementMenuWrapper!)
|
||||
// const elementCard1 = within(elementMenuWrapper!.parentElement!).getByRole('button', {name: 'card1'})
|
||||
// expect(elementCard1).not.toBeNull()
|
||||
// const elementMenuWrapperCard1 = within(elementCard1).getByRole('button', {name: 'menuwrapper'})
|
||||
// expect(elementMenuWrapperCard1).not.toBeNull()
|
||||
// userEvent.click(elementMenuWrapperCard1)
|
||||
// const elementEditMenuTemplate = within(elementMenuWrapperCard1).getByRole('button', {name: 'Edit'})
|
||||
// expect(elementMenuWrapperCard1).not.toBeNull()
|
||||
// userEvent.click(elementEditMenuTemplate)
|
||||
// expect(container).toMatchSnapshot()
|
||||
// })
|
||||
test('click on new card to add card from template', () => {
|
||||
activeView.fields.viewType = 'table'
|
||||
activeView.fields.defaultTemplateId = '1'
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CenterPanel
|
||||
cards={[card1, card2]}
|
||||
views={[activeView]}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
readonly={false}
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const elementMenuWrapper = container.querySelector('.ButtonWithMenu > div.MenuWrapper')
|
||||
expect(elementMenuWrapper).not.toBeNull()
|
||||
userEvent.click(elementMenuWrapper!)
|
||||
const elementCard1 = within(elementMenuWrapper!.parentElement!).getByRole('button', {name: 'card1'})
|
||||
expect(elementCard1).not.toBeNull()
|
||||
userEvent.click(elementCard1)
|
||||
expect(mockedMutator.performAsUndoGroup).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('click on new card to edit template', () => {
|
||||
activeView.fields.viewType = 'table'
|
||||
activeView.fields.defaultTemplateId = '1'
|
||||
const {container} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CenterPanel
|
||||
cards={[card1, card2]}
|
||||
views={[activeView]}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
readonly={false}
|
||||
showCard={jest.fn()}
|
||||
groupByProperty={groupProperty}
|
||||
shownCardId={card1.id}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const elementMenuWrapper = container.querySelector('.ButtonWithMenu > div.MenuWrapper')
|
||||
expect(elementMenuWrapper).not.toBeNull()
|
||||
userEvent.click(elementMenuWrapper!)
|
||||
const elementCard1 = within(elementMenuWrapper!.parentElement!).getByRole('button', {name: 'card1'})
|
||||
expect(elementCard1).not.toBeNull()
|
||||
const elementMenuWrapperCard1 = within(elementCard1).getByRole('button', {name: 'menuwrapper'})
|
||||
expect(elementMenuWrapperCard1).not.toBeNull()
|
||||
userEvent.click(elementMenuWrapperCard1)
|
||||
const elementEditMenuTemplate = within(elementMenuWrapperCard1).getByRole('button', {name: 'Edit'})
|
||||
expect(elementMenuWrapperCard1).not.toBeNull()
|
||||
userEvent.click(elementEditMenuTemplate)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -16,7 +16,8 @@ import {CardFilter} from '../cardFilter'
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
import {UserSettings} from '../userSettings'
|
||||
import {getCurrentCard, addCard as addCardAction, addTemplate as addTemplateAction} from '../store/cards'
|
||||
import {getCurrentCard, addCard as addCardAction, addTemplate as addTemplateAction, showCardHiddenWarning} from '../store/cards'
|
||||
import {getCardLimitTimestamp} from '../store/limits'
|
||||
import {updateView} from '../store/views'
|
||||
import {getVisibleAndHiddenGroups} from '../boardUtils'
|
||||
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient'
|
||||
@ -51,6 +52,8 @@ import Table from './table/table'
|
||||
|
||||
import CalendarFullView from './calendar/fullCalendar'
|
||||
|
||||
import CardLimitNotification from './cardLimitNotification'
|
||||
|
||||
import Gallery from './gallery/gallery'
|
||||
import {BoardTourSteps, FINISHED, TOUR_BOARD, TOUR_CARD} from './onboardingTour'
|
||||
import ShareBoardTourStep from './onboardingTour/shareBoard/shareBoard'
|
||||
@ -66,6 +69,7 @@ type Props = {
|
||||
readonly: boolean
|
||||
shownCardId?: string
|
||||
showCard: (cardId?: string) => void
|
||||
hiddenCardsCount: number
|
||||
}
|
||||
|
||||
const CenterPanel = (props: Props) => {
|
||||
@ -76,6 +80,7 @@ const CenterPanel = (props: Props) => {
|
||||
const onboardingTourStarted = useAppSelector(getOnboardingTourStarted)
|
||||
const onboardingTourCategory = useAppSelector(getOnboardingTourCategory)
|
||||
const onboardingTourStep = useAppSelector(getOnboardingTourStep)
|
||||
const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp)
|
||||
const me = useAppSelector(getMe)
|
||||
const currentCard = useAppSelector(getCurrentCard)
|
||||
const dispatch = useAppDispatch()
|
||||
@ -198,6 +203,7 @@ const CenterPanel = (props: Props) => {
|
||||
showCard(undefined)
|
||||
},
|
||||
)
|
||||
dispatch(showCardHiddenWarning(cardLimitTimestamp > 0))
|
||||
await mutator.changeViewCardOrder(board.id, activeView.id, activeView.fields.cardOrder, [...activeView.fields.cardOrder, newCard.id], 'add-card')
|
||||
})
|
||||
}, [props.activeView, props.board.id, props.board.cardProperties, props.groupByProperty, showCard])
|
||||
@ -349,6 +355,7 @@ const CenterPanel = (props: Props) => {
|
||||
className='BoardComponent'
|
||||
onClick={backgroundClicked}
|
||||
>
|
||||
<CardLimitNotification/>
|
||||
{props.shownCardId &&
|
||||
<RootPortal>
|
||||
<CardDialog
|
||||
@ -412,6 +419,7 @@ const CenterPanel = (props: Props) => {
|
||||
onCardClicked={cardClicked}
|
||||
addCard={addCard}
|
||||
showCard={showCard}
|
||||
hiddenCardsCount={props.hiddenCardsCount}
|
||||
/>}
|
||||
{activeView.fields.viewType === 'table' &&
|
||||
<Table
|
||||
@ -427,6 +435,7 @@ const CenterPanel = (props: Props) => {
|
||||
showCard={showCard}
|
||||
addCard={addCard}
|
||||
onCardClicked={cardClicked}
|
||||
hiddenCardsCount={props.hiddenCardsCount}
|
||||
/>}
|
||||
{activeView.fields.viewType === 'calendar' &&
|
||||
<CalendarFullView
|
||||
@ -450,6 +459,7 @@ const CenterPanel = (props: Props) => {
|
||||
onCardClicked={cardClicked}
|
||||
selectedCardIds={selectedCardIds}
|
||||
addCard={(show) => addCard('', show)}
|
||||
hiddenCardsCount={props.hiddenCardsCount}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
|
@ -33,6 +33,7 @@ const checkboxBlock: ContentBlock = {
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
deleteAt: 0,
|
||||
limited: false,
|
||||
}
|
||||
|
||||
const cardDetailContextValue = (autoAdded: boolean): CardDetailContextType => ({
|
||||
|
@ -30,6 +30,7 @@ const contentBlock: ContentBlock = {
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
deleteAt: 0,
|
||||
limited: false,
|
||||
}
|
||||
|
||||
const wrap = (child: ReactNode): ReactElement => (
|
||||
|
@ -36,6 +36,7 @@ describe('components/content/ImageElement', () => {
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
deleteAt: 0,
|
||||
limited: false,
|
||||
}
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
|
@ -37,6 +37,7 @@ const defaultBlock: TextBlock = {
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
deleteAt: 0,
|
||||
limited: false,
|
||||
}
|
||||
describe('components/content/TextElement', () => {
|
||||
beforeAll(() => {
|
||||
|
@ -1,5 +1,87 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`src/components/gallery/Gallery limited card count check 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Gallery"
|
||||
>
|
||||
<div
|
||||
class="GalleryCard selected"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="GalleryCard"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="gallery-item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="octo-gallery-new"
|
||||
>
|
||||
+ New
|
||||
</div>
|
||||
<div
|
||||
class="gallery-hidden-cards"
|
||||
>
|
||||
<div
|
||||
class="HiddenCardCount"
|
||||
>
|
||||
<div
|
||||
class="hidden-card-title"
|
||||
>
|
||||
Cards Hidden
|
||||
</div>
|
||||
<button
|
||||
class="Button"
|
||||
title="hidden-card-count"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
2
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/gallery/Gallery return Gallery and click new 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
@ -25,4 +25,8 @@
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-hidden-cards {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
@ -38,10 +38,12 @@ describe('src/components/gallery/Gallery', () => {
|
||||
},
|
||||
cards: {
|
||||
current: '',
|
||||
limitTimestamp: 0,
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
templates: {},
|
||||
cardHiddenWarning: true,
|
||||
},
|
||||
teams: {
|
||||
current: {id: 'team-id'},
|
||||
@ -63,7 +65,7 @@ describe('src/components/gallery/Gallery', () => {
|
||||
id: 'user_id_1',
|
||||
props: {},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
const store = mockStateStore([], state)
|
||||
beforeEach(() => {
|
||||
@ -80,6 +82,7 @@ describe('src/components/gallery/Gallery', () => {
|
||||
addCard={jest.fn()}
|
||||
selectedCardIds={[card.id]}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -99,6 +102,7 @@ describe('src/components/gallery/Gallery', () => {
|
||||
addCard={jest.fn()}
|
||||
selectedCardIds={[card.id]}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -118,6 +122,7 @@ describe('src/components/gallery/Gallery', () => {
|
||||
addCard={mockAddCard}
|
||||
selectedCardIds={[card.id]}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -140,6 +145,7 @@ describe('src/components/gallery/Gallery', () => {
|
||||
addCard={jest.fn()}
|
||||
selectedCardIds={[card.id]}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -156,6 +162,7 @@ describe('src/components/gallery/Gallery', () => {
|
||||
addCard={jest.fn()}
|
||||
selectedCardIds={[]}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
@ -168,4 +175,67 @@ describe('src/components/gallery/Gallery', () => {
|
||||
fireEvent.drop(drop)
|
||||
expect(mockedMutator.performAsUndoGroup).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('limited card count check', () => {
|
||||
const boardTest = TestBlockFactory.createBoard()
|
||||
const card1 = TestBlockFactory.createCard(boardTest)
|
||||
const card3 = TestBlockFactory.createCard(boardTest)
|
||||
const stateTest = {
|
||||
contents: {
|
||||
contents: blocksById(contents),
|
||||
contentsByCard: {
|
||||
[card.id]: [contents[0], contents[1]],
|
||||
[card2.id]: [contents[2]],
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
current: '',
|
||||
cards: {
|
||||
[card1.id]: card1,
|
||||
[card3.id]: card3,
|
||||
},
|
||||
templates: {},
|
||||
cardHiddenWarning: true,
|
||||
limitTimestamp: 2,
|
||||
},
|
||||
users: {
|
||||
me: {
|
||||
id: 'user_id_1',
|
||||
props: {},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
current: {id: 'team-id'},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
boards: {
|
||||
current: board.id,
|
||||
boards: {
|
||||
[board.id]: board,
|
||||
},
|
||||
myBoardMemberships: {
|
||||
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
const storeTest = mockStateStore([], stateTest)
|
||||
const {container, getByTitle} = render(wrapDNDIntl(
|
||||
<ReduxProvider store={storeTest}>
|
||||
<Gallery
|
||||
board={boardTest}
|
||||
cards={[card1, card3]}
|
||||
activeView={activeView}
|
||||
readonly={false}
|
||||
addCard={jest.fn()}
|
||||
selectedCardIds={[card1.id]}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={2}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
expect(getByTitle('hidden-card-count').innerHTML).toBe('<span>2</span>')
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
@ -4,6 +4,8 @@ import React, {useMemo, useCallback} from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {Constants, Permission} from '../../constants'
|
||||
import HiddenCardCount from '../../components/hiddenCardCount/hiddenCardCount'
|
||||
|
||||
import {Card} from '../../blocks/card'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
@ -23,10 +25,11 @@ type Props = {
|
||||
addCard: (show: boolean) => Promise<void>
|
||||
selectedCardIds: string[]
|
||||
onCardClicked: (e: React.MouseEvent, card: Card) => void
|
||||
hiddenCardsCount: number
|
||||
}
|
||||
|
||||
const Gallery = (props: Props): JSX.Element => {
|
||||
const {activeView, board, cards} = props
|
||||
const {activeView, board, cards, hiddenCardsCount} = props
|
||||
const visiblePropertyTemplates = useMemo(() => {
|
||||
return board.cardProperties.filter(
|
||||
(template: IPropertyTemplate) => activeView.fields.visiblePropertyIds.includes(template.id),
|
||||
@ -61,6 +64,7 @@ const Gallery = (props: Props): JSX.Element => {
|
||||
const visibleBadges = activeView.fields.visiblePropertyIds.includes(Constants.badgesColumnId)
|
||||
|
||||
return (
|
||||
|
||||
<div className='Gallery'>
|
||||
{cards.filter((c) => c.boardId === board.id).map((card) => {
|
||||
return (
|
||||
@ -97,6 +101,12 @@ const Gallery = (props: Props): JSX.Element => {
|
||||
</div>
|
||||
</BoardPermissionGate>
|
||||
}
|
||||
{hiddenCardsCount > 0 &&
|
||||
<div className='gallery-hidden-cards'>
|
||||
<HiddenCardCount
|
||||
hiddenCardsCount={hiddenCardsCount}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
13
webapp/src/components/hiddenCardCount/hiddenCardCount.scss
Normal file
13
webapp/src/components/hiddenCardCount/hiddenCardCount.scss
Normal file
@ -0,0 +1,13 @@
|
||||
.HiddenCardCount {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
|
||||
.hidden-card-title {
|
||||
background: rgba(243, 192, 199, 0.2);
|
||||
color: #d24b4e;
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
25
webapp/src/components/hiddenCardCount/hiddenCardCount.tsx
Normal file
25
webapp/src/components/hiddenCardCount/hiddenCardCount.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import Button from '../../widgets/buttons/button'
|
||||
|
||||
import './hiddenCardCount.scss'
|
||||
|
||||
type Props = {
|
||||
hiddenCardsCount: number
|
||||
}
|
||||
|
||||
const HiddenCardCount = (props: Props): JSX.Element => {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<div className='HiddenCardCount'>
|
||||
<div className='hidden-card-title'>{intl.formatMessage({id: 'limitedCard.title', defaultMessage: 'Cards Hidden'})}</div>
|
||||
<Button title='hidden-card-count'>{props.hiddenCardsCount}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HiddenCardCount
|
@ -1,5 +1,34 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`src/components/kanban/kanbanHiddenColumnItem limited card check 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="octo-board-hidden-item"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
class="Label propColorDefault "
|
||||
>
|
||||
propOption
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="Button"
|
||||
title="hidden-card-count"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
2
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/kanban/kanbanHiddenColumnItem return kanbanHiddenColumnItem and click menuwrapper 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
@ -93,4 +93,12 @@
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden-card {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.kanban-hidden-cards {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
@ -127,6 +127,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
@ -161,6 +162,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
@ -194,6 +196,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
@ -229,6 +232,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
@ -274,6 +278,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
@ -319,6 +324,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
@ -365,6 +371,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={mockedAddCard}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
@ -402,6 +409,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
@ -439,6 +447,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
@ -483,6 +492,7 @@ describe('src/component/kanban/kanban', () => {
|
||||
onCardClicked={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
showCard={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
|
@ -19,6 +19,7 @@ import {Constants, Permission} from '../../constants'
|
||||
import {dragAndDropRearrange} from '../cardDetail/cardDetailContentsUtility'
|
||||
|
||||
import BoardPermissionGate from '../permissions/boardPermissionGate'
|
||||
import HiddenCardCount from '../../components/hiddenCardCount/hiddenCardCount'
|
||||
|
||||
import KanbanCard from './kanbanCard'
|
||||
import KanbanColumn from './kanbanColumn'
|
||||
@ -40,6 +41,7 @@ type Props = {
|
||||
onCardClicked: (e: React.MouseEvent, card: Card) => void
|
||||
addCard: (groupByOptionId?: string, show?:boolean) => Promise<void>
|
||||
showCard: (cardId?: string) => void
|
||||
hiddenCardsCount: number
|
||||
}
|
||||
|
||||
const ScrollingComponent = withScrolling('div')
|
||||
@ -47,7 +49,7 @@ const hStrength = createHorizontalStrength(Utils.isMobile() ? 60 : 250)
|
||||
const vStrength = createVerticalStrength(Utils.isMobile() ? 60 : 250)
|
||||
|
||||
const Kanban = (props: Props) => {
|
||||
const {board, activeView, cards, groupByProperty, visibleGroups, hiddenGroups} = props
|
||||
const {board, activeView, cards, groupByProperty, visibleGroups, hiddenGroups, hiddenCardsCount} = props
|
||||
|
||||
if (!groupByProperty) {
|
||||
Utils.assertFailure('Board views must have groupByProperty set')
|
||||
@ -232,7 +234,7 @@ const Kanban = (props: Props) => {
|
||||
|
||||
{/* Hidden column header */}
|
||||
|
||||
{hiddenGroups.length > 0 &&
|
||||
{(hiddenGroups.length > 0 || hiddenCardsCount > 0) &&
|
||||
<div className='octo-board-header-cell narrow'>
|
||||
<FormattedMessage
|
||||
id='BoardComponent.hidden-columns'
|
||||
@ -304,7 +306,7 @@ const Kanban = (props: Props) => {
|
||||
|
||||
{/* Hidden columns */}
|
||||
|
||||
{hiddenGroups.length > 0 &&
|
||||
{(hiddenGroups.length > 0 || hiddenCardsCount > 0) &&
|
||||
<div className='octo-board-column narrow'>
|
||||
{hiddenGroups.map((group) => (
|
||||
<KanbanHiddenColumnItem
|
||||
@ -316,6 +318,10 @@ const Kanban = (props: Props) => {
|
||||
onDrop={(card: Card) => onDropToColumn(group.option, card)}
|
||||
/>
|
||||
))}
|
||||
{hiddenCardsCount > 0 &&
|
||||
<div className='ml-1'>
|
||||
<HiddenCardCount hiddenCardsCount={hiddenCardsCount}/>
|
||||
</div>}
|
||||
</div>}
|
||||
</div>
|
||||
</ScrollingComponent>
|
||||
|
@ -22,6 +22,7 @@ describe('src/components/kanban/kanbanHiddenColumnItem', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
const activeView = TestBlockFactory.createBoardView(board)
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const card2 = TestBlockFactory.createCard(board)
|
||||
const option:IPropertyOption = {
|
||||
id: 'id1',
|
||||
value: 'propOption',
|
||||
@ -99,4 +100,24 @@ describe('src/components/kanban/kanbanHiddenColumnItem', () => {
|
||||
userEvent.click(buttonShow)
|
||||
expect(mockedMutator.unhideViewColumn).toBeCalledWith(activeView.boardId, activeView, option.id)
|
||||
})
|
||||
|
||||
test('limited card check', () => {
|
||||
card.limited = true
|
||||
card2.limited = true
|
||||
option.id = 'hidden-card-group-id'
|
||||
const {container, getByTitle} = render(wrapDNDIntl(
|
||||
<KanbanHiddenColumnItem
|
||||
activeView={activeView}
|
||||
group={{
|
||||
option,
|
||||
cards: [card, card2],
|
||||
}}
|
||||
readonly={false}
|
||||
onDrop={jest.fn()}
|
||||
intl={intl}
|
||||
/>,
|
||||
))
|
||||
expect(getByTitle('hidden-card-count')).toHaveTextContent('2')
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
@ -6,7 +6,6 @@ import {IntlShape} from 'react-intl'
|
||||
import {useDrop} from 'react-dnd'
|
||||
|
||||
import mutator from '../../mutator'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import ShowIcon from '../../widgets/icons/show'
|
||||
@ -15,6 +14,8 @@ import {Card} from '../../blocks/card'
|
||||
import {BoardGroup} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
|
||||
import Button from '../../widgets/buttons/button'
|
||||
|
||||
type Props = {
|
||||
activeView: BoardView
|
||||
group: BoardGroup
|
||||
@ -25,6 +26,8 @@ type Props = {
|
||||
|
||||
export default function KanbanHiddenColumnItem(props: Props): JSX.Element {
|
||||
const {activeView, intl, group} = props
|
||||
const hiddenCardGroupId = 'hidden-card-group-id'
|
||||
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: 'card',
|
||||
collect: (monitor) => ({
|
||||
@ -64,7 +67,8 @@ export default function KanbanHiddenColumnItem(props: Props): JSX.Element {
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<Button>{`${group.cards.length}`}</Button>
|
||||
{props.group.option.id !== hiddenCardGroupId && <Button>{`${group.cards.length}`}</Button>}
|
||||
{props.group.option.id === hiddenCardGroupId && <Button title='hidden-card-count'>{`${group.cards.length}`}</Button>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ describe('components/messages/CloudMessage', () => {
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
const state = {
|
||||
users: {
|
||||
@ -75,6 +76,7 @@ describe('components/messages/CloudMessage', () => {
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
const state = {
|
||||
users: {
|
||||
@ -103,6 +105,7 @@ describe('components/messages/CloudMessage', () => {
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
const state = {
|
||||
users: {
|
||||
@ -139,6 +142,7 @@ describe('components/messages/CloudMessage', () => {
|
||||
create_at: 0,
|
||||
update_at: Date.now() - (1000 * 60 * 60 * 24), //24 hours,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
const state = {
|
||||
users: {
|
||||
|
@ -96,7 +96,7 @@ card3.id = 'card3'
|
||||
card3.title = 'card-3'
|
||||
card3.boardId = fakeBoard.id
|
||||
|
||||
const me: IUser = {id: 'user-id-1', username: 'username_1', email: '', props: {}, create_at: 0, update_at: 0, is_bot: false}
|
||||
const me: IUser = {id: 'user-id-1', username: 'username_1', email: '', props: {}, create_at: 0, update_at: 0, is_bot: false, roles: 'system_user'}
|
||||
|
||||
const categoryAttribute1 = TestBlockFactory.createCategoryBoards()
|
||||
categoryAttribute1.name = 'Category 1'
|
||||
|
@ -1856,6 +1856,414 @@ exports[`components/table/Table extended should match snapshot with UpdatedBy 1`
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/table/Table limited card in table view 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Table"
|
||||
>
|
||||
<div
|
||||
class="octo-table-body"
|
||||
>
|
||||
<div
|
||||
class="octo-table-header TableHeaders"
|
||||
id="mainBoardHeader"
|
||||
>
|
||||
<div
|
||||
class="octo-table-cell header-cell"
|
||||
style="overflow: unset; opacity: 1; width: 100px;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper disabled"
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
class="Label empty "
|
||||
>
|
||||
Name
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell header-cell"
|
||||
draggable="true"
|
||||
style="overflow: unset; opacity: 1; width: 100px;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper disabled"
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
class="Label empty "
|
||||
>
|
||||
Property 1
|
||||
<svg
|
||||
class="SortDownIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline
|
||||
points="50,20 50,80"
|
||||
/>
|
||||
<polyline
|
||||
points="30,60 50,80 70,60"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell header-cell"
|
||||
draggable="true"
|
||||
style="overflow: unset; opacity: 1; width: 100px;"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper disabled"
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
class="Label empty "
|
||||
>
|
||||
Property 2
|
||||
<svg
|
||||
class="SortUpIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline
|
||||
points="50,20 50,80"
|
||||
/>
|
||||
<polyline
|
||||
points="30,40 50,20 70,40"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="table-row-container"
|
||||
>
|
||||
<div
|
||||
class="TableRow octo-table-row"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="action-cell octo-table-cell-btn"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
aria-label="MenuBtn"
|
||||
class="IconButton"
|
||||
title="MenuBtn"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="GripIcon Icon"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0V0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell title-cell"
|
||||
id="mainBoardHeader"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
class="octo-icontitle"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<input
|
||||
class="Editable readonly "
|
||||
placeholder="Untitled"
|
||||
readonly=""
|
||||
spellcheck="true"
|
||||
title="title"
|
||||
value="title"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="open-button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Open
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
class="octo-propertyvalue octo-propertyvalue--readonly"
|
||||
data-testid="select-non-editable"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="Label propColorBrown "
|
||||
>
|
||||
<span
|
||||
class="Label-text"
|
||||
>
|
||||
value 1
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
class="octo-propertyvalue octo-propertyvalue--readonly"
|
||||
data-testid="select-non-editable"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="Label empty "
|
||||
>
|
||||
<span
|
||||
class="Label-text"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableRow octo-table-row"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="action-cell octo-table-cell-btn"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper optionsMenu"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
aria-label="MenuBtn"
|
||||
class="IconButton"
|
||||
title="MenuBtn"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="GripIcon Icon"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0V0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell title-cell"
|
||||
id="mainBoardHeader"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
class="octo-icontitle"
|
||||
>
|
||||
<div
|
||||
class="octo-icon"
|
||||
>
|
||||
i
|
||||
</div>
|
||||
<input
|
||||
class="Editable readonly "
|
||||
placeholder="Untitled"
|
||||
readonly=""
|
||||
spellcheck="true"
|
||||
title="title"
|
||||
value="title"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="open-button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Open
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
class="octo-propertyvalue octo-propertyvalue--readonly"
|
||||
data-testid="select-non-editable"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="Label propColorBrown "
|
||||
>
|
||||
<span
|
||||
class="Label-text"
|
||||
>
|
||||
value 1
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-cell"
|
||||
style="width: 100px;"
|
||||
>
|
||||
<div
|
||||
class="octo-propertyvalue octo-propertyvalue--readonly"
|
||||
data-testid="select-non-editable"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="Label empty "
|
||||
>
|
||||
<span
|
||||
class="Label-text"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="octo-table-footer"
|
||||
/>
|
||||
<div
|
||||
class="CalculationRow octo-table-row"
|
||||
>
|
||||
<div
|
||||
class="Calculation count octo-table-cell disabled "
|
||||
style="width: 100px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="calculationLabel"
|
||||
>
|
||||
Count
|
||||
</span>
|
||||
<span
|
||||
class="calculationValue"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="Calculation none octo-table-cell disabled "
|
||||
style="width: 100px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="calculationLabel"
|
||||
>
|
||||
Calculate
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-up ChevronUpIcon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Calculation none octo-table-cell disabled "
|
||||
style="width: 100px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="calculationLabel"
|
||||
>
|
||||
Calculate
|
||||
</span>
|
||||
<i
|
||||
class="CompassIcon icon-chevron-up ChevronUpIcon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="HiddenCardCount"
|
||||
>
|
||||
<div
|
||||
class="hidden-card-title"
|
||||
>
|
||||
Cards Hidden
|
||||
</div>
|
||||
<button
|
||||
class="Button"
|
||||
title="hidden-card-count"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
2
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/table/Table should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
@ -99,6 +99,7 @@ describe('components/table/Table', () => {
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
@ -127,6 +128,7 @@ describe('components/table/Table', () => {
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
@ -155,6 +157,7 @@ describe('components/table/Table', () => {
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
@ -190,12 +193,74 @@ describe('components/table/Table', () => {
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('limited card in table view', () => {
|
||||
const callback = jest.fn()
|
||||
const addCard = jest.fn()
|
||||
const boardTest = TestBlockFactory.createBoard()
|
||||
const card1 = TestBlockFactory.createCard(boardTest)
|
||||
const card2 = TestBlockFactory.createCard(boardTest)
|
||||
const mockStore = configureStore([])
|
||||
|
||||
const stateTest = {
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
contents: {
|
||||
contents: {},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card1.id]: card1,
|
||||
[card2.id]: card2,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
current: {id: 'team-id'},
|
||||
},
|
||||
boards: {
|
||||
current: boardTest.id,
|
||||
boards: {
|
||||
[boardTest.id]: boardTest,
|
||||
},
|
||||
myBoardMemberships: {
|
||||
[boardTest.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const storeTest = mockStore(stateTest)
|
||||
card.limited = true
|
||||
|
||||
const component = wrapDNDIntl(
|
||||
<ReduxProvider store={storeTest}>
|
||||
<Table
|
||||
board={boardTest}
|
||||
activeView={view}
|
||||
visibleGroups={[]}
|
||||
cards={[card1, card2]}
|
||||
views={[view, view2]}
|
||||
selectedCardIds={[]}
|
||||
readonly={true}
|
||||
cardIdToFocusOnRender=''
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={2}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container, getByTitle} = render(component)
|
||||
expect(getByTitle('hidden-card-count')).toHaveTextContent('2')
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('components/table/Table extended', () => {
|
||||
@ -295,6 +360,7 @@ describe('components/table/Table extended', () => {
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
@ -379,6 +445,7 @@ describe('components/table/Table extended', () => {
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
@ -436,6 +503,7 @@ describe('components/table/Table extended', () => {
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
@ -526,6 +594,7 @@ describe('components/table/Table extended', () => {
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
@ -577,6 +646,7 @@ describe('components/table/Table extended', () => {
|
||||
showCard={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
onCardClicked={jest.fn()}
|
||||
hiddenCardsCount={0}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
@ -18,6 +18,8 @@ import BoardPermissionGate from '../permissions/boardPermissionGate'
|
||||
|
||||
import './table.scss'
|
||||
|
||||
import HiddenCardCount from '../../components/hiddenCardCount/hiddenCardCount'
|
||||
|
||||
import TableHeaders from './tableHeaders'
|
||||
import TableRows from './tableRows'
|
||||
import TableGroup from './tableGroup'
|
||||
@ -37,10 +39,11 @@ type Props = {
|
||||
showCard: (cardId?: string) => void
|
||||
addCard: (groupByOptionId?: string) => Promise<void>
|
||||
onCardClicked: (e: React.MouseEvent, card: Card) => void
|
||||
hiddenCardsCount: number
|
||||
}
|
||||
|
||||
const Table = (props: Props): JSX.Element => {
|
||||
const {board, cards, activeView, visibleGroups, groupByProperty, views} = props
|
||||
const {board, cards, activeView, visibleGroups, groupByProperty, views, hiddenCardsCount} = props
|
||||
const isManualSort = activeView.fields.sortOptions?.length === 0
|
||||
const canEditBoardProperties = useHasCurrentBoardPermissions([Permission.ManageBoardProperties])
|
||||
const canEditCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards])
|
||||
@ -251,6 +254,11 @@ const Table = (props: Props): JSX.Element => {
|
||||
/>
|
||||
</div>
|
||||
</ColumnResizeProvider>
|
||||
|
||||
{hiddenCardsCount > 0 &&
|
||||
<HiddenCardCount
|
||||
hiddenCardsCount={hiddenCardsCount}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ card3.id = 'card3'
|
||||
card3.title = 'card-3'
|
||||
card3.boardId = fakeBoard.id
|
||||
|
||||
const me: IUser = {id: 'user-id-1', username: 'username_1', email: '', props: {}, create_at: 0, update_at: 0, is_bot: false}
|
||||
const me: IUser = {id: 'user-id-1', username: 'username_1', email: '', props: {}, create_at: 0, update_at: 0, is_bot: false, roles: 'system_user'}
|
||||
|
||||
const categoryAttribute1 = TestBlockFactory.createCategoryBoards()
|
||||
categoryAttribute1.name = 'Category 1'
|
||||
@ -114,6 +114,14 @@ describe('src/components/workspace', () => {
|
||||
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
limits: {
|
||||
cards: 0,
|
||||
used_cards: 0,
|
||||
card_limit_timestamp: 0,
|
||||
views: 0,
|
||||
},
|
||||
},
|
||||
globalTemplates: {
|
||||
value: [],
|
||||
},
|
||||
@ -302,6 +310,7 @@ describe('src/components/workspace', () => {
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
},
|
||||
boardUsers: [me],
|
||||
blockSubscriptions: [],
|
||||
@ -316,6 +325,14 @@ describe('src/components/workspace', () => {
|
||||
[welcomeBoard.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
limits: {
|
||||
cards: 0,
|
||||
used_cards: 0,
|
||||
card_limit_timestamp: 0,
|
||||
views: 0,
|
||||
},
|
||||
},
|
||||
globalTemplates: {
|
||||
value: [],
|
||||
},
|
||||
@ -393,6 +410,7 @@ describe('src/components/workspace', () => {
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
},
|
||||
boardUsers: [me],
|
||||
blockSubscriptions: [],
|
||||
@ -407,6 +425,14 @@ describe('src/components/workspace', () => {
|
||||
[welcomeBoard.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
limits: {
|
||||
cards: 0,
|
||||
used_cards: 0,
|
||||
card_limit_timestamp: 0,
|
||||
views: 0,
|
||||
},
|
||||
},
|
||||
globalTemplates: {
|
||||
value: [],
|
||||
},
|
||||
@ -489,6 +515,7 @@ describe('src/components/workspace', () => {
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
},
|
||||
boardUsers: [me],
|
||||
blockSubscriptions: [],
|
||||
@ -503,6 +530,14 @@ describe('src/components/workspace', () => {
|
||||
[welcomeBoard.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
limits: {
|
||||
cards: 0,
|
||||
used_cards: 0,
|
||||
card_limit_timestamp: 0,
|
||||
views: 0,
|
||||
},
|
||||
},
|
||||
globalTemplates: {
|
||||
value: [],
|
||||
},
|
||||
|
@ -5,8 +5,8 @@ import {generatePath, useRouteMatch, useHistory} from 'react-router-dom'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {getCurrentTeam} from '../store/teams'
|
||||
import {getCurrentBoard, isLoadingBoard} from '../store/boards'
|
||||
import {getCurrentViewCardsSortedFilteredAndGrouped, setCurrent as setCurrentCard} from '../store/cards'
|
||||
import {getCurrentBoard, isLoadingBoard, getTemplates} from '../store/boards'
|
||||
import {refreshCards, getCardLimitTimestamp, getCurrentBoardHiddenCardsCount, setLimitTimestamp, getCurrentViewCardsSortedFilteredAndGrouped, setCurrent as setCurrentCard} from '../store/cards'
|
||||
import {
|
||||
getCurrentBoardViews,
|
||||
getCurrentViewGroupBy,
|
||||
@ -37,12 +37,15 @@ function CenterContent(props: Props) {
|
||||
const isLoading = useAppSelector(isLoadingBoard)
|
||||
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string}>()
|
||||
const board = useAppSelector(getCurrentBoard)
|
||||
const templates = useAppSelector(getTemplates)
|
||||
const cards = useAppSelector(getCurrentViewCardsSortedFilteredAndGrouped)
|
||||
const activeView = useAppSelector(getCurrentView)
|
||||
const views = useAppSelector(getCurrentBoardViews)
|
||||
const groupByProperty = useAppSelector(getCurrentViewGroupBy)
|
||||
const dateDisplayProperty = useAppSelector(getCurrentViewDisplayBy)
|
||||
const clientConfig = useAppSelector(getClientConfig)
|
||||
const hiddenCardsCount = useAppSelector(getCurrentBoardHiddenCardsCount)
|
||||
const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp)
|
||||
const history = useHistory()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@ -61,10 +64,19 @@ function CenterContent(props: Props) {
|
||||
dispatch(setClientConfig(config))
|
||||
}
|
||||
wsClient.addOnConfigChange(onConfigChangeHandler)
|
||||
|
||||
const onCardLimitTimestampChangeHandler = (_: WSClient, timestamp: number) => {
|
||||
dispatch(setLimitTimestamp({timestamp, templates}))
|
||||
if (cardLimitTimestamp > timestamp) {
|
||||
dispatch(refreshCards(timestamp))
|
||||
}
|
||||
}
|
||||
wsClient.addOnCardLimitTimestampChange(onCardLimitTimestampChangeHandler)
|
||||
|
||||
return () => {
|
||||
wsClient.removeOnConfigChange(onConfigChangeHandler)
|
||||
}
|
||||
}, [])
|
||||
}, [cardLimitTimestamp, match.params.boardId, templates])
|
||||
|
||||
if (board && activeView) {
|
||||
let property = groupByProperty
|
||||
@ -89,6 +101,7 @@ function CenterContent(props: Props) {
|
||||
groupByProperty={property}
|
||||
dateDisplayProperty={displayProperty}
|
||||
views={views}
|
||||
hiddenCardsCount={hiddenCardsCount}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import {Team} from './store/teams'
|
||||
import {Subscription} from './wsclient'
|
||||
import {PrepareOnboardingResponse} from './onboardingTour'
|
||||
import {Constants} from "./constants"
|
||||
import {BoardsCloudLimits} from './boardsCloudLimits'
|
||||
|
||||
//
|
||||
// OctoClient is the client interface to the server APIs
|
||||
@ -806,6 +807,19 @@ class OctoClient {
|
||||
|
||||
return (await this.getJson(response, {})) as PrepareOnboardingResponse
|
||||
}
|
||||
|
||||
// limits
|
||||
async getBoardsCloudLimits(): Promise<BoardsCloudLimits | undefined> {
|
||||
const path = '/api/v2/limits'
|
||||
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
|
||||
if (response.status !== 200) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const limits = (await this.getJson(response, {})) as BoardsCloudLimits
|
||||
Utils.log(`Cloud limits: cards=${limits.cards} views=${limits.views}`)
|
||||
return limits
|
||||
}
|
||||
}
|
||||
|
||||
const octoClient = new OctoClient()
|
||||
|
@ -1,18 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSlice, PayloadAction, createSelector} from '@reduxjs/toolkit'
|
||||
import {createSlice, PayloadAction, createSelector, createAsyncThunk} from '@reduxjs/toolkit'
|
||||
|
||||
import {Card} from '../blocks/card'
|
||||
import {IUser} from '../user'
|
||||
import {Board} from '../blocks/board'
|
||||
import {Block} from '../blocks/block'
|
||||
import {BoardView} from '../blocks/boardView'
|
||||
import {CommentBlock} from '../blocks/commentBlock'
|
||||
import {Utils} from '../utils'
|
||||
import {Constants} from '../constants'
|
||||
import {CardFilter} from '../cardFilter'
|
||||
import {default as client} from '../octoClient'
|
||||
|
||||
import {loadBoardData, initialReadOnlyLoad} from './initialLoad'
|
||||
import {loadBoardData, initialReadOnlyLoad, initialLoad} from './initialLoad'
|
||||
import {getCurrentBoard} from './boards'
|
||||
import {getBoardUsers} from './users'
|
||||
import {getLastCommentByCard} from './comments'
|
||||
@ -23,28 +25,76 @@ import {RootState} from './index'
|
||||
|
||||
type CardsState = {
|
||||
current: string
|
||||
limitTimestamp: number
|
||||
cards: {[key: string]: Card}
|
||||
templates: {[key: string]: Card}
|
||||
cardHiddenWarning: boolean
|
||||
}
|
||||
|
||||
export const refreshCards = createAsyncThunk<Block[], number, {state: RootState}>(
|
||||
'refreshCards',
|
||||
async (cardLimitTimestamp: number, thunkAPI) => {
|
||||
const {cards} = thunkAPI.getState().cards
|
||||
const blocksPromises = []
|
||||
|
||||
for (const card of Object.values(cards)) {
|
||||
if (card.limited && card.updateAt >= cardLimitTimestamp) {
|
||||
blocksPromises.push(client.getBlocksWithBlockID(card.id, card.boardId).then((blocks) => blocks.find((b) => b?.type === 'card')))
|
||||
}
|
||||
}
|
||||
const blocks = await Promise.all(blocksPromises)
|
||||
|
||||
return blocks.filter((b: Block|undefined): boolean => Boolean(b)) as Block[]
|
||||
},
|
||||
)
|
||||
|
||||
const limitCard = (isBoardTemplate: boolean, limitTimestamp:number, card: Card): Card => {
|
||||
if (isBoardTemplate) {
|
||||
return card
|
||||
}
|
||||
if (card.updateAt >= limitTimestamp) {
|
||||
return card
|
||||
}
|
||||
return {
|
||||
...card,
|
||||
fields: {
|
||||
icon: card.fields.icon,
|
||||
properties: {},
|
||||
contentOrder: [],
|
||||
},
|
||||
limited: true,
|
||||
}
|
||||
}
|
||||
|
||||
const cardsSlice = createSlice({
|
||||
name: 'cards',
|
||||
initialState: {
|
||||
current: '',
|
||||
limitTimestamp: 0,
|
||||
cards: {},
|
||||
templates: {},
|
||||
cardHiddenWarning: false,
|
||||
} as CardsState,
|
||||
reducers: {
|
||||
setCurrent: (state, action: PayloadAction<string>) => {
|
||||
state.current = action.payload
|
||||
},
|
||||
setLimitTimestamp: (state, action: PayloadAction<{timestamp: number, templates: {[key: string]: Board}}>) => {
|
||||
state.limitTimestamp = action.payload.timestamp
|
||||
for (const card of Object.values(state.cards)) {
|
||||
state.cards[card.id] = limitCard(Boolean(action.payload.templates[card.id]), state.limitTimestamp, card)
|
||||
}
|
||||
},
|
||||
addCard: (state, action: PayloadAction<Card>) => {
|
||||
state.cards[action.payload.id] = action.payload
|
||||
},
|
||||
addTemplate: (state, action: PayloadAction<Card>) => {
|
||||
showCardHiddenWarning: (state, action: PayloadAction<boolean>) => {
|
||||
state.cardHiddenWarning = action.payload
|
||||
},
|
||||
addTemplate: (state: CardsState, action: PayloadAction<Card>) => {
|
||||
state.templates[action.payload.id] = action.payload
|
||||
},
|
||||
updateCards: (state, action: PayloadAction<Card[]>) => {
|
||||
updateCards: (state: CardsState, action: PayloadAction<Card[]>) => {
|
||||
for (const card of action.payload) {
|
||||
if (card.deleteAt !== 0) {
|
||||
delete state.cards[card.id]
|
||||
@ -58,6 +108,11 @@ const cardsSlice = createSlice({
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(refreshCards.fulfilled, (state, action) => {
|
||||
for (const block of action.payload) {
|
||||
state.cards[block.id] = block as Card
|
||||
}
|
||||
})
|
||||
builder.addCase(initialReadOnlyLoad.fulfilled, (state, action) => {
|
||||
state.cards = {}
|
||||
state.templates = {}
|
||||
@ -69,6 +124,9 @@ const cardsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
})
|
||||
builder.addCase(initialLoad.fulfilled, (state, action) => {
|
||||
state.limitTimestamp = action.payload.limits?.card_limit_timestamp || 0
|
||||
})
|
||||
builder.addCase(loadBoardData.fulfilled, (state, action) => {
|
||||
state.cards = {}
|
||||
state.templates = {}
|
||||
@ -83,7 +141,7 @@ const cardsSlice = createSlice({
|
||||
},
|
||||
})
|
||||
|
||||
export const {updateCards, addCard, addTemplate, setCurrent} = cardsSlice.actions
|
||||
export const {updateCards, addCard, addTemplate, setCurrent, setLimitTimestamp, showCardHiddenWarning} = cardsSlice.actions
|
||||
export const {reducer} = cardsSlice
|
||||
|
||||
export const getCards = (state: RootState): {[key: string]: Card} => state.cards.cards
|
||||
@ -106,7 +164,7 @@ export const getSortedTemplates = createSelector(
|
||||
|
||||
export function getCard(cardId: string): (state: RootState) => Card|undefined {
|
||||
return (state: RootState): Card|undefined => {
|
||||
return state.cards.cards[cardId] || state.cards.templates[cardId]
|
||||
return getCards(state)[cardId] || getTemplates(state)[cardId]
|
||||
}
|
||||
}
|
||||
|
||||
@ -294,7 +352,7 @@ function searchFilterCards(cards: Card[], board: Board, searchTextRaw: string):
|
||||
})
|
||||
}
|
||||
|
||||
export const getCurrentViewCardsSortedFilteredAndGrouped = createSelector(
|
||||
export const getCurrentViewCardsSortedFilteredAndGroupedWithoutLimit = createSelector(
|
||||
getCurrentBoardCards,
|
||||
getLastCommentByCard,
|
||||
getCurrentBoard,
|
||||
@ -305,7 +363,7 @@ export const getCurrentViewCardsSortedFilteredAndGrouped = createSelector(
|
||||
if (!view || !board || !users || !cards) {
|
||||
return []
|
||||
}
|
||||
let result = cards
|
||||
let result = cards.filter((c) => !c.limited)
|
||||
if (view.fields.filter) {
|
||||
result = CardFilter.applyFilterGroup(view.fields.filter, board.cardProperties, result)
|
||||
}
|
||||
@ -318,8 +376,21 @@ export const getCurrentViewCardsSortedFilteredAndGrouped = createSelector(
|
||||
},
|
||||
)
|
||||
|
||||
export const getCurrentViewCardsSortedFilteredAndGrouped = createSelector(
|
||||
getCurrentViewCardsSortedFilteredAndGroupedWithoutLimit,
|
||||
(cards) => cards.filter((c) => !c.limited),
|
||||
)
|
||||
|
||||
export const getCurrentBoardHiddenCardsCount = createSelector(
|
||||
getCurrentBoardCards,
|
||||
(cards) => Object.values(cards).filter((c) => c.limited).length,
|
||||
)
|
||||
|
||||
export const getCurrentCard = createSelector(
|
||||
(state: RootState) => state.cards.current,
|
||||
(state: RootState) => state.cards.cards,
|
||||
getCards,
|
||||
(current, cards) => cards[current],
|
||||
)
|
||||
|
||||
export const getCardLimitTimestamp = (state: RootState): number => state.cards.limitTimestamp
|
||||
export const getCardHiddenWarning = (state: RootState): boolean => state.cards.cardHiddenWarning
|
||||
|
@ -18,6 +18,7 @@ import {reducer as searchTextReducer} from './searchText'
|
||||
import {reducer as globalErrorReducer} from './globalError'
|
||||
import {reducer as clientConfigReducer} from './clientConfig'
|
||||
import {reducer as sidebarReducer} from './sidebar'
|
||||
import {reducer as limitsReducer} from './limits'
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
@ -36,6 +37,7 @@ const store = configureStore({
|
||||
globalError: globalErrorReducer,
|
||||
clientConfig: clientConfigReducer,
|
||||
sidebar: sidebarReducer,
|
||||
limits: limitsReducer,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -12,13 +12,14 @@ import {RootState} from './index'
|
||||
export const initialLoad = createAsyncThunk(
|
||||
'initialLoad',
|
||||
async () => {
|
||||
const [me, team, teams, boards, boardsMemberships, boardTemplates] = await Promise.all([
|
||||
const [me, team, teams, boards, boardsMemberships, boardTemplates, limits] = await Promise.all([
|
||||
client.getMe(),
|
||||
client.getTeam(),
|
||||
client.getTeams(),
|
||||
client.getBoards(),
|
||||
client.getMyBoardMemberships(),
|
||||
client.getTeamTemplates(),
|
||||
client.getBoardsCloudLimits(),
|
||||
])
|
||||
|
||||
// if no me, normally user not logged in
|
||||
@ -36,6 +37,7 @@ export const initialLoad = createAsyncThunk(
|
||||
boards,
|
||||
boardsMemberships,
|
||||
boardTemplates,
|
||||
limits,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
49
webapp/src/store/limits.ts
Normal file
49
webapp/src/store/limits.ts
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSlice, PayloadAction} from '@reduxjs/toolkit'
|
||||
|
||||
import {BoardsCloudLimits} from '../boardsCloudLimits'
|
||||
|
||||
import {initialLoad} from './initialLoad'
|
||||
|
||||
import {RootState} from './index'
|
||||
|
||||
type LimitsState = {
|
||||
limits: BoardsCloudLimits,
|
||||
}
|
||||
|
||||
const defaultLimits = {
|
||||
cards: 0,
|
||||
used_cards: 0,
|
||||
card_limit_timestamp: 0,
|
||||
views: 0,
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
limits: defaultLimits,
|
||||
} as LimitsState
|
||||
|
||||
const limitsSlice = createSlice({
|
||||
name: 'limits',
|
||||
initialState,
|
||||
reducers: {
|
||||
setLimits: (state, action: PayloadAction<BoardsCloudLimits>) => {
|
||||
state.limits = action.payload
|
||||
},
|
||||
setCardLimitTimestamp: (state, action: PayloadAction<number>) => {
|
||||
state.limits.card_limit_timestamp = action.payload
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(initialLoad.fulfilled, (state, action) => {
|
||||
state.limits = action.payload.limits || defaultLimits
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const {reducer} = limitsSlice
|
||||
export const {setCardLimitTimestamp} = limitsSlice.actions
|
||||
|
||||
export const getLimits = (state: RootState): BoardsCloudLimits | undefined => state.limits.limits
|
||||
export const getCardLimitTimestamp = (state: RootState): number => state.limits.limits.card_limit_timestamp
|
@ -155,3 +155,31 @@ export const getCloudMessageCanceled = createSelector(
|
||||
return Boolean(me.props?.focalboard_cloudMessageCanceled)
|
||||
},
|
||||
)
|
||||
|
||||
export const getCardLimitSnoozeUntil = createSelector(
|
||||
getMe,
|
||||
(me): number => {
|
||||
if (!me) {
|
||||
return 0
|
||||
}
|
||||
try {
|
||||
return parseInt(me.props?.focalboard_cardLimitSnoozeUntil, 10) || 0
|
||||
} catch (_) {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const getCardHiddenWarningSnoozeUntil = createSelector(
|
||||
getMe,
|
||||
(me): number => {
|
||||
if (!me) {
|
||||
return 0
|
||||
}
|
||||
try {
|
||||
return parseInt(me.props?.focalboard_cardHiddenWarningSnoozeUntil, 10) || 0
|
||||
} catch (_) {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -44,6 +44,10 @@ export const TelemetryActions = {
|
||||
StartTour: 'welcomeScreen_startTour',
|
||||
SkipTour: 'welcomeScreen_skipTour',
|
||||
CloudMoreInfo: 'cloud_more_info',
|
||||
ViewLimitReached: 'limit_ViewLimitReached',
|
||||
ViewLimitCTAPerformed: 'limit_ViewLimitLinkOpen',
|
||||
LimitCardLimitReached: 'limit_cardLimitReached',
|
||||
LimitCardLimitLinkOpen: 'limit_cardLimitLinkOpen',
|
||||
}
|
||||
|
||||
interface IEventProps {
|
||||
|
@ -118,7 +118,6 @@ class TestBlockFactory {
|
||||
card.title = 'title'
|
||||
card.fields.icon = 'i'
|
||||
card.fields.properties.property1 = 'value1'
|
||||
|
||||
return card
|
||||
}
|
||||
|
||||
@ -192,6 +191,7 @@ class TestBlockFactory {
|
||||
create_at: Date.now(),
|
||||
update_at: Date.now(),
|
||||
is_bot: false,
|
||||
roles: 'system_user',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ interface IUser {
|
||||
create_at: number,
|
||||
update_at: number,
|
||||
is_bot: boolean,
|
||||
roles: string,
|
||||
}
|
||||
|
||||
interface UserWorkspace {
|
||||
|
140
webapp/src/widgets/__snapshots__/notification-box.test.tsx.snap
Normal file
140
webapp/src/widgets/__snapshots__/notification-box.test.tsx.snap
Normal file
@ -0,0 +1,140 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`widgets/NotificationBox should match snapshot with close with tooltip 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="NotificationBox"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p
|
||||
class="title"
|
||||
>
|
||||
title
|
||||
</p>
|
||||
CONTENT
|
||||
</div>
|
||||
<div
|
||||
class="octo-tooltip tooltip-top"
|
||||
data-tooltip="tooltip"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`widgets/NotificationBox should match snapshot with close without tooltip 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="NotificationBox"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p
|
||||
class="title"
|
||||
>
|
||||
title
|
||||
</p>
|
||||
CONTENT
|
||||
</div>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`widgets/NotificationBox should match snapshot with icon 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="NotificationBox"
|
||||
>
|
||||
<div
|
||||
class="NotificationBox__icon"
|
||||
>
|
||||
ICON
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p
|
||||
class="title"
|
||||
>
|
||||
title
|
||||
</p>
|
||||
CONTENT
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`widgets/NotificationBox should match snapshot with icon and close with tooltip 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="NotificationBox"
|
||||
>
|
||||
<div
|
||||
class="NotificationBox__icon"
|
||||
>
|
||||
ICON
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p
|
||||
class="title"
|
||||
>
|
||||
title
|
||||
</p>
|
||||
CONTENT
|
||||
</div>
|
||||
<div
|
||||
class="octo-tooltip tooltip-top"
|
||||
data-tooltip="tooltip"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`widgets/NotificationBox should match snapshot without icon and close 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="NotificationBox"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p
|
||||
class="title"
|
||||
>
|
||||
title
|
||||
</p>
|
||||
CONTENT
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: 1px solid var(--error-text-rgb);
|
||||
border: 1px solid rgb(var(--error-text-rgb));
|
||||
border-radius: var(--default-rad);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: 1px solid var(--error-text-rgb);
|
||||
border: 1px solid rgb(var(--error-text-rgb));
|
||||
border-radius: var(--default-rad);
|
||||
}
|
||||
}
|
||||
|
15
webapp/src/widgets/icons/alert.tsx
Normal file
15
webapp/src/widgets/icons/alert.tsx
Normal 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 AlertIcon(): JSX.Element {
|
||||
return (
|
||||
<CompassIcon
|
||||
icon='alert-outline'
|
||||
className='AlertIcon'
|
||||
/>
|
||||
)
|
||||
}
|
39
webapp/src/widgets/notification-box.scss
Normal file
39
webapp/src/widgets/notification-box.scss
Normal file
@ -0,0 +1,39 @@
|
||||
.NotificationBox {
|
||||
position: fixed;
|
||||
bottom: 52px;
|
||||
right: 32px;
|
||||
border-radius: 4px;
|
||||
background: 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;
|
||||
display: flex;
|
||||
padding: 22px;
|
||||
width: 400px;
|
||||
z-index: 1000;
|
||||
|
||||
.NotificationBox__icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0;
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.octo-tooltip {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
.IconButton {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
85
webapp/src/widgets/notification-box.test.tsx
Normal file
85
webapp/src/widgets/notification-box.test.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
// 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 '@testing-library/jest-dom'
|
||||
|
||||
import {wrapIntl} from '../testUtils'
|
||||
|
||||
import NotificationBox from './notification-box'
|
||||
|
||||
describe('widgets/NotificationBox', () => {
|
||||
beforeEach(() => {
|
||||
// Quick fix to disregard console error when unmounting a component
|
||||
console.error = jest.fn()
|
||||
document.execCommand = jest.fn()
|
||||
})
|
||||
|
||||
test('should match snapshot without icon and close', () => {
|
||||
const component = wrapIntl(
|
||||
<NotificationBox
|
||||
title='title'
|
||||
>
|
||||
{'CONTENT'}
|
||||
</NotificationBox>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot with icon', () => {
|
||||
const component = wrapIntl(
|
||||
<NotificationBox
|
||||
title='title'
|
||||
icon='ICON'
|
||||
>
|
||||
{'CONTENT'}
|
||||
</NotificationBox>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot with close without tooltip', () => {
|
||||
const component = wrapIntl(
|
||||
<NotificationBox
|
||||
title='title'
|
||||
onClose={() => null}
|
||||
>
|
||||
{'CONTENT'}
|
||||
</NotificationBox>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot with close with tooltip', () => {
|
||||
const component = wrapIntl(
|
||||
<NotificationBox
|
||||
title='title'
|
||||
onClose={() => null}
|
||||
closeTooltip='tooltip'
|
||||
>
|
||||
{'CONTENT'}
|
||||
</NotificationBox>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot with icon and close with tooltip', () => {
|
||||
const component = wrapIntl(
|
||||
<NotificationBox
|
||||
title='title'
|
||||
icon='ICON'
|
||||
onClose={() => null}
|
||||
closeTooltip='tooltip'
|
||||
>
|
||||
{'CONTENT'}
|
||||
</NotificationBox>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
57
webapp/src/widgets/notification-box.tsx
Normal file
57
webapp/src/widgets/notification-box.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import IconButton from './buttons/iconButton'
|
||||
import CloseIcon from './icons/close'
|
||||
import Tooltip from './tooltip'
|
||||
|
||||
import './notification-box.scss'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
icon?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
onClose?: () => void
|
||||
closeTooltip?: string
|
||||
}
|
||||
|
||||
function renderClose(onClose?: () => void, closeTooltip?: string) {
|
||||
if (!onClose) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (closeTooltip) {
|
||||
return (
|
||||
<Tooltip title={closeTooltip}>
|
||||
<IconButton
|
||||
icon={<CloseIcon/>}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Tooltip>)
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={<CloseIcon/>}
|
||||
onClick={onClose}
|
||||
/>)
|
||||
}
|
||||
|
||||
function NotificationBox(props: Props): JSX.Element {
|
||||
return (
|
||||
<div className='NotificationBox'>
|
||||
{props.icon &&
|
||||
<div className='NotificationBox__icon'>
|
||||
{props.icon}
|
||||
</div>}
|
||||
<div className='content'>
|
||||
<p className='title'>{props.title}</p>
|
||||
{props.children}
|
||||
</div>
|
||||
{renderClose(props.onClose, props.closeTooltip)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(NotificationBox)
|
Loading…
Reference in New Issue
Block a user