1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-03-03 15:32:14 +02:00

Merge remote-tracking branch 'origin/main' into main

This commit is contained in:
Hosted Weblate 2022-02-15 09:15:43 +01:00
commit 716551bc7c
136 changed files with 4285 additions and 2951 deletions

View File

@ -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

View File

@ -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

View File

@ -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)
- **<a name="scott.bishel">Scott Bishel</a>**
- @scott.bishel on [community.mattermost.com](https://community.mattermost.com/core/messages/@scott.bishel) and [@sbishel](https://github.com/sbishel) on GitHub
- **<a name="jesús.espino">Jesús Espino</a>**
- @jesus.espino on [community.mattermost.com](https://community.mattermost.com/core/messages/@jesus.espino) and [@jespino](https://github.com/jespino) on GitHub
- **<a name="doug.lauder">Doug Lauder</a>**
- @doug.lauder on [community.mattermost.com](https://community.mattermost.com/core/messages/@doug.lauder) and [@wiggin77](https://github.com/wiggin77) on GitHub
- **<a name="miguel.delacruz">Miguel de la Cruz</a>**
- @miguel.delacruz on [community.mattermost.com](https://community.mattermost.com/core/messages/@miguel.delacruz) and [@mgdelacroix](https://github.com/mgdelacroix) on GitHub
- **<a name="harshil.sharma">Harshil Sharma</a>**
- @harshil.sharma on [community.mattermost.com](https://community.mattermost.com/core/messages/@harshil.sharma) and [@harshilsharma63](https://github.com/harshilsharma63) on GitHub
- **<a name="chen.lim">Chen Lim</a>**
- @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.
- **<a name="ogi.marusic">Ogi Marušić</a>**
- @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)
- **<a name="winson.wu">Winson Wu</a>**
- @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)
- **<a name="justine.geffen">Justine Geffen</a>**
- @justine.geffen on [community.mattermost.com](https://community.mattermost.com/core/messages/@justine.geffen) and [@justinegeffen ](https://github.com/justinegeffen) on GitHub

View File

@ -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`

3
docs/_config.yml Normal file
View File

@ -0,0 +1,3 @@
title: Focalboard Developers
google_analytics: UA-64458817-2
theme: jekyll-theme-architect

View File

@ -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).
<!-- * Documentation changes must be reviewed by a [product manager](/contribute/getting-started/core-committers/#product-managers).
* Product managers may ask for reviews from [core committers](/contribute/getting-started/core-committers/#core-committers) and [QA testers](/contribute/getting-started/core-committers/#qa-testers) as required. -->
## 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.

View File

@ -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!
<!-- Come join our [Contributors community channel](https://community.mattermost.com/core/channels/tickets) on the community server, where you can discuss questions with community members and the Focalboard core team. -->
<!-- To help with translations, [see the localization process](https://docs.mattermost.com/developer/localization.html). -->
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.
<!-- That's all! If you have any feedback about this checklist, let us know in the [Contributors channel](https://community.mattermost.com/core/channels/tickets). -->
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.

View File

@ -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.

20
docs/core-committers.md Normal file
View File

@ -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:
- **<a name="scott.bishel">Scott Bishel</a>**
- @scott.bishel on [community.mattermost.com](https://community.mattermost.com/core/messages/@scott.bishel) and [@sbishel](https://github.com/sbishel) on GitHub
- **<a name="jesús.espino">Jesús Espino</a>**
- @jesus.espino on [community.mattermost.com](https://community.mattermost.com/core/messages/@jesus.espino) and [@jespino](https://github.com/jespino) on GitHub
- **<a name="doug.lauder">Doug Lauder</a>**
- @doug.lauder on [community.mattermost.com](https://community.mattermost.com/core/messages/@doug.lauder) and [@wiggin77](https://github.com/wiggin77) on GitHub
- **<a name="miguel.delacruz">Miguel de la Cruz</a>**
- @miguel.delacruz on [community.mattermost.com](https://community.mattermost.com/core/messages/@miguel.delacruz) and [@mgdelacroix](https://github.com/mgdelacroix) on GitHub
- **<a name="harshil.sharma">Harshil Sharma</a>**
- @harshil.sharma on [community.mattermost.com](https://community.mattermost.com/core/messages/@harshil.sharma) and [@harshilsharma63](https://github.com/harshilsharma63) on GitHub
- **<a name="chen.lim">Chen Lim</a>**
- @chen-i.lim on [community.mattermost.com](https://community.mattermost.com/core/messages/@chen-i.lim) and [@chenilim](https://github.com/chenilim) on GitHub

View File

@ -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?

19
docs/index.md Normal file
View File

@ -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!

View File

@ -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.

View File

@ -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.

View File

@ -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'

View File

@ -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.

View File

@ -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'

View File

@ -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.

View File

@ -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.

View File

@ -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'

View File

@ -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'

View File

@ -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.

View File

@ -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'

81
import/util/archive.ts Normal file
View File

@ -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}

View File

@ -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 = "<group>"; };
804E57FB27441B6B008526F0 /* whatsnew.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = whatsnew.txt; sourceTree = "<group>"; };
80672A8A27BAFEBA00257B8C /* DownloadHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadHandler.swift; sourceTree = "<group>"; };
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 = "<group>"; };
80D6DEBC252E13CB00AEED9E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
@ -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;

View File

@ -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")
}
}

View File

@ -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!) {

View File

@ -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)

View File

@ -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.

View File

@ -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",

View File

@ -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<string>(getLanguage)
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
const globalError = useAppSelector<string>(getGlobalError)
@ -277,6 +277,6 @@ const App = React.memo((): JSX.Element => {
</DndProvider>
</IntlProvider>
)
})
}
export default App
export default React.memo(App)

View File

@ -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,
},

File diff suppressed because it is too large Load Diff

View File

@ -1,601 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/components/shareBoardComponent return shareBoardComponent and click Copy link 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<button
type="button"
>
<span>
Copied!
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoardComponent return shareBoardComponent and click Regenerate token 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<button
type="button"
>
<span>
Copy link
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoardComponent return shareBoardComponent and click Switch 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<button
type="button"
>
<span>
Copy link
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoardComponent return shareBoardComponent and click Switch without sharing 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=aToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=aToken
</a>
<button
type="button"
>
<span>
Copy link
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoardComponent should match snapshot 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Publish and share this board with anyone who has the link
</div>
<div
class="Switch"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoardComponent should match snapshot with sharing 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<button
type="button"
>
<span>
Copy link
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoardComponent should match snapshot with sharing and subpath 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/test-subpath/plugins/boards/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/test-subpath/plugins/boards/workspace/1/shared/1/1?r=oneToken
</a>
<button
type="button"
>
<span>
Copy link
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoardComponent should match snapshot with sharing and without workspaceId 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/shared/1/1?r=oneToken
</a>
<button
type="button"
>
<span>
Copy link
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoardComponent should match snapshot with sharing and without workspaceId and subpath 1`] = `
<div>
<div
class="Modal bottom"
>
<div
class="toolbar hideOnWidescreen"
>
<button
aria-label="Close"
title="Close"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="ShareBoardComponent"
>
<div
class="row"
>
<div>
Anyone with the link can view this board and all cards in it.
</div>
<div
class="Switch on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
<div
class="row"
>
<a
class="shareUrl"
href="http://localhost/test-subpath/plugins/boards/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/test-subpath/plugins/boards/shared/1/1?r=oneToken
</a>
<button
type="button"
>
<span>
Copy link
</span>
</button>
</div>
<div
class="row"
>
<button
type="button"
>
<span>
Regenerate token
</span>
</button>
</div>
</div>
</div>
</div>
`;

View File

@ -250,106 +250,110 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
</a>
</div>
<div
class="ViewTitle"
class="mid-head"
>
<div
class="add-buttons add-visible"
>
<button
type="button"
>
<svg
class="HideIcon Icon"
viewBox="0 0 640 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z"
/>
</svg>
<span>
hide description
</span>
</button>
</div>
<div
class="title"
class="ViewTitle"
>
<div
class="BlockIconSelector"
class="add-buttons add-visible"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
<button
type="button"
>
<div
class="octo-icon size-m"
<svg
class="HideIcon Icon"
viewBox="0 0 640 512"
xmlns="http://www.w3.org/2000/svg"
>
<span>
i
</span>
</div>
</div>
<path
d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z"
/>
</svg>
<span>
hide description
</span>
</button>
</div>
<input
class="Editable title"
placeholder="Untitled board"
spellcheck="true"
title="board title"
value="board title"
/>
</div>
<div
class="description"
>
<div
class="MarkdownEditor octo-editor "
class="title"
>
<div
class="octo-editor-preview"
data-testid="preview-element"
/>
<div
class="MarkdownEditorInput MarkdownEditorInput--IsNotEditing"
class="BlockIconSelector"
>
<div
class="DraftEditor-root"
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="DraftEditor-editorContainer"
class="octo-icon size-m"
>
<span>
i
</span>
</div>
</div>
</div>
<input
class="Editable title"
placeholder="Untitled board"
spellcheck="true"
title="board title"
value="board title"
/>
</div>
<div
class="description"
>
<div
class="MarkdownEditor octo-editor "
>
<div
class="octo-editor-preview"
data-testid="preview-element"
/>
<div
class="MarkdownEditorInput MarkdownEditorInput--IsNotEditing"
>
<div
class="DraftEditor-root"
>
<div
aria-autocomplete="list"
aria-expanded="false"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="combobox"
spellcheck="false"
style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
class="DraftEditor-editorContainer"
>
<div
data-contents="true"
aria-autocomplete="list"
aria-expanded="false"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="combobox"
spellcheck="false"
style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
>
<div
class=""
data-block="true"
data-editor="123"
data-offset-key="123-0-0"
data-contents="true"
>
<div
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
class=""
data-block="true"
data-editor="123"
data-offset-key="123-0-0"
>
<span
<div
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
data-offset-key="123-0-0"
>
<span
data-text="true"
data-offset-key="123-0-0"
>
description
<span
data-text="true"
>
description
</span>
</span>
</span>
</div>
</div>
</div>
</div>
@ -359,6 +363,21 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
</div>
</div>
</div>
<div
class="ShareBoardButton"
>
<button
title="Share board"
type="button"
>
<span>
<i
class="CompassIcon icon-globe CompassIcon"
/>
Share
</span>
</button>
</div>
</div>
<div
class="ViewHeader"
@ -807,90 +826,94 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
</a>
</div>
<div
class="ViewTitle"
class="mid-head"
>
<div
class="add-buttons add-visible"
/>
<div
class="title"
class="ViewTitle"
>
<div
class="BlockIconSelector"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-icon size-m"
>
<span>
i
</span>
</div>
</div>
</div>
<input
class="Editable readonly title"
placeholder="Untitled board"
readonly=""
spellcheck="true"
title="board title"
value="board title"
class="add-buttons add-visible"
/>
</div>
<div
class="description"
>
<div
class="MarkdownEditor octo-editor "
class="title"
>
<div
class="octo-editor-preview"
data-testid="preview-element"
/>
<div
class="MarkdownEditorInput MarkdownEditorInput--IsNotEditing"
class="BlockIconSelector"
>
<div
class="DraftEditor-root"
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="DraftEditor-editorContainer"
class="octo-icon size-m"
>
<span>
i
</span>
</div>
</div>
</div>
<input
class="Editable readonly title"
placeholder="Untitled board"
readonly=""
spellcheck="true"
title="board title"
value="board title"
/>
</div>
<div
class="description"
>
<div
class="MarkdownEditor octo-editor "
>
<div
class="octo-editor-preview"
data-testid="preview-element"
/>
<div
class="MarkdownEditorInput MarkdownEditorInput--IsNotEditing"
>
<div
class="DraftEditor-root"
>
<div
aria-autocomplete="list"
aria-expanded="false"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="combobox"
spellcheck="false"
style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
class="DraftEditor-editorContainer"
>
<div
data-contents="true"
aria-autocomplete="list"
aria-expanded="false"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="combobox"
spellcheck="false"
style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
>
<div
class=""
data-block="true"
data-editor="123"
data-offset-key="123-0-0"
data-contents="true"
>
<div
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
class=""
data-block="true"
data-editor="123"
data-offset-key="123-0-0"
>
<span
<div
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
data-offset-key="123-0-0"
>
<span
data-text="true"
data-offset-key="123-0-0"
>
description
<span
data-text="true"
>
description
</span>
</span>
</span>
</div>
</div>
</div>
</div>
@ -1451,106 +1474,110 @@ exports[`src/components/workspace should match snapshot 1`] = `
</a>
</div>
<div
class="ViewTitle"
class="mid-head"
>
<div
class="add-buttons add-visible"
>
<button
type="button"
>
<svg
class="HideIcon Icon"
viewBox="0 0 640 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z"
/>
</svg>
<span>
hide description
</span>
</button>
</div>
<div
class="title"
class="ViewTitle"
>
<div
class="BlockIconSelector"
class="add-buttons add-visible"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
<button
type="button"
>
<div
class="octo-icon size-m"
<svg
class="HideIcon Icon"
viewBox="0 0 640 512"
xmlns="http://www.w3.org/2000/svg"
>
<span>
i
</span>
</div>
</div>
<path
d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z"
/>
</svg>
<span>
hide description
</span>
</button>
</div>
<input
class="Editable title"
placeholder="Untitled board"
spellcheck="true"
title="board title"
value="board title"
/>
</div>
<div
class="description"
>
<div
class="MarkdownEditor octo-editor "
class="title"
>
<div
class="octo-editor-preview"
data-testid="preview-element"
/>
<div
class="MarkdownEditorInput MarkdownEditorInput--IsNotEditing"
class="BlockIconSelector"
>
<div
class="DraftEditor-root"
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="DraftEditor-editorContainer"
class="octo-icon size-m"
>
<span>
i
</span>
</div>
</div>
</div>
<input
class="Editable title"
placeholder="Untitled board"
spellcheck="true"
title="board title"
value="board title"
/>
</div>
<div
class="description"
>
<div
class="MarkdownEditor octo-editor "
>
<div
class="octo-editor-preview"
data-testid="preview-element"
/>
<div
class="MarkdownEditorInput MarkdownEditorInput--IsNotEditing"
>
<div
class="DraftEditor-root"
>
<div
aria-autocomplete="list"
aria-expanded="false"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="combobox"
spellcheck="false"
style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
class="DraftEditor-editorContainer"
>
<div
data-contents="true"
aria-autocomplete="list"
aria-expanded="false"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="combobox"
spellcheck="false"
style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
>
<div
class=""
data-block="true"
data-editor="123"
data-offset-key="123-0-0"
data-contents="true"
>
<div
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
class=""
data-block="true"
data-editor="123"
data-offset-key="123-0-0"
>
<span
<div
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
data-offset-key="123-0-0"
>
<span
data-text="true"
data-offset-key="123-0-0"
>
description
<span
data-text="true"
>
description
</span>
</span>
</span>
</div>
</div>
</div>
</div>
@ -1560,6 +1587,21 @@ exports[`src/components/workspace should match snapshot 1`] = `
</div>
</div>
</div>
<div
class="ShareBoardButton"
>
<button
title="Share board"
type="button"
>
<span>
<i
class="CompassIcon icon-globe CompassIcon"
/>
Share
</span>
</button>
</div>
</div>
<div
class="ViewHeader"
@ -2008,90 +2050,94 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
</a>
</div>
<div
class="ViewTitle"
class="mid-head"
>
<div
class="add-buttons add-visible"
/>
<div
class="title"
class="ViewTitle"
>
<div
class="BlockIconSelector"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-icon size-m"
>
<span>
i
</span>
</div>
</div>
</div>
<input
class="Editable readonly title"
placeholder="Untitled board"
readonly=""
spellcheck="true"
title="board title"
value="board title"
class="add-buttons add-visible"
/>
</div>
<div
class="description"
>
<div
class="MarkdownEditor octo-editor "
class="title"
>
<div
class="octo-editor-preview"
data-testid="preview-element"
/>
<div
class="MarkdownEditorInput MarkdownEditorInput--IsNotEditing"
class="BlockIconSelector"
>
<div
class="DraftEditor-root"
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="DraftEditor-editorContainer"
class="octo-icon size-m"
>
<span>
i
</span>
</div>
</div>
</div>
<input
class="Editable readonly title"
placeholder="Untitled board"
readonly=""
spellcheck="true"
title="board title"
value="board title"
/>
</div>
<div
class="description"
>
<div
class="MarkdownEditor octo-editor "
>
<div
class="octo-editor-preview"
data-testid="preview-element"
/>
<div
class="MarkdownEditorInput MarkdownEditorInput--IsNotEditing"
>
<div
class="DraftEditor-root"
>
<div
aria-autocomplete="list"
aria-expanded="false"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="combobox"
spellcheck="false"
style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
class="DraftEditor-editorContainer"
>
<div
data-contents="true"
aria-autocomplete="list"
aria-expanded="false"
class="notranslate public-DraftEditor-content"
contenteditable="true"
role="combobox"
spellcheck="false"
style="outline: none; user-select: text; white-space: pre-wrap; word-wrap: break-word;"
>
<div
class=""
data-block="true"
data-editor="123"
data-offset-key="123-0-0"
data-contents="true"
>
<div
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
class=""
data-block="true"
data-editor="123"
data-offset-key="123-0-0"
>
<span
<div
class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"
data-offset-key="123-0-0"
>
<span
data-text="true"
data-offset-key="123-0-0"
>
description
<span
data-text="true"
>
description
</span>
</span>
</span>
</div>
</div>
</div>
</div>

View File

@ -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)

View File

@ -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) => {
}
</div>
)
})
}
export default BlockIconSelector
export default React.memo(BlockIconSelector)

