mirror of
https://github.com/mattermost/focalboard.git
synced 2025-04-11 11:19:56 +02:00
Permissions feature branch (#2578)
* wip * Added data migration for populating categories * wip * Added data migration for populating categories * Store WIP * migration WIP * category CRUD APIs complete * category block API WIP * block category update API done * Fetcehed data into store * Started displayting sidebar data * sidebar WIP * Dashboard - basic changes * Sidebar dashboard btn and board switcher UI only * Sidebar dashboard btn and board switcher UI only * create category dialog WIP * Create category webapp side done * Integrated move card to other category * board to block * Disabled dashboard route for now as we'll implement it in phase 2 * WIP * Added logic to open last board/view on per team level * Add workspace to teams and boards migrations (#1986) * Add workspace to teams and boards migrations * Update json annotations on board models * boards search dialog WIP * Seach dialog WIP * Implemented opening boiard from search results * Boards switcher styliung * Handled update category WS event * Template support * personal server support and styling fixes * test fix WIP * Fixed a bug causing boards to not be moved correctly beteen categories * Fixed webapp tests * fix * Store changes (#2011) * Permissions phase 1 - Websocket updates (#2014) * Store changes * Websockets changes * Permissions phase 1 - Permissions service (#2015) * Store changes * Websockets changes * Permissions service * Api and app updates (#2016) * Store changes * Websockets changes * Permissions service * New API and App changes * Delete and Patch boards and blocks endpoints * Used correct variable * Webapp changes WIP * Open correct team URL * Fixed get block API * Used React context for workspace users * WIP * On load navigation sorted out * WIP * Nav fix * categories WS broadcast * Used real search API * Fixed unfurl ppreview * set active team in sidebar * IMplemented navigation on changing team in sidebar * Misc fixes * close rows inside transaction (#2045) * update syntax for mysql (#2044) * Upadted mutator for new patchBlock API * Updated patchBlock API to use new URL * Listeining to correct event in plugin mode * Implemented WS messages for category operations: * Fix duplicated build tags on Makefile * Sidebar enhancements * Add missing prefix to SQLite migration and fix flaky tests * Sidebar boards menu enhancement * Fix board page interactions (#2144) * Fix patch board card properties error * Fix board interactions * Fix insert blocks interactions * Fix app tests (#2104) * Add json1 tag to vscode launch (#2157) * Fix add, delete and update boards and add board patch generation (#2146) * Fix update boards and add board patch generation * Make add board and add template work, as well as deleting a board * Update the state on board deletion * Delete unused variable * Fix bad parenthesis * Fix board creation inside plugin, options were coming null due websocket message serialization * update property type mutators to use boards API (#2168) * Add permissions modal (#2196) * Initial integration * Permissions modal, websocket updates and API tests implemented * Avoid updating/removing user if there is only one admin left * Fix duplicates on board search * Adds integration test * Addressing PR review comments Co-authored-by: Jesús Espino <jespinog@gmail.com> * Merge * I'm able to compile now * Some fixes around tests execution * Fixing migrations * Fixing migrations order * WIP * Fixing some other compilation problems on tests * Some typescript tests fixed * Fixing javascript tests * Fixing compilation * Fixing some problems to create boards * Load the templates on initial load * Improvements over initial team templates import * Adding new fields in the database * Working on adding duplicate board api * Removing RootID concept entirely * Improving a bit the subscriptions * Fixing store tests for notificationHints * Fixing more tests * fixing tests * Fixing tests * Fixing tests * Fixing some small bugs related to templates * Fixing registration link generation/regeneration * Fixing cypress tests * Adding store tests for duplicateBoard and duplicateBlock * Addressing some TODO comments * Making the export api simpler * Add redirect component for old workspace urls * Removing Dashboard code * Delete only the built-in templates on update * fixing tests * Adding users autocompletion * Updating snapshots * Fixing bad merge * fix panic when creating new card in notifysubscriptions (#2352) * fix lint errors (#2353) * fix lint errors * fix panic when creating new card in notifysubscriptions (#2352) * fix lint errors * fix unit test * Revert "fix unit test" This reverts commit 0ad78aed65745521c0bb45790c9ea91b6c316c44. Co-authored-by: Doug Lauder <wiggin77@warpmail.net> * fix sql syntax error for SearchUsersByTeam (#2357) * Fix mentions delivery (#2358) * fix sql syntax error for SearchUsersByTeam * fix mentions delivery Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * update api for octoClient calls, pass correct variables to mutator (#2359) * Fixing tests after merge * Fix sidebar context menu UI issue (#2399) * Fix notification diff for text blocks (#2386) * fix notification diff for text blocks; fix various linter errors. * fix URLs to cards Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Permissions branch: Fix card links (#2391) * fix notification diff for text blocks; fix various linter errors. * fix URLs to cards Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Fixing sqlite tests * Fixing server tests * Update migrations to create global templates. (#2397) * fix duplicate templates * revert migrate.go * update UI for empty templates * implement updating built-in templates as global (teamId = 0) * handle error if board not found * update unit test * fix more tests * Update blocks_test.go Fix merge issue * fix migration sql error (#2414) * Fixing frontend tests * Set target team ID when using a global template (#2419) * Fix some server tests * Fixing onboarding creation * Permissions branch: Fix unit tests and CI errors (part 1) (#2425) * Fixing some small memory leaks (#2400) * Fixing some small memory leaks * fixing tests * passing the tags to all test targets * Increasing the timeout of the tests * Fix some type checkings * Permissions branch: Fixes all the linter errors (#2429) * fix linter errors * Reestructuring the router and splitting in more subcomponents (#2403) * Reestructuring the router and splitting in more subcomponents * Removing console.log calls * Removing unneeded selector * Addressing PR comment * Fix redirection to one team when you load directly the boards home path * Using properly the lastTeamID to redirect the user if needed * don't allow last admin change/deleted (#2416) * don't allow last admin change/deleted * update for i18-extract * fixed en.json Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> * Splitting BoardPage component into simpler/smaller components (#2435) * Splitting BoardPage component into simpler/smaller components * Removing unneeded import * Replace go migrate with morph permissions (#2424) * merge origin/replace-go-migrate-with-morph * run go mod tidy on mattermost-plugin and increase test timeout * fix merge issue temprorarily * remove some debug changes * fixing the linter * Allow always team 0 (global) templates fetch (#2472) * Fix problem with viewId 0 in the URL (#2473) * Migrate from binddata to goembed (#2471) * Adding join logic to the board switcher (#2434) * Adding join logic to the board switcher * Using already existing client function and removing the joinBoard one * Adding support for autojoin based on url * Fixing frontend tests * fix webapp compile error, missing enableSharedBoards (#2501) * Fixing duplication on postgres * Adding back views to the sidebar (#2494) * Fix #2507. Update Swagger comments (#2508) * Fix the flash of the template selector on board/team switch (#2490) * Fix the flash of the template selector on board/team switch * More fixes specially around error handling * Fixing the bot badge (#2487) * simplifying a bit the team store sync between channels and focalboard (#2481) * Fix menu tests (#2528) * fix failing menu tests * fix lint error * Added keyboard shortcut for boards switcher (#2407) * Added keyboard shortcut for boards switcher * Fixed a type error * Added some inline comments * Fixed lint * Fixed bug with scroll jumping when the card is opened: (#2477) - avoid remounting of `ScrollingComponent` for each render of `Kanban` component - property `autoFocus` set to false for `CalculationOptions` because it triggers `blur` even for the button in Jest tests and closes the menu - snapshots for tests with `CalculationOptions` updated * Adding the frontend support for permissions and applying it to a big part of the interface. (#2536) * Initial work on permissions gates * Applying permissions gates in more places * Adding more checks to the interface * Adding more permissions gates and keeping the store up to date * fixing some tests * Fixing some more tests * Fixing another test * Fixing all tests and adding some more * Adding no-permission snapshot tests * Addressing PR review comments * Fixing invert behavior * Permissions branch: No sqlstore calls after app shutdown (#2530) * fix webapp compile error, missing enableSharedBoards * refactor app init wip * - ensure all block change notifications are finished before shutting down app - fix unit tests for mysql (insert_at only has 1 second resolution!) * adjust logging Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Fixed migrations to allow upgrading from previous version (#2535) * Added mechanism to check if schema migration is needed * WIP * WIP * WIP * WIP * Fixed migration * Fixed for SQLite * minor cleaniup * Deleted old schema migration table after running migrations * Removed a debug log * Fixed a bug where the code always tried to delete a table which may or may not exist * Show properly the user avatar in the ShareBoard component (#2542) * Fixing the last CI problems from the permissions-branch (#2541) * Fix history ordering * Giving some times to avoid possible race conditions * Empty * Reverting accidental change in the config.json * Optimizing table view (#2540) * Optimizing table view * Reducing the amount of rendering for tables * Some other performance improvements * Improve the activeView updates * Some extra simplifications * Another small improvement * Fixing tests * Fixing linter errors * Reducing a bit the amount of dependency with big objects in the store * Small simplification * Removing Commenter role from the user role selector (#2561) * Shareboard cleanup (#2550) * Initial work on permissions gates * Applying permissions gates in more places * Adding more checks to the interface * Adding more permissions gates and keeping the store up to date * fixing some tests * Fixing some more tests * Fixing another test * Fixing all tests and adding some more * Adding no-permission snapshot tests * Addressing PR review comments * cleanup some shareboard settings * remove unused property, fix for user items being displayed for non admin * revert change, allow users to show Co-authored-by: Jesús Espino <jespinog@gmail.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Fixing comments and cards with the new optimizations in the store (#2560) * Fixing property creation (#2563) * Fix user selection in table view (#2565) * Fixing focus new row in table view (#2567) * Permissions branch: Fix sqlite table lock (CI) (#2568) * fix sqlite table lock * remove test db on teardown * revert .gitignore * fix goimport on migration code * fix typo * more linter fixes * clean up tmp db for sqlstore tests Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Fixing snapshots * Migrating center panel to functional component (#2562) * Migrating center panel to functional component * Fixing some tests * Fixing another test * Fixing linter errors * Fixing types errors * Fixing linter error * Fixing cypress tests * Fixing the last cypress test * Simpliying a bit the code * Making property insertion more robust * Updating checkbox test Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Miguel de la Cruz <miguel@mcrx.me> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Chen-I Lim <46905241+chenilim@users.noreply.github.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> Co-authored-by: kamre <eremchenko@gmail.com>
This commit is contained in:
parent
f3b8a49ea9
commit
aa540e73ce
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
@ -9,6 +9,7 @@
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"buildFlags": "-tags 'json1'",
|
||||
"program": "${workspaceFolder}/server/main",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
|
29
Makefile
29
Makefile
@ -12,6 +12,8 @@ ifeq ($(BUILD_NUMBER),)
|
||||
BUILD_DATE := n/a
|
||||
endif
|
||||
|
||||
BUILD_TAGS += json1
|
||||
|
||||
LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildNumber=$(BUILD_NUMBER)"
|
||||
LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildDate=$(BUILD_DATE)"
|
||||
LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildHash=$(BUILD_HASH)"
|
||||
@ -35,25 +37,25 @@ ci: server-test
|
||||
|
||||
server: ## Build server for local environment.
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=dev")
|
||||
cd server; go build -ldflags '$(LDFLAGS)' -o ../bin/focalboard-server ./main
|
||||
cd server; go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/focalboard-server ./main
|
||||
|
||||
server-mac: ## Build server for Mac.
|
||||
mkdir -p bin/mac
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=mac")
|
||||
cd server; env GOOS=darwin GOARCH=$(MAC_GO_ARCH) go build -ldflags '$(LDFLAGS)' -o ../bin/mac/focalboard-server ./main
|
||||
cd server; env GOOS=darwin GOARCH=$(MAC_GO_ARCH) go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/mac/focalboard-server ./main
|
||||
|
||||
server-linux: ## Build server for Linux.
|
||||
mkdir -p bin/linux
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=linux")
|
||||
cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o ../bin/linux/focalboard-server ./main
|
||||
cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/linux/focalboard-server ./main
|
||||
|
||||
server-win: ## Build server for Windows.
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win")
|
||||
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o ../bin/win/focalboard-server.exe ./main
|
||||
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/win/focalboard-server.exe ./main
|
||||
|
||||
server-dll: ## Build server as Windows DLL.
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win")
|
||||
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -buildmode=c-shared -o ../bin/win-dll/focalboard-server.dll ./main
|
||||
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -buildmode=c-shared -o ../bin/win-dll/focalboard-server.dll ./main
|
||||
|
||||
server-linux-package: server-linux webapp
|
||||
rm -rf package
|
||||
@ -83,7 +85,6 @@ server-linux-package-docker:
|
||||
|
||||
generate: ## Install and run code generators.
|
||||
cd server; go get -modfile=go.tools.mod github.com/golang/mock/mockgen
|
||||
cd server; go get -modfile=go.tools.mod github.com/jteeuwen/go-bindata
|
||||
cd server; go generate ./...
|
||||
|
||||
server-lint: ## Run linters on server code.
|
||||
@ -101,18 +102,18 @@ modd-precheck:
|
||||
fi; \
|
||||
|
||||
watch: modd-precheck ## Run both server and webapp watching for changes
|
||||
modd
|
||||
env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd
|
||||
|
||||
watch-single-user: modd-precheck ## Run both server and webapp in single user mode watching for changes
|
||||
env FOCALBOARDSERVER_ARGS=--single-user modd
|
||||
env FOCALBOARDSERVER_ARGS=--single-user FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd
|
||||
|
||||
watch-server-test: modd-precheck ## Run server tests watching for changes
|
||||
modd -f modd-servertest.conf
|
||||
env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd -f modd-servertest.conf
|
||||
|
||||
server-test: server-test-sqlite server-test-mysql server-test-postgres ## Run server tests
|
||||
|
||||
server-test-sqlite: ## Run server tests using sqlite
|
||||
cd server; go test -race -v -count=1 ./...
|
||||
cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./...
|
||||
|
||||
server-test-mysql: export FB_UNIT_TESTING=1
|
||||
server-test-mysql: export FB_STORE_TEST_DB_TYPE=mysql
|
||||
@ -120,8 +121,9 @@ server-test-mysql: export FB_STORE_TEST_DOCKER_PORT=44445
|
||||
|
||||
server-test-mysql: ## Run server tests using mysql
|
||||
@echo Starting docker container for mysql
|
||||
docker-compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans
|
||||
docker-compose -f ./docker-testing/docker-compose-mysql.yml run start_dependencies
|
||||
cd server; go test -race -v -count=1 ./...
|
||||
cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./...
|
||||
docker-compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans
|
||||
|
||||
server-test-postgres: export FB_UNIT_TESTING=1
|
||||
@ -130,8 +132,9 @@ server-test-postgres: export FB_STORE_TEST_DOCKER_PORT=44446
|
||||
|
||||
server-test-postgres: ## Run server tests using postgres
|
||||
@echo Starting docker container for postgres
|
||||
docker-compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans
|
||||
docker-compose -f ./docker-testing/docker-compose-postgres.yml run start_dependencies
|
||||
cd server; go test -race -v -count=1 ./...
|
||||
cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./...
|
||||
docker-compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans
|
||||
|
||||
webapp: ## Build webapp.
|
||||
@ -141,7 +144,7 @@ webapp-test: ## jest tests for webapp
|
||||
cd webapp; npm run test
|
||||
|
||||
watch-plugin: modd-precheck ## Run and upload the plugin to a development server
|
||||
modd -f modd-watchplugin.conf
|
||||
env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd -f modd-watchplugin.conf
|
||||
|
||||
live-watch-plugin: modd-precheck ## Run and update locally the plugin in the development server
|
||||
cd mattermost-plugin; make live-watch
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"serverRoot": "http://localhost:8000",
|
||||
"port": 8000,
|
||||
"dbtype": "sqlite3",
|
||||
"dbconfig": "./focalboard.db",
|
||||
"port": 8000,
|
||||
"dbtype": "sqlite3",
|
||||
"dbconfig": "./focalboard.db",
|
||||
"dbtableprefix": "",
|
||||
"postgres_dbconfig": "dbname=focalboard sslmode=disable",
|
||||
"test_dbconfig": "file::memory:?cache=shared",
|
||||
@ -21,5 +21,5 @@
|
||||
"authMode": "native",
|
||||
"logging_cfg_file": "",
|
||||
"audit_cfg_file": "",
|
||||
"enablepublicsharedboards": false
|
||||
"enablePublicSharedBoards": false
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ export async function logIn(host: string, username: string, password: string) {
|
||||
|
||||
export async function getBoards(host: string, token: string) {
|
||||
const json = await request('GET', host, 'workspaces/0/blocks?type=board', null, token) as Board[]
|
||||
return json.filter(board => !board.fields.isTemplate)
|
||||
return json.filter(board => !board.isTemplate)
|
||||
}
|
||||
|
||||
export async function findUrlPropertyId(host: string, token: string, boardId: string) {
|
||||
|
@ -129,7 +129,7 @@ function convert(input: Asana): Block[] {
|
||||
type: 'select',
|
||||
options
|
||||
}
|
||||
board.fields.cardProperties = [cardProperty]
|
||||
board.cardProperties = [cardProperty]
|
||||
blocks.push(board)
|
||||
|
||||
// Board view
|
||||
|
@ -90,25 +90,25 @@ function convert(items: any[]) {
|
||||
board.title = 'Jira import'
|
||||
|
||||
// Compile standard properties
|
||||
board.fields.cardProperties = []
|
||||
board.cardProperties = []
|
||||
|
||||
const priorityProperty = buildCardPropertyFromValues('Priority', items.map(o => o.priority?._))
|
||||
board.fields.cardProperties.push(priorityProperty)
|
||||
board.cardProperties.push(priorityProperty)
|
||||
|
||||
const statusProperty = buildCardPropertyFromValues('Status', items.map(o => o.status?._))
|
||||
board.fields.cardProperties.push(statusProperty)
|
||||
board.cardProperties.push(statusProperty)
|
||||
|
||||
const resolutionProperty = buildCardPropertyFromValues('Resolution', items.map(o => o.resolution?._))
|
||||
board.fields.cardProperties.push(resolutionProperty)
|
||||
board.cardProperties.push(resolutionProperty)
|
||||
|
||||
const typeProperty = buildCardPropertyFromValues('Type', items.map(o => o.type?._))
|
||||
board.fields.cardProperties.push(typeProperty)
|
||||
board.cardProperties.push(typeProperty)
|
||||
|
||||
const assigneeProperty = buildCardPropertyFromValues('Assignee', items.map(o => o.assignee?._))
|
||||
board.fields.cardProperties.push(assigneeProperty)
|
||||
board.cardProperties.push(assigneeProperty)
|
||||
|
||||
const reporterProperty = buildCardPropertyFromValues('Reporter', items.map(o => o.reporter?._))
|
||||
board.fields.cardProperties.push(reporterProperty)
|
||||
board.cardProperties.push(reporterProperty)
|
||||
|
||||
const originalUrlProperty: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
@ -116,7 +116,7 @@ function convert(items: any[]) {
|
||||
type: 'url',
|
||||
options: []
|
||||
}
|
||||
board.fields.cardProperties.push(originalUrlProperty)
|
||||
board.cardProperties.push(originalUrlProperty)
|
||||
|
||||
const createdDateProperty: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
@ -124,7 +124,7 @@ function convert(items: any[]) {
|
||||
type: 'date',
|
||||
options: []
|
||||
}
|
||||
board.fields.cardProperties.push(createdDateProperty)
|
||||
board.cardProperties.push(createdDateProperty)
|
||||
|
||||
blocks.push(board)
|
||||
|
||||
@ -240,4 +240,4 @@ function showHelp() {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
export { run }
|
||||
export { run }
|
||||
|
@ -135,7 +135,7 @@ function convert(input: any[], title: string): Block[] {
|
||||
type: 'select',
|
||||
options: []
|
||||
}
|
||||
board.fields.cardProperties.push(cardProperty)
|
||||
board.cardProperties.push(cardProperty)
|
||||
})
|
||||
|
||||
// Set all column types to select
|
||||
@ -177,7 +177,7 @@ function convert(input: any[], title: string): Block[] {
|
||||
continue
|
||||
}
|
||||
|
||||
const cardProperty = board.fields.cardProperties.find((o) => o.name === key)!
|
||||
const cardProperty = board.cardProperties.find((o) => o.name === key)!
|
||||
let option = cardProperty.options.find((o) => o.value === value)
|
||||
if (!option) {
|
||||
const color = optionColors[optionColorIndex % optionColors.length]
|
||||
|
@ -82,7 +82,7 @@ function convert(input: Todoist, project: Project): Block[] {
|
||||
console.log(`Board: ${project.name}`)
|
||||
board.rootId = board.id
|
||||
board.title = project.name
|
||||
board.fields.description = project.name
|
||||
board.description = project.name
|
||||
|
||||
// Convert lists (columns) to a Select property
|
||||
const optionIdMap = new Map<string, string>()
|
||||
@ -114,7 +114,7 @@ function convert(input: Todoist, project: Project): Block[] {
|
||||
type: 'select',
|
||||
options
|
||||
}
|
||||
board.fields.cardProperties = [cardProperty]
|
||||
board.cardProperties = [cardProperty]
|
||||
blocks.push(board)
|
||||
|
||||
// Board view
|
||||
|
@ -68,7 +68,7 @@ function convert(input: Trello): Block[] {
|
||||
console.log(`Board: ${input.name}`)
|
||||
board.rootId = board.id
|
||||
board.title = input.name
|
||||
board.fields.description = input.desc
|
||||
board.description = input.desc
|
||||
|
||||
// Convert lists (columns) to a Select property
|
||||
const optionIdMap = new Map<string, string>()
|
||||
@ -92,7 +92,7 @@ function convert(input: Trello): Block[] {
|
||||
type: 'select',
|
||||
options
|
||||
}
|
||||
board.fields.cardProperties = [cardProperty]
|
||||
board.cardProperties = [cardProperty]
|
||||
blocks.push(board)
|
||||
|
||||
// Board view
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
|
||||
"github.com/webview/webview"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
@ -66,6 +67,8 @@ func runServer(port int) (*server.Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
permissionsService := localpermissions.New(db, logger)
|
||||
|
||||
params := server.Params{
|
||||
Cfg: config,
|
||||
SingleUserToken: sessionToken,
|
||||
@ -74,6 +77,7 @@ func runServer(port int) (*server.Server, error) {
|
||||
ServerID: "",
|
||||
WSAdapter: nil,
|
||||
NotifyBackends: nil,
|
||||
PermissionsService: permissionsService,
|
||||
}
|
||||
|
||||
server, err := server.New(params)
|
||||
|
@ -61,7 +61,6 @@ github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOv
|
||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||
github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-storage-blob-go v0.13.0/go.mod h1:pA9kNqtjUeQF2zOSu4s//nUdBD+e64lEuc4sVnuOfNs=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
@ -91,7 +90,6 @@ github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/
|
||||
github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
|
||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk=
|
||||
@ -235,7 +233,6 @@ github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkb
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY=
|
||||
github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
@ -260,6 +257,7 @@ github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ1
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
|
||||
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -272,20 +270,15 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ=
|
||||
github.com/dhui/dktest v0.3.4 h1:VbUEcaSP+U2/yUr9d2JhSThXYEnDlGabRSHe2rIE46E=
|
||||
github.com/dhui/dktest v0.3.4/go.mod h1:4m4n6lmXlmVfESth7mzdcv8nBI5mOb5UROPqjM02csU=
|
||||
github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU=
|
||||
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompatible h1:nhVo1udYfMj0Jsw0lnqrTjjf33aLpdgW9Wve9fHVzhQ=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
@ -315,6 +308,7 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL
|
||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
@ -410,10 +404,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0=
|
||||
github.com/golang-migrate/migrate/v4 v4.15.0 h1:LKvQ+CgezLw0zuR/ib1y9sQStG0vepWaEVUsQof0bo0=
|
||||
github.com/golang-migrate/migrate/v4 v4.15.0/go.mod h1:g9qbiDvB47WyrRnNu2t2gMZFNHKnatsYRxsGZbCi4EM=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
@ -542,7 +534,6 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
@ -557,7 +548,6 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP
|
||||
github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
|
||||
github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=
|
||||
@ -684,6 +674,7 @@ github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYb
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
@ -778,6 +769,8 @@ github.com/mattermost/mattermost-server/v6 v6.0.0-20210901153517-42e75fad4dae/go
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0/go.mod h1:TUSk5lYJmwfTKTJLXR0eAsjJNlKkWzS5aGZegXG0J08=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20211022142730-a6cca93ba3c3 h1:+E2WOqMrCGZTGjhmWVsszj2Qqx7Amh/OBUedkLLtnP4=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20211022142730-a6cca93ba3c3/go.mod h1:VH26NcOr3xgkSBAvh4q+9+RoBD/M9gYO2F1PISq9KMw=
|
||||
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 h1:agJMxBP8LV0nyV90PZ/BHmmjNyvzTWqR20wLwiXHx14=
|
||||
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw=
|
||||
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
@ -807,6 +800,7 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
@ -852,7 +846,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
@ -905,9 +898,7 @@ github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
||||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/oov/psd v0.0.0-20210618170533-9fb823ddb631/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
@ -990,6 +981,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
@ -1233,7 +1225,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
@ -1321,6 +1312,7 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hM
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -1512,9 +1504,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211006225509-1a26e0398eed h1:E159xujlywdAeN3FqsTBPzRKGUq/pDHolXbuttkC36E=
|
||||
golang.org/x/sys v0.0.0-20211006225509-1a26e0398eed/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1612,6 +1606,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -1830,31 +1825,147 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg=
|
||||
modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878=
|
||||
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
|
||||
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo=
|
||||
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
|
||||
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
|
||||
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
|
||||
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
|
||||
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
|
||||
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
|
||||
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
|
||||
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
|
||||
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
|
||||
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
|
||||
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
|
||||
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
|
||||
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
|
||||
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
|
||||
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
|
||||
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
|
||||
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
|
||||
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
|
||||
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
|
||||
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
|
||||
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
|
||||
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
|
||||
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
|
||||
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
|
||||
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
|
||||
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
|
||||
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
|
||||
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
|
||||
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
|
||||
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
|
||||
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
|
||||
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
|
||||
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
|
||||
modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
|
||||
modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU=
|
||||
modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM=
|
||||
modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko=
|
||||
modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA=
|
||||
modernc.org/ccgo/v3 v3.12.95 h1:Ym2JG2G3P4IyZqjTTojHTl7qO0RysXeGSYPSoKPSBxc=
|
||||
modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8=
|
||||
modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA=
|
||||
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8=
|
||||
modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw=
|
||||
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
|
||||
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM=
|
||||
modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
|
||||
modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
|
||||
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
|
||||
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
|
||||
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
|
||||
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
|
||||
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
|
||||
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
|
||||
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
|
||||
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
|
||||
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
|
||||
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
|
||||
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
|
||||
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
|
||||
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
|
||||
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
|
||||
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
|
||||
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
|
||||
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
|
||||
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
|
||||
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
|
||||
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
|
||||
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
|
||||
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
|
||||
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
|
||||
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
|
||||
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
|
||||
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
|
||||
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
|
||||
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
|
||||
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
|
||||
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
|
||||
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
|
||||
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
|
||||
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
|
||||
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
|
||||
modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ=
|
||||
modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
|
||||
modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
|
||||
modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
|
||||
modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
|
||||
modernc.org/libc v1.11.104 h1:gxoa5b3HPo7OzD4tKZjgnwXk/w//u1oovvjSMP3Q96Q=
|
||||
modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ=
|
||||
modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8=
|
||||
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
|
||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
|
||||
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
|
||||
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
|
||||
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
|
||||
modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
|
||||
modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs=
|
||||
modernc.org/sqlite v1.14.3 h1:psrTwgpEujgWEP3FNdsC9yNh5tSeA77U0GeWhHH4XmQ=
|
||||
modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y=
|
||||
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
|
||||
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo=
|
||||
modernc.org/tcl v1.9.2 h1:YA87dFLOsR2KqMka371a2Xgr+YsyUwo7OmHVSv/kztw=
|
||||
modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
|
||||
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
|
||||
modernc.org/z v1.2.20 h1:DyboxM1sJR2NB803j2StnbnL6jcQXz273OhHDGu8dGk=
|
||||
modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo=
|
||||
modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/notify"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/mmpermissions"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer"
|
||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||
@ -29,16 +30,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
boardsFeatureFlagName = "BoardsFeatureFlags"
|
||||
pluginName = "focalboard"
|
||||
sharedBoardsName = "enablepublicsharedboards"
|
||||
boardsFeatureFlagName = "BoardsFeatureFlags"
|
||||
pluginName = "focalboard"
|
||||
sharedBoardsName = "enablepublicsharedboards"
|
||||
|
||||
notifyFreqCardSecondsKey = "notify_freq_card_seconds"
|
||||
notifyFreqBoardSecondsKey = "notify_freq_board_seconds"
|
||||
)
|
||||
|
||||
type BoardsEmbed struct {
|
||||
OriginalPath string `json:"originalPath"`
|
||||
WorkspaceID string `json:"workspaceID"`
|
||||
TeamID string `json:"teamID"`
|
||||
ViewID string `json:"viewID"`
|
||||
BoardID string `json:"boardID"`
|
||||
CardID string `json:"cardID"`
|
||||
@ -112,7 +114,9 @@ func (p *Plugin) OnActivate() error {
|
||||
db = layeredStore
|
||||
}
|
||||
|
||||
p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db), logger)
|
||||
permissionsService := mmpermissions.New(db, p.API)
|
||||
|
||||
p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db, permissionsService), db, logger)
|
||||
|
||||
backendParams := notifyBackendParams{
|
||||
cfg: cfg,
|
||||
@ -137,13 +141,14 @@ func (p *Plugin) OnActivate() error {
|
||||
mentionsBackend.AddListener(subscriptionsBackend)
|
||||
|
||||
params := server.Params{
|
||||
Cfg: cfg,
|
||||
SingleUserToken: "",
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
ServerID: serverID,
|
||||
WSAdapter: p.wsPluginAdapter,
|
||||
NotifyBackends: notifyBackends,
|
||||
Cfg: cfg,
|
||||
SingleUserToken: "",
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
ServerID: serverID,
|
||||
WSAdapter: p.wsPluginAdapter,
|
||||
NotifyBackends: notifyBackends,
|
||||
PermissionsService: permissionsService,
|
||||
}
|
||||
|
||||
server, err := server.New(params)
|
||||
@ -373,11 +378,11 @@ func postWithBoardsEmbed(post *mmModel.Post) *mmModel.Post {
|
||||
return post
|
||||
}
|
||||
|
||||
workspaceID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||
|
||||
if workspaceID != "" && boardID != "" && viewID != "" && cardID != "" {
|
||||
if teamID != "" && boardID != "" && viewID != "" && cardID != "" {
|
||||
b, _ := json.Marshal(BoardsEmbed{
|
||||
WorkspaceID: workspaceID,
|
||||
TeamID: teamID,
|
||||
BoardID: boardID,
|
||||
ViewID: viewID,
|
||||
CardID: cardID,
|
||||
@ -430,7 +435,7 @@ func getFirstLinkAndShortenAllBoardsLink(postMessage string) (firstLink, newPost
|
||||
return firstLink, newPostMessage
|
||||
}
|
||||
|
||||
func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardID string) {
|
||||
func returnBoardsParams(pathArray []string) (teamID, boardID, viewID, cardID string) {
|
||||
// The reason we are doing this search for the first instance of boards or plugins is to take into account URL subpaths
|
||||
index := -1
|
||||
for i := 0; i < len(pathArray); i++ {
|
||||
@ -441,7 +446,7 @@ func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardI
|
||||
}
|
||||
|
||||
if index == -1 {
|
||||
return workspaceID, boardID, viewID, cardID
|
||||
return teamID, boardID, viewID, cardID
|
||||
}
|
||||
|
||||
// If at index, the parameter in the path is boards,
|
||||
@ -450,27 +455,27 @@ func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardI
|
||||
// If at index, the parameter in the path is plugins,
|
||||
// then we've copied this from a shared board
|
||||
|
||||
// For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/workspace/workspaceID/boardID/viewID/cardID
|
||||
// For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/team/teamID/boardID/viewID/cardID
|
||||
|
||||
// For card links copied on a shared board, the path looks like
|
||||
// {...Mattermost Url}.../plugins/focalboard/workspace/workspaceID/shared/boardID/viewID/cardID?r=read_token
|
||||
// {...Mattermost Url}.../plugins/focalboard/team/teamID/shared/boardID/viewID/cardID?r=read_token
|
||||
|
||||
// This is a non-shared board card link
|
||||
if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "workspace" {
|
||||
workspaceID = pathArray[index+2]
|
||||
if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "team" {
|
||||
teamID = pathArray[index+2]
|
||||
boardID = pathArray[index+3]
|
||||
viewID = pathArray[index+4]
|
||||
cardID = pathArray[index+5]
|
||||
} else if len(pathArray)-index == 8 && pathArray[index] == "plugins" &&
|
||||
pathArray[index+1] == "focalboard" &&
|
||||
pathArray[index+2] == "workspace" &&
|
||||
pathArray[index+2] == "team" &&
|
||||
pathArray[index+4] == "shared" { // This is a shared board card link
|
||||
workspaceID = pathArray[index+3]
|
||||
teamID = pathArray[index+3]
|
||||
boardID = pathArray[index+5]
|
||||
viewID = pathArray[index+6]
|
||||
cardID = pathArray[index+7]
|
||||
}
|
||||
return workspaceID, boardID, viewID, cardID
|
||||
return teamID, boardID, viewID, cardID
|
||||
}
|
||||
|
||||
func isBoardsLink(link string) bool {
|
||||
@ -489,6 +494,6 @@ func isBoardsLink(link string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
workspaceID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||
return workspaceID != "" && boardID != "" && viewID != "" && cardID != ""
|
||||
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||
return teamID != "" && boardID != "" && viewID != "" && cardID != ""
|
||||
}
|
||||
|
@ -36,17 +36,34 @@ function mapStateToProps(state: GlobalState) {
|
||||
}
|
||||
}
|
||||
|
||||
class FocalboardEmbeddedData {
|
||||
teamID: string
|
||||
cardID: string
|
||||
boardID: string
|
||||
readToken: string
|
||||
originalPath: string
|
||||
|
||||
constructor(rawData: string) {
|
||||
const parsed = JSON.parse(rawData)
|
||||
this.teamID = parsed.teamID || parsed.workspaceID
|
||||
this.cardID = parsed.cardID
|
||||
this.boardID = parsed.boardID
|
||||
this.readToken = parsed.readToken
|
||||
this.originalPath = parsed.originalPath
|
||||
}
|
||||
}
|
||||
|
||||
const BoardsUnfurl = (props: Props): JSX.Element => {
|
||||
if (!props.embed || !props.embed.data) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const {embed, locale} = props
|
||||
const focalboardInformation = JSON.parse(embed.data)
|
||||
const {workspaceID, cardID, boardID, readToken, originalPath} = focalboardInformation
|
||||
const focalboardInformation: FocalboardEmbeddedData = new FocalboardEmbeddedData(embed.data)
|
||||
const {teamID, cardID, boardID, readToken, originalPath} = focalboardInformation
|
||||
const baseURL = window.location.origin
|
||||
|
||||
if (!workspaceID || !cardID || !boardID) {
|
||||
if (!teamID || !cardID || !boardID) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
@ -57,20 +74,19 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const [cards, boards] = await Promise.all(
|
||||
const [cards, fetchedBoard] = await Promise.all(
|
||||
[
|
||||
octoClient.getBlocksWithBlockID(cardID, workspaceID, readToken),
|
||||
octoClient.getBlocksWithBlockID(boardID, workspaceID, readToken),
|
||||
octoClient.getBlocksWithBlockID(cardID, boardID, readToken),
|
||||
octoClient.getBoard(boardID),
|
||||
],
|
||||
)
|
||||
const [firstCard] = cards as Card[]
|
||||
const [firstBoard] = boards as Board[]
|
||||
if (!firstCard || !firstBoard) {
|
||||
if (!firstCard || !fetchedBoard) {
|
||||
setLoading(false)
|
||||
return null
|
||||
}
|
||||
setCard(firstCard)
|
||||
setBoard(firstBoard)
|
||||
setBoard(fetchedBoard)
|
||||
|
||||
if (firstCard.fields.contentOrder.length) {
|
||||
let [firstContentBlockID] = firstCard.fields?.contentOrder
|
||||
@ -79,7 +95,7 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
|
||||
[firstContentBlockID] = firstContentBlockID
|
||||
}
|
||||
|
||||
const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, workspaceID, readToken) as ContentBlock[]
|
||||
const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, boardID, readToken) as ContentBlock[]
|
||||
const [firstContentBlock] = contentBlock
|
||||
if (!firstContentBlock) {
|
||||
setLoading(false)
|
||||
@ -103,8 +119,8 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
|
||||
let totalNumberOfCheckBoxes = 0
|
||||
|
||||
// We will just display the first 3 or less select/multi-select properties and do a +n for remainder if any remainder
|
||||
for (let i = 0; i < board.fields.cardProperties.length; i++) {
|
||||
const optionInBoard = board.fields.cardProperties[i]
|
||||
for (let i = 0; i < board.cardProperties.length; i++) {
|
||||
const optionInBoard = board.cardProperties[i]
|
||||
let valueToLookUp = card.fields.properties[optionInBoard.id]
|
||||
|
||||
// Since these are always set and not included in the card properties
|
||||
@ -132,7 +148,11 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
|
||||
continue
|
||||
}
|
||||
|
||||
propertiesToDisplay.push({optionName: optionInBoard.name, optionValue: optionSelected.value, optionValueColour: optionSelected.color})
|
||||
propertiesToDisplay.push({
|
||||
optionName: optionInBoard.name,
|
||||
optionValue: optionSelected.value,
|
||||
optionValueColour: optionSelected.color,
|
||||
})
|
||||
}
|
||||
remainder += (Object.keys(card.fields.properties).length - propertiesToDisplay.length - totalNumberOfCheckBoxes)
|
||||
html = Utils.htmlFromMarkdown(content?.title || '')
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {Utils} from '../../../webapp/src/utils'
|
||||
|
||||
@ -10,9 +9,12 @@ type State = {
|
||||
hasError: boolean
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends React.Component {
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
state = {hasError: false}
|
||||
propTypes = {children: PropTypes.node.isRequired}
|
||||
msg = 'Redirecting to error page...'
|
||||
|
||||
handleError = (): void => {
|
||||
|
@ -9,18 +9,22 @@ import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/r
|
||||
|
||||
import {GlobalState} from 'mattermost-redux/types/store'
|
||||
|
||||
const windowAny = (window as any)
|
||||
import {selectTeam} from 'mattermost-redux/actions/teams'
|
||||
|
||||
import {SuiteWindow} from '../../../webapp/src/types/index'
|
||||
|
||||
const windowAny = (window as SuiteWindow)
|
||||
windowAny.baseURL = '/plugins/focalboard'
|
||||
windowAny.frontendBaseURL = '/boards'
|
||||
windowAny.isFocalboardPlugin = true
|
||||
|
||||
import App from '../../../webapp/src/app'
|
||||
import store from '../../../webapp/src/store'
|
||||
import {setTeam} from '../../../webapp/src/store/teams'
|
||||
import {Utils} from '../../../webapp/src/utils'
|
||||
import GlobalHeader from '../../../webapp/src/components/globalHeader/globalHeader'
|
||||
import FocalboardIcon from '../../../webapp/src/widgets/icons/logo'
|
||||
import {setMattermostTheme} from '../../../webapp/src/theme'
|
||||
import {UserSettings} from '../../../webapp/src/userSettings'
|
||||
|
||||
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient'
|
||||
|
||||
@ -30,7 +34,13 @@ import '../../../webapp/src/styles/labels.scss'
|
||||
import octoClient from '../../../webapp/src/octoClient'
|
||||
|
||||
import BoardsUnfurl from './components/boardsUnfurl/boardsUnfurl'
|
||||
import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK, ACTION_UPDATE_CLIENT_CONFIG, ACTION_UPDATE_SUBSCRIPTION} from './../../../webapp/src/wsclient'
|
||||
import wsClient, {
|
||||
MMWebSocketClient,
|
||||
ACTION_UPDATE_BLOCK,
|
||||
ACTION_UPDATE_CLIENT_CONFIG,
|
||||
ACTION_UPDATE_SUBSCRIPTION,
|
||||
ACTION_UPDATE_CATEGORY, ACTION_UPDATE_BLOCK_CATEGORY, ACTION_UPDATE_BOARD,
|
||||
} from './../../../webapp/src/wsclient'
|
||||
|
||||
import manifest from './manifest'
|
||||
import ErrorBoundary from './error_boundary'
|
||||
@ -164,6 +174,7 @@ export default class Plugin {
|
||||
let theme = mmStore.getState().entities.preferences.myPreferences.theme
|
||||
setMattermostTheme(theme)
|
||||
let lastViewedChannel = mmStore.getState().entities.channels.currentChannelId
|
||||
let prevTeamID: string
|
||||
mmStore.subscribe(() => {
|
||||
const currentUserId = mmStore.getState().entities.users.currentUserId
|
||||
const currentChannel = mmStore.getState().entities.channels.currentChannelId
|
||||
@ -171,22 +182,41 @@ export default class Plugin {
|
||||
localStorage.setItem('focalboardLastViewedChannel:' + currentUserId, currentChannel)
|
||||
lastViewedChannel = currentChannel
|
||||
}
|
||||
|
||||
// Watch for change in active team.
|
||||
// This handles the user selecting a team from the team sidebar.
|
||||
const currentTeamID = mmStore.getState().entities.teams.currentTeamId
|
||||
if (currentTeamID && currentTeamID !== prevTeamID) {
|
||||
prevTeamID = currentTeamID
|
||||
store.dispatch(setTeam(currentTeamID))
|
||||
browserHistory.push(`/team/${currentTeamID}`)
|
||||
wsClient.subscribeToTeam(currentTeamID)
|
||||
}
|
||||
})
|
||||
|
||||
if (this.registry.registerProduct) {
|
||||
windowAny.frontendBaseURL = subpath + '/boards'
|
||||
const goToFocalboardWorkspace = () => {
|
||||
const currentChannel = mmStore.getState().entities.channels.currentChannelId
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelHeader, {workspaceID: currentChannel})
|
||||
window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`, '_blank', 'noopener')
|
||||
const goToFocalboard = () => {
|
||||
const currentTeam = mmStore.getState().entities.teams.currentTeamId
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelHeader, {teamID: currentTeam})
|
||||
window.open(`${windowAny.frontendBaseURL}/team/${currentTeam}`, '_blank', 'noopener')
|
||||
}
|
||||
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, goToFocalboardWorkspace, 'Boards', 'Boards')
|
||||
|
||||
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, goToFocalboard, 'Boards', 'Boards')
|
||||
this.registry.registerProduct(
|
||||
'/boards',
|
||||
'product-boards',
|
||||
'Boards',
|
||||
'/boards',
|
||||
MainApp,
|
||||
HeaderComponent,
|
||||
() => null,
|
||||
true,
|
||||
)
|
||||
|
||||
const goToFocalboardTemplate = () => {
|
||||
const currentChannel = mmStore.getState().entities.channels.currentChannelId
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {workspaceID: currentChannel})
|
||||
UserSettings.lastBoardId = null
|
||||
UserSettings.lastViewId = null
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {channelID: currentChannel})
|
||||
window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
@ -194,11 +224,9 @@ export default class Plugin {
|
||||
this.channelHeaderButtonId = registry.registerChannelIntroButtonAction(<FocalboardIcon/>, goToFocalboardTemplate, 'Boards')
|
||||
}
|
||||
|
||||
this.registry.registerProduct('/boards', 'product-boards', 'Boards', '/boards/welcome', MainApp, HeaderComponent)
|
||||
|
||||
if (this.registry.registerAppBarComponent) {
|
||||
const appBarIconURL = windowAny.baseURL + '/public/app-bar-icon.png'
|
||||
this.registry.registerAppBarComponent(appBarIconURL, goToFocalboardWorkspace, 'Open Boards Workspace')
|
||||
this.registry.registerAppBarComponent(appBarIconURL, goToFocalboard, 'Open Boards')
|
||||
}
|
||||
|
||||
this.registry.registerPostWillRenderEmbedComponent((embed) => embed.type === 'boards', BoardsUnfurl, false)
|
||||
@ -247,7 +275,9 @@ export default class Plugin {
|
||||
}
|
||||
|
||||
// register websocket handlers
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK}`, (e: any) => wsClient.updateBlockHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BOARD}`, (e: any) => wsClient.updateHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CLIENT_CONFIG}`, (e: any) => wsClient.updateClientConfigHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_SUBSCRIPTION}`, (e: any) => wsClient.updateSubscriptionHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler('plugin_statuses_changed', (e: any) => wsClient.pluginStatusesChangedHandler(e.data))
|
||||
@ -267,6 +297,16 @@ export default class Plugin {
|
||||
}
|
||||
}
|
||||
})
|
||||
windowAny.setTeamInSidebar = (teamID: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
mmStore.dispatch(selectTeam(teamID))
|
||||
}
|
||||
windowAny.getCurrentTeamId = (): string => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return mmStore.getState().entities.teams.currentTeamId
|
||||
}
|
||||
}
|
||||
|
||||
uninitialize(): void {
|
||||
|
@ -10,7 +10,7 @@ export interface PluginRegistry {
|
||||
registerCustomRoute(route: string, component: React.ElementType)
|
||||
registerProductRoute(route: string, component: React.ElementType)
|
||||
unregisterComponent(componentId: string)
|
||||
registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCompoent: React.ElementType)
|
||||
registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCentreComponent: React.ElementType, headerRightComponent?: React.ElementType, showTeamSidebar: boolean)
|
||||
registerPostWillRenderEmbedComponent(match: (embed: {type: string, data: any}) => void, component: any, toggleable: boolean)
|
||||
registerWebSocketEventHandler(event: string, handler: (e: any) => void)
|
||||
unregisterWebSocketEventHandler(event: string)
|
||||
|
@ -1,3 +1,3 @@
|
||||
**/*.go {
|
||||
prep: cd server && go test -race -v ./...
|
||||
prep: cd server && go test -tags $FOCALBOARD_BUILD_TAGS -race -v ./...
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
**/*.go !**/*_test.go {
|
||||
prep: cd server && go build -o ../bin/focalboard-server ./main
|
||||
prep: cd server && go build -tags $FOCALBOARD_BUILD_TAGS -o ../bin/focalboard-server ./main
|
||||
daemon +sigterm: ./bin/focalboard-server $FOCALBOARDSERVER_ARGS
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ linters-settings:
|
||||
disable:
|
||||
- fieldalignment
|
||||
lll:
|
||||
line-length: 150
|
||||
line-length: 180
|
||||
dupl:
|
||||
threshold: 200
|
||||
revive:
|
||||
|
2627
server/api/api.go
2627
server/api/api.go
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/audit"
|
||||
)
|
||||
@ -13,26 +14,20 @@ const (
|
||||
archiveExtension = ".boardarchive"
|
||||
)
|
||||
|
||||
func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET /api/v1/workspaces/{workspaceID}/archive/export archiveExport
|
||||
func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET /api/v1/boards/{boardID}/archive/export archiveExportBoard
|
||||
//
|
||||
// Exports an archive of all blocks for one or more boards. If board_id is provided then
|
||||
// only that board will be exported, otherwise all boards in the workspace are exported.
|
||||
// Exports an archive of all blocks for one boards.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: workspaceID
|
||||
// - name: boardID
|
||||
// in: path
|
||||
// description: Workspace ID
|
||||
// description: Id of board to export
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: board_id
|
||||
// in: path
|
||||
// description: Id of board to to export
|
||||
// required: false
|
||||
// type: string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
@ -47,25 +42,92 @@ func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) {
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
query := r.URL.Query()
|
||||
boardID := query.Get("board_id")
|
||||
container, err := a.getContainer(r)
|
||||
if err != nil {
|
||||
a.noContainerErrorResponse(w, r.URL.Path, err)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
boardID := vars["boardID"]
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "archiveExport", audit.Fail)
|
||||
auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("BoardID", boardID)
|
||||
|
||||
var boardIDs []string
|
||||
if boardID != "" {
|
||||
boardIDs = []string{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
|
||||
}
|
||||
|
||||
opts := model.ExportArchiveOptions{
|
||||
WorkspaceID: container.WorkspaceID,
|
||||
BoardIDs: boardIDs,
|
||||
TeamID: board.TeamID,
|
||||
BoardIDs: []string{board.ID},
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension)
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
|
||||
w.Header().Set("Content-Transfer-Encoding", "binary")
|
||||
|
||||
if err := a.app.ExportArchive(w, opts); err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET /api/v1/teams/{teamID}/archive/export archiveExportTeam
|
||||
//
|
||||
// Exports an archive of all blocks for all the boards in a team.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: Id of team
|
||||
// required: true
|
||||
// type: string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// content:
|
||||
// application-octet-stream:
|
||||
// type: string
|
||||
// format: binary
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
vars := mux.Vars(r)
|
||||
teamID := vars["teamID"]
|
||||
|
||||
ctx := r.Context()
|
||||
session, _ := ctx.Value(sessionContextKey).(*model.Session)
|
||||
userID := session.UserID
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "archiveExportTeam", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("TeamID", teamID)
|
||||
|
||||
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
ids := []string{}
|
||||
for _, board := range boards {
|
||||
ids = append(ids, board.ID)
|
||||
}
|
||||
|
||||
opts := model.ExportArchiveOptions{
|
||||
TeamID: teamID,
|
||||
BoardIDs: ids,
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension)
|
||||
@ -81,7 +143,7 @@ func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation POST /api/v1/workspaces/{workspaceID}/archive/import archiveImport
|
||||
// swagger:operation POST /api/v1/boards/{boardID}/archive/import archiveImport
|
||||
//
|
||||
// Import an archive of boards.
|
||||
//
|
||||
@ -91,7 +153,7 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// parameters:
|
||||
// - name: workspaceID
|
||||
// - name: boardID
|
||||
// in: path
|
||||
// description: Workspace ID
|
||||
// required: true
|
||||
@ -111,16 +173,13 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
container, err := a.getContainer(r)
|
||||
if err != nil {
|
||||
a.noContainerErrorResponse(w, r.URL.Path, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
session, _ := ctx.Value(sessionContextKey).(*model.Session)
|
||||
userID := session.UserID
|
||||
|
||||
vars := mux.Vars(r)
|
||||
teamID := vars["teamID"]
|
||||
|
||||
file, handle, err := r.FormFile(UploadFormFileKey)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "%v", err)
|
||||
@ -134,8 +193,8 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.AddMeta("size", handle.Size)
|
||||
|
||||
opt := model.ImportArchiveOptions{
|
||||
WorkspaceID: container.WorkspaceID,
|
||||
ModifiedBy: userID,
|
||||
TeamID: teamID,
|
||||
ModifiedBy: userID,
|
||||
}
|
||||
|
||||
if err := a.app.ImportArchive(file, opt); err != nil {
|
||||
|
@ -17,12 +17,7 @@ func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus strin
|
||||
userID = session.UserID
|
||||
}
|
||||
|
||||
workspaceID := "unknown"
|
||||
container, err := a.getContainer(r)
|
||||
if err == nil {
|
||||
workspaceID = container.WorkspaceID
|
||||
}
|
||||
|
||||
teamID := "unknown"
|
||||
rec := &audit.Record{
|
||||
APIPath: r.URL.Path,
|
||||
Event: event,
|
||||
@ -31,7 +26,7 @@ func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus strin
|
||||
SessionID: sessionID,
|
||||
Client: r.UserAgent(),
|
||||
IPAddress: r.RemoteAddr,
|
||||
Meta: []audit.Meta{{K: audit.KeyWorkspaceID, V: workspaceID}},
|
||||
Meta: []audit.Meta{{K: audit.KeyTeamID, V: teamID}},
|
||||
}
|
||||
|
||||
return rec
|
||||
|
@ -302,13 +302,13 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Validate token
|
||||
if len(registerData.Token) > 0 {
|
||||
workspace, err2 := a.app.GetRootWorkspace()
|
||||
team, err2 := a.app.GetRootTeam()
|
||||
if err2 != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2)
|
||||
return
|
||||
}
|
||||
|
||||
if registerData.Token != workspace.SignupToken {
|
||||
if registerData.Token != team.SignupToken {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid token", nil)
|
||||
return
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/metrics"
|
||||
"github.com/mattermost/focalboard/server/services/notify"
|
||||
"github.com/mattermost/focalboard/server/services/permissions"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/webhook"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
@ -14,6 +18,12 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||
)
|
||||
|
||||
const (
|
||||
blockChangeNotifierQueueSize = 100
|
||||
blockChangeNotifierPoolSize = 10
|
||||
blockChangeNotifierShutdownTimeout = time.Second * 10
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
Auth *auth.Auth
|
||||
Store store.Store
|
||||
@ -22,19 +32,21 @@ type Services struct {
|
||||
Metrics *metrics.Metrics
|
||||
Notifications *notify.Service
|
||||
Logger *mlog.Logger
|
||||
Permissions permissions.PermissionsService
|
||||
SkipTemplateInit bool
|
||||
}
|
||||
|
||||
type App struct {
|
||||
config *config.Configuration
|
||||
store store.Store
|
||||
auth *auth.Auth
|
||||
wsAdapter ws.Adapter
|
||||
filesBackend filestore.FileBackend
|
||||
webhook *webhook.Client
|
||||
metrics *metrics.Metrics
|
||||
notifications *notify.Service
|
||||
logger *mlog.Logger
|
||||
config *config.Configuration
|
||||
store store.Store
|
||||
auth *auth.Auth
|
||||
wsAdapter ws.Adapter
|
||||
filesBackend filestore.FileBackend
|
||||
webhook *webhook.Client
|
||||
metrics *metrics.Metrics
|
||||
notifications *notify.Service
|
||||
logger *mlog.Logger
|
||||
blockChangeNotifier *utils.CallbackQueue
|
||||
}
|
||||
|
||||
func (a *App) SetConfig(config *config.Configuration) {
|
||||
@ -43,15 +55,16 @@ func (a *App) SetConfig(config *config.Configuration) {
|
||||
|
||||
func New(config *config.Configuration, wsAdapter ws.Adapter, services Services) *App {
|
||||
app := &App{
|
||||
config: config,
|
||||
store: services.Store,
|
||||
auth: services.Auth,
|
||||
wsAdapter: wsAdapter,
|
||||
filesBackend: services.FilesBackend,
|
||||
webhook: services.Webhook,
|
||||
metrics: services.Metrics,
|
||||
notifications: services.Notifications,
|
||||
logger: services.Logger,
|
||||
config: config,
|
||||
store: services.Store,
|
||||
auth: services.Auth,
|
||||
wsAdapter: wsAdapter,
|
||||
filesBackend: services.FilesBackend,
|
||||
webhook: services.Webhook,
|
||||
metrics: services.Metrics,
|
||||
notifications: services.Notifications,
|
||||
logger: services.Logger,
|
||||
blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger),
|
||||
}
|
||||
app.initialize(services.SkipTemplateInit)
|
||||
return app
|
||||
|
@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/auth"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
@ -25,8 +24,8 @@ func (a *App) GetSession(token string) (*model.Session, error) {
|
||||
}
|
||||
|
||||
// IsValidReadToken validates the read token for a block.
|
||||
func (a *App) IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) {
|
||||
return a.auth.IsValidReadToken(c, blockID, readToken)
|
||||
func (a *App) IsValidReadToken(boardID string, readToken string) (bool, error) {
|
||||
return a.auth.IsValidReadToken(boardID, readToken)
|
||||
}
|
||||
|
||||
// GetRegisteredUserCount returns the number of registered users.
|
||||
|
@ -1,137 +1,192 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/notify"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (a *App) GetBlocks(c store.Container, parentID string, blockType string) ([]model.Block, error) {
|
||||
var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards")
|
||||
|
||||
func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]model.Block, error) {
|
||||
if boardID == "" {
|
||||
return []model.Block{}, nil
|
||||
}
|
||||
|
||||
if blockType != "" && parentID != "" {
|
||||
return a.store.GetBlocksWithParentAndType(c, parentID, blockType)
|
||||
return a.store.GetBlocksWithParentAndType(boardID, parentID, blockType)
|
||||
}
|
||||
|
||||
if blockType != "" {
|
||||
return a.store.GetBlocksWithType(c, blockType)
|
||||
return a.store.GetBlocksWithType(boardID, blockType)
|
||||
}
|
||||
|
||||
return a.store.GetBlocksWithParent(c, parentID)
|
||||
return a.store.GetBlocksWithParent(boardID, parentID)
|
||||
}
|
||||
|
||||
func (a *App) GetBlocksWithRootID(c store.Container, rootID string) ([]model.Block, error) {
|
||||
return a.store.GetBlocksWithRootID(c, rootID)
|
||||
func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]model.Block, error) {
|
||||
board, err := a.GetBoard(boardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if board == nil {
|
||||
return nil, fmt.Errorf("cannot fetch board %s for DuplicateBlock: %w", boardID, err)
|
||||
}
|
||||
|
||||
blocks, err := a.store.DuplicateBlock(boardID, blockID, userID, asTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
for _, block := range blocks {
|
||||
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
func (a *App) GetRootID(c store.Container, blockID string) (string, error) {
|
||||
return a.store.GetRootID(c, blockID)
|
||||
func (a *App) GetBlocksWithBoardID(boardID string) ([]model.Block, error) {
|
||||
return a.store.GetBlocksWithBoardID(boardID)
|
||||
}
|
||||
|
||||
func (a *App) GetParentID(c store.Container, blockID string) (string, error) {
|
||||
return a.store.GetParentID(c, blockID)
|
||||
}
|
||||
|
||||
func (a *App) PatchBlock(c store.Container, blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
|
||||
oldBlock, err := a.store.GetBlock(c, blockID)
|
||||
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
|
||||
oldBlock, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = a.store.PatchBlock(c, blockID, blockPatch, modifiedByID)
|
||||
board, err := a.store.GetBoard(oldBlock.BoardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = a.store.PatchBlock(blockID, blockPatch, modifiedByID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.metrics.IncrementBlocksPatched(1)
|
||||
block, err := a.store.GetBlock(c, blockID)
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *block)
|
||||
go func() {
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
// broadcast on websocket
|
||||
a.wsAdapter.BroadcastBlockChange(board.TeamID, *block)
|
||||
|
||||
// broadcast on webhooks
|
||||
a.webhook.NotifyUpdate(*block)
|
||||
a.notifyBlockChanged(notify.Update, c, block, oldBlock, modifiedByID)
|
||||
}()
|
||||
|
||||
// send notifications
|
||||
a.notifyBlockChanged(notify.Update, block, oldBlock, modifiedByID)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) PatchBlocks(c store.Container, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
|
||||
func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
|
||||
oldBlocks := make([]model.Block, 0, len(blockPatches.BlockIDs))
|
||||
for _, blockID := range blockPatches.BlockIDs {
|
||||
oldBlock, err := a.store.GetBlock(c, blockID)
|
||||
oldBlock, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
oldBlocks = append(oldBlocks, *oldBlock)
|
||||
}
|
||||
|
||||
err := a.store.PatchBlocks(c, blockPatches, modifiedByID)
|
||||
err := a.store.PatchBlocks(blockPatches, modifiedByID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.metrics.IncrementBlocksPatched(len(oldBlocks))
|
||||
for i, blockID := range blockPatches.BlockIDs {
|
||||
newBlock, err := a.store.GetBlock(c, blockID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *newBlock)
|
||||
go func(currentIndex int) {
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
a.metrics.IncrementBlocksPatched(len(oldBlocks))
|
||||
for i, blockID := range blockPatches.BlockIDs {
|
||||
newBlock, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
a.wsAdapter.BroadcastBlockChange(teamID, *newBlock)
|
||||
a.webhook.NotifyUpdate(*newBlock)
|
||||
a.notifyBlockChanged(notify.Update, c, newBlock, &oldBlocks[currentIndex], modifiedByID)
|
||||
}(i)
|
||||
}
|
||||
|
||||
a.notifyBlockChanged(notify.Update, newBlock, &oldBlocks[i], modifiedByID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) InsertBlock(c store.Container, block model.Block, modifiedByID string) error {
|
||||
err := a.store.InsertBlock(c, &block, modifiedByID)
|
||||
func (a *App) InsertBlock(block model.Block, modifiedByID string) error {
|
||||
board, bErr := a.store.GetBoard(block.BoardID)
|
||||
if bErr != nil {
|
||||
return bErr
|
||||
}
|
||||
|
||||
err := a.store.InsertBlock(&block, modifiedByID)
|
||||
if err == nil {
|
||||
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, block)
|
||||
a.metrics.IncrementBlocksInserted(1)
|
||||
go func() {
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
|
||||
a.metrics.IncrementBlocksInserted(1)
|
||||
a.webhook.NotifyUpdate(block)
|
||||
a.notifyBlockChanged(notify.Add, c, &block, nil, modifiedByID)
|
||||
}()
|
||||
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) InsertBlocks(c store.Container, blocks []model.Block, modifiedByID string, allowNotifications bool) ([]model.Block, error) {
|
||||
func (a *App) InsertBlocks(blocks []model.Block, modifiedByID string, allowNotifications bool) ([]model.Block, error) {
|
||||
if len(blocks) == 0 {
|
||||
return []model.Block{}, nil
|
||||
}
|
||||
|
||||
// all blocks must belong to the same board
|
||||
boardID := blocks[0].BoardID
|
||||
for _, block := range blocks {
|
||||
if block.BoardID != boardID {
|
||||
return nil, ErrBlocksFromMultipleBoards
|
||||
}
|
||||
}
|
||||
|
||||
board, err := a.store.GetBoard(boardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
needsNotify := make([]model.Block, 0, len(blocks))
|
||||
for i := range blocks {
|
||||
err := a.store.InsertBlock(c, &blocks[i], modifiedByID)
|
||||
err := a.store.InsertBlock(&blocks[i], modifiedByID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocks[i].WorkspaceID = c.WorkspaceID
|
||||
needsNotify = append(needsNotify, blocks[i])
|
||||
|
||||
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, blocks[i])
|
||||
a.wsAdapter.BroadcastBlockChange(board.TeamID, blocks[i])
|
||||
a.metrics.IncrementBlocksInserted(1)
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
for _, b := range needsNotify {
|
||||
block := b
|
||||
a.webhook.NotifyUpdate(block)
|
||||
if allowNotifications {
|
||||
a.notifyBlockChanged(notify.Add, c, &block, nil, modifiedByID)
|
||||
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks []model.Block) error {
|
||||
func (a *App) CopyCardFiles(sourceBoardID string, blocks []model.Block) error {
|
||||
// Images attached in cards have a path comprising the card's board ID.
|
||||
// When we create a template from this board, we need to copy the files
|
||||
// with the new board ID in path.
|
||||
@ -139,7 +194,7 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks
|
||||
// template) to fail to load.
|
||||
|
||||
// look up ID of source board, which may be different than the blocks.
|
||||
board, err := a.GetBlockByID(store.Container{}, sourceBoardID)
|
||||
board, err := a.GetBlockByID(sourceBoardID)
|
||||
if err != nil || board == nil {
|
||||
return fmt.Errorf("cannot fetch board %s for CopyCardFiles: %w", sourceBoardID, err)
|
||||
}
|
||||
@ -153,8 +208,8 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks
|
||||
ext := filepath.Ext(fileName.(string))
|
||||
destFilename := utils.NewID(utils.IDTypeNone) + ext
|
||||
|
||||
sourceFilePath := filepath.Join(board.WorkspaceID, sourceBoardID, fileName.(string))
|
||||
destinationFilePath := filepath.Join(destWorkspaceID, block.RootID, destFilename)
|
||||
sourceFilePath := filepath.Join(sourceBoardID, fileName.(string))
|
||||
destinationFilePath := filepath.Join(block.BoardID, destFilename)
|
||||
|
||||
a.logger.Debug(
|
||||
"Copying card file",
|
||||
@ -179,24 +234,26 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetSubTree(c store.Container, blockID string, levels int) ([]model.Block, error) {
|
||||
func (a *App) GetSubTree(boardID, blockID string, levels int, opts model.QuerySubtreeOptions) ([]model.Block, error) {
|
||||
// Only 2 or 3 levels are supported for now
|
||||
if levels >= 3 {
|
||||
return a.store.GetSubTree3(c, blockID, model.QuerySubtreeOptions{})
|
||||
return a.store.GetSubTree3(boardID, blockID, opts)
|
||||
}
|
||||
return a.store.GetSubTree2(c, blockID, model.QuerySubtreeOptions{})
|
||||
|
||||
return a.store.GetSubTree2(boardID, blockID, opts)
|
||||
}
|
||||
|
||||
func (a *App) GetAllBlocks(c store.Container) ([]model.Block, error) {
|
||||
return a.store.GetAllBlocks(c)
|
||||
func (a *App) GetBlockByID(blockID string) (*model.Block, error) {
|
||||
return a.store.GetBlock(blockID)
|
||||
}
|
||||
|
||||
func (a *App) GetBlockByID(c store.Container, blockID string) (*model.Block, error) {
|
||||
return a.store.GetBlock(c, blockID)
|
||||
}
|
||||
func (a *App) DeleteBlock(blockID string, modifiedBy string) error {
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) error {
|
||||
block, err := a.store.GetBlock(c, blockID)
|
||||
board, err := a.store.GetBoard(block.BoardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -206,7 +263,7 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = a.store.DeleteBlock(c, blockID, modifiedBy)
|
||||
err = a.store.DeleteBlock(blockID, modifiedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -214,7 +271,7 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string)
|
||||
if block.Type == model.TypeImage {
|
||||
fileName, fileIDExists := block.Fields["fileId"]
|
||||
if fileName, fileIDIsString := fileName.(string); fileIDExists && fileIDIsString {
|
||||
filePath := filepath.Join(block.WorkspaceID, block.RootID, fileName)
|
||||
filePath := filepath.Join(block.BoardID, fileName)
|
||||
err = a.filesBackend.RemoveFile(filePath)
|
||||
|
||||
if err != nil {
|
||||
@ -225,31 +282,32 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string)
|
||||
}
|
||||
}
|
||||
|
||||
a.wsAdapter.BroadcastBlockDelete(c.WorkspaceID, blockID, block.ParentID)
|
||||
a.metrics.IncrementBlocksDeleted(1)
|
||||
go func() {
|
||||
a.notifyBlockChanged(notify.Delete, c, block, block, modifiedBy)
|
||||
}()
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID)
|
||||
a.metrics.IncrementBlocksDeleted(1)
|
||||
a.notifyBlockChanged(notify.Delete, block, block, modifiedBy)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UndeleteBlock(c store.Container, blockID string, modifiedBy string) error {
|
||||
blocks, err := a.store.GetBlockHistory(c, blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
|
||||
func (a *App) UndeleteBlock(blockID string, modifiedBy string) error {
|
||||
blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(blocks) == 0 {
|
||||
// deleting non-existing block not considered an error
|
||||
// undeleting non-existing block not considered an error
|
||||
return nil
|
||||
}
|
||||
|
||||
err = a.store.UndeleteBlock(c, blockID, modifiedBy)
|
||||
err = a.store.UndeleteBlock(blockID, modifiedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
block, err := a.store.GetBlock(c, blockID)
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -259,12 +317,19 @@ func (a *App) UndeleteBlock(c store.Container, blockID string, modifiedBy string
|
||||
return nil
|
||||
}
|
||||
|
||||
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *block)
|
||||
a.metrics.IncrementBlocksInserted(1)
|
||||
go func() {
|
||||
board, err := a.store.GetBoard(block.BoardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
a.wsAdapter.BroadcastBlockChange(board.TeamID, *block)
|
||||
a.metrics.IncrementBlocksInserted(1)
|
||||
a.webhook.NotifyUpdate(*block)
|
||||
a.notifyBlockChanged(notify.Add, c, block, nil, modifiedBy)
|
||||
}()
|
||||
a.notifyBlockChanged(notify.Add, block, nil, modifiedBy)
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -272,13 +337,17 @@ func (a *App) GetBlockCountsByType() (map[string]int64, error) {
|
||||
return a.store.GetBlockCountsByType()
|
||||
}
|
||||
|
||||
func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block *model.Block, oldBlock *model.Block, modifiedByID string) {
|
||||
func (a *App) GetBlocksForBoard(boardID string) ([]model.Block, error) {
|
||||
return a.store.GetBlocksForBoard(boardID)
|
||||
}
|
||||
|
||||
func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBlock *model.Block, modifiedByID string) {
|
||||
if a.notifications == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// find card and board for the changed block.
|
||||
board, card, err := a.store.GetBoardAndCard(c, block)
|
||||
board, card, err := a.getBoardAndCard(block)
|
||||
if err != nil {
|
||||
a.logger.Error("Error notifying for block change; cannot determine board or card", mlog.Err(err))
|
||||
return
|
||||
@ -286,7 +355,7 @@ func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block
|
||||
|
||||
evt := notify.BlockChangeEvent{
|
||||
Action: action,
|
||||
Workspace: c.WorkspaceID,
|
||||
TeamID: board.TeamID,
|
||||
Board: board,
|
||||
Card: card,
|
||||
BlockChanged: block,
|
||||
@ -295,3 +364,35 @@ func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block
|
||||
}
|
||||
a.notifications.BlockChanged(evt)
|
||||
}
|
||||
|
||||
const (
|
||||
maxSearchDepth = 50
|
||||
)
|
||||
|
||||
// getBoardAndCard returns the first parent of type `card` its board for the specified block.
|
||||
// `board` and/or `card` may return nil without error if the block does not belong to a board or card.
|
||||
func (a *App) getBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error) {
|
||||
board, err = a.store.GetBoard(block.BoardID)
|
||||
if err != nil {
|
||||
return board, card, err
|
||||
}
|
||||
|
||||
var count int // don't let invalid blocks hierarchy cause infinite loop.
|
||||
iter := block
|
||||
for {
|
||||
count++
|
||||
if card == nil && iter.Type == model.TypeCard {
|
||||
card = iter
|
||||
}
|
||||
|
||||
if iter.ParentID == "" || (board != nil && card != nil) || count > maxSearchDepth {
|
||||
break
|
||||
}
|
||||
|
||||
iter, err = a.store.GetBlock(iter.ParentID)
|
||||
if err != nil || iter == nil {
|
||||
return board, card, err
|
||||
}
|
||||
}
|
||||
return board, card, nil
|
||||
}
|
||||
|
@ -3,10 +3,9 @@ package app
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
st "github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -18,47 +17,28 @@ func (be blockError) Error() string {
|
||||
return be.msg
|
||||
}
|
||||
|
||||
func TestGetParentID(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
container := st.Container{
|
||||
WorkspaceID: "0",
|
||||
}
|
||||
t.Run("success query", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetParentID(gomock.Eq(container), gomock.Eq("test-id")).Return("test-parent-id", nil)
|
||||
result, err := th.App.GetParentID(container, "test-id")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test-parent-id", result)
|
||||
})
|
||||
|
||||
t.Run("fail query", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetParentID(gomock.Eq(container), gomock.Eq("test-id")).Return("", blockError{"block-not-found"})
|
||||
_, err := th.App.GetParentID(container, "test-id")
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, blockError{"block-not-found"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInsertBlock(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
container := st.Container{
|
||||
WorkspaceID: "0",
|
||||
}
|
||||
|
||||
t.Run("success scenerio", func(t *testing.T) {
|
||||
block := model.Block{}
|
||||
th.Store.EXPECT().InsertBlock(gomock.Eq(container), gomock.Eq(&block), gomock.Eq("user-id-1")).Return(nil)
|
||||
err := th.App.InsertBlock(container, block, "user-id-1")
|
||||
boardID := testBoardID
|
||||
block := model.Block{BoardID: boardID}
|
||||
board := &model.Board{ID: boardID}
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(board, nil)
|
||||
th.Store.EXPECT().InsertBlock(&block, "user-id-1").Return(nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil)
|
||||
err := th.App.InsertBlock(block, "user-id-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("error scenerio", func(t *testing.T) {
|
||||
block := model.Block{}
|
||||
th.Store.EXPECT().InsertBlock(gomock.Eq(container), gomock.Eq(&block), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||
err := th.App.InsertBlock(container, block, "user-id-1")
|
||||
boardID := testBoardID
|
||||
block := model.Block{BoardID: boardID}
|
||||
board := &model.Board{ID: boardID}
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(board, nil)
|
||||
th.Store.EXPECT().InsertBlock(&block, "user-id-1").Return(blockError{"error"})
|
||||
err := th.App.InsertBlock(block, "user-id-1")
|
||||
require.Error(t, err, "error")
|
||||
})
|
||||
}
|
||||
@ -67,20 +47,17 @@ func TestPatchBlocks(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
container := st.Container{
|
||||
WorkspaceID: "0",
|
||||
}
|
||||
t.Run("patchBlocks success scenerio", func(t *testing.T) {
|
||||
blockPatches := model.BlockPatchBatch{}
|
||||
th.Store.EXPECT().PatchBlocks(gomock.Eq(container), gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil)
|
||||
err := th.App.PatchBlocks(container, &blockPatches, "user-id-1")
|
||||
th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil)
|
||||
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("patchBlocks error scenerio", func(t *testing.T) {
|
||||
blockPatches := model.BlockPatchBatch{}
|
||||
th.Store.EXPECT().PatchBlocks(gomock.Eq(container), gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||
err := th.App.PatchBlocks(container, &blockPatches, "user-id-1")
|
||||
th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
||||
require.Error(t, err, "error")
|
||||
})
|
||||
}
|
||||
@ -89,27 +66,32 @@ func TestDeleteBlock(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
container := st.Container{
|
||||
WorkspaceID: "0",
|
||||
}
|
||||
|
||||
t.Run("success scenerio", func(t *testing.T) {
|
||||
boardID := testBoardID
|
||||
board := &model.Board{ID: boardID}
|
||||
block := model.Block{
|
||||
ID: "block-id",
|
||||
ID: "block-id",
|
||||
BoardID: board.ID,
|
||||
}
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil)
|
||||
th.Store.EXPECT().DeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil)
|
||||
err := th.App.DeleteBlock(container, "block-id", "user-id-1")
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
||||
th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil)
|
||||
th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil)
|
||||
err := th.App.DeleteBlock("block-id", "user-id-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("error scenerio", func(t *testing.T) {
|
||||
boardID := testBoardID
|
||||
board := &model.Board{ID: boardID}
|
||||
block := model.Block{
|
||||
ID: "block-id",
|
||||
ID: "block-id",
|
||||
BoardID: board.ID,
|
||||
}
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil)
|
||||
th.Store.EXPECT().DeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||
err := th.App.DeleteBlock(container, "block-id", "user-id-1")
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
||||
th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||
th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil)
|
||||
err := th.App.DeleteBlock("block-id", "user-id-1")
|
||||
require.Error(t, err, "error")
|
||||
})
|
||||
}
|
||||
@ -118,22 +100,22 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
container := st.Container{
|
||||
WorkspaceID: "0",
|
||||
}
|
||||
|
||||
t.Run("success scenerio", func(t *testing.T) {
|
||||
boardID := testBoardID
|
||||
board := &model.Board{ID: boardID}
|
||||
block := model.Block{
|
||||
ID: "block-id",
|
||||
ID: "block-id",
|
||||
BoardID: board.ID,
|
||||
}
|
||||
th.Store.EXPECT().GetBlockHistory(
|
||||
gomock.Eq(container),
|
||||
gomock.Eq("block-id"),
|
||||
gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}),
|
||||
).Return([]model.Block{block}, nil)
|
||||
th.Store.EXPECT().UndeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil)
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil)
|
||||
err := th.App.UndeleteBlock(container, "block-id", "user-id-1")
|
||||
th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil)
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(board, nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil)
|
||||
err := th.App.UndeleteBlock("block-id", "user-id-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@ -142,13 +124,12 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
ID: "block-id",
|
||||
}
|
||||
th.Store.EXPECT().GetBlockHistory(
|
||||
gomock.Eq(container),
|
||||
gomock.Eq("block-id"),
|
||||
gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}),
|
||||
).Return([]model.Block{block}, nil)
|
||||
th.Store.EXPECT().UndeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil)
|
||||
err := th.App.UndeleteBlock(container, "block-id", "user-id-1")
|
||||
th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
||||
err := th.App.UndeleteBlock("block-id", "user-id-1")
|
||||
require.Error(t, err, "error")
|
||||
})
|
||||
}
|
||||
|
254
server/app/boards.go
Normal file
254
server/app/boards.go
Normal file
@ -0,0 +1,254 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBoardMemberIsLastAdmin = errors.New("cannot leave a board with no admins")
|
||||
ErrNewBoardCannotHaveID = errors.New("new board cannot have an ID")
|
||||
)
|
||||
|
||||
func (a *App) GetBoard(boardID string) (*model.Board, error) {
|
||||
board, err := a.store.GetBoard(boardID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return board, nil
|
||||
}
|
||||
|
||||
func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
|
||||
bab, members, err := a.store.DuplicateBoard(boardID, userID, toTeam, asTemplate)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
go func() {
|
||||
teamID := ""
|
||||
for _, board := range bab.Boards {
|
||||
teamID = board.TeamID
|
||||
a.wsAdapter.BroadcastBoardChange(teamID, board)
|
||||
}
|
||||
for _, block := range bab.Blocks {
|
||||
a.wsAdapter.BroadcastBlockChange(teamID, block)
|
||||
}
|
||||
for _, member := range members {
|
||||
a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member)
|
||||
}
|
||||
}()
|
||||
return bab, members, err
|
||||
}
|
||||
|
||||
func (a *App) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) {
|
||||
return a.store.GetBoardsForUserAndTeam(userID, teamID)
|
||||
}
|
||||
|
||||
func (a *App) GetTemplateBoards(teamID string) ([]*model.Board, error) {
|
||||
return a.store.GetTemplateBoards(teamID)
|
||||
}
|
||||
|
||||
func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*model.Board, error) {
|
||||
if board.ID != "" {
|
||||
return nil, ErrNewBoardCannotHaveID
|
||||
}
|
||||
board.ID = utils.NewID(utils.IDTypeBoard)
|
||||
|
||||
var newBoard *model.Board
|
||||
var member *model.BoardMember
|
||||
var err error
|
||||
if addMember {
|
||||
newBoard, member, err = a.store.InsertBoardWithAdmin(board, userID)
|
||||
} else {
|
||||
newBoard, err = a.store.InsertBoard(board, userID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastBoardChange(newBoard.TeamID, newBoard)
|
||||
|
||||
if addMember {
|
||||
a.wsAdapter.BroadcastMemberChange(newBoard.TeamID, newBoard.ID, member)
|
||||
}
|
||||
}()
|
||||
|
||||
return newBoard, nil
|
||||
}
|
||||
|
||||
func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*model.Board, error) {
|
||||
updatedBoard, err := a.store.PatchBoard(boardID, patch, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastBoardChange(updatedBoard.TeamID, updatedBoard)
|
||||
}()
|
||||
|
||||
return updatedBoard, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteBoard(boardID, userID string) error {
|
||||
board, err := a.store.GetBoard(boardID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.store.DeleteBoard(boardID, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastBoardDelete(board.TeamID, boardID)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) {
|
||||
return a.store.GetMembersForBoard(boardID)
|
||||
}
|
||||
|
||||
func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) {
|
||||
return a.store.GetMembersForUser(userID)
|
||||
}
|
||||
|
||||
func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) {
|
||||
board, err := a.store.GetBoard(member.BoardID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingMembership, err := a.store.GetMemberForBoard(member.BoardID, member.UserID)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingMembership != nil {
|
||||
return existingMembership, nil
|
||||
}
|
||||
|
||||
newMember, err := a.store.SaveMember(member)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member)
|
||||
}()
|
||||
|
||||
return newMember, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, error) {
|
||||
board, bErr := a.store.GetBoard(member.BoardID)
|
||||
if errors.Is(bErr, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if bErr != nil {
|
||||
return nil, bErr
|
||||
}
|
||||
|
||||
oldMember, err := a.store.GetMemberForBoard(member.BoardID, member.UserID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if we're updating an admin, we need to check that there is at
|
||||
// least still another admin on the board
|
||||
if oldMember.SchemeAdmin && !member.SchemeAdmin {
|
||||
isLastAdmin, err2 := a.isLastAdmin(member.UserID, member.BoardID)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
if isLastAdmin {
|
||||
return nil, ErrBoardMemberIsLastAdmin
|
||||
}
|
||||
}
|
||||
|
||||
newMember, err := a.store.SaveMember(member)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member)
|
||||
}()
|
||||
|
||||
return newMember, nil
|
||||
}
|
||||
|
||||
func (a *App) isLastAdmin(userID, boardID string) (bool, error) {
|
||||
members, err := a.store.GetMembersForBoard(boardID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, m := range members {
|
||||
if m.SchemeAdmin && m.UserID != userID {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteBoardMember(boardID, userID string) error {
|
||||
board, bErr := a.store.GetBoard(boardID)
|
||||
if errors.Is(bErr, sql.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
if bErr != nil {
|
||||
return bErr
|
||||
}
|
||||
|
||||
oldMember, err := a.store.GetMemberForBoard(boardID, userID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we're removing an admin, we need to check that there is at
|
||||
// least still another admin on the board
|
||||
if oldMember.SchemeAdmin {
|
||||
isLastAdmin, err := a.isLastAdmin(userID, boardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isLastAdmin {
|
||||
return ErrBoardMemberIsLastAdmin
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.store.DeleteMember(boardID, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastMemberDelete(board.TeamID, boardID, userID)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) SearchBoardsForUserAndTeam(term, userID, teamID string) ([]*model.Board, error) {
|
||||
return a.store.SearchBoardsForUserAndTeam(term, userID, teamID)
|
||||
}
|
126
server/app/boards_and_blocks.go
Normal file
126
server/app/boards_and_blocks.go
Normal file
@ -0,0 +1,126 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/notify"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (a *App) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string, addMember bool) (*model.BoardsAndBlocks, error) {
|
||||
var newBab *model.BoardsAndBlocks
|
||||
var members []*model.BoardMember
|
||||
var err error
|
||||
|
||||
if addMember {
|
||||
newBab, members, err = a.store.CreateBoardsAndBlocksWithAdmin(bab, userID)
|
||||
} else {
|
||||
newBab, err = a.store.CreateBoardsAndBlocks(bab, userID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// all new boards should belong to the same team
|
||||
teamID := newBab.Boards[0].TeamID
|
||||
|
||||
// This can be synchronous because this action is not common
|
||||
for _, board := range newBab.Boards {
|
||||
a.wsAdapter.BroadcastBoardChange(teamID, board)
|
||||
}
|
||||
|
||||
for _, block := range newBab.Blocks {
|
||||
b := block
|
||||
a.wsAdapter.BroadcastBlockChange(teamID, b)
|
||||
a.metrics.IncrementBlocksInserted(1)
|
||||
a.webhook.NotifyUpdate(b)
|
||||
a.notifyBlockChanged(notify.Add, &b, nil, userID)
|
||||
}
|
||||
|
||||
if addMember {
|
||||
for _, member := range members {
|
||||
a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member)
|
||||
}
|
||||
}
|
||||
|
||||
return newBab, nil
|
||||
}
|
||||
|
||||
func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
|
||||
oldBlocksMap := map[string]*model.Block{}
|
||||
for _, blockID := range pbab.BlockIDs {
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldBlocksMap[blockID] = block
|
||||
}
|
||||
|
||||
bab, err := a.store.PatchBoardsAndBlocks(pbab, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
teamID := bab.Boards[0].TeamID
|
||||
|
||||
for _, block := range bab.Blocks {
|
||||
oldBlock, ok := oldBlocksMap[block.ID]
|
||||
if !ok {
|
||||
a.logger.Error("Error notifying for block change on patch boards and blocks; cannot get old block", mlog.String("blockID", block.ID))
|
||||
continue
|
||||
}
|
||||
|
||||
b := block
|
||||
a.metrics.IncrementBlocksPatched(1)
|
||||
a.wsAdapter.BroadcastBlockChange(teamID, b)
|
||||
a.webhook.NotifyUpdate(b)
|
||||
a.notifyBlockChanged(notify.Update, &b, oldBlock, userID)
|
||||
}
|
||||
|
||||
for _, board := range bab.Boards {
|
||||
a.wsAdapter.BroadcastBoardChange(board.TeamID, board)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return bab, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error {
|
||||
firstBoard, err := a.store.GetBoard(dbab.Boards[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we need the block entity to notify of the block changes, so we
|
||||
// fetch and store the blocks first
|
||||
blocks := []*model.Block{}
|
||||
for _, blockID := range dbab.Blocks {
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
|
||||
if err := a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
for _, block := range blocks {
|
||||
a.wsAdapter.BroadcastBlockDelete(firstBoard.TeamID, block.ID, block.BoardID)
|
||||
a.metrics.IncrementBlocksDeleted(1)
|
||||
a.notifyBlockChanged(notify.Update, block, block, userID)
|
||||
}
|
||||
|
||||
for _, boardID := range dbab.Boards {
|
||||
a.wsAdapter.BroadcastBoardDelete(firstBoard.TeamID, boardID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
103
server/app/category.go
Normal file
103
server/app/category.go
Normal file
@ -0,0 +1,103 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorCategoryPermissionDenied = errors.New("category doesn't belong to user")
|
||||
ErrorCategoryDeleted = errors.New("category is deleted")
|
||||
)
|
||||
|
||||
func (a *App) CreateCategory(category *model.Category) (*model.Category, error) {
|
||||
category.Hydrate()
|
||||
if err := category.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.store.CreateCategory(*category); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createdCategory, err := a.store.GetCategory(category.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastCategoryChange(*createdCategory)
|
||||
}()
|
||||
|
||||
return createdCategory, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateCategory(category *model.Category) (*model.Category, error) {
|
||||
// verify if category belongs to the user
|
||||
existingCategory, err := a.store.GetCategory(category.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingCategory.DeleteAt != 0 {
|
||||
return nil, ErrorCategoryDeleted
|
||||
}
|
||||
|
||||
if existingCategory.UserID != category.UserID {
|
||||
return nil, ErrorCategoryPermissionDenied
|
||||
}
|
||||
|
||||
category.UpdateAt = utils.GetMillis()
|
||||
if err = category.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = a.store.UpdateCategory(*category); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedCategory, err := a.store.GetCategory(category.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastCategoryChange(*updatedCategory)
|
||||
}()
|
||||
|
||||
return updatedCategory, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category, error) {
|
||||
existingCategory, err := a.store.GetCategory(categoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// category is already deleted. This avoids
|
||||
// overriding the original deleted at timestamp
|
||||
if existingCategory.DeleteAt != 0 {
|
||||
return existingCategory, nil
|
||||
}
|
||||
|
||||
// verify if category belongs to the user
|
||||
if existingCategory.UserID != userID {
|
||||
return nil, ErrorCategoryPermissionDenied
|
||||
}
|
||||
|
||||
if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deletedCategory, err := a.store.GetCategory(categoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastCategoryChange(*deletedCategory)
|
||||
}()
|
||||
|
||||
return deletedCategory, nil
|
||||
}
|
26
server/app/category_blocks.go
Normal file
26
server/app/category_blocks.go
Normal file
@ -0,0 +1,26 @@
|
||||
package app
|
||||
|
||||
import "github.com/mattermost/focalboard/server/model"
|
||||
|
||||
func (a *App) GetUserCategoryBlocks(userID, teamID string) ([]model.CategoryBlocks, error) {
|
||||
return a.store.GetUserCategoryBlocks(userID, teamID)
|
||||
}
|
||||
|
||||
func (a *App) AddUpdateUserCategoryBlock(teamID, userID, categoryID, blockID string) error {
|
||||
err := a.store.AddUpdateCategoryBlock(userID, categoryID, blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.wsAdapter.BroadcastCategoryBlockChange(
|
||||
teamID,
|
||||
userID,
|
||||
model.BlockCategoryWebsocketData{
|
||||
BlockID: blockID,
|
||||
CategoryID: categoryID,
|
||||
})
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/wiggin77/merror"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
@ -18,10 +17,7 @@ var (
|
||||
)
|
||||
|
||||
func (a *App) ExportArchive(w io.Writer, opt model.ExportArchiveOptions) (errs error) {
|
||||
container := store.Container{
|
||||
WorkspaceID: opt.WorkspaceID,
|
||||
}
|
||||
boards, err := a.getBoardsForArchive(container, opt.BoardIDs)
|
||||
boards, err := a.getBoardsForArchive(opt.BoardIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -71,7 +67,7 @@ func (a *App) writeArchiveVersion(zw *zip.Writer) error {
|
||||
}
|
||||
|
||||
// writeArchiveBoard writes a single board to the archive in a zip directory.
|
||||
func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Block, opt model.ExportArchiveOptions) error {
|
||||
func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.ExportArchiveOptions) error {
|
||||
// create a directory per board
|
||||
w, err := zw.Create(board.ID + "/board.jsonl")
|
||||
if err != nil {
|
||||
@ -79,18 +75,14 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Block, opt model.Exp
|
||||
}
|
||||
|
||||
// write the board block first
|
||||
if err = a.writeArchiveBlockLine(w, board); err != nil {
|
||||
if err = a.writeArchiveBoardLine(w, board); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var files []string
|
||||
container := store.Container{
|
||||
WorkspaceID: opt.WorkspaceID,
|
||||
}
|
||||
|
||||
// write the board's blocks
|
||||
// TODO: paginate this
|
||||
blocks, err := a.GetBlocksWithRootID(container, board.ID)
|
||||
blocks, err := a.GetBlocksWithBoardID(board.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -143,6 +135,32 @@ func (a *App) writeArchiveBlockLine(w io.Writer, block model.Block) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// writeArchiveBlockLine writes a single block to the archive.
|
||||
func (a *App) writeArchiveBoardLine(w io.Writer, board model.Board) error {
|
||||
b, err := json.Marshal(&board)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
line := model.ArchiveLine{
|
||||
Type: "board",
|
||||
Data: b,
|
||||
}
|
||||
|
||||
b, err = json.Marshal(&line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// jsonl files need a newline
|
||||
_, err = w.Write(newline)
|
||||
return err
|
||||
}
|
||||
|
||||
// writeArchiveFile writes a single file to the archive.
|
||||
func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, opt model.ExportArchiveOptions) error {
|
||||
dest, err := zw.Create(boardID + "/" + filename)
|
||||
@ -150,12 +168,12 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string,
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := a.GetFileReader(opt.WorkspaceID, boardID, filename)
|
||||
src, err := a.GetFileReader(opt.TeamID, boardID, filename)
|
||||
if err != nil {
|
||||
// just log this; image file is missing but we'll still export an equivalent board
|
||||
a.logger.Error("image file missing for export",
|
||||
mlog.String("filename", filename),
|
||||
mlog.String("workspace_id", opt.WorkspaceID),
|
||||
mlog.String("team_id", opt.TeamID),
|
||||
mlog.String("board_id", boardID),
|
||||
)
|
||||
return nil
|
||||
@ -168,27 +186,25 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string,
|
||||
|
||||
// getBoardsForArchive fetches all the specified boards, or all boards in the workspace/team
|
||||
// if `boardIDs` is empty.
|
||||
func (a *App) getBoardsForArchive(container store.Container, boardIDs []string) ([]model.Block, error) {
|
||||
func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) {
|
||||
if len(boardIDs) == 0 {
|
||||
boards, err := a.GetBlocks(container, "", model.TypeBoard)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch all boards: %w", err)
|
||||
}
|
||||
return boards, nil
|
||||
// TODO: implement this
|
||||
// boards, err := a.GetAllBoards("", "board")
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("could not fetch all boards: %w", err)
|
||||
// }
|
||||
// return boards, nil
|
||||
return []model.Board{}, nil
|
||||
}
|
||||
|
||||
boards := make([]model.Block, 0, len(boardIDs))
|
||||
boards := make([]model.Board, 0, len(boardIDs))
|
||||
|
||||
for _, id := range boardIDs {
|
||||
b, err := a.GetBlockByID(container, id)
|
||||
b, err := a.GetBoard(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch board %s: %w", id, err)
|
||||
}
|
||||
|
||||
if b.Type != model.TypeBoard {
|
||||
return nil, fmt.Errorf("block %s is not a board: %w", b.ID, model.ErrInvalidBoardBlock)
|
||||
}
|
||||
|
||||
boards = append(boards, *b)
|
||||
}
|
||||
return boards, nil
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||
)
|
||||
|
||||
func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (string, error) {
|
||||
func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (string, error) {
|
||||
// NOTE: File extension includes the dot
|
||||
fileExtension := strings.ToLower(filepath.Ext(filename))
|
||||
if fileExtension == ".jpeg" {
|
||||
@ -21,7 +21,7 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (
|
||||
}
|
||||
|
||||
createdFilename := fmt.Sprintf(`%s%s`, utils.NewID(utils.IDTypeNone), fileExtension)
|
||||
filePath := filepath.Join(workspaceID, rootID, createdFilename)
|
||||
filePath := filepath.Join(teamID, rootID, createdFilename)
|
||||
|
||||
_, appErr := a.filesBackend.WriteFile(reader, filePath)
|
||||
if appErr != nil {
|
||||
@ -31,14 +31,14 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (
|
||||
return createdFilename, nil
|
||||
}
|
||||
|
||||
func (a *App) GetFileReader(workspaceID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
|
||||
filePath := filepath.Join(workspaceID, rootID, filename)
|
||||
func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
|
||||
filePath := filepath.Join(teamID, rootID, filename)
|
||||
exists, err := a.filesBackend.FileExists(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// FIXUP: Check the deprecated old location
|
||||
if workspaceID == "0" && !exists {
|
||||
if teamID == "0" && !exists {
|
||||
oldExists, err2 := a.filesBackend.FileExists(filename)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
|
@ -5,16 +5,17 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
testFileName = "temp-file-name"
|
||||
testRootID = "test-root-id"
|
||||
testFilePath = "1/test-root-id/temp-file-name"
|
||||
testBoardID = "test-board-id"
|
||||
testFilePath = "1/test-board-id/temp-file-name"
|
||||
)
|
||||
|
||||
type TestError struct{}
|
||||
@ -45,7 +46,7 @@ func TestGetFileReader(t *testing.T) {
|
||||
|
||||
mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc)
|
||||
mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc)
|
||||
actual, _ := th.App.GetFileReader("1", testRootID, testFileName)
|
||||
actual, _ := th.App.GetFileReader("1", testBoardID, testFileName)
|
||||
assert.Equal(t, mockedReadCloseSeek, actual)
|
||||
})
|
||||
|
||||
@ -71,7 +72,7 @@ func TestGetFileReader(t *testing.T) {
|
||||
|
||||
mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc)
|
||||
mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc)
|
||||
actual, err := th.App.GetFileReader("1", testRootID, testFileName)
|
||||
actual, err := th.App.GetFileReader("1", testBoardID, testFileName)
|
||||
assert.Error(t, err, mockedError)
|
||||
assert.Nil(t, actual)
|
||||
})
|
||||
@ -98,13 +99,13 @@ func TestGetFileReader(t *testing.T) {
|
||||
|
||||
mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc)
|
||||
mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc)
|
||||
actual, err := th.App.GetFileReader("1", testRootID, testFileName)
|
||||
actual, err := th.App.GetFileReader("1", testBoardID, testFileName)
|
||||
assert.Error(t, err, mockedError)
|
||||
assert.Nil(t, actual)
|
||||
})
|
||||
|
||||
t.Run("should move file from old filepath to new filepath, if file doesnot exists in new filepath and workspace id is 0", func(t *testing.T) {
|
||||
filePath := "0/test-root-id/temp-file-name"
|
||||
filePath := "0/test-board-id/temp-file-name"
|
||||
workspaceid := "0"
|
||||
mockedFileBackend := &mocks.FileBackend{}
|
||||
th.App.filesBackend = mockedFileBackend
|
||||
@ -134,12 +135,12 @@ func TestGetFileReader(t *testing.T) {
|
||||
mockedFileBackend.On("MoveFile", testFileName, filePath).Return(moveFileFunc)
|
||||
mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc)
|
||||
|
||||
actual, _ := th.App.GetFileReader(workspaceid, testRootID, testFileName)
|
||||
actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName)
|
||||
assert.Equal(t, mockedReadCloseSeek, actual)
|
||||
})
|
||||
|
||||
t.Run("should return file reader, if file doesnot exists in new filepath and old file path", func(t *testing.T) {
|
||||
filePath := "0/test-root-id/temp-file-name"
|
||||
filePath := "0/test-board-id/temp-file-name"
|
||||
fileName := testFileName
|
||||
workspaceid := "0"
|
||||
mockedFileBackend := &mocks.FileBackend{}
|
||||
@ -170,7 +171,7 @@ func TestGetFileReader(t *testing.T) {
|
||||
mockedFileBackend.On("MoveFile", fileName, filePath).Return(moveFileFunc)
|
||||
mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc)
|
||||
|
||||
actual, _ := th.App.GetFileReader(workspaceid, testRootID, testFileName)
|
||||
actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName)
|
||||
assert.Equal(t, mockedReadCloseSeek, actual)
|
||||
})
|
||||
}
|
||||
@ -186,7 +187,7 @@ func TestSaveFile(t *testing.T) {
|
||||
writeFileFunc := func(reader io.Reader, path string) int64 {
|
||||
paths := strings.Split(path, "/")
|
||||
assert.Equal(t, "1", paths[0])
|
||||
assert.Equal(t, testRootID, paths[1])
|
||||
assert.Equal(t, testBoardID, paths[1])
|
||||
fileName = paths[2]
|
||||
return int64(10)
|
||||
}
|
||||
@ -196,7 +197,7 @@ func TestSaveFile(t *testing.T) {
|
||||
}
|
||||
|
||||
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
|
||||
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testRootID, fileName)
|
||||
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testBoardID, fileName)
|
||||
assert.Equal(t, fileName, actual)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
@ -209,7 +210,7 @@ func TestSaveFile(t *testing.T) {
|
||||
writeFileFunc := func(reader io.Reader, path string) int64 {
|
||||
paths := strings.Split(path, "/")
|
||||
assert.Equal(t, "1", paths[0])
|
||||
assert.Equal(t, "test-root-id", paths[1])
|
||||
assert.Equal(t, "test-board-id", paths[1])
|
||||
assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1])
|
||||
return int64(10)
|
||||
}
|
||||
@ -219,7 +220,7 @@ func TestSaveFile(t *testing.T) {
|
||||
}
|
||||
|
||||
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
|
||||
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-root-id", fileName)
|
||||
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, actual)
|
||||
})
|
||||
@ -233,7 +234,7 @@ func TestSaveFile(t *testing.T) {
|
||||
writeFileFunc := func(reader io.Reader, path string) int64 {
|
||||
paths := strings.Split(path, "/")
|
||||
assert.Equal(t, "1", paths[0])
|
||||
assert.Equal(t, "test-root-id", paths[1])
|
||||
assert.Equal(t, "test-board-id", paths[1])
|
||||
assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1])
|
||||
return int64(10)
|
||||
}
|
||||
@ -243,7 +244,7 @@ func TestSaveFile(t *testing.T) {
|
||||
}
|
||||
|
||||
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
|
||||
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-root-id", fileName)
|
||||
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName)
|
||||
assert.Equal(t, "", actual)
|
||||
assert.Equal(t, "unable to store the file in the files storage: Mocked File backend error", err.Error())
|
||||
})
|
||||
|
@ -29,11 +29,10 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
defer ctrl.Finish()
|
||||
cfg := config.Configuration{}
|
||||
store := mockstore.NewMockStore(ctrl)
|
||||
|
||||
auth := auth.New(&cfg, store)
|
||||
auth := auth.New(&cfg, store, nil)
|
||||
logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)
|
||||
sessionToken := "TESTTOKEN"
|
||||
wsserver := ws.NewServer(auth, sessionToken, false, logger)
|
||||
wsserver := ws.NewServer(auth, sessionToken, false, logger, store)
|
||||
webhook := webhook.NewClient(&cfg, logger)
|
||||
metricsService := metrics.NewMetrics(metrics.InstanceInfo{})
|
||||
|
||||
@ -49,6 +48,7 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
app2 := New(&cfg, wsserver, appServices)
|
||||
|
||||
tearDown := func() {
|
||||
app2.Shutdown()
|
||||
if logger != nil {
|
||||
_ = logger.Shutdown()
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
"github.com/krolaw/zipstream"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
@ -84,7 +83,7 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
|
||||
continue
|
||||
}
|
||||
// save file with original filename so it matches name in image block.
|
||||
filePath := filepath.Join(opt.WorkspaceID, boardID, filename)
|
||||
filePath := filepath.Join(opt.TeamID, boardID, filename)
|
||||
_, err := a.filesBackend.WriteFile(zr, filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err)
|
||||
@ -103,7 +102,10 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
|
||||
func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (string, error) {
|
||||
// TODO: Stream this once `model.GenerateBlockIDs` can take a stream of blocks.
|
||||
// We don't want to load the whole file in memory, even though it's a single board.
|
||||
blocks := make([]model.Block, 0, 10)
|
||||
boardsAndBlocks := &model.BoardsAndBlocks{
|
||||
Blocks: make([]model.Block, 0, 10),
|
||||
Boards: make([]*model.Board, 0, 10),
|
||||
}
|
||||
lineReader := bufio.NewReader(r)
|
||||
|
||||
userID := opt.ModifiedBy
|
||||
@ -137,7 +139,16 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
|
||||
}
|
||||
block.ModifiedBy = userID
|
||||
block.UpdateAt = now
|
||||
blocks = append(blocks, block)
|
||||
boardsAndBlocks.Blocks = append(boardsAndBlocks.Blocks, block)
|
||||
case "board":
|
||||
var board model.Board
|
||||
if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil {
|
||||
return "", fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2)
|
||||
}
|
||||
board.ModifiedBy = userID
|
||||
board.UpdateAt = now
|
||||
board.TeamID = opt.TeamID
|
||||
boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, &board)
|
||||
default:
|
||||
return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
|
||||
}
|
||||
@ -154,36 +165,33 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
|
||||
}
|
||||
|
||||
modInfoCache := make(map[string]interface{})
|
||||
modBlocks := make([]model.Block, 0, len(blocks))
|
||||
for _, block := range blocks {
|
||||
b := block
|
||||
if opt.BlockModifier != nil && !opt.BlockModifier(&b, modInfoCache) {
|
||||
modBoards := make([]*model.Board, 0, len(boardsAndBlocks.Boards))
|
||||
for _, board := range boardsAndBlocks.Boards {
|
||||
b := *board
|
||||
if opt.BoardModifier != nil && !opt.BoardModifier(&b, modInfoCache) {
|
||||
a.logger.Debug("skipping insert block per block modifier",
|
||||
mlog.String("blockID", block.ID),
|
||||
mlog.String("block_type", block.Type.String()),
|
||||
mlog.String("blockID", board.ID),
|
||||
)
|
||||
continue
|
||||
}
|
||||
modBlocks = append(modBlocks, b)
|
||||
}
|
||||
|
||||
blocks = model.GenerateBlockIDs(modBlocks, a.logger)
|
||||
|
||||
container := store.Container{
|
||||
WorkspaceID: opt.WorkspaceID,
|
||||
modBoards = append(modBoards, &b)
|
||||
}
|
||||
boardsAndBlocks.Boards = modBoards
|
||||
|
||||
var err error
|
||||
blocks, err = a.InsertBlocks(container, blocks, opt.ModifiedBy, false)
|
||||
boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error inserting archive blocks: %w", err)
|
||||
}
|
||||
|
||||
boardsAndBlocks, err = a.CreateBoardsAndBlocks(boardsAndBlocks, opt.ModifiedBy, false)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error inserting archive blocks: %w", err)
|
||||
}
|
||||
|
||||
// find new board id
|
||||
for _, block := range blocks {
|
||||
if block.Type == model.TypeBoard {
|
||||
return block.ID, nil
|
||||
}
|
||||
for _, board := range boardsAndBlocks.Boards {
|
||||
return board.ID, nil
|
||||
}
|
||||
return "", fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock)
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package app
|
||||
|
||||
import "github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
// initialize is called when the App is first created.
|
||||
func (a *App) initialize(skipTemplateInit bool) {
|
||||
@ -10,3 +14,13 @@ func (a *App) initialize(skipTemplateInit bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Shutdown() {
|
||||
if a.blockChangeNotifier != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), blockChangeNotifierShutdownTimeout)
|
||||
defer cancel()
|
||||
if !a.blockChangeNotifier.Shutdown(ctx) {
|
||||
a.logger.Warn("blockChangeNotifier shutdown timed out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -21,17 +20,12 @@ const (
|
||||
|
||||
var (
|
||||
errUnableToFindWelcomeBoard = errors.New("unable to find welcome board in newly created blocks")
|
||||
errCannotCreateBoard = errors.New("new board wasn't created")
|
||||
)
|
||||
|
||||
func (a *App) PrepareOnboardingTour(userID string) (string, string, error) {
|
||||
// create a private workspace for the user
|
||||
workspaceID, err := a.store.CreatePrivateWorkspace(userID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
func (a *App) PrepareOnboardingTour(userID string, teamID string) (string, string, error) {
|
||||
// copy the welcome board into this workspace
|
||||
boardID, err := a.createWelcomeBoard(userID, workspaceID)
|
||||
boardID, err := a.createWelcomeBoard(userID, teamID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@ -48,18 +42,18 @@ func (a *App) PrepareOnboardingTour(userID string) (string, string, error) {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return workspaceID, boardID, nil
|
||||
return teamID, boardID, nil
|
||||
}
|
||||
|
||||
func (a *App) getOnboardingBoardID() (string, error) {
|
||||
blocks, err := a.store.GetDefaultTemplateBlocks()
|
||||
boards, err := a.store.GetTemplateBoards(globalTeamID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var onboardingBoardID string
|
||||
for _, block := range blocks {
|
||||
if block.Type == model.TypeBoard && block.Title == WelcomeBoardTitle {
|
||||
for _, block := range boards {
|
||||
if block.Title == WelcomeBoardTitle {
|
||||
onboardingBoardID = block.ID
|
||||
break
|
||||
}
|
||||
@ -72,42 +66,20 @@ func (a *App) getOnboardingBoardID() (string, error) {
|
||||
return onboardingBoardID, nil
|
||||
}
|
||||
|
||||
func (a *App) createWelcomeBoard(userID, workspaceID string) (string, error) {
|
||||
func (a *App) createWelcomeBoard(userID, teamID string) (string, error) {
|
||||
onboardingBoardID, err := a.getOnboardingBoardID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
blocks, err := a.GetSubTree(store.Container{WorkspaceID: "0"}, onboardingBoardID, 3)
|
||||
bab, _, err := a.DuplicateBoard(onboardingBoardID, userID, teamID, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
blocks = model.GenerateBlockIDs(blocks, a.logger)
|
||||
|
||||
if errUpdateFileIDs := a.CopyCardFiles(onboardingBoardID, workspaceID, blocks); errUpdateFileIDs != nil {
|
||||
return "", errUpdateFileIDs
|
||||
if len(bab.Boards) != 1 {
|
||||
return "", errCannotCreateBoard
|
||||
}
|
||||
|
||||
// we're copying from a global template, so we need to set the
|
||||
// `isTemplate` flag to false on the board
|
||||
var welcomeBoardID string
|
||||
for i := range blocks {
|
||||
if blocks[i].Type == model.TypeBoard {
|
||||
blocks[i].Fields["isTemplate"] = false
|
||||
|
||||
if blocks[i].Title == WelcomeBoardTitle {
|
||||
welcomeBoardID = blocks[i].ID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model.StampModificationMetadata(userID, blocks, nil)
|
||||
_, err = a.InsertBlocks(store.Container{WorkspaceID: workspaceID}, blocks, userID, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return welcomeBoardID, nil
|
||||
return bab.Boards[0].ID, nil
|
||||
}
|
||||
|
@ -3,44 +3,32 @@ package app
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
testTeamID = "team_id"
|
||||
)
|
||||
|
||||
func TestPrepareOnboardingTour(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
welcomeBoard := model.Block{
|
||||
ID: "block_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Welcome to Boards!",
|
||||
Fields: map[string]interface{}{
|
||||
"isTemplate": true,
|
||||
},
|
||||
teamID := testTeamID
|
||||
userID := "user_id_1"
|
||||
welcomeBoard := model.Board{
|
||||
ID: "board_id_1",
|
||||
Title: "Welcome to Boards!",
|
||||
TeamID: "0",
|
||||
IsTemplate: true,
|
||||
}
|
||||
|
||||
blocks := []model.Block{welcomeBoard}
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
th.Store.EXPECT().GetSubTree3(
|
||||
store.Container{WorkspaceID: "0"},
|
||||
"block_id_1",
|
||||
gomock.Any(),
|
||||
).Return([]model.Block{welcomeBoard}, nil)
|
||||
|
||||
th.Store.EXPECT().InsertBlock(
|
||||
store.Container{WorkspaceID: "workspace_id_1"},
|
||||
gomock.Any(),
|
||||
"user_id_1",
|
||||
).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetBlock(gomock.Any(), "block_id_1").Return(&welcomeBoard, nil)
|
||||
|
||||
th.Store.EXPECT().CreatePrivateWorkspace("user_id_1").Return("workspace_id_1", nil)
|
||||
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
|
||||
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}},
|
||||
nil, nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil)
|
||||
|
||||
userPropPatch := model.UserPropPatch{
|
||||
UpdatedFields: map[string]string{
|
||||
@ -50,11 +38,11 @@ func TestPrepareOnboardingTour(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
th.Store.EXPECT().PatchUserProps("user_id_1", userPropPatch).Return(nil)
|
||||
th.Store.EXPECT().PatchUserProps(userID, userPropPatch).Return(nil)
|
||||
|
||||
workspaceID, boardID, err := th.App.PrepareOnboardingTour("user_id_1")
|
||||
teamID, boardID, err := th.App.PrepareOnboardingTour(userID, teamID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "workspace_id_1", workspaceID)
|
||||
assert.Equal(t, testTeamID, teamID)
|
||||
assert.NotEmpty(t, boardID)
|
||||
})
|
||||
}
|
||||
@ -64,88 +52,41 @@ func TestCreateWelcomeBoard(t *testing.T) {
|
||||
defer tearDown()
|
||||
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
welcomeBoard := model.Block{
|
||||
ID: "block_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Welcome to Boards!",
|
||||
Fields: map[string]interface{}{
|
||||
"isTemplate": true,
|
||||
},
|
||||
teamID := testTeamID
|
||||
userID := "user_id_1"
|
||||
welcomeBoard := model.Board{
|
||||
ID: "board_id_1",
|
||||
Title: "Welcome to Boards!",
|
||||
TeamID: "0",
|
||||
IsTemplate: true,
|
||||
}
|
||||
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
|
||||
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).
|
||||
Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, nil, nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil)
|
||||
|
||||
blocks := []model.Block{welcomeBoard}
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
th.Store.EXPECT().GetSubTree3(
|
||||
store.Container{WorkspaceID: "0"},
|
||||
"block_id_1",
|
||||
gomock.Any(),
|
||||
).Return([]model.Block{welcomeBoard}, nil)
|
||||
|
||||
th.Store.EXPECT().InsertBlock(
|
||||
store.Container{WorkspaceID: "workspace_id_1"},
|
||||
gomock.Any(),
|
||||
"user_id_1",
|
||||
).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetBlock(gomock.Any(), "block_id_1").Return(&welcomeBoard, nil)
|
||||
|
||||
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
|
||||
boardID, err := th.App.createWelcomeBoard(userID, teamID)
|
||||
assert.Nil(t, err)
|
||||
assert.NotEmpty(t, boardID)
|
||||
})
|
||||
|
||||
t.Run("template doesn't contain a board", func(t *testing.T) {
|
||||
welcomeBoard := model.Block{
|
||||
ID: "block_id_1",
|
||||
Type: model.TypeComment,
|
||||
Title: "Welcome to Boards!",
|
||||
}
|
||||
blocks := []model.Block{welcomeBoard}
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
th.Store.EXPECT().GetSubTree3(
|
||||
store.Container{WorkspaceID: "0"},
|
||||
"buixxjic3xjfkieees4iafdrznc",
|
||||
gomock.Any(),
|
||||
).Return([]model.Block{welcomeBoard}, nil)
|
||||
|
||||
th.Store.EXPECT().InsertBlock(
|
||||
store.Container{WorkspaceID: "workspace_id_1"},
|
||||
gomock.Any(),
|
||||
"user_id_1",
|
||||
).Return(nil)
|
||||
|
||||
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
|
||||
teamID := testTeamID
|
||||
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil)
|
||||
boardID, err := th.App.createWelcomeBoard("user_id_1", teamID)
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, boardID)
|
||||
})
|
||||
|
||||
t.Run("template doesn't contain the welcome board", func(t *testing.T) {
|
||||
welcomeBoard := model.Block{
|
||||
ID: "block_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Jean luc Picard",
|
||||
Fields: map[string]interface{}{
|
||||
"isTemplate": true,
|
||||
},
|
||||
teamID := testTeamID
|
||||
welcomeBoard := model.Board{
|
||||
ID: "board_id_1",
|
||||
Title: "Other template",
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}
|
||||
|
||||
blocks := []model.Block{welcomeBoard}
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
|
||||
th.Store.EXPECT().GetSubTree3(
|
||||
store.Container{WorkspaceID: "0"},
|
||||
"buixxjic3xjfkieees4iafdrznc",
|
||||
gomock.Any(),
|
||||
).Return([]model.Block{welcomeBoard}, nil)
|
||||
|
||||
th.Store.EXPECT().InsertBlock(
|
||||
store.Container{WorkspaceID: "workspace_id_1"},
|
||||
gomock.Any(),
|
||||
"user_id_1",
|
||||
).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
|
||||
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, boardID)
|
||||
@ -157,24 +98,13 @@ func TestGetOnboardingBoardID(t *testing.T) {
|
||||
defer tearDown()
|
||||
|
||||
t.Run("base case", func(t *testing.T) {
|
||||
board := model.Block{
|
||||
ID: "board_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Welcome to Boards!",
|
||||
welcomeBoard := model.Board{
|
||||
ID: "board_id_1",
|
||||
Title: "Welcome to Boards!",
|
||||
TeamID: "0",
|
||||
IsTemplate: true,
|
||||
}
|
||||
|
||||
card := model.Block{
|
||||
ID: "card_id_1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: board.ID,
|
||||
}
|
||||
|
||||
blocks := []model.Block{
|
||||
board,
|
||||
card,
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
|
||||
|
||||
onboardingBoardID, err := th.App.getOnboardingBoardID()
|
||||
assert.NoError(t, err)
|
||||
@ -182,9 +112,7 @@ func TestGetOnboardingBoardID(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("no blocks found", func(t *testing.T) {
|
||||
blocks := []model.Block{}
|
||||
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil)
|
||||
|
||||
onboardingBoardID, err := th.App.getOnboardingBoardID()
|
||||
assert.Error(t, err)
|
||||
@ -192,24 +120,13 @@ func TestGetOnboardingBoardID(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("onboarding board doesn't exists", func(t *testing.T) {
|
||||
board := model.Block{
|
||||
ID: "board_id_1",
|
||||
Type: model.TypeBoard,
|
||||
Title: "Some board title",
|
||||
welcomeBoard := model.Board{
|
||||
ID: "board_id_1",
|
||||
Title: "Other template",
|
||||
TeamID: "0",
|
||||
IsTemplate: true,
|
||||
}
|
||||
|
||||
card := model.Block{
|
||||
ID: "card_id_1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: board.ID,
|
||||
}
|
||||
|
||||
blocks := []model.Block{
|
||||
board,
|
||||
card,
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
|
||||
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
|
||||
|
||||
onboardingBoardID, err := th.App.getOnboardingBoardID()
|
||||
assert.Error(t, err)
|
||||
|
@ -5,11 +5,10 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
)
|
||||
|
||||
func (a *App) GetSharing(c store.Container, rootID string) (*model.Sharing, error) {
|
||||
sharing, err := a.store.GetSharing(c, rootID)
|
||||
func (a *App) GetSharing(boardID string) (*model.Sharing, error) {
|
||||
sharing, err := a.store.GetSharing(boardID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
@ -19,6 +18,6 @@ func (a *App) GetSharing(c store.Container, rootID string) (*model.Sharing, erro
|
||||
return sharing, nil
|
||||
}
|
||||
|
||||
func (a *App) UpsertSharing(c store.Container, sharing model.Sharing) error {
|
||||
return a.store.UpsertSharing(c, sharing)
|
||||
func (a *App) UpsertSharing(sharing model.Sharing) error {
|
||||
return a.store.UpsertSharing(sharing)
|
||||
}
|
||||
|
@ -4,9 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
st "github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -16,10 +14,6 @@ func TestGetSharing(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
container := st.Container{
|
||||
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
|
||||
}
|
||||
|
||||
t.Run("should get a sharing successfully", func(t *testing.T) {
|
||||
want := &model.Sharing{
|
||||
ID: utils.NewID(utils.IDTypeBlock),
|
||||
@ -28,9 +22,9 @@ func TestGetSharing(t *testing.T) {
|
||||
ModifiedBy: "otherid",
|
||||
UpdateAt: utils.GetMillis(),
|
||||
}
|
||||
th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return(want, nil)
|
||||
th.Store.EXPECT().GetSharing("test-id").Return(want, nil)
|
||||
|
||||
result, err := th.App.GetSharing(container, "test-id")
|
||||
result, err := th.App.GetSharing("test-id")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, result, want)
|
||||
@ -38,11 +32,11 @@ func TestGetSharing(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should fail to get a sharing", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return(
|
||||
th.Store.EXPECT().GetSharing("test-id").Return(
|
||||
nil,
|
||||
errors.New("sharing not found"),
|
||||
)
|
||||
result, err := th.App.GetSharing(container, "test-id")
|
||||
result, err := th.App.GetSharing("test-id")
|
||||
|
||||
require.Nil(t, result)
|
||||
require.Error(t, err)
|
||||
@ -50,11 +44,11 @@ func TestGetSharing(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should return a tuple of nil", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return(
|
||||
th.Store.EXPECT().GetSharing("test-id").Return(
|
||||
nil,
|
||||
sql.ErrNoRows,
|
||||
)
|
||||
result, err := th.App.GetSharing(container, "test-id")
|
||||
result, err := th.App.GetSharing("test-id")
|
||||
|
||||
require.Nil(t, result)
|
||||
require.NoError(t, err)
|
||||
@ -65,9 +59,6 @@ func TestUpsertSharing(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
container := st.Container{
|
||||
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
|
||||
}
|
||||
sharing := model.Sharing{
|
||||
ID: utils.NewID(utils.IDTypeBlock),
|
||||
Enabled: true,
|
||||
@ -77,15 +68,15 @@ func TestUpsertSharing(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("should success to upsert sharing", func(t *testing.T) {
|
||||
th.Store.EXPECT().UpsertSharing(gomock.Eq(container), gomock.Eq(sharing)).Return(nil)
|
||||
err := th.App.UpsertSharing(container, sharing)
|
||||
th.Store.EXPECT().UpsertSharing(sharing).Return(nil)
|
||||
err := th.App.UpsertSharing(sharing)
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should fail to upsert a sharing", func(t *testing.T) {
|
||||
th.Store.EXPECT().UpsertSharing(gomock.Eq(container), gomock.Eq(sharing)).Return(errors.New("sharing not found"))
|
||||
err := th.App.UpsertSharing(container, sharing)
|
||||
th.Store.EXPECT().UpsertSharing(sharing).Return(errors.New("sharing not found"))
|
||||
err := th.App.UpsertSharing(sharing)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "sharing not found", err.Error())
|
||||
|
@ -2,41 +2,40 @@ package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
)
|
||||
|
||||
func (a *App) CreateSubscription(c store.Container, sub *model.Subscription) (*model.Subscription, error) {
|
||||
sub, err := a.store.CreateSubscription(c, sub)
|
||||
func (a *App) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) {
|
||||
sub, err := a.store.CreateSubscription(sub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.notifySubscriptionChanged(c, sub)
|
||||
a.notifySubscriptionChanged(sub)
|
||||
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteSubscription(c store.Container, blockID string, subscriberID string) (*model.Subscription, error) {
|
||||
sub, err := a.store.GetSubscription(c, blockID, subscriberID)
|
||||
func (a *App) DeleteSubscription(blockID string, subscriberID string) (*model.Subscription, error) {
|
||||
sub, err := a.store.GetSubscription(blockID, subscriberID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.store.DeleteSubscription(c, blockID, subscriberID); err != nil {
|
||||
if err := a.store.DeleteSubscription(blockID, subscriberID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub.DeleteAt = utils.GetMillis()
|
||||
a.notifySubscriptionChanged(c, sub)
|
||||
a.notifySubscriptionChanged(sub)
|
||||
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (a *App) GetSubscriptions(c store.Container, subscriberID string) ([]*model.Subscription, error) {
|
||||
return a.store.GetSubscriptions(c, subscriberID)
|
||||
func (a *App) GetSubscriptions(subscriberID string) ([]*model.Subscription, error) {
|
||||
return a.store.GetSubscriptions(subscriberID)
|
||||
}
|
||||
|
||||
func (a *App) notifySubscriptionChanged(c store.Container, subscription *model.Subscription) {
|
||||
func (a *App) notifySubscriptionChanged(subscription *model.Subscription) {
|
||||
if a.notifications == nil {
|
||||
return
|
||||
}
|
||||
a.notifications.BroadcastSubscriptionChange(c.WorkspaceID, subscription)
|
||||
a.notifications.BroadcastSubscriptionChange(subscription)
|
||||
}
|
||||
|
68
server/app/teams.go
Normal file
68
server/app/teams.go
Normal file
@ -0,0 +1,68 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (a *App) GetRootTeam() (*model.Team, error) {
|
||||
teamID := "0"
|
||||
team, _ := a.store.GetTeam(teamID)
|
||||
if team == nil {
|
||||
team = &model.Team{
|
||||
ID: teamID,
|
||||
SignupToken: utils.NewID(utils.IDTypeToken),
|
||||
}
|
||||
err := a.store.UpsertTeamSignupToken(*team)
|
||||
if err != nil {
|
||||
a.logger.Error("Unable to initialize team", mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
team, err = a.store.GetTeam(teamID)
|
||||
if err != nil {
|
||||
a.logger.Error("Unable to get initialized team", mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.logger.Info("initialized team")
|
||||
}
|
||||
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func (a *App) GetTeam(id string) (*model.Team, error) {
|
||||
team, err := a.store.GetTeam(id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func (a *App) GetTeamsForUser(userID string) ([]*model.Team, error) {
|
||||
return a.store.GetTeamsForUser(userID)
|
||||
}
|
||||
|
||||
func (a *App) DoesUserHaveTeamAccess(userID string, teamID string) bool {
|
||||
return a.auth.DoesUserHaveTeamAccess(userID, teamID)
|
||||
}
|
||||
|
||||
func (a *App) UpsertTeamSettings(team model.Team) error {
|
||||
return a.store.UpsertTeamSettings(team)
|
||||
}
|
||||
|
||||
func (a *App) UpsertTeamSignupToken(team model.Team) error {
|
||||
return a.store.UpsertTeamSignupToken(team)
|
||||
}
|
||||
|
||||
func (a *App) GetTeamCount() (int64, error) {
|
||||
return a.store.GetTeamCount()
|
||||
}
|
152
server/app/teams_test.go
Normal file
152
server/app/teams_test.go
Normal file
@ -0,0 +1,152 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var errInvalidTeam = errors.New("invalid team id")
|
||||
|
||||
var mockTeam = &model.Team{
|
||||
ID: "mock-team-id",
|
||||
Title: "MockTeam",
|
||||
}
|
||||
|
||||
var errUpsertSignupToken = errors.New("upsert error")
|
||||
|
||||
func TestGetRootTeam(t *testing.T) {
|
||||
var newRootTeam = &model.Team{
|
||||
ID: "0",
|
||||
Title: "NewRootTeam",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
title string
|
||||
teamToReturnBeforeUpsert *model.Team
|
||||
teamToReturnAfterUpsert *model.Team
|
||||
isError bool
|
||||
}{
|
||||
{
|
||||
"Success, Return new root team, when root team returned by mockstore is nil",
|
||||
nil,
|
||||
newRootTeam,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Success, Return existing root team, when root team returned by mockstore is notnil",
|
||||
newRootTeam,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Fail, Return nil, when root team returned by mockstore is nil, and upsert new root team fails",
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.title, func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnBeforeUpsert, nil)
|
||||
th.Store.EXPECT().UpsertTeamSignupToken(gomock.Any()).DoAndReturn(
|
||||
func(arg0 model.Team) error {
|
||||
if tc.isError {
|
||||
return errUpsertSignupToken
|
||||
}
|
||||
th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnAfterUpsert, nil)
|
||||
return nil
|
||||
})
|
||||
rootTeam, err := th.App.GetRootTeam()
|
||||
|
||||
if tc.isError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
assert.NotNil(t, rootTeam.ID)
|
||||
assert.NotNil(t, rootTeam.SignupToken)
|
||||
assert.Equal(t, "", rootTeam.ModifiedBy)
|
||||
assert.Equal(t, int64(0), rootTeam.UpdateAt)
|
||||
assert.Equal(t, "NewRootTeam", rootTeam.Title)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rootTeam)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTeam(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
testCases := []struct {
|
||||
title string
|
||||
teamID string
|
||||
isError bool
|
||||
}{
|
||||
{
|
||||
"Success, Return new root team, when team returned by mockstore is not nil",
|
||||
"mock-team-id",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Success, Return nil, when get team returns an sql error",
|
||||
"team-not-available-id",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Fail, Return nil, when get team by mockstore returns an error",
|
||||
"invalid-team-id",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetTeam("mock-team-id").Return(mockTeam, nil)
|
||||
th.Store.EXPECT().GetTeam("invalid-team-id").Return(nil, errInvalidTeam)
|
||||
th.Store.EXPECT().GetTeam("team-not-available-id").Return(nil, sql.ErrNoRows)
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.title, func(t *testing.T) {
|
||||
t.Log(tc.title)
|
||||
team, err := th.App.GetTeam(tc.teamID)
|
||||
|
||||
if tc.isError {
|
||||
require.Error(t, err)
|
||||
} else if tc.teamID != "team-not-available-id" {
|
||||
assert.NotNil(t, team.ID)
|
||||
assert.NotNil(t, team.SignupToken)
|
||||
assert.Equal(t, "mock-team-id", team.ID)
|
||||
assert.Equal(t, "", team.ModifiedBy)
|
||||
assert.Equal(t, int64(0), team.UpdateAt)
|
||||
assert.Equal(t, "MockTeam", team.Title)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, team)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamOperations(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
th.Store.EXPECT().UpsertTeamSettings(*mockTeam).Return(nil)
|
||||
th.Store.EXPECT().UpsertTeamSignupToken(*mockTeam).Return(nil)
|
||||
th.Store.EXPECT().GetTeamCount().Return(int64(10), nil)
|
||||
|
||||
errUpsertTeamSettings := th.App.UpsertTeamSettings(*mockTeam)
|
||||
assert.NoError(t, errUpsertTeamSettings)
|
||||
|
||||
errUpsertTeamSignupToken := th.App.UpsertTeamSignupToken(*mockTeam)
|
||||
assert.NoError(t, errUpsertTeamSignupToken)
|
||||
|
||||
count, errGetTeamCount := th.App.GetTeamCount()
|
||||
assert.NoError(t, errGetTeamCount)
|
||||
assert.Equal(t, int64(10), count)
|
||||
}
|
Binary file not shown.
@ -14,21 +14,26 @@ import (
|
||||
|
||||
const (
|
||||
defaultTemplateVersion = 2
|
||||
globalTeamID = "0"
|
||||
)
|
||||
|
||||
//go:embed templates.boardarchive
|
||||
var defTemplates []byte
|
||||
|
||||
// initializeTemplates imports default templates if the blocks table is empty.
|
||||
func (a *App) InitTemplates() error {
|
||||
return a.initializeTemplates()
|
||||
}
|
||||
|
||||
// initializeTemplates imports default templates if the boards table is empty.
|
||||
func (a *App) initializeTemplates() error {
|
||||
blocks, err := a.store.GetDefaultTemplateBlocks()
|
||||
boards, err := a.store.GetTemplateBoards(globalTeamID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot initialize templates: %w", err)
|
||||
}
|
||||
|
||||
a.logger.Debug("Fetched template blocks", mlog.Int("count", len(blocks)))
|
||||
a.logger.Debug("Fetched template boards", mlog.Int("count", len(boards)))
|
||||
|
||||
isNeeded, reason := a.isInitializationNeeded(blocks)
|
||||
isNeeded, reason := a.isInitializationNeeded(boards)
|
||||
if !isNeeded {
|
||||
a.logger.Debug("Template import not needed, skipping")
|
||||
return nil
|
||||
@ -36,67 +41,55 @@ func (a *App) initializeTemplates() error {
|
||||
|
||||
a.logger.Debug("Importing new default templates", mlog.String("reason", reason))
|
||||
|
||||
if err := a.store.RemoveDefaultTemplates(blocks); err != nil {
|
||||
return fmt.Errorf("cannot remove old templates: %w", err)
|
||||
// Remove in case of newer Templates
|
||||
if err = a.store.RemoveDefaultTemplates(boards); err != nil {
|
||||
return fmt.Errorf("cannot remove old template boards: %w", err)
|
||||
}
|
||||
|
||||
r := bytes.NewReader(defTemplates)
|
||||
|
||||
opt := model.ImportArchiveOptions{
|
||||
WorkspaceID: "0",
|
||||
TeamID: globalTeamID,
|
||||
ModifiedBy: "system",
|
||||
BlockModifier: fixTemplateBlock,
|
||||
BoardModifier: fixTemplateBoard,
|
||||
}
|
||||
|
||||
return a.ImportArchive(r, opt)
|
||||
if err = a.ImportArchive(r, opt); err != nil {
|
||||
return fmt.Errorf("cannot initialize global templates for team %s: %w", globalTeamID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isInitializationNeeded returns true if the blocks table contains no default templates,
|
||||
// or contains at least one default template with an old version number.
|
||||
func (a *App) isInitializationNeeded(blocks []model.Block) (bool, string) {
|
||||
if len(blocks) == 0 {
|
||||
func (a *App) isInitializationNeeded(boards []*model.Board) (bool, string) {
|
||||
if len(boards) == 0 {
|
||||
return true, "no default templates found"
|
||||
}
|
||||
|
||||
// look for any template blocks with the wrong version number (or no version #).
|
||||
for _, block := range blocks {
|
||||
v, ok := block.Fields["templateVer"]
|
||||
if !ok {
|
||||
return true, "block missing templateVer"
|
||||
// look for any built-in template boards with the wrong version number (or no version #).
|
||||
for _, board := range boards {
|
||||
// if not built-in board...skip
|
||||
if board.CreatedBy != "system" {
|
||||
continue
|
||||
}
|
||||
version, ok := v.(float64)
|
||||
if !ok {
|
||||
return true, "templateVer NaN"
|
||||
}
|
||||
if version < defaultTemplateVersion {
|
||||
return true, "templateVer too old"
|
||||
if board.TemplateVersion < defaultTemplateVersion {
|
||||
return true, "template_version too old"
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// fixTemplateBlock fixes a block to be inserted as part of a template.
|
||||
func fixTemplateBlock(block *model.Block, cache map[string]interface{}) bool {
|
||||
// cache contains ids of skipped blocks. Ensure their children are skipped as well.
|
||||
if _, ok := cache[block.ParentID]; ok {
|
||||
cache[block.ID] = struct{}{}
|
||||
return false
|
||||
}
|
||||
|
||||
// fixTemplateBoard fixes a board to be inserted as part of a template.
|
||||
func fixTemplateBoard(board *model.Board, cache map[string]interface{}) bool {
|
||||
// filter out template blocks; we only want the non-template
|
||||
// blocks which we will turn into default template blocks.
|
||||
if b, ok := block.Fields["isTemplate"]; ok {
|
||||
if val, ok := b.(bool); ok && val {
|
||||
cache[block.ID] = struct{}{}
|
||||
return false
|
||||
}
|
||||
if board.IsTemplate {
|
||||
cache[board.ID] = struct{}{}
|
||||
}
|
||||
|
||||
// remove '(NEW)' from title & force template flag
|
||||
if block.Type == model.TypeBoard {
|
||||
block.Title = strings.ReplaceAll(block.Title, "(NEW)", "")
|
||||
block.Fields["isTemplate"] = true
|
||||
block.Fields["templateVer"] = defaultTemplateVersion
|
||||
}
|
||||
board.Title = strings.ReplaceAll(board.Title, "(NEW)", "")
|
||||
board.IsTemplate = true
|
||||
board.TemplateVersion = defaultTemplateVersion
|
||||
return true
|
||||
}
|
||||
|
@ -2,8 +2,12 @@ package app
|
||||
|
||||
import "github.com/mattermost/focalboard/server/model"
|
||||
|
||||
func (a *App) GetWorkspaceUsers(workspaceID string) ([]*model.User, error) {
|
||||
return a.store.GetUsersByWorkspace(workspaceID)
|
||||
func (a *App) GetTeamUsers(teamID string) ([]*model.User, error) {
|
||||
return a.store.GetUsersByTeam(teamID)
|
||||
}
|
||||
|
||||
func (a *App) SearchTeamUsers(teamID string, searchQuery string) ([]*model.User, error) {
|
||||
return a.store.SearchUsersByTeam(teamID, searchQuery)
|
||||
}
|
||||
|
||||
func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[string]interface{}, error) {
|
||||
|
@ -1,67 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (a *App) GetRootWorkspace() (*model.Workspace, error) {
|
||||
workspaceID := "0"
|
||||
workspace, _ := a.store.GetWorkspace(workspaceID)
|
||||
if workspace == nil {
|
||||
workspace = &model.Workspace{
|
||||
ID: workspaceID,
|
||||
SignupToken: utils.NewID(utils.IDTypeToken),
|
||||
}
|
||||
err := a.store.UpsertWorkspaceSignupToken(*workspace)
|
||||
if err != nil {
|
||||
a.logger.Error("Unable to initialize workspace", mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
workspace, err = a.store.GetWorkspace(workspaceID)
|
||||
if err != nil {
|
||||
a.logger.Error("Unable to get initialized workspace", mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.logger.Info("initialized workspace")
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
func (a *App) GetWorkspace(id string) (*model.Workspace, error) {
|
||||
workspace, err := a.store.GetWorkspace(id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
func (a *App) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool {
|
||||
return a.auth.DoesUserHaveWorkspaceAccess(userID, workspaceID)
|
||||
}
|
||||
|
||||
func (a *App) UpsertWorkspaceSettings(workspace model.Workspace) error {
|
||||
return a.store.UpsertWorkspaceSettings(workspace)
|
||||
}
|
||||
|
||||
func (a *App) UpsertWorkspaceSignupToken(workspace model.Workspace) error {
|
||||
return a.store.UpsertWorkspaceSignupToken(workspace)
|
||||
}
|
||||
|
||||
func (a *App) GetWorkspaceCount() (int64, error) {
|
||||
return a.store.GetWorkspaceCount()
|
||||
}
|
||||
|
||||
func (a *App) GetUserWorkspaces(userID string) ([]model.UserWorkspace, error) {
|
||||
return a.store.GetUserWorkspaces(userID)
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var errInvalidWorkspace = errors.New("invalid workspace id")
|
||||
|
||||
var mockWorkspace = &model.Workspace{
|
||||
ID: "mock-workspace-id",
|
||||
Title: "MockWorkspace",
|
||||
}
|
||||
|
||||
var mockUserWorkspaces = []model.UserWorkspace{
|
||||
{
|
||||
ID: "mock-user-workspace-id",
|
||||
Title: "MockUserWorkspace",
|
||||
},
|
||||
}
|
||||
|
||||
var errUpsertSignupToken = errors.New("upsert error")
|
||||
|
||||
func TestGetRootWorkspace(t *testing.T) {
|
||||
var newRootWorkspace = &model.Workspace{
|
||||
ID: "0",
|
||||
Title: "NewRootWorkspace",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
title string
|
||||
workSpaceToReturnBeforeUpsert *model.Workspace
|
||||
workSpaceToReturnAfterUpsert *model.Workspace
|
||||
isError bool
|
||||
}{
|
||||
{
|
||||
"Success, Return new root workspace, when root workspace returned by mockstore is nil",
|
||||
nil,
|
||||
newRootWorkspace,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Success, Return existing root workspace, when root workspace returned by mockstore is notnil",
|
||||
newRootWorkspace,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Fail, Return nil, when root workspace returned by mockstore is nil, and upsert new root workspace fails",
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, eachTestacase := range testCases {
|
||||
t.Run(eachTestacase.title, func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
t.Log(eachTestacase.title)
|
||||
th.Store.EXPECT().GetWorkspace("0").Return(eachTestacase.workSpaceToReturnBeforeUpsert, nil)
|
||||
th.Store.EXPECT().UpsertWorkspaceSignupToken(gomock.Any()).DoAndReturn(
|
||||
func(arg0 model.Workspace) error {
|
||||
if eachTestacase.isError {
|
||||
return errUpsertSignupToken
|
||||
}
|
||||
th.Store.EXPECT().GetWorkspace("0").Return(eachTestacase.workSpaceToReturnAfterUpsert, nil)
|
||||
return nil
|
||||
})
|
||||
rootWorkSpace, err := th.App.GetRootWorkspace()
|
||||
|
||||
if eachTestacase.isError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
assert.NotNil(t, rootWorkSpace.ID)
|
||||
assert.NotNil(t, rootWorkSpace.SignupToken)
|
||||
assert.Equal(t, "", rootWorkSpace.ModifiedBy)
|
||||
assert.Equal(t, int64(0), rootWorkSpace.UpdateAt)
|
||||
assert.Equal(t, "NewRootWorkspace", rootWorkSpace.Title)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rootWorkSpace)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWorkspace(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
testCases := []struct {
|
||||
title string
|
||||
workspaceID string
|
||||
isError bool
|
||||
}{
|
||||
{
|
||||
"Success, Return new root workspace, when workspace returned by mockstore is not nil",
|
||||
"mock-workspace-id",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Success, Return nil, when get workspace returns an sql error",
|
||||
"workspace-not-available-id",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Fail, Return nil, when get workspace by mockstore retruns an error",
|
||||
"invalid-workspace-id",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetWorkspace("mock-workspace-id").Return(mockWorkspace, nil)
|
||||
th.Store.EXPECT().GetWorkspace("invalid-workspace-id").Return(nil, errInvalidWorkspace)
|
||||
th.Store.EXPECT().GetWorkspace("workspace-not-available-id").Return(nil, sql.ErrNoRows)
|
||||
for _, eachTestacase := range testCases {
|
||||
t.Run(eachTestacase.title, func(t *testing.T) {
|
||||
t.Log(eachTestacase.title)
|
||||
workSpace, err := th.App.GetWorkspace(eachTestacase.workspaceID)
|
||||
|
||||
if eachTestacase.isError {
|
||||
require.Error(t, err)
|
||||
} else if eachTestacase.workspaceID != "workspace-not-available-id" {
|
||||
assert.NotNil(t, workSpace.ID)
|
||||
assert.NotNil(t, workSpace.SignupToken)
|
||||
assert.Equal(t, "mock-workspace-id", workSpace.ID)
|
||||
assert.Equal(t, "", workSpace.ModifiedBy)
|
||||
assert.Equal(t, int64(0), workSpace.UpdateAt)
|
||||
assert.Equal(t, "MockWorkspace", workSpace.Title)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, workSpace)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkspaceOperations(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
th.Store.EXPECT().UpsertWorkspaceSettings(*mockWorkspace).Return(nil)
|
||||
th.Store.EXPECT().UpsertWorkspaceSignupToken(*mockWorkspace).Return(nil)
|
||||
th.Store.EXPECT().GetWorkspaceCount().Return(int64(10), nil)
|
||||
th.Store.EXPECT().GetUserWorkspaces("mock-user-id").Return(mockUserWorkspaces, nil)
|
||||
|
||||
errUpsertWorkspaceSettings := th.App.UpsertWorkspaceSettings(*mockWorkspace)
|
||||
assert.NoError(t, errUpsertWorkspaceSettings)
|
||||
|
||||
errUpsertWorkspaceSignupToken := th.App.UpsertWorkspaceSignupToken(*mockWorkspace)
|
||||
assert.NoError(t, errUpsertWorkspaceSignupToken)
|
||||
|
||||
count, errGetWorkspaceCount := th.App.GetWorkspaceCount()
|
||||
assert.NoError(t, errGetWorkspaceCount)
|
||||
assert.Equal(t, int64(10), count)
|
||||
|
||||
userWorkSpace, errGetUserWorkSpace := th.App.GetUserWorkspaces("mock-user-id")
|
||||
assert.NoError(t, errGetUserWorkSpace)
|
||||
assert.NotNil(t, userWorkSpace)
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/permissions"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/pkg/errors"
|
||||
@ -13,19 +14,20 @@ import (
|
||||
|
||||
type AuthInterface interface {
|
||||
GetSession(token string) (*model.Session, error)
|
||||
IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error)
|
||||
DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool
|
||||
IsValidReadToken(boardID string, readToken string) (bool, error)
|
||||
DoesUserHaveTeamAccess(userID string, teamID string) bool
|
||||
}
|
||||
|
||||
// Auth authenticates sessions.
|
||||
type Auth struct {
|
||||
config *config.Configuration
|
||||
store store.Store
|
||||
config *config.Configuration
|
||||
store store.Store
|
||||
permissions permissions.PermissionsService
|
||||
}
|
||||
|
||||
// New returns a new Auth.
|
||||
func New(config *config.Configuration, store store.Store) *Auth {
|
||||
return &Auth{config: config, store: store}
|
||||
func New(config *config.Configuration, store store.Store, permissions permissions.PermissionsService) *Auth {
|
||||
return &Auth{config: config, store: store, permissions: permissions}
|
||||
}
|
||||
|
||||
// GetSession Get a user active session and refresh the session if needed.
|
||||
@ -44,14 +46,9 @@ func (a *Auth) GetSession(token string) (*model.Session, error) {
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// IsValidReadToken validates the read token for a block.
|
||||
func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) {
|
||||
rootID, err := a.store.GetRootID(c, blockID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
sharing, err := a.store.GetSharing(c, rootID)
|
||||
// IsValidReadToken validates the read token for a board.
|
||||
func (a *Auth) IsValidReadToken(boardID string, readToken string) (bool, error) {
|
||||
sharing, err := a.store.GetSharing(boardID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
@ -59,17 +56,13 @@ func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken str
|
||||
return false, err
|
||||
}
|
||||
|
||||
if sharing != nil && (sharing.ID == rootID && sharing.Enabled && sharing.Token == readToken) {
|
||||
if sharing != nil && (sharing.ID == boardID && sharing.Enabled && sharing.Token == readToken) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (a *Auth) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool {
|
||||
hasAccess, err := a.store.HasWorkspaceAccess(userID, workspaceID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return hasAccess
|
||||
func (a *Auth) DoesUserHaveTeamAccess(userID string, teamID string) bool {
|
||||
return a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam)
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
|
||||
mockpermissions "github.com/mattermost/focalboard/server/services/permissions/mocks"
|
||||
"github.com/mattermost/focalboard/server/services/store/mockstore"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -31,14 +32,19 @@ var mockSession = &model.Session{
|
||||
func setupTestHelper(t *testing.T) *TestHelper {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctrlPermissions := gomock.NewController(t)
|
||||
defer ctrlPermissions.Finish()
|
||||
cfg := config.Configuration{}
|
||||
mockStore := mockstore.NewMockStore(ctrl)
|
||||
newAuth := New(&cfg, mockStore)
|
||||
mockPermissions := mockpermissions.NewMockStore(ctrlPermissions)
|
||||
logger, err := mlog.NewLogger()
|
||||
require.NoError(t, err)
|
||||
newAuth := New(&cfg, mockStore, localpermissions.New(mockPermissions, logger))
|
||||
|
||||
// called during default template setup for every test
|
||||
mockStore.EXPECT().GetDefaultTemplateBlocks().AnyTimes()
|
||||
mockStore.EXPECT().GetTemplateBoards(gomock.Any()).AnyTimes()
|
||||
mockStore.EXPECT().RemoveDefaultTemplates(gomock.Any()).AnyTimes()
|
||||
mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||
mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
return &TestHelper{
|
||||
Auth: newAuth,
|
||||
@ -83,55 +89,57 @@ func TestGetSession(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsValidReadToken(t *testing.T) {
|
||||
th := setupTestHelper(t)
|
||||
// ToDo: reimplement
|
||||
|
||||
validBlockID := "testBlockID"
|
||||
mockContainer := store.Container{
|
||||
WorkspaceID: "testWorkspaceID",
|
||||
}
|
||||
validReadToken := "testReadToken"
|
||||
mockSharing := model.Sharing{
|
||||
ID: "testRootID",
|
||||
Enabled: true,
|
||||
Token: validReadToken,
|
||||
}
|
||||
// th := setupTestHelper(t)
|
||||
|
||||
testcases := []struct {
|
||||
title string
|
||||
container store.Container
|
||||
blockID string
|
||||
readToken string
|
||||
isError bool
|
||||
isSuccess bool
|
||||
}{
|
||||
{"fail, error GetRootID", mockContainer, "badBlock", "", true, false},
|
||||
{"fail, rootID not found", mockContainer, "goodBlockID", "", false, false},
|
||||
{"fail, sharing throws error", mockContainer, "goodBlockID2", "", true, false},
|
||||
{"fail, bad readToken", mockContainer, validBlockID, "invalidReadToken", false, false},
|
||||
{"success", mockContainer, validBlockID, validReadToken, false, true},
|
||||
}
|
||||
// validBlockID := "testBlockID"
|
||||
// mockContainer := store.Container{
|
||||
// TeamID: "testTeamID",
|
||||
// }
|
||||
// validReadToken := "testReadToken"
|
||||
// mockSharing := model.Sharing{
|
||||
// ID: "testRootID",
|
||||
// Enabled: true,
|
||||
// Token: validReadToken,
|
||||
// }
|
||||
|
||||
th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "badBlock").Return("", errors.New("invalid block"))
|
||||
th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID").Return("rootNotFound", nil)
|
||||
th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID2").Return("rootError", nil)
|
||||
th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), validBlockID).Return("testRootID", nil).Times(2)
|
||||
th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootNotFound").Return(nil, sql.ErrNoRows)
|
||||
th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootError").Return(nil, errors.New("another error"))
|
||||
th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "testRootID").Return(&mockSharing, nil).Times(2)
|
||||
// testcases := []struct {
|
||||
// title string
|
||||
// container store.Container
|
||||
// blockID string
|
||||
// readToken string
|
||||
// isError bool
|
||||
// isSuccess bool
|
||||
// }{
|
||||
// {"fail, error GetRootID", mockContainer, "badBlock", "", true, false},
|
||||
// {"fail, rootID not found", mockContainer, "goodBlockID", "", false, false},
|
||||
// {"fail, sharing throws error", mockContainer, "goodBlockID2", "", true, false},
|
||||
// {"fail, bad readToken", mockContainer, validBlockID, "invalidReadToken", false, false},
|
||||
// {"success", mockContainer, validBlockID, validReadToken, false, true},
|
||||
// }
|
||||
|
||||
for _, test := range testcases {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
success, err := th.Auth.IsValidReadToken(test.container, test.blockID, test.readToken)
|
||||
if test.isError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if test.isSuccess {
|
||||
require.True(t, success)
|
||||
} else {
|
||||
require.False(t, success)
|
||||
}
|
||||
})
|
||||
}
|
||||
// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "badBlock").Return("", errors.New("invalid block"))
|
||||
// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID").Return("rootNotFound", nil)
|
||||
// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID2").Return("rootError", nil)
|
||||
// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), validBlockID).Return("testRootID", nil).Times(2)
|
||||
// th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootNotFound").Return(nil, sql.ErrNoRows)
|
||||
// th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootError").Return(nil, errors.New("another error"))
|
||||
// th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "testRootID").Return(&mockSharing, nil).Times(2)
|
||||
|
||||
// for _, test := range testcases {
|
||||
// t.Run(test.title, func(t *testing.T) {
|
||||
// success, err := th.Auth.IsValidReadToken(test.container, test.blockID, test.readToken)
|
||||
// if test.isError {
|
||||
// require.Error(t, err)
|
||||
// } else {
|
||||
// require.NoError(t, err)
|
||||
// }
|
||||
// if test.isSuccess {
|
||||
// require.True(t, success)
|
||||
// } else {
|
||||
// require.False(t, success)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
model "github.com/mattermost/focalboard/server/model"
|
||||
store "github.com/mattermost/focalboard/server/services/store"
|
||||
)
|
||||
|
||||
// MockAuthInterface is a mock of AuthInterface interface.
|
||||
@ -35,18 +34,18 @@ func (m *MockAuthInterface) EXPECT() *MockAuthInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// DoesUserHaveWorkspaceAccess mocks base method.
|
||||
func (m *MockAuthInterface) DoesUserHaveWorkspaceAccess(arg0, arg1 string) bool {
|
||||
// DoesUserHaveTeamAccess mocks base method.
|
||||
func (m *MockAuthInterface) DoesUserHaveTeamAccess(arg0, arg1 string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DoesUserHaveWorkspaceAccess", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "DoesUserHaveTeamAccess", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DoesUserHaveWorkspaceAccess indicates an expected call of DoesUserHaveWorkspaceAccess.
|
||||
func (mr *MockAuthInterfaceMockRecorder) DoesUserHaveWorkspaceAccess(arg0, arg1 interface{}) *gomock.Call {
|
||||
// DoesUserHaveTeamAccess indicates an expected call of DoesUserHaveTeamAccess.
|
||||
func (mr *MockAuthInterfaceMockRecorder) DoesUserHaveTeamAccess(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoesUserHaveWorkspaceAccess", reflect.TypeOf((*MockAuthInterface)(nil).DoesUserHaveWorkspaceAccess), arg0, arg1)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoesUserHaveTeamAccess", reflect.TypeOf((*MockAuthInterface)(nil).DoesUserHaveTeamAccess), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetSession mocks base method.
|
||||
@ -65,16 +64,16 @@ func (mr *MockAuthInterfaceMockRecorder) GetSession(arg0 interface{}) *gomock.Ca
|
||||
}
|
||||
|
||||
// IsValidReadToken mocks base method.
|
||||
func (m *MockAuthInterface) IsValidReadToken(arg0 store.Container, arg1, arg2 string) (bool, error) {
|
||||
func (m *MockAuthInterface) IsValidReadToken(arg0, arg1 string) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsValidReadToken", arg0, arg1, arg2)
|
||||
ret := m.ctrl.Call(m, "IsValidReadToken", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsValidReadToken indicates an expected call of IsValidReadToken.
|
||||
func (mr *MockAuthInterfaceMockRecorder) IsValidReadToken(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
func (mr *MockAuthInterfaceMockRecorder) IsValidReadToken(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidReadToken", reflect.TypeOf((*MockAuthInterface)(nil).IsValidReadToken), arg0, arg1, arg2)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidReadToken", reflect.TypeOf((*MockAuthInterface)(nil).IsValidReadToken), arg0, arg1)
|
||||
}
|
||||
|
@ -101,8 +101,8 @@ func (c *Client) DoAPIPut(url, data string) (*http.Response, error) {
|
||||
return c.DoAPIRequest(http.MethodPut, c.APIURL+url, data, "")
|
||||
}
|
||||
|
||||
func (c *Client) DoAPIDelete(url string) (*http.Response, error) {
|
||||
return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, "", "")
|
||||
func (c *Client) DoAPIDelete(url string, data string) (*http.Response, error) {
|
||||
return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, data, "")
|
||||
}
|
||||
|
||||
func (c *Client) DoAPIRequest(method, url, data, etag string) (*http.Response, error) {
|
||||
@ -152,20 +152,50 @@ func (c *Client) doAPIRequestReader(method, url string, data io.Reader, _ /* eta
|
||||
return rp, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetBlocksRoute() string {
|
||||
return "/workspaces/0/blocks"
|
||||
func (c *Client) GetTeamRoute(teamID string) string {
|
||||
return fmt.Sprintf("%s/%s", c.GetTeamsRoute(), teamID)
|
||||
}
|
||||
|
||||
func (c *Client) GetBlockRoute(id string) string {
|
||||
return fmt.Sprintf("%s/%s", c.GetBlocksRoute(), id)
|
||||
func (c *Client) GetTeamsRoute() string {
|
||||
return "/teams"
|
||||
}
|
||||
|
||||
func (c *Client) GetSubtreeRoute(id string) string {
|
||||
return fmt.Sprintf("%s/subtree", c.GetBlockRoute(id))
|
||||
func (c *Client) GetBlockRoute(boardID, blockID string) string {
|
||||
return fmt.Sprintf("%s/%s", c.GetBlocksRoute(boardID), blockID)
|
||||
}
|
||||
|
||||
func (c *Client) GetBlocks() ([]model.Block, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetBlocksRoute(), "")
|
||||
func (c *Client) GetSubtreeRoute(boardID, blockID string) string {
|
||||
return fmt.Sprintf("%s/subtree", c.GetBlockRoute(boardID, blockID))
|
||||
}
|
||||
|
||||
func (c *Client) GetBoardsRoute() string {
|
||||
return "/boards"
|
||||
}
|
||||
|
||||
func (c *Client) GetBoardRoute(boardID string) string {
|
||||
return fmt.Sprintf("%s/%s", c.GetBoardsRoute(), boardID)
|
||||
}
|
||||
|
||||
func (c *Client) GetBlocksRoute(boardID string) string {
|
||||
return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID))
|
||||
}
|
||||
|
||||
func (c *Client) GetBoardsAndBlocksRoute() string {
|
||||
return "/boards-and-blocks"
|
||||
}
|
||||
|
||||
func (c *Client) GetTeam(teamID string) (*model.Team, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetTeamRoute(teamID), "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.TeamFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetBlocksForBoard(boardID string) ([]model.Block, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetBlocksRoute(boardID), "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -174,8 +204,8 @@ func (c *Client) GetBlocks() ([]model.Block, *Response) {
|
||||
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool, *Response) {
|
||||
r, err := c.DoAPIPatch(c.GetBlockRoute(blockID), toJSON(blockPatch))
|
||||
func (c *Client) PatchBlock(boardID, blockID string, blockPatch *model.BlockPatch) (bool, *Response) {
|
||||
r, err := c.DoAPIPatch(c.GetBlockRoute(boardID, blockID), toJSON(blockPatch))
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -184,8 +214,46 @@ func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool,
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) InsertBlocks(blocks []model.Block) ([]model.Block, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetBlocksRoute(), toJSON(blocks))
|
||||
func (c *Client) DuplicateBoard(boardID string, asTemplate bool, teamID string) (bool, *Response) {
|
||||
queryParams := "?asTemplate=false&"
|
||||
if asTemplate {
|
||||
queryParams = "?asTemplate=true"
|
||||
}
|
||||
r, err := c.DoAPIPost(c.GetBoardRoute(boardID)+"/duplicate"+queryParams, "")
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) DuplicateBlock(boardID, blockID string, asTemplate bool) (bool, *Response) {
|
||||
queryParams := "?asTemplate=false"
|
||||
if asTemplate {
|
||||
queryParams = "?asTemplate=true"
|
||||
}
|
||||
r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/duplicate"+queryParams, "")
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) UndeleteBlock(boardID, blockID string) (bool, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/undelete", "")
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) InsertBlocks(boardID string, blocks []model.Block) ([]model.Block, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetBlocksRoute(boardID), toJSON(blocks))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -194,8 +262,8 @@ func (c *Client) InsertBlocks(blocks []model.Block) ([]model.Block, *Response) {
|
||||
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteBlock(blockID string) (bool, *Response) {
|
||||
r, err := c.DoAPIDelete(c.GetBlockRoute(blockID))
|
||||
func (c *Client) DeleteBlock(boardID, blockID string) (bool, *Response) {
|
||||
r, err := c.DoAPIDelete(c.GetBlockRoute(boardID, blockID), "")
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -204,18 +272,8 @@ func (c *Client) DeleteBlock(blockID string) (bool, *Response) {
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) UndeleteBlock(blockID string) (bool, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetBlockRoute(blockID)+"/undelete", "")
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetSubtree(blockID string) ([]model.Block, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetSubtreeRoute(blockID), "")
|
||||
func (c *Client) GetSubtree(boardID, blockID string) ([]model.Block, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetSubtreeRoute(boardID, blockID), "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -224,14 +282,45 @@ func (c *Client) GetSubtree(blockID string) ([]model.Block, *Response) {
|
||||
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
// Boards and blocks.
|
||||
func (c *Client) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks) (*model.BoardsAndBlocks, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetBoardsAndBlocksRoute(), toJSON(bab))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks) (*model.BoardsAndBlocks, *Response) {
|
||||
r, err := c.DoAPIPatch(c.GetBoardsAndBlocksRoute(), toJSON(pbab))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks) (bool, *Response) {
|
||||
r, err := c.DoAPIDelete(c.GetBoardsAndBlocksRoute(), toJSON(dbab))
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
// Sharing
|
||||
|
||||
func (c *Client) GetSharingRoute(rootID string) string {
|
||||
return fmt.Sprintf("/workspaces/0/sharing/%s", rootID)
|
||||
func (c *Client) GetSharingRoute(boardID string) string {
|
||||
return fmt.Sprintf("%s/sharing", c.GetBoardRoute(boardID))
|
||||
}
|
||||
|
||||
func (c *Client) GetSharing(rootID string) (*model.Sharing, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetSharingRoute(rootID), "")
|
||||
func (c *Client) GetSharing(boardID string) (*model.Sharing, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetSharingRoute(boardID), "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -241,7 +330,7 @@ func (c *Client) GetSharing(rootID string) (*model.Sharing, *Response) {
|
||||
return &sharing, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) PostSharing(sharing model.Sharing) (bool, *Response) {
|
||||
func (c *Client) PostSharing(sharing *model.Sharing) (bool, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetSharingRoute(sharing.ID), toJSON(sharing))
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
@ -338,11 +427,116 @@ func (c *Client) UserChangePassword(id string, data *api.ChangePasswordRequest)
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetWorkspaceUploadFileRoute(workspaceID, rootID string) string {
|
||||
return fmt.Sprintf("/workspaces/%s/%s/files", workspaceID, rootID)
|
||||
func (c *Client) CreateBoard(board *model.Board) (*model.Board, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetBoardsRoute(), toJSON(board))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader) (*api.FileUploadResponse, *Response) {
|
||||
func (c *Client) PatchBoard(boardID string, patch *model.BoardPatch) (*model.Board, *Response) {
|
||||
r, err := c.DoAPIPatch(c.GetBoardRoute(boardID), toJSON(patch))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteBoard(boardID string) (bool, *Response) {
|
||||
r, err := c.DoAPIDelete(c.GetBoardRoute(boardID), "")
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetBoard(boardID, readToken string) (*model.Board, *Response) {
|
||||
url := c.GetBoardRoute(boardID)
|
||||
if readToken != "" {
|
||||
url += fmt.Sprintf("?read_token=%s", readToken)
|
||||
}
|
||||
|
||||
r, err := c.DoAPIGet(url, "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetBoardsForTeam(teamID string) ([]*model.Board, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards", "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardsFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) SearchBoardsForTeam(teamID, term string) ([]*model.Board, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards/search?q="+term, "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardsFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetMembersForBoard(boardID string) ([]*model.BoardMember, *Response) {
|
||||
r, err := c.DoAPIGet(c.GetBoardRoute(boardID)+"/members", "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardMembersFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetBoardRoute(member.BoardID)+"/members", toJSON(member))
|
||||
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 {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteBoardMember(member *model.BoardMember) (bool, *Response) {
|
||||
r, err := c.DoAPIDelete(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, "")
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetTeamUploadFileRoute(teamID, boardID string) string {
|
||||
return fmt.Sprintf("%s/%s/files", c.GetTeamRoute(teamID), boardID)
|
||||
}
|
||||
|
||||
func (c *Client) TeamUploadFile(teamID, boardID string, data io.Reader) (*api.FileUploadResponse, *Response) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile(api.UploadFormFileKey, "file")
|
||||
@ -358,7 +552,7 @@ func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader)
|
||||
r.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
}
|
||||
|
||||
r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetWorkspaceUploadFileRoute(workspaceID, rootID), body, "", opt)
|
||||
r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetTeamUploadFileRoute(teamID, boardID), body, "", opt)
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -372,12 +566,12 @@ func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader)
|
||||
return fileUploadResponse, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetSubscriptionsRoute(workspaceID string) string {
|
||||
return fmt.Sprintf("/workspaces/%s/subscriptions", workspaceID)
|
||||
func (c *Client) GetSubscriptionsRoute() string {
|
||||
return "/subscriptions"
|
||||
}
|
||||
|
||||
func (c *Client) CreateSubscription(workspaceID string, sub *model.Subscription) (*model.Subscription, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetSubscriptionsRoute(workspaceID), toJSON(&sub))
|
||||
func (c *Client) CreateSubscription(sub *model.Subscription) (*model.Subscription, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetSubscriptionsRoute(), toJSON(&sub))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -390,10 +584,10 @@ func (c *Client) CreateSubscription(workspaceID string, sub *model.Subscription)
|
||||
return subNew, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteSubscription(workspaceID string, blockID string, subscriberID string) *Response {
|
||||
url := fmt.Sprintf("%s/%s/%s", c.GetSubscriptionsRoute(workspaceID), blockID, subscriberID)
|
||||
func (c *Client) DeleteSubscription(blockID string, subscriberID string) *Response {
|
||||
url := fmt.Sprintf("%s/%s/%s", c.GetSubscriptionsRoute(), blockID, subscriberID)
|
||||
|
||||
r, err := c.DoAPIDelete(url)
|
||||
r, err := c.DoAPIDelete(url, "")
|
||||
if err != nil {
|
||||
return BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -402,8 +596,8 @@ func (c *Client) DeleteSubscription(workspaceID string, blockID string, subscrib
|
||||
return BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetSubscriptions(workspaceID string, subscriberID string) ([]*model.Subscription, *Response) {
|
||||
url := fmt.Sprintf("%s/%s", c.GetSubscriptionsRoute(workspaceID), subscriberID)
|
||||
func (c *Client) GetSubscriptions(subscriberID string) ([]*model.Subscription, *Response) {
|
||||
url := fmt.Sprintf("%s/%s", c.GetSubscriptionsRoute(), subscriberID)
|
||||
|
||||
r, err := c.DoAPIGet(url, "")
|
||||
if err != nil {
|
||||
|
@ -5,7 +5,6 @@ go 1.16
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.0
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/golang-migrate/migrate/v4 v4.14.1
|
||||
github.com/golang/mock v1.5.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
@ -14,6 +13,7 @@ require (
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.21
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0
|
||||
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/oklog/run v1.1.0
|
||||
|
158
server/go.sum
158
server/go.sum
@ -47,7 +47,6 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
|
||||
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
@ -68,7 +67,6 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0
|
||||
github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8=
|
||||
github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk=
|
||||
@ -161,7 +159,6 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE
|
||||
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
|
||||
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
@ -185,6 +182,7 @@ github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ1
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
|
||||
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -195,19 +193,14 @@ github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFau
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dhui/dktest v0.3.3 h1:DBuH/9GFaWbDRa42qsut/hbQu+srAQ0rPWnUoiGX7CA=
|
||||
github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ=
|
||||
github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU=
|
||||
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
@ -233,6 +226,7 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL
|
||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
||||
@ -290,9 +284,7 @@ github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhD
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang-migrate/migrate/v4 v4.14.1 h1:qmRd/rNGjM1r3Ve5gHd5ZplytrD02UcItYNxJ3iUHHE=
|
||||
github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
@ -346,6 +338,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
@ -366,8 +359,9 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
@ -403,7 +397,6 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
@ -416,7 +409,6 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP
|
||||
github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0=
|
||||
github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
|
||||
@ -513,6 +505,8 @@ github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYb
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@ -564,6 +558,7 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
@ -592,6 +587,8 @@ github.com/mattermost/mattermost-plugin-api v0.0.21/go.mod h1:qz19Y+5HLbjtzY2RZ6
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20210901153517-42e75fad4dae/go.mod h1:kmxJuVgpX13Th+e5L1ZsBs4aq+ETmmDg9joo5r4cIw8=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0 h1:A7TCgCGF9JmAHBQv9qGm5SfPYTAl8dOXy/u6lCSV8ow=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0/go.mod h1:TUSk5lYJmwfTKTJLXR0eAsjJNlKkWzS5aGZegXG0J08=
|
||||
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 h1:agJMxBP8LV0nyV90PZ/BHmmjNyvzTWqR20wLwiXHx14=
|
||||
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw=
|
||||
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
@ -616,6 +613,7 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
@ -658,7 +656,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
@ -705,9 +702,7 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||
github.com/oov/psd v0.0.0-20210618170533-9fb823ddb631/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
@ -782,6 +777,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@ -1063,6 +1059,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -1203,6 +1200,7 @@ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -1213,8 +1211,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1287,9 +1287,11 @@ golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc
|
||||
golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -1454,17 +1456,139 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg=
|
||||
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
|
||||
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
|
||||
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
|
||||
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
|
||||
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
|
||||
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
|
||||
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
|
||||
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
|
||||
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
|
||||
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
|
||||
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
|
||||
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
|
||||
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
|
||||
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
|
||||
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
|
||||
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
|
||||
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
|
||||
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
|
||||
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
|
||||
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
|
||||
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
|
||||
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
|
||||
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
|
||||
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
|
||||
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
|
||||
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
|
||||
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
|
||||
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
|
||||
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
|
||||
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
|
||||
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
|
||||
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
|
||||
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
|
||||
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
|
||||
modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
|
||||
modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU=
|
||||
modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM=
|
||||
modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko=
|
||||
modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA=
|
||||
modernc.org/ccgo/v3 v3.12.95 h1:Ym2JG2G3P4IyZqjTTojHTl7qO0RysXeGSYPSoKPSBxc=
|
||||
modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8=
|
||||
modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA=
|
||||
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8=
|
||||
modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw=
|
||||
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
|
||||
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM=
|
||||
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
|
||||
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
|
||||
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
|
||||
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
|
||||
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
|
||||
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
|
||||
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
|
||||
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
|
||||
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
|
||||
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
|
||||
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
|
||||
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
|
||||
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
|
||||
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
|
||||
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
|
||||
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
|
||||
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
|
||||
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
|
||||
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
|
||||
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
|
||||
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
|
||||
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
|
||||
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
|
||||
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
|
||||
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
|
||||
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
|
||||
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
|
||||
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
|
||||
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
|
||||
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
|
||||
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
|
||||
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
|
||||
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
|
||||
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
|
||||
modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ=
|
||||
modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
|
||||
modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
|
||||
modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
|
||||
modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
|
||||
modernc.org/libc v1.11.104 h1:gxoa5b3HPo7OzD4tKZjgnwXk/w//u1oovvjSMP3Q96Q=
|
||||
modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ=
|
||||
modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8=
|
||||
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
|
||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
|
||||
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
|
||||
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
|
||||
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
|
||||
modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
|
||||
modernc.org/sqlite v1.14.3 h1:psrTwgpEujgWEP3FNdsC9yNh5tSeA77U0GeWhHH4XmQ=
|
||||
modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y=
|
||||
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
|
||||
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/tcl v1.9.2 h1:YA87dFLOsR2KqMka371a2Xgr+YsyUwo7OmHVSv/kztw=
|
||||
modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.2.20 h1:DyboxM1sJR2NB803j2StnbnL6jcQXz273OhHDGu8dGk=
|
||||
modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo=
|
||||
modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
@ -11,40 +11,38 @@ import (
|
||||
)
|
||||
|
||||
func TestGetBlocks(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
th := SetupTestHelperWithToken(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
initialCount := len(blocks)
|
||||
board := th.CreateBoard("team-id", model.BoardTypeOpen)
|
||||
|
||||
initialID1 := utils.NewID(utils.IDTypeBlock)
|
||||
initialID2 := utils.NewID(utils.IDTypeBlock)
|
||||
newBlocks := []model.Block{
|
||||
{
|
||||
ID: initialID1,
|
||||
RootID: initialID1,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
Type: model.TypeCard,
|
||||
},
|
||||
{
|
||||
ID: initialID2,
|
||||
RootID: initialID2,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
Type: model.TypeCard,
|
||||
},
|
||||
}
|
||||
newBlocks, resp = th.Client.InsertBlocks(newBlocks)
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 2)
|
||||
blockID1 := newBlocks[0].ID
|
||||
blockID2 := newBlocks[1].ID
|
||||
|
||||
blocks, resp = th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount+2)
|
||||
require.Len(t, blocks, 2)
|
||||
|
||||
blockIDs := make([]string, len(blocks))
|
||||
for i, b := range blocks {
|
||||
@ -55,12 +53,10 @@ func TestGetBlocks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPostBlock(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
th := SetupTestHelperWithToken(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
initialCount := len(blocks)
|
||||
board := th.CreateBoard("team-id", model.BoardTypeOpen)
|
||||
|
||||
var blockID1 string
|
||||
var blockID2 string
|
||||
@ -70,21 +66,21 @@ func TestPostBlock(t *testing.T) {
|
||||
initialID1 := utils.NewID(utils.IDTypeBlock)
|
||||
block := model.Block{
|
||||
ID: initialID1,
|
||||
RootID: initialID1,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
Type: model.TypeCard,
|
||||
Title: "New title",
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
blockID1 = newBlocks[0].ID
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount+1)
|
||||
require.Len(t, blocks, 1)
|
||||
|
||||
blockIDs := make([]string, len(blocks))
|
||||
for i, b := range blocks {
|
||||
@ -99,21 +95,21 @@ func TestPostBlock(t *testing.T) {
|
||||
newBlocks := []model.Block{
|
||||
{
|
||||
ID: initialID2,
|
||||
RootID: initialID2,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
Type: model.TypeCard,
|
||||
},
|
||||
{
|
||||
ID: initialID3,
|
||||
RootID: initialID3,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
Type: model.TypeCard,
|
||||
},
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks(newBlocks)
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 2)
|
||||
blockID2 = newBlocks[0].ID
|
||||
@ -121,9 +117,9 @@ func TestPostBlock(t *testing.T) {
|
||||
require.NotEqual(t, initialID2, blockID2)
|
||||
require.NotEqual(t, initialID3, blockID3)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount+3)
|
||||
require.Len(t, blocks, 3)
|
||||
|
||||
blockIDs := make([]string, len(blocks))
|
||||
for i, b := range blocks {
|
||||
@ -137,22 +133,22 @@ func TestPostBlock(t *testing.T) {
|
||||
t.Run("Update a block should not be possible through the insert endpoint", func(t *testing.T) {
|
||||
block := model.Block{
|
||||
ID: blockID1,
|
||||
RootID: blockID1,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 20,
|
||||
Type: model.TypeBoard,
|
||||
Type: model.TypeCard,
|
||||
Title: "Updated title",
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
blockID4 := newBlocks[0].ID
|
||||
require.NotEqual(t, blockID1, blockID4)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount+4)
|
||||
require.Len(t, blocks, 4)
|
||||
|
||||
var block4 model.Block
|
||||
for _, b := range blocks {
|
||||
@ -166,42 +162,41 @@ func TestPostBlock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPatchBlock(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
th := SetupTestHelperWithToken(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
initialID := utils.NewID(utils.IDTypeBlock)
|
||||
|
||||
board := th.CreateBoard("team-id", model.BoardTypeOpen)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
block := model.Block{
|
||||
ID: initialID,
|
||||
RootID: initialID,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
Type: model.TypeCard,
|
||||
Title: "New title",
|
||||
Fields: map[string]interface{}{"test": "test value", "test2": "test value 2"},
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||
require.NoError(t, resp.Error)
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
th.CheckOK(resp)
|
||||
require.Len(t, newBlocks, 1)
|
||||
blockID := newBlocks[0].ID
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
initialCount := len(blocks)
|
||||
|
||||
t.Run("Patch a block basic field", func(t *testing.T) {
|
||||
newTitle := "Updated title"
|
||||
blockPatch := &model.BlockPatch{
|
||||
Title: &newTitle,
|
||||
}
|
||||
|
||||
_, resp := th.Client.PatchBlock(blockID, blockPatch)
|
||||
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount)
|
||||
require.Len(t, blocks, 1)
|
||||
|
||||
var updatedBlock model.Block
|
||||
for _, b := range blocks {
|
||||
@ -221,12 +216,12 @@ func TestPatchBlock(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, resp := th.Client.PatchBlock(blockID, blockPatch)
|
||||
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount)
|
||||
require.Len(t, blocks, 1)
|
||||
|
||||
var updatedBlock model.Block
|
||||
for _, b := range blocks {
|
||||
@ -244,12 +239,12 @@ func TestPatchBlock(t *testing.T) {
|
||||
DeletedFields: []string{"test", "test3", "test100"},
|
||||
}
|
||||
|
||||
_, resp := th.Client.PatchBlock(blockID, blockPatch)
|
||||
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount)
|
||||
require.Len(t, blocks, 1)
|
||||
|
||||
var updatedBlock model.Block
|
||||
for _, b := range blocks {
|
||||
@ -265,35 +260,34 @@ func TestPatchBlock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteBlock(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
th := SetupTestHelperWithToken(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
initialCount := len(blocks)
|
||||
board := th.CreateBoard("team-id", model.BoardTypeOpen)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
var blockID string
|
||||
t.Run("Create a block", func(t *testing.T) {
|
||||
initialID := utils.NewID(utils.IDTypeBlock)
|
||||
block := model.Block{
|
||||
ID: initialID,
|
||||
RootID: initialID,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
Type: model.TypeCard,
|
||||
Title: "New title",
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
require.NotZero(t, newBlocks[0].ID)
|
||||
require.NotEqual(t, initialID, newBlocks[0].ID)
|
||||
blockID = newBlocks[0].ID
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount+1)
|
||||
require.Len(t, blocks, 1)
|
||||
|
||||
blockIDs := make([]string, len(blocks))
|
||||
for i, b := range blocks {
|
||||
@ -307,43 +301,45 @@ func TestDeleteBlock(t *testing.T) {
|
||||
// id,insert_at on block history
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
_, resp := th.Client.DeleteBlock(blockID)
|
||||
_, resp := th.Client.DeleteBlock(board.ID, blockID)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount)
|
||||
require.Empty(t, blocks)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUndeleteBlock(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
board := th.CreateBoard("team-id", model.BoardTypeOpen)
|
||||
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
initialCount := len(blocks)
|
||||
|
||||
var blockID string
|
||||
t.Run("Create a block", func(t *testing.T) {
|
||||
initialID := utils.NewID(utils.IDTypeBlock)
|
||||
initialID := utils.NewID(utils.IDTypeBoard)
|
||||
block := model.Block{
|
||||
ID: initialID,
|
||||
RootID: initialID,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
Title: "New title",
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
require.NotZero(t, newBlocks[0].ID)
|
||||
require.NotEqual(t, initialID, newBlocks[0].ID)
|
||||
blockID = newBlocks[0].ID
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount+1)
|
||||
|
||||
@ -359,10 +355,10 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
// id,insert_at on block history
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
_, resp := th.Client.DeleteBlock(blockID)
|
||||
_, resp := th.Client.DeleteBlock(board.ID, blockID)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount)
|
||||
})
|
||||
@ -372,10 +368,10 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
// id,insert_at on block history
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
_, resp := th.Client.UndeleteBlock(blockID)
|
||||
_, resp := th.Client.UndeleteBlock(board.ID, blockID)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount+1)
|
||||
})
|
||||
@ -384,12 +380,10 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
func TestGetSubtree(t *testing.T) {
|
||||
t.Skip("TODO: fix flaky test")
|
||||
|
||||
th := SetupTestHelper().InitBasic()
|
||||
th := SetupTestHelperWithToken(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
initialCount := len(blocks)
|
||||
board := th.CreateBoard("team-id", model.BoardTypeOpen)
|
||||
|
||||
parentBlockID := utils.NewID(utils.IDTypeBlock)
|
||||
childBlockID1 := utils.NewID(utils.IDTypeBlock)
|
||||
@ -399,14 +393,14 @@ func TestGetSubtree(t *testing.T) {
|
||||
newBlocks := []model.Block{
|
||||
{
|
||||
ID: parentBlockID,
|
||||
RootID: parentBlockID,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
Type: model.TypeCard,
|
||||
},
|
||||
{
|
||||
ID: childBlockID1,
|
||||
RootID: parentBlockID,
|
||||
BoardID: board.ID,
|
||||
ParentID: parentBlockID,
|
||||
CreateAt: 2,
|
||||
UpdateAt: 2,
|
||||
@ -414,7 +408,7 @@ func TestGetSubtree(t *testing.T) {
|
||||
},
|
||||
{
|
||||
ID: childBlockID2,
|
||||
RootID: parentBlockID,
|
||||
BoardID: board.ID,
|
||||
ParentID: parentBlockID,
|
||||
CreateAt: 2,
|
||||
UpdateAt: 2,
|
||||
@ -422,12 +416,12 @@ func TestGetSubtree(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, resp := th.Client.InsertBlocks(newBlocks)
|
||||
_, resp := th.Client.InsertBlocks(board.ID, newBlocks)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, initialCount+1) // GetBlocks returns root blocks (null ParentID)
|
||||
require.Len(t, blocks, 1) // GetBlocks returns root blocks (null ParentID)
|
||||
|
||||
blockIDs := make([]string, len(blocks))
|
||||
for i, b := range blocks {
|
||||
@ -437,7 +431,7 @@ func TestGetSubtree(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Get subtree for parent ID", func(t *testing.T) {
|
||||
blocks, resp := th.Client.GetSubtree(parentBlockID)
|
||||
blocks, resp := th.Client.GetSubtree(board.ID, parentBlockID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, 3)
|
||||
|
||||
|
1270
server/integrationtests/board_test.go
Normal file
1270
server/integrationtests/board_test.go
Normal file
File diff suppressed because it is too large
Load Diff
816
server/integrationtests/boards_and_blocks_test.go
Normal file
816
server/integrationtests/boards_and_blocks_test.go
Normal file
@ -0,0 +1,816 @@
|
||||
package integrationtests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateBoardsAndBlocks(t *testing.T) {
|
||||
teamID := testTeamID
|
||||
|
||||
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
newBab := &model.BoardsAndBlocks{
|
||||
Boards: []*model.Board{},
|
||||
Blocks: []model.Block{},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
|
||||
th.CheckUnauthorized(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("invalid boards and blocks", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("no boards", func(t *testing.T) {
|
||||
newBab := &model.BoardsAndBlocks{
|
||||
Boards: []*model.Board{},
|
||||
Blocks: []model.Block{
|
||||
{ID: "block-id", BoardID: "board-id", Type: model.TypeCard},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("no blocks", func(t *testing.T) {
|
||||
newBab := &model.BoardsAndBlocks{
|
||||
Boards: []*model.Board{
|
||||
{ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate},
|
||||
},
|
||||
Blocks: []model.Block{},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("blocks from nonexistent boards", func(t *testing.T) {
|
||||
newBab := &model.BoardsAndBlocks{
|
||||
Boards: []*model.Board{
|
||||
{ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate},
|
||||
},
|
||||
Blocks: []model.Block{
|
||||
{ID: "block-id", BoardID: "nonexistent-board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("boards with no IDs", func(t *testing.T) {
|
||||
newBab := &model.BoardsAndBlocks{
|
||||
Boards: []*model.Board{
|
||||
{ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate},
|
||||
{TeamID: teamID, Type: model.BoardTypePrivate},
|
||||
},
|
||||
Blocks: []model.Block{
|
||||
{ID: "block-id", BoardID: "board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("boards from different teams", func(t *testing.T) {
|
||||
newBab := &model.BoardsAndBlocks{
|
||||
Boards: []*model.Board{
|
||||
{ID: "board-id-1", TeamID: "team-id-1", Type: model.BoardTypePrivate},
|
||||
{ID: "board-id-2", TeamID: "team-id-2", Type: model.BoardTypePrivate},
|
||||
},
|
||||
Blocks: []model.Block{
|
||||
{ID: "block-id", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("creating boards and blocks", func(t *testing.T) {
|
||||
newBab := &model.BoardsAndBlocks{
|
||||
Boards: []*model.Board{
|
||||
{ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen},
|
||||
{ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate},
|
||||
},
|
||||
Blocks: []model.Block{
|
||||
{ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
|
||||
{ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, bab)
|
||||
|
||||
require.Len(t, bab.Boards, 2)
|
||||
require.Len(t, bab.Blocks, 2)
|
||||
|
||||
// board 1 should have been created with a new ID, and its
|
||||
// block should be there too
|
||||
boardsTermPublic, resp := th.Client.SearchBoardsForTeam(teamID, "public")
|
||||
th.CheckOK(resp)
|
||||
require.Len(t, boardsTermPublic, 1)
|
||||
board1 := boardsTermPublic[0]
|
||||
require.Equal(t, "public board", board1.Title)
|
||||
require.Equal(t, model.BoardTypeOpen, board1.Type)
|
||||
require.NotEqual(t, "board-id-1", board1.ID)
|
||||
blocks1, err := th.Server.App().GetBlocksForBoard(board1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks1, 1)
|
||||
require.Equal(t, "block 1", blocks1[0].Title)
|
||||
|
||||
// board 1 should have been created with a new ID, and its
|
||||
// block should be there too
|
||||
boardsTermPrivate, resp := th.Client.SearchBoardsForTeam(teamID, "private")
|
||||
th.CheckOK(resp)
|
||||
require.Len(t, boardsTermPrivate, 1)
|
||||
board2 := boardsTermPrivate[0]
|
||||
require.Equal(t, "private board", board2.Title)
|
||||
require.Equal(t, model.BoardTypePrivate, board2.Type)
|
||||
require.NotEqual(t, "board-id-2", board2.ID)
|
||||
blocks2, err := th.Server.App().GetBlocksForBoard(board2.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks2, 1)
|
||||
require.Equal(t, "block 2", blocks2[0].Title)
|
||||
|
||||
// user should be an admin of both newly created boards
|
||||
user1 := th.GetUser1()
|
||||
members1, err := th.Server.App().GetMembersForBoard(board1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members1, 1)
|
||||
require.Equal(t, user1.ID, members1[0].UserID)
|
||||
members2, err := th.Server.App().GetMembersForBoard(board2.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members2, 1)
|
||||
require.Equal(t, user1.ID, members2[0].UserID)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchBoardsAndBlocks(t *testing.T) {
|
||||
teamID := "team-id"
|
||||
|
||||
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
pbab := &model.PatchBoardsAndBlocks{}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckUnauthorized(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("invalid patch boards and blocks", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userID := th.GetUser1().ID
|
||||
initialTitle := "initial title 1"
|
||||
newTitle := "new title 1"
|
||||
|
||||
newBoard1 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board1)
|
||||
|
||||
newBoard2 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board2, err := th.Server.App().CreateBoard(newBoard2, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board2)
|
||||
|
||||
newBlock1 := model.Block{
|
||||
ID: "block-id-1",
|
||||
BoardID: board1.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
|
||||
block1, err := th.Server.App().GetBlockByID("block-id-1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block1)
|
||||
|
||||
newBlock2 := model.Block{
|
||||
ID: "block-id-2",
|
||||
BoardID: board2.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
|
||||
block2, err := th.Server.App().GetBlockByID("block-id-2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block2)
|
||||
|
||||
t.Run("no board IDs", func(t *testing.T) {
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{block1.ID, block2.ID},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("missmatch board IDs and patches", func(t *testing.T) {
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{board1.ID, board2.ID},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{block1.ID, block2.ID},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("no block IDs", func(t *testing.T) {
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{board1.ID, board2.ID},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("missmatch block IDs and patches", func(t *testing.T) {
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{board1.ID, board2.ID},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{block1.ID, block2.ID},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("block that doesn't belong to any board", func(t *testing.T) {
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{board1.ID},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{block1.ID, "board-id-2"},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("if the user doesn't have permissions for one of the boards, nothing should be updated", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userID := th.GetUser1().ID
|
||||
initialTitle := "initial title 2"
|
||||
newTitle := "new title 2"
|
||||
|
||||
newBoard1 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board1)
|
||||
|
||||
newBoard2 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board2, err := th.Server.App().CreateBoard(newBoard2, userID, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board2)
|
||||
|
||||
newBlock1 := model.Block{
|
||||
ID: "block-id-1",
|
||||
BoardID: board1.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
|
||||
block1, err := th.Server.App().GetBlockByID("block-id-1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block1)
|
||||
|
||||
newBlock2 := model.Block{
|
||||
ID: "block-id-2",
|
||||
BoardID: board2.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
|
||||
block2, err := th.Server.App().GetBlockByID("block-id-2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block2)
|
||||
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{board1.ID, board2.ID},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{block1.ID, block2.ID},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckForbidden(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("boards belonging to different teams should be rejected", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userID := th.GetUser1().ID
|
||||
initialTitle := "initial title 3"
|
||||
newTitle := "new title 3"
|
||||
|
||||
newBoard1 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board1)
|
||||
|
||||
newBoard2 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: "different-team-id",
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board2, err := th.Server.App().CreateBoard(newBoard2, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board2)
|
||||
|
||||
newBlock1 := model.Block{
|
||||
ID: "block-id-1",
|
||||
BoardID: board1.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
|
||||
block1, err := th.Server.App().GetBlockByID("block-id-1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block1)
|
||||
|
||||
newBlock2 := model.Block{
|
||||
ID: "block-id-2",
|
||||
BoardID: board2.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
|
||||
block2, err := th.Server.App().GetBlockByID("block-id-2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block2)
|
||||
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{board1.ID, board2.ID},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{block1.ID, "board-id-2"},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("patches should be rejected if one is invalid", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userID := th.GetUser1().ID
|
||||
initialTitle := "initial title 4"
|
||||
newTitle := "new title 4"
|
||||
|
||||
newBoard1 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board1)
|
||||
|
||||
newBoard2 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board2, err := th.Server.App().CreateBoard(newBoard2, userID, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board2)
|
||||
|
||||
newBlock1 := model.Block{
|
||||
ID: "block-id-1",
|
||||
BoardID: board1.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
|
||||
block1, err := th.Server.App().GetBlockByID("block-id-1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block1)
|
||||
|
||||
newBlock2 := model.Block{
|
||||
ID: "block-id-2",
|
||||
BoardID: board2.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
|
||||
block2, err := th.Server.App().GetBlockByID("block-id-2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block2)
|
||||
|
||||
var invalidPatchType model.BoardType = "invalid"
|
||||
invalidPatch := &model.BoardPatch{Type: &invalidPatchType}
|
||||
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{board1.ID, board2.ID},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
invalidPatch,
|
||||
},
|
||||
BlockIDs: []string{block1.ID, "board-id-2"},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("patches should be rejected if there is a block that doesn't belong to the boards being patched", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userID := th.GetUser1().ID
|
||||
initialTitle := "initial title"
|
||||
newTitle := "new title"
|
||||
|
||||
newBoard1 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board1)
|
||||
|
||||
newBoard2 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board2, err := th.Server.App().CreateBoard(newBoard2, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board2)
|
||||
|
||||
newBlock1 := model.Block{
|
||||
ID: "block-id-1",
|
||||
BoardID: board1.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
|
||||
block1, err := th.Server.App().GetBlockByID("block-id-1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block1)
|
||||
|
||||
newBlock2 := model.Block{
|
||||
ID: "block-id-2",
|
||||
BoardID: board2.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
|
||||
block2, err := th.Server.App().GetBlockByID("block-id-2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block2)
|
||||
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{board1.ID},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{block1.ID, block2.ID},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, bab)
|
||||
})
|
||||
|
||||
t.Run("patches should be applied if they're valid and they're related", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userID := th.GetUser1().ID
|
||||
initialTitle := "initial title"
|
||||
newTitle := "new title"
|
||||
|
||||
newBoard1 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board1)
|
||||
|
||||
newBoard2 := &model.Board{
|
||||
Title: initialTitle,
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board2, err := th.Server.App().CreateBoard(newBoard2, userID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board2)
|
||||
|
||||
newBlock1 := model.Block{
|
||||
ID: "block-id-1",
|
||||
BoardID: board1.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
|
||||
block1, err := th.Server.App().GetBlockByID("block-id-1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block1)
|
||||
|
||||
newBlock2 := model.Block{
|
||||
ID: "block-id-2",
|
||||
BoardID: board2.ID,
|
||||
Title: initialTitle,
|
||||
}
|
||||
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
|
||||
block2, err := th.Server.App().GetBlockByID("block-id-2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block2)
|
||||
|
||||
pbab := &model.PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{board1.ID, board2.ID},
|
||||
BoardPatches: []*model.BoardPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{block1.ID, block2.ID},
|
||||
BlockPatches: []*model.BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, bab)
|
||||
require.Len(t, bab.Boards, 2)
|
||||
require.Len(t, bab.Blocks, 2)
|
||||
|
||||
// ensure that the entities have been updated
|
||||
rBoard1, err := th.Server.App().GetBoard(board1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newTitle, rBoard1.Title)
|
||||
rBlock1, err := th.Server.App().GetBlockByID(block1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newTitle, rBlock1.Title)
|
||||
|
||||
rBoard2, err := th.Server.App().GetBoard(board2.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newTitle, rBoard2.Title)
|
||||
rBlock2, err := th.Server.App().GetBlockByID(block2.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newTitle, rBlock2.Title)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteBoardsAndBlocks(t *testing.T) {
|
||||
teamID := "team-id"
|
||||
|
||||
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
dbab := &model.DeleteBoardsAndBlocks{}
|
||||
|
||||
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
|
||||
th.CheckUnauthorized(resp)
|
||||
require.False(t, success)
|
||||
})
|
||||
|
||||
t.Run("invalid delete boards and blocks", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// a board is required for the permission checks
|
||||
newBoard := &model.Board{
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board)
|
||||
|
||||
t.Run("no boards", func(t *testing.T) {
|
||||
dbab := &model.DeleteBoardsAndBlocks{
|
||||
Blocks: []string{"block-id-1"},
|
||||
}
|
||||
|
||||
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.False(t, success)
|
||||
})
|
||||
|
||||
t.Run("no blocks", func(t *testing.T) {
|
||||
dbab := &model.DeleteBoardsAndBlocks{
|
||||
Boards: []string{board.ID},
|
||||
}
|
||||
|
||||
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.False(t, success)
|
||||
})
|
||||
|
||||
t.Run("boards from different teams", func(t *testing.T) {
|
||||
newOtherTeamsBoard := &model.Board{
|
||||
TeamID: "another-team-id",
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
otherTeamsBoard, err := th.Server.App().CreateBoard(newOtherTeamsBoard, th.GetUser1().ID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board)
|
||||
|
||||
dbab := &model.DeleteBoardsAndBlocks{
|
||||
Boards: []string{board.ID, otherTeamsBoard.ID},
|
||||
Blocks: []string{"block-id-1"},
|
||||
}
|
||||
|
||||
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
|
||||
th.CheckBadRequest(resp)
|
||||
require.False(t, success)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("if the user has no permissions to one of the boards, nothing should be deleted", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// the user is an admin of the first board
|
||||
newBoard1 := &model.Board{
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board1, err := th.Server.App().CreateBoard(newBoard1, th.GetUser1().ID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board1)
|
||||
|
||||
// but not of the second
|
||||
newBoard2 := &model.Board{
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
board2, err := th.Server.App().CreateBoard(newBoard2, th.GetUser1().ID, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board2)
|
||||
|
||||
dbab := &model.DeleteBoardsAndBlocks{
|
||||
Boards: []string{board1.ID, board2.ID},
|
||||
Blocks: []string{"block-id-1"},
|
||||
}
|
||||
|
||||
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
|
||||
th.CheckForbidden(resp)
|
||||
require.False(t, success)
|
||||
})
|
||||
|
||||
t.Run("all boards and blocks should be deleted if the request is correct", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
newBab := &model.BoardsAndBlocks{
|
||||
Boards: []*model.Board{
|
||||
{ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen},
|
||||
{ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate},
|
||||
},
|
||||
Blocks: []model.Block{
|
||||
{ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
|
||||
{ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
|
||||
},
|
||||
}
|
||||
|
||||
bab, err := th.Server.App().CreateBoardsAndBlocks(newBab, th.GetUser1().ID, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, bab.Boards, 2)
|
||||
require.Len(t, bab.Blocks, 2)
|
||||
|
||||
// ensure that the entities have been successfully created
|
||||
board1, err := th.Server.App().GetBoard("board-id-1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board1)
|
||||
block1, err := th.Server.App().GetBlockByID("block-id-1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block1)
|
||||
|
||||
board2, err := th.Server.App().GetBoard("board-id-2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board2)
|
||||
block2, err := th.Server.App().GetBlockByID("block-id-2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block2)
|
||||
|
||||
// call the API to delete boards and blocks
|
||||
dbab := &model.DeleteBoardsAndBlocks{
|
||||
Boards: []string{"board-id-1", "board-id-2"},
|
||||
Blocks: []string{"block-id-1", "block-id-2"},
|
||||
}
|
||||
|
||||
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
|
||||
th.CheckOK(resp)
|
||||
require.True(t, success)
|
||||
|
||||
// ensure that the entities have been successfully deleted
|
||||
board1, err = th.Server.App().GetBoard("board-id-1")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, board1)
|
||||
block1, err = th.Server.App().GetBlockByID("block-id-1")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, block1)
|
||||
|
||||
board2, err = th.Server.App().GetBoard("board-id-2")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, board2)
|
||||
block2, err = th.Server.App().GetBlockByID("block-id-2")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, block2)
|
||||
})
|
||||
}
|
@ -4,19 +4,30 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/focalboard/server/api"
|
||||
"github.com/mattermost/focalboard/server/client"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
|
||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
user1Username = "user1"
|
||||
user2Username = "user2"
|
||||
password = "Pa$$word"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
T *testing.T
|
||||
Server *server.Server
|
||||
Client *client.Client
|
||||
Client2 *client.Client
|
||||
@ -80,11 +91,14 @@ func newTestServer(singleUserToken string) *server.Server {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
permissionsService := localpermissions.New(db, logger)
|
||||
|
||||
params := server.Params{
|
||||
Cfg: cfg,
|
||||
SingleUserToken: singleUserToken,
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
Cfg: cfg,
|
||||
SingleUserToken: singleUserToken,
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
PermissionsService: permissionsService,
|
||||
}
|
||||
|
||||
srv, err := server.New(params)
|
||||
@ -95,23 +109,26 @@ func newTestServer(singleUserToken string) *server.Server {
|
||||
return srv
|
||||
}
|
||||
|
||||
func SetupTestHelper() *TestHelper {
|
||||
func SetupTestHelperWithToken(t *testing.T) *TestHelper {
|
||||
sessionToken := "TESTTOKEN"
|
||||
th := &TestHelper{}
|
||||
th := &TestHelper{T: t}
|
||||
th.Server = newTestServer(sessionToken)
|
||||
th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
|
||||
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
|
||||
return th
|
||||
}
|
||||
|
||||
func SetupTestHelperWithoutToken() *TestHelper {
|
||||
th := &TestHelper{}
|
||||
func SetupTestHelper(t *testing.T) *TestHelper {
|
||||
th := &TestHelper{T: t}
|
||||
th.Server = newTestServer("")
|
||||
th.Client = client.NewClient(th.Server.Config().ServerRoot, "")
|
||||
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, "")
|
||||
return th
|
||||
}
|
||||
|
||||
func (th *TestHelper) InitBasic() *TestHelper {
|
||||
// Start starts the test server and ensures that it's correctly
|
||||
// responding to requests before returning.
|
||||
func (th *TestHelper) Start() *TestHelper {
|
||||
go func() {
|
||||
if err := th.Server.Start(); err != nil {
|
||||
panic(err)
|
||||
@ -144,51 +161,28 @@ func (th *TestHelper) InitBasic() *TestHelper {
|
||||
return th
|
||||
}
|
||||
|
||||
var ErrRegisterFail = errors.New("register failed")
|
||||
// InitBasic starts the test server and initializes the clients of the
|
||||
// helper, registering them and logging them into the system.
|
||||
func (th *TestHelper) InitBasic() *TestHelper {
|
||||
th.Start()
|
||||
|
||||
func (th *TestHelper) InitUsers(username1 string, username2 string) error {
|
||||
workspace, err := th.Server.App().GetRootWorkspace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// user1
|
||||
th.RegisterAndLogin(th.Client, user1Username, "user1@sample.com", password, "")
|
||||
|
||||
clients := []*client.Client{th.Client, th.Client2}
|
||||
usernames := []string{username1, username2}
|
||||
// get token
|
||||
team, resp := th.Client.GetTeam("0")
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(th.T, team)
|
||||
require.NotNil(th.T, team.SignupToken)
|
||||
|
||||
for i, client := range clients {
|
||||
// register a new user
|
||||
password := utils.NewID(utils.IDTypeNone)
|
||||
registerRequest := &api.RegisterRequest{
|
||||
Username: usernames[i],
|
||||
Email: usernames[i] + "@example.com",
|
||||
Password: password,
|
||||
Token: workspace.SignupToken,
|
||||
}
|
||||
success, resp := client.Register(registerRequest)
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
if !success {
|
||||
return ErrRegisterFail
|
||||
}
|
||||
// user2
|
||||
th.RegisterAndLogin(th.Client2, user2Username, "user2@sample.com", password, team.SignupToken)
|
||||
|
||||
// login
|
||||
loginRequest := &api.LoginRequest{
|
||||
Type: "normal",
|
||||
Username: registerRequest.Username,
|
||||
Email: registerRequest.Email,
|
||||
Password: registerRequest.Password,
|
||||
}
|
||||
data, resp := client.Login(loginRequest)
|
||||
if resp.Error != nil {
|
||||
return resp.Error
|
||||
}
|
||||
|
||||
client.Token = data.Token
|
||||
}
|
||||
return nil
|
||||
return th
|
||||
}
|
||||
|
||||
var ErrRegisterFail = errors.New("register failed")
|
||||
|
||||
func (th *TestHelper) TearDown() {
|
||||
defer func() { _ = th.Server.Logger().Shutdown() }()
|
||||
|
||||
@ -198,4 +192,96 @@ func (th *TestHelper) TearDown() {
|
||||
}
|
||||
|
||||
os.RemoveAll(th.Server.Config().FilesPath)
|
||||
|
||||
if err := os.Remove(th.Server.Config().DBConfigString); err == nil {
|
||||
th.Server.Logger().Debug("Removed test database", mlog.String("file", th.Server.Config().DBConfigString))
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) RegisterAndLogin(client *client.Client, username, email, password, token string) {
|
||||
req := &api.RegisterRequest{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
success, resp := th.Client.Register(req)
|
||||
th.CheckOK(resp)
|
||||
require.True(th.T, success)
|
||||
|
||||
th.Login(client, username, password)
|
||||
}
|
||||
|
||||
func (th *TestHelper) Login(client *client.Client, username, password string) {
|
||||
req := &api.LoginRequest{
|
||||
Type: "normal",
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
data, resp := client.Login(req)
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(th.T, data)
|
||||
}
|
||||
|
||||
func (th *TestHelper) Login1() {
|
||||
th.Login(th.Client, user1Username, password)
|
||||
}
|
||||
|
||||
func (th *TestHelper) Login2() {
|
||||
th.Login(th.Client2, user2Username, password)
|
||||
}
|
||||
|
||||
func (th *TestHelper) Logout(client *client.Client) {
|
||||
client.Token = ""
|
||||
}
|
||||
|
||||
func (th *TestHelper) Me(client *client.Client) *model.User {
|
||||
user, resp := client.GetMe()
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(th.T, user)
|
||||
return user
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateBoard(teamID string, boardType model.BoardType) *model.Board {
|
||||
newBoard := &model.Board{
|
||||
TeamID: teamID,
|
||||
Type: boardType,
|
||||
}
|
||||
board, resp := th.Client.CreateBoard(newBoard)
|
||||
th.CheckOK(resp)
|
||||
return board
|
||||
}
|
||||
|
||||
func (th *TestHelper) GetUser1() *model.User {
|
||||
return th.Me(th.Client)
|
||||
}
|
||||
|
||||
func (th *TestHelper) GetUser2() *model.User {
|
||||
return th.Me(th.Client2)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CheckOK(r *client.Response) {
|
||||
require.Equal(th.T, http.StatusOK, r.StatusCode)
|
||||
require.NoError(th.T, r.Error)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CheckBadRequest(r *client.Response) {
|
||||
require.Equal(th.T, http.StatusBadRequest, r.StatusCode)
|
||||
require.Error(th.T, r.Error)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CheckNotFound(r *client.Response) {
|
||||
require.Equal(th.T, http.StatusNotFound, r.StatusCode)
|
||||
require.Error(th.T, r.Error)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CheckUnauthorized(r *client.Response) {
|
||||
require.Equal(th.T, http.StatusUnauthorized, r.StatusCode)
|
||||
require.Error(th.T, r.Error)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CheckForbidden(r *client.Response) {
|
||||
require.Equal(th.T, http.StatusForbidden, r.StatusCode)
|
||||
require.Error(th.T, r.Error)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package integrationtests
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
@ -9,60 +10,84 @@ import (
|
||||
)
|
||||
|
||||
func TestSharing(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
rootID := utils.NewID(utils.IDTypeBlock)
|
||||
var boardID string
|
||||
token := utils.NewID(utils.IDTypeToken)
|
||||
|
||||
t.Run("an unauthenticated client should not be able to get a sharing", func(t *testing.T) {
|
||||
th.Logout(th.Client)
|
||||
|
||||
sharing, resp := th.Client.GetSharing("board-id")
|
||||
th.CheckUnauthorized(resp)
|
||||
require.Nil(t, sharing)
|
||||
})
|
||||
|
||||
t.Run("Check no initial sharing", func(t *testing.T) {
|
||||
sharing, resp := th.Client.GetSharing(rootID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Empty(t, sharing.ID)
|
||||
require.False(t, sharing.Enabled)
|
||||
th.Login1()
|
||||
|
||||
teamID := "0"
|
||||
newBoard := &model.Board{
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
|
||||
board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, board)
|
||||
boardID = board.ID
|
||||
|
||||
s, err := th.Server.App().GetSharing(boardID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, s)
|
||||
|
||||
sharing, resp := th.Client.GetSharing(boardID)
|
||||
th.CheckNotFound(resp)
|
||||
require.Nil(t, sharing)
|
||||
})
|
||||
|
||||
t.Run("POST sharing, config = false", func(t *testing.T) {
|
||||
sharing := model.Sharing{
|
||||
ID: rootID,
|
||||
ID: boardID,
|
||||
Token: token,
|
||||
Enabled: true,
|
||||
UpdateAt: 1,
|
||||
}
|
||||
|
||||
// it will fail with default config
|
||||
success, resp := th.Client.PostSharing(sharing)
|
||||
success, resp := th.Client.PostSharing(&sharing)
|
||||
require.False(t, success)
|
||||
require.Error(t, resp.Error)
|
||||
|
||||
t.Run("GET sharing", func(t *testing.T) {
|
||||
sharing, resp := th.Client.GetSharing(rootID)
|
||||
// Expect no error, but no Id returned
|
||||
require.NoError(t, resp.Error)
|
||||
require.NotNil(t, sharing)
|
||||
require.Equal(t, "", sharing.ID)
|
||||
sharing, resp := th.Client.GetSharing(boardID)
|
||||
// Expect not found error
|
||||
require.Error(t, resp.Error)
|
||||
require.Equal(t, resp.StatusCode, http.StatusNotFound)
|
||||
require.Nil(t, sharing)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("POST sharing, config = true", func(t *testing.T) {
|
||||
th.Server.Config().EnablePublicSharedBoards = true
|
||||
sharing := model.Sharing{
|
||||
ID: rootID,
|
||||
ID: boardID,
|
||||
Token: token,
|
||||
Enabled: true,
|
||||
UpdateAt: 1,
|
||||
}
|
||||
|
||||
// it will succeed with updated config
|
||||
success, resp := th.Client.PostSharing(sharing)
|
||||
success, resp := th.Client.PostSharing(&sharing)
|
||||
require.True(t, success)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
t.Run("GET sharing", func(t *testing.T) {
|
||||
sharing, resp := th.Client.GetSharing(rootID)
|
||||
sharing, resp := th.Client.GetSharing(boardID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.NotNil(t, sharing)
|
||||
require.Equal(t, sharing.ID, rootID)
|
||||
require.Equal(t, sharing.ID, boardID)
|
||||
require.True(t, sharing.Enabled)
|
||||
require.Equal(t, sharing.Token, token)
|
||||
})
|
||||
|
@ -6,13 +6,12 @@ import (
|
||||
|
||||
"github.com/mattermost/focalboard/server/client"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createTestSubscriptions(client *client.Client, num int, workspaceID string) ([]*model.Subscription, string, error) {
|
||||
func createTestSubscriptions(client *client.Client, num int) ([]*model.Subscription, string, error) {
|
||||
newSubs := make([]*model.Subscription, 0, num)
|
||||
|
||||
user, resp := client.GetMe()
|
||||
@ -20,29 +19,27 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string)
|
||||
return nil, "", fmt.Errorf("cannot get current user: %w", resp.Error)
|
||||
}
|
||||
|
||||
board := model.Block{
|
||||
ID: utils.NewID(utils.IDTypeBoard),
|
||||
RootID: workspaceID,
|
||||
board := &model.Board{
|
||||
TeamID: "0",
|
||||
Type: model.BoardTypeOpen,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeBoard,
|
||||
}
|
||||
boards, resp := client.InsertBlocks([]model.Block{board})
|
||||
board, resp = client.CreateBoard(board)
|
||||
if resp.Error != nil {
|
||||
return nil, "", fmt.Errorf("cannot insert test board block: %w", resp.Error)
|
||||
}
|
||||
board = boards[0]
|
||||
|
||||
for n := 0; n < num; n++ {
|
||||
newBlock := model.Block{
|
||||
ID: utils.NewID(utils.IDTypeCard),
|
||||
RootID: board.ID,
|
||||
BoardID: board.ID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: model.TypeCard,
|
||||
}
|
||||
|
||||
newBlocks, resp := client.InsertBlocks([]model.Block{newBlock})
|
||||
newBlocks, resp := client.InsertBlocks(board.ID, []model.Block{newBlock})
|
||||
if resp.Error != nil {
|
||||
return nil, "", fmt.Errorf("cannot insert test card block: %w", resp.Error)
|
||||
}
|
||||
@ -51,12 +48,11 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string)
|
||||
sub := &model.Subscription{
|
||||
BlockType: newBlock.Type,
|
||||
BlockID: newBlock.ID,
|
||||
WorkspaceID: workspaceID,
|
||||
SubscriberType: model.SubTypeUser,
|
||||
SubscriberID: user.ID,
|
||||
}
|
||||
|
||||
subNew, resp := client.CreateSubscription(workspaceID, sub)
|
||||
subNew, resp := client.CreateSubscription(sub)
|
||||
if resp.Error != nil {
|
||||
return nil, "", resp.Error
|
||||
}
|
||||
@ -66,20 +62,16 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string)
|
||||
}
|
||||
|
||||
func TestCreateSubscription(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
container := store.Container{
|
||||
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
|
||||
}
|
||||
|
||||
t.Run("Create valid subscription", func(t *testing.T) {
|
||||
subs, userID, err := createTestSubscriptions(th.Client, 5, container.WorkspaceID)
|
||||
subs, userID, err := createTestSubscriptions(th.Client, 5)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, subs, 5)
|
||||
|
||||
// fetch the newly created subscriptions and compare
|
||||
subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, userID)
|
||||
subsFound, resp := th.Client.GetSubscriptions(userID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, subsFound, 5)
|
||||
assert.ElementsMatch(t, subs, subsFound)
|
||||
@ -90,47 +82,38 @@ func TestCreateSubscription(t *testing.T) {
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
sub := &model.Subscription{
|
||||
WorkspaceID: container.WorkspaceID,
|
||||
SubscriberID: user.ID,
|
||||
}
|
||||
_, resp = th.Client.CreateSubscription(container.WorkspaceID, sub)
|
||||
_, resp = th.Client.CreateSubscription(sub)
|
||||
require.Error(t, resp.Error)
|
||||
})
|
||||
|
||||
t.Run("Create subscription for another user", func(t *testing.T) {
|
||||
sub := &model.Subscription{
|
||||
WorkspaceID: container.WorkspaceID,
|
||||
SubscriberID: utils.NewID(utils.IDTypeUser),
|
||||
}
|
||||
_, resp := th.Client.CreateSubscription(container.WorkspaceID, sub)
|
||||
_, resp := th.Client.CreateSubscription(sub)
|
||||
require.Error(t, resp.Error)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSubscriptions(t *testing.T) {
|
||||
th := SetupTestHelperWithoutToken().InitBasic()
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
err := th.InitUsers("user1", "user2")
|
||||
require.NoError(t, err, "failed to init users")
|
||||
|
||||
container := store.Container{
|
||||
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
|
||||
}
|
||||
|
||||
t.Run("Get subscriptions for user", func(t *testing.T) {
|
||||
mySubs, user1ID, err := createTestSubscriptions(th.Client, 5, container.WorkspaceID)
|
||||
mySubs, user1ID, err := createTestSubscriptions(th.Client, 5)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, mySubs, 5)
|
||||
|
||||
// create more subscriptions with different user
|
||||
otherSubs, _, err := createTestSubscriptions(th.Client2, 10, container.WorkspaceID)
|
||||
otherSubs, _, err := createTestSubscriptions(th.Client2, 10)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, otherSubs, 10)
|
||||
|
||||
// fetch the newly created subscriptions for current user, making sure only
|
||||
// the ones created for the current user are returned.
|
||||
subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, user1ID)
|
||||
subsFound, resp := th.Client.GetSubscriptions(user1ID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, subsFound, 5)
|
||||
assert.ElementsMatch(t, mySubs, subsFound)
|
||||
@ -138,23 +121,19 @@ func TestGetSubscriptions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteSubscription(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
container := store.Container{
|
||||
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
|
||||
}
|
||||
|
||||
t.Run("Delete valid subscription", func(t *testing.T) {
|
||||
subs, userID, err := createTestSubscriptions(th.Client, 3, container.WorkspaceID)
|
||||
subs, userID, err := createTestSubscriptions(th.Client, 3)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, subs, 3)
|
||||
|
||||
resp := th.Client.DeleteSubscription(container.WorkspaceID, subs[1].BlockID, userID)
|
||||
resp := th.Client.DeleteSubscription(subs[1].BlockID, userID)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
// fetch the subscriptions and ensure the list is correct
|
||||
subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, userID)
|
||||
subsFound, resp := th.Client.GetSubscriptions(userID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, subsFound, 2)
|
||||
|
||||
@ -167,7 +146,7 @@ func TestDeleteSubscription(t *testing.T) {
|
||||
user, resp := th.Client.GetMe()
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
resp = th.Client.DeleteSubscription(container.WorkspaceID, "bogus", user.ID)
|
||||
resp = th.Client.DeleteSubscription("bogus", user.ID)
|
||||
require.Error(t, resp.Error)
|
||||
})
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/api"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -16,7 +17,7 @@ const (
|
||||
)
|
||||
|
||||
func TestUserRegister(t *testing.T) {
|
||||
th := SetupTestHelperWithoutToken().InitBasic()
|
||||
th := SetupTestHelper(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
// register
|
||||
@ -29,14 +30,14 @@ func TestUserRegister(t *testing.T) {
|
||||
require.NoError(t, resp.Error)
|
||||
require.True(t, success)
|
||||
|
||||
// register again will failed
|
||||
// register again will fail
|
||||
success, resp = th.Client.Register(registerRequest)
|
||||
require.Error(t, resp.Error)
|
||||
require.False(t, success)
|
||||
}
|
||||
|
||||
func TestUserLogin(t *testing.T) {
|
||||
th := SetupTestHelperWithoutToken().InitBasic()
|
||||
th := SetupTestHelper(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("with nonexist user", func(t *testing.T) {
|
||||
@ -78,7 +79,7 @@ func TestUserLogin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetMe(t *testing.T) {
|
||||
th := SetupTestHelperWithoutToken().InitBasic()
|
||||
th := SetupTestHelper(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("not login yet", func(t *testing.T) {
|
||||
@ -120,7 +121,7 @@ func TestGetMe(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetUser(t *testing.T) {
|
||||
th := SetupTestHelperWithoutToken().InitBasic()
|
||||
th := SetupTestHelper(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
// register
|
||||
@ -165,7 +166,7 @@ func TestGetUser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUserChangePassword(t *testing.T) {
|
||||
th := SetupTestHelperWithoutToken().InitBasic()
|
||||
th := SetupTestHelper(t).Start()
|
||||
defer th.TearDown()
|
||||
|
||||
// register
|
||||
@ -210,30 +211,58 @@ func randomBytes(t *testing.T, n int) []byte {
|
||||
return bb
|
||||
}
|
||||
|
||||
func TestWorkspaceUploadFile(t *testing.T) {
|
||||
func TestTeamUploadFile(t *testing.T) {
|
||||
t.Run("no permission", func(t *testing.T) { // native auth, but not login
|
||||
th := SetupTestHelperWithoutToken().InitBasic()
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
workspaceID := "0"
|
||||
rootID := utils.NewID(utils.IDTypeBlock)
|
||||
teamID := "0"
|
||||
boardID := utils.NewID(utils.IDTypeBoard)
|
||||
data := randomBytes(t, 1024)
|
||||
result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data))
|
||||
result, resp := th.Client.TeamUploadFile(teamID, boardID, bytes.NewReader(data))
|
||||
require.Error(t, resp.Error)
|
||||
require.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) { // single token auth
|
||||
th := SetupTestHelper().InitBasic()
|
||||
t.Run("a board admin should be able to update a file", func(t *testing.T) { // single token auth
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
workspaceID := "0"
|
||||
rootID := utils.NewID(utils.IDTypeBlock)
|
||||
teamID := "0"
|
||||
newBoard := &model.Board{
|
||||
Type: model.BoardTypeOpen,
|
||||
TeamID: teamID,
|
||||
}
|
||||
board, resp := th.Client.CreateBoard(newBoard)
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, board)
|
||||
|
||||
data := randomBytes(t, 1024)
|
||||
result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data))
|
||||
require.NoError(t, resp.Error)
|
||||
result, resp := th.Client.TeamUploadFile(teamID, board.ID, bytes.NewReader(data))
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, result)
|
||||
require.NotEmpty(t, result.FileID)
|
||||
// TODO get the uploaded file
|
||||
})
|
||||
|
||||
t.Run("user that doesn't belong to the board should not be able to upload a file", func(t *testing.T) {
|
||||
th := SetupTestHelper(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
teamID := "0"
|
||||
newBoard := &model.Board{
|
||||
Type: model.BoardTypeOpen,
|
||||
TeamID: teamID,
|
||||
}
|
||||
board, resp := th.Client.CreateBoard(newBoard)
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, board)
|
||||
|
||||
data := randomBytes(t, 1024)
|
||||
|
||||
// a user that doesn't belong to the board tries to upload the file
|
||||
result, resp := th.Client2.TeamUploadFile(teamID, board.ID, bytes.NewReader(data))
|
||||
th.CheckForbidden(resp)
|
||||
require.Nil(t, result)
|
||||
})
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
|
||||
)
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
@ -145,11 +146,14 @@ func main() {
|
||||
logger.Fatal("server.NewStore ERROR", mlog.Err(err))
|
||||
}
|
||||
|
||||
permissionsService := localpermissions.New(db, logger)
|
||||
|
||||
params := server.Params{
|
||||
Cfg: config,
|
||||
SingleUserToken: singleUserToken,
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
Cfg: config,
|
||||
SingleUserToken: singleUserToken,
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
PermissionsService: permissionsService,
|
||||
}
|
||||
|
||||
server, err := server.New(params)
|
||||
@ -233,11 +237,14 @@ func startServer(webPath string, filesPath string, port int, singleUserToken, db
|
||||
logger.Fatal("server.NewStore ERROR", mlog.Err(err))
|
||||
}
|
||||
|
||||
permissionsService := localpermissions.New(db, logger)
|
||||
|
||||
params := server.Params{
|
||||
Cfg: config,
|
||||
SingleUserToken: singleUserToken,
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
Cfg: config,
|
||||
SingleUserToken: singleUserToken,
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
PermissionsService: permissionsService,
|
||||
}
|
||||
|
||||
pServer, err = server.New(params)
|
||||
|
@ -19,10 +19,6 @@ type Block struct {
|
||||
// required: false
|
||||
ParentID string `json:"parentId"`
|
||||
|
||||
// The id for this block's root block
|
||||
// required: true
|
||||
RootID string `json:"rootId"`
|
||||
|
||||
// The id for user who created this block
|
||||
// required: true
|
||||
CreatedBy string `json:"createdBy"`
|
||||
@ -59,9 +55,13 @@ type Block struct {
|
||||
// required: false
|
||||
DeleteAt int64 `json:"deleteAt"`
|
||||
|
||||
// The workspace id that the block belongs to
|
||||
// Deprecated. The workspace id that the block belongs to
|
||||
// required: false
|
||||
WorkspaceID string `json:"-"`
|
||||
|
||||
// The board id that the block belongs to
|
||||
// required: true
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
BoardID string `json:"boardId"`
|
||||
}
|
||||
|
||||
// BlockPatch is a patch for modify blocks
|
||||
@ -71,10 +71,6 @@ type BlockPatch struct {
|
||||
// required: false
|
||||
ParentID *string `json:"parentId"`
|
||||
|
||||
// The id for this block's root block
|
||||
// required: false
|
||||
RootID *string `json:"rootId"`
|
||||
|
||||
// The schema version of this block
|
||||
// required: false
|
||||
Schema *int64 `json:"schema"`
|
||||
@ -94,6 +90,10 @@ type BlockPatch struct {
|
||||
// The block removed fields
|
||||
// required: false
|
||||
DeletedFields []string `json:"deletedFields"`
|
||||
|
||||
// The board id that the block belongs to
|
||||
// required: false
|
||||
BoardID *string `json:"boardId"`
|
||||
}
|
||||
|
||||
// BlockPatchBatch is a batch of IDs and patches for modify blocks
|
||||
@ -106,11 +106,11 @@ type BlockPatchBatch struct {
|
||||
BlockPatches []BlockPatch `json:"block_patches"`
|
||||
}
|
||||
|
||||
// BlockModifier is a callback that can modify each block during an import.
|
||||
// BoardModifier is a callback that can modify each board during an import.
|
||||
// A cache of arbitrary data will be passed for each call and any changes
|
||||
// to the cache will be preserved for the next call.
|
||||
// Return true to import the block or false to skip import.
|
||||
type BlockModifier func(block *Block, cache map[string]interface{}) bool
|
||||
type BoardModifier func(board *Board, cache map[string]interface{}) bool
|
||||
|
||||
func BlocksFromJSON(data io.Reader) []Block {
|
||||
var blocks []Block
|
||||
@ -123,12 +123,12 @@ func (b Block) LogClone() interface{} {
|
||||
return struct {
|
||||
ID string
|
||||
ParentID string
|
||||
RootID string
|
||||
BoardID string
|
||||
Type BlockType
|
||||
}{
|
||||
ID: b.ID,
|
||||
ParentID: b.ParentID,
|
||||
RootID: b.RootID,
|
||||
BoardID: b.BoardID,
|
||||
Type: b.Type,
|
||||
}
|
||||
}
|
||||
@ -139,8 +139,8 @@ func (p *BlockPatch) Patch(block *Block) *Block {
|
||||
block.ParentID = *p.ParentID
|
||||
}
|
||||
|
||||
if p.RootID != nil {
|
||||
block.RootID = *p.RootID
|
||||
if p.BoardID != nil {
|
||||
block.BoardID = *p.BoardID
|
||||
}
|
||||
|
||||
if p.Schema != nil {
|
||||
|
@ -20,61 +20,61 @@ func TestGenerateBlockIDs(t *testing.T) {
|
||||
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
|
||||
|
||||
require.NotEqual(t, blockID, blocks[0].ID)
|
||||
require.Zero(t, blocks[0].RootID)
|
||||
require.Zero(t, blocks[0].BoardID)
|
||||
require.Zero(t, blocks[0].ParentID)
|
||||
})
|
||||
|
||||
t.Run("Should generate a new ID for a single block with references", func(t *testing.T) {
|
||||
blockID := utils.NewID(utils.IDTypeBlock)
|
||||
rootID := utils.NewID(utils.IDTypeBlock)
|
||||
boardID := utils.NewID(utils.IDTypeBlock)
|
||||
parentID := utils.NewID(utils.IDTypeBlock)
|
||||
blocks := []Block{{ID: blockID, RootID: rootID, ParentID: parentID}}
|
||||
blocks := []Block{{ID: blockID, BoardID: boardID, ParentID: parentID}}
|
||||
|
||||
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
|
||||
|
||||
require.NotEqual(t, blockID, blocks[0].ID)
|
||||
require.Equal(t, rootID, blocks[0].RootID)
|
||||
require.Equal(t, boardID, blocks[0].BoardID)
|
||||
require.Equal(t, parentID, blocks[0].ParentID)
|
||||
})
|
||||
|
||||
t.Run("Should generate IDs and link multiple blocks with existing references", func(t *testing.T) {
|
||||
blockID1 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID1 := utils.NewID(utils.IDTypeBlock)
|
||||
boardID1 := utils.NewID(utils.IDTypeBlock)
|
||||
parentID1 := utils.NewID(utils.IDTypeBlock)
|
||||
block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1}
|
||||
block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1}
|
||||
|
||||
blockID2 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID2 := blockID1
|
||||
boardID2 := blockID1
|
||||
parentID2 := utils.NewID(utils.IDTypeBlock)
|
||||
block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2}
|
||||
block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2}
|
||||
|
||||
blocks := []Block{block1, block2}
|
||||
|
||||
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
|
||||
|
||||
require.NotEqual(t, blockID1, blocks[0].ID)
|
||||
require.Equal(t, rootID1, blocks[0].RootID)
|
||||
require.Equal(t, boardID1, blocks[0].BoardID)
|
||||
require.Equal(t, parentID1, blocks[0].ParentID)
|
||||
|
||||
require.NotEqual(t, blockID2, blocks[1].ID)
|
||||
require.NotEqual(t, rootID2, blocks[1].RootID)
|
||||
require.NotEqual(t, boardID2, blocks[1].BoardID)
|
||||
require.Equal(t, parentID2, blocks[1].ParentID)
|
||||
|
||||
// blockID1 was referenced, so it should still be after the ID
|
||||
// changes
|
||||
require.Equal(t, blocks[0].ID, blocks[1].RootID)
|
||||
require.Equal(t, blocks[0].ID, blocks[1].BoardID)
|
||||
})
|
||||
|
||||
t.Run("Should generate new IDs but not modify nonexisting references", func(t *testing.T) {
|
||||
blockID1 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID1 := ""
|
||||
boardID1 := ""
|
||||
parentID1 := utils.NewID(utils.IDTypeBlock)
|
||||
block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1}
|
||||
block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1}
|
||||
|
||||
blockID2 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID2 := utils.NewID(utils.IDTypeBlock)
|
||||
boardID2 := utils.NewID(utils.IDTypeBlock)
|
||||
parentID2 := ""
|
||||
block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2}
|
||||
block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2}
|
||||
|
||||
blocks := []Block{block1, block2}
|
||||
|
||||
@ -82,37 +82,37 @@ func TestGenerateBlockIDs(t *testing.T) {
|
||||
|
||||
// only the IDs should have changed
|
||||
require.NotEqual(t, blockID1, blocks[0].ID)
|
||||
require.Zero(t, blocks[0].RootID)
|
||||
require.Zero(t, blocks[0].BoardID)
|
||||
require.Equal(t, parentID1, blocks[0].ParentID)
|
||||
|
||||
require.NotEqual(t, blockID2, blocks[1].ID)
|
||||
require.Equal(t, rootID2, blocks[1].RootID)
|
||||
require.Equal(t, boardID2, blocks[1].BoardID)
|
||||
require.Zero(t, blocks[1].ParentID)
|
||||
})
|
||||
|
||||
t.Run("Should modify correctly multiple blocks with existing and nonexisting references", func(t *testing.T) {
|
||||
blockID1 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID1 := utils.NewID(utils.IDTypeBlock)
|
||||
boardID1 := utils.NewID(utils.IDTypeBlock)
|
||||
parentID1 := utils.NewID(utils.IDTypeBlock)
|
||||
block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1}
|
||||
block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1}
|
||||
|
||||
// linked to 1
|
||||
blockID2 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID2 := blockID1
|
||||
boardID2 := blockID1
|
||||
parentID2 := utils.NewID(utils.IDTypeBlock)
|
||||
block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2}
|
||||
block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2}
|
||||
|
||||
// linked to 2
|
||||
blockID3 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID3 := blockID2
|
||||
boardID3 := blockID2
|
||||
parentID3 := utils.NewID(utils.IDTypeBlock)
|
||||
block3 := Block{ID: blockID3, RootID: rootID3, ParentID: parentID3}
|
||||
block3 := Block{ID: blockID3, BoardID: boardID3, ParentID: parentID3}
|
||||
|
||||
// linked to 1
|
||||
blockID4 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID4 := blockID1
|
||||
boardID4 := blockID1
|
||||
parentID4 := utils.NewID(utils.IDTypeBlock)
|
||||
block4 := Block{ID: blockID4, RootID: rootID4, ParentID: parentID4}
|
||||
block4 := Block{ID: blockID4, BoardID: boardID4, ParentID: parentID4}
|
||||
|
||||
// blocks are shuffled
|
||||
blocks := []Block{block4, block2, block1, block3}
|
||||
@ -121,44 +121,44 @@ func TestGenerateBlockIDs(t *testing.T) {
|
||||
|
||||
// block 1
|
||||
require.NotEqual(t, blockID1, blocks[2].ID)
|
||||
require.Equal(t, rootID1, blocks[2].RootID)
|
||||
require.Equal(t, boardID1, blocks[2].BoardID)
|
||||
require.Equal(t, parentID1, blocks[2].ParentID)
|
||||
|
||||
// block 2
|
||||
require.NotEqual(t, blockID2, blocks[1].ID)
|
||||
require.NotEqual(t, rootID2, blocks[1].RootID)
|
||||
require.Equal(t, blocks[2].ID, blocks[1].RootID) // link to 1
|
||||
require.NotEqual(t, boardID2, blocks[1].BoardID)
|
||||
require.Equal(t, blocks[2].ID, blocks[1].BoardID) // link to 1
|
||||
require.Equal(t, parentID2, blocks[1].ParentID)
|
||||
|
||||
// block 3
|
||||
require.NotEqual(t, blockID3, blocks[3].ID)
|
||||
require.NotEqual(t, rootID3, blocks[3].RootID)
|
||||
require.Equal(t, blocks[1].ID, blocks[3].RootID) // link to 2
|
||||
require.NotEqual(t, boardID3, blocks[3].BoardID)
|
||||
require.Equal(t, blocks[1].ID, blocks[3].BoardID) // link to 2
|
||||
require.Equal(t, parentID3, blocks[3].ParentID)
|
||||
|
||||
// block 4
|
||||
require.NotEqual(t, blockID4, blocks[0].ID)
|
||||
require.NotEqual(t, rootID4, blocks[0].RootID)
|
||||
require.Equal(t, blocks[2].ID, blocks[0].RootID) // link to 1
|
||||
require.NotEqual(t, boardID4, blocks[0].BoardID)
|
||||
require.Equal(t, blocks[2].ID, blocks[0].BoardID) // link to 1
|
||||
require.Equal(t, parentID4, blocks[0].ParentID)
|
||||
})
|
||||
|
||||
t.Run("Should update content order", func(t *testing.T) {
|
||||
blockID1 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID1 := utils.NewID(utils.IDTypeBlock)
|
||||
boardID1 := utils.NewID(utils.IDTypeBlock)
|
||||
parentID1 := utils.NewID(utils.IDTypeBlock)
|
||||
block1 := Block{
|
||||
ID: blockID1,
|
||||
RootID: rootID1,
|
||||
BoardID: boardID1,
|
||||
ParentID: parentID1,
|
||||
}
|
||||
|
||||
blockID2 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID2 := utils.NewID(utils.IDTypeBlock)
|
||||
boardID2 := utils.NewID(utils.IDTypeBlock)
|
||||
parentID2 := utils.NewID(utils.IDTypeBlock)
|
||||
block2 := Block{
|
||||
ID: blockID2,
|
||||
RootID: rootID2,
|
||||
BoardID: boardID2,
|
||||
ParentID: parentID2,
|
||||
Fields: map[string]interface{}{
|
||||
"contentOrder": []interface{}{
|
||||
@ -172,11 +172,11 @@ func TestGenerateBlockIDs(t *testing.T) {
|
||||
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
|
||||
|
||||
require.NotEqual(t, blockID1, blocks[0].ID)
|
||||
require.Equal(t, rootID1, blocks[0].RootID)
|
||||
require.Equal(t, boardID1, blocks[0].BoardID)
|
||||
require.Equal(t, parentID1, blocks[0].ParentID)
|
||||
|
||||
require.NotEqual(t, blockID2, blocks[1].ID)
|
||||
require.Equal(t, rootID2, blocks[1].RootID)
|
||||
require.Equal(t, boardID2, blocks[1].BoardID)
|
||||
require.Equal(t, parentID2, blocks[1].ParentID)
|
||||
|
||||
// since block 1 was referenced in block 2,
|
||||
@ -189,35 +189,35 @@ func TestGenerateBlockIDs(t *testing.T) {
|
||||
|
||||
t.Run("Should update content order when it contain slices", func(t *testing.T) {
|
||||
blockID1 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID1 := utils.NewID(utils.IDTypeBlock)
|
||||
boardID1 := utils.NewID(utils.IDTypeBlock)
|
||||
parentID1 := utils.NewID(utils.IDTypeBlock)
|
||||
block1 := Block{
|
||||
ID: blockID1,
|
||||
RootID: rootID1,
|
||||
BoardID: boardID1,
|
||||
ParentID: parentID1,
|
||||
}
|
||||
|
||||
blockID2 := utils.NewID(utils.IDTypeBlock)
|
||||
block2 := Block{
|
||||
ID: blockID2,
|
||||
RootID: rootID1,
|
||||
BoardID: boardID1,
|
||||
ParentID: parentID1,
|
||||
}
|
||||
|
||||
blockID3 := utils.NewID(utils.IDTypeBlock)
|
||||
block3 := Block{
|
||||
ID: blockID3,
|
||||
RootID: rootID1,
|
||||
BoardID: boardID1,
|
||||
ParentID: parentID1,
|
||||
}
|
||||
|
||||
blockID4 := utils.NewID(utils.IDTypeBlock)
|
||||
rootID2 := utils.NewID(utils.IDTypeBlock)
|
||||
boardID2 := utils.NewID(utils.IDTypeBlock)
|
||||
parentID2 := utils.NewID(utils.IDTypeBlock)
|
||||
|
||||
block4 := Block{
|
||||
ID: blockID4,
|
||||
RootID: rootID2,
|
||||
BoardID: boardID2,
|
||||
ParentID: parentID2,
|
||||
Fields: map[string]interface{}{
|
||||
"contentOrder": []interface{}{
|
||||
@ -235,11 +235,11 @@ func TestGenerateBlockIDs(t *testing.T) {
|
||||
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
|
||||
|
||||
require.NotEqual(t, blockID1, blocks[0].ID)
|
||||
require.Equal(t, rootID1, blocks[0].RootID)
|
||||
require.Equal(t, boardID1, blocks[0].BoardID)
|
||||
require.Equal(t, parentID1, blocks[0].ParentID)
|
||||
|
||||
require.NotEqual(t, blockID4, blocks[3].ID)
|
||||
require.Equal(t, rootID2, blocks[3].RootID)
|
||||
require.Equal(t, boardID2, blocks[3].BoardID)
|
||||
require.Equal(t, parentID2, blocks[3].ParentID)
|
||||
|
||||
// since block 1 was referenced in block 2,
|
||||
|
@ -20,8 +20,8 @@ func GenerateBlockIDs(blocks []Block, logger *mlog.Logger) []Block {
|
||||
blockIDs[block.ID] = block.Type
|
||||
}
|
||||
|
||||
if _, ok := referenceIDs[block.RootID]; !ok {
|
||||
referenceIDs[block.RootID] = true
|
||||
if _, ok := referenceIDs[block.BoardID]; !ok {
|
||||
referenceIDs[block.BoardID] = true
|
||||
}
|
||||
if _, ok := referenceIDs[block.ParentID]; !ok {
|
||||
referenceIDs[block.ParentID] = true
|
||||
@ -81,7 +81,7 @@ func GenerateBlockIDs(blocks []Block, logger *mlog.Logger) []Block {
|
||||
newBlocks := make([]Block, len(blocks))
|
||||
for i, block := range blocks {
|
||||
block.ID = getExistingOrNewID(block.ID)
|
||||
block.RootID = getExistingOrOldID(block.RootID)
|
||||
block.BoardID = getExistingOrOldID(block.BoardID)
|
||||
block.ParentID = getExistingOrOldID(block.ParentID)
|
||||
|
||||
blockMod := block
|
||||
|
304
server/model/board.go
Normal file
304
server/model/board.go
Normal file
@ -0,0 +1,304 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type BoardType string
|
||||
|
||||
const (
|
||||
BoardTypeOpen BoardType = "O"
|
||||
BoardTypePrivate BoardType = "P"
|
||||
)
|
||||
|
||||
// Board groups a set of blocks and its layout
|
||||
// swagger:model
|
||||
type Board struct {
|
||||
// The ID for the board
|
||||
// required: true
|
||||
ID string `json:"id"`
|
||||
|
||||
// The ID of the team that the board belongs to
|
||||
// required: true
|
||||
TeamID string `json:"teamId"`
|
||||
|
||||
// The ID of the channel that the board was created from
|
||||
// required: false
|
||||
ChannelID string `json:"channelId"`
|
||||
|
||||
// The ID of the user that created the board
|
||||
// required: true
|
||||
CreatedBy string `json:"createdBy"`
|
||||
|
||||
// The ID of the last user that updated the board
|
||||
// required: true
|
||||
ModifiedBy string `json:"modifiedBy"`
|
||||
|
||||
// The type of the board
|
||||
// required: true
|
||||
Type BoardType `json:"type"`
|
||||
|
||||
// The title of the board
|
||||
// required: false
|
||||
Title string `json:"title"`
|
||||
|
||||
// The description of the board
|
||||
// required: false
|
||||
Description string `json:"description"`
|
||||
|
||||
// The icon of the board
|
||||
// required: false
|
||||
Icon string `json:"icon"`
|
||||
|
||||
// Indicates if the board shows the description on the interface
|
||||
// required: false
|
||||
ShowDescription bool `json:"showDescription"`
|
||||
|
||||
// Marks the template boards
|
||||
// required: false
|
||||
IsTemplate bool `json:"isTemplate"`
|
||||
|
||||
// Marks the template boards
|
||||
// required: false
|
||||
TemplateVersion int `json:"templateVersion"`
|
||||
|
||||
// The properties of the board
|
||||
// required: false
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
|
||||
// The properties of the board cards
|
||||
// required: false
|
||||
CardProperties []map[string]interface{} `json:"cardProperties"`
|
||||
|
||||
// The calculations on the board's cards
|
||||
// required: false
|
||||
ColumnCalculations map[string]interface{} `json:"columnCalculations"`
|
||||
|
||||
// The creation time
|
||||
// required: true
|
||||
CreateAt int64 `json:"createAt"`
|
||||
|
||||
// The last modified time
|
||||
// required: true
|
||||
UpdateAt int64 `json:"updateAt"`
|
||||
|
||||
// The deleted time. Set to indicate this block is deleted
|
||||
// required: false
|
||||
DeleteAt int64 `json:"deleteAt"`
|
||||
}
|
||||
|
||||
// BoardPatch is a patch for modify boards
|
||||
// swagger:model
|
||||
type BoardPatch struct {
|
||||
// The type of the board
|
||||
// required: false
|
||||
Type *BoardType `json:"type"`
|
||||
|
||||
// The title of the board
|
||||
// required: false
|
||||
Title *string `json:"title"`
|
||||
|
||||
// The description of the board
|
||||
// required: false
|
||||
Description *string `json:"description"`
|
||||
|
||||
// The icon of the board
|
||||
// required: false
|
||||
Icon *string `json:"icon"`
|
||||
|
||||
// Indicates if the board shows the description on the interface
|
||||
// required: false
|
||||
ShowDescription *bool `json:"showDescription"`
|
||||
|
||||
// The board updated properties
|
||||
// required: false
|
||||
UpdatedProperties map[string]interface{} `json:"updatedProperties"`
|
||||
|
||||
// The board removed properties
|
||||
// required: false
|
||||
DeletedProperties []string `json:"deletedProperties"`
|
||||
|
||||
// The board updated card properties
|
||||
// required: false
|
||||
UpdatedCardProperties []map[string]interface{} `json:"updatedCardProperties"`
|
||||
|
||||
// The board removed card properties
|
||||
// required: false
|
||||
DeletedCardProperties []string `json:"deletedCardProperties"`
|
||||
|
||||
// The board updated column calculations
|
||||
// required: false
|
||||
UpdatedColumnCalculations map[string]interface{} `json:"updatedColumnCalculations"`
|
||||
|
||||
// The board deleted column calculations
|
||||
// required: false
|
||||
DeletedColumnCalculations []string `json:"deletedColumnCalculations"`
|
||||
}
|
||||
|
||||
// BoardMember stores the information of the membership of a user on a board
|
||||
// swagger:model
|
||||
type BoardMember struct {
|
||||
// The ID of the board
|
||||
// required: true
|
||||
BoardID string `json:"boardId"`
|
||||
|
||||
// The ID of the user
|
||||
// required: true
|
||||
UserID string `json:"userId"`
|
||||
|
||||
// The independent roles of the user on the board
|
||||
// required: false
|
||||
Roles string `json:"roles"`
|
||||
|
||||
// Marks the user as an admin of the board
|
||||
// required: true
|
||||
SchemeAdmin bool `json:"schemeAdmin"`
|
||||
|
||||
// Marks the user as an editor of the board
|
||||
// required: true
|
||||
SchemeEditor bool `json:"schemeEditor"`
|
||||
|
||||
// Marks the user as an commenter of the board
|
||||
// required: true
|
||||
SchemeCommenter bool `json:"schemeCommenter"`
|
||||
|
||||
// Marks the user as an viewer of the board
|
||||
// required: true
|
||||
SchemeViewer bool `json:"schemeViewer"`
|
||||
}
|
||||
|
||||
func BoardFromJSON(data io.Reader) *Board {
|
||||
var board *Board
|
||||
_ = json.NewDecoder(data).Decode(&board)
|
||||
return board
|
||||
}
|
||||
|
||||
func BoardsFromJSON(data io.Reader) []*Board {
|
||||
var boards []*Board
|
||||
_ = json.NewDecoder(data).Decode(&boards)
|
||||
return boards
|
||||
}
|
||||
|
||||
func BoardMemberFromJSON(data io.Reader) *BoardMember {
|
||||
var boardMember *BoardMember
|
||||
_ = json.NewDecoder(data).Decode(&boardMember)
|
||||
return boardMember
|
||||
}
|
||||
|
||||
func BoardMembersFromJSON(data io.Reader) []*BoardMember {
|
||||
var boardMembers []*BoardMember
|
||||
_ = json.NewDecoder(data).Decode(&boardMembers)
|
||||
return boardMembers
|
||||
}
|
||||
|
||||
// Patch returns an updated version of the board.
|
||||
func (p *BoardPatch) Patch(board *Board) *Board {
|
||||
if p.Type != nil {
|
||||
board.Type = *p.Type
|
||||
}
|
||||
|
||||
if p.Title != nil {
|
||||
board.Title = *p.Title
|
||||
}
|
||||
|
||||
if p.Description != nil {
|
||||
board.Description = *p.Description
|
||||
}
|
||||
|
||||
if p.Icon != nil {
|
||||
board.Icon = *p.Icon
|
||||
}
|
||||
|
||||
if p.ShowDescription != nil {
|
||||
board.ShowDescription = *p.ShowDescription
|
||||
}
|
||||
|
||||
for key, property := range p.UpdatedProperties {
|
||||
board.Properties[key] = property
|
||||
}
|
||||
|
||||
for _, key := range p.DeletedProperties {
|
||||
delete(board.Properties, key)
|
||||
}
|
||||
|
||||
if len(p.UpdatedCardProperties) != 0 || len(p.DeletedCardProperties) != 0 {
|
||||
// first we accumulate all properties indexed by ID
|
||||
cardPropertyMap := map[string]map[string]interface{}{}
|
||||
for _, prop := range board.CardProperties {
|
||||
id, ok := prop["id"].(string)
|
||||
if !ok {
|
||||
// bad property, skipping
|
||||
continue
|
||||
}
|
||||
|
||||
cardPropertyMap[id] = prop
|
||||
}
|
||||
|
||||
// if there are properties marked for removal, we delete them
|
||||
for _, propertyID := range p.DeletedCardProperties {
|
||||
delete(cardPropertyMap, propertyID)
|
||||
}
|
||||
|
||||
// if there are properties marked for update, we replace the
|
||||
// existing ones or add them
|
||||
for _, newprop := range p.UpdatedCardProperties {
|
||||
id, ok := newprop["id"].(string)
|
||||
if !ok {
|
||||
// bad new property, skipping
|
||||
continue
|
||||
}
|
||||
|
||||
cardPropertyMap[id] = newprop
|
||||
}
|
||||
|
||||
// and finally we flatten and save the updated properties
|
||||
newCardProperties := []map[string]interface{}{}
|
||||
for _, p := range cardPropertyMap {
|
||||
newCardProperties = append(newCardProperties, p)
|
||||
}
|
||||
|
||||
board.CardProperties = newCardProperties
|
||||
}
|
||||
|
||||
for key, columnCalculation := range p.UpdatedColumnCalculations {
|
||||
board.ColumnCalculations[key] = columnCalculation
|
||||
}
|
||||
|
||||
for _, key := range p.DeletedColumnCalculations {
|
||||
delete(board.ColumnCalculations, key)
|
||||
}
|
||||
|
||||
return board
|
||||
}
|
||||
|
||||
func IsBoardTypeValid(t BoardType) bool {
|
||||
return t == BoardTypeOpen || t == BoardTypePrivate
|
||||
}
|
||||
|
||||
func (p *BoardPatch) IsValid() error {
|
||||
if p.Type != nil && !IsBoardTypeValid(*p.Type) {
|
||||
return InvalidBoardErr{"invalid-board-type"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type InvalidBoardErr struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (ibe InvalidBoardErr) Error() string {
|
||||
return ibe.msg
|
||||
}
|
||||
|
||||
func (b *Board) IsValid() error {
|
||||
if b.TeamID == "" {
|
||||
return InvalidBoardErr{"empty-team-id"}
|
||||
}
|
||||
|
||||
if !IsBoardTypeValid(b.Type) {
|
||||
return InvalidBoardErr{"invalid-board-type"}
|
||||
}
|
||||
return nil
|
||||
}
|
164
server/model/boards_and_blocks.go
Normal file
164
server/model/boards_and_blocks.go
Normal file
@ -0,0 +1,164 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
var ErrNoBoardsInBoardsAndBlocks = errors.New("at least one board is required")
|
||||
var ErrNoBlocksInBoardsAndBlocks = errors.New("at least one block is required")
|
||||
var ErrNoTeamInBoardsAndBlocks = errors.New("team ID cannot be empty")
|
||||
var ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("board ids and patches need to match")
|
||||
var ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("block ids and patches need to match")
|
||||
|
||||
type BlockDoesntBelongToAnyBoardErr struct {
|
||||
blockID string
|
||||
}
|
||||
|
||||
func (e BlockDoesntBelongToAnyBoardErr) Error() string {
|
||||
return fmt.Sprintf("block %s doesn't belong to any board", e.blockID)
|
||||
}
|
||||
|
||||
// BoardsAndBlocks is used to operate over boards and blocks at the
|
||||
// same time
|
||||
// swagger:model
|
||||
type BoardsAndBlocks struct {
|
||||
// The boards
|
||||
// required: false
|
||||
Boards []*Board `json:"boards"`
|
||||
|
||||
// The blocks
|
||||
// required: false
|
||||
Blocks []Block `json:"blocks"`
|
||||
}
|
||||
|
||||
func (bab *BoardsAndBlocks) IsValid() error {
|
||||
if len(bab.Boards) == 0 {
|
||||
return ErrNoBoardsInBoardsAndBlocks
|
||||
}
|
||||
|
||||
if len(bab.Blocks) == 0 {
|
||||
return ErrNoBlocksInBoardsAndBlocks
|
||||
}
|
||||
|
||||
boardsMap := map[string]bool{}
|
||||
for _, board := range bab.Boards {
|
||||
boardsMap[board.ID] = true
|
||||
}
|
||||
|
||||
for _, block := range bab.Blocks {
|
||||
if _, ok := boardsMap[block.BoardID]; !ok {
|
||||
return BlockDoesntBelongToAnyBoardErr{block.ID}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteBoardsAndBlocks is used to list the boards and blocks to
|
||||
// delete on a request
|
||||
// swagger:model
|
||||
type DeleteBoardsAndBlocks struct {
|
||||
// The boards
|
||||
// required: true
|
||||
Boards []string `json:"boards"`
|
||||
|
||||
// The blocks
|
||||
// required: true
|
||||
Blocks []string `json:"blocks"`
|
||||
}
|
||||
|
||||
func (dbab *DeleteBoardsAndBlocks) IsValid() error {
|
||||
if len(dbab.Boards) == 0 {
|
||||
return ErrNoBoardsInBoardsAndBlocks
|
||||
}
|
||||
|
||||
if len(dbab.Blocks) == 0 {
|
||||
return ErrNoBlocksInBoardsAndBlocks
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PatchBoardsAndBlocks is used to patch multiple boards and blocks on
|
||||
// a single request
|
||||
// swagger:model
|
||||
type PatchBoardsAndBlocks struct {
|
||||
// The board IDs to patch
|
||||
// required: true
|
||||
BoardIDs []string `json:"boardIDs"`
|
||||
|
||||
// The board patches
|
||||
// required: true
|
||||
BoardPatches []*BoardPatch `json:"boardPatches"`
|
||||
|
||||
// The block IDs to patch
|
||||
// required: true
|
||||
BlockIDs []string `json:"blockIDs"`
|
||||
|
||||
// The block patches
|
||||
// required: true
|
||||
BlockPatches []*BlockPatch `json:"blockPatches"`
|
||||
}
|
||||
|
||||
func (dbab *PatchBoardsAndBlocks) IsValid() error {
|
||||
if len(dbab.BoardIDs) == 0 {
|
||||
return ErrNoBoardsInBoardsAndBlocks
|
||||
}
|
||||
|
||||
if len(dbab.BoardIDs) != len(dbab.BoardPatches) {
|
||||
return ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks
|
||||
}
|
||||
|
||||
if len(dbab.BlockIDs) == 0 {
|
||||
return ErrNoBlocksInBoardsAndBlocks
|
||||
}
|
||||
|
||||
if len(dbab.BlockIDs) != len(dbab.BlockPatches) {
|
||||
return ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenerateBoardsAndBlocksIDs(bab *BoardsAndBlocks, logger *mlog.Logger) (*BoardsAndBlocks, error) {
|
||||
if err := bab.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blocksByBoard := map[string][]Block{}
|
||||
for _, block := range bab.Blocks {
|
||||
blocksByBoard[block.BoardID] = append(blocksByBoard[block.BoardID], block)
|
||||
}
|
||||
|
||||
boards := []*Board{}
|
||||
blocks := []Block{}
|
||||
for _, board := range bab.Boards {
|
||||
newID := utils.NewID(utils.IDTypeBoard)
|
||||
for _, block := range blocksByBoard[board.ID] {
|
||||
block.BoardID = newID
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
|
||||
board.ID = newID
|
||||
boards = append(boards, board)
|
||||
}
|
||||
|
||||
newBab := &BoardsAndBlocks{
|
||||
Boards: boards,
|
||||
Blocks: GenerateBlockIDs(blocks, logger),
|
||||
}
|
||||
|
||||
return newBab, nil
|
||||
}
|
||||
|
||||
func BoardsAndBlocksFromJSON(data io.Reader) *BoardsAndBlocks {
|
||||
var bab *BoardsAndBlocks
|
||||
_ = json.NewDecoder(data).Decode(&bab)
|
||||
return bab
|
||||
}
|
264
server/model/boards_and_blocks_test.go
Normal file
264
server/model/boards_and_blocks_test.go
Normal file
@ -0,0 +1,264 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func TestIsValidBoardsAndBlocks(t *testing.T) {
|
||||
t.Run("no boards", func(t *testing.T) {
|
||||
bab := &BoardsAndBlocks{
|
||||
Blocks: []Block{
|
||||
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
|
||||
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
|
||||
},
|
||||
}
|
||||
|
||||
require.ErrorIs(t, bab.IsValid(), ErrNoBoardsInBoardsAndBlocks)
|
||||
})
|
||||
|
||||
t.Run("no blocks", func(t *testing.T) {
|
||||
bab := &BoardsAndBlocks{
|
||||
Boards: []*Board{
|
||||
{ID: "board-id-1", Type: BoardTypeOpen},
|
||||
{ID: "board-id-2", Type: BoardTypePrivate},
|
||||
},
|
||||
}
|
||||
|
||||
require.ErrorIs(t, bab.IsValid(), ErrNoBlocksInBoardsAndBlocks)
|
||||
})
|
||||
|
||||
t.Run("block that doesn't belong to the boards", func(t *testing.T) {
|
||||
bab := &BoardsAndBlocks{
|
||||
Boards: []*Board{
|
||||
{ID: "board-id-1", Type: BoardTypeOpen},
|
||||
{ID: "board-id-2", Type: BoardTypePrivate},
|
||||
},
|
||||
Blocks: []Block{
|
||||
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
|
||||
{ID: "block-id-3", BoardID: "board-id-3", Type: TypeCard},
|
||||
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
|
||||
},
|
||||
}
|
||||
|
||||
require.ErrorIs(t, bab.IsValid(), BlockDoesntBelongToAnyBoardErr{"block-id-3"})
|
||||
})
|
||||
|
||||
t.Run("valid boards and blocks", func(t *testing.T) {
|
||||
bab := &BoardsAndBlocks{
|
||||
Boards: []*Board{
|
||||
{ID: "board-id-1", Type: BoardTypeOpen},
|
||||
{ID: "board-id-2", Type: BoardTypePrivate},
|
||||
},
|
||||
Blocks: []Block{
|
||||
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
|
||||
{ID: "block-id-3", BoardID: "board-id-2", Type: TypeCard},
|
||||
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, bab.IsValid())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateBoardsAndBlocksIDs(t *testing.T) {
|
||||
logger, err := mlog.NewLogger()
|
||||
require.NoError(t, err)
|
||||
|
||||
getBlockByType := func(blocks []Block, blockType BlockType) Block {
|
||||
for _, b := range blocks {
|
||||
if b.Type == blockType {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return Block{}
|
||||
}
|
||||
|
||||
getBoardByTitle := func(boards []*Board, title string) *Board {
|
||||
for _, b := range boards {
|
||||
if b.Title == title {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Run("invalid boards and blocks", func(t *testing.T) {
|
||||
bab := &BoardsAndBlocks{
|
||||
Blocks: []Block{
|
||||
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
|
||||
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
|
||||
},
|
||||
}
|
||||
|
||||
rBab, err := GenerateBoardsAndBlocksIDs(bab, logger)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, rBab)
|
||||
})
|
||||
|
||||
t.Run("correctly generates IDs for all the boards and links the blocks to them, with new IDs too", func(t *testing.T) {
|
||||
bab := &BoardsAndBlocks{
|
||||
Boards: []*Board{
|
||||
{ID: "board-id-1", Type: BoardTypeOpen, Title: "board1"},
|
||||
{ID: "board-id-2", Type: BoardTypePrivate, Title: "board2"},
|
||||
{ID: "board-id-3", Type: BoardTypeOpen, Title: "board3"},
|
||||
},
|
||||
Blocks: []Block{
|
||||
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
|
||||
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeView},
|
||||
{ID: "block-id-3", BoardID: "board-id-2", Type: TypeText},
|
||||
},
|
||||
}
|
||||
|
||||
rBab, err := GenerateBoardsAndBlocksIDs(bab, logger)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rBab)
|
||||
|
||||
// all boards and blocks should have refreshed their IDs, and
|
||||
// blocks should be correctly linked to the new board IDs
|
||||
board1 := getBoardByTitle(rBab.Boards, "board1")
|
||||
require.NotNil(t, board1)
|
||||
require.NotEmpty(t, board1.ID)
|
||||
require.NotEqual(t, "board-id-1", board1.ID)
|
||||
board2 := getBoardByTitle(rBab.Boards, "board2")
|
||||
require.NotNil(t, board2)
|
||||
require.NotEmpty(t, board2.ID)
|
||||
require.NotEqual(t, "board-id-2", board2.ID)
|
||||
board3 := getBoardByTitle(rBab.Boards, "board3")
|
||||
require.NotNil(t, board3)
|
||||
require.NotEmpty(t, board3.ID)
|
||||
require.NotEqual(t, "board-id-3", board3.ID)
|
||||
|
||||
block1 := getBlockByType(rBab.Blocks, TypeCard)
|
||||
require.NotNil(t, block1)
|
||||
require.NotEmpty(t, block1.ID)
|
||||
require.NotEqual(t, "block-id-1", block1.ID)
|
||||
require.Equal(t, board1.ID, block1.BoardID)
|
||||
block2 := getBlockByType(rBab.Blocks, TypeView)
|
||||
require.NotNil(t, block2)
|
||||
require.NotEmpty(t, block2.ID)
|
||||
require.NotEqual(t, "block-id-2", block2.ID)
|
||||
require.Equal(t, board2.ID, block2.BoardID)
|
||||
block3 := getBlockByType(rBab.Blocks, TypeText)
|
||||
require.NotNil(t, block3)
|
||||
require.NotEmpty(t, block3.ID)
|
||||
require.NotEqual(t, "block-id-3", block3.ID)
|
||||
require.Equal(t, board2.ID, block3.BoardID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsValidPatchBoardsAndBlocks(t *testing.T) {
|
||||
newTitle := "new title"
|
||||
newDescription := "new description"
|
||||
var schema int64 = 1
|
||||
|
||||
t.Run("no board ids", func(t *testing.T) {
|
||||
pbab := &PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{},
|
||||
BlockIDs: []string{"block-id-1"},
|
||||
BlockPatches: []*BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Schema: &schema},
|
||||
},
|
||||
}
|
||||
|
||||
require.ErrorIs(t, pbab.IsValid(), ErrNoBoardsInBoardsAndBlocks)
|
||||
})
|
||||
|
||||
t.Run("missmatch board IDs and patches", func(t *testing.T) {
|
||||
pbab := &PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{"board-id-1", "board-id-2"},
|
||||
BoardPatches: []*BoardPatch{
|
||||
{Title: &newTitle},
|
||||
},
|
||||
BlockIDs: []string{"block-id-1"},
|
||||
BlockPatches: []*BlockPatch{
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
require.ErrorIs(t, pbab.IsValid(), ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks)
|
||||
})
|
||||
|
||||
t.Run("no block ids", func(t *testing.T) {
|
||||
pbab := &PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{"board-id-1", "board-id-2"},
|
||||
BoardPatches: []*BoardPatch{
|
||||
{Title: &newTitle},
|
||||
{Description: &newDescription},
|
||||
},
|
||||
BlockIDs: []string{},
|
||||
}
|
||||
|
||||
require.ErrorIs(t, pbab.IsValid(), ErrNoBlocksInBoardsAndBlocks)
|
||||
})
|
||||
|
||||
t.Run("missmatch block IDs and patches", func(t *testing.T) {
|
||||
pbab := &PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{"board-id-1", "board-id-2"},
|
||||
BoardPatches: []*BoardPatch{
|
||||
{Title: &newTitle},
|
||||
{Description: &newDescription},
|
||||
},
|
||||
BlockIDs: []string{"block-id-1"},
|
||||
BlockPatches: []*BlockPatch{
|
||||
{Title: &newTitle},
|
||||
{Schema: &schema},
|
||||
},
|
||||
}
|
||||
|
||||
require.ErrorIs(t, pbab.IsValid(), ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks)
|
||||
})
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
pbab := &PatchBoardsAndBlocks{
|
||||
BoardIDs: []string{"board-id-1", "board-id-2"},
|
||||
BoardPatches: []*BoardPatch{
|
||||
{Title: &newTitle},
|
||||
{Description: &newDescription},
|
||||
},
|
||||
BlockIDs: []string{"block-id-1"},
|
||||
BlockPatches: []*BlockPatch{
|
||||
{Title: &newTitle},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, pbab.IsValid())
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsValidDeleteBoardsAndBlocks(t *testing.T) {
|
||||
/*
|
||||
TODO fix this
|
||||
t.Run("no board ids", func(t *testing.T) {
|
||||
dbab := &DeleteBoardsAndBlocks{
|
||||
TeamID: "team-id",
|
||||
Blocks: []string{"block-id-1"},
|
||||
}
|
||||
|
||||
require.ErrorIs(t, dbab.IsValid(), NoBoardsInBoardsAndBlocksErr)
|
||||
})
|
||||
|
||||
t.Run("no block ids", func(t *testing.T) {
|
||||
dbab := &DeleteBoardsAndBlocks{
|
||||
TeamID: "team-id",
|
||||
Boards: []string{"board-id-1", "board-id-2"},
|
||||
}
|
||||
|
||||
require.ErrorIs(t, dbab.IsValid(), NoBlocksInBoardsAndBlocksErr)
|
||||
})
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
dbab := &DeleteBoardsAndBlocks{
|
||||
TeamID: "team-id",
|
||||
Boards: []string{"board-id-1", "board-id-2"},
|
||||
Blocks: []string{"block-id-1"},
|
||||
}
|
||||
|
||||
require.NoError(t, dbab.IsValid())
|
||||
})
|
||||
*/
|
||||
}
|
57
server/model/category.go
Normal file
57
server/model/category.go
Normal file
@ -0,0 +1,57 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
)
|
||||
|
||||
type Category struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UserID string `json:"userID"`
|
||||
TeamID string `json:"teamID"`
|
||||
CreateAt int64 `json:"createAt"`
|
||||
UpdateAt int64 `json:"updateAt"`
|
||||
DeleteAt int64 `json:"deleteAt"`
|
||||
}
|
||||
|
||||
func (c *Category) Hydrate() {
|
||||
c.ID = utils.NewID(utils.IDTypeNone)
|
||||
c.CreateAt = utils.GetMillis()
|
||||
c.UpdateAt = c.CreateAt
|
||||
}
|
||||
|
||||
func (c *Category) IsValid() error {
|
||||
if strings.TrimSpace(c.ID) == "" {
|
||||
return newErrInvalidCategory("category ID cannot be empty")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(c.Name) == "" {
|
||||
return newErrInvalidCategory("category name cannot be empty")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(c.UserID) == "" {
|
||||
return newErrInvalidCategory("category user ID cannot be empty")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(c.TeamID) == "" {
|
||||
return newErrInvalidCategory("category team id ID cannot be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ErrInvalidCategory struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func newErrInvalidCategory(msg string) *ErrInvalidCategory {
|
||||
return &ErrInvalidCategory{
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ErrInvalidCategory) Error() string {
|
||||
return e.msg
|
||||
}
|
11
server/model/category_blocks.go
Normal file
11
server/model/category_blocks.go
Normal file
@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
type CategoryBlocks struct {
|
||||
Category
|
||||
BlockIDs []string `json:"blockIDs"`
|
||||
}
|
||||
|
||||
type BlockCategoryWebsocketData struct {
|
||||
BlockID string `json:"blockID"`
|
||||
CategoryID string `json:"categoryID"`
|
||||
}
|
7
server/model/database.go
Normal file
7
server/model/database.go
Normal file
@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
const (
|
||||
SqliteDBType = "sqlite3"
|
||||
PostgresDBType = "postgres"
|
||||
MysqlDBType = "mysql"
|
||||
)
|
@ -33,7 +33,7 @@ type ArchiveLine struct {
|
||||
// ExportArchiveOptions provides options when exporting one or more boards
|
||||
// to an archive.
|
||||
type ExportArchiveOptions struct {
|
||||
WorkspaceID string
|
||||
TeamID string
|
||||
|
||||
// BoardIDs is the list of boards to include in the archive.
|
||||
// Empty slice means export all boards from workspace/team.
|
||||
@ -42,9 +42,9 @@ type ExportArchiveOptions struct {
|
||||
|
||||
// ImportArchiveOptions provides options when importing an archive.
|
||||
type ImportArchiveOptions struct {
|
||||
WorkspaceID string
|
||||
TeamID string
|
||||
ModifiedBy string
|
||||
BlockModifier BlockModifier
|
||||
BoardModifier BoardModifier
|
||||
}
|
||||
|
||||
// ErrUnsupportedArchiveVersion is an error returned when trying to import an
|
||||
|
@ -18,10 +18,6 @@ type NotificationHint struct {
|
||||
// required: true
|
||||
BlockID string `json:"block_id"`
|
||||
|
||||
// WorkspaceID is id of workspace the block belongs to
|
||||
// required: true
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
|
||||
// ModifiedByID is the id of the user who made the block change
|
||||
ModifiedByID string `json:"modified_by_id"`
|
||||
|
||||
@ -41,9 +37,6 @@ func (s *NotificationHint) IsValid() error {
|
||||
if s.BlockID == "" {
|
||||
return ErrInvalidNotificationHint{"missing block id"}
|
||||
}
|
||||
if s.WorkspaceID == "" {
|
||||
return ErrInvalidNotificationHint{"missing workspace id"}
|
||||
}
|
||||
if s.BlockType == "" {
|
||||
return ErrInvalidNotificationHint{"missing block type"}
|
||||
}
|
||||
@ -57,7 +50,6 @@ func (s *NotificationHint) Copy() *NotificationHint {
|
||||
return &NotificationHint{
|
||||
BlockType: s.BlockType,
|
||||
BlockID: s.BlockID,
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
ModifiedByID: s.ModifiedByID,
|
||||
CreateAt: s.CreateAt,
|
||||
NotifyAt: s.NotifyAt,
|
||||
@ -68,14 +60,12 @@ func (s *NotificationHint) LogClone() interface{} {
|
||||
return struct {
|
||||
BlockType BlockType `json:"block_type"`
|
||||
BlockID string `json:"block_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
ModifiedByID string `json:"modified_by_id"`
|
||||
CreateAt string `json:"create_at"`
|
||||
NotifyAt string `json:"notify_at"`
|
||||
}{
|
||||
BlockType: s.BlockType,
|
||||
BlockID: s.BlockID,
|
||||
WorkspaceID: s.WorkspaceID,
|
||||
ModifiedByID: s.ModifiedByID,
|
||||
CreateAt: utils.TimeFromMillis(s.CreateAt).Format(time.StampMilli),
|
||||
NotifyAt: utils.TimeFromMillis(s.NotifyAt).Format(time.StampMilli),
|
||||
|
19
server/model/permission.go
Normal file
19
server/model/permission.go
Normal file
@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
var (
|
||||
PermissionViewTeam = mmModel.PermissionViewTeam
|
||||
PermissionViewMembers = mmModel.PermissionViewMembers
|
||||
PermissionCreatePublicChannel = mmModel.PermissionCreatePublicChannel
|
||||
PermissionCreatePrivateChannel = mmModel.PermissionCreatePrivateChannel
|
||||
PermissionManageBoardType = &mmModel.Permission{Id: "manage_board_type", Name: "", Description: "", Scope: ""}
|
||||
PermissionDeleteBoard = &mmModel.Permission{Id: "delete_board", Name: "", Description: "", Scope: ""}
|
||||
PermissionViewBoard = &mmModel.Permission{Id: "view_board", Name: "", Description: "", Scope: ""}
|
||||
PermissionManageBoardRoles = &mmModel.Permission{Id: "manage_board_roles", Name: "", Description: "", Scope: ""}
|
||||
PermissionShareBoard = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""}
|
||||
PermissionManageBoardCards = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""}
|
||||
PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""}
|
||||
)
|
@ -147,30 +147,10 @@ func (pd PropDef) ParseDate(s string) (string, error) {
|
||||
// schema for all cards within the board.
|
||||
// The result is provided as a map for quick lookup, and the original order is
|
||||
// preserved via the `Index` field.
|
||||
func ParsePropertySchema(board *Block) (PropSchema, error) {
|
||||
if board == nil || board.Type != TypeBoard {
|
||||
return nil, ErrInvalidBoardBlock
|
||||
}
|
||||
|
||||
func ParsePropertySchema(board *Board) (PropSchema, error) {
|
||||
schema := make(map[string]PropDef)
|
||||
|
||||
// cardProperties contains a slice of maps (untyped at this point).
|
||||
cardPropsIface, ok := board.Fields["cardProperties"]
|
||||
if !ok {
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
cardProps, ok := cardPropsIface.([]interface{})
|
||||
if !ok || len(cardProps) == 0 {
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
for i, cp := range cardProps {
|
||||
prop, ok := cp.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, ErrInvalidPropSchema
|
||||
}
|
||||
|
||||
for i, prop := range board.CardProperties {
|
||||
pd := PropDef{
|
||||
ID: getMapString("id", prop),
|
||||
Index: i,
|
||||
|
@ -13,14 +13,13 @@ import (
|
||||
)
|
||||
|
||||
func Test_parsePropertySchema(t *testing.T) {
|
||||
board := &Block{
|
||||
ID: utils.NewID(utils.IDTypeBoard),
|
||||
Type: TypeBoard,
|
||||
Title: "Test Board",
|
||||
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
|
||||
board := &Board{
|
||||
ID: utils.NewID(utils.IDTypeBoard),
|
||||
Title: "Test Board",
|
||||
TeamID: utils.NewID(utils.IDTypeTeam),
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(fieldsExample), &board.Fields)
|
||||
err := json.Unmarshal([]byte(cardPropertiesExample), &board.CardProperties)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("parse schema", func(t *testing.T) {
|
||||
@ -46,93 +45,74 @@ func Test_parsePropertySchema(t *testing.T) {
|
||||
}
|
||||
|
||||
const (
|
||||
fieldsExample = `
|
||||
{
|
||||
"cardProperties":[
|
||||
{
|
||||
"id":"7c212e78-9345-4c60-81b5-0b0e37ce463f",
|
||||
"name":"Type",
|
||||
"options":[
|
||||
{
|
||||
"color":"propColorYellow",
|
||||
"id":"31da50ca-f1a9-4d21-8636-17dc387c1a23",
|
||||
"value":"Ad Hoc"
|
||||
},
|
||||
{
|
||||
"color":"propColorBlue",
|
||||
"id":"def6317c-ec11-410d-8a6b-ea461320f392",
|
||||
"value":"Standup"
|
||||
},
|
||||
{
|
||||
"color":"propColorPurple",
|
||||
"id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7",
|
||||
"value":"Weekly Sync"
|
||||
}
|
||||
],
|
||||
"type":"select"
|
||||
},
|
||||
{
|
||||
"id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4",
|
||||
"name":"Summary",
|
||||
"options":[
|
||||
|
||||
],
|
||||
"type":"text"
|
||||
},
|
||||
{
|
||||
"id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c",
|
||||
"name":"Color",
|
||||
"options":[
|
||||
{
|
||||
"color":"propColorDefault",
|
||||
"id":"efb0c783-f9ea-4938-8b86-9cf425296cd1",
|
||||
"value":"RED"
|
||||
},
|
||||
{
|
||||
"color":"propColorDefault",
|
||||
"id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311",
|
||||
"value":"GREEN"
|
||||
},
|
||||
{
|
||||
"color":"propColorDefault",
|
||||
"id":"a05bdc80-bd90-45b0-8805-a7e77a4884be",
|
||||
"value":"BLUE"
|
||||
}
|
||||
],
|
||||
"type":"select"
|
||||
},
|
||||
{
|
||||
"id":"aawg1s8rxq8o1bbksxmsmpsdd3r",
|
||||
"name":"MyTextProp",
|
||||
"options":[
|
||||
|
||||
],
|
||||
"type":"text"
|
||||
},
|
||||
{
|
||||
"id":"awdwfigo4kse63bdfp56mzhip6w",
|
||||
"name":"MyCheckBox",
|
||||
"options":[
|
||||
|
||||
],
|
||||
"type":"checkbox"
|
||||
},
|
||||
{
|
||||
"id":"a8spou7if43eo1rqzb9qeq488so",
|
||||
"name":"MyDate",
|
||||
"options":[
|
||||
|
||||
],
|
||||
"type":"date"
|
||||
}
|
||||
],
|
||||
"columnCalculations":[
|
||||
|
||||
],
|
||||
"description":"",
|
||||
"icon":"🗒️",
|
||||
"isTemplate":false,
|
||||
"showDescription":false
|
||||
}
|
||||
`
|
||||
cardPropertiesExample = `[
|
||||
{
|
||||
"id":"7c212e78-9345-4c60-81b5-0b0e37ce463f",
|
||||
"name":"Type",
|
||||
"options":[
|
||||
{
|
||||
"color":"propColorYellow",
|
||||
"id":"31da50ca-f1a9-4d21-8636-17dc387c1a23",
|
||||
"value":"Ad Hoc"
|
||||
},
|
||||
{
|
||||
"color":"propColorBlue",
|
||||
"id":"def6317c-ec11-410d-8a6b-ea461320f392",
|
||||
"value":"Standup"
|
||||
},
|
||||
{
|
||||
"color":"propColorPurple",
|
||||
"id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7",
|
||||
"value":"Weekly Sync"
|
||||
}
|
||||
],
|
||||
"type":"select"
|
||||
},
|
||||
{
|
||||
"id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4",
|
||||
"name":"Summary",
|
||||
"options":[],
|
||||
"type":"text"
|
||||
},
|
||||
{
|
||||
"id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c",
|
||||
"name":"Color",
|
||||
"options":[
|
||||
{
|
||||
"color":"propColorDefault",
|
||||
"id":"efb0c783-f9ea-4938-8b86-9cf425296cd1",
|
||||
"value":"RED"
|
||||
},
|
||||
{
|
||||
"color":"propColorDefault",
|
||||
"id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311",
|
||||
"value":"GREEN"
|
||||
},
|
||||
{
|
||||
"color":"propColorDefault",
|
||||
"id":"a05bdc80-bd90-45b0-8805-a7e77a4884be",
|
||||
"value":"BLUE"
|
||||
}
|
||||
],
|
||||
"type":"select"
|
||||
},
|
||||
{
|
||||
"id":"aawg1s8rxq8o1bbksxmsmpsdd3r",
|
||||
"name":"MyTextProp",
|
||||
"options":[],
|
||||
"type":"text"
|
||||
},
|
||||
{
|
||||
"id":"awdwfigo4kse63bdfp56mzhip6w",
|
||||
"name":"MyCheckBox",
|
||||
"options":[],
|
||||
"type":"checkbox"
|
||||
},
|
||||
{
|
||||
"id":"a8spou7if43eo1rqzb9qeq488so",
|
||||
"name":"MyDate",
|
||||
"options":[],
|
||||
"type":"date"
|
||||
}
|
||||
]`
|
||||
)
|
||||
|
@ -31,10 +31,6 @@ type Subscription struct {
|
||||
// required: true
|
||||
BlockID string `json:"blockId"`
|
||||
|
||||
// WorkspaceID is id of the workspace the block belongs to
|
||||
// required: true
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
|
||||
// SubscriberType is the type of the entity (e.g. user, channel) that is subscribing
|
||||
// required: true
|
||||
SubscriberType SubscriberType `json:"subscriberType"`
|
||||
@ -63,9 +59,6 @@ func (s *Subscription) IsValid() error {
|
||||
if s.BlockID == "" {
|
||||
return ErrInvalidSubscription{"missing block id"}
|
||||
}
|
||||
if s.WorkspaceID == "" {
|
||||
return ErrInvalidSubscription{"missing workspace id"}
|
||||
}
|
||||
if s.BlockType == "" {
|
||||
return ErrInvalidSubscription{"missing block type"}
|
||||
}
|
||||
|
46
server/model/team.go
Normal file
46
server/model/team.go
Normal file
@ -0,0 +1,46 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Team is information global to a team
|
||||
// swagger:model
|
||||
type Team struct {
|
||||
// ID of the team
|
||||
// required: true
|
||||
ID string `json:"id"`
|
||||
|
||||
// Title of the team
|
||||
// required: false
|
||||
Title string `json:"title"`
|
||||
|
||||
// Token required to register new users
|
||||
// required: true
|
||||
SignupToken string `json:"signupToken"`
|
||||
|
||||
// Team settings
|
||||
// required: false
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
|
||||
// ID of user who last modified this
|
||||
// required: true
|
||||
ModifiedBy string `json:"modifiedBy"`
|
||||
|
||||
// Updated time
|
||||
// required: true
|
||||
UpdateAt int64 `json:"updateAt"`
|
||||
}
|
||||
|
||||
func TeamFromJSON(data io.Reader) *Team {
|
||||
var team *Team
|
||||
_ = json.NewDecoder(data).Decode(&team)
|
||||
return team
|
||||
}
|
||||
|
||||
func TeamsFromJSON(data io.Reader) []*Team {
|
||||
var teams []*Team
|
||||
_ = json.NewDecoder(data).Decode(&teams)
|
||||
return teams
|
||||
}
|
@ -57,6 +57,8 @@ type User struct {
|
||||
IsBot bool `json:"is_bot"`
|
||||
}
|
||||
|
||||
// UserPropPatch is a user property patch
|
||||
// swagger:model
|
||||
type UserPropPatch struct {
|
||||
// The user prop updated fields
|
||||
// required: false
|
||||
|
@ -1,45 +0,0 @@
|
||||
package model
|
||||
|
||||
// Workspace is information global to a workspace
|
||||
// swagger:model
|
||||
type Workspace struct {
|
||||
// ID of the workspace
|
||||
// required: true
|
||||
ID string `json:"id"`
|
||||
|
||||
// Title of the workspace
|
||||
// required: false
|
||||
Title string `json:"title"`
|
||||
|
||||
// Token required to register new users
|
||||
// required: true
|
||||
SignupToken string `json:"signupToken"`
|
||||
|
||||
// Workspace settings
|
||||
// required: false
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
|
||||
// ID of user who last modified this
|
||||
// required: true
|
||||
ModifiedBy string `json:"modifiedBy"`
|
||||
|
||||
// Updated time
|
||||
// required: true
|
||||
UpdateAt int64 `json:"updateAt"`
|
||||
}
|
||||
|
||||
// UserWorkspace is a summary of a single association between
|
||||
// a user and a workspace
|
||||
// swagger:model
|
||||
type UserWorkspace struct {
|
||||
// ID of the workspace
|
||||
// required: true
|
||||
ID string `json:"id"`
|
||||
|
||||
// Title of the workspace
|
||||
// required: false
|
||||
Title string `json:"title"`
|
||||
|
||||
// Number of boards in the workspace
|
||||
BoardCount int `json:"boardCount"`
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/notify"
|
||||
"github.com/mattermost/focalboard/server/services/permissions"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
||||
@ -12,13 +13,14 @@ import (
|
||||
)
|
||||
|
||||
type Params struct {
|
||||
Cfg *config.Configuration
|
||||
SingleUserToken string
|
||||
DBStore store.Store
|
||||
Logger *mlog.Logger
|
||||
ServerID string
|
||||
WSAdapter ws.Adapter
|
||||
NotifyBackends []notify.Backend
|
||||
Cfg *config.Configuration
|
||||
SingleUserToken string
|
||||
DBStore store.Store
|
||||
Logger *mlog.Logger
|
||||
ServerID string
|
||||
WSAdapter ws.Adapter
|
||||
NotifyBackends []notify.Backend
|
||||
PermissionsService permissions.PermissionsService
|
||||
}
|
||||
|
||||
func (p Params) CheckValid() error {
|
||||
@ -33,6 +35,10 @@ func (p Params) CheckValid() error {
|
||||
if p.Logger == nil {
|
||||
return ErrServerParam{name: "Logger", issue: "cannot be nil"}
|
||||
}
|
||||
|
||||
if p.PermissionsService == nil {
|
||||
return ErrServerParam{name: "Permissions", issue: "cannot be nil"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -74,12 +74,12 @@ func New(params Params) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authenticator := auth.New(params.Cfg, params.DBStore)
|
||||
authenticator := auth.New(params.Cfg, params.DBStore, params.PermissionsService)
|
||||
|
||||
// if no ws adapter is provided, we spin up a websocket server
|
||||
wsAdapter := params.WSAdapter
|
||||
if wsAdapter == nil {
|
||||
wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger)
|
||||
wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger, params.DBStore)
|
||||
}
|
||||
|
||||
filesBackendSettings := filestore.FileBackendSettings{}
|
||||
@ -137,18 +137,19 @@ func New(params Params) (*Server, error) {
|
||||
Metrics: metricsService,
|
||||
Notifications: notificationService,
|
||||
Logger: params.Logger,
|
||||
Permissions: params.PermissionsService,
|
||||
}
|
||||
app := app.New(params.Cfg, wsAdapter, appServices)
|
||||
|
||||
focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.Logger, auditService)
|
||||
focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.PermissionsService, params.Logger, auditService)
|
||||
|
||||
// Local router for admin APIs
|
||||
localRouter := mux.NewRouter()
|
||||
focalboardAPI.RegisterAdminRoutes(localRouter)
|
||||
|
||||
// Init workspace
|
||||
if _, err := app.GetRootWorkspace(); err != nil {
|
||||
params.Logger.Error("Unable to get root workspace", mlog.Err(err))
|
||||
// Init team
|
||||
if _, err := app.GetRootTeam(); err != nil {
|
||||
params.Logger.Error("Unable to get root team", mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -202,6 +203,11 @@ func New(params Params) (*Server, error) {
|
||||
|
||||
server.initHandlers()
|
||||
|
||||
if err := app.InitTemplates(); err != nil {
|
||||
params.Logger.Error("Unable initialize team templates", mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &server, nil
|
||||
}
|
||||
|
||||
@ -272,13 +278,13 @@ func (s *Server) Start() error {
|
||||
for blockType, count := range blockCounts {
|
||||
s.metricsService.ObserveBlockCount(blockType, count)
|
||||
}
|
||||
workspaceCount, err := s.store.GetWorkspaceCount()
|
||||
teamCount, err := s.store.GetTeamCount()
|
||||
if err != nil {
|
||||
s.logger.Error("Error updating metrics", mlog.String("group", "workspaces"), mlog.Err(err))
|
||||
s.logger.Error("Error updating metrics", mlog.String("group", "teams"), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
s.logger.Log(mlog.LvlFBMetrics, "Workspace metrics collected", mlog.Int64("workspace_count", workspaceCount))
|
||||
s.metricsService.ObserveWorkspaceCount(workspaceCount)
|
||||
s.logger.Log(mlog.LvlFBMetrics, "Team metrics collected", mlog.Int64("team_count", teamCount))
|
||||
s.metricsService.ObserveTeamCount(teamCount)
|
||||
}
|
||||
// metricsUpdater() Calling this immediately causes integration unit tests to fail.
|
||||
s.metricsUpdaterTask = scheduler.CreateRecurringTask("updateMetrics", metricsUpdater, updateMetricsTaskFrequency)
|
||||
@ -336,6 +342,8 @@ func (s *Server) Shutdown() error {
|
||||
s.logger.Warn("Error occurred when shutting down notification service", mlog.Err(err))
|
||||
}
|
||||
|
||||
s.app.Shutdown()
|
||||
|
||||
defer s.logger.Info("Server.Shutdown")
|
||||
|
||||
return s.store.Shutdown()
|
||||
@ -472,13 +480,13 @@ func initTelemetry(opts telemetryOptions) *telemetry.Service {
|
||||
}
|
||||
return m, nil
|
||||
})
|
||||
telemetryService.RegisterTracker("workspaces", func() (telemetry.Tracker, error) {
|
||||
count, err := opts.app.GetWorkspaceCount()
|
||||
telemetryService.RegisterTracker("teams", func() (telemetry.Tracker, error) {
|
||||
count, err := opts.app.GetTeamCount()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := map[string]interface{}{
|
||||
"workspaces": count,
|
||||
"teams": count,
|
||||
}
|
||||
return m, nil
|
||||
})
|
||||
|
@ -7,15 +7,15 @@ import (
|
||||
const (
|
||||
DefMaxQueueSize = 1000
|
||||
|
||||
KeyAPIPath = "api_path"
|
||||
KeyEvent = "event"
|
||||
KeyStatus = "status"
|
||||
KeyUserID = "user_id"
|
||||
KeySessionID = "session_id"
|
||||
KeyClient = "client"
|
||||
KeyIPAddress = "ip_address"
|
||||
KeyClusterID = "cluster_id"
|
||||
KeyWorkspaceID = "workspace_id"
|
||||
KeyAPIPath = "api_path"
|
||||
KeyEvent = "event"
|
||||
KeyStatus = "status"
|
||||
KeyUserID = "user_id"
|
||||
KeySessionID = "session_id"
|
||||
KeyClient = "client"
|
||||
KeyIPAddress = "ip_address"
|
||||
KeyClusterID = "cluster_id"
|
||||
KeyTeamID = "team_id"
|
||||
|
||||
Success = "success"
|
||||
Attempt = "attempt"
|
||||
|
@ -2,7 +2,6 @@ package auth
|
||||
|
||||
import "regexp"
|
||||
|
||||
//nolint:lll
|
||||
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
|
||||
// IsEmailValid checks if the email provided passes the required structure and length.
|
||||
|
@ -8,10 +8,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MetricsNamespace = "focalboard"
|
||||
MetricsSubsystemBlocks = "blocks"
|
||||
MetricsSubsystemWorkspaces = "workspaces"
|
||||
MetricsSubsystemSystem = "system"
|
||||
MetricsNamespace = "focalboard"
|
||||
MetricsSubsystemBlocks = "blocks"
|
||||
MetricsSubsystemTeams = "teams"
|
||||
MetricsSubsystemSystem = "system"
|
||||
|
||||
MetricsCloudInstallationLabel = "installationId"
|
||||
)
|
||||
@ -38,8 +38,8 @@ type Metrics struct {
|
||||
blocksPatchedCount prometheus.Counter
|
||||
blocksDeletedCount prometheus.Counter
|
||||
|
||||
blockCount *prometheus.GaugeVec
|
||||
workspaceCount prometheus.Gauge
|
||||
blockCount *prometheus.GaugeVec
|
||||
teamCount prometheus.Gauge
|
||||
|
||||
blockLastActivity prometheus.Gauge
|
||||
}
|
||||
@ -143,14 +143,14 @@ func NewMetrics(info InstanceInfo) *Metrics {
|
||||
}, []string{"BlockType"})
|
||||
m.registry.MustRegister(m.blockCount)
|
||||
|
||||
m.workspaceCount = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
m.teamCount = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: MetricsSubsystemWorkspaces,
|
||||
Name: "workspaces_total",
|
||||
Help: "Total number of workspaces.",
|
||||
Subsystem: MetricsSubsystemTeams,
|
||||
Name: "teams_total",
|
||||
Help: "Total number of teams.",
|
||||
ConstLabels: additionalLabels,
|
||||
})
|
||||
m.registry.MustRegister(m.workspaceCount)
|
||||
m.registry.MustRegister(m.teamCount)
|
||||
|
||||
m.blockLastActivity = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
@ -209,8 +209,8 @@ func (m *Metrics) ObserveBlockCount(blockType string, count int64) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metrics) ObserveWorkspaceCount(count int64) {
|
||||
func (m *Metrics) ObserveTeamCount(count int64) {
|
||||
if m != nil {
|
||||
m.workspaceCount.Set(float64(count))
|
||||
m.teamCount.Set(float64(count))
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,6 @@ import (
|
||||
// SubscriptionDelivery provides an interface for delivering subscription notifications to other systems, such as
|
||||
// channels server via plugin API.
|
||||
type SubscriptionDelivery interface {
|
||||
SubscriptionDeliverSlackAttachments(workspaceID string, subscriberID string, subscriberType model.SubscriberType,
|
||||
SubscriptionDeliverSlackAttachments(subscriberID string, subscriberType model.SubscriberType,
|
||||
attachments []*mm_model.SlackAttachment) error
|
||||
}
|
||||
|
@ -8,14 +8,13 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
// Diff represents a difference between two versions of a block.
|
||||
type Diff struct {
|
||||
Board *model.Block
|
||||
Board *model.Board
|
||||
Card *model.Block
|
||||
Authors StringMap
|
||||
|
||||
@ -40,16 +39,15 @@ type PropDiff struct {
|
||||
}
|
||||
|
||||
type SchemaDiff struct {
|
||||
Board *model.Block
|
||||
Board *model.Board
|
||||
|
||||
OldPropDef *model.PropDef
|
||||
NewPropDef *model.PropDef
|
||||
}
|
||||
|
||||
type diffGenerator struct {
|
||||
container store.Container
|
||||
board *model.Block
|
||||
card *model.Block
|
||||
board *model.Board
|
||||
card *model.Block
|
||||
|
||||
store Store
|
||||
hint *model.NotificationHint
|
||||
@ -63,7 +61,7 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) {
|
||||
Limit: 1,
|
||||
Descending: true,
|
||||
}
|
||||
blocks, err := dg.store.GetBlockHistory(dg.container, dg.hint.BlockID, opts)
|
||||
blocks, err := dg.store.GetBlockHistory(dg.hint.BlockID, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get block for notification: %w", err)
|
||||
}
|
||||
@ -84,7 +82,10 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) {
|
||||
|
||||
switch block.Type {
|
||||
case model.TypeBoard:
|
||||
return dg.generateDiffsForBoard(block, schema)
|
||||
dg.logger.Warn("generateDiffs for board skipped", mlog.String("block_id", block.ID))
|
||||
// TODO: Fix this
|
||||
// return dg.generateDiffsForBoard(block, schema)
|
||||
return nil, nil
|
||||
case model.TypeCard:
|
||||
diff, err := dg.generateDiffsForCard(block, schema)
|
||||
if err != nil || diff == nil {
|
||||
@ -100,27 +101,29 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (dg *diffGenerator) generateDiffsForBoard(board *model.Block, schema model.PropSchema) ([]*Diff, error) {
|
||||
// TODO: fix this
|
||||
/*
|
||||
func (dg *diffGenerator) generateDiffsForBoard(board *model.Board, schema model.PropSchema) ([]*Diff, error) {
|
||||
opts := model.QuerySubtreeOptions{
|
||||
AfterUpdateAt: dg.lastNotifyAt,
|
||||
}
|
||||
|
||||
// find all child blocks of the board that updated since last notify.
|
||||
blocks, err := dg.store.GetSubTree2(dg.container, board.ID, opts)
|
||||
find all child blocks of the board that updated since last notify.
|
||||
blocks, err := dg.store.GetSubTree2(board.ID, board.ID, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get subtree for board %s: %w", board.ID, err)
|
||||
}
|
||||
|
||||
var diffs []*Diff
|
||||
|
||||
// generate diff for board title change or description
|
||||
generate diff for board title change or description
|
||||
boardDiff, err := dg.generateDiffForBlock(board, schema)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate diff for board %s: %w", board.ID, err)
|
||||
}
|
||||
|
||||
if boardDiff != nil {
|
||||
// TODO: phase 2 feature (generate schema diffs and add to board diff) goes here.
|
||||
TODO: phase 2 feature (generate schema diffs and add to board diff) goes here.
|
||||
diffs = append(diffs, boardDiff)
|
||||
}
|
||||
|
||||
@ -136,6 +139,7 @@ func (dg *diffGenerator) generateDiffsForBoard(board *model.Block, schema model.
|
||||
}
|
||||
return diffs, nil
|
||||
}
|
||||
*/
|
||||
|
||||
func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.PropSchema) (*Diff, error) {
|
||||
// generate diff for card title change and properties.
|
||||
@ -148,7 +152,7 @@ func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.Pr
|
||||
opts := model.QuerySubtreeOptions{
|
||||
AfterUpdateAt: dg.lastNotifyAt,
|
||||
}
|
||||
blocks, err := dg.store.GetSubTree2(dg.container, card.ID, opts)
|
||||
blocks, err := dg.store.GetSubTree2(card.BoardID, card.ID, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get subtree for card %s: %w", card.ID, err)
|
||||
}
|
||||
@ -214,7 +218,7 @@ func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema mode
|
||||
Limit: 1,
|
||||
Descending: true,
|
||||
}
|
||||
history, err := dg.store.GetBlockHistory(dg.container, newBlock.ID, opts)
|
||||
history, err := dg.store.GetBlockHistory(newBlock.ID, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get block history for block %s: %w", newBlock.ID, err)
|
||||
}
|
||||
@ -237,7 +241,7 @@ func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema mode
|
||||
AfterUpdateAt: dg.lastNotifyAt,
|
||||
Descending: true,
|
||||
}
|
||||
chgBlocks, err := dg.store.GetBlockHistory(dg.container, newBlock.ID, opts)
|
||||
chgBlocks, err := dg.store.GetBlockHistory(newBlock.ID, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting block history for block %s: %w", newBlock.ID, err)
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ var (
|
||||
// DiffConvOpts provides options when converting diffs to slack attachments.
|
||||
type DiffConvOpts struct {
|
||||
Language string
|
||||
MakeCardLink func(block *model.Block, board *model.Block, card *model.Block) string
|
||||
MakeCardLink func(block *model.Block, board *model.Board, card *model.Block) string
|
||||
Logger *mlog.Logger
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ func getTemplate(name string, opts DiffConvOpts, def string) (*template.Template
|
||||
t = template.New(key)
|
||||
|
||||
if opts.MakeCardLink == nil {
|
||||
opts.MakeCardLink = func(block *model.Block, _ *model.Block, _ *model.Block) string {
|
||||
opts.MakeCardLink = func(block *model.Block, _ *model.Board, _ *model.Block) string {
|
||||
return fmt.Sprintf("`%s`", block.Title)
|
||||
}
|
||||
}
|
||||
@ -160,6 +160,7 @@ func cardDiff2SlackAttachment(cardDiff *Diff, opts DiffConvOpts) (*mm_model.Slac
|
||||
mlog.String("card_id", cardDiff.Card.ID),
|
||||
mlog.String("new_block_id", cardDiff.NewBlock.ID),
|
||||
mlog.String("old_block_id", cardDiff.OldBlock.ID),
|
||||
mlog.Int("childDiffs", len(cardDiff.Diffs)),
|
||||
)
|
||||
|
||||
buf.Reset()
|
||||
|
@ -144,12 +144,8 @@ func (n *notifier) notify() {
|
||||
}
|
||||
|
||||
func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
|
||||
c := store.Container{
|
||||
WorkspaceID: hint.WorkspaceID,
|
||||
}
|
||||
|
||||
// get the subscriber list
|
||||
subs, err := n.store.GetSubscribersForBlock(c, hint.BlockID)
|
||||
subs, err := n.store.GetSubscribersForBlock(hint.BlockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -162,7 +158,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
|
||||
oldestNotifiedAt := subs[0].NotifiedAt
|
||||
|
||||
// need the block's board and card.
|
||||
board, card, err := n.store.GetBoardAndCardByID(c, hint.BlockID)
|
||||
board, card, err := n.store.GetBoardAndCardByID(hint.BlockID)
|
||||
if err != nil || board == nil || card == nil {
|
||||
return fmt.Errorf("could not get board & card for block %s: %w", hint.BlockID, err)
|
||||
}
|
||||
@ -175,7 +171,6 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
|
||||
)
|
||||
|
||||
dg := &diffGenerator{
|
||||
container: c,
|
||||
board: board,
|
||||
card: card,
|
||||
store: n.store,
|
||||
@ -204,8 +199,8 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
|
||||
|
||||
opts := DiffConvOpts{
|
||||
Language: "en", // TODO: use correct language with i18n available on server.
|
||||
MakeCardLink: func(block *model.Block, board *model.Block, card *model.Block) string {
|
||||
return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.WorkspaceID, board.ID, card.ID))
|
||||
MakeCardLink: func(block *model.Block, board *model.Board, card *model.Block) string {
|
||||
return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.TeamID, board.ID, card.ID))
|
||||
},
|
||||
Logger: n.logger,
|
||||
}
|
||||
@ -236,7 +231,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
|
||||
mlog.String("subscriber_type", string(sub.SubscriberType)),
|
||||
)
|
||||
|
||||
if err = n.delivery.SubscriptionDeliverSlackAttachments(hint.WorkspaceID, sub.SubscriberID, sub.SubscriberType, attachments); err != nil {
|
||||
if err = n.delivery.SubscriptionDeliverSlackAttachments(sub.SubscriberID, sub.SubscriberType, attachments); err != nil {
|
||||
merr.Append(fmt.Errorf("cannot deliver notification to subscriber %s [%s]: %w",
|
||||
sub.SubscriberID, sub.SubscriberType, err))
|
||||
}
|
||||
@ -262,7 +257,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
|
||||
}
|
||||
|
||||
// update the last notified_at for all subscribers since we at least attempted to notify all of them.
|
||||
err = dg.store.UpdateSubscribersNotifiedAt(dg.container, dg.hint.BlockID, notifiedAt)
|
||||
err = dg.store.UpdateSubscribersNotifiedAt(dg.hint.BlockID, notifiedAt)
|
||||
if err != nil {
|
||||
merr.Append(fmt.Errorf("could not update subscribers notified_at for block %s: %w", dg.hint.BlockID, err))
|
||||
}
|
||||
|
@ -7,21 +7,20 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
GetBlock(c store.Container, blockID string) (*model.Block, error)
|
||||
GetBlockHistory(c store.Container, blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error)
|
||||
GetSubTree2(c store.Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error)
|
||||
GetBoardAndCardByID(c store.Container, blockID string) (board *model.Block, card *model.Block, err error)
|
||||
GetBlock(blockID string) (*model.Block, error)
|
||||
GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error)
|
||||
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error)
|
||||
GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error)
|
||||
|
||||
GetUserByID(userID string) (*model.User, error)
|
||||
|
||||
CreateSubscription(c store.Container, sub *model.Subscription) (*model.Subscription, error)
|
||||
GetSubscribersForBlock(c store.Container, blockID string) ([]*model.Subscriber, error)
|
||||
GetSubscribersCountForBlock(c store.Container, blockID string) (int, error)
|
||||
UpdateSubscribersNotifiedAt(c store.Container, blockID string, notifyAt int64) error
|
||||
CreateSubscription(sub *model.Subscription) (*model.Subscription, error)
|
||||
GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error)
|
||||
GetSubscribersCountForBlock(blockID string) (int, error)
|
||||
UpdateSubscribersNotifiedAt(blockID string, notifyAt int64) error
|
||||
|
||||
UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error)
|
||||
GetNextNotificationHint(remove bool) (*model.NotificationHint, error)
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/notify"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
"github.com/wiggin77/merror"
|
||||
|
||||
@ -77,8 +76,6 @@ func (b *Backend) getBlockUpdateFreq(blockType model.BlockType) time.Duration {
|
||||
switch blockType {
|
||||
case model.TypeCard:
|
||||
return time.Second * time.Duration(b.notifyFreqCardSeconds)
|
||||
case model.TypeBoard:
|
||||
return time.Second * time.Duration(b.notifyFreqBoardSeconds)
|
||||
default:
|
||||
return defBlockNotificationFreq
|
||||
}
|
||||
@ -95,35 +92,30 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
|
||||
merr := merror.New()
|
||||
var err error
|
||||
|
||||
c := store.Container{
|
||||
WorkspaceID: evt.Workspace,
|
||||
}
|
||||
|
||||
// if new card added, automatically subscribe the author.
|
||||
if evt.Action == notify.Add && evt.BlockChanged.Type == model.TypeCard {
|
||||
sub := &model.Subscription{
|
||||
BlockType: model.TypeCard,
|
||||
BlockID: evt.BlockChanged.ID,
|
||||
WorkspaceID: evt.Workspace,
|
||||
SubscriberType: model.SubTypeUser,
|
||||
SubscriberID: evt.ModifiedByID,
|
||||
}
|
||||
|
||||
if sub, err = b.store.CreateSubscription(c, sub); err != nil {
|
||||
if _, err = b.store.CreateSubscription(sub); err != nil {
|
||||
b.logger.Warn("Cannot subscribe card author to card",
|
||||
mlog.String("card_id", evt.BlockChanged.ID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
b.wsAdapter.BroadcastSubscriptionChange(c.WorkspaceID, sub)
|
||||
b.wsAdapter.BroadcastSubscriptionChange(evt.TeamID, sub)
|
||||
}
|
||||
|
||||
// notify board subscribers
|
||||
subs, err := b.store.GetSubscribersForBlock(c, evt.Board.ID)
|
||||
subs, err := b.store.GetSubscribersForBlock(evt.Board.ID)
|
||||
if err != nil {
|
||||
merr.Append(fmt.Errorf("cannot fetch subscribers for board %s: %w", evt.Board.ID, err))
|
||||
}
|
||||
if err = b.notifySubscribers(subs, evt.Board, evt.ModifiedByID); err != nil {
|
||||
if err = b.notifySubscribers(subs, evt.Board.ID, model.TypeBoard, evt.ModifiedByID); err != nil {
|
||||
merr.Append(fmt.Errorf("cannot notify board subscribers for board %s: %w", evt.Board.ID, err))
|
||||
}
|
||||
|
||||
@ -132,21 +124,21 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
|
||||
}
|
||||
|
||||
// notify card subscribers
|
||||
subs, err = b.store.GetSubscribersForBlock(c, evt.Card.ID)
|
||||
subs, err = b.store.GetSubscribersForBlock(evt.Card.ID)
|
||||
if err != nil {
|
||||
merr.Append(fmt.Errorf("cannot fetch subscribers for card %s: %w", evt.Card.ID, err))
|
||||
}
|
||||
if err = b.notifySubscribers(subs, evt.Card, evt.ModifiedByID); err != nil {
|
||||
if err = b.notifySubscribers(subs, evt.Card.ID, model.TypeCard, evt.ModifiedByID); err != nil {
|
||||
merr.Append(fmt.Errorf("cannot notify card subscribers for card %s: %w", evt.Card.ID, err))
|
||||
}
|
||||
|
||||
// notify block subscribers (if/when other types can be subscribed to)
|
||||
if evt.Board.ID != evt.BlockChanged.ID && evt.Card.ID != evt.BlockChanged.ID {
|
||||
subs, err := b.store.GetSubscribersForBlock(c, evt.BlockChanged.ID)
|
||||
subs, err := b.store.GetSubscribersForBlock(evt.BlockChanged.ID)
|
||||
if err != nil {
|
||||
merr.Append(fmt.Errorf("cannot fetch subscribers for block %s: %w", evt.BlockChanged.ID, err))
|
||||
}
|
||||
if err := b.notifySubscribers(subs, evt.BlockChanged, evt.ModifiedByID); err != nil {
|
||||
if err := b.notifySubscribers(subs, evt.BlockChanged.ID, evt.BlockChanged.Type, evt.ModifiedByID); err != nil {
|
||||
merr.Append(fmt.Errorf("cannot notify block subscribers for block %s: %w", evt.BlockChanged.ID, err))
|
||||
}
|
||||
}
|
||||
@ -154,24 +146,26 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
|
||||
}
|
||||
|
||||
// notifySubscribers triggers a change notification for subscribers by writing a notification hint to the database.
|
||||
func (b *Backend) notifySubscribers(subs []*model.Subscriber, block *model.Block, modifiedByID string) error {
|
||||
func (b *Backend) notifySubscribers(subs []*model.Subscriber, blockID string, idType model.BlockType, modifiedByID string) error {
|
||||
if len(subs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hint := &model.NotificationHint{
|
||||
BlockType: block.Type,
|
||||
BlockID: block.ID,
|
||||
WorkspaceID: block.WorkspaceID,
|
||||
BlockType: idType,
|
||||
BlockID: blockID,
|
||||
ModifiedByID: modifiedByID,
|
||||
}
|
||||
|
||||
hint, err := b.store.UpsertNotificationHint(hint, b.getBlockUpdateFreq(block.Type))
|
||||
hint, err := b.store.UpsertNotificationHint(hint, b.getBlockUpdateFreq(idType))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot upsert notification hint: %w", err)
|
||||
}
|
||||
if err := b.notifier.onNotifyHint(hint); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.notifier.onNotifyHint(hint)
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnMention satisfies the `MentionListener` interface and is called whenever a @mention notification
|
||||
@ -188,17 +182,13 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) {
|
||||
sub := &model.Subscription{
|
||||
BlockType: model.TypeCard,
|
||||
BlockID: evt.Card.ID,
|
||||
WorkspaceID: evt.Workspace,
|
||||
SubscriberType: model.SubTypeUser,
|
||||
SubscriberID: userID,
|
||||
}
|
||||
|
||||
c := store.Container{
|
||||
WorkspaceID: evt.Workspace,
|
||||
}
|
||||
var err error
|
||||
|
||||
if sub, err = b.store.CreateSubscription(c, sub); err != nil {
|
||||
if sub, err = b.store.CreateSubscription(sub); err != nil {
|
||||
b.logger.Warn("Cannot subscribe mentioned user to card",
|
||||
mlog.String("user_id", userID),
|
||||
mlog.String("card_id", evt.Card.ID),
|
||||
@ -206,7 +196,7 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) {
|
||||
)
|
||||
return
|
||||
}
|
||||
b.wsAdapter.BroadcastSubscriptionChange(c.WorkspaceID, sub)
|
||||
b.wsAdapter.BroadcastSubscriptionChange(evt.TeamID, sub)
|
||||
|
||||
b.logger.Debug("Subscribed mentioned user to card",
|
||||
mlog.String("user_id", userID),
|
||||
|
@ -14,13 +14,7 @@ import (
|
||||
|
||||
// MentionDeliver notifies a user they have been mentioned in a block.
|
||||
func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) (string, error) {
|
||||
// determine which team the workspace is associated with
|
||||
teamID, err := pd.getTeamID(evt)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot determine teamID for block change notification: %w", err)
|
||||
}
|
||||
|
||||
member, err := teamMemberFromUsername(pd.api, mentionUsername, teamID)
|
||||
member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID)
|
||||
if err != nil {
|
||||
if isErrNotFound(err) {
|
||||
// not really an error; could just be someone typed "@sometext"
|
||||
@ -30,16 +24,6 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string,
|
||||
}
|
||||
}
|
||||
|
||||
// check that user is a member of the channel
|
||||
_, err = pd.api.GetChannelMember(evt.Workspace, member.UserId)
|
||||
if err != nil {
|
||||
if pd.api.IsErrNotFound(err) {
|
||||
// mentioned user is not a member of the channel; fail silently.
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("cannot fetch channel member for user %s: %w", member.UserId, err)
|
||||
}
|
||||
|
||||
author, err := pd.api.GetUserByID(evt.ModifiedByID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot find user: %w", err)
|
||||
@ -49,7 +33,7 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string,
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot get direct channel: %w", err)
|
||||
}
|
||||
link := utils.MakeCardLink(pd.serverRoot, evt.Workspace, evt.Board.ID, evt.Card.ID)
|
||||
link := utils.MakeCardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID, evt.Card.ID)
|
||||
|
||||
post := &model.Post{
|
||||
UserId: pd.botID,
|
||||
|
@ -4,7 +4,10 @@
|
||||
package plugindelivery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/focalboard/server/services/notify"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
@ -52,11 +55,32 @@ func New(botID string, serverRoot string, api PluginAPI) *PluginDelivery {
|
||||
}
|
||||
}
|
||||
|
||||
func (pd *PluginDelivery) getTeamID(evt notify.BlockChangeEvent) (string, error) {
|
||||
// for now, the workspace ID is also the channel ID
|
||||
channel, err := pd.api.GetChannelByID(evt.Workspace)
|
||||
func (pd *PluginDelivery) Deliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) error {
|
||||
member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
if isErrNotFound(err) {
|
||||
// not really an error; could just be someone typed "@sometext"
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("cannot lookup mentioned user: %w", err)
|
||||
}
|
||||
}
|
||||
return channel.TeamId, nil
|
||||
|
||||
author, err := pd.api.GetUserByID(evt.ModifiedByID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot find user: %w", err)
|
||||
}
|
||||
|
||||
channel, err := pd.api.GetDirectChannel(member.UserId, pd.botID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get direct channel: %w", err)
|
||||
}
|
||||
link := utils.MakeCardLink(pd.serverRoot, evt.TeamID, evt.Board.ID, evt.Card.ID)
|
||||
|
||||
post := &mm_model.Post{
|
||||
UserId: pd.botID,
|
||||
ChannelId: channel.Id,
|
||||
Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged),
|
||||
}
|
||||
return pd.api.CreatePost(post)
|
||||
}
|
||||
|
@ -17,10 +17,10 @@ var (
|
||||
)
|
||||
|
||||
// SubscriptionDeliverSlashAttachments notifies a user that changes were made to a block they are subscribed to.
|
||||
func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(workspaceID string, subscriberID string, subscriptionType model.SubscriberType,
|
||||
func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(subscriberID string, subscriptionType model.SubscriberType,
|
||||
attachments []*mm_model.SlackAttachment) error {
|
||||
// check subscriber is member of channel
|
||||
_, err := pd.api.GetChannelMember(workspaceID, subscriberID)
|
||||
_, err := pd.api.GetUserByID(subscriberID)
|
||||
if err != nil {
|
||||
if pd.api.IsErrNotFound(err) {
|
||||
// subscriber is not a member of the channel; fail silently.
|
||||
|
@ -22,8 +22,8 @@ const (
|
||||
|
||||
type BlockChangeEvent struct {
|
||||
Action Action
|
||||
Workspace string
|
||||
Board *model.Block
|
||||
TeamID string
|
||||
Board *model.Board
|
||||
Card *model.Block
|
||||
BlockChanged *model.Block
|
||||
BlockOld *model.Block
|
||||
@ -31,7 +31,7 @@ type BlockChangeEvent struct {
|
||||
}
|
||||
|
||||
type SubscriptionChangeNotifier interface {
|
||||
BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription)
|
||||
BroadcastSubscriptionChange(subscription *model.Subscription)
|
||||
}
|
||||
|
||||
// Backend provides an interface for sending notifications.
|
||||
@ -113,7 +113,7 @@ func (s *Service) BlockChanged(evt BlockChangeEvent) {
|
||||
|
||||
// BroadcastSubscriptionChange sends a websocket message with details of the changed subscription to all
|
||||
// connected users in the workspace.
|
||||
func (s *Service) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) {
|
||||
func (s *Service) BroadcastSubscriptionChange(subscription *model.Subscription) {
|
||||
s.mux.RLock()
|
||||
backends := make([]Backend, len(s.backends))
|
||||
copy(backends, s.backends)
|
||||
@ -122,11 +122,10 @@ func (s *Service) BroadcastSubscriptionChange(workspaceID string, subscription *
|
||||
for _, backend := range backends {
|
||||
if scn, ok := backend.(SubscriptionChangeNotifier); ok {
|
||||
s.logger.Debug("Delivering subscription change notification",
|
||||
mlog.String("workspace_id", workspaceID),
|
||||
mlog.String("block_id", subscription.BlockID),
|
||||
mlog.String("subscriber_id", subscription.SubscriberID),
|
||||
)
|
||||
scn.BroadcastSubscriptionChange(workspaceID, subscription)
|
||||
scn.BroadcastSubscriptionChange(subscription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
server/services/permissions/localpermissions/helpers_test.go
Normal file
61
server/services/permissions/localpermissions/helpers_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localpermissions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
t *testing.T
|
||||
ctrl *gomock.Controller
|
||||
store *permissionsMocks.MockStore
|
||||
permissions *Service
|
||||
}
|
||||
|
||||
func SetupTestHelper(t *testing.T) *TestHelper {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := permissionsMocks.NewMockStore(ctrl)
|
||||
|
||||
return &TestHelper{
|
||||
t: t,
|
||||
ctrl: ctrl,
|
||||
store: mockStore,
|
||||
permissions: New(mockStore, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, hasPermissionTo, hasNotPermissionTo []*mmModel.Permission) {
|
||||
for _, p := range hasPermissionTo {
|
||||
th.t.Run(roleName+" "+p.Id, func(t *testing.T) {
|
||||
th.store.EXPECT().
|
||||
GetMemberForBoard(member.BoardID, member.UserID).
|
||||
Return(member, nil).
|
||||
Times(1)
|
||||
|
||||
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
}
|
||||
|
||||
for _, p := range hasNotPermissionTo {
|
||||
th.t.Run(roleName+" "+p.Id, func(t *testing.T) {
|
||||
th.store.EXPECT().
|
||||
GetMemberForBoard(member.BoardID, member.UserID).
|
||||
Return(member, nil).
|
||||
Times(1)
|
||||
|
||||
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localpermissions
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/permissions"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
store permissions.Store
|
||||
logger *mlog.Logger
|
||||
}
|
||||
|
||||
func New(store permissions.Store, logger *mlog.Logger) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool {
|
||||
if userID == "" || teamID == "" || permission == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmModel.Permission) bool {
|
||||
if userID == "" || boardID == "" || permission == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
member, err := s.store.GetMemberForBoard(boardID, userID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("error getting member for board",
|
||||
mlog.String("boardID", boardID),
|
||||
mlog.String("userID", userID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
switch permission {
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
|
||||
return member.SchemeAdmin
|
||||
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
|
||||
return member.SchemeAdmin || member.SchemeEditor
|
||||
case model.PermissionViewBoard:
|
||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localpermissions
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHasPermissionToTeam(t *testing.T) {
|
||||
th := SetupTestHelper(t)
|
||||
|
||||
t.Run("empty input should always unauthorize", func(t *testing.T) {
|
||||
assert.False(t, th.permissions.HasPermissionToTeam("", "team-id", model.PermissionManageBoardCards))
|
||||
assert.False(t, th.permissions.HasPermissionToTeam("user-id", "", model.PermissionManageBoardCards))
|
||||
assert.False(t, th.permissions.HasPermissionToTeam("user-id", "team-id", nil))
|
||||
})
|
||||
|
||||
t.Run("all users have all permissions on teams", func(t *testing.T) {
|
||||
hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageBoardCards)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHasPermissionToBoard(t *testing.T) {
|
||||
th := SetupTestHelper(t)
|
||||
|
||||
t.Run("empty input should always unauthorize", func(t *testing.T) {
|
||||
assert.False(t, th.permissions.HasPermissionToBoard("", "board-id", model.PermissionManageBoardCards))
|
||||
assert.False(t, th.permissions.HasPermissionToBoard("user-id", "", model.PermissionManageBoardCards))
|
||||
assert.False(t, th.permissions.HasPermissionToBoard("user-id", "board-id", nil))
|
||||
})
|
||||
|
||||
t.Run("nonexistent user", func(t *testing.T) {
|
||||
userID := "user-id"
|
||||
boardID := "board-id"
|
||||
|
||||
th.store.EXPECT().
|
||||
GetMemberForBoard(boardID, userID).
|
||||
Return(nil, sql.ErrNoRows).
|
||||
Times(1)
|
||||
|
||||
hasPermission := th.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
|
||||
t.Run("board admin", func(t *testing.T) {
|
||||
member := &model.BoardMember{
|
||||
UserID: "user-id",
|
||||
BoardID: "board-id",
|
||||
SchemeAdmin: true,
|
||||
}
|
||||
|
||||
hasPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionManageBoardType,
|
||||
model.PermissionDeleteBoard,
|
||||
model.PermissionManageBoardRoles,
|
||||
model.PermissionShareBoard,
|
||||
model.PermissionManageBoardCards,
|
||||
model.PermissionViewBoard,
|
||||
model.PermissionManageBoardProperties,
|
||||
}
|
||||
|
||||
hasNotPermissionTo := []*mmModel.Permission{}
|
||||
|
||||
th.checkBoardPermissions("admin", member, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
|
||||
t.Run("board editor", func(t *testing.T) {
|
||||
member := &model.BoardMember{
|
||||
UserID: "user-id",
|
||||
BoardID: "board-id",
|
||||
SchemeEditor: true,
|
||||
}
|
||||
|
||||
hasPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionManageBoardCards,
|
||||
model.PermissionViewBoard,
|
||||
model.PermissionManageBoardProperties,
|
||||
}
|
||||
|
||||
hasNotPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionManageBoardType,
|
||||
model.PermissionDeleteBoard,
|
||||
model.PermissionManageBoardRoles,
|
||||
model.PermissionShareBoard,
|
||||
}
|
||||
|
||||
th.checkBoardPermissions("editor", member, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
|
||||
t.Run("board commenter", func(t *testing.T) {
|
||||
member := &model.BoardMember{
|
||||
UserID: "user-id",
|
||||
BoardID: "board-id",
|
||||
SchemeCommenter: true,
|
||||
}
|
||||
|
||||
hasPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionViewBoard,
|
||||
}
|
||||
|
||||
hasNotPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionManageBoardType,
|
||||
model.PermissionDeleteBoard,
|
||||
model.PermissionManageBoardRoles,
|
||||
model.PermissionShareBoard,
|
||||
model.PermissionManageBoardCards,
|
||||
model.PermissionManageBoardProperties,
|
||||
}
|
||||
|
||||
th.checkBoardPermissions("commenter", member, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
|
||||
t.Run("board viewer", func(t *testing.T) {
|
||||
member := &model.BoardMember{
|
||||
UserID: "user-id",
|
||||
BoardID: "board-id",
|
||||
SchemeViewer: true,
|
||||
}
|
||||
|
||||
hasPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionViewBoard,
|
||||
}
|
||||
|
||||
hasNotPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionManageBoardType,
|
||||
model.PermissionDeleteBoard,
|
||||
model.PermissionManageBoardRoles,
|
||||
model.PermissionShareBoard,
|
||||
model.PermissionManageBoardCards,
|
||||
model.PermissionManageBoardProperties,
|
||||
}
|
||||
|
||||
th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
}
|
86
server/services/permissions/mmpermissions/helpers_test.go
Normal file
86
server/services/permissions/mmpermissions/helpers_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mmpermissions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
mmpermissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mmpermissions/mocks"
|
||||
permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
t *testing.T
|
||||
ctrl *gomock.Controller
|
||||
store *permissionsMocks.MockStore
|
||||
api *mmpermissionsMocks.MockAPI
|
||||
permissions *Service
|
||||
}
|
||||
|
||||
func SetupTestHelper(t *testing.T) *TestHelper {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := permissionsMocks.NewMockStore(ctrl)
|
||||
mockAPI := mmpermissionsMocks.NewMockAPI(ctrl)
|
||||
|
||||
return &TestHelper{
|
||||
t: t,
|
||||
ctrl: ctrl,
|
||||
store: mockStore,
|
||||
api: mockAPI,
|
||||
permissions: New(mockStore, mockAPI),
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, teamID string,
|
||||
hasPermissionTo, hasNotPermissionTo []*mmModel.Permission) {
|
||||
for _, p := range hasPermissionTo {
|
||||
th.t.Run(roleName+" "+p.Id, func(t *testing.T) {
|
||||
th.store.EXPECT().
|
||||
GetBoard(member.BoardID).
|
||||
Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil).
|
||||
Times(1)
|
||||
|
||||
th.api.EXPECT().
|
||||
HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
th.store.EXPECT().
|
||||
GetMemberForBoard(member.BoardID, member.UserID).
|
||||
Return(member, nil).
|
||||
Times(1)
|
||||
|
||||
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
}
|
||||
|
||||
for _, p := range hasNotPermissionTo {
|
||||
th.t.Run(roleName+" "+p.Id, func(t *testing.T) {
|
||||
th.store.EXPECT().
|
||||
GetBoard(member.BoardID).
|
||||
Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil).
|
||||
Times(1)
|
||||
|
||||
th.api.EXPECT().
|
||||
HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
th.store.EXPECT().
|
||||
GetMemberForBoard(member.BoardID, member.UserID).
|
||||
Return(member, nil).
|
||||
Times(1)
|
||||
|
||||
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user