mirror of
https://github.com/mattermost/focalboard.git
synced 2025-03-03 15:32:14 +02:00
Merge branch 'main' into MM-40430-redirect-login
This commit is contained in:
commit
9e4be43545
@ -1,24 +1,22 @@
|
||||
# Code Contribution Guidelines
|
||||
|
||||
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.
|
||||
Thank you for your interest in contributing! Please read the [Focalboard Contribution Guide](https://developers.mattermost.com/contribute/focalboard/) to learn the process for making code contributions, and [join our Focalboard community channel](https://community.mattermost.com/core/channels/focalboard) to get help from community members and the core team.
|
||||
|
||||
When you submit a pull request, it goes through a [code review process outlined here](https://mattermost.github.io/focalboard/code-review).
|
||||
When you submit a pull request, it goes through a code review process outlined [here](https://developers.mattermost.com/contribute/getting-started/code-review/).
|
||||
|
||||
# Updating Changelog
|
||||
After a noteable bug fix or improvement is merged, submit a pull request to the [CHANGELOG](CHANGELOG.md) under the next release section.
|
||||
|
||||
After a 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
|
||||
## 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!
|
||||
You can contribute to the [Mattermost Boards documentation](https://docs.mattermost.com/guides/boards.html). Read more about how the contribution process works in the repository's [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
|
||||
## Contributors
|
||||
|
||||
**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**: Maintains the Focalboard project and 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.
|
||||
|
||||
- **<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
|
||||
@ -38,12 +36,12 @@ You can contribute to our documentation in the [Mattermost Boards documentation]
|
||||
- **<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.
|
||||
**Community Organizers**: Responds with comments to bug reports, issues, and pull requests with tags, edits and mentions to core committers and contributors.
|
||||
|
||||
- **<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
|
||||
|
||||
**Documentation**: Verifies documentation changes, and updates documentation for new features.
|
||||
**Documentation**: Verifies documentation changes and updates documentation for new features.
|
||||
|
||||
- **<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
|
||||
|
128
README.md
128
README.md
@ -16,108 +16,106 @@ Like what you see? :eyes: Give us a GitHub Star! :star:
|
||||
|
||||
It helps define, organize, track and manage work across individuals and teams. Focalboard comes in two main editions:
|
||||
|
||||
* **[Personal Desktop](https://www.focalboard.com/download/personal-edition/desktop/)**: A stand-alone single-user Mac, Windows, or Linux desktop app for your todos and personal projects.
|
||||
* **[Personal Desktop](https://www.focalboard.com/download/personal-edition/desktop/)**: A standalone, single-user Mac, Windows, or Linux desktop app for your own todos and personal projects.
|
||||
|
||||
* **[Mattermost Boards](https://www.focalboard.com/download/mattermost/)**: A self-hosted or cloud server for your team to plan and collaborate.
|
||||
|
||||
Focalboard can also be installed as a standalone [personal server](https://www.focalboard.com/download/personal-edition/ubuntu/) for development and personal use.
|
||||
Focalboard can also be installed as a standalone **[Personal Server](https://www.focalboard.com/download/personal-edition/ubuntu/)** for development and personal use.
|
||||
|
||||
## Try out Focalboard
|
||||
## Try Focalboard
|
||||
|
||||
### Focalboard Personal Desktop (Windows, Mac or Linux Desktop)
|
||||
### Personal Desktop (Windows, Mac or Linux Desktop)
|
||||
|
||||
Try out the single-user **Focalboard Personal Desktop**:
|
||||
* macOS: Download from the [Mac App Store](https://apps.apple.com/us/app/focalboard-insiders/id1556908618?mt=12).
|
||||
* Windows: Download from the [Windows App Store](https://www.microsoft.com/store/productId/9NLN2T0SX9VF) or download `focalboard-win.zip` from the [latest release](https://github.com/mattermost/focalboard/releases), unpack, and run `Focalboard.exe`
|
||||
* Linux Desktop: Download `focalboard-linux.tar.gz` from the [latest release](https://github.com/mattermost/focalboard/releases), unpack, and open `focalboard-app`
|
||||
* **Windows**: Download from the [Windows App Store](https://www.microsoft.com/store/productId/9NLN2T0SX9VF) or download `focalboard-win.zip` from the [latest release](https://github.com/mattermost/focalboard/releases), unpack, and run `Focalboard.exe`.
|
||||
* **Mac**: Download from the [Mac App Store](https://apps.apple.com/us/app/focalboard-insiders/id1556908618?mt=12).
|
||||
* **Linux Desktop**: Download `focalboard-linux.tar.gz` from the [latest release](https://github.com/mattermost/focalboard/releases), unpack, and open `focalboard-app`.
|
||||
|
||||
### Mattermost Boards
|
||||
|
||||
Mattermost Boards combines project management tools with messaging and collaboration for teams of all sizes. To access and use Boards, install or upgrade to Mattermost v6.0 or later as a [self-hosted server](https://docs.mattermost.com/guides/deployment.html?utm_source=focalboard&utm_campaign=focalboard) or [Cloud server](https://mattermost.com/get-started/?utm_source=focalboard&utm_campaign=focalboard). After logging into Mattermost, select the menu in the top left corner of Mattermost and choose **Boards**.
|
||||
**Mattermost Boards** is the Mattermost plugin version of Focalboard that combines project management tools with messaging and collaboration for teams of all sizes. To access and use **Mattermost Boards**, install or upgrade to Mattermost v6.0 or later as a [self-hosted server](https://docs.mattermost.com/guides/deployment.html?utm_source=focalboard&utm_campaign=focalboard) or [Cloud server](https://mattermost.com/get-started/?utm_source=focalboard&utm_campaign=focalboard). After logging into Mattermost, select the menu in the top left corner and select **Boards**.
|
||||
|
||||
See the [setup guide](https://www.focalboard.com/download/mattermost/) for more details.
|
||||
***Mattermost Boards** is installed and enabled by default in Mattermost v6.0 and later.*
|
||||
|
||||
### Focalboard Personal Server (Ubuntu)
|
||||
See the [plugin setup guide](https://www.focalboard.com/download/mattermost/) for more details.
|
||||
|
||||
You can download and run the compiled **Focalboard Personal Server** by following [our latest install guide](https://www.focalboard.com/download/personal-edition/ubuntu/).
|
||||
### Personal Server
|
||||
|
||||
Download the latest server release from [GitHub releases](https://github.com/mattermost/focalboard/releases)
|
||||
**Ubuntu**: You can download and run the compiled Focalboard **Personal Server** on Ubuntu by following [our latest install guide](https://www.focalboard.com/download/personal-edition/ubuntu/).
|
||||
|
||||
## Building the server
|
||||
## Contribute to Focalboard
|
||||
|
||||
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:
|
||||
Contribute code, bug reports, and ideas to the future of the Focalboard project. We welcome your input! Please see [CONTRIBUTING](CONTRIBUTING.md) for details on how to get involved.
|
||||
|
||||
First, install basic dependencies:
|
||||
* Go 1.15+
|
||||
* Node 16.3+ and npm
|
||||
* Mingw64 on Windows
|
||||
### Getting started
|
||||
|
||||
Our [developer guide](https://developers.mattermost.com/contribute/focalboard/personal-server-setup-guide) has detailed instructions on how to set up your development environment for the **Personal Server**. It also provides more information about contributing to our open source community.
|
||||
|
||||
To build the server:
|
||||
|
||||
```
|
||||
make prebuild
|
||||
make
|
||||
```
|
||||
|
||||
## Running and testing the server
|
||||
To run the server:
|
||||
|
||||
To start the server, run `./bin/focalboard-server`
|
||||
```
|
||||
./bin/focalboard-server
|
||||
```
|
||||
|
||||
Server settings are in config.json (or the path specified with --config).
|
||||
Then navigate your browser to [`http://localhost:8000`](http://localhost:8000) to access your Focalboard server. The port is configured in `config.json`.
|
||||
|
||||
Open a browser to [http://localhost:8000](http://localhost:8000) to start.
|
||||
Once the server is running, you can rebuild just the web app via `make webapp` in a separate terminal window. Reload your browser to see the changes.
|
||||
|
||||
## Building and running standalone desktop apps
|
||||
### Building and running standalone desktop apps
|
||||
|
||||
You can build standalone apps that package the server to run locally against SQLite:
|
||||
|
||||
* Mac:
|
||||
* **Windows**:
|
||||
* *Requires Windows 10, [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) 10.0.19041.0, and .NET 4.8 developer pack*
|
||||
* Open a `git-bash` prompt.
|
||||
* Run `make win-wpf-app`
|
||||
* Run `cd win-wpf/msix && focalboard.exe`
|
||||
* **Mac**:
|
||||
* *Requires macOS 11.3+ and Xcode 13.2.1+*
|
||||
* `make mac-app`
|
||||
* run `mac/dist/Focalboard.app`
|
||||
* *Requires: macOS 11.3+, Xcode 13.2.1+*
|
||||
* Linux:
|
||||
* Install webgtk dependencies
|
||||
* `open mac/dist/Focalboard.app`
|
||||
* **Linux**:
|
||||
* *Tested on Ubuntu 18.04*
|
||||
* Install `webgtk` dependencies
|
||||
* `sudo apt-get install libgtk-3-dev`
|
||||
* `sudo apt-get install libwebkit2gtk-4.0-dev`
|
||||
* `make linux-app`
|
||||
* uncompress `linux/dist/focalboard-linux.tar.gz` to a directory of your choice
|
||||
* run `focalboard-app` from the directory you have chosen
|
||||
* *Tested with: Ubuntu 18.04*
|
||||
* Windows:
|
||||
* Open a git-bash prompt
|
||||
* `make win-wpf-app`
|
||||
* run `cd win-wpf/msix && focalboard.exe`
|
||||
* *Requires: Windows 10, [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) 10.0.19041.0, .NET 4.8 developer pack*
|
||||
* Docker:
|
||||
* To run it locally from Offical Image
|
||||
* `docker run -it -p 80:8000 mattermost/focalboard`
|
||||
* To Build it for your Current Architecture
|
||||
* `docker build -f docker/Dockerfile .`
|
||||
* To Build it for a custom Architecture (Experimental)
|
||||
* `docker build -f docker/Dockerfile --platform linux/arm64 .`
|
||||
* Uncompress `linux/dist/focalboard-linux.tar.gz` to a directory of your choice
|
||||
* Run `focalboard-app` from the directory you have chosen
|
||||
* **Docker**:
|
||||
* To run it locally from offical image:
|
||||
* `docker run -it -p 80:8000 mattermost/focalboard`
|
||||
* To build it for your current architecture:
|
||||
* `docker build -f docker/Dockerfile .`
|
||||
* To build it for a custom architecture (experimental):
|
||||
* `docker build -f docker/Dockerfile --platform linux/arm64 .`
|
||||
|
||||
Cross-compilation currently isn't fully supported, so please build on the appropriate platform. Refer to the GitHub Actions workflows (build-mac.yml, build-win.yml, build-ubuntu.yml) for the detailed list of steps on each platform.
|
||||
Cross-compilation currently isn't fully supported, so please build on the appropriate platform. Refer to the GitHub Actions workflows (`build-mac.yml`, `build-win.yml`, `build-ubuntu.yml`) for the detailed list of steps on each platform.
|
||||
|
||||
## Unit tests
|
||||
### Unit testing
|
||||
|
||||
Before checking-in commits, run: `make ci`, which is similar 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 UI tests: `cd webapp; npm run cypress:ci`
|
||||
Before checking in commits, run `make ci`, which is similar to the `.gitlab-ci.yml` workflow and includes:
|
||||
|
||||
## Stay informed on progress
|
||||
* **Server unit tests**: `make server-test`
|
||||
* **Web app ESLint**: `cd webapp; npm run check`
|
||||
* **Web app unit tests**: `cd webapp; npm run test`
|
||||
* **Web app UI tests**: `cd webapp; npm run cypress:ci`
|
||||
|
||||
* **Changelog**: See [CHANGELOG.md](CHANGELOG.md) for the latest updates
|
||||
* **Developer Discussion**: Join the [Developer Discussion](https://github.com/mattermost/focalboard/discussions) board
|
||||
* **Chat**: Join the [Focalboard community channel](https://community.mattermost.com/core/channels/focalboard)
|
||||
|
||||
## Share your feedback
|
||||
|
||||
File bugs, suggest features, join our forum, learn more [here](https://github.com/mattermost/focalboard/wiki/Share-your-feedback)!
|
||||
|
||||
## Contributing
|
||||
|
||||
Contribute code, bug reports, and ideas to the future of the Focalboard project. We welcome your input! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get involved.
|
||||
|
||||
## Translating
|
||||
### Translating
|
||||
|
||||
Help translate Focalboard! The app is already translated into several languages. We welcome corrections and new language translations! You can add new languages or improve existing translations at [Weblate](https://translate.mattermost.com/engage/focalboard/).
|
||||
|
||||
### Staying informed
|
||||
|
||||
Are you interested in influencing the future of the Focalboard open source project? Here's how you can get involved:
|
||||
|
||||
* **Changes**: See the [CHANGELOG](CHANGELOG.md) for the latest updates
|
||||
* **GitHub Discussions**: Join the [Developer Discussion](https://github.com/mattermost/focalboard/discussions) board
|
||||
* **Bug Reports**: [File a bug report](https://github.com/mattermost/focalboard/issues/new?assignees=&labels=bug&template=bug_report.md&title=)
|
||||
* **Chat**: Join the [Focalboard community channel](https://community.mattermost.com/core/channels/focalboard)
|
||||
|
@ -1,3 +1,7 @@
|
||||
# Focalboard Plugin for Mattermost
|
||||
# Mattermost Boards (Focalboard Plugin)
|
||||
|
||||
This plugin allows to run focalboard inside your mattermost instance as a plugin.
|
||||
**[Mattermost Boards](https://mattermost.com/boards/)** is the Mattermost plugin version of Focalboard that combines project management tools with messaging and collaboration for teams of all sizes. To access and use **Mattermost Boards**, install or upgrade to Mattermost v6.0 or later as a [self-hosted server](https://docs.mattermost.com/guides/deployment.html?utm_source=focalboard&utm_campaign=focalboard) or [Cloud server](https://mattermost.com/get-started/?utm_source=focalboard&utm_campaign=focalboard). After logging into Mattermost, select the menu in the top left corner of Mattermost and select **Boards**.
|
||||
|
||||
***Mattermost Boards** is installed and enabled by default in Mattermost v6.0 and later.*
|
||||
|
||||
To build your own version of Matterboard Boards and upload it to your own Mattermost server, follow the instructions [here](https://developers.mattermost.com/contribute/focalboard/mattermost-boards-setup-guide/).
|
||||
|
@ -189,8 +189,11 @@ export default class Plugin {
|
||||
if (currentTeamID && currentTeamID !== prevTeamID) {
|
||||
prevTeamID = currentTeamID
|
||||
store.dispatch(setTeam(currentTeamID))
|
||||
browserHistory.push(`/team/${currentTeamID}`)
|
||||
wsClient.subscribeToTeam(currentTeamID)
|
||||
if (window.location.pathname.startsWith(windowAny.frontendBaseURL || '')) {
|
||||
console.log("REDIRECTING HERE")
|
||||
browserHistory.push(`/team/${currentTeamID}`)
|
||||
wsClient.subscribeToTeam(currentTeamID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -99,6 +99,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
apiv1.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST")
|
||||
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT")
|
||||
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE")
|
||||
apiv1.HandleFunc("/boards/{boardID}/join", a.sessionRequired(a.handleJoinBoard)).Methods("POST")
|
||||
|
||||
// Sharing APIs
|
||||
apiv1.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST")
|
||||
@ -3214,6 +3215,98 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation POST /boards/{boardID}/join joinBoard
|
||||
//
|
||||
// Become a member of a board
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: boardID
|
||||
// in: path
|
||||
// description: Board ID
|
||||
// required: true
|
||||
// type: string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// $ref: '#/definitions/BoardMember'
|
||||
// '404':
|
||||
// description: board not found
|
||||
// '503':
|
||||
// description: access denied
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
userID := getUserID(r)
|
||||
if userID == "" {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
boardID := mux.Vars(r)["boardID"]
|
||||
board, err := a.app.GetBoard(boardID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if board == nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||
return
|
||||
}
|
||||
if board.Type != model.BoardTypeOpen {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// currently all memberships are created as editors by default
|
||||
// TODO: Support different public roles
|
||||
newBoardMember := &model.BoardMember{
|
||||
UserID: userID,
|
||||
BoardID: boardID,
|
||||
SchemeEditor: true,
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "joinBoard", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||
auditRec.AddMeta("boardID", boardID)
|
||||
auditRec.AddMeta("addedUserID", userID)
|
||||
|
||||
member, err := a.app.AddMemberToBoard(newBoardMember)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.logger.Debug("AddMember",
|
||||
mlog.String("boardID", board.ID),
|
||||
mlog.String("addedUserID", userID),
|
||||
)
|
||||
|
||||
data, err := json.Marshal(member)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
// response
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation PUT /boards/{boardID}/members/{userID} updateMember
|
||||
//
|
||||
|
@ -176,6 +176,10 @@ func (c *Client) GetBoardRoute(boardID string) string {
|
||||
return fmt.Sprintf("%s/%s", c.GetBoardsRoute(), boardID)
|
||||
}
|
||||
|
||||
func (c *Client) GetJoinBoardRoute(boardID string) string {
|
||||
return fmt.Sprintf("%s/%s/join", c.GetBoardsRoute(), boardID)
|
||||
}
|
||||
|
||||
func (c *Client) GetBlocksRoute(boardID string) string {
|
||||
return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID))
|
||||
}
|
||||
@ -512,6 +516,16 @@ func (c *Client) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember
|
||||
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) JoinBoard(boardID string) (*model.BoardMember, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetJoinBoardRoute(boardID), "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, *Response) {
|
||||
r, err := c.DoAPIPut(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, toJSON(member))
|
||||
if err != nil {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package integrationtests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/client"
|
||||
@ -1268,3 +1269,78 @@ func TestDeleteMember(t *testing.T) {
|
||||
require.True(t, members[0].SchemeAdmin)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJoinBoard(t *testing.T) {
|
||||
t.Run("create and join public board", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
me := th.GetUser1()
|
||||
|
||||
title := "Public board"
|
||||
teamID := testTeamID
|
||||
newBoard := &model.Board{
|
||||
Title: title,
|
||||
Type: model.BoardTypeOpen,
|
||||
TeamID: teamID,
|
||||
}
|
||||
board, resp := th.Client.CreateBoard(newBoard)
|
||||
th.CheckOK(resp)
|
||||
require.NoError(t, resp.Error)
|
||||
require.NotNil(t, board)
|
||||
require.NotNil(t, board.ID)
|
||||
require.Equal(t, title, board.Title)
|
||||
require.Equal(t, model.BoardTypeOpen, board.Type)
|
||||
require.Equal(t, teamID, board.TeamID)
|
||||
require.Equal(t, me.ID, board.CreatedBy)
|
||||
require.Equal(t, me.ID, board.ModifiedBy)
|
||||
|
||||
member, resp := th.Client2.JoinBoard(board.ID)
|
||||
th.CheckOK(resp)
|
||||
require.NoError(t, resp.Error)
|
||||
require.NotNil(t, member)
|
||||
require.Equal(t, board.ID, member.BoardID)
|
||||
require.Equal(t, th.GetUser2().ID, member.UserID)
|
||||
|
||||
s, _ := json.MarshalIndent(member, "", "\t")
|
||||
t.Log(string(s))
|
||||
})
|
||||
|
||||
t.Run("create and join private board (should not succeed)", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
me := th.GetUser1()
|
||||
|
||||
title := "Private board"
|
||||
teamID := testTeamID
|
||||
newBoard := &model.Board{
|
||||
Title: title,
|
||||
Type: model.BoardTypePrivate,
|
||||
TeamID: teamID,
|
||||
}
|
||||
board, resp := th.Client.CreateBoard(newBoard)
|
||||
th.CheckOK(resp)
|
||||
require.NoError(t, resp.Error)
|
||||
require.NotNil(t, board)
|
||||
require.NotNil(t, board.ID)
|
||||
require.Equal(t, title, board.Title)
|
||||
require.Equal(t, model.BoardTypePrivate, board.Type)
|
||||
require.Equal(t, teamID, board.TeamID)
|
||||
require.Equal(t, me.ID, board.CreatedBy)
|
||||
require.Equal(t, me.ID, board.ModifiedBy)
|
||||
|
||||
member, resp := th.Client2.JoinBoard(board.ID)
|
||||
th.CheckForbidden(resp)
|
||||
require.Nil(t, member)
|
||||
})
|
||||
|
||||
t.Run("join invalid board", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
member, resp := th.Client2.JoinBoard("nonexistent-board-ID")
|
||||
th.CheckNotFound(resp)
|
||||
require.Nil(t, member)
|
||||
})
|
||||
}
|
||||
|
@ -311,3 +311,23 @@ func (b *Board) IsValid() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BoardMemberHistoryEntry stores the information of the membership of a user on a board
|
||||
// swagger:model
|
||||
type BoardMemberHistoryEntry struct {
|
||||
// The ID of the board
|
||||
// required: true
|
||||
BoardID string `json:"boardId"`
|
||||
|
||||
// The ID of the user
|
||||
// required: true
|
||||
UserID string `json:"userId"`
|
||||
|
||||
// The action that added this history entry (created or deleted)
|
||||
// required: false
|
||||
Action string `json:"action"`
|
||||
|
||||
// The insertion time
|
||||
// required: true
|
||||
InsertAt int64 `json:"insertAt"`
|
||||
}
|
||||
|
@ -505,6 +505,21 @@ func (mr *MockStoreMockRecorder) GetBoardAndCardByID(arg0 interface{}) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardAndCardByID", reflect.TypeOf((*MockStore)(nil).GetBoardAndCardByID), arg0)
|
||||
}
|
||||
|
||||
// GetBoardMemberHistory mocks base method.
|
||||
func (m *MockStore) GetBoardMemberHistory(arg0, arg1 string, arg2 uint64) ([]*model.BoardMemberHistoryEntry, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBoardMemberHistory", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]*model.BoardMemberHistoryEntry)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetBoardMemberHistory indicates an expected call of GetBoardMemberHistory.
|
||||
func (mr *MockStoreMockRecorder) GetBoardMemberHistory(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardMemberHistory", reflect.TypeOf((*MockStore)(nil).GetBoardMemberHistory), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// GetBoardsForUserAndTeam mocks base method.
|
||||
func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string) ([]*model.Board, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -151,6 +151,28 @@ func (s *SQLStore) boardMembersFromRows(rows *sql.Rows) ([]*model.BoardMember, e
|
||||
return boardMembers, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) boardMemberHistoryEntriesFromRows(rows *sql.Rows) ([]*model.BoardMemberHistoryEntry, error) {
|
||||
boardMemberHistoryEntries := []*model.BoardMemberHistoryEntry{}
|
||||
|
||||
for rows.Next() {
|
||||
var boardMemberHistoryEntry model.BoardMemberHistoryEntry
|
||||
|
||||
err := rows.Scan(
|
||||
&boardMemberHistoryEntry.BoardID,
|
||||
&boardMemberHistoryEntry.UserID,
|
||||
&boardMemberHistoryEntry.Action,
|
||||
&boardMemberHistoryEntry.InsertAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boardMemberHistoryEntries = append(boardMemberHistoryEntries, &boardMemberHistoryEntry)
|
||||
}
|
||||
|
||||
return boardMemberHistoryEntries, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBoardByCondition(db sq.BaseRunner, conditions ...interface{}) (*model.Board, error) {
|
||||
boards, err := s.getBoardsByCondition(db, conditions...)
|
||||
if err != nil {
|
||||
@ -405,6 +427,11 @@ func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.B
|
||||
"scheme_viewer": bm.SchemeViewer,
|
||||
}
|
||||
|
||||
oldMember, err := s.getMemberForBoard(db, bm.BoardID, bm.UserID)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder(db).
|
||||
Insert(s.tablePrefix + "board_members").
|
||||
SetMap(queryValues)
|
||||
@ -425,6 +452,17 @@ func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.B
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if oldMember == nil {
|
||||
addToMembersHistory := s.getQueryBuilder(db).
|
||||
Insert(s.tablePrefix+"board_members_history").
|
||||
Columns("board_id", "user_id", "action", "insert_at").
|
||||
Values(bm.BoardID, bm.UserID, "created", model.GetMillis())
|
||||
|
||||
if _, err := addToMembersHistory.Exec(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return bm, nil
|
||||
}
|
||||
|
||||
@ -434,10 +472,27 @@ func (s *SQLStore) deleteMember(db sq.BaseRunner, boardID, userID string) error
|
||||
Where(sq.Eq{"board_id": boardID}).
|
||||
Where(sq.Eq{"user_id": userID})
|
||||
|
||||
if _, err := deleteQuery.Exec(); err != nil {
|
||||
result, err := deleteQuery.Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected > 0 {
|
||||
addToMembersHistory := s.getQueryBuilder(db).
|
||||
Insert(s.tablePrefix+"board_members_history").
|
||||
Columns("board_id", "user_id", "action", "insert_at").
|
||||
Values(boardID, userID, "deleted", model.GetMillis())
|
||||
|
||||
if _, err := addToMembersHistory.Exec(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -549,3 +604,30 @@ func (s *SQLStore) searchBoardsForUserAndTeam(db sq.BaseRunner, term, userID, te
|
||||
|
||||
return s.boardsFromRows(rows)
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBoardMemberHistory(db sq.BaseRunner, boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select("board_id", "user_id", "action", "insert_at").
|
||||
From(s.tablePrefix + "board_members_history").
|
||||
Where(sq.Eq{"board_id": boardID}).
|
||||
Where(sq.Eq{"user_id": userID}).
|
||||
OrderBy("insert_at DESC")
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error(`getBoardMemberHistory ERROR`, mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
memberHistory, err := s.boardMemberHistoryEntriesFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memberHistory, nil
|
||||
}
|
||||
|
@ -557,7 +557,12 @@ func (s *SQLStore) getDMBoards(tx sq.BaseRunner) ([]*model.Board, error) {
|
||||
},
|
||||
}
|
||||
|
||||
return s.getBoardsByCondition(tx, conditions)
|
||||
boards, err := s.getBoardsByCondition(tx, conditions)
|
||||
if err != nil && errors.Is(err, sql.ErrNoRows) {
|
||||
return []*model.Board{}, nil
|
||||
}
|
||||
|
||||
return boards, err
|
||||
}
|
||||
|
||||
// The destination is selected as the first team where all members
|
||||
|
@ -0,0 +1 @@
|
||||
DROP TABLE {{.prefix}}board_members_history;
|
@ -0,0 +1,18 @@
|
||||
CREATE TABLE {{.prefix}}board_members_history (
|
||||
{{if .postgres}}id SERIAL PRIMARY KEY,{{end}}
|
||||
{{if .sqlite}}id INTEGER PRIMARY KEY AUTOINCREMENT,{{end}}
|
||||
{{if .mysql}}id INT PRIMARY KEY AUTO_INCREMENT,{{end}}
|
||||
board_id VARCHAR(36) NOT NULL,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
action VARCHAR(10),
|
||||
insert_at BIGINT NOT NULL
|
||||
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
|
||||
|
||||
CREATE INDEX idx_boardmembershistory_user_id ON {{.prefix}}board_members_history(user_id);
|
||||
CREATE INDEX idx_boardmembershistory_board_id_userid ON {{.prefix}}board_members_history(board_id, user_id);
|
||||
|
||||
INSERT INTO {{.prefix}}board_members_history (board_id, user_id, action, insert_at) SELECT board_id, user_id, 'created',
|
||||
{{if .postgres}}CAST(extract(epoch from now()) * 1000 AS BIGINT){{end}}
|
||||
{{if .sqlite}}strftime('%s')*1000{{end}}
|
||||
{{if .mysql}}UNIX_TIMESTAMP(now())*1000{{end}}
|
||||
from {{.prefix}}board_members;
|
@ -309,6 +309,11 @@ func (s *SQLStore) GetBoardAndCardByID(blockID string) (*model.Board, *model.Blo
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBoardMemberHistory(boardID string, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) {
|
||||
return s.getBoardMemberHistory(s.db, boardID, userID, limit)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string) ([]*model.Board, error) {
|
||||
return s.getBoardsForUserAndTeam(s.db, userID, teamID)
|
||||
|
||||
|
@ -90,6 +90,7 @@ type Store interface {
|
||||
SaveMember(bm *model.BoardMember) (*model.BoardMember, error)
|
||||
DeleteMember(boardID, userID string) error
|
||||
GetMemberForBoard(boardID, userID string) (*model.BoardMember, error)
|
||||
GetBoardMemberHistory(boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error)
|
||||
GetMembersForBoard(boardID string) ([]*model.BoardMember, error)
|
||||
GetMembersForUser(userID string) ([]*model.BoardMember, error)
|
||||
SearchBoardsForUserAndTeam(term, userID, teamID string) ([]*model.Board, error)
|
||||
|
@ -517,12 +517,20 @@ func testSaveMember(t *testing.T, store store.Store) {
|
||||
SchemeAdmin: true,
|
||||
}
|
||||
|
||||
memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0)
|
||||
require.NoError(t, err)
|
||||
initialMemberHistory := len(memberHistory)
|
||||
|
||||
nbm, err := store.SaveMember(bm)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userID, nbm.UserID)
|
||||
require.Equal(t, boardID, nbm.BoardID)
|
||||
|
||||
require.True(t, nbm.SchemeAdmin)
|
||||
|
||||
memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, memberHistory, initialMemberHistory+1)
|
||||
})
|
||||
|
||||
t.Run("should correctly update a member", func(t *testing.T) {
|
||||
@ -533,6 +541,10 @@ func testSaveMember(t *testing.T, store store.Store) {
|
||||
SchemeViewer: true,
|
||||
}
|
||||
|
||||
memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0)
|
||||
require.NoError(t, err)
|
||||
initialMemberHistory := len(memberHistory)
|
||||
|
||||
nbm, err := store.SaveMember(bm)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userID, nbm.UserID)
|
||||
@ -541,6 +553,10 @@ func testSaveMember(t *testing.T, store store.Store) {
|
||||
require.False(t, nbm.SchemeAdmin)
|
||||
require.True(t, nbm.SchemeEditor)
|
||||
require.True(t, nbm.SchemeViewer)
|
||||
|
||||
memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, memberHistory, initialMemberHistory)
|
||||
})
|
||||
}
|
||||
|
||||
@ -626,7 +642,15 @@ func testDeleteMember(t *testing.T, store store.Store) {
|
||||
boardID := testBoardID
|
||||
|
||||
t.Run("should return nil if deleting a nonexistent member", func(t *testing.T) {
|
||||
memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0)
|
||||
require.NoError(t, err)
|
||||
initialMemberHistory := len(memberHistory)
|
||||
|
||||
require.NoError(t, store.DeleteMember(boardID, userID))
|
||||
|
||||
memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, memberHistory, initialMemberHistory)
|
||||
})
|
||||
|
||||
t.Run("should correctly delete a member", func(t *testing.T) {
|
||||
@ -640,11 +664,19 @@ func testDeleteMember(t *testing.T, store store.Store) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, nbm)
|
||||
|
||||
memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0)
|
||||
require.NoError(t, err)
|
||||
initialMemberHistory := len(memberHistory)
|
||||
|
||||
require.NoError(t, store.DeleteMember(boardID, userID))
|
||||
|
||||
rbm, err := store.GetMemberForBoard(boardID, userID)
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
require.Nil(t, rbm)
|
||||
|
||||
memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, memberHistory, initialMemberHistory+1)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
5.3.0
|
||||
5.4.0
|
@ -1840,6 +1840,25 @@ paths:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/api/v1/users/me/memberships:
|
||||
get:
|
||||
description: Returns the currently users board memberships
|
||||
operationId: getMyMemberships
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/BoardMember'
|
||||
type: array
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/api/v1/workspaces/{workspaceID}/blocks/{blockID}/undelete:
|
||||
post:
|
||||
description: Undeletes a block
|
||||
@ -1900,6 +1919,33 @@ paths:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/boards/{boardID}/join:
|
||||
post:
|
||||
description: Become a member of a board
|
||||
operationId: joinBoard
|
||||
parameters:
|
||||
- description: Board ID
|
||||
in: path
|
||||
name: boardID
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
schema:
|
||||
$ref: '#/definitions/BoardMember'
|
||||
"404":
|
||||
description: board not found
|
||||
"503":
|
||||
description: access denied
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/boards/{boardID}/members:
|
||||
post:
|
||||
description: Adds a new member to a board
|
||||
|
2
webapp/cypress/global.d.ts
vendored
2
webapp/cypress/global.d.ts
vendored
@ -15,7 +15,7 @@ declare namespace Cypress {
|
||||
apiGetMe: () => Chainable<string>
|
||||
apiChangePassword: (userId: string, oldPassword: string, newPassword: string) => Chainable
|
||||
apiInitServer: () => Chainable
|
||||
apiDeleteBlock: (id: string) => Chainable
|
||||
apiDeleteBoard: (id: string) => Chainable
|
||||
apiResetBoards: () => Chainable
|
||||
apiSkipTour: (userID: string) => Chainable
|
||||
|
||||
|
@ -97,7 +97,7 @@ describe('Card URL Property', () => {
|
||||
const addView = (type: ViewType) => {
|
||||
cy.log(`**Add ${type} view**`)
|
||||
cy.findByRole('button', {name: 'View menu'}).click()
|
||||
cy.findByText('Add view').click()
|
||||
cy.findByText('Add view').realHover()
|
||||
cy.findByRole('button', {name: type}).click()
|
||||
cy.findByRole('textbox', {name: `${type} view`}).should('exist')
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ describe('Create and delete board / card', () => {
|
||||
// Create table view
|
||||
cy.log('**Create table view**')
|
||||
cy.get('.ViewHeader').get('.DropdownIcon').first().parent().click()
|
||||
cy.get('.ViewHeader').contains('Add view').click()
|
||||
cy.get('.ViewHeader').contains('Add view').realHover()
|
||||
cy.get('.ViewHeader').
|
||||
contains('Add view').
|
||||
parent().
|
||||
|
@ -52,32 +52,32 @@ Cypress.Commands.add('apiInitServer', () => {
|
||||
return cy.apiRegisterUser(data, '', false).apiLoginUser(data)
|
||||
})
|
||||
|
||||
Cypress.Commands.add('apiDeleteBlock', (id: string) => {
|
||||
Cypress.Commands.add('apiDeleteBoard', (id: string) => {
|
||||
return cy.request({
|
||||
method: 'DELETE',
|
||||
url: `/api/v1/workspaces/0/blocks/${encodeURIComponent(id)}`,
|
||||
url: `/api/v1/boards/${encodeURIComponent(id)}`,
|
||||
...headers(),
|
||||
})
|
||||
})
|
||||
|
||||
const deleteBlocks = (ids: string[]) => {
|
||||
const deleteBoards = (ids: string[]) => {
|
||||
if (ids.length === 0) {
|
||||
return
|
||||
}
|
||||
const [id, ...other] = ids
|
||||
cy.apiDeleteBlock(id).then(() => deleteBlocks(other))
|
||||
cy.apiDeleteBoard(id).then(() => deleteBoards(other))
|
||||
}
|
||||
|
||||
Cypress.Commands.add('apiResetBoards', () => {
|
||||
return cy.request({
|
||||
method: 'GET',
|
||||
url: '/api/v1/workspaces/0/blocks?type=board',
|
||||
url: '/api/v1/teams/0/boards',
|
||||
...headers(),
|
||||
}).then((response) => {
|
||||
if (Array.isArray(response.body)) {
|
||||
const boards = response.body as Board[]
|
||||
const toDelete = boards.filter((b) => !b.isTemplate).map((b) => b.id)
|
||||
deleteBlocks(toDelete)
|
||||
deleteBoards(toDelete)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
546
webapp/package-lock.json
generated
546
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -61,7 +61,7 @@
|
||||
"react-intl": "^5.24.7",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^5.2.1",
|
||||
"react-select": "^4.3.0",
|
||||
"react-select": "^5.2.2",
|
||||
"trim-newlines": "^4.0.2"
|
||||
},
|
||||
"jest": {
|
||||
@ -108,7 +108,7 @@
|
||||
"@types/react-intl": "^3.0.0",
|
||||
"@types/react-redux": "^7.1.23",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-select": "^4.0.13",
|
||||
"@types/react-select": "^5.0.0",
|
||||
"@types/redux-mock-store": "^1.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||
"@typescript-eslint/parser": "^5.16.0",
|
||||
|
@ -88,6 +88,8 @@ Object {
|
||||
<div
|
||||
class="MenuOption MenuSeparator menu-separator"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Duplicate view"
|
||||
class="MenuOption TextOption menu-option"
|
||||
@ -105,6 +107,8 @@ Object {
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Delete view"
|
||||
class="MenuOption TextOption menu-option"
|
||||
@ -122,6 +126,8 @@ Object {
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="MenuOption SubMenuOption menu-option"
|
||||
id="__addView"
|
||||
@ -258,6 +264,8 @@ Object {
|
||||
<div
|
||||
class="MenuOption MenuSeparator menu-separator"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Duplicate view"
|
||||
class="MenuOption TextOption menu-option"
|
||||
@ -275,6 +283,8 @@ Object {
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Delete view"
|
||||
class="MenuOption TextOption menu-option"
|
||||
@ -292,6 +302,8 @@ Object {
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="MenuOption SubMenuOption menu-option"
|
||||
id="__addView"
|
||||
@ -486,6 +498,9 @@ Object {
|
||||
class="MenuOption MenuSeparator menu-separator"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -600,6 +615,9 @@ Object {
|
||||
class="MenuOption MenuSeparator menu-separator"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
|
@ -27,21 +27,27 @@ const BoardTemplateSelectorPreview = (props: Props) => {
|
||||
const [activeTemplateCards, setActiveTemplateCards] = useState<Card[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true
|
||||
if (activeTemplate) {
|
||||
setActiveTemplateCards([])
|
||||
setActiveView(null)
|
||||
setActiveTemplateCards([])
|
||||
octoClient.getAllBlocks(activeTemplate.id).then((blocks) => {
|
||||
const cards = blocks.filter((b) => b.type === 'card')
|
||||
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)
|
||||
}
|
||||
if (cards.length > 0) {
|
||||
setActiveTemplateCards(cards as Card[])
|
||||
if (isSubscribed) {
|
||||
const cards = blocks.filter((b) => b.type === 'card')
|
||||
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)
|
||||
}
|
||||
if (cards.length > 0) {
|
||||
setActiveTemplateCards(cards as Card[])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
isSubscribed = false
|
||||
}
|
||||
}, [activeTemplate])
|
||||
|
||||
const dateDisplayProperty = useMemo(() => {
|
||||
|
@ -86,8 +86,12 @@ exports[`components/calculations/Calculation should match snapshot - option chan
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="CalculationOptions css-2b097c-container"
|
||||
class="CalculationOptions css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -101,15 +105,21 @@ exports[`components/calculations/Calculation should match snapshot - option chan
|
||||
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__single-value css-1brck82-singleValue"
|
||||
class="CalculationOptions__single-value css-1qlwihv-singleValue"
|
||||
>
|
||||
Calculate
|
||||
</div>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
class="css-wmatm6-dummyInput-DummyInput"
|
||||
aria-controls="react-select-2-listbox"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-2-listbox"
|
||||
aria-readonly="true"
|
||||
class="css-mohuvp-dummyInput-DummyInput"
|
||||
id="react-select-2-input"
|
||||
readonly=""
|
||||
inputmode="none"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
|
@ -3,8 +3,12 @@
|
||||
exports[`components/calculations/Options should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="CalculationOptions css-2b097c-container"
|
||||
class="CalculationOptions css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -18,15 +22,21 @@ exports[`components/calculations/Options should match snapshot 1`] = `
|
||||
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__single-value css-1brck82-singleValue"
|
||||
class="CalculationOptions__single-value css-1qlwihv-singleValue"
|
||||
>
|
||||
Calculate
|
||||
</div>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
class="css-wmatm6-dummyInput-DummyInput"
|
||||
aria-controls="react-select-2-listbox"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-2-listbox"
|
||||
aria-readonly="true"
|
||||
class="css-mohuvp-dummyInput-DummyInput"
|
||||
id="react-select-2-input"
|
||||
readonly=""
|
||||
inputmode="none"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
@ -76,8 +86,12 @@ exports[`components/calculations/Options should match snapshot 1`] = `
|
||||
exports[`components/calculations/Options should match snapshot menu open 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="CalculationOptions css-2b097c-container"
|
||||
class="CalculationOptions css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-3-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -91,15 +105,21 @@ exports[`components/calculations/Options should match snapshot menu open 1`] = `
|
||||
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__single-value css-1brck82-singleValue"
|
||||
class="CalculationOptions__single-value css-1qlwihv-singleValue"
|
||||
>
|
||||
Calculate
|
||||
</div>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
class="css-wmatm6-dummyInput-DummyInput"
|
||||
aria-controls="react-select-3-listbox"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-3-listbox"
|
||||
aria-readonly="true"
|
||||
class="css-mohuvp-dummyInput-DummyInput"
|
||||
id="react-select-3-input"
|
||||
readonly=""
|
||||
inputmode="none"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
@ -139,11 +159,13 @@ exports[`components/calculations/Options should match snapshot menu open 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="CalculationOptions__menu css-1rsmi4x-menu"
|
||||
id="react-select-3-listbox"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__menu-list css-g29tl0-MenuList"
|
||||
>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="CalculationOptions__option css-14xsrqy-option"
|
||||
id="react-select-3-option-0"
|
||||
tabindex="-1"
|
||||
@ -151,6 +173,7 @@ exports[`components/calculations/Options should match snapshot menu open 1`] = `
|
||||
Count
|
||||
</div>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="CalculationOptions__option css-14xsrqy-option"
|
||||
id="react-select-3-option-1"
|
||||
tabindex="-1"
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import Select, {components, IndicatorProps} from 'react-select'
|
||||
import Select, {components, DropdownIndicatorProps} from 'react-select'
|
||||
|
||||
import {CSSObject} from '@emotion/serialize'
|
||||
|
||||
@ -151,7 +151,7 @@ const styles = {
|
||||
}),
|
||||
}
|
||||
|
||||
const DropdownIndicator = (props: IndicatorProps<Option, false>) => {
|
||||
const DropdownIndicator = (props: DropdownIndicatorProps<Option, false>) => {
|
||||
return (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<ChevronUp/>
|
||||
|
@ -329,8 +329,12 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="CalculationOptions css-2b097c-container"
|
||||
class="CalculationOptions css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -344,15 +348,21 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
|
||||
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__single-value css-1brck82-singleValue"
|
||||
class="CalculationOptions__single-value css-1qlwihv-singleValue"
|
||||
>
|
||||
Count
|
||||
</div>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
class="css-wmatm6-dummyInput-DummyInput"
|
||||
aria-controls="react-select-2-listbox"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-2-listbox"
|
||||
aria-readonly="true"
|
||||
class="css-mohuvp-dummyInput-DummyInput"
|
||||
id="react-select-2-input"
|
||||
readonly=""
|
||||
inputmode="none"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
@ -392,6 +402,7 @@ exports[`src/component/kanban/kanban return kanban and click on KanbanCalculatio
|
||||
</div>
|
||||
<div
|
||||
class="CalculationOptions__menu css-1rsmi4x-menu"
|
||||
id="react-select-2-listbox"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__menu-list css-g29tl0-MenuList"
|
||||
|
@ -33,8 +33,12 @@ exports[`components/kanban/calculation/KanbanCalculation calculations menu open
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="CalculationOptions css-2b097c-container"
|
||||
class="CalculationOptions css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -48,15 +52,21 @@ exports[`components/kanban/calculation/KanbanCalculation calculations menu open
|
||||
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__single-value css-1brck82-singleValue"
|
||||
class="CalculationOptions__single-value css-1qlwihv-singleValue"
|
||||
>
|
||||
Count
|
||||
</div>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
class="css-wmatm6-dummyInput-DummyInput"
|
||||
aria-controls="react-select-2-listbox"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-2-listbox"
|
||||
aria-readonly="true"
|
||||
class="css-mohuvp-dummyInput-DummyInput"
|
||||
id="react-select-2-input"
|
||||
readonly=""
|
||||
inputmode="none"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
@ -96,6 +106,7 @@ exports[`components/kanban/calculation/KanbanCalculation calculations menu open
|
||||
</div>
|
||||
<div
|
||||
class="CalculationOptions__menu css-1rsmi4x-menu"
|
||||
id="react-select-2-listbox"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__menu-list css-g29tl0-MenuList"
|
||||
|
@ -3,8 +3,12 @@
|
||||
exports[`components/kanban/calculations/KanbanCalculationOptions base case 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="CalculationOptions css-2b097c-container"
|
||||
class="CalculationOptions css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -18,15 +22,21 @@ exports[`components/kanban/calculations/KanbanCalculationOptions base case 1`] =
|
||||
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__single-value css-1brck82-singleValue"
|
||||
class="CalculationOptions__single-value css-1qlwihv-singleValue"
|
||||
>
|
||||
Count
|
||||
</div>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
class="css-wmatm6-dummyInput-DummyInput"
|
||||
aria-controls="react-select-2-listbox"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-2-listbox"
|
||||
aria-readonly="true"
|
||||
class="css-mohuvp-dummyInput-DummyInput"
|
||||
id="react-select-2-input"
|
||||
readonly=""
|
||||
inputmode="none"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
@ -76,8 +86,12 @@ exports[`components/kanban/calculations/KanbanCalculationOptions base case 1`] =
|
||||
exports[`components/kanban/calculations/KanbanCalculationOptions with menu open 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="CalculationOptions css-2b097c-container"
|
||||
class="CalculationOptions css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-3-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -91,15 +105,21 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with menu open
|
||||
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__single-value css-1brck82-singleValue"
|
||||
class="CalculationOptions__single-value css-1qlwihv-singleValue"
|
||||
>
|
||||
Count
|
||||
</div>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
class="css-wmatm6-dummyInput-DummyInput"
|
||||
aria-controls="react-select-3-listbox"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-3-listbox"
|
||||
aria-readonly="true"
|
||||
class="css-mohuvp-dummyInput-DummyInput"
|
||||
id="react-select-3-input"
|
||||
readonly=""
|
||||
inputmode="none"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
@ -139,6 +159,7 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with menu open
|
||||
</div>
|
||||
<div
|
||||
class="CalculationOptions__menu css-1rsmi4x-menu"
|
||||
id="react-select-3-listbox"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__menu-list css-g29tl0-MenuList"
|
||||
@ -231,8 +252,12 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with menu open
|
||||
exports[`components/kanban/calculations/KanbanCalculationOptions with submenu open 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="CalculationOptions css-2b097c-container"
|
||||
class="CalculationOptions css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-4-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -246,15 +271,21 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with submenu op
|
||||
class="CalculationOptions__value-container CalculationOptions__value-container--has-value css-1mxrbau-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__single-value css-1brck82-singleValue"
|
||||
class="CalculationOptions__single-value css-1qlwihv-singleValue"
|
||||
>
|
||||
Count
|
||||
</div>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
class="css-wmatm6-dummyInput-DummyInput"
|
||||
aria-controls="react-select-4-listbox"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-4-listbox"
|
||||
aria-readonly="true"
|
||||
class="css-mohuvp-dummyInput-DummyInput"
|
||||
id="react-select-4-input"
|
||||
readonly=""
|
||||
inputmode="none"
|
||||
role="combobox"
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
@ -294,6 +325,7 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with submenu op
|
||||
</div>
|
||||
<div
|
||||
class="CalculationOptions__menu css-1rsmi4x-menu"
|
||||
id="react-select-4-listbox"
|
||||
>
|
||||
<div
|
||||
class="CalculationOptions__menu-list css-g29tl0-MenuList"
|
||||
|
@ -93,7 +93,7 @@ describe('components/properties/multiSelect', () => {
|
||||
|
||||
userEvent.click(screen.getByTestId(nonEditableMultiSelectTestId))
|
||||
|
||||
expect(screen.getByRole('textbox', {name: /value selector/i})).toBeInTheDocument()
|
||||
expect(screen.getByRole('combobox', {name: /value selector/i})).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('can select a option', async () => {
|
||||
@ -118,7 +118,7 @@ describe('components/properties/multiSelect', () => {
|
||||
|
||||
userEvent.click(screen.getByTestId(nonEditableMultiSelectTestId))
|
||||
|
||||
userEvent.type(screen.getByRole('textbox', {name: /value selector/i}), 'b{enter}')
|
||||
userEvent.type(screen.getByRole('combobox', {name: /value selector/i}), 'b{enter}')
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['multi-option-1', 'multi-option-2'])
|
||||
})
|
||||
@ -175,7 +175,7 @@ describe('components/properties/multiSelect', () => {
|
||||
|
||||
userEvent.click(screen.getByTestId(nonEditableMultiSelectTestId))
|
||||
|
||||
userEvent.type(screen.getByRole('textbox', {name: /value selector/i}), 'new-value{enter}')
|
||||
userEvent.type(screen.getByRole('combobox', {name: /value selector/i}), 'new-value{enter}')
|
||||
|
||||
const selectedValues = propertyTemplate.options.filter((option: IPropertyOption) => propertyValue.includes(option.id))
|
||||
|
||||
|
@ -186,7 +186,7 @@ describe('components/properties/select', () => {
|
||||
))
|
||||
|
||||
userEvent.click(screen.getByTestId(nonEditableSelectTestId))
|
||||
userEvent.type(screen.getByRole('textbox', {name: /value selector/i}), `${newOption}{enter}`)
|
||||
userEvent.type(screen.getByRole('combobox', {name: /value selector/i}), `${newOption}{enter}`)
|
||||
|
||||
expect(onCreate).toHaveBeenCalledWith(newOption)
|
||||
})
|
||||
|
@ -3,8 +3,12 @@
|
||||
exports[`components/properties/user not readonly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UserProperty css-2b097c-container"
|
||||
class="UserProperty css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-3-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -15,10 +19,10 @@ exports[`components/properties/user not readonly 1`] = `
|
||||
class="react-select__control css-18140j1-Control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-o7cxt9-ValueContainer"
|
||||
class="react-select__value-container react-select__value-container--has-value css-433wy7-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-14cfm31-singleValue"
|
||||
class="react-select__single-value css-1lixa2z-singleValue"
|
||||
>
|
||||
<div
|
||||
class="UserProperty-item"
|
||||
@ -27,28 +31,27 @@ exports[`components/properties/user not readonly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class="react-select__input-container css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-3-listbox"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-3-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="react-select__input"
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-3-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-3-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -100,8 +103,12 @@ exports[`components/properties/user not readonly 1`] = `
|
||||
exports[`components/properties/user not readonly not existing user 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UserProperty css-2b097c-container"
|
||||
class="UserProperty css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -112,36 +119,37 @@ exports[`components/properties/user not readonly not existing user 1`] = `
|
||||
class="react-select__control css-18140j1-Control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container css-o7cxt9-ValueContainer"
|
||||
class="react-select__value-container css-433wy7-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="react-select__placeholder css-1wa3eu0-placeholder"
|
||||
class="react-select__placeholder css-14el2xx-placeholder"
|
||||
id="react-select-2-placeholder"
|
||||
>
|
||||
Empty
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class="react-select__input-container css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-2-listbox"
|
||||
aria-describedby="react-select-2-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-2-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="react-select__input"
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-2-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-2-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -190,8 +198,12 @@ exports[`components/properties/user readonly view 1`] = `
|
||||
exports[`components/properties/user user dropdown open 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UserProperty css-2b097c-container"
|
||||
class="UserProperty css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-4-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -211,10 +223,10 @@ exports[`components/properties/user user dropdown open 1`] = `
|
||||
class="react-select__control react-select__control--is-focused react-select__control--menu-is-open css-18140j1-Control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-o7cxt9-ValueContainer"
|
||||
class="react-select__value-container react-select__value-container--has-value css-433wy7-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-14cfm31-singleValue"
|
||||
class="react-select__single-value css-1lixa2z-singleValue"
|
||||
>
|
||||
<div
|
||||
class="UserProperty-item"
|
||||
@ -223,28 +235,27 @@ exports[`components/properties/user user dropdown open 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class="react-select__input-container css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-4-listbox"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-4-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="react-select__input"
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-4-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-4-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -291,11 +302,13 @@ exports[`components/properties/user user dropdown open 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="react-select__menu css-10b6da7-menu"
|
||||
id="react-select-4-listbox"
|
||||
>
|
||||
<div
|
||||
class="react-select__menu-list css-g29tl0-MenuList"
|
||||
>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="react-select__option react-select__option--is-focused react-select__option--is-selected css-10e3bcm-option"
|
||||
id="react-select-4-option-0"
|
||||
tabindex="-1"
|
||||
|
@ -127,7 +127,7 @@ describe('components/properties/user', () => {
|
||||
if (container) {
|
||||
// this is the actual element where the click event triggers
|
||||
// opening of the dropdown
|
||||
const userProperty = container.querySelector('.UserProperty > div > div:nth-child(1) > div:nth-child(2) > div > input')
|
||||
const userProperty = container.querySelector('.UserProperty > div > div:nth-child(1) > div:nth-child(2) > input')
|
||||
expect(userProperty).not.toBeNull()
|
||||
|
||||
act(() => {
|
||||
|
@ -24,6 +24,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="cardToolbar"
|
||||
>
|
||||
<span
|
||||
class="text-heading5"
|
||||
>
|
||||
Share Board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="share-input__container"
|
||||
@ -35,8 +44,12 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<div
|
||||
class="userSearchInput css-2b097c-container"
|
||||
class="userSearchInput css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-4-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -47,36 +60,37 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
class=" css-1wmrr75-Control"
|
||||
>
|
||||
<div
|
||||
class=" css-kpfmlq-ValueContainer"
|
||||
class=" css-30zlo3-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-1wa3eu0-placeholder"
|
||||
class=" css-14el2xx-placeholder"
|
||||
id="react-select-4-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class=" css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-4-listbox"
|
||||
aria-describedby="react-select-4-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-4-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-4-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-4-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -87,7 +101,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
class=" css-byrije-loadingIndicator"
|
||||
>
|
||||
<span
|
||||
class="css-1yvy2vo-LoadingDot"
|
||||
class="css-1xtdfmb-LoadingDot"
|
||||
/>
|
||||
<span
|
||||
class="css-zoievk-LoadingDot"
|
||||
@ -234,6 +248,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="cardToolbar"
|
||||
>
|
||||
<span
|
||||
class="text-heading5"
|
||||
>
|
||||
Share Board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="share-input__container"
|
||||
@ -245,8 +268,12 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<div
|
||||
class="userSearchInput css-2b097c-container"
|
||||
class="userSearchInput css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-4-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -257,36 +284,37 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
class=" css-1wmrr75-Control"
|
||||
>
|
||||
<div
|
||||
class=" css-kpfmlq-ValueContainer"
|
||||
class=" css-30zlo3-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-1wa3eu0-placeholder"
|
||||
class=" css-14el2xx-placeholder"
|
||||
id="react-select-4-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class=" css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-4-listbox"
|
||||
aria-describedby="react-select-4-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-4-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-4-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-4-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -297,7 +325,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
class=" css-byrije-loadingIndicator"
|
||||
>
|
||||
<span
|
||||
class="css-1yvy2vo-LoadingDot"
|
||||
class="css-1xtdfmb-LoadingDot"
|
||||
/>
|
||||
<span
|
||||
class="css-zoievk-LoadingDot"
|
||||
@ -444,6 +472,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="cardToolbar"
|
||||
>
|
||||
<span
|
||||
class="text-heading5"
|
||||
>
|
||||
Share Board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="share-input__container"
|
||||
@ -455,8 +492,12 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<div
|
||||
class="userSearchInput css-2b097c-container"
|
||||
class="userSearchInput css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-5-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -467,36 +508,37 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene
|
||||
class=" css-1wmrr75-Control"
|
||||
>
|
||||
<div
|
||||
class=" css-kpfmlq-ValueContainer"
|
||||
class=" css-30zlo3-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-1wa3eu0-placeholder"
|
||||
class=" css-14el2xx-placeholder"
|
||||
id="react-select-5-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class=" css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-5-listbox"
|
||||
aria-describedby="react-select-5-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-5-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-5-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-5-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -507,7 +549,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene
|
||||
class=" css-byrije-loadingIndicator"
|
||||
>
|
||||
<span
|
||||
class="css-1yvy2vo-LoadingDot"
|
||||
class="css-1xtdfmb-LoadingDot"
|
||||
/>
|
||||
<span
|
||||
class="css-zoievk-LoadingDot"
|
||||
@ -677,6 +719,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="cardToolbar"
|
||||
>
|
||||
<span
|
||||
class="text-heading5"
|
||||
>
|
||||
Share Board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="share-input__container"
|
||||
@ -688,8 +739,12 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<div
|
||||
class="userSearchInput css-2b097c-container"
|
||||
class="userSearchInput css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-6-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -700,36 +755,37 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc
|
||||
class=" css-1wmrr75-Control"
|
||||
>
|
||||
<div
|
||||
class=" css-kpfmlq-ValueContainer"
|
||||
class=" css-30zlo3-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-1wa3eu0-placeholder"
|
||||
class=" css-14el2xx-placeholder"
|
||||
id="react-select-6-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class=" css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-6-listbox"
|
||||
aria-describedby="react-select-6-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-6-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-6-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-6-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -740,7 +796,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc
|
||||
class=" css-byrije-loadingIndicator"
|
||||
>
|
||||
<span
|
||||
class="css-1yvy2vo-LoadingDot"
|
||||
class="css-1xtdfmb-LoadingDot"
|
||||
/>
|
||||
<span
|
||||
class="css-zoievk-LoadingDot"
|
||||
@ -910,6 +966,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="cardToolbar"
|
||||
>
|
||||
<span
|
||||
class="text-heading5"
|
||||
>
|
||||
Share Board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="share-input__container"
|
||||
@ -921,8 +986,12 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<div
|
||||
class="userSearchInput css-2b097c-container"
|
||||
class="userSearchInput css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-7-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -933,36 +1002,37 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
|
||||
class=" css-1wmrr75-Control"
|
||||
>
|
||||
<div
|
||||
class=" css-kpfmlq-ValueContainer"
|
||||
class=" css-30zlo3-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-1wa3eu0-placeholder"
|
||||
class=" css-14el2xx-placeholder"
|
||||
id="react-select-7-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class=" css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-7-listbox"
|
||||
aria-describedby="react-select-7-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-7-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-7-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-7-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -973,7 +1043,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
|
||||
class=" css-byrije-loadingIndicator"
|
||||
>
|
||||
<span
|
||||
class="css-1yvy2vo-LoadingDot"
|
||||
class="css-1xtdfmb-LoadingDot"
|
||||
/>
|
||||
<span
|
||||
class="css-zoievk-LoadingDot"
|
||||
@ -1143,6 +1213,15 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="cardToolbar"
|
||||
>
|
||||
<span
|
||||
class="text-heading5"
|
||||
>
|
||||
Share Board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="share-input__container"
|
||||
@ -1154,8 +1233,12 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<div
|
||||
class="userSearchInput css-2b097c-container"
|
||||
class="userSearchInput css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -1166,36 +1249,37 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
|
||||
class=" css-1wmrr75-Control"
|
||||
>
|
||||
<div
|
||||
class=" css-kpfmlq-ValueContainer"
|
||||
class=" css-30zlo3-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-1wa3eu0-placeholder"
|
||||
class=" css-14el2xx-placeholder"
|
||||
id="react-select-2-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class=" css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-2-listbox"
|
||||
aria-describedby="react-select-2-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-2-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-2-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-2-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -1206,7 +1290,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
|
||||
class=" css-byrije-loadingIndicator"
|
||||
>
|
||||
<span
|
||||
class="css-1yvy2vo-LoadingDot"
|
||||
class="css-1xtdfmb-LoadingDot"
|
||||
/>
|
||||
<span
|
||||
class="css-zoievk-LoadingDot"
|
||||
@ -1353,6 +1437,15 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="cardToolbar"
|
||||
>
|
||||
<span
|
||||
class="text-heading5"
|
||||
>
|
||||
Share Board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="share-input__container"
|
||||
@ -1364,8 +1457,12 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<div
|
||||
class="userSearchInput css-2b097c-container"
|
||||
class="userSearchInput css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-3-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -1376,36 +1473,37 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class=" css-1wmrr75-Control"
|
||||
>
|
||||
<div
|
||||
class=" css-kpfmlq-ValueContainer"
|
||||
class=" css-30zlo3-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-1wa3eu0-placeholder"
|
||||
class=" css-14el2xx-placeholder"
|
||||
id="react-select-3-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class=" css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-3-listbox"
|
||||
aria-describedby="react-select-3-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-3-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-3-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-3-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -1416,7 +1514,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class=" css-byrije-loadingIndicator"
|
||||
>
|
||||
<span
|
||||
class="css-1yvy2vo-LoadingDot"
|
||||
class="css-1xtdfmb-LoadingDot"
|
||||
/>
|
||||
<span
|
||||
class="css-zoievk-LoadingDot"
|
||||
@ -1563,6 +1661,15 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="cardToolbar"
|
||||
>
|
||||
<span
|
||||
class="text-heading5"
|
||||
>
|
||||
Share Board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="share-input__container"
|
||||
@ -1574,8 +1681,12 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<div
|
||||
class="userSearchInput css-2b097c-container"
|
||||
class="userSearchInput css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-9-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -1586,36 +1697,37 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class=" css-1wmrr75-Control"
|
||||
>
|
||||
<div
|
||||
class=" css-kpfmlq-ValueContainer"
|
||||
class=" css-30zlo3-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-1wa3eu0-placeholder"
|
||||
class=" css-14el2xx-placeholder"
|
||||
id="react-select-9-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class=" css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-9-listbox"
|
||||
aria-describedby="react-select-9-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-9-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-9-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-9-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -1626,7 +1738,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class=" css-byrije-loadingIndicator"
|
||||
>
|
||||
<span
|
||||
class="css-1yvy2vo-LoadingDot"
|
||||
class="css-1xtdfmb-LoadingDot"
|
||||
/>
|
||||
<span
|
||||
class="css-zoievk-LoadingDot"
|
||||
@ -1773,6 +1885,15 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="cardToolbar"
|
||||
>
|
||||
<span
|
||||
class="text-heading5"
|
||||
>
|
||||
Share Board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="share-input__container"
|
||||
@ -1784,8 +1905,12 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<div
|
||||
class="userSearchInput css-2b097c-container"
|
||||
class="userSearchInput css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-8-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
@ -1796,36 +1921,37 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class=" css-1wmrr75-Control"
|
||||
>
|
||||
<div
|
||||
class=" css-kpfmlq-ValueContainer"
|
||||
class=" css-30zlo3-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class=" css-1wa3eu0-placeholder"
|
||||
class=" css-14el2xx-placeholder"
|
||||
id="react-select-8-placeholder"
|
||||
>
|
||||
Select...
|
||||
</div>
|
||||
<div
|
||||
class="css-1shkodo-Input"
|
||||
class=" css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<div
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-8-listbox"
|
||||
aria-describedby="react-select-8-placeholder"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-8-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class=""
|
||||
style="display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
id="react-select-8-input"
|
||||
spellcheck="false"
|
||||
style="box-sizing: content-box; width: 2px; border: 0px; opacity: 1; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
id="react-select-8-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -1836,7 +1962,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
class=" css-byrije-loadingIndicator"
|
||||
>
|
||||
<span
|
||||
class="css-1yvy2vo-LoadingDot"
|
||||
class="css-1xtdfmb-LoadingDot"
|
||||
/>
|
||||
<span
|
||||
class="css-zoievk-LoadingDot"
|
||||
|
@ -239,10 +239,20 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
|
||||
))
|
||||
}
|
||||
|
||||
const toolbar = (
|
||||
<span className='text-heading5'>
|
||||
<FormattedMessage
|
||||
id={'ShareBoard.Title'}
|
||||
defaultMessage={'Share Board'}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onClose={props.onClose}
|
||||
className='ShareBoardDialog'
|
||||
toolbar={toolbar}
|
||||
>
|
||||
<BoardPermissionGate permissions={[Permission.ManageBoardRoles]}>
|
||||
<div className='share-input__container'>
|
||||
@ -253,7 +263,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
|
||||
value={selectedUser}
|
||||
className={'userSearchInput'}
|
||||
cacheOptions={true}
|
||||
loadOptions={(inputValue) => client.searchTeamUsers(inputValue)}
|
||||
loadOptions={(inputValue: string) => client.searchTeamUsers(inputValue)}
|
||||
components={{DropdownIndicator: () => null, IndicatorSeparator: () => null}}
|
||||
defaultOptions={true}
|
||||
getOptionValue={(u) => u.id}
|
||||
|
@ -160,3 +160,12 @@
|
||||
left: calc(240px - 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.team-sidebar + .product-wrapper {
|
||||
.SidebarBoardItem {
|
||||
.Menu.noselect.left {
|
||||
right: calc(100% - 480px - 64px + 50px);
|
||||
left: calc(64px + 240px - 50px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -257,6 +257,7 @@ exports[`components/table/TableHeaderMenu should match snapshot, title column 1`
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
|
@ -31,6 +31,7 @@ exports[`components/viewHeader/newCardButton return NewCardButton 1`] = `
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
aria-label="Empty card"
|
||||
@ -154,6 +155,7 @@ exports[`components/viewHeader/newCardButton return NewCardButton and addCard 1`
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
aria-label="Empty card"
|
||||
@ -277,6 +279,7 @@ exports[`components/viewHeader/newCardButton return NewCardButton and addCardTem
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
aria-label="Empty card"
|
||||
|
@ -403,6 +403,7 @@ exports[`components/viewHeader/viewHeaderGroupByMenu return groupBy menu 1`] = `
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
aria-label="Status"
|
||||
|
@ -24,6 +24,7 @@ exports[`components/viewHeader/viewHeaderPropertiesMenu return properties menu 1
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
aria-label="Status"
|
||||
|
@ -253,23 +253,29 @@ const ViewMenu = (props: Props) => {
|
||||
/>))}
|
||||
<BoardPermissionGate permissions={[Permission.ManageBoardProperties]}>
|
||||
<Menu.Separator/>
|
||||
{!props.readonly &&
|
||||
</BoardPermissionGate>
|
||||
{!props.readonly &&
|
||||
<BoardPermissionGate permissions={[Permission.ManageBoardProperties]}>
|
||||
<Menu.Text
|
||||
id='__duplicateView'
|
||||
name={duplicateViewText}
|
||||
icon={<DuplicateIcon/>}
|
||||
onClick={handleDuplicateView}
|
||||
/>
|
||||
}
|
||||
{!props.readonly && views.length > 1 &&
|
||||
</BoardPermissionGate>
|
||||
}
|
||||
{!props.readonly && views.length > 1 &&
|
||||
<BoardPermissionGate permissions={[Permission.ManageBoardProperties]}>
|
||||
<Menu.Text
|
||||
id='__deleteView'
|
||||
name={deleteViewText}
|
||||
icon={<DeleteIcon/>}
|
||||
onClick={handleDeleteView}
|
||||
/>
|
||||
}
|
||||
{!props.readonly &&
|
||||
</BoardPermissionGate>
|
||||
}
|
||||
{!props.readonly &&
|
||||
<BoardPermissionGate permissions={[Permission.ManageBoardProperties]}>
|
||||
<Menu.SubMenu
|
||||
id='__addView'
|
||||
name={addViewText}
|
||||
@ -300,8 +306,8 @@ const ViewMenu = (props: Props) => {
|
||||
onClick={handleAddViewCalendar}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
}
|
||||
</BoardPermissionGate>
|
||||
</BoardPermissionGate>
|
||||
}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
@ -435,6 +435,21 @@ class OctoClient {
|
||||
return this.getJson<BoardMember>(response, {} as BoardMember)
|
||||
}
|
||||
|
||||
async joinBoard(boardId: string): Promise<BoardMember|undefined> {
|
||||
Utils.log(`joinBoard: board ${boardId}`)
|
||||
|
||||
const response = await fetch(this.getBaseURL() + `/api/v1/boards/${boardId}/join`, {
|
||||
method: 'POST',
|
||||
headers: this.headers()
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.getJson<BoardMember>(response, {} as BoardMember)
|
||||
}
|
||||
|
||||
async updateBoardMember(member: BoardMember): Promise<Response> {
|
||||
Utils.log(`udpateBoardMember: user ${member.userId} and board ${member.boardId}`)
|
||||
|
||||
|
@ -84,7 +84,7 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
// and fetch its data
|
||||
const result: any = await dispatch(loadBoardData(boardId))
|
||||
if (result.payload.blocks.length === 0 && userId) {
|
||||
const member = await octoClient.createBoardMember({userId, boardId})
|
||||
const member = await octoClient.joinBoard(boardId)
|
||||
if (!member) {
|
||||
UserSettings.setLastBoardID(boardTeamId, null)
|
||||
UserSettings.setLastViewId(boardId, null)
|
||||
|
@ -48,8 +48,11 @@ const WelcomePage = () => {
|
||||
history.replace(queryString.get('r')!)
|
||||
return
|
||||
}
|
||||
|
||||
history.replace(`/team/${currentTeam?.id}`)
|
||||
if (currentTeam) {
|
||||
history.replace(`/team/${currentTeam?.id}`)
|
||||
} else {
|
||||
history.replace('/')
|
||||
}
|
||||
}
|
||||
|
||||
const skipTour = async () => {
|
||||
|
@ -4,7 +4,6 @@ import React from 'react'
|
||||
import {
|
||||
Redirect,
|
||||
Route,
|
||||
useRouteMatch,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import {Utils} from './utils'
|
||||
@ -27,14 +26,10 @@ type RouteProps = {
|
||||
|
||||
function FBRoute(props: RouteProps) {
|
||||
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
|
||||
const match = useRouteMatch<any>()
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||
|
||||
let originalPath
|
||||
if (props.getOriginalPath) {
|
||||
originalPath = props.getOriginalPath(match)
|
||||
}
|
||||
let redirect: React.ReactNode = null
|
||||
|
||||
const showWelcomePage = !clientConfig.featureFlags.disableTour &&
|
||||
Utils.isFocalboardPlugin() &&
|
||||
@ -44,37 +39,38 @@ function FBRoute(props: RouteProps) {
|
||||
!me?.props[UserPropPrefix + UserSettingKey.WelcomePageViewed]
|
||||
|
||||
if (showWelcomePage) {
|
||||
if (originalPath) {
|
||||
return <Redirect to={`/welcome?r=${originalPath}`}/>
|
||||
}
|
||||
return <Redirect to='/welcome'/>
|
||||
}
|
||||
|
||||
if (loggedIn === false && props.loginRequired) {
|
||||
if (originalPath) {
|
||||
let redirectUrl = '/' + Utils.buildURL(originalPath)
|
||||
if (redirectUrl.indexOf('//') === 0) {
|
||||
redirectUrl = redirectUrl.slice(1)
|
||||
redirect = ({match}: any) => {
|
||||
if (props.getOriginalPath) {
|
||||
return <Redirect to={`/welcome?r=${props.getOriginalPath!(match)}`}/>
|
||||
}
|
||||
const loginUrl = `/error?id=not-logged-in&r=${encodeURIComponent(redirectUrl)}`
|
||||
return <Redirect to={loginUrl}/>
|
||||
return <Redirect to='/welcome'/>
|
||||
}
|
||||
return <Redirect to='/error?id=not-logged-in'/>
|
||||
}
|
||||
|
||||
if (loggedIn === true || !props.loginRequired) {
|
||||
return (
|
||||
<Route
|
||||
path={props.path}
|
||||
render={props.render}
|
||||
component={props.component}
|
||||
exact={props.exact}
|
||||
>
|
||||
{props.children}
|
||||
</Route>
|
||||
)
|
||||
if (redirect === null && loggedIn === false && props.loginRequired) {
|
||||
redirect = ({match}: any) => {
|
||||
if (props.getOriginalPath) {
|
||||
let redirectUrl = '/' + Utils.buildURL(props.getOriginalPath!(match))
|
||||
if (redirectUrl.indexOf('//') === 0) {
|
||||
redirectUrl = redirectUrl.slice(1)
|
||||
}
|
||||
const loginUrl = `/error?id=not-logged-in&r=${encodeURIComponent(redirectUrl)}`
|
||||
return <Redirect to={loginUrl}/>
|
||||
}
|
||||
return <Redirect to='/error?id=not-logged-in'/>
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
return (
|
||||
<Route
|
||||
path={props.path}
|
||||
render={props.render}
|
||||
component={props.component}
|
||||
exact={props.exact}
|
||||
>
|
||||
{redirect || props.children}
|
||||
</Route>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FBRoute)
|
||||
|
@ -7,6 +7,7 @@ import {Card} from '../blocks/card'
|
||||
import {IUser} from '../user'
|
||||
import {Board} from '../blocks/board'
|
||||
import {BoardView} from '../blocks/boardView'
|
||||
import {CommentBlock} from '../blocks/commentBlock'
|
||||
import {Utils} from '../utils'
|
||||
import {Constants} from '../constants'
|
||||
import {CardFilter} from '../cardFilter'
|
||||
@ -14,6 +15,7 @@ import {CardFilter} from '../cardFilter'
|
||||
import {loadBoardData, initialReadOnlyLoad} from './initialLoad'
|
||||
import {getCurrentBoard} from './boards'
|
||||
import {getBoardUsers} from './users'
|
||||
import {getLastCommentByCard} from './comments'
|
||||
import {getCurrentView} from './views'
|
||||
import {getSearchText} from './searchText'
|
||||
|
||||
@ -157,7 +159,7 @@ function manualOrder(activeView: BoardView, cardA: Card, cardB: Card) {
|
||||
return indexA - indexB
|
||||
}
|
||||
|
||||
function sortCards(cards: Card[], board: Board, activeView: BoardView, usersById: {[key: string]: IUser}): Card[] {
|
||||
function sortCards(cards: Card[], lastCommentByCard: {[key: string]: CommentBlock}, board: Board, activeView: BoardView, usersById: {[key: string]: IUser}): Card[] {
|
||||
if (!activeView) {
|
||||
return cards
|
||||
}
|
||||
@ -217,7 +219,9 @@ function sortCards(cards: Card[], board: Board, activeView: BoardView, usersById
|
||||
} else if (template.type === 'createdTime') {
|
||||
result = a.createAt - b.createAt
|
||||
} else if (template.type === 'updatedTime') {
|
||||
result = a.updateAt - b.updateAt
|
||||
const aUpdateAt = Math.max(a.updateAt, lastCommentByCard[a.id]?.updateAt || 0)
|
||||
const bUpdateAt = Math.max(b.updateAt, lastCommentByCard[b.id]?.updateAt || 0)
|
||||
result = aUpdateAt - bUpdateAt
|
||||
} else {
|
||||
// Text-based sort
|
||||
|
||||
@ -292,11 +296,12 @@ function searchFilterCards(cards: Card[], board: Board, searchTextRaw: string):
|
||||
|
||||
export const getCurrentViewCardsSortedFilteredAndGrouped = createSelector(
|
||||
getCurrentBoardCards,
|
||||
getLastCommentByCard,
|
||||
getCurrentBoard,
|
||||
getCurrentView,
|
||||
getSearchText,
|
||||
getBoardUsers,
|
||||
(cards, board, view, searchText, users) => {
|
||||
(cards, lastCommentByCard, board, view, searchText, users) => {
|
||||
if (!view || !board || !users || !cards) {
|
||||
return []
|
||||
}
|
||||
@ -308,7 +313,7 @@ export const getCurrentViewCardsSortedFilteredAndGrouped = createSelector(
|
||||
if (searchText) {
|
||||
result = searchFilterCards(result, board, searchText)
|
||||
}
|
||||
result = sortCards(result, board, view, users)
|
||||
result = sortCards(result, lastCommentByCard, board, view, users)
|
||||
return result
|
||||
},
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSlice, PayloadAction} from '@reduxjs/toolkit'
|
||||
import {createSlice, PayloadAction, createSelector} from '@reduxjs/toolkit'
|
||||
|
||||
import {CommentBlock} from '../blocks/commentBlock'
|
||||
|
||||
@ -92,3 +92,17 @@ export function getLastCardComment(cardId: string): (state: RootState) => Commen
|
||||
return comments?.[comments?.length - 1]
|
||||
}
|
||||
}
|
||||
|
||||
export const getLastCommentByCard = createSelector(
|
||||
(state: RootState) => state.comments?.commentsByCard || null,
|
||||
(commentsByCard: {[key: string]: CommentBlock[]}|null): {[key: string]: CommentBlock} => {
|
||||
const lastCommentByCard: {[key: string]: CommentBlock} = {}
|
||||
Object.keys(commentsByCard || {}).forEach((cardId) => {
|
||||
if (commentsByCard && commentsByCard[cardId]) {
|
||||
const comments = commentsByCard[cardId]
|
||||
lastCommentByCard[cardId] = comments?.[comments?.length - 1]
|
||||
}
|
||||
})
|
||||
return lastCommentByCard
|
||||
},
|
||||
)
|
||||
|
@ -12,7 +12,8 @@ import {RootState} from './index'
|
||||
export const initialLoad = createAsyncThunk(
|
||||
'initialLoad',
|
||||
async () => {
|
||||
const [team, teams, boards, boardsMemberships, boardTemplates] = await Promise.all([
|
||||
const [me, team, teams, boards, boardsMemberships, boardTemplates] = await Promise.all([
|
||||
client.getMe(),
|
||||
client.getTeam(),
|
||||
client.getTeams(),
|
||||
client.getBoards(),
|
||||
@ -20,6 +21,11 @@ export const initialLoad = createAsyncThunk(
|
||||
client.getTeamTemplates(),
|
||||
])
|
||||
|
||||
// if no me, normally user not logged in
|
||||
if (!me) {
|
||||
throw new Error(ErrorId.NotLoggedIn)
|
||||
}
|
||||
|
||||
// if no team, either bad id, or user doesn't have access
|
||||
if (!team) {
|
||||
throw new Error(ErrorId.TeamUndefined)
|
||||
|
@ -6,7 +6,7 @@ import SeparatorOption from './separatorOption'
|
||||
import SwitchOption from './switchOption'
|
||||
import TextOption from './textOption'
|
||||
import ColorOption from './colorOption'
|
||||
import SubMenuOption from './subMenuOption'
|
||||
import SubMenuOption, {HoveringContext} from './subMenuOption'
|
||||
import LabelOption from './labelOption'
|
||||
|
||||
import './menu.scss'
|
||||
@ -27,7 +27,7 @@ export default class Menu extends React.PureComponent<Props> {
|
||||
static Label = LabelOption
|
||||
|
||||
public state = {
|
||||
hoveringIdx: -1,
|
||||
hovering: null,
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
@ -36,16 +36,14 @@ export default class Menu extends React.PureComponent<Props> {
|
||||
<div className={'Menu noselect ' + (position || 'bottom')}>
|
||||
<div className='menu-contents'>
|
||||
<div className='menu-options'>
|
||||
{React.Children.map(children, (child, i) => {
|
||||
return addChildMenuItem({
|
||||
child,
|
||||
onMouseEnter: () =>
|
||||
this.setState({
|
||||
hoveringIdx: i,
|
||||
}),
|
||||
isHovering: () => i === this.state.hoveringIdx,
|
||||
})
|
||||
})}
|
||||
{React.Children.map(children, (child) => (
|
||||
<div
|
||||
onMouseEnter={() => this.setState({hovering: child})}
|
||||
>
|
||||
<HoveringContext.Provider value={child == this.state.hovering}>
|
||||
{child}
|
||||
</HoveringContext.Provider>
|
||||
</div>))}
|
||||
</div>
|
||||
|
||||
<div className='menu-spacer hideOnWidescreen'/>
|
||||
@ -67,28 +65,3 @@ export default class Menu extends React.PureComponent<Props> {
|
||||
// No need to do anything, as click bubbled up to MenuWrapper, which closes
|
||||
}
|
||||
}
|
||||
|
||||
function addChildMenuItem(props: {child: React.ReactNode, onMouseEnter: () => void, isHovering: () => boolean}): JSX.Element | null {
|
||||
const {child, onMouseEnter, isHovering} = props
|
||||
if (child !== null) {
|
||||
if (React.isValidElement(child)) {
|
||||
const castedChild = child as React.ReactElement
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
{castedChild.type === SubMenuOption ? (
|
||||
<castedChild.type
|
||||
{...castedChild.props}
|
||||
isHovering={isHovering}
|
||||
/>
|
||||
) : (
|
||||
<castedChild.type {...castedChild.props}/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (null)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import React, {useEffect, useState, useContext} from 'react'
|
||||
|
||||
import SubmenuTriangleIcon from '../icons/submenuTriangle'
|
||||
|
||||
@ -8,25 +8,27 @@ import Menu from '.'
|
||||
|
||||
import './subMenuOption.scss'
|
||||
|
||||
export const HoveringContext = React.createContext(false)
|
||||
|
||||
type SubMenuOptionProps = {
|
||||
id: string
|
||||
name: string
|
||||
position?: 'bottom' | 'top' | 'left' | 'left-bottom'
|
||||
icon?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
isHovering?: boolean
|
||||
}
|
||||
|
||||
function SubMenuOption(props: SubMenuOptionProps): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const isHovering = useContext(HoveringContext)
|
||||
|
||||
const openLeftClass = props.position === 'left' || props.position === 'left-bottom' ? ' open-left' : ''
|
||||
|
||||
useEffect(() => {
|
||||
if (props.isHovering !== undefined) {
|
||||
setIsOpen(props.isHovering)
|
||||
if (isHovering !== undefined) {
|
||||
setIsOpen(isHovering)
|
||||
}
|
||||
}, [props.isHovering])
|
||||
}, [isHovering])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -2,7 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
import {ActionMeta, FormatOptionLabelMeta, ValueType} from 'react-select'
|
||||
import {ActionMeta, OnChangeValue} from 'react-select'
|
||||
import {FormatOptionLabelMeta} from 'react-select/base'
|
||||
import CreatableSelect from 'react-select/creatable'
|
||||
|
||||
import {CSSObject} from '@emotion/serialize'
|
||||
@ -37,7 +38,7 @@ type Props = {
|
||||
|
||||
type LabelProps = {
|
||||
option: IPropertyOption
|
||||
meta: FormatOptionLabelMeta<IPropertyOption, true | false>
|
||||
meta: FormatOptionLabelMeta<IPropertyOption>
|
||||
onChangeColor: (option: IPropertyOption, color: string) => void
|
||||
onDeleteOption: (option: IPropertyOption) => void
|
||||
onDeleteValue?: (value: IPropertyOption) => void
|
||||
@ -163,7 +164,7 @@ function ValueSelector(props: Props): JSX.Element {
|
||||
isMulti={props.isMulti}
|
||||
isClearable={true}
|
||||
styles={valueSelectorStyle}
|
||||
formatOptionLabel={(option: IPropertyOption, meta: FormatOptionLabelMeta<IPropertyOption, true | false>) => (
|
||||
formatOptionLabel={(option: IPropertyOption, meta: FormatOptionLabelMeta<IPropertyOption>) => (
|
||||
<ValueSelectorLabel
|
||||
option={option}
|
||||
meta={meta}
|
||||
@ -178,7 +179,7 @@ function ValueSelector(props: Props): JSX.Element {
|
||||
options={props.options}
|
||||
getOptionLabel={(o: IPropertyOption) => o.value}
|
||||
getOptionValue={(o: IPropertyOption) => o.id}
|
||||
onChange={(value: ValueType<IPropertyOption, true | false>, action: ActionMeta<IPropertyOption>): void => {
|
||||
onChange={(value: OnChangeValue<IPropertyOption, true | false>, action: ActionMeta<IPropertyOption>): void => {
|
||||
if (action.action === 'select-option') {
|
||||
if (Array.isArray(value)) {
|
||||
props.onChange((value as IPropertyOption[]).map((option) => option.id))
|
||||
|
@ -18,10 +18,10 @@ Popular hosted options include:
|
||||
|
||||
## Install Focalboard
|
||||
|
||||
Download the Ubuntu archive package from the appropriate [release in GitHub](https://github.com/mattermost/focalboard/releases). E.g. this is the link for v0.9.2 (which may no longer be the latest one):
|
||||
Download the Ubuntu archive package from the appropriate [release in GitHub](https://github.com/mattermost/focalboard/releases). The example below uses the link for **v0.15.0**, but you're encouraged to use the latest version in the release list:
|
||||
|
||||
```
|
||||
wget https://github.com/mattermost/focalboard/releases/download/v0.9.2/focalboard-server-linux-amd64.tar.gz
|
||||
wget https://github.com/mattermost/focalboard/releases/download/v0.15.0/focalboard-server-linux-amd64.tar.gz
|
||||
tar -xvzf focalboard-server-linux-amd64.tar.gz
|
||||
sudo mv focalboard /opt
|
||||
```
|
||||
|
Loading…
x
Reference in New Issue
Block a user