View File

@ -110,11 +110,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plu
>
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
</div>
/>
<div
class="buttons"
>
@ -239,11 +235,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plu
>
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
</div>
/>
<div
class="buttons"
>
@ -368,11 +360,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plu
>
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
</div>
/>
<div
class="buttons"
>
@ -507,11 +495,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector not a focalboard
>
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
</div>
/>
<div
class="buttons"
>

View File

@ -7,9 +7,6 @@ exports[`components/boardTemplateSelector/boardTemplateSelectorPreview should ma
<div
class="BoardTemplateSelectorPreview"
>
<div
class="prevent-click"
/>
<div
class="top-head"
>

View File

@ -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;

View File

@ -100,6 +100,8 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
{id: 'global-id-5'},
],
dateDisplayPropertyId: 'global-id-5',
isTemplate: true,
templateVer: 2,
},
}],
},

View File

@ -27,7 +27,7 @@ type Props = {
onClose?: () => void
}
const BoardTemplateSelector = React.memo((props: Props) => {
const BoardTemplateSelector = (props: Props) => {
const globalTemplates = useAppSelector<Board[]>(getGlobalTemplates) || []
const currentBoard = useAppSelector<Board>(getCurrentBoard) || null
const {title, description, onClose} = props
@ -166,7 +166,6 @@ const BoardTemplateSelector = React.memo((props: Props) => {
</div>
</div>
)
})
export default BoardTemplateSelector
}
export default React.memo(BoardTemplateSelector)

View File

@ -40,6 +40,10 @@
position: relative;
display: none;
right: -8px;
.DeleteIcon {
padding-top: 3px;
}
}
}

View File

@ -127,6 +127,8 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
cardProperties: [groupProperty],
dateDisplayPropertyId: 'global-id-5',
columnCalculations: {},
isTemplate: true,
templateVer: 2,
},
}

View File

@ -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<boolean>(false)
@ -38,7 +38,7 @@ const BoardTemplateSelectorItem = React.memo((props: Props) => {
>
<span className='template-icon'>{template.fields.icon}</span>
<span className='template-name'>{template.title}</span>
{template.workspaceId !== '0' &&
{!template.fields.templateVer &&
<div className='actions'>
<IconButton
icon={<DeleteIcon/>}
@ -65,6 +65,6 @@ const BoardTemplateSelectorItem = React.memo((props: Props) => {
/>}
</div>
)
})
}
export default BoardTemplateSelectorItem
export default React.memo(BoardTemplateSelectorItem)

View File

@ -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;
}
}

View File

@ -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<BoardView|null>(null)
const [activeTemplateCards, setActiveTemplateCards] = useState<Card[]>([])
@ -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 (
<div className='BoardTemplateSelectorPreview'>
<div className='prevent-click'/>
{activeView &&
<div className='top-head'>
<ViewTitle
key={activeTemplate?.id + activeTemplate?.title}
board={activeTemplate}
readonly={true}
/>
<ViewHeader
board={activeTemplate}
activeView={activeView}
cards={activeTemplateCards}
views={[activeView]}
groupByProperty={groupByProperty}
addCard={() => null}
addCardFromTemplate={() => null}
addCardTemplate={() => null}
editCardTemplate={() => null}
readonly={false}
showShared={false}
/>
</div>}
<div className='top-head'>
<ViewTitle
key={activeTemplate?.id + activeTemplate?.title}
board={activeTemplate}
readonly={true}
/>
<ViewHeader
board={activeTemplate}
activeView={activeView}
cards={activeTemplateCards}
views={[activeView]}
groupByProperty={groupByProperty}
addCard={() => null}
addCardFromTemplate={() => null}
addCardTemplate={() => null}
editCardTemplate={() => null}
readonly={false}
/>
</div>}
{activeView?.fields.viewType === 'board' &&
<Kanban
@ -103,43 +101,43 @@ const BoardTemplateSelectorPreview = React.memo((props: Props) => {
showCard={() => null}
/>}
{activeView?.fields.viewType === 'table' &&
<Table
board={activeTemplate}
activeView={activeView}
cards={activeTemplateCards}
groupByProperty={groupByProperty}
views={[activeView]}
visibleGroups={visibleGroups}
selectedCardIds={[]}
readonly={false}
cardIdToFocusOnRender={''}
onCardClicked={() => null}
addCard={() => Promise.resolve()}
showCard={() => null}
/>}
<Table
board={activeTemplate}
activeView={activeView}
cards={activeTemplateCards}
groupByProperty={groupByProperty}
views={[activeView]}
visibleGroups={visibleGroups}
selectedCardIds={[]}
readonly={false}
cardIdToFocusOnRender={''}
onCardClicked={() => null}
addCard={() => Promise.resolve()}
showCard={() => null}
/>}
{activeView?.fields.viewType === 'gallery' &&
<Gallery
board={activeTemplate}
cards={activeTemplateCards}
activeView={activeView}
readonly={false}
selectedCardIds={[]}
onCardClicked={() => null}
addCard={() => Promise.resolve()}
/>}
<Gallery
board={activeTemplate}
cards={activeTemplateCards}
activeView={activeView}
readonly={false}
selectedCardIds={[]}
onCardClicked={() => null}
addCard={() => Promise.resolve()}
/>}
{activeView?.fields.viewType === 'calendar' &&
<CalendarFullView
board={activeTemplate}
cards={activeTemplateCards}
activeView={activeView}
readonly={false}
dateDisplayProperty={dateDisplayProperty}
showCard={() => null}
addCard={() => Promise.resolve()}
/>}
<CalendarFullView
board={activeTemplate}
cards={activeTemplateCards}
activeView={activeView}
readonly={false}
dateDisplayProperty={dateDisplayProperty}
showCard={() => null}
addCard={() => Promise.resolve()}
/>}
</div>
)
})
}
export default BoardTemplateSelectorPreview
export default React.memo(BoardTemplateSelectorPreview)

View File

@ -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) => {
</div>
</div>
)
})
}
export default CardDetailContents
export default React.memo(CardDetailContents)

View File

@ -37,7 +37,7 @@ function addContentMenu(intl: IntlShape, type: BlockTypes): JSX.Element {
)
}
const CardDetailContentsMenu = React.memo(() => {
const CardDetailContentsMenu = () => {
const intl = useIntl()
return (
<div className='CardDetailContentsMenu content add-content'>
@ -54,6 +54,6 @@ const CardDetailContentsMenu = React.memo(() => {
</MenuWrapper>
</div>
)
})
}
export default CardDetailContentsMenu
export default React.memo(CardDetailContentsMenu)

View File

@ -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) => {
}
</div>
)
})
}
export default CardDetailProperties
export default React.memo(CardDetailProperties)

View File

@ -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<IUser|null>(getMe)
@ -97,6 +97,6 @@ const CommentsList = React.memo((props: Props) => {
{!(comments.length === 0 && props.readonly) && <hr className='CommentsList__divider'/>}
</div>
)
})
}
export default CommentsList
export default React.memo(CommentsList)

View File

@ -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;

View File

@ -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(
<ReduxProvider store={store}>
<CenterPanel
cards={[card1]}
views={[activeView]}
board={board}
activeView={activeView}
readonly={false}
showCard={jest.fn()}
showShared={false}
groupByProperty={groupProperty}
shownCardId={card1.id}
/>
</ReduxProvider>,
))
expect(container).toMatchSnapshot()
})
test('should match snapshot for Kanban', () => {
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>

View File

@ -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<Props, State> {
@ -102,6 +104,7 @@ class CenterPanel extends React.Component<Props, State> {
this.state = {
selectedCardIds: [],
cardIdToFocusOnRender: '',
showShareDialog: false,
}
}
@ -146,11 +149,18 @@ class CenterPanel extends React.Component<Props, State> {
<div className='top-head'>
<TopBar/>
<ViewTitle
key={board.id + board.title}
board={board}
readonly={this.props.readonly}
/>
<div className='mid-head'>
<ViewTitle
key={board.id + board.title}
board={board}
readonly={this.props.readonly}
/>
{!this.props.readonly && this.props.showShared &&
<ShareBoardButton
boardId={this.props.board.id}
/>
}
</div>
<ViewHeader
board={this.props.board}
activeView={this.props.activeView}
@ -163,7 +173,6 @@ class CenterPanel extends React.Component<Props, State> {
addCardTemplate={this.addCardTemplate}
editCardTemplate={this.editCardTemplate}
readonly={this.props.readonly}
showShared={this.props.showShared}
/>
</div>

View File

@ -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<Focusable>(null)
@ -83,7 +83,7 @@ const CheckboxElement = React.memo((props: Props) => {
/>
</div>
)
})
}
contentRegistry.registerContentType({
type: 'checkbox',
@ -104,4 +104,4 @@ contentRegistry.registerContentType({
},
})
export default CheckboxElement
export default React.memo(CheckboxElement)

View File

@ -8,7 +8,7 @@ import DividerIcon from '../../widgets/icons/divider'
import {contentRegistry} from './contentRegistry'
import './dividerElement.scss'
const DividerElement = React.memo((): JSX.Element => <div className='DividerElement'/>)
const DividerElement = (): JSX.Element => <div className='DividerElement'/>
contentRegistry.registerContentType({
type: 'divider',
@ -20,4 +20,4 @@ contentRegistry.registerContentType({
createComponent: () => <DividerElement/>,
})
export default DividerElement
export default React.memo(DividerElement)

View File

@ -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<string|null>(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) => <ImageElement block={block}/>,
})
export default ImageElement
export default React.memo(ImageElement)

View File

@ -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)

View File

@ -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 => {
/>
</div>
)
})
}
export default ContentBlock
export default React.memo(ContentBlock)

View File

@ -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'
>
<div className='toolbar'>
{title && <h1 className='text-heading5 mt-2'>{title}</h1>}
{
!props.hideCloseButton &&
<IconButton
@ -71,6 +73,6 @@ const Dialog = React.memo((props: Props) => {
</div>
</div>
)
})
}
export default Dialog
export default React.memo(Dialog)

View File

@ -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) => {
/>}
</div>
)
})
}
export default GalleryCard
export default React.memo(GalleryCard)

View File

@ -239,7 +239,7 @@ exports[`components/sidebar/GlobalHeaderSettingsMenu imports menu open should ma
Random icons
</div>
<div
class="Switch on"
class="Switch size--small on"
>
<div
class="octo-switch-inner"
@ -710,7 +710,7 @@ exports[`components/sidebar/GlobalHeaderSettingsMenu languages menu open should
Random icons
</div>
<div
class="Switch on"
class="Switch size--small on"
>
<div
class="octo-switch-inner"
@ -872,7 +872,7 @@ exports[`components/sidebar/GlobalHeaderSettingsMenu settings menu open should m
Random icons
</div>
<div
class="Switch on"
class="Switch size--small on"
>
<div
class="octo-switch-inner"

View File

@ -17,7 +17,7 @@ import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../teleme
import './globalHeaderSettingsMenu.scss'
const GlobalHeaderSettingsMenu = React.memo(() => {
const GlobalHeaderSettingsMenu = () => {
const intl = useIntl()
const dispatch = useAppDispatch()
@ -96,6 +96,6 @@ const GlobalHeaderSettingsMenu = React.memo(() => {
</MenuWrapper>
</div>
)
})
}
export default GlobalHeaderSettingsMenu
export default React.memo(GlobalHeaderSettingsMenu)

View File

@ -85,9 +85,14 @@
.octo-board-hidden-item {
display: flex;
flex-direction: row;
align-items: center;
> div {
margin-right: 5px;
}
.Label {
margin: 5px;
}
}
}

View File

@ -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)

View File

@ -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}
</div>
)
})
}
export default KanbanColumn
export default React.memo(KanbanColumn)

