diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index e12a7396b..a4d7e3e82 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -68,7 +68,7 @@ jobs: path: ${{ github.workspace }}/linux/dist/focalboard-linux.tar.gz macos: - runs-on: macos-10.15 + runs-on: macos-11 steps: @@ -101,7 +101,7 @@ jobs: - name: Build macOS run: make mac-app env: - DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_13.2.1.app/Contents/Developer BUILD_NUMBER: ${{ github.run_id }} - name: Upload macOS package diff --git a/.github/workflows/prod-release.yml b/.github/workflows/prod-release.yml index 293b7a8e0..1d9facabe 100644 --- a/.github/workflows/prod-release.yml +++ b/.github/workflows/prod-release.yml @@ -63,7 +63,7 @@ jobs: path: ${{ github.workspace }}/linux/dist/focalboard-linux.tar.gz macos: - runs-on: macos-10.15 + runs-on: macos-11 steps: @@ -96,7 +96,7 @@ jobs: - name: Build macOS run: make mac-app env: - DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_13.2.1.app/Contents/Developer BUILD_NUMBER: ${{ github.run_id }} - name: Upload macOS package diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c90151b2..8df8e6072 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,33 +1,49 @@ # Code Contribution Guidelines -Thank you for your interest in contributing! Please see the [Focalboard Contribution Guide](https://www.focalboard.com/contribute/getting-started/) which describes the process for making code contributions, and [join our Focalboard community channel](https://community.mattermost.com/core/channels/focalboard) to ask questions from community members and the core team. +Thank you for your interest in contributing! Please see the [Focalboard Contribution Guide](https://mattermost.github.io/focalboard/) which describes the process for making code contributions, and [join our Focalboard community channel](https://community.mattermost.com/core/channels/focalboard) to ask questions from community members and the core team. -When you submit a pull request, it goes through a [code review process outlined here](https://www.focalboard.com/contribute/getting-started/code-review/). +When you submit a pull request, it goes through a [code review process outlined here](https://mattermost.github.io/focalboard/code-review). # Updating Changelog -After a noteable bug fix or an improvement you've submitted is merged, please consider making a pull request to the [CHANGELOG.md](https://github.com/mattermost/focalboard/blob/main/CHANGELOG.md) under the next release section. +After a noteable bug fix or an improvement you've submitted is merged, please consider making a pull request to the [CHANGELOG.md](https://github.com/mattermost/focalboard/blob/main/CHANGELOG.md) under the next release section. # Bug reports Please file a [GitHub issue](https://github.com/mattermost/focalboard/issues) if anything isn't working the way you expect. -# Documentation +# Documentation You can contribute to our documentation in the [Mattermost Boards documentation](https://docs.mattermost.com/guides/boards.html). Read more about how the contribution process works in the repo [README](https://github.com/mattermost/docs/blob/master/README.md). Visit the [Documentation Working Group channel](https://community.mattermost.com/core/channels/dwg-documentation-working-group) on our Community server if you have any questions! # Contributors -**Quality Assurance Contributors**: Correctly files verified bug reports. +**Core Committers**: A core committer is a maintainer on the Focalboard project who has merge access to the repositories. They are responsible for reviewing pull requests, cultivating the developer community, and guiding the technical vision of Focalboard. If you have a question or need some help, these are the people to ask. -- [chenilim](https://github.com/chenilim) +- **Scott Bishel** + - @scott.bishel on [community.mattermost.com](https://community.mattermost.com/core/messages/@scott.bishel) and [@sbishel](https://github.com/sbishel) on GitHub +- **Jesús Espino** + - @jesus.espino on [community.mattermost.com](https://community.mattermost.com/core/messages/@jesus.espino) and [@jespino](https://github.com/jespino) on GitHub +- **Doug Lauder** + - @doug.lauder on [community.mattermost.com](https://community.mattermost.com/core/messages/@doug.lauder) and [@wiggin77](https://github.com/wiggin77) on GitHub +- **Miguel de la Cruz** + - @miguel.delacruz on [community.mattermost.com](https://community.mattermost.com/core/messages/@miguel.delacruz) and [@mgdelacroix](https://github.com/mgdelacroix) on GitHub +- **Harshil Sharma** + - @harshil.sharma on [community.mattermost.com](https://community.mattermost.com/core/messages/@harshil.sharma) and [@harshilsharma63](https://github.com/harshilsharma63) on GitHub +- **Chen Lim** + - @chen-i.lim on [community.mattermost.com](https://community.mattermost.com/core/messages/@chen-i.lim) and [@chenilim](https://github.com/chenilim) on GitHub + +**Quality Assurance**: Checks quality of code and verifies bug fixes. + +- **Ogi Marušić** + - @ogi.marusic on [community.mattermost.com](https://community.mattermost.com/core/messages/@ogi.marusic) and [@ogi-m](https://github.com/ogi-m) on GitHub **Community Organizers**: Responds with comments to Bug Reports, Issues, and Pull Requests with tags, edits and mentions to Core Committers and contributors. -- [it33](https://github.com/it33) +- **Winson Wu** + - @winson.wu on [community.mattermost.com](https://community.mattermost.com/core/messages/@winson.wu) and [@wuwinson](https://github.com/wuwinson) on GitHub -**Core Committers**: Updates project. Approves and merges pull requests from contributors. +**Documentation**: Verifies documentation changes, and updates documentation for new features. -- [chenilim](https://github.com/chenilim) -- [jespino](https://github.com/jespino) -- [coreyhulen](https://github.com/coreyhulen) +- **Justine Geffen** + - @justine.geffen on [community.mattermost.com](https://community.mattermost.com/core/messages/@justine.geffen) and [@justinegeffen ](https://github.com/justinegeffen) on GitHub diff --git a/README.md b/README.md index 1efd5928b..ca2800b8b 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Download the latest server release from [GitHub releases](https://github.com/mat ## Building the server -Most development can be done on the Personal Server edition. Please refer to the [Developer's Tips & Tricks](https://www.focalboard.com/contribute/getting-started/dev-tips/) for more detailed steps. Here's a summary: +Most development can be done on the Personal Server edition. Please refer to the [Developer's Tips & Tricks](https://mattermost.github.io/focalboard/dev-tips) for more detailed steps. Here's a summary: First, install basic dependencies: * Go 1.15+ @@ -72,7 +72,7 @@ You can build standalone apps that package the server to run locally against SQL * Mac: * `make mac-app` * run `mac/dist/Focalboard.app` - * *Requires: macOS Catalina (10.15)+, Xcode 12+.* + * *Requires: macOS 11.3+, Xcode 13.2.1+* * Linux: * Install webgtk dependencies * `sudo apt-get install libgtk-3-dev` diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..74ecfc801 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,3 @@ +title: Focalboard Developers +google_analytics: UA-64458817-2 +theme: jekyll-theme-architect \ No newline at end of file diff --git a/website/site/content/contribute/getting-started/code-review.md b/docs/code-review.md similarity index 79% rename from website/site/content/contribute/getting-started/code-review.md rename to docs/code-review.md index 734d5696b..0931d11a4 100644 --- a/website/site/content/contribute/getting-started/code-review.md +++ b/docs/code-review.md @@ -1,19 +1,10 @@ ---- -title: "Code Review" -date: 2021-02-03T08:00:00-00:00 -weight: 5 -subsection: Getting Started ---- +# Code Review Checklist -Currently, all changes to the product must be reviewed by a [core committer](/contribute/getting-started/core-committers/#core-committers). +Currently, all changes to the product must be reviewed by a [core committer](core-committers.md). - +## If you are a community member seeking a review -If you are a community member seeking a review ----------------------------------------------- - -1. Submit your pull request. +1. Submit your pull request (PR). * Follow the [contribution checklist](../contribution-checklist/). 2. Wait for a reviewer to be assigned. * Product managers are on the lookout for new pull requests and usually handle this for you automatically. @@ -23,16 +14,15 @@ If you are a community member seeking a review 3. [Wait for a review](#if-you-are-awaiting-a-review). * Expect some interaction with at least one reviewer within 5 business days (weekdays, Monday through Friday, excluding [statutory holidays](https://docs.mattermost.com/process/working-at-mattermost.html#holidays)). * Keep in mind that core committers are geographically distributed around the world and likely in a different time zone than your own. - * If no interaction has occurred after 5 business days, please at-mention a reviewer with a comment on your PR. + * If no interaction has occurred after 5 business days, please [at-mention](https://github.blog/2011-03-23-mention-somebody-they-re-notified/) a reviewer with a comment on your pull request. 4. Make any necessary changes. * If a reviewer requests changes, your pull request will disappear from their queue of reviews. - * Once you've addressed the concerns, please at-mention the reviewer with a comment on your PR. + * Once you've addressed the concerns, please at-mention the reviewer with a comment on your pull request. 5. Wait for your code to be merged. * Larger pull requests may require more time to review. * Once all reviewers have approved your changes, they will handle merging your code. -If you are awaiting a review ----------------------------- +## If you are awaiting a review 1. Wait patiently for reviews to complete. * Expect some interaction with each of your reviewers within 5 business days. @@ -41,8 +31,7 @@ If you are awaiting a review * If a reviewer requests changes, your pull request will disappear from their queue of reviews. * Once you've addressed the concerns, assign them as a reviewer again to put your pull request back in their queue. -If you are a core committer asked to give a review --------------------------------------------------- +## If you are a core committer asked to give a review 1. Respond promptly to requested reviews. * Assume the requested review is urgent and blocking unless explicitly stated otherwise. diff --git a/website/site/content/contribute/getting-started/contribution-checklist.md b/docs/contribution-checklist.md similarity index 57% rename from website/site/content/contribute/getting-started/contribution-checklist.md rename to docs/contribution-checklist.md index fcf981a00..ac1d60dac 100644 --- a/website/site/content/contribute/getting-started/contribution-checklist.md +++ b/docs/contribution-checklist.md @@ -1,27 +1,16 @@ ---- -title: "Contribution Checklist" -date: 2021-02-03T08:00:00-00:00 -weight: 2 -subsection: Getting Started ---- +# Contribution Checklist Thanks for your interest in contributing code! - - - - Follow this checklist for submitting a pull request (PR): 1. You've signed the [Contributor License Agreement](http://www.mattermost.org/mattermost-contributor-agreement/), so you can be added to the Mattermost [Approved Contributor List](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true). 2. Your ticket is a Help Wanted GitHub issue for the project you're contributing to. - - If not, follow the process [here](/contribute/getting-started/contributions-without-ticket). + - If not, follow the process [here](contributions-without-ticket.md). 3. Your code is thoroughly tested, including appropriate unit tests, and manual testing. 4. If applicable, user interface strings are included in the localization file ([en.json](https://github.com/mattermost/focalboard/blob/main/webapp/i18n/en.json)) - In the webapp folder, run `npm run i18n-extract` to generate the new/updated strings. 5. The PR is submitted against the `main` branch from your fork. 6. The PR title begins with the GitHub Ticket ID (e.g. `[GH-394]`) and the summary template is filled out. -Once submitted, the automated build process must pass in order for the PR to be accepted. Any errors or failures need to be addressed in order for the PR to be accepted. Next, the PR goes through [code review](../code-review/). To learn about the review process for each project, read the `CONTRIBUTING.md` file of that GitHub repository. - - +Once submitted, the automated build process must pass in order for the PR to be accepted. Any errors or failures need to be addressed in order for the PR to be accepted. Next, the PR goes through [code review](code-review.md). To learn about the review process for each project, read the [CONTRIBUTING.md](https://github.com/mattermost/focalboard/blob/main/CONTRIBUTING.md) file of that GitHub repository. diff --git a/website/site/content/contribute/getting-started/contributions-without-ticket.md b/docs/contributions-without-ticket.md similarity index 93% rename from website/site/content/contribute/getting-started/contributions-without-ticket.md rename to docs/contributions-without-ticket.md index 9ee9e06f1..e61282b5c 100644 --- a/website/site/content/contribute/getting-started/contributions-without-ticket.md +++ b/docs/contributions-without-ticket.md @@ -1,9 +1,4 @@ ---- -title: "Contributions Without Ticket" -date: 2021-02-03T08:00:00-00:00 -weight: 3 -subsection: Getting Started ---- +# Contributions Without Ticket Contributions for minor corrections and improvements without a corresponding `Help Wanted` ticket are welcome. For example, a pull request for a bug or incremental improvement, with less than 20 lines of code change, is usually accepted if the change to existing behaviour is minor. diff --git a/docs/core-committers.md b/docs/core-committers.md new file mode 100644 index 000000000..df5f8dea7 --- /dev/null +++ b/docs/core-committers.md @@ -0,0 +1,20 @@ +# Core Committers + +A core committer is a maintainer on the Focalboard project who has merge access to the repositories. They are responsible for reviewing pull requests, cultivating the developer community, and guiding the technical vision of Focalboard. If you have a question or need some help, these are the people to ask. + +## Core Committers + +Below is the list of core committers working on Focalboard: + +- **Scott Bishel** + - @scott.bishel on [community.mattermost.com](https://community.mattermost.com/core/messages/@scott.bishel) and [@sbishel](https://github.com/sbishel) on GitHub +- **Jesús Espino** + - @jesus.espino on [community.mattermost.com](https://community.mattermost.com/core/messages/@jesus.espino) and [@jespino](https://github.com/jespino) on GitHub +- **Doug Lauder** + - @doug.lauder on [community.mattermost.com](https://community.mattermost.com/core/messages/@doug.lauder) and [@wiggin77](https://github.com/wiggin77) on GitHub +- **Miguel de la Cruz** + - @miguel.delacruz on [community.mattermost.com](https://community.mattermost.com/core/messages/@miguel.delacruz) and [@mgdelacroix](https://github.com/mgdelacroix) on GitHub +- **Harshil Sharma** + - @harshil.sharma on [community.mattermost.com](https://community.mattermost.com/core/messages/@harshil.sharma) and [@harshilsharma63](https://github.com/harshilsharma63) on GitHub +- **Chen Lim** + - @chen-i.lim on [community.mattermost.com](https://community.mattermost.com/core/messages/@chen-i.lim) and [@chenilim](https://github.com/chenilim) on GitHub diff --git a/website/site/content/contribute/getting-started/dev-tips.md b/docs/dev-tips.md similarity index 88% rename from website/site/content/contribute/getting-started/dev-tips.md rename to docs/dev-tips.md index cd4038f52..d32d79a6f 100644 --- a/website/site/content/contribute/getting-started/dev-tips.md +++ b/docs/dev-tips.md @@ -1,11 +1,6 @@ ---- -title: "Developer Tips & Tricks" -date: 2021-02-03T00:08:00-00:00 -weight: 1 -subsection: Getting Started ---- +# Developer Tips and Tricks -## Install pre-reqs +## Installation prerequisites Check that you have recent versions of the basic dependencies installed: * [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) @@ -16,13 +11,14 @@ Check that you have recent versions of the basic dependencies installed: On Windows: * Install [Mingw64](https://chocolatey.org/packages/mingw) via [Chocolatey](https://chocolatey.org/) -On Mac, to build the Mac app: +On macOS, to build the Mac app: * Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) (v12+) * Install the Xcode commandline tools, via the IDE or run `xcode-select --install` On Linux, to build the Linux app: * `sudo apt-get install libgtk-3-dev` * `sudo apt-get install libwebkit2gtk-4.0-dev` +* `sudo apt-get install autoconf dh-autoreconf` ## Clone the project source code @@ -42,7 +38,7 @@ Then open a browser to `http://localhost:8000` to access it. The port is configu Once the server is running, you can rebuild just the webapp with `make webapp` (in a separate terminal window), then reload the browser. -## VSCode Setup +## VSCode setup Here's a recommended dev-test loop using VSCode: * Open a bash terminal window to the project folder @@ -73,24 +69,22 @@ Debug the Go code in VSCode. This is set up automatically when you launch the se To start, add a breakpoint to `handleGetBlocks()` in `server/api/api.go`, then refresh the browser to see how data is retrieved. -## Localization / Internationalization / Translation +## Localization/Internationalization/Translation -We use `i18n` to localize the web app. Localized string generally use `intl.formatMessage`. - -When adding or modifying localized strings, run `npm run i18n-extract` in `webapp` to rebuild `webapp/i18n/en.json`. +We use `i18n` to localize the web app. Localized string generally use `intl.formatMessage`. When adding or modifying localized strings, run `npm run i18n-extract` in `webapp` to rebuild `webapp/i18n/en.json`. Translated strings are stored in other json files under `webapp/i18n`, e.g. `es.json` for Spanish. ## Database -By default, data is stored in a sqlite datebase `focalboard.db`. You can view and edit this directly using `sqlite3 focalboard.db` from bash. +By default, data is stored in a sqlite database `focalboard.db`. You can view and edit this directly using `sqlite3 focalboard.db` from bash. ## Unit tests Before checking-in commits, run: `make ci`, which is simlar to the ci.yml workflow and includes: * Server unit tests: `make server-test` * Webapp eslint: `cd webapp; npm run check` -* Webapp unit tests: `cd webapp; npm run test` +* Webapp unit tests: `make webapp-test` * Webapp UI tests: `cd webapp; npm run cypress:ci` ## Running into problems or have questions? diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..f83c9e28d --- /dev/null +++ b/docs/index.md @@ -0,0 +1,19 @@ +# Focalboard / Mattermost Boards Contributors Guide + +Welcome to the [Focalboard](https://www.focalboard.com) / [Mattermost Boards](https://mattermost.com/boards/?utm_source=focalboard) project! + +We're very glad you want to check it out and perhaps contribute code our repository in GitHub. + +Our goal is to make your experience as great as possible. Follow these simple steps to contribute: + +1. [Clone the project from GitHub](https://github.com/mattermost/focalboard) and follow the steps in the README to build. Read the [developer tips & tricks](dev-tips.md) to get started. + +2. Find [help wanted tickets in GitHub](https://github.com/mattermost/focalboard/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22). Comment to let everyone know you’re working on it, and to allow a core contributor to assign the issue to you. If there’s no ticket for what you want to work on see [contributions without a ticket.](contributions-without-ticket.md). + +3. When your changes are ready, run through our [checklist for pull requests](contribution-checklist.md). Note that if it’s your first contribution, there is a standard [CLA](https://www.mattermost.org/mattermost-contributor-agreement/) to sign. + +## Just ask if you need help! + +You can find us on our [public Focalboard channel](https://community.mattermost.com/core/channels/focalboard) on the Mattermost community server. Also feel free to [file a bug](https://github.com/mattermost/focalboard/issues/new/choose) for any issues you run into, or [start a discussion](https://github.com/mattermost/focalboard/discussions). + +We're glad ❤️ you're here! Good luck and have fun! diff --git a/import/README.md b/import/README.md index 6905b42ab..a9f15658c 100644 --- a/import/README.md +++ b/import/README.md @@ -8,4 +8,4 @@ This subfolder contains scripts to import data from other systems. It is at an e * Todoist * Nextcloud Deck -[Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this. +[Contribute code](https://mattermost.github.io/focalboard/) to expand this. diff --git a/import/asana/README.md b/import/asana/README.md index 86dc8a032..055f93d03 100644 --- a/import/asana/README.md +++ b/import/asana/README.md @@ -10,4 +10,4 @@ This node app converts an Asana json archive into a Focalboard archive. To use: ## Import scope -Currently, the script imports all cards from a single board, including their section (column) membership, names, and notes. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this. +Currently, the script imports all cards from a single board, including their section (column) membership, names, and notes. [Contribute code](https://mattermost.github.io/focalboard/) to expand this. diff --git a/import/asana/importAsana.ts b/import/asana/importAsana.ts index 58ca3cca9..806c1f5f9 100644 --- a/import/asana/importAsana.ts +++ b/import/asana/importAsana.ts @@ -3,7 +3,7 @@ import * as fs from 'fs' import minimist from 'minimist' import {exit} from 'process' -import {ArchiveUtils} from '../../webapp/src/blocks/archive' +import {ArchiveUtils} from '../util/archive' import {Block} from '../../webapp/src/blocks/block' import {IPropertyOption, IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board' import {createBoardView} from '../../webapp/src/blocks/boardView' diff --git a/import/jira/README.md b/import/jira/README.md index 6eb913a45..c50dc3cd8 100644 --- a/import/jira/README.md +++ b/import/jira/README.md @@ -20,4 +20,4 @@ The following aren't currently imported: * Comments * Embedded files -[Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this. +[Contribute code](https://mattermost.github.io/focalboard/) to expand this. diff --git a/import/jira/jiraImporter.ts b/import/jira/jiraImporter.ts index e35d33cff..afca95171 100644 --- a/import/jira/jiraImporter.ts +++ b/import/jira/jiraImporter.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import * as fs from 'fs' import {exit} from 'process' -import {ArchiveUtils} from '../../webapp/src/blocks/archive' +import {ArchiveUtils} from '../util/archive' import {Block} from '../../webapp/src/blocks/block' import {IPropertyOption, IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board' import {createBoardView} from '../../webapp/src/blocks/boardView' diff --git a/import/nextcloud-deck/README.md b/import/nextcloud-deck/README.md index fa3442c32..283486a1a 100644 --- a/import/nextcloud-deck/README.md +++ b/import/nextcloud-deck/README.md @@ -11,6 +11,6 @@ This node app converts data from a Nextcloud Server with the [app Deck](https:// ## Import scope -Currently, the script imports all cards from a single board, including their stacks (column) membership, labels, names, descriptions, duedate and comments. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this. +Currently, the script imports all cards from a single board, including their stacks (column) membership, labels, names, descriptions, duedate and comments. [Contribute code](https://mattermost.github.io/focalboard/) to expand this. diff --git a/import/notion/README.md b/import/notion/README.md index 985bbe3e7..05148568e 100644 --- a/import/notion/README.md +++ b/import/notion/README.md @@ -15,4 +15,4 @@ Currently, the script imports all cards from a single board, including their pro The Notion export format does not preserve property types, so the script currently imports all card properties as a Select type. You can change the type after importing into Focalboard. -[Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this. +[Contribute code](https://mattermost.github.io/focalboard/) to expand this. diff --git a/import/notion/importNotion.ts b/import/notion/importNotion.ts index 4f48acbbf..10a71e887 100644 --- a/import/notion/importNotion.ts +++ b/import/notion/importNotion.ts @@ -3,7 +3,7 @@ import * as fs from 'fs' import minimist from 'minimist' import path from 'path' import {exit} from 'process' -import {ArchiveUtils} from '../../webapp/src/blocks/archive' +import {ArchiveUtils} from '../util/archive' import {Block} from '../../webapp/src/blocks/block' import {IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board' import {createBoardView} from '../../webapp/src/blocks/boardView' diff --git a/import/todoist/importTodoist.ts b/import/todoist/importTodoist.ts index bbbb338ce..a881b59bb 100644 --- a/import/todoist/importTodoist.ts +++ b/import/todoist/importTodoist.ts @@ -3,7 +3,7 @@ import * as fs from 'fs' import minimist from 'minimist' import {exit} from 'process' -import {ArchiveUtils} from '../../webapp/src/blocks/archive' +import {ArchiveUtils} from '../util/archive' import {Block} from '../../webapp/src/blocks/block' import {IPropertyOption, IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board' import {createBoardView} from '../../webapp/src/blocks/boardView' diff --git a/import/trello/README.md b/import/trello/README.md index 14035274e..003604e12 100644 --- a/import/trello/README.md +++ b/import/trello/README.md @@ -11,6 +11,6 @@ This node app converts a Trello json archive into a Focalboard archive. To use: ## Import scope -Currently, the script imports all cards from a single board, including their list (column) membership, names, and descriptions. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this. +Currently, the script imports all cards from a single board, including their list (column) membership, names, and descriptions. [Contribute code](https://mattermost.github.io/focalboard/) to expand this. diff --git a/import/trello/importTrello.ts b/import/trello/importTrello.ts index 7157d0f9c..afa95cba3 100644 --- a/import/trello/importTrello.ts +++ b/import/trello/importTrello.ts @@ -3,7 +3,7 @@ import * as fs from 'fs' import minimist from 'minimist' import {exit} from 'process' -import {ArchiveUtils} from '../../webapp/src/blocks/archive' +import {ArchiveUtils} from '../util/archive' import {Block} from '../../webapp/src/blocks/block' import {IPropertyOption, IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board' import {createBoardView} from '../../webapp/src/blocks/boardView' diff --git a/import/util/archive.ts b/import/util/archive.ts new file mode 100644 index 000000000..e4502d3f4 --- /dev/null +++ b/import/util/archive.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Block} from '../../webapp/src/blocks/block' + +interface ArchiveHeader { + version: number + date: number +} + +interface ArchiveLine { + type: string, + data: unknown, +} + +// This schema allows the expansion of additional line types in the future +interface BlockArchiveLine extends ArchiveLine { + type: 'block', + data: Block +} + +class ArchiveUtils { + static buildBlockArchive(blocks: readonly Block[]): string { + const header: ArchiveHeader = { + version: 1, + date: Date.now(), + } + + const headerString = JSON.stringify(header) + let content = headerString + '\n' + for (const block of blocks) { + const line: BlockArchiveLine = { + type: 'block', + data: block, + } + const lineString = JSON.stringify(line) + content += lineString + content += '\n' + } + + return content + } + + static parseBlockArchive(contents: string): Block[] { + const blocks: Block[] = [] + const allLineStrings = contents.split('\n') + if (allLineStrings.length >= 2) { + const headerString = allLineStrings[0] + const header = JSON.parse(headerString) as ArchiveHeader + if (header.date && header.version >= 1) { + const lineStrings = allLineStrings.slice(1) + let lineNum = 2 + for (const lineString of lineStrings) { + if (!lineString) { + // Ignore empty lines, e.g. last line + continue + } + const line = JSON.parse(lineString) as ArchiveLine + if (!line || !line.type || !line.data) { + throw new Error(`ERROR parsing line ${lineNum}`) + } + switch (line.type) { + case 'block': { + const blockLine = line as BlockArchiveLine + const block = blockLine.data + blocks.push(block) + break + } + } + + lineNum += 1 + } + } else { + throw new Error('ERROR parsing header') + } + } + + return blocks + } +} + +export {ArchiveHeader, ArchiveLine, BlockArchiveLine, ArchiveUtils} \ No newline at end of file diff --git a/mac/Focalboard.xcodeproj/project.pbxproj b/mac/Focalboard.xcodeproj/project.pbxproj index 4adf28c4e..8ad05f0a9 100644 --- a/mac/Focalboard.xcodeproj/project.pbxproj +++ b/mac/Focalboard.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 8014951C261598D600A51700 /* PortUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8014951B261598D600A51700 /* PortUtils.swift */; }; 804E57FC27441B6B008526F0 /* whatsnew.txt in Resources */ = {isa = PBXBuildFile; fileRef = 804E57FB27441B6B008526F0 /* whatsnew.txt */; }; + 80672A8B27BAFEBA00257B8C /* DownloadHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80672A8A27BAFEBA00257B8C /* DownloadHandler.swift */; }; 80D6DEBB252E13CB00AEED9E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D6DEBA252E13CB00AEED9E /* AppDelegate.swift */; }; 80D6DEBD252E13CB00AEED9E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D6DEBC252E13CB00AEED9E /* ViewController.swift */; }; 80D6DEBF252E13CD00AEED9E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 80D6DEBE252E13CD00AEED9E /* Assets.xcassets */; }; @@ -42,6 +43,7 @@ /* Begin PBXFileReference section */ 8014951B261598D600A51700 /* PortUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortUtils.swift; sourceTree = ""; }; 804E57FB27441B6B008526F0 /* whatsnew.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = whatsnew.txt; sourceTree = ""; }; + 80672A8A27BAFEBA00257B8C /* DownloadHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadHandler.swift; sourceTree = ""; }; 80D6DEB7252E13CB00AEED9E /* Focalboard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Focalboard.app; sourceTree = BUILT_PRODUCTS_DIR; }; 80D6DEBA252E13CB00AEED9E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 80D6DEBC252E13CB00AEED9E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -116,6 +118,7 @@ 80F8BF572624EB0C00FF3943 /* Globals.swift */, 8014951B261598D600A51700 /* PortUtils.swift */, 80D6DEBC252E13CB00AEED9E /* ViewController.swift */, + 80672A8A27BAFEBA00257B8C /* DownloadHandler.swift */, 80F174B62788C1A2000A9EEA /* CustomWKWebView.swift */, 80F8BF4F2624E1BB00FF3943 /* WhatsNewViewController.swift */, 804E57FB27441B6B008526F0 /* whatsnew.txt */, @@ -303,6 +306,7 @@ 80F8BF502624E1BB00FF3943 /* WhatsNewViewController.swift in Sources */, 80F174B72788C1A2000A9EEA /* CustomWKWebView.swift in Sources */, 80F8BF582624EB0C00FF3943 /* Globals.swift in Sources */, + 80672A8B27BAFEBA00257B8C /* DownloadHandler.swift in Sources */, 80D6DF18252F9BDE00AEED9E /* AutoSaveWindowController.swift in Sources */, 80D6DEBD252E13CB00AEED9E /* ViewController.swift in Sources */, 80D6DEBB252E13CB00AEED9E /* AppDelegate.swift in Sources */, @@ -403,7 +407,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 11.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -458,7 +462,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 11.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/mac/Focalboard/DownloadHandler.swift b/mac/Focalboard/DownloadHandler.swift new file mode 100644 index 000000000..7b5afe033 --- /dev/null +++ b/mac/Focalboard/DownloadHandler.swift @@ -0,0 +1,40 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import Foundation +import WebKit + +class DownloadHandler: NSObject, WKDownloadDelegate { + func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) { + DispatchQueue.main.async { + // Let user select location of file + let savePanel = NSSavePanel() + savePanel.canCreateDirectories = true + savePanel.nameFieldStringValue = suggestedFilename + // BUGBUG: Specifying the allowedFileTypes causes Catalina to hang / error out + //savePanel.allowedFileTypes = [".boardsarchive"] + savePanel.begin { (result) in + if result.rawValue == NSApplication.ModalResponse.OK.rawValue, + let fileUrl = savePanel.url { + if (FileManager.default.fileExists(atPath: fileUrl.path)) { + // HACKHACK: WKWebView doesn't appear to overwrite files, so delete exsiting files first + do { + try FileManager.default.removeItem(at: fileUrl) + } catch { + let alert = NSAlert() + alert.messageText = "Unable to replace \(fileUrl.path)" + alert.addButton(withTitle: "OK") + alert.alertStyle = .warning + alert.runModal() + } + } + completionHandler(fileUrl) + } + } + } + } + + func downloadDidFinish(_ download: WKDownload) { + NSLog("downloadDidFinish") + } +} diff --git a/mac/Focalboard/ViewController.swift b/mac/Focalboard/ViewController.swift index a35685c3e..0c6f98c4c 100644 --- a/mac/Focalboard/ViewController.swift +++ b/mac/Focalboard/ViewController.swift @@ -12,6 +12,7 @@ class ViewController: @IBOutlet var webView: CustomWKWebView! private var didLoad = false private var refreshWebViewOnLoad = true + private let downloadHandler = DownloadHandler() override func viewDidLoad() { super.viewDidLoad() @@ -100,60 +101,6 @@ class ViewController: webView.load(request) } - private func downloadJsonUrl(_ url: URL) { - NSLog("downloadJsonUrl") - let prefix = "data:text/json," - let urlString = url.absoluteString - let encodedJson = String(urlString[urlString.index(urlString.startIndex, offsetBy: prefix.lengthOfBytes(using: .utf8))...]) - guard let json = encodedJson.removingPercentEncoding else { - return - } - - // Form file name - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-M-d" - let dateString = dateFormatter.string(from: Date()) - let filename = "archive-\(dateString).focalboard" - - // Save file - let savePanel = NSSavePanel() - savePanel.canCreateDirectories = true - savePanel.nameFieldStringValue = filename - // BUGBUG: Specifying the allowedFileTypes causes Catalina to hang / error out - //savePanel.allowedFileTypes = [".focalboard"] - savePanel.begin { (result) in - if result.rawValue == NSApplication.ModalResponse.OK.rawValue, - let fileUrl = savePanel.url { - try? json.write(to: fileUrl, atomically: true, encoding: .utf8) - } - } - } - - private func downloadCsvUrl(_ url: URL) { - NSLog("downloadCsvUrl") - let prefix = "data:text/csv;charset=utf-8," - let urlString = url.absoluteString - let encodedContents = String(urlString[urlString.index(urlString.startIndex, offsetBy: prefix.lengthOfBytes(using: .utf8))...]) - guard let contents = encodedContents.removingPercentEncoding else { - return - } - - let filename = "data.csv" - - // Save file - let savePanel = NSSavePanel() - savePanel.canCreateDirectories = true - savePanel.nameFieldStringValue = filename - // BUGBUG: Specifying the allowedFileTypes causes Catalina to hang / error out - //savePanel.allowedFileTypes = [".focalboard"] - savePanel.begin { (result) in - if result.rawValue == NSApplication.ModalResponse.OK.rawValue, - let fileUrl = savePanel.url { - try? contents.write(to: fileUrl, atomically: true, encoding: .utf8) - } - } - } - func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) { NSLog("webView runOpenPanel") let openPanel = NSOpenPanel() @@ -169,22 +116,30 @@ class ViewController: } } - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if let url = navigationAction.request.url { - // Intercept archive downloads, and present native UI - if (url.absoluteString.hasPrefix("data:text/json,")) { - decisionHandler(.cancel) - downloadJsonUrl(url) - return - } - if (url.absoluteString.hasPrefix("data:text/csv;charset=utf-8,")) { - decisionHandler(.cancel) - downloadCsvUrl(url) - return - } - NSLog("decidePolicyFor navigationAction: \(url.absoluteString)") + // Handle downloads + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { + if navigationAction.shouldPerformDownload { + decisionHandler(.download, preferences) + } else { + decisionHandler(.allow, preferences) } - decisionHandler(.allow) + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + if navigationResponse.canShowMIMEType { + decisionHandler(.allow) + } else { + decisionHandler(.download) + } + } + + func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) { + download.delegate = downloadHandler + } + + func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) { + download.delegate = downloadHandler } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { diff --git a/mattermost-plugin/webapp/src/index.tsx b/mattermost-plugin/webapp/src/index.tsx index a33d7fdf4..28735de61 100644 --- a/mattermost-plugin/webapp/src/index.tsx +++ b/mattermost-plugin/webapp/src/index.tsx @@ -172,7 +172,14 @@ export default class Plugin { } if (rudderKey !== '') { - rudderAnalytics.load(rudderKey, rudderUrl) + const rudderCfg = {} as {setCookieDomain: string} + if (siteURL && siteURL !== '') { + try { + rudderCfg.setCookieDomain = new URL(siteURL).hostname + // eslint-disable-next-line no-empty + } catch (_) {} + } + rudderAnalytics.load(rudderKey, rudderUrl, rudderCfg) rudderAnalytics.identify(config?.telemetryid, {}, TELEMETRY_OPTIONS) diff --git a/server/swagger/README.md b/server/swagger/README.md index 504df0d02..f349f1129 100644 --- a/server/swagger/README.md +++ b/server/swagger/README.md @@ -18,3 +18,27 @@ brew install openapi-generator # Server API documentation See the generated [server API documentation here](https://htmlpreview.github.io/?https://github.com/mattermost/focalboard/blob/main/server/swagger/docs/html/index.html). + +# Differences for Mattermost Boards + +The auto-generated Swagger API documentation is for Focalboard Personal Server. If you are calling the API on Mattermost Boards, the additional changes are: + +### API URLs endpoint + +The API endpoint is at `https://SERVERNAME/plugins/focalboard/api/`, e.g. `https://community.mattermost.com/plugins/focalboard/api/`. + +### Use the Mattermost auth token + +Refer to the [Mattermost API documentation here](https://api.mattermost.com/#tag/authentication) on how to obtain the auth token. + +Pass this token as a bearer token to the Boards APIs, e.g. + +``` +curl -i -H "X-Requested-With: XMLHttpRequest" -H 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz' https://community.mattermost.com/plugins/focalboard/api/v1/workspaces +``` + +Note that the `X-Requested-With: XMLHttpRequest` header is required to pass the CSRF check. + +# We want to hear from you! + +If you are planning on using the Boards API, we would love to hear about what you'd like to do, and how we can improve the APIs in the future. [See here](https://github.com/mattermost/focalboard/discussions/2139) for more details on how to connect. diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index af934a051..b260ef3ea 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -11,7 +11,7 @@ "BoardPage.syncFailed": "Board may be deleted or access revoked.", "BoardTemplateSelector.add-template": "New template", "BoardTemplateSelector.create-empty-board": "Create empty board", - "BoardTemplateSelector.delete-template": "Delete template", + "BoardTemplateSelector.delete-template": "Delete", "BoardTemplateSelector.description": "Choose a template to help you get started. Easily customize the template to fit your needs, or create an empty board to start from scratch.", "BoardTemplateSelector.edit-template": "Edit", "BoardTemplateSelector.plugin.no-content-description": "Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of \"{workspaceName}\" will have access to boards created here.", @@ -81,6 +81,7 @@ "CardDialog.delete-confirmation-dialog-heading": "Confirm card delete!", "CardDialog.editing-template": "You're editing a template.", "CardDialog.nocard": "This card doesn't exist or is inaccessible.", + "CenterPanel.Share": "Share", "ColorOption.selectColor": "Select {color} Color", "Comment.delete": "Delete", "CommentsList.send": "Send", @@ -161,9 +162,13 @@ "RegistrationLink.description": "Share this link for others to create accounts:", "RegistrationLink.regenerateToken": "Regenerate token", "RegistrationLink.tokenRegenerated": "Registration link regenerated", + "ShareBoard.PublishDescription": "Publish and share a “read only” link with everyone on the web", + "ShareBoard.PublishTitle": "Publish to the web", + "ShareBoard.Title": "Share Board", "ShareBoard.confirmRegenerateToken": "This will invalidate previously shared links. Continue?", "ShareBoard.copiedLink": "Copied!", "ShareBoard.copyLink": "Copy link", + "ShareBoard.regenerate": "Regenerate token", "ShareBoard.regenerateToken": "Regenerate token", "ShareBoard.share": "Publish and share this board with anyone who has the link", "ShareBoard.tokenRegenrated": "Token regenerated", @@ -232,6 +237,7 @@ "ViewHeader.share-board": "Share board", "ViewHeader.sort": "Sort", "ViewHeader.untitled": "Untitled", + "ViewHeader.view-header-menu": "View header menu", "ViewHeader.view-menu": "View menu", "ViewTitle.hide-description": "hide description", "ViewTitle.pick-icon": "Pick icon", diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 794584fe3..2b1934eb1 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -42,7 +42,7 @@ declare let window: IAppWindow const UUID_REGEX = new RegExp(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/) -const App = React.memo((): JSX.Element => { +const App = (): JSX.Element => { const language = useAppSelector(getLanguage) const loggedIn = useAppSelector(getLoggedIn) const globalError = useAppSelector(getGlobalError) @@ -277,6 +277,6 @@ const App = React.memo((): JSX.Element => { ) -}) +} -export default App +export default React.memo(App) diff --git a/webapp/src/blocks/board.ts b/webapp/src/blocks/board.ts index 5decb9632..8eb844f4d 100644 --- a/webapp/src/blocks/board.ts +++ b/webapp/src/blocks/board.ts @@ -67,6 +67,7 @@ function createBoard(block?: Block): Board { description: block?.fields.description || '', icon: block?.fields.icon || '', isTemplate: block?.fields.isTemplate || false, + templateVer: block?.fields.templateVer || 0, columnCalculations: block?.fields.columnCalculations || [], cardProperties, }, diff --git a/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap b/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap index 6eb6c21c6..7284ee18c 100644 --- a/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap +++ b/webapp/src/components/__snapshots__/centerPanel.test.tsx.snap @@ -30,106 +30,110 @@ exports[`components/centerPanel return centerPanel and click on card to show car
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -139,6 +143,21 @@ exports[`components/centerPanel return centerPanel and click on card to show car
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -688,6 +711,21 @@ exports[`components/centerPanel return centerPanel and click on new card to edit
+
+ +
-
- -
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -1626,106 +1668,110 @@ exports[`components/centerPanel return centerPanel and press touch ctrl+d for on
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -1735,6 +1781,21 @@ exports[`components/centerPanel return centerPanel and press touch ctrl+d for on
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -2343,6 +2408,21 @@ exports[`components/centerPanel return centerPanel and press touch del for one c
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -2951,6 +3035,21 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -3559,6 +3662,21 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -4167,6 +4289,21 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -4775,6 +4916,21 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -5383,6 +5543,21 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -5991,6 +6170,21 @@ exports[`components/centerPanel return centerPanel and select one card and click
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -6599,6 +6797,21 @@ exports[`components/centerPanel return centerPanel and select one card and click
+
+ +
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -7207,6 +7424,21 @@ exports[`components/centerPanel should match snapshot for Gallery 1`] = `
+
+ +
+
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+ + +
+
+ +
+
+ +
+ + +
+ +
+
+
+
+ New +
+ +
+
+
+
+
+
+ + No name + + +
+ +
+
+ + +
+
+ + + +
+ +
+
+ + +
+
+ + + +
+ +
+
+ + +
+
+ +
+
+
+
+ +
+
+
- +
+ +
+
+
+
+
+`; + +exports[`components/centerPanel should match snapshot for Kanban, not shared 1`] = ` +
+
+
+ +
+
+ +
+
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -7900,106 +8671,110 @@ exports[`components/centerPanel should match snapshot for Table 1`] = `
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -8009,6 +8784,21 @@ exports[`components/centerPanel should match snapshot for Table 1`] = `
+
+ +
- -`; - -exports[`src/components/shareBoardComponent return shareBoardComponent and click Regenerate token 1`] = ` -
- -`; - -exports[`src/components/shareBoardComponent return shareBoardComponent and click Switch 1`] = ` -
- -`; - -exports[`src/components/shareBoardComponent return shareBoardComponent and click Switch without sharing 1`] = ` -
- -`; - -exports[`src/components/shareBoardComponent should match snapshot 1`] = ` -
- -`; - -exports[`src/components/shareBoardComponent should match snapshot with sharing 1`] = ` -
- -`; - -exports[`src/components/shareBoardComponent should match snapshot with sharing and subpath 1`] = ` -
- -`; - -exports[`src/components/shareBoardComponent should match snapshot with sharing and without workspaceId 1`] = ` -
- -`; - -exports[`src/components/shareBoardComponent should match snapshot with sharing and without workspaceId and subpath 1`] = ` -
- -`; diff --git a/webapp/src/components/__snapshots__/workspace.test.tsx.snap b/webapp/src/components/__snapshots__/workspace.test.tsx.snap index c20b9fe69..841d0656d 100644 --- a/webapp/src/components/__snapshots__/workspace.test.tsx.snap +++ b/webapp/src/components/__snapshots__/workspace.test.tsx.snap @@ -250,106 +250,110 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -359,6 +363,21 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
+
+ +
-
- -
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -1451,106 +1474,110 @@ exports[`src/components/workspace should match snapshot 1`] = `
- -
-
- + + + + hide description + +
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
@@ -1560,6 +1587,21 @@ exports[`src/components/workspace should match snapshot 1`] = `
+
+ +
-
- -
- -
-
-
+ + i + +
+
+
+ +
+
+
+
+
+
diff --git a/webapp/src/components/addContentMenuItem.tsx b/webapp/src/components/addContentMenuItem.tsx index 434715300..1e121b641 100644 --- a/webapp/src/components/addContentMenuItem.tsx +++ b/webapp/src/components/addContentMenuItem.tsx @@ -18,7 +18,7 @@ type Props = { cords: {x: number, y?: number, z?: number} } -const AddContentMenuItem = React.memo((props:Props): JSX.Element => { +const AddContentMenuItem = (props:Props): JSX.Element => { const {card, type, cords} = props const index = cords.x const contentOrder = card.fields.contentOrder.slice() @@ -51,6 +51,6 @@ const AddContentMenuItem = React.memo((props:Props): JSX.Element => { }} /> ) -}) +} -export default AddContentMenuItem +export default React.memo(AddContentMenuItem) diff --git a/webapp/src/components/blockIconSelector.tsx b/webapp/src/components/blockIconSelector.tsx index 0f95ee4dd..f404f9bdd 100644 --- a/webapp/src/components/blockIconSelector.tsx +++ b/webapp/src/components/blockIconSelector.tsx @@ -20,7 +20,7 @@ type Props = { readonly?: boolean } -const BlockIconSelector = React.memo((props: Props) => { +const BlockIconSelector = (props: Props) => { const {block, size} = props const intl = useIntl() @@ -72,6 +72,6 @@ const BlockIconSelector = React.memo((props: Props) => { }
) -}) +} -export default BlockIconSelector +export default React.memo(BlockIconSelector) diff --git a/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelector.test.tsx.snap b/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelector.test.tsx.snap index e1d2c8bc2..6cfb85141 100644 --- a/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelector.test.tsx.snap +++ b/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelector.test.tsx.snap @@ -110,11 +110,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plu >
-
-
+ />
@@ -239,11 +235,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plu >
-
-
+ />
@@ -368,11 +360,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plu >
-
-
+ />
@@ -507,11 +495,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector not a focalboard >
-
-
+ />
diff --git a/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelectorPreview.test.tsx.snap b/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelectorPreview.test.tsx.snap index b26f668e3..f96559440 100644 --- a/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelectorPreview.test.tsx.snap +++ b/webapp/src/components/boardTemplateSelector/__snapshots__/boardTemplateSelectorPreview.test.tsx.snap @@ -7,9 +7,6 @@ exports[`components/boardTemplateSelector/boardTemplateSelectorPreview should ma
-
diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.scss b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.scss index 80431f4ae..5c5b20dd2 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.scss +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.scss @@ -41,7 +41,7 @@ .templates-list { margin-right: 32px; - min-width: 200px; + width: 300px; max-height: 500px; overflow-y: auto; @@ -82,6 +82,10 @@ justify-content: center; } + .empty-board { + background-color: rgb(var(--center-channel-bg-rgb)); + } + .Button { &:first-child { margin-right: 16px; diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.test.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.test.tsx index b46a8a308..126b9a3be 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.test.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.test.tsx @@ -100,6 +100,8 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => { {id: 'global-id-5'}, ], dateDisplayPropertyId: 'global-id-5', + isTemplate: true, + templateVer: 2, }, }], }, diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.tsx index 329c49150..e680128e7 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelector.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelector.tsx @@ -27,7 +27,7 @@ type Props = { onClose?: () => void } -const BoardTemplateSelector = React.memo((props: Props) => { +const BoardTemplateSelector = (props: Props) => { const globalTemplates = useAppSelector(getGlobalTemplates) || [] const currentBoard = useAppSelector(getCurrentBoard) || null const {title, description, onClose} = props @@ -166,7 +166,6 @@ const BoardTemplateSelector = React.memo((props: Props) => {
) -}) - -export default BoardTemplateSelector +} +export default React.memo(BoardTemplateSelector) diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.scss b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.scss index 265e3e49e..f30662215 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.scss +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.scss @@ -40,6 +40,10 @@ position: relative; display: none; right: -8px; + + .DeleteIcon { + padding-top: 3px; + } } } diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.test.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.test.tsx index cf6a8e151..08cc574ec 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.test.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.test.tsx @@ -127,6 +127,8 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => { cardProperties: [groupProperty], dateDisplayPropertyId: 'global-id-5', columnCalculations: {}, + isTemplate: true, + templateVer: 2, }, } diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.tsx index d659ea410..0d0899b04 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorItem.tsx @@ -19,7 +19,7 @@ type Props = { onEdit: (templateId: string) => void } -const BoardTemplateSelectorItem = React.memo((props: Props) => { +const BoardTemplateSelectorItem = (props: Props) => { const {isActive, template, onEdit, onDelete, onSelect} = props const intl = useIntl() const [deleteOpen, setDeleteOpen] = useState(false) @@ -38,7 +38,7 @@ const BoardTemplateSelectorItem = React.memo((props: Props) => { > {template.fields.icon} {template.title} - {template.workspaceId !== '0' && + {!template.fields.templateVer &&
} @@ -65,6 +65,6 @@ const BoardTemplateSelectorItem = React.memo((props: Props) => { />}
) -}) +} -export default BoardTemplateSelectorItem +export default React.memo(BoardTemplateSelectorItem) diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.scss b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.scss index 97220abb5..c51efaa39 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.scss +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.scss @@ -4,15 +4,9 @@ width: 158%; height: 480px; border-radius: var(--modal-rad); + pointer-events: none; .Kanban { overflow: hidden; } - - .prevent-click { - position: absolute; - width: 100%; - height: 100%; - z-index: 1100; - } } diff --git a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.tsx b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.tsx index af7cfc2d1..02d8314cb 100644 --- a/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.tsx +++ b/webapp/src/components/boardTemplateSelector/boardTemplateSelectorPreview.tsx @@ -21,7 +21,7 @@ type Props = { activeTemplate: Board|null } -const BoardTemplateSelectorPreview = React.memo((props: Props) => { +const BoardTemplateSelectorPreview = (props: Props) => { const {activeTemplate} = props const [activeView, setActiveView] = useState(null) const [activeTemplateCards, setActiveTemplateCards] = useState([]) @@ -33,7 +33,7 @@ const BoardTemplateSelectorPreview = React.memo((props: Props) => { setActiveTemplateCards([]) octoClient.getSubtree(activeTemplate.id, activeView?.fields.viewType === 'gallery' ? 3 : 2, activeTemplate.workspaceId).then((blocks) => { const cards = blocks.filter((b) => b.type === 'card') - const views = blocks.filter((b) => b.type === 'view') + const views = blocks.filter((b) => b.type === 'view').sort((a, b) => a.title.localeCompare(b.title)) if (views.length > 0) { setActiveView(views[0] as BoardView) } @@ -65,28 +65,26 @@ const BoardTemplateSelectorPreview = React.memo((props: Props) => { return (
-
{activeView && -
- - null} - addCardFromTemplate={() => null} - addCardTemplate={() => null} - editCardTemplate={() => null} - readonly={false} - showShared={false} - /> -
} +
+ + null} + addCardFromTemplate={() => null} + addCardTemplate={() => null} + editCardTemplate={() => null} + readonly={false} + /> +
} {activeView?.fields.viewType === 'board' && { showCard={() => null} />} {activeView?.fields.viewType === 'table' && - null} - addCard={() => Promise.resolve()} - showCard={() => null} - />} +
null} + addCard={() => Promise.resolve()} + showCard={() => null} + />} {activeView?.fields.viewType === 'gallery' && - null} - addCard={() => Promise.resolve()} - />} + null} + addCard={() => Promise.resolve()} + />} {activeView?.fields.viewType === 'calendar' && - null} - addCard={() => Promise.resolve()} - />} + null} + addCard={() => Promise.resolve()} + />} ) -}) +} -export default BoardTemplateSelectorPreview +export default React.memo(BoardTemplateSelectorPreview) diff --git a/webapp/src/components/cardDetail/cardDetailContents.tsx b/webapp/src/components/cardDetail/cardDetailContents.tsx index 3b1eb6467..4c2d00adf 100644 --- a/webapp/src/components/cardDetail/cardDetailContents.tsx +++ b/webapp/src/components/cardDetail/cardDetailContents.tsx @@ -147,7 +147,7 @@ const ContentBlockWithDragAndDrop = (props: ContentBlockWithDragAndDropProps) => ) } -const CardDetailContents = React.memo((props: Props) => { +const CardDetailContents = (props: Props) => { const intl = useIntl() const {contents, card, id} = props if (contents.length) { @@ -188,6 +188,6 @@ const CardDetailContents = React.memo((props: Props) => { ) -}) +} -export default CardDetailContents +export default React.memo(CardDetailContents) diff --git a/webapp/src/components/cardDetail/cardDetailContentsMenu.tsx b/webapp/src/components/cardDetail/cardDetailContentsMenu.tsx index ee11c4165..ed1bc1690 100644 --- a/webapp/src/components/cardDetail/cardDetailContentsMenu.tsx +++ b/webapp/src/components/cardDetail/cardDetailContentsMenu.tsx @@ -37,7 +37,7 @@ function addContentMenu(intl: IntlShape, type: BlockTypes): JSX.Element { ) } -const CardDetailContentsMenu = React.memo(() => { +const CardDetailContentsMenu = () => { const intl = useIntl() return (
@@ -54,6 +54,6 @@ const CardDetailContentsMenu = React.memo(() => {
) -}) +} -export default CardDetailContentsMenu +export default React.memo(CardDetailContentsMenu) diff --git a/webapp/src/components/cardDetail/cardDetailProperties.tsx b/webapp/src/components/cardDetail/cardDetailProperties.tsx index f21f2af69..0b096ce20 100644 --- a/webapp/src/components/cardDetail/cardDetailProperties.tsx +++ b/webapp/src/components/cardDetail/cardDetailProperties.tsx @@ -32,7 +32,7 @@ type Props = { readonly: boolean } -const CardDetailProperties = React.memo((props: Props) => { +const CardDetailProperties = (props: Props) => { const {board, card, cards, views, activeView, contents, comments} = props const [newTemplateId, setNewTemplateId] = useState('') const intl = useIntl() @@ -202,6 +202,6 @@ const CardDetailProperties = React.memo((props: Props) => { } ) -}) +} -export default CardDetailProperties +export default React.memo(CardDetailProperties) diff --git a/webapp/src/components/cardDetail/commentsList.tsx b/webapp/src/components/cardDetail/commentsList.tsx index 86017196a..df64a6847 100644 --- a/webapp/src/components/cardDetail/commentsList.tsx +++ b/webapp/src/components/cardDetail/commentsList.tsx @@ -24,7 +24,7 @@ type Props = { readonly: boolean } -const CommentsList = React.memo((props: Props) => { +const CommentsList = (props: Props) => { const [newComment, setNewComment] = useState('') const me = useAppSelector(getMe) @@ -97,6 +97,6 @@ const CommentsList = React.memo((props: Props) => { {!(comments.length === 0 && props.readonly) &&
} ) -}) +} -export default CommentsList +export default React.memo(CommentsList) diff --git a/webapp/src/components/centerPanel.scss b/webapp/src/components/centerPanel.scss index 56fa05085..cfe3aa3fe 100644 --- a/webapp/src/components/centerPanel.scss +++ b/webapp/src/components/centerPanel.scss @@ -44,7 +44,15 @@ left: 0; background: rgb(var(--center-channel-bg-rgb)); z-index: 100; + + > .mid-head { + flex: 0 0 auto; + display: flex; + flex-direction: row; + justify-content: space-between; + } } + > div:nth-child(2) { padding: 0 0 0 1px; diff --git a/webapp/src/components/centerPanel.test.tsx b/webapp/src/components/centerPanel.test.tsx index bc4055c8a..0d900724e 100644 --- a/webapp/src/components/centerPanel.test.tsx +++ b/webapp/src/components/centerPanel.test.tsx @@ -110,6 +110,24 @@ describe('components/centerPanel', () => { activeView.fields.viewType = 'board' jest.clearAllMocks() }) + test('should match snapshot for Kanban, not shared', () => { + const {container} = render(wrapDNDIntl( + + + , + )) + expect(container).toMatchSnapshot() + }) test('should match snapshot for Kanban', () => { const {container} = render(wrapDNDIntl( diff --git a/webapp/src/components/centerPanel.tsx b/webapp/src/components/centerPanel.tsx index 81f38e9d3..1d1ee0305 100644 --- a/webapp/src/components/centerPanel.tsx +++ b/webapp/src/components/centerPanel.tsx @@ -20,11 +20,12 @@ import {UserSettings} from '../userSettings' import {addCard, addTemplate} from '../store/cards' import {updateView} from '../store/views' import {getVisibleAndHiddenGroups} from '../boardUtils' +import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient' + +import ShareBoardButton from './shareBoard/shareBoardButton' import './centerPanel.scss' -import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient' - import CardDialog from './cardDialog' import RootPortal from './rootPortal' import TopBar from './topBar' @@ -59,6 +60,7 @@ type Props = { type State = { selectedCardIds: string[] cardIdToFocusOnRender: string + showShareDialog: boolean } class CenterPanel extends React.Component { @@ -102,6 +104,7 @@ class CenterPanel extends React.Component { this.state = { selectedCardIds: [], cardIdToFocusOnRender: '', + showShareDialog: false, } } @@ -146,11 +149,18 @@ class CenterPanel extends React.Component {
- +
+ + {!this.props.readonly && this.props.showShared && + + } +
{ addCardTemplate={this.addCardTemplate} editCardTemplate={this.editCardTemplate} readonly={this.props.readonly} - showShared={this.props.showShared} />
diff --git a/webapp/src/components/content/checkboxElement.tsx b/webapp/src/components/content/checkboxElement.tsx index c86602eea..15955a546 100644 --- a/webapp/src/components/content/checkboxElement.tsx +++ b/webapp/src/components/content/checkboxElement.tsx @@ -21,7 +21,7 @@ type Props = { onDeleteElement?: () => void } -const CheckboxElement = React.memo((props: Props) => { +const CheckboxElement = (props: Props) => { const {block, readonly} = props const intl = useIntl() const titleRef = useRef(null) @@ -83,7 +83,7 @@ const CheckboxElement = React.memo((props: Props) => { /> ) -}) +} contentRegistry.registerContentType({ type: 'checkbox', @@ -104,4 +104,4 @@ contentRegistry.registerContentType({ }, }) -export default CheckboxElement +export default React.memo(CheckboxElement) diff --git a/webapp/src/components/content/dividerElement.tsx b/webapp/src/components/content/dividerElement.tsx index 11c3dca94..cc7c1cad9 100644 --- a/webapp/src/components/content/dividerElement.tsx +++ b/webapp/src/components/content/dividerElement.tsx @@ -8,7 +8,7 @@ import DividerIcon from '../../widgets/icons/divider' import {contentRegistry} from './contentRegistry' import './dividerElement.scss' -const DividerElement = React.memo((): JSX.Element =>
) +const DividerElement = (): JSX.Element =>
contentRegistry.registerContentType({ type: 'divider', @@ -20,4 +20,4 @@ contentRegistry.registerContentType({ createComponent: () => , }) -export default DividerElement +export default React.memo(DividerElement) diff --git a/webapp/src/components/content/imageElement.tsx b/webapp/src/components/content/imageElement.tsx index ffdc8eb8e..a687102fe 100644 --- a/webapp/src/components/content/imageElement.tsx +++ b/webapp/src/components/content/imageElement.tsx @@ -14,7 +14,7 @@ type Props = { block: ContentBlock } -const ImageElement = React.memo((props: Props): JSX.Element|null => { +const ImageElement = (props: Props): JSX.Element|null => { const [imageDataUrl, setImageDataUrl] = useState(null) const {block} = props @@ -40,7 +40,7 @@ const ImageElement = React.memo((props: Props): JSX.Element|null => { alt={block.title} /> ) -}) +} contentRegistry.registerContentType({ type: 'image', @@ -65,4 +65,4 @@ contentRegistry.registerContentType({ createComponent: (block) => , }) -export default ImageElement +export default React.memo(ImageElement) diff --git a/webapp/src/components/content/textElement.tsx b/webapp/src/components/content/textElement.tsx index 73955fe51..beef17a7a 100644 --- a/webapp/src/components/content/textElement.tsx +++ b/webapp/src/components/content/textElement.tsx @@ -16,7 +16,7 @@ type Props = { readonly: boolean } -const TextElement = React.memo((props: Props): JSX.Element => { +const TextElement = (props: Props): JSX.Element => { const {block, readonly} = props const intl = useIntl() @@ -32,7 +32,7 @@ const TextElement = React.memo((props: Props): JSX.Element => { readonly={readonly} /> ) -}) +} contentRegistry.registerContentType({ type: 'text', @@ -51,4 +51,4 @@ contentRegistry.registerContentType({ }, }) -export default TextElement +export default React.memo(TextElement) diff --git a/webapp/src/components/contentBlock.tsx b/webapp/src/components/contentBlock.tsx index 718d2e81e..f2de21e5b 100644 --- a/webapp/src/components/contentBlock.tsx +++ b/webapp/src/components/contentBlock.tsx @@ -34,7 +34,7 @@ type Props = { cords: {x: number, y?: number, z?: number} } -const ContentBlock = React.memo((props: Props): JSX.Element => { +const ContentBlock = (props: Props): JSX.Element => { const {card, block, readonly, cords} = props const intl = useIntl() const [, , gripRef, itemRef] = useSortableWithGrip('content', {block, cords}, true, () => {}) @@ -158,6 +158,6 @@ const ContentBlock = React.memo((props: Props): JSX.Element => { />
) -}) +} -export default ContentBlock +export default React.memo(ContentBlock) diff --git a/webapp/src/components/dialog.tsx b/webapp/src/components/dialog.tsx index 42bbd2d93..8d7642ded 100644 --- a/webapp/src/components/dialog.tsx +++ b/webapp/src/components/dialog.tsx @@ -16,12 +16,13 @@ type Props = { toolbar?: React.ReactNode hideCloseButton?: boolean className?: string + title?: string onClose: () => void, } -const Dialog = React.memo((props: Props) => { +const Dialog = (props: Props) => { const {toolsMenu} = props - const {toolbar} = props + const {toolbar, title} = props const intl = useIntl() const closeDialogText = intl.formatMessage({ @@ -47,6 +48,7 @@ const Dialog = React.memo((props: Props) => { className='dialog' >
+ {title &&

{title}

} { !props.hideCloseButton && {
) -}) +} -export default Dialog +export default React.memo(Dialog) diff --git a/webapp/src/components/gallery/galleryCard.tsx b/webapp/src/components/gallery/galleryCard.tsx index d7f11a6b1..b0017b319 100644 --- a/webapp/src/components/gallery/galleryCard.tsx +++ b/webapp/src/components/gallery/galleryCard.tsx @@ -42,7 +42,7 @@ type Props = { onDrop: (srcCard: Card, dstCard: Card) => void } -const GalleryCard = React.memo((props: Props) => { +const GalleryCard = (props: Props) => { const {card, board} = props const intl = useIntl() const [isDragging, isOver, cardRef] = useSortable('card', card, props.isManualSort && !props.readonly, props.onDrop) @@ -185,6 +185,6 @@ const GalleryCard = React.memo((props: Props) => { />} ) -}) +} -export default GalleryCard +export default React.memo(GalleryCard) diff --git a/webapp/src/components/globalHeader/__snapshots__/globalHeaderSettingsMenu.test.tsx.snap b/webapp/src/components/globalHeader/__snapshots__/globalHeaderSettingsMenu.test.tsx.snap index 7407f3d5a..446cfabba 100644 --- a/webapp/src/components/globalHeader/__snapshots__/globalHeaderSettingsMenu.test.tsx.snap +++ b/webapp/src/components/globalHeader/__snapshots__/globalHeaderSettingsMenu.test.tsx.snap @@ -239,7 +239,7 @@ exports[`components/sidebar/GlobalHeaderSettingsMenu imports menu open should ma Random icons
{ +const GlobalHeaderSettingsMenu = () => { const intl = useIntl() const dispatch = useAppDispatch() @@ -96,6 +96,6 @@ const GlobalHeaderSettingsMenu = React.memo(() => {
) -}) +} -export default GlobalHeaderSettingsMenu +export default React.memo(GlobalHeaderSettingsMenu) diff --git a/webapp/src/components/kanban/kanban.scss b/webapp/src/components/kanban/kanban.scss index 229e45013..356c2c9c7 100644 --- a/webapp/src/components/kanban/kanban.scss +++ b/webapp/src/components/kanban/kanban.scss @@ -85,9 +85,14 @@ .octo-board-hidden-item { display: flex; flex-direction: row; + align-items: center; > div { margin-right: 5px; } + + .Label { + margin: 5px; + } } } diff --git a/webapp/src/components/kanban/kanbanCard.tsx b/webapp/src/components/kanban/kanbanCard.tsx index 494fcbf62..57dfcbc02 100644 --- a/webapp/src/components/kanban/kanbanCard.tsx +++ b/webapp/src/components/kanban/kanbanCard.tsx @@ -40,7 +40,7 @@ type Props = { isManualSort: boolean } -const KanbanCard = React.memo((props: Props) => { +const KanbanCard = (props: Props) => { const {card, board} = props const intl = useIntl() const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly, props.onDrop) @@ -174,6 +174,6 @@ const KanbanCard = React.memo((props: Props) => { ) -}) +} -export default KanbanCard +export default React.memo(KanbanCard) diff --git a/webapp/src/components/kanban/kanbanColumn.tsx b/webapp/src/components/kanban/kanbanColumn.tsx index 069811f89..d5924cfbe 100644 --- a/webapp/src/components/kanban/kanbanColumn.tsx +++ b/webapp/src/components/kanban/kanbanColumn.tsx @@ -11,7 +11,7 @@ type Props = { children: React.ReactNode } -const KanbanColumn = React.memo((props: Props) => { +const KanbanColumn = (props: Props) => { const [{isOver}, drop] = useDrop(() => ({ accept: 'card', collect: (monitor) => ({ @@ -36,6 +36,6 @@ const KanbanColumn = React.memo((props: Props) => { {props.children}
) -}) +} -export default KanbanColumn +export default React.memo(KanbanColumn) diff --git a/webapp/src/components/modal.tsx b/webapp/src/components/modal.tsx index c83dbf525..a66da1857 100644 --- a/webapp/src/components/modal.tsx +++ b/webapp/src/components/modal.tsx @@ -12,7 +12,7 @@ type Props = { children: React.ReactNode } -const Modal = React.memo((props: Props): JSX.Element => { +const Modal = (props: Props): JSX.Element => { const node = useRef(null) const {position, onClose, children} = props @@ -47,6 +47,6 @@ const Modal = React.memo((props: Props): JSX.Element => { {children}
) -}) +} -export default Modal +export default React.memo(Modal) diff --git a/webapp/src/components/modalWrapper.tsx b/webapp/src/components/modalWrapper.tsx index b8cc1c209..e344fc8bd 100644 --- a/webapp/src/components/modalWrapper.tsx +++ b/webapp/src/components/modalWrapper.tsx @@ -7,12 +7,12 @@ type Props = { children: React.ReactNode } -const ModalWrapper = React.memo((props: Props) => { +const ModalWrapper = (props: Props) => { return (
{props.children}
) -}) +} -export default ModalWrapper +export default React.memo(ModalWrapper) diff --git a/webapp/src/components/properties/select/select.tsx b/webapp/src/components/properties/select/select.tsx index 8560e7838..74b48a379 100644 --- a/webapp/src/components/properties/select/select.tsx +++ b/webapp/src/components/properties/select/select.tsx @@ -20,7 +20,7 @@ type Props = { isEditable: boolean } -const SelectProperty = React.memo((props: Props) => { +const SelectProperty = (props: Props) => { const {emptyValue, propertyValue, propertyTemplate, isEditable} = props const [open, setOpen] = useState(false) @@ -56,6 +56,6 @@ const SelectProperty = React.memo((props: Props) => { onBlur={() => setOpen(false)} /> ) -}) +} -export default SelectProperty +export default React.memo(SelectProperty) diff --git a/webapp/src/components/rootPortal.tsx b/webapp/src/components/rootPortal.tsx index 5e17db6f5..5c930a8d6 100644 --- a/webapp/src/components/rootPortal.tsx +++ b/webapp/src/components/rootPortal.tsx @@ -8,7 +8,7 @@ type Props = { children: React.ReactNode } -const RootPortal = React.memo((props: Props): JSX.Element => { +const RootPortal = (props: Props): JSX.Element => { const [el] = useState(document.createElement('div')) const rootPortal = document.getElementById('focalboard-root-portal') @@ -24,6 +24,6 @@ const RootPortal = React.memo((props: Props): JSX.Element => { }, []) return ReactDOM.createPortal(props.children, el) // eslint-disable-line -}) +} -export default RootPortal +export default React.memo(RootPortal) diff --git a/webapp/src/components/shareBoard/__snapshots__/shareBoard.test.tsx.snap b/webapp/src/components/shareBoard/__snapshots__/shareBoard.test.tsx.snap new file mode 100644 index 000000000..e32d4300b --- /dev/null +++ b/webapp/src/components/shareBoard/__snapshots__/shareBoard.test.tsx.snap @@ -0,0 +1,950 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 1`] = ` +
+
+
+ +
+
+`; + +exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 2`] = ` +
+
+
+ +
+
+`; + +exports[`src/components/shareBoard/shareBoard return shareBoard and click Regenerate token 1`] = ` +
+
+
+ +
+
+`; + +exports[`src/components/shareBoard/shareBoard return shareBoard, and click switch 1`] = ` +
+
+
+ +
+
+`; + +exports[`src/components/shareBoard/shareBoard return shareBoardComponent and click Switch without sharing 1`] = ` +
+
+
+ +
+
+`; + +exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = ` +
+
+
+ +
+
+`; + +exports[`src/components/shareBoard/shareBoard should match snapshot with sharing 1`] = ` +
+
+
+ +
+
+`; + +exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and subpath 1`] = ` +
+
+
+ +
+
+`; + +exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and without workspaceId and subpath 1`] = ` +
+
+
+ +
+
+`; diff --git a/webapp/src/components/shareBoard/__snapshots__/shareBoardButton.test.tsx.snap b/webapp/src/components/shareBoard/__snapshots__/shareBoardButton.test.tsx.snap new file mode 100644 index 000000000..8e2341beb --- /dev/null +++ b/webapp/src/components/shareBoard/__snapshots__/shareBoardButton.test.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = ` +
+
+ +
+
+`; diff --git a/webapp/src/components/shareBoard/shareBoard.scss b/webapp/src/components/shareBoard/shareBoard.scss new file mode 100644 index 000000000..50d8c3104 --- /dev/null +++ b/webapp/src/components/shareBoard/shareBoard.scss @@ -0,0 +1,66 @@ +.ShareBoardDialog { + .dialog { + @media not screen and (max-width: 975px) { + max-width: 600px; + height: auto; + } + } + + .tabs-modal { + padding: 24px; + border-radius: 8px; + background: rgba(var(--center-channel-bg-rgb), 1); + border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.16); + + .d-flex { + display: flex; + } + + .justify-content-between { + justify-content: space-between; + } + + .flex-column { + flex-direction: column; + } + + .tabs-inputs { + margin-top: 16px; + + .input-container { + border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); + border-radius: 4px; + width: 400px; + height: 40px; + } + + .IconButton { + height: 100%; + width: 40px; + } + + .icon-refresh { + margin: 4px; + font-size: 18px; + } + + .icon-content-copy { + margin-right: 4px; + font-size: 18px; + } + + .shareUrl { + flex-grow: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin: auto 4px; + padding: 0 16px; + + :hover { + background-color: rgb(var(--center-channel-color-rgb), 0.1); + } + } + } + } +} diff --git a/webapp/src/components/shareBoardComponent.test.tsx b/webapp/src/components/shareBoard/shareBoard.test.tsx similarity index 67% rename from webapp/src/components/shareBoardComponent.test.tsx rename to webapp/src/components/shareBoard/shareBoard.test.tsx index 57b341a87..08a263065 100644 --- a/webapp/src/components/shareBoardComponent.test.tsx +++ b/webapp/src/components/shareBoard/shareBoard.test.tsx @@ -7,13 +7,13 @@ import React from 'react' import {MemoryRouter} from 'react-router' import {mocked} from 'ts-jest/utils' -import {ISharing} from '../blocks/sharing' -import {TestBlockFactory} from '../test/testBlockFactory' -import {wrapDNDIntl} from '../testUtils' -import client from '../octoClient' -import {Utils} from '../utils' +import {ISharing} from '../../blocks/sharing' +import {TestBlockFactory} from '../../test/testBlockFactory' +import {wrapDNDIntl} from '../../testUtils' +import client from '../../octoClient' +import {Utils} from '../../utils' -import ShareBoardComponent from './shareBoardComponent' +import ShareBoard from './shareBoard' jest.useFakeTimers() @@ -21,8 +21,8 @@ const boardId = '1' const workspaceId: string|undefined = boardId const viewId = boardId -jest.mock('../octoClient') -jest.mock('../utils') +jest.mock('../../octoClient') +jest.mock('../../utils') const mockedOctoClient = mocked(client, true) const mockedUtils = mocked(Utils, true) @@ -46,7 +46,7 @@ jest.mock('react-router', () => { const board = TestBlockFactory.createBoard() board.id = boardId -describe('src/components/shareBoardComponent', () => { +describe('src/components/shareBoard/shareBoard', () => { const w = (window as any) const oldBaseURL = w.baseURL @@ -69,15 +69,22 @@ describe('src/components/shareBoardComponent', () => { mockedOctoClient.getSharing.mockResolvedValue(undefined) let container await act(async () => { - const result = render(wrapDNDIntl( - ), {wrapper: MemoryRouter}) + const result = render( + wrapDNDIntl( + ), + {wrapper: MemoryRouter}, + ) container = result.container }) + expect(container).toMatchSnapshot() + const closeButton = screen.getByRole('button', {name: 'Close dialog'}) + expect(closeButton).toBeDefined() }) + test('should match snapshot with sharing', async () => { const sharing:ISharing = { id: boardId, @@ -85,62 +92,64 @@ describe('src/components/shareBoardComponent', () => { token: 'oneToken', } mockedOctoClient.getSharing.mockResolvedValue(sharing) + let container await act(async () => { - const result = render(wrapDNDIntl( - ), {wrapper: MemoryRouter}) - container = result.container - }) - expect(container).toMatchSnapshot() - }) - test('should match snapshot with sharing and without workspaceId', async () => { - const sharing:ISharing = { - id: boardId, - enabled: true, - token: 'oneToken', - } - params = { - boardId, - viewId, - } - mockedOctoClient.getSharing.mockResolvedValue(sharing) - let container - await act(async () => { - const result = render(wrapDNDIntl( - ), {wrapper: MemoryRouter}) - container = result.container - }) - expect(container).toMatchSnapshot() - }) - test('return shareBoardComponent and click Copy link', async () => { - const sharing:ISharing = { - id: boardId, - enabled: true, - token: 'oneToken', - } - mockedOctoClient.getSharing.mockResolvedValue(sharing) - let container: Element | undefined - await act(async () => { - const result = render(wrapDNDIntl( - ), {wrapper: MemoryRouter}) + const result = render( + wrapDNDIntl( + ), + {wrapper: MemoryRouter}, + ) container = result.container }) const copyLinkElement = screen.getByRole('button', {name: 'Copy link'}) expect(copyLinkElement).toBeDefined() - userEvent.click(copyLinkElement) - expect(mockedUtils.copyTextToClipboard).toBeCalledTimes(1) + expect(container).toMatchSnapshot() }) - test('return shareBoardComponent and click Regenerate token', async () => { + + test('return shareBoard and click Copy link', async () => { + const sharing:ISharing = { + id: boardId, + enabled: true, + token: 'oneToken', + } + mockedOctoClient.getSharing.mockResolvedValue(sharing) + + let container + await act(async () => { + const result = render( + wrapDNDIntl( + ), + {wrapper: MemoryRouter}, + ) + container = result.container + }) + + expect(container).toMatchSnapshot() + + const copyLinkElement = screen.getByRole('button', {name: 'Copy link'}) + expect(copyLinkElement).toBeDefined() + + await act(async () => { + userEvent.click(copyLinkElement!) + }) + + expect(mockedUtils.copyTextToClipboard).toBeCalledTimes(1) + expect(container).toMatchSnapshot() + + const copiedLinkElement = screen.getByRole('button', {name: 'Copy link'}) + expect(copiedLinkElement).toBeDefined() + expect(copiedLinkElement.textContent).toContain('Copied!') + }) + + test('return shareBoard and click Regenerate token', async () => { window.confirm = jest.fn(() => { return true }) @@ -150,15 +159,20 @@ describe('src/components/shareBoardComponent', () => { token: 'oneToken', } mockedOctoClient.getSharing.mockResolvedValue(sharing) - let container: Element | undefined + + let container await act(async () => { - const result = render(wrapDNDIntl( - ), {wrapper: MemoryRouter}) + const result = render( + wrapDNDIntl( + ), + {wrapper: MemoryRouter}, + ) container = result.container }) + sharing.token = 'anotherToken' mockedUtils.createGuid.mockReturnValue('anotherToken') mockedOctoClient.getSharing.mockResolvedValue(sharing) @@ -172,7 +186,7 @@ describe('src/components/shareBoardComponent', () => { expect(mockedOctoClient.setSharing).toBeCalledTimes(1) expect(container).toMatchSnapshot() }) - test('return shareBoardComponent and click Switch', async () => { + test('return shareBoard, and click switch', async () => { const sharing:ISharing = { id: boardId, enabled: true, @@ -181,18 +195,24 @@ describe('src/components/shareBoardComponent', () => { mockedOctoClient.getSharing.mockResolvedValue(sharing) let container: Element | undefined await act(async () => { - const result = render(wrapDNDIntl( - ), {wrapper: MemoryRouter}) + const result = render( + wrapDNDIntl( + ), + {wrapper: MemoryRouter}, + ) container = result.container }) const switchElement = container?.querySelector('.Switch') expect(switchElement).toBeDefined() - userEvent.click(switchElement!) + await act(async () => { + userEvent.click(switchElement!) + }) + expect(mockedOctoClient.setSharing).toBeCalledTimes(1) - expect(mockedOctoClient.getSharing).toBeCalledTimes(1) + expect(mockedOctoClient.getSharing).toBeCalledTimes(2) expect(container).toMatchSnapshot() }) test('return shareBoardComponent and click Switch without sharing', async () => { @@ -200,11 +220,14 @@ describe('src/components/shareBoardComponent', () => { mockedUtils.createGuid.mockReturnValue('aToken') let container: Element | undefined await act(async () => { - const result = render(wrapDNDIntl( - ), {wrapper: MemoryRouter}) + const result = render( + wrapDNDIntl( + ), + {wrapper: MemoryRouter}, + ) container = result.container mockedOctoClient.getSharing.mockResolvedValue({ id: boardId, @@ -215,11 +238,12 @@ describe('src/components/shareBoardComponent', () => { expect(switchElement).toBeDefined() userEvent.click(switchElement!) jest.runOnlyPendingTimers() - result.rerender(wrapDNDIntl( - )) + result.rerender( + wrapDNDIntl( + )) }) expect(mockedOctoClient.setSharing).toBeCalledTimes(1) @@ -227,7 +251,6 @@ describe('src/components/shareBoardComponent', () => { expect(mockedUtils.createGuid).toBeCalledTimes(1) expect(container).toMatchSnapshot() }) - test('should match snapshot with sharing and without workspaceId and subpath', async () => { w.baseURL = '/test-subpath/plugins/boards' const sharing:ISharing = { @@ -243,7 +266,7 @@ describe('src/components/shareBoardComponent', () => { let container await act(async () => { const result = render(wrapDNDIntl( - ), {wrapper: MemoryRouter}) @@ -263,7 +286,7 @@ describe('src/components/shareBoardComponent', () => { let container await act(async () => { const result = render(wrapDNDIntl( - ), {wrapper: MemoryRouter}) diff --git a/webapp/src/components/shareBoard/shareBoard.tsx b/webapp/src/components/shareBoard/shareBoard.tsx new file mode 100644 index 000000000..dccb063b1 --- /dev/null +++ b/webapp/src/components/shareBoard/shareBoard.tsx @@ -0,0 +1,178 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState, useEffect} from 'react' +import {useIntl, FormattedMessage} from 'react-intl' +import {generatePath, useRouteMatch} from 'react-router' + +import {Utils, IDType} from '../../utils' +import Tooltip from '../../widgets/tooltip' + +import {ISharing} from '../../blocks/sharing' + +import client from '../../octoClient' +import Dialog from '../dialog' +import Switch from '../../widgets/switch' +import Button from '../../widgets/buttons/button' +import {sendFlashMessage} from '../flashMessages' + +import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient' + +import CompassIcon from '../../widgets/icons/compassIcon' +import IconButton from '../../widgets/buttons/iconButton' +import './shareBoard.scss' + +type Props = { + boardId: string + onClose: () => void +} + +export default function ShareBoardDialog(props: Props): JSX.Element { + const [wasCopied, setWasCopied] = useState(false) + const [sharing, setSharing] = useState(undefined) + + const intl = useIntl() + const match = useRouteMatch<{workspaceId?: string, boardId: string, viewId: string}>() + + const loadData = async () => { + const newSharing = await client.getSharing(props.boardId) + setSharing(newSharing) + setWasCopied(false) + } + + const createSharingInfo = () => { + const newSharing: ISharing = { + id: props.boardId, + enabled: true, + token: Utils.createGuid(IDType.Token), + } + return newSharing + } + + const onShareChanged = async (isOn: boolean) => { + const newSharing: ISharing = sharing || createSharingInfo() + newSharing.id = props.boardId + newSharing.enabled = isOn + TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ShareBoard, {board: props.boardId, shareBoardEnabled: isOn}) + await client.setSharing(newSharing) + await loadData() + } + + const onRegenerateToken = async () => { + // eslint-disable-next-line no-alert + const accept = window.confirm(intl.formatMessage({id: 'ShareBoard.confirmRegenerateToken', defaultMessage: 'This will invalidate previously shared links. Continue?'})) + if (accept) { + const newSharing: ISharing = sharing || createSharingInfo() + newSharing.token = Utils.createGuid(IDType.Token) + await client.setSharing(newSharing) + await loadData() + + const description = intl.formatMessage({id: 'ShareBoard.tokenRegenrated', defaultMessage: 'Token regenerated'}) + sendFlashMessage({content: description, severity: 'low'}) + } + } + + useEffect(() => { + loadData() + }, []) + + const isSharing = Boolean(sharing && sharing.id === props.boardId && sharing.enabled) + const readToken = (sharing && isSharing) ? sharing.token : '' + const shareUrl = new URL(window.location.toString()) + shareUrl.searchParams.set('r', readToken) + + if (match.params.workspaceId) { + const newPath = generatePath('/workspace/:workspaceId/shared/:boardId/:viewId', { + boardId: match.params.boardId, + viewId: match.params.viewId, + workspaceId: match.params.workspaceId, + }) + shareUrl.pathname = Utils.buildURL(newPath) + } else { + const newPath = generatePath('/shared/:boardId/:viewId', { + boardId: match.params.boardId, + viewId: match.params.viewId, + }) + shareUrl.pathname = Utils.buildURL(newPath) + } + + return ( + +
+
+
+
+
{intl.formatMessage({id: 'ShareBoard.PublishTitle', defaultMessage: 'Publish to the web'})}
+
{intl.formatMessage({id: 'ShareBoard.PublishDescription', defaultMessage: 'Publish and share a “read only” link with everyone on the web'})}
+
+
+ +
+
+
+ {isSharing && + (
+
+ + {shareUrl.toString()} + + + } + title={intl.formatMessage({id: 'ShareBoard.regenerate', defaultMessage: 'Regenerate token'})} + className='IconButton--large' + /> + +
+ +
) + } +
+
+ ) +} diff --git a/webapp/src/components/shareBoard/shareBoardButton.scss b/webapp/src/components/shareBoard/shareBoardButton.scss new file mode 100644 index 000000000..f5c429d4d --- /dev/null +++ b/webapp/src/components/shareBoard/shareBoardButton.scss @@ -0,0 +1,20 @@ +.ShareBoardButton { + flex: 0 0 auto; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 8px; + + > .Button { + margin-top: 36px; + padding: 3px 10px; + background-color: rgb(var(--button-bg-rgb)); + color: rgb(var(--button-color-rgb)); + border-radius: 5px; + + &:hover { + background-color: rgba(var(--button-bg-rgb), 0.8); + } + } +} + diff --git a/webapp/src/components/shareBoard/shareBoardButton.test.tsx b/webapp/src/components/shareBoard/shareBoardButton.test.tsx new file mode 100644 index 000000000..fb3027018 --- /dev/null +++ b/webapp/src/components/shareBoard/shareBoardButton.test.tsx @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {render} from '@testing-library/react' +import React from 'react' + +import {TestBlockFactory} from '../../test/testBlockFactory' +import {wrapDNDIntl} from '../../testUtils' + +import ShareBoardButton from './shareBoardButton' + +jest.useFakeTimers() + +const boardId = '1' + +const board = TestBlockFactory.createBoard() +board.id = boardId + +describe('src/components/shareBoard/shareBoard', () => { + test('should match snapshot', async () => { + const result = render( + wrapDNDIntl( + )) + + const renderer = result.container + + expect(renderer).toMatchSnapshot() + }) +}) diff --git a/webapp/src/components/shareBoard/shareBoardButton.tsx b/webapp/src/components/shareBoard/shareBoardButton.tsx new file mode 100644 index 000000000..ef1aaa055 --- /dev/null +++ b/webapp/src/components/shareBoard/shareBoardButton.tsx @@ -0,0 +1,52 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState} from 'react' +import {FormattedMessage} from 'react-intl' + +import Button from '../../widgets/buttons/button' +import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient' + +import CompassIcon from '../../widgets/icons/compassIcon' + +import './shareBoardButton.scss' + +import ShareBoardDialog from './shareBoard' + +type Props = { + boardId: string +} +const ShareBoardButton = (props: Props) => { + const [showShareDialog, setShowShareDialog] = useState(false) + + return ( +
+ + {showShareDialog && + setShowShareDialog(false)} + boardId={props.boardId} + /> + } +
+ ) +} + +export default React.memo(ShareBoardButton) diff --git a/webapp/src/components/shareBoardComponent.scss b/webapp/src/components/shareBoardComponent.scss deleted file mode 100644 index 64d0bef21..000000000 --- a/webapp/src/components/shareBoardComponent.scss +++ /dev/null @@ -1,36 +0,0 @@ -.ShareBoardComponent { - display: flex; - flex-direction: column; - padding: 5px; - color: rgb(var(--center-channel-color-rgb)); - max-width: 500px; - white-space: normal; - - .Switch { - margin-left: 8px; - } - - > .row { - display: flex; - flex-direction: row; - align-items: center; - margin: 0 0 10px; - } - - > .row:last-child { - margin-bottom: 0; - } - - .spacer { - flex-grow: 1; - } - - a.shareUrl { - max-width: 320px; - flex-grow: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-right: 5px; - } -} diff --git a/webapp/src/components/shareBoardComponent.tsx b/webapp/src/components/shareBoardComponent.tsx deleted file mode 100644 index 474eef985..000000000 --- a/webapp/src/components/shareBoardComponent.tsx +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import React, {useState, useEffect} from 'react' -import {FormattedMessage, useIntl} from 'react-intl' -import {generatePath, useRouteMatch} from 'react-router' - -import {ISharing} from '../blocks/sharing' - -import client from '../octoClient' - -import {Utils, IDType} from '../utils' -import {sendFlashMessage} from '../components/flashMessages' - -import Button from '../widgets/buttons/button' -import Switch from '../widgets/switch' - -import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../telemetry/telemetryClient' - -import Modal from './modal' -import './shareBoardComponent.scss' - -type Props = { - boardId: string - onClose: () => void -} - -const ShareBoardComponent = React.memo((props: Props): JSX.Element => { - const [wasCopied, setWasCopied] = useState(false) - const [sharing, setSharing] = useState(undefined) - const intl = useIntl() - const match = useRouteMatch<{workspaceId?: string, boardId: string, viewId: string}>() - - const loadData = async () => { - const newSharing = await client.getSharing(props.boardId) - setSharing(newSharing) - setWasCopied(false) - } - - const createSharingInfo = () => { - const newSharing: ISharing = { - id: props.boardId, - enabled: true, - token: Utils.createGuid(IDType.Token), - } - return newSharing - } - - const onShareChanged = async (isOn: boolean) => { - const newSharing: ISharing = sharing || createSharingInfo() - newSharing.id = props.boardId - newSharing.enabled = isOn - TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ShareBoard, {board: props.boardId, shareBoardEnabled: isOn}) - await client.setSharing(newSharing) - await loadData() - } - - const onRegenerateToken = async () => { - // eslint-disable-next-line no-alert - const accept = window.confirm(intl.formatMessage({id: 'ShareBoard.confirmRegenerateToken', defaultMessage: 'This will invalidate previously shared links. Continue?'})) - if (accept) { - const newSharing: ISharing = sharing || createSharingInfo() - newSharing.token = Utils.createGuid(IDType.Token) - await client.setSharing(newSharing) - await loadData() - - const description = intl.formatMessage({id: 'ShareBoard.tokenRegenrated', defaultMessage: 'Token regenerated'}) - sendFlashMessage({content: description, severity: 'low'}) - } - } - - useEffect(() => { - loadData() - }, []) - - const isSharing = Boolean(sharing && sharing.id === props.boardId && sharing.enabled) - const readToken = (sharing && isSharing) ? sharing.token : '' - - const shareUrl = new URL(window.location.toString()) - shareUrl.searchParams.set('r', readToken) - - if (match.params.workspaceId) { - const newPath = generatePath('/workspace/:workspaceId/shared/:boardId/:viewId', { - boardId: match.params.boardId, - viewId: match.params.viewId, - workspaceId: match.params.workspaceId, - }) - shareUrl.pathname = Utils.buildURL(newPath) - } else { - const newPath = generatePath('/shared/:boardId/:viewId', { - boardId: match.params.boardId, - viewId: match.params.viewId, - }) - shareUrl.pathname = Utils.buildURL(newPath) - } - - return ( - -
-
-
- {isSharing && - } - {!isSharing && - } -
- -
- {isSharing && <> -
- - {shareUrl.toString()} - - -
-
- -
- } -
-
- ) -}) - -export default ShareBoardComponent diff --git a/webapp/src/components/sidebar/__snapshots__/sidebarSettingsMenu.test.tsx.snap b/webapp/src/components/sidebar/__snapshots__/sidebarSettingsMenu.test.tsx.snap index 53c0da2b1..5c2fb9bba 100644 --- a/webapp/src/components/sidebar/__snapshots__/sidebarSettingsMenu.test.tsx.snap +++ b/webapp/src/components/sidebar/__snapshots__/sidebarSettingsMenu.test.tsx.snap @@ -259,7 +259,7 @@ exports[`components/sidebar/SidebarSettingsMenu imports menu open should match s Random icons
void } -const RegistrationLink = React.memo((props: Props) => { +const RegistrationLink = (props: Props) => { const {onClose} = props const intl = useIntl() const workspace = useAppSelector(getCurrentWorkspace) @@ -89,6 +89,6 @@ const RegistrationLink = React.memo((props: Props) => {
) -}) +} -export default RegistrationLink +export default React.memo(RegistrationLink) diff --git a/webapp/src/components/sidebar/sidebar.tsx b/webapp/src/components/sidebar/sidebar.tsx index 35a5c8d3f..97aea8d22 100644 --- a/webapp/src/components/sidebar/sidebar.tsx +++ b/webapp/src/components/sidebar/sidebar.tsx @@ -39,7 +39,7 @@ function getWindowDimensions() { } } -const Sidebar = React.memo((props: Props) => { +const Sidebar = (props: Props) => { const [isHidden, setHidden] = useState(false) const [userHidden, setUserHidden] = useState(false) const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions()) @@ -209,6 +209,6 @@ const Sidebar = React.memo((props: Props) => { }
) -}) +} -export default Sidebar +export default React.memo(Sidebar) diff --git a/webapp/src/components/sidebar/sidebarBoardItem.tsx b/webapp/src/components/sidebar/sidebarBoardItem.tsx index d1e5622d1..e751d638e 100644 --- a/webapp/src/components/sidebar/sidebarBoardItem.tsx +++ b/webapp/src/components/sidebar/sidebarBoardItem.tsx @@ -33,7 +33,7 @@ type Props = { hideSidebar: () => void } -const SidebarBoardItem = React.memo((props: Props) => { +const SidebarBoardItem = (props: Props) => { const [collapsed, setCollapsed] = useState(false) const intl = useIntl() const history = useHistory() @@ -207,6 +207,6 @@ const SidebarBoardItem = React.memo((props: Props) => { />}
) -}) +} -export default SidebarBoardItem +export default React.memo(SidebarBoardItem) diff --git a/webapp/src/components/sidebar/sidebarSettingsMenu.tsx b/webapp/src/components/sidebar/sidebarSettingsMenu.tsx index 46b9767f4..02fcf4c17 100644 --- a/webapp/src/components/sidebar/sidebarSettingsMenu.tsx +++ b/webapp/src/components/sidebar/sidebarSettingsMenu.tsx @@ -30,7 +30,7 @@ type Props = { activeTheme: string } -const SidebarSettingsMenu = React.memo((props: Props) => { +const SidebarSettingsMenu = (props: Props) => { const intl = useIntl() const dispatch = useAppDispatch() @@ -164,6 +164,6 @@ const SidebarSettingsMenu = React.memo((props: Props) => {
) -}) +} -export default SidebarSettingsMenu +export default React.memo(SidebarSettingsMenu) diff --git a/webapp/src/components/sidebar/sidebarUserMenu.tsx b/webapp/src/components/sidebar/sidebarUserMenu.tsx index 9dfe31da2..be031600f 100644 --- a/webapp/src/components/sidebar/sidebarUserMenu.tsx +++ b/webapp/src/components/sidebar/sidebarUserMenu.tsx @@ -25,7 +25,7 @@ import './sidebarUserMenu.scss' declare let window: IAppWindow -const SidebarUserMenu = React.memo(() => { +const SidebarUserMenu = () => { const history = useHistory() const [showRegistrationLinkDialog, setShowRegistrationLinkDialog] = useState(false) const user = useAppSelector(getMe) @@ -106,6 +106,6 @@ const SidebarUserMenu = React.memo(() => {
) -}) +} -export default SidebarUserMenu +export default React.memo(SidebarUserMenu) diff --git a/webapp/src/components/table/calculation/calculationRow.tsx b/webapp/src/components/table/calculation/calculationRow.tsx index 17b9ab655..712cdc3c7 100644 --- a/webapp/src/components/table/calculation/calculationRow.tsx +++ b/webapp/src/components/table/calculation/calculationRow.tsx @@ -39,7 +39,7 @@ const CalculationRow = (props: Props): JSX.Element => { const templates: IPropertyTemplate[] = [ titleTemplate, - ...props.board.fields.cardProperties.filter((template) => props.activeView.fields.visiblePropertyIds.includes(template.id)), + ...props.activeView.fields.visiblePropertyIds.map((id) => props.board.fields.cardProperties.find((t) => t.id === id)).filter((i) => i) as IPropertyTemplate[], ] const selectedCalculations = props.board.fields.columnCalculations || [] diff --git a/webapp/src/components/table/horizontalGrip.tsx b/webapp/src/components/table/horizontalGrip.tsx index 67e3ea1ee..8c5dd9a9f 100644 --- a/webapp/src/components/table/horizontalGrip.tsx +++ b/webapp/src/components/table/horizontalGrip.tsx @@ -10,7 +10,7 @@ type Props = { onAutoSizeColumn: (columnID: string) => void; } -const HorizontalGrip = React.memo((props: Props): JSX.Element => { +const HorizontalGrip = (props: Props): JSX.Element => { const [, drag] = useDrag(() => ({ type: 'horizontalGrip', item: {id: props.templateId}, @@ -23,6 +23,6 @@ const HorizontalGrip = React.memo((props: Props): JSX.Element => { onDoubleClick={() => props.onAutoSizeColumn(props.templateId)} /> ) -}) +} -export default HorizontalGrip +export default React.memo(HorizontalGrip) diff --git a/webapp/src/components/table/tableGroup.tsx b/webapp/src/components/table/tableGroup.tsx index 6c9b5845b..90453fb74 100644 --- a/webapp/src/components/table/tableGroup.tsx +++ b/webapp/src/components/table/tableGroup.tsx @@ -31,7 +31,7 @@ type Props = { onDropToGroup: (srcCard: Card, groupID: string, dstCardID: string) => void } -const TableGroup = React.memo((props: Props): JSX.Element => { +const TableGroup = (props: Props): JSX.Element => { const {board, activeView, group, onDropToGroup, groupByProperty} = props const groupId = group.option.id @@ -86,6 +86,6 @@ const TableGroup = React.memo((props: Props): JSX.Element => { />}
) -}) +} -export default TableGroup +export default React.memo(TableGroup) diff --git a/webapp/src/components/table/tableGroupHeaderRow.tsx b/webapp/src/components/table/tableGroupHeaderRow.tsx index 715a9ed0d..ced304689 100644 --- a/webapp/src/components/table/tableGroupHeaderRow.tsx +++ b/webapp/src/components/table/tableGroupHeaderRow.tsx @@ -33,7 +33,7 @@ type Props = { onDrop: (srcOption: IPropertyOption, dstOption?: IPropertyOption) => void } -const TableGroupHeaderRow = React.memo((props: Props): JSX.Element => { +const TableGroupHeaderRow = (props: Props): JSX.Element => { const {board, activeView, group, groupByProperty} = props const [groupTitle, setGroupTitle] = useState(group.option.value) @@ -147,6 +147,6 @@ const TableGroupHeaderRow = React.memo((props: Props): JSX.Element => { }
) -}) +} -export default TableGroupHeaderRow +export default React.memo(TableGroupHeaderRow) diff --git a/webapp/src/components/table/tableHeader.tsx b/webapp/src/components/table/tableHeader.tsx index 6eee6fbd0..b7bc32b45 100644 --- a/webapp/src/components/table/tableHeader.tsx +++ b/webapp/src/components/table/tableHeader.tsx @@ -32,7 +32,7 @@ type Props = { onAutoSizeColumn: (columnID: string, headerWidth: number) => void } -const TableHeader = React.memo((props: Props): JSX.Element => { +const TableHeader = (props: Props): JSX.Element => { const [isDragging, isOver, columnRef] = useSortable('column', props.template, !props.readonly, props.onDrop) const columnWidth = (templateId: string): number => { @@ -85,6 +85,6 @@ const TableHeader = React.memo((props: Props): JSX.Element => { }
) -}) +} -export default TableHeader +export default React.memo(TableHeader) diff --git a/webapp/src/components/table/tableRow.tsx b/webapp/src/components/table/tableRow.tsx index 1e4cab5dd..5e0b8510b 100644 --- a/webapp/src/components/table/tableRow.tsx +++ b/webapp/src/components/table/tableRow.tsx @@ -43,7 +43,7 @@ export const columnWidth = (resizingColumn: string, columnWidths: Record { +const TableRow = (props: Props) => { const {board, activeView, onSaveWithEnter, columnRefs, card} = props const contents = useAppSelector(getCardContents(card.id || '')) const comments = useAppSelector(getCardComments(card.id)) @@ -150,6 +150,6 @@ const TableRow = React.memo((props: Props) => { })}
) -}) +} -export default TableRow +export default React.memo(TableRow) diff --git a/webapp/src/components/topBar.tsx b/webapp/src/components/topBar.tsx index 9e6f1b8ee..22d4b1ce2 100644 --- a/webapp/src/components/topBar.tsx +++ b/webapp/src/components/topBar.tsx @@ -10,7 +10,7 @@ import HelpIcon from '../widgets/icons/help' import {Utils} from '../utils' import {Constants} from '../constants' -const TopBar = React.memo((): JSX.Element => { +const TopBar = (): JSX.Element => { if (Utils.isFocalboardPlugin()) { const feedbackUrl = 'https://www.focalboard.com/fwlink/feedback-boards.html?v=' + Constants.versionString return ( @@ -65,6 +65,6 @@ const TopBar = React.memo((): JSX.Element => {
) -}) +} -export default TopBar +export default React.memo(TopBar) diff --git a/webapp/src/components/viewHeader/__snapshots__/filterValue.test.tsx.snap b/webapp/src/components/viewHeader/__snapshots__/filterValue.test.tsx.snap index 8c81b25aa..93b6731c1 100644 --- a/webapp/src/components/viewHeader/__snapshots__/filterValue.test.tsx.snap +++ b/webapp/src/components/viewHeader/__snapshots__/filterValue.test.tsx.snap @@ -38,7 +38,7 @@ exports[`components/viewHeader/filterValue return filterValue 1`] = ` Status
+
+ +
+
+`; + +exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call to board archive 1`] = ` +
+
+