View File

@ -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<HTMLDivElement>(null)
const {position, onClose, children} = props
@ -47,6 +47,6 @@ const Modal = React.memo((props: Props): JSX.Element => {
{children}
</div>
)
})
}
export default Modal
export default React.memo(Modal)

View File

@ -7,12 +7,12 @@ type Props = {
children: React.ReactNode
}
const ModalWrapper = React.memo((props: Props) => {
const ModalWrapper = (props: Props) => {
return (
<div className='ModalWrapper'>
{props.children}
</div>
)
})
}
export default ModalWrapper
export default React.memo(ModalWrapper)

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,950 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<h1
class="text-heading5 mt-2"
>
Share Board
</h1>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="tabs-modal"
>
<div>
<div
class="d-flex justify-content-between"
>
<div
class="d-flex flex-column"
>
<div
class="text-heading2"
>
Publish to the web
</div>
<div
class="text-light"
>
Publish and share a “read only” link with everyone on the web
</div>
</div>
<div>
<div
class="Switch size--medium on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
<div
class="d-flex justify-content-between tabs-inputs"
>
<div
class="d-flex input-container"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<div
class="octo-tooltip tooltip-top"
data-tooltip="Regenerate token"
>
<button
aria-label="Regenerate token"
title="Regenerate token"
type="button"
>
<i
class="CompassIcon icon-refresh Icon Icon--right"
/>
</button>
</div>
</div>
<button
title="Copy link"
type="button"
>
<span>
<i
class="CompassIcon icon-content-copy CompassIcon"
/>
Copy link
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 2`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<h1
class="text-heading5 mt-2"
>
Share Board
</h1>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="tabs-modal"
>
<div>
<div
class="d-flex justify-content-between"
>
<div
class="d-flex flex-column"
>
<div
class="text-heading2"
>
Publish to the web
</div>
<div
class="text-light"
>
Publish and share a “read only” link with everyone on the web
</div>
</div>
<div>
<div
class="Switch size--medium on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
<div
class="d-flex justify-content-between tabs-inputs"
>
<div
class="d-flex input-container"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<div
class="octo-tooltip tooltip-top"
data-tooltip="Regenerate token"
>
<button
aria-label="Regenerate token"
title="Regenerate token"
type="button"
>
<i
class="CompassIcon icon-refresh Icon Icon--right"
/>
</button>
</div>
</div>
<button
title="Copy link"
type="button"
>
<span>
<i
class="CompassIcon icon-content-copy CompassIcon"
/>
Copied!
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/shareBoard return shareBoard and click Regenerate token 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<h1
class="text-heading5 mt-2"
>
Share Board
</h1>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="tabs-modal"
>
<div>
<div
class="d-flex justify-content-between"
>
<div
class="d-flex flex-column"
>
<div
class="text-heading2"
>
Publish to the web
</div>
<div
class="text-light"
>
Publish and share a “read only” link with everyone on the web
</div>
</div>
<div>
<div
class="Switch size--medium on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
<div
class="d-flex justify-content-between tabs-inputs"
>
<div
class="d-flex input-container"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<div
class="octo-tooltip tooltip-top"
data-tooltip="Regenerate token"
>
<button
aria-label="Regenerate token"
title="Regenerate token"
type="button"
>
<i
class="CompassIcon icon-refresh Icon Icon--right"
/>
</button>
</div>
</div>
<button
title="Copy link"
type="button"
>
<span>
<i
class="CompassIcon icon-content-copy CompassIcon"
/>
Copy link
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/shareBoard return shareBoard, and click switch 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<h1
class="text-heading5 mt-2"
>
Share Board
</h1>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="tabs-modal"
>
<div>
<div
class="d-flex justify-content-between"
>
<div
class="d-flex flex-column"
>
<div
class="text-heading2"
>
Publish to the web
</div>
<div
class="text-light"
>
Publish and share a “read only” link with everyone on the web
</div>
</div>
<div>
<div
class="Switch size--medium on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
<div
class="d-flex justify-content-between tabs-inputs"
>
<div
class="d-flex input-container"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<div
class="octo-tooltip tooltip-top"
data-tooltip="Regenerate token"
>
<button
aria-label="Regenerate token"
title="Regenerate token"
type="button"
>
<i
class="CompassIcon icon-refresh Icon Icon--right"
/>
</button>
</div>
</div>
<button
title="Copy link"
type="button"
>
<span>
<i
class="CompassIcon icon-content-copy CompassIcon"
/>
Copy link
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/shareBoard return shareBoardComponent and click Switch without sharing 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<h1
class="text-heading5 mt-2"
>
Share Board
</h1>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="tabs-modal"
>
<div>
<div
class="d-flex justify-content-between"
>
<div
class="d-flex flex-column"
>
<div
class="text-heading2"
>
Publish to the web
</div>
<div
class="text-light"
>
Publish and share a “read only” link with everyone on the web
</div>
</div>
<div>
<div
class="Switch size--medium on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
<div
class="d-flex justify-content-between tabs-inputs"
>
<div
class="d-flex input-container"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=aToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=aToken
</a>
<div
class="octo-tooltip tooltip-top"
data-tooltip="Regenerate token"
>
<button
aria-label="Regenerate token"
title="Regenerate token"
type="button"
>
<i
class="CompassIcon icon-refresh Icon Icon--right"
/>
</button>
</div>
</div>
<button
title="Copy link"
type="button"
>
<span>
<i
class="CompassIcon icon-content-copy CompassIcon"
/>
Copy link
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<h1
class="text-heading5 mt-2"
>
Share Board
</h1>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="tabs-modal"
>
<div>
<div
class="d-flex justify-content-between"
>
<div
class="d-flex flex-column"
>
<div
class="text-heading2"
>
Publish to the web
</div>
<div
class="text-light"
>
Publish and share a “read only” link with everyone on the web
</div>
</div>
<div>
<div
class="Switch size--medium"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/shareBoard should match snapshot with sharing 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<h1
class="text-heading5 mt-2"
>
Share Board
</h1>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="tabs-modal"
>
<div>
<div
class="d-flex justify-content-between"
>
<div
class="d-flex flex-column"
>
<div
class="text-heading2"
>
Publish to the web
</div>
<div
class="text-light"
>
Publish and share a “read only” link with everyone on the web
</div>
</div>
<div>
<div
class="Switch size--medium on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
<div
class="d-flex justify-content-between tabs-inputs"
>
<div
class="d-flex input-container"
>
<a
class="shareUrl"
href="http://localhost/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/workspace/1/shared/1/1?r=oneToken
</a>
<div
class="octo-tooltip tooltip-top"
data-tooltip="Regenerate token"
>
<button
aria-label="Regenerate token"
title="Regenerate token"
type="button"
>
<i
class="CompassIcon icon-refresh Icon Icon--right"
/>
</button>
</div>
</div>
<button
title="Copy link"
type="button"
>
<span>
<i
class="CompassIcon icon-content-copy CompassIcon"
/>
Copy link
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and subpath 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<h1
class="text-heading5 mt-2"
>
Share Board
</h1>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="tabs-modal"
>
<div>
<div
class="d-flex justify-content-between"
>
<div
class="d-flex flex-column"
>
<div
class="text-heading2"
>
Publish to the web
</div>
<div
class="text-light"
>
Publish and share a “read only” link with everyone on the web
</div>
</div>
<div>
<div
class="Switch size--medium on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
<div
class="d-flex justify-content-between tabs-inputs"
>
<div
class="d-flex input-container"
>
<a
class="shareUrl"
href="http://localhost/test-subpath/plugins/boards/workspace/1/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/test-subpath/plugins/boards/workspace/1/shared/1/1?r=oneToken
</a>
<div
class="octo-tooltip tooltip-top"
data-tooltip="Regenerate token"
>
<button
aria-label="Regenerate token"
title="Regenerate token"
type="button"
>
<i
class="CompassIcon icon-refresh Icon Icon--right"
/>
</button>
</div>
</div>
<button
title="Copy link"
type="button"
>
<span>
<i
class="CompassIcon icon-content-copy CompassIcon"
/>
Copy link
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and without workspaceId and subpath 1`] = `
<div>
<div
class="Dialog dialog-back ShareBoardDialog"
>
<div
class="wrapper"
>
<div
class="dialog"
role="dialog"
>
<div
class="toolbar"
>
<h1
class="text-heading5 mt-2"
>
Share Board
</h1>
<button
aria-label="Close dialog"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="tabs-modal"
>
<div>
<div
class="d-flex justify-content-between"
>
<div
class="d-flex flex-column"
>
<div
class="text-heading2"
>
Publish to the web
</div>
<div
class="text-light"
>
Publish and share a “read only” link with everyone on the web
</div>
</div>
<div>
<div
class="Switch size--medium on"
>
<div
class="octo-switch-inner"
/>
</div>
</div>
</div>
</div>
<div
class="d-flex justify-content-between tabs-inputs"
>
<div
class="d-flex input-container"
>
<a
class="shareUrl"
href="http://localhost/test-subpath/plugins/boards/shared/1/1?r=oneToken"
rel="noreferrer"
target="_blank"
>
http://localhost/test-subpath/plugins/boards/shared/1/1?r=oneToken
</a>
<div
class="octo-tooltip tooltip-top"
data-tooltip="Regenerate token"
>
<button
aria-label="Regenerate token"
title="Regenerate token"
type="button"
>
<i
class="CompassIcon icon-refresh Icon Icon--right"
/>
</button>
</div>
</div>
<button
title="Copy link"
type="button"
>
<span>
<i
class="CompassIcon icon-content-copy CompassIcon"
/>
Copy link
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
<div>
<div
class="ShareBoardButton"
>
<button
class="Button emphasis--secondary size--medium"
title="Share board"
type="button"
>
<span>
<i
class="CompassIcon icon-globe CompassIcon"
/>
Share
</span>
</button>
</div>
</div>
`;

View File

@ -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);
}
}
}
}
}

View File

@ -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(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>), {wrapper: MemoryRouter})
const result = render(
wrapDNDIntl(
<ShareBoard
boardId={board.id}
onClose={jest.fn()}
/>),
{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(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>), {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(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>), {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(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>), {wrapper: MemoryRouter})
const result = render(
wrapDNDIntl(
<ShareBoard
boardId={board.id}
onClose={jest.fn()}
/>),
{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(
<ShareBoard
boardId={board.id}
onClose={jest.fn()}
/>),
{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(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>), {wrapper: MemoryRouter})
const result = render(
wrapDNDIntl(
<ShareBoard
boardId={board.id}
onClose={jest.fn()}
/>),
{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(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>), {wrapper: MemoryRouter})
const result = render(
wrapDNDIntl(
<ShareBoard
boardId={board.id}
onClose={jest.fn()}
/>),
{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(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>), {wrapper: MemoryRouter})
const result = render(
wrapDNDIntl(
<ShareBoard
boardId={board.id}
onClose={jest.fn()}
/>),
{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(
<ShareBoardComponent
boardId={board.id}
onClose={jest.fn()}
/>))
result.rerender(
wrapDNDIntl(
<ShareBoard
boardId={board.id}
onClose={jest.fn()}
/>))
})
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(
<ShareBoardComponent
<ShareBoard
boardId={board.id}
onClose={jest.fn()}
/>), {wrapper: MemoryRouter})
@ -263,7 +286,7 @@ describe('src/components/shareBoardComponent', () => {
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ShareBoardComponent
<ShareBoard
boardId={board.id}
onClose={jest.fn()}
/>), {wrapper: MemoryRouter})

View File

@ -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<ISharing|undefined>(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 (
<Dialog
onClose={props.onClose}
className='ShareBoardDialog'
title={intl.formatMessage({id: 'ShareBoard.Title', defaultMessage: 'Share Board'})}
>
<div className='tabs-modal'>
<div>
<div className='d-flex justify-content-between'>
<div className='d-flex flex-column'>
<div className='text-heading2'>{intl.formatMessage({id: 'ShareBoard.PublishTitle', defaultMessage: 'Publish to the web'})}</div>
<div className='text-light'>{intl.formatMessage({id: 'ShareBoard.PublishDescription', defaultMessage: 'Publish and share a “read only” link with everyone on the web'})}</div>
</div>
<div>
<Switch
isOn={isSharing}
size='medium'
onChanged={onShareChanged}
/>
</div>
</div>
</div>
{isSharing &&
(<div className='d-flex justify-content-between tabs-inputs'>
<div className='d-flex input-container'>
<a
className='shareUrl'
href={shareUrl.toString()}
target='_blank'
rel='noreferrer'
>
{shareUrl.toString()}
</a>
<Tooltip
key={'regenerateToken'}
title={intl.formatMessage({id: 'ShareBoard.regenerate', defaultMessage: 'Regenerate token'})}
>
<IconButton
onClick={onRegenerateToken}
icon={
<CompassIcon
icon='refresh'
className='Icon Icon--right'
/>}
title={intl.formatMessage({id: 'ShareBoard.regenerate', defaultMessage: 'Regenerate token'})}
className='IconButton--large'
/>
</Tooltip>
</div>
<Button
emphasis='secondary'
size='medium'
title='Copy link'
onClick={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ShareLinkPublicCopy, {board: props.boardId})
Utils.copyTextToClipboard(shareUrl.toString())
setWasCopied(true)
}}
>
<CompassIcon
icon='content-copy'
className='CompassIcon'
/>
{wasCopied &&
<FormattedMessage
id='ShareBoard.copiedLink'
defaultMessage='Copied!'
/>}
{!wasCopied &&
<FormattedMessage
id='ShareBoard.copyLink'
defaultMessage='Copy link'
/>}
</Button>
</div>)
}
</div>
</Dialog>
)
}

View File

@ -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);
}
}
}

View File

@ -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(
<ShareBoardButton
boardId={board.id}
/>))
const renderer = result.container
expect(renderer).toMatchSnapshot()
})
})

View File

@ -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 (
<div className='ShareBoardButton'>
<Button
title='Share board'
size='medium'
emphasis='secondary'
onClick={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ShareBoardOpenModal, {board: props.boardId})
setShowShareDialog(!showShareDialog)
}}
>
<CompassIcon
icon='globe'
className='CompassIcon'
/>
<FormattedMessage
id='CenterPanel.Share'
defaultMessage='Share'
/>
</Button>
{showShareDialog &&
<ShareBoardDialog
onClose={() => setShowShareDialog(false)}
boardId={props.boardId}
/>
}
</div>
)
}
export default React.memo(ShareBoardButton)

View File

@ -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;
}
}

View File

@ -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<ISharing|undefined>(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 (
<Modal
onClose={props.onClose}
>
<div className='ShareBoardComponent'>
<div className='row'>
<div>
{isSharing &&
<FormattedMessage
id='ShareBoard.unshare'
defaultMessage='Anyone with the link can view this board and all cards in it.'
/>}
{!isSharing &&
<FormattedMessage
id='ShareBoard.share'
defaultMessage='Publish and share this board with anyone who has the link'
/>}
</div>
<Switch
isOn={Boolean(isSharing)}
onChanged={onShareChanged}
/>
</div>
{isSharing && <>
<div className='row'>
<a
className='shareUrl'
href={shareUrl.toString()}
target='_blank'
rel='noreferrer'
>
{shareUrl.toString()}
</a>
<Button
filled={true}
size='small'
onClick={() => {
Utils.copyTextToClipboard(shareUrl.toString())
setWasCopied(true)
}}
>
{wasCopied &&
<FormattedMessage
id='ShareBoard.copiedLink'
defaultMessage='Copied!'
/>}
{!wasCopied &&
<FormattedMessage
id='ShareBoard.copyLink'
defaultMessage='Copy link'
/>}
</Button>
</div>
<div className='row'>
<Button
onClick={onRegenerateToken}
emphasis='secondary'
size='small'
>
<FormattedMessage
id='ShareBoard.regenerateToken'
defaultMessage='Regenerate token'
/>
</Button>
</div>
</>}
</div>
</Modal>
)
})
export default ShareBoardComponent

View File

@ -259,7 +259,7 @@ exports[`components/sidebar/SidebarSettingsMenu imports menu open should match s
Random icons
</div>
<div
class="Switch on"
class="Switch size--small on"
>
<div
class="octo-switch-inner"
@ -750,7 +750,7 @@ exports[`components/sidebar/SidebarSettingsMenu languages menu open should match
Random icons
</div>
<div
class="Switch on"
class="Switch size--small on"
>
<div
class="octo-switch-inner"
@ -930,7 +930,7 @@ exports[`components/sidebar/SidebarSettingsMenu settings menu open should match
Random icons
</div>
<div
class="Switch on"
class="Switch size--small on"
>
<div
class="octo-switch-inner"
@ -1200,7 +1200,7 @@ exports[`components/sidebar/SidebarSettingsMenu theme menu open should match sna
Random icons
</div>
<div
class="Switch on"
class="Switch size--small on"
>
<div
class="octo-switch-inner"

View File

@ -19,7 +19,7 @@ type Props = {
onClose: () => void
}
const RegistrationLink = React.memo((props: Props) => {
const RegistrationLink = (props: Props) => {
const {onClose} = props
const intl = useIntl()
const workspace = useAppSelector<IWorkspace|null>(getCurrentWorkspace)
@ -89,6 +89,6 @@ const RegistrationLink = React.memo((props: Props) => {
</div>
</Modal>
)
})
}
export default RegistrationLink
export default React.memo(RegistrationLink)

View File

@ -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) => {
<SidebarSettingsMenu activeTheme={getActiveThemeName()}/>}
</div>
)
})
}
export default Sidebar
export default React.memo(Sidebar)

View File

@ -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) => {
/>}
</div>
)
})
}
export default SidebarBoardItem
export default React.memo(SidebarBoardItem)

View File

@ -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) => {
</MenuWrapper>
</div>
)
})
}
export default SidebarSettingsMenu
export default React.memo(SidebarSettingsMenu)

View File

@ -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<IUser|null>(getMe)
@ -106,6 +106,6 @@ const SidebarUserMenu = React.memo(() => {
</ModalWrapper>
</div>
)
})
}
export default SidebarUserMenu
export default React.memo(SidebarUserMenu)

View File

@ -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 || []

View File

@ -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)

View File

@ -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 => {
/>}
</div>
)
})
}
export default TableGroup
export default React.memo(TableGroup)

View File

@ -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 => {
}
</div>
)
})
}
export default TableGroupHeaderRow
export default React.memo(TableGroupHeaderRow)

View File

@ -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 => {
}
</div>
)
})
}
export default TableHeader
export default React.memo(TableHeader)

View File

@ -43,7 +43,7 @@ export const columnWidth = (resizingColumn: string, columnWidths: Record<string,
return Math.max(Constants.minColumnWidth, columnWidths[templateId] || 0)
}
const TableRow = React.memo((props: Props) => {
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) => {
})}
</div>
)
})
}
export default TableRow
export default React.memo(TableRow)

View File

@ -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 => {
</a>
</div>
)
})
}
export default TopBar
export default React.memo(TopBar)

View File

@ -38,7 +38,7 @@ exports[`components/viewHeader/filterValue return filterValue 1`] = `
Status
</div>
<div
class="Switch on"
class="Switch size--small on"
>
<div
class="octo-switch-inner"

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call to board archive 1`] = `
exports[`components/viewHeader/viewHeaderActionsMenu return menu 1`] = `
<div>
<div
class="ModalWrapper"
@ -61,8 +61,67 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
class="noicon"
/>
</div>
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Share board"
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Cancel
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call to board archive 1`] = `
<div>
<div
class="ModalWrapper"
>
<div
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
<button
class="IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
<div
class="Menu noselect bottom"
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div
aria-label="Export to CSV"
class="MenuOption TextOption menu-option"
role="button"
>
@ -72,7 +131,24 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
<div
class="menu-name"
>
Share board
Export to CSV
</div>
<div
class="noicon"
/>
</div>
<div
aria-label="Export board archive"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Export board archive
</div>
<div
class="noicon"
@ -111,226 +187,6 @@ exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call
`;
exports[`components/viewHeader/viewHeaderActionsMenu return menu and verify call to csv exporter 1`] = `
<div>
<div
class="ModalWrapper"
>
<div
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
<button
class="IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
<div
class="Menu noselect bottom"
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div
aria-label="Export to CSV"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Export to CSV
</div>
<div
class="noicon"
/>
</div>
<div
aria-label="Export board archive"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Export board archive
</div>
<div
class="noicon"
/>
</div>
<div
aria-label="Share board"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Share board
</div>
<div
class="noicon"
/>
</div>
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Cancel
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/viewHeader/viewHeaderActionsMenu return menu with Share Boards 1`] = `
<div>
<div
class="ModalWrapper"
>
<div
aria-label="View header menu"
class="MenuWrapper"
role="button"
>
<button
class="IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
<div
class="Menu noselect bottom"
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div
aria-label="Export to CSV"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Export to CSV
</div>
<div
class="noicon"
/>
</div>
<div
aria-label="Export board archive"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Export board archive
</div>
<div
class="noicon"
/>
</div>
<div
aria-label="Share board"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Share board
</div>
<div
class="noicon"
/>
</div>
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="noicon"
/>
<div
class="menu-name"
>
Cancel
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/viewHeader/viewHeaderActionsMenu return menu without Share Boards 1`] = `
<div>
<div
class="ModalWrapper"

View File

@ -38,7 +38,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
Status
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -59,7 +59,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
Property 1
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -80,7 +80,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
Property 2
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -101,7 +101,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
Property 3
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -122,7 +122,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
Comments and Description
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -198,7 +198,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
Title
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -219,7 +219,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
Status
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -240,7 +240,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
Property 1
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -261,7 +261,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
Property 2
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -282,7 +282,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
Property 3
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"
@ -303,7 +303,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu w
Comments and Description
</div>
<div
class="Switch"
class="Switch size--small"
>
<div
class="octo-switch-inner"

View File

@ -20,7 +20,7 @@ type Props = {
addCard: () => void
}
const EmptyCardButton = React.memo((props: Props) => {
const EmptyCardButton = (props: Props) => {
const currentView = useAppSelector(getCurrentView)
const intl = useIntl()
@ -52,6 +52,6 @@ const EmptyCardButton = React.memo((props: Props) => {
</MenuWrapper>
}
/>)
})
}
export default EmptyCardButton
export default React.memo(EmptyCardButton)

View File

@ -23,7 +23,7 @@ type Props = {
onClose: () => void
}
const FilterComponent = React.memo((props: Props): JSX.Element => {
const FilterComponent = (props: Props): JSX.Element => {
const conditionClicked = (optionId: string, filter: FilterClause): void => {
const {activeView} = props
@ -91,6 +91,6 @@ const FilterComponent = React.memo((props: Props): JSX.Element => {
</div>
</Modal>
)
})
}
export default FilterComponent
export default React.memo(FilterComponent)

View File

@ -25,7 +25,7 @@ type Props = {
filter: FilterClause
}
const FilterEntry = React.memo((props: Props): JSX.Element => {
const FilterEntry = (props: Props): JSX.Element => {
const {board, view, filter} = props
const intl = useIntl()
@ -106,6 +106,6 @@ const FilterEntry = React.memo((props: Props): JSX.Element => {
</Button>
</div>
)
})
}
export default FilterEntry
export default React.memo(FilterEntry)

View File

@ -22,7 +22,7 @@ type Props = {
editCardTemplate: (cardTemplateId: string) => void
}
const NewCardButton = React.memo((props: Props): JSX.Element => {
const NewCardButton = (props: Props): JSX.Element => {
const cardTemplates: Card[] = useAppSelector(getCurrentBoardTemplates)
const currentView = useAppSelector(getCurrentView)
const intl = useIntl()
@ -79,6 +79,6 @@ const NewCardButton = React.memo((props: Props): JSX.Element => {
</Menu>
</ButtonWithMenu>
)
})
}
export default NewCardButton
export default React.memo(NewCardButton)

View File

@ -22,7 +22,7 @@ type Props = {
editCardTemplate: (cardTemplateId: string) => void
}
const NewCardButtonTemplateItem = React.memo((props: Props) => {
const NewCardButtonTemplateItem = (props: Props) => {
const currentView = useAppSelector(getCurrentView)
const {cardTemplate} = props
const intl = useIntl()
@ -77,6 +77,6 @@ const NewCardButtonTemplateItem = React.memo((props: Props) => {
}
/>
)
})
}
export default NewCardButtonTemplateItem
export default React.memo(NewCardButtonTemplateItem)

Some files were not shown because too many files have changed in this diff Show More