mirror of
https://github.com/mattermost/focalboard.git
synced 2024-11-19 20:32:00 +02:00
[MM-59253]: Remove plugin code from focalboard repo (#5027)
* refactor: updated mysql docker image version * refactor: removed isPlugin code from webapp * refactor: removed isFocalboardPlugin from test * refactor: removed package-lock.json from root * removed unnecessary component * nit: removed comments * reverted the mysql docker version to original * nit * revert setting.json changes * Removed `mattermost-plugin` folder (#5029) * refactor: removed mattermost-plugin folder * reverted the mysql image version * Removed `mattermost-plugin` from make rules (#5030) * removed mattermost-plugin from make rules * removed: mattermost-plugin code from the repo * updated snapshot and fix test (#5031) * updated snapshot and fix test * Updated snapshot and removed unnecessary tests * chore: minor fix ci * refactor: updated the mac-os version supported by github actions * reverted: mac os version * refactor: updated mac os version and also changed docker-compose to docker compose * removed version from docker compose as no long needed * reverted mysql docker version * updated mysql version * testing * revert testing * refactor: added version for mysql docker compose file * test: test commit * updated snapshot and fix test (#5032) * removed mattermost-plugin from make rules * removed: mattermost-plugin code from the repo * updated snapshot and fix test * Updated snapshot and removed unnecessary tests * chore: minor fix ci * refactor: removed isplugin code from server * final attempt * ci: Minor ci tweaks and upgrades --------- Co-authored-by: Antonis Stamatiou <stamatiou.antonis@gmail.com> * linter fixes --------- Co-authored-by: Antonis Stamatiou <stamatiou.antonis@gmail.com>
This commit is contained in:
parent
1932acb628
commit
bfaa37fc24
@ -5,7 +5,6 @@ node_modules
|
||||
.github/
|
||||
mac/
|
||||
win-wpf/
|
||||
mattermost-plugin/
|
||||
website/
|
||||
linux/
|
||||
go.work
|
||||
|
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@ -15,7 +15,7 @@ env:
|
||||
jobs:
|
||||
|
||||
ci-ubuntu-server:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@ -27,38 +27,36 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21
|
||||
go-version-file: server/go.mod
|
||||
|
||||
- name: "Test server: ${{matrix['db']}}"
|
||||
run: cd focalboard; make server-test-${{matrix['db']}}
|
||||
run: make server-test-${{matrix['db']}}
|
||||
|
||||
ci-ubuntu-webapp:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: "focalboard"
|
||||
|
||||
- name: npm ci
|
||||
run: |
|
||||
cd focalboard/webapp && npm ci && cd -
|
||||
cd focalboard/mattermost-plugin/webapp && npm ci
|
||||
run: cd focalboard/webapp && npm ci && cd -
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21
|
||||
go-version-file: focalboard/server/go.mod
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.0
|
||||
node-version-file: focalboard/webapp/.nvmrc
|
||||
|
||||
- name: Build Linux server
|
||||
run: cd focalboard; make server-linux-package
|
||||
@ -85,20 +83,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: "focalboard"
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21
|
||||
go-version-file: focalboard/server/go.mod
|
||||
|
||||
- name: "Test server (minimum): ${{matrix['db']}}"
|
||||
run: cd focalboard; make server-test-mini-${{matrix['db']}}
|
||||
|
||||
ci-mac-server:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@ -107,14 +105,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: "focalboard"
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21
|
||||
go-version-file: focalboard/server/go.mod
|
||||
|
||||
- name: "Test server (minimum): ${{matrix['db']}}"
|
||||
run: cd focalboard; make server-test-mini-${{matrix['db']}}
|
||||
|
80
.github/workflows/dev-release.yml
vendored
80
.github/workflows/dev-release.yml
vendored
@ -22,16 +22,10 @@ jobs:
|
||||
|
||||
- name: Replace token 1 server
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 1 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
|
||||
- name: Replace token 2 server
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 2 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
|
||||
- name: npm ci
|
||||
run: cd focalboard/webapp; npm ci --no-optional
|
||||
|
||||
@ -72,7 +66,7 @@ jobs:
|
||||
path: ${{ github.workspace }}/focalboard/linux/dist/focalboard-linux.tar.gz
|
||||
|
||||
macos:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
|
||||
@ -83,15 +77,9 @@ jobs:
|
||||
- name: Replace token 1 server
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 1 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: Replace token 2 server
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 2 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: npm ci
|
||||
run: cd focalboard/webapp; npm ci --no-optional
|
||||
|
||||
@ -126,15 +114,6 @@ jobs:
|
||||
- name: Replace token 1 server
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 1 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: Replace token 2 server
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 2 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v1.1
|
||||
|
||||
@ -170,56 +149,3 @@ jobs:
|
||||
with:
|
||||
name: focalboard-win.zip
|
||||
path: ${{ github.workspace }}/focalboard/win-wpf/dist/focalboard-win.zip
|
||||
|
||||
plugin:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
|
||||
- name: Replace token 1 server
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 1 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: Replace token 2 server
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 2 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: npm ci
|
||||
run: cd focalboard/webapp; npm ci --no-optional
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.21
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.11.0
|
||||
|
||||
- name: Build webapp
|
||||
run: cd focalboard; make webapp
|
||||
|
||||
- name: npm ci plugin dependencies
|
||||
run: cd focalboard/mattermost-plugin/webapp; npm ci --no-optional
|
||||
|
||||
- name: Build plugin
|
||||
run: cd focalboard/mattermost-plugin; make dist
|
||||
env:
|
||||
BUILD_NUMBER: ${{ github.run_id }}
|
||||
|
||||
- name: Rename plugin file
|
||||
run: cd focalboard/mattermost-plugin/dist; mv focalboard-*.tar.gz mattermost-plugin-focalboard.tar.gz
|
||||
|
||||
- name: Upload plugin artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mattermost-plugin-focalboard.tar.gz
|
||||
path: ${{ github.workspace }}/focalboard/mattermost-plugin/dist/mattermost-plugin-focalboard.tar.gz
|
||||
|
75
.github/workflows/prod-release.yml
vendored
75
.github/workflows/prod-release.yml
vendored
@ -20,15 +20,9 @@ jobs:
|
||||
- name: Replace token 1 server
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 1 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: Replace token 2 server
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 2 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: npm ci
|
||||
run: cd focalboard/webapp; npm ci --no-optional
|
||||
|
||||
@ -69,7 +63,7 @@ jobs:
|
||||
path: ${{ github.workspace }}/focalboard/linux/dist/focalboard-linux.tar.gz
|
||||
|
||||
macos:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
|
||||
@ -81,15 +75,9 @@ jobs:
|
||||
- name: Replace token 1 server
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 1 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: Replace token 2 server
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 2 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: npm ci
|
||||
run: cd focalboard/webapp; npm ci --no-optional
|
||||
|
||||
@ -125,15 +113,9 @@ jobs:
|
||||
- name: Replace token 1 server
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 1 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: Replace token 2 server
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 2 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v1.1
|
||||
|
||||
@ -169,57 +151,4 @@ jobs:
|
||||
with:
|
||||
name: focalboard-win.zip
|
||||
path: ${{ github.workspace }}/focalboard/win-wpf/dist/focalboard-win.zip
|
||||
|
||||
plugin-release:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
|
||||
- name: Replace token 1 server
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 1 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: Replace token 2 server
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
|
||||
|
||||
- name: Replace token 2 webapp
|
||||
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/focalboard/mattermost-plugin/webapp/src/index.tsx
|
||||
|
||||
- name: npm ci
|
||||
run: cd focalboard/webapp; npm ci --no-optional
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.21
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.11.0
|
||||
|
||||
- name: Build webapp
|
||||
run: cd focalboard; make webapp
|
||||
|
||||
- name: npm ci plugin dependencies
|
||||
run: cd focalboard/mattermost-plugin/webapp && npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: cd focalboard/mattermost-plugin; make dist
|
||||
env:
|
||||
BUILD_NUMBER: ${{ github.run_id }}
|
||||
|
||||
- name: Rename plugin file
|
||||
run: cd focalboard/mattermost-plugin/dist; mv focalboard-*.tar.gz mattermost-plugin-focalboard.tar.gz
|
||||
|
||||
- name: Upload plugin artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mattermost-plugin-focalboard.tar.gz
|
||||
path: ${{ github.workspace }}/focalboard/mattermost-plugin/dist/mattermost-plugin-focalboard.tar.gz
|
||||
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -70,10 +70,7 @@ webapp/cypress/screenshots
|
||||
webapp/cypress/videos
|
||||
server/swagger/clients
|
||||
server/vendor
|
||||
mattermost-plugin/vendor
|
||||
mattermost-plugin/dist
|
||||
.idea
|
||||
docker/certs
|
||||
docker/data
|
||||
server/**/*.coverage
|
||||
mattermost-plugin/**/*.coverage
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -37,8 +37,7 @@
|
||||
"linux/dist/**": true,
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
// "source.organizeImports": true,
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
|
34
Makefile
34
Makefile
@ -35,7 +35,6 @@ all: webapp server ## Build server and webapp.
|
||||
|
||||
prebuild: ## Run prebuild actions (install dependencies etc.).
|
||||
cd webapp; npm install
|
||||
cd mattermost-plugin/webapp; npm install
|
||||
|
||||
ci: webapp-ci server-test ## Simulate CI, locally.
|
||||
|
||||
@ -108,7 +107,6 @@ server-lint: ## Run linters on server code.
|
||||
exit 1; \
|
||||
fi;
|
||||
cd server; golangci-lint run ./...
|
||||
cd mattermost-plugin; golangci-lint run ./...
|
||||
|
||||
modd-precheck:
|
||||
@if ! [ -x "$$(command -v modd)" ]; then \
|
||||
@ -144,13 +142,11 @@ server-test-mysql: export FOCALBOARD_STORE_TEST_DOCKER_PORT=44446
|
||||
|
||||
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
|
||||
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 -tags '$(BUILD_TAGS)' -race -v -coverpkg=./... -coverprofile=server-mysql-profile.coverage -count=1 -timeout=30m ./...
|
||||
cd server; go tool cover -func server-mysql-profile.coverage
|
||||
cd mattermost-plugin/server; go test -tags '$(BUILD_TAGS)' -race -v -coverpkg=./... -coverprofile=plugin-mysql-profile.coverage -count=1 -timeout=30m ./...
|
||||
cd mattermost-plugin/server; go tool cover -func plugin-mysql-profile.coverage
|
||||
docker-compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans
|
||||
docker compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans
|
||||
|
||||
server-test-mariadb: export FOCALBOARD_UNIT_TESTING=1
|
||||
server-test-mariadb: export FOCALBOARD_STORE_TEST_DB_TYPE=mariadb
|
||||
@ -158,13 +154,11 @@ server-test-mariadb: export FOCALBOARD_STORE_TEST_DOCKER_PORT=44445
|
||||
|
||||
server-test-mariadb: templates-archive ## Run server tests using mysql
|
||||
@echo Starting docker container for mariadb
|
||||
docker-compose -f ./docker-testing/docker-compose-mariadb.yml down -v --remove-orphans
|
||||
docker-compose -f ./docker-testing/docker-compose-mariadb.yml run start_dependencies
|
||||
docker compose -f ./docker-testing/docker-compose-mariadb.yml down -v --remove-orphans
|
||||
docker compose -f ./docker-testing/docker-compose-mariadb.yml run start_dependencies
|
||||
cd server; go test -tags '$(BUILD_TAGS)' -race -v -coverpkg=./... -coverprofile=server-mariadb-profile.coverage -count=1 -timeout=30m ./...
|
||||
cd server; go tool cover -func server-mariadb-profile.coverage
|
||||
cd mattermost-plugin/server; go test -tags '$(BUILD_TAGS)' -race -v -coverpkg=./... -coverprofile=plugin-mariadb-profile.coverage -count=1 -timeout=30m ./...
|
||||
cd mattermost-plugin/server; go tool cover -func plugin-mariadb-profile.coverage
|
||||
docker-compose -f ./docker-testing/docker-compose-mariadb.yml down -v --remove-orphans
|
||||
docker compose -f ./docker-testing/docker-compose-mariadb.yml down -v --remove-orphans
|
||||
|
||||
server-test-postgres: export FOCALBOARD_UNIT_TESTING=1
|
||||
server-test-postgres: export FOCALBOARD_STORE_TEST_DB_TYPE=postgres
|
||||
@ -172,33 +166,23 @@ server-test-postgres: export FOCALBOARD_STORE_TEST_DOCKER_PORT=44447
|
||||
|
||||
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
|
||||
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 -tags '$(BUILD_TAGS)' -race -v -coverpkg=./... -coverprofile=server-postgres-profile.coverage -count=1 -timeout=30m ./...
|
||||
cd server; go tool cover -func server-postgres-profile.coverage
|
||||
cd mattermost-plugin/server; go test -tags '$(BUILD_TAGS)' -race -v -coverpkg=./... -coverprofile=plugin-postgres-profile.coverage -count=1 -timeout=30m ./...
|
||||
cd mattermost-plugin/server; go tool cover -func plugin-postgres-profile.coverage
|
||||
docker-compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans
|
||||
docker compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans
|
||||
|
||||
webapp: ## Build webapp.
|
||||
cd webapp; npm run pack
|
||||
|
||||
webapp-ci: ## Webapp CI: linting & testing.
|
||||
cd webapp; npm run check
|
||||
cd mattermost-plugin/webapp; npm run lint
|
||||
cd webapp; npm run test
|
||||
cd mattermost-plugin/webapp; npm run test
|
||||
cd webapp; npm run cypress:ci
|
||||
|
||||
webapp-test: ## jest tests for webapp
|
||||
cd webapp; npm run test
|
||||
|
||||
watch-plugin: modd-precheck ## Run and upload the plugin to a development server
|
||||
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
|
||||
|
||||
mac-app: server-mac webapp ## Build Mac application.
|
||||
rm -rf mac/temp
|
||||
rm -rf mac/dist
|
||||
|
@ -1,12 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
plugin-ci: mattermost/plugin-ci@0.1.0
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
ci:
|
||||
jobs:
|
||||
- plugin-ci/lint
|
||||
- plugin-ci/test
|
||||
- plugin-ci/build
|
@ -1,27 +0,0 @@
|
||||
# http://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[*.{js, jsx, ts, tsx, json, html}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[webapp/package.json]
|
||||
indent_size = 2
|
||||
|
||||
[{Makefile, *.mk}]
|
||||
indent_style = tab
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
1
mattermost-plugin/.gitattributes
vendored
1
mattermost-plugin/.gitattributes
vendored
@ -1 +0,0 @@
|
||||
server/manifest.go linguist-generated=true
|
@ -1,81 +0,0 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: readonly
|
||||
skip-files:
|
||||
- product/boards_product.go
|
||||
|
||||
linters-settings:
|
||||
gofmt:
|
||||
simplify: true
|
||||
goimports:
|
||||
local-prefixes: github.com/mattermost/mattermost-starter-template
|
||||
golint:
|
||||
min-confidence: 0
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 150
|
||||
revive:
|
||||
enableAllRules: true
|
||||
rules:
|
||||
- name: exported
|
||||
disabled: true
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
- ineffassign
|
||||
- unparam
|
||||
- errcheck
|
||||
- govet
|
||||
- bodyclose
|
||||
- durationcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- gosec
|
||||
- makezero
|
||||
- staticcheck
|
||||
- prealloc
|
||||
- asciicheck
|
||||
- dogsled
|
||||
- dupl
|
||||
- goconst
|
||||
- gocritic
|
||||
- godot
|
||||
- err113
|
||||
- goheader
|
||||
- revive
|
||||
- nakedret
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosimple
|
||||
- lll
|
||||
- misspell
|
||||
- nolintlint
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- whitespace
|
||||
- gocyclo
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: server/manifest.go
|
||||
linters:
|
||||
- unused
|
||||
- path: server/configuration.go
|
||||
linters:
|
||||
- unused
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- bodyclose
|
||||
- scopelint # https://github.com/kyoh86/scopelint/issues/4
|
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,328 +0,0 @@
|
||||
# Build Flags
|
||||
BUILD_NUMBER ?= $(BUILD_NUMBER:)
|
||||
BUILD_DATE = $(shell date -u)
|
||||
BUILD_HASH = $(shell git rev-parse HEAD)
|
||||
# If we don't set the build number it defaults to dev
|
||||
ifeq ($(BUILD_NUMBER),)
|
||||
BUILD_NUMBER := dev
|
||||
BUILD_DATE := n/a
|
||||
endif
|
||||
|
||||
MM_SERVER_PATH ?= $(MM_SERVER_PATH:)
|
||||
ifeq ($(MM_SERVER_PATH),)
|
||||
MM_SERVER_PATH := ../../mattermost-server
|
||||
endif
|
||||
|
||||
|
||||
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)"
|
||||
LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=plugin"
|
||||
|
||||
GO ?= $(shell command -v go 2> /dev/null)
|
||||
NPM ?= $(shell command -v npm 2> /dev/null)
|
||||
CURL ?= $(shell command -v curl 2> /dev/null)
|
||||
MM_DEBUG ?=
|
||||
MANIFEST_FILE ?= plugin.json
|
||||
GOPATH ?= $(shell go env GOPATH)
|
||||
GO_TEST_FLAGS ?= -race
|
||||
GO_BUILD_FLAGS ?= -ldflags '$(LDFLAGS)'
|
||||
MM_UTILITIES_DIR ?= ../mattermost-utilities
|
||||
DLV_DEBUG_PORT := 2346
|
||||
MATTERMOST_PLUGINS_PATH=$(MM_SERVER_PATH)/plugins
|
||||
FOCALBOARD_PLUGIN_PATH=$(MATTERMOST_PLUGINS_PATH)/focalboard
|
||||
|
||||
export GO111MODULE=on
|
||||
|
||||
# You can include assets this directory into the bundle. This can be e.g. used to include profile pictures.
|
||||
ASSETS_DIR ?= assets
|
||||
|
||||
## Define the default target (make all)
|
||||
.PHONY: default
|
||||
default: all
|
||||
|
||||
# Verify environment, and define PLUGIN_ID, PLUGIN_VERSION, HAS_SERVER and HAS_WEBAPP as needed.
|
||||
include build/setup.mk
|
||||
|
||||
BUNDLE_NAME ?= $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz
|
||||
|
||||
# Include custom makefile, if present
|
||||
ifneq ($(wildcard build/custom.mk),)
|
||||
include build/custom.mk
|
||||
endif
|
||||
|
||||
## Checks the code style, tests, builds and bundles the plugin.
|
||||
.PHONY: all
|
||||
all: check-style test dist
|
||||
|
||||
## Propagates plugin manifest information into the server/ and webapp/ folders.
|
||||
.PHONY: apply
|
||||
apply:
|
||||
./build/bin/manifest apply
|
||||
|
||||
## Runs eslint and golangci-lint
|
||||
.PHONY: check-style
|
||||
check-style: webapp/node_modules
|
||||
@echo Checking for style guide compliance
|
||||
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
cd webapp && npm run lint
|
||||
cd webapp && npm run check-types
|
||||
endif
|
||||
|
||||
ifneq ($(HAS_SERVER),)
|
||||
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
|
||||
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install-golangci-lint for installation instructions."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
|
||||
@echo Running golangci-lint
|
||||
golangci-lint run ./...
|
||||
endif
|
||||
|
||||
templates-archive: ## Build templates archive file
|
||||
cd ../server/assets/build-template-archive; go run -tags '$(BUILD_TAGS)' main.go --dir="../templates-boardarchive" --out="../templates.boardarchive"
|
||||
|
||||
## Builds the server, if it exists, for all supported architectures.
|
||||
.PHONY: server
|
||||
server: templates-archive
|
||||
ifneq ($(HAS_SERVER),)
|
||||
mkdir -p server/dist;
|
||||
ifeq ($(MM_DEBUG),)
|
||||
cd server && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -trimpath -o dist/plugin-linux-amd64;
|
||||
cd server && env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build $(GO_BUILD_FLAGS) -trimpath -o dist/plugin-linux-arm64;
|
||||
cd server && env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -trimpath -o dist/plugin-darwin-amd64;
|
||||
cd server && env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) build $(GO_BUILD_FLAGS) -trimpath -o dist/plugin-darwin-arm64;
|
||||
cd server && env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -trimpath -o dist/plugin-windows-amd64.exe;
|
||||
else
|
||||
$(info DEBUG mode is on; to disable, unset MM_DEBUG)
|
||||
|
||||
cd server && env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -trimpath -o dist/plugin-darwin-amd64;
|
||||
cd server && env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -trimpath -o dist/plugin-darwin-arm64;
|
||||
cd server && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -trimpath -o dist/plugin-linux-amd64;
|
||||
cd server && env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -trimpath -o dist/plugin-linux-arm64;
|
||||
cd server && env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -trimpath -o dist/plugin-windows-amd64.exe;
|
||||
endif
|
||||
endif
|
||||
|
||||
## Ensures NPM dependencies are installed without having to run this all the time.
|
||||
webapp/node_modules: $(wildcard webapp/package.json)
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
cd webapp && $(NPM) install
|
||||
touch $@
|
||||
endif
|
||||
|
||||
## Builds the webapp, if it exists.
|
||||
.PHONY: webapp
|
||||
webapp: webapp/node_modules
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
ifeq ($(MM_DEBUG),)
|
||||
cd webapp && $(NPM) run build;
|
||||
else
|
||||
cd webapp && $(NPM) run debug;
|
||||
endif
|
||||
endif
|
||||
|
||||
## Generates a tar bundle of the plugin for install.
|
||||
.PHONY: bundle
|
||||
bundle:
|
||||
rm -rf dist/
|
||||
mkdir -p dist/$(PLUGIN_ID)
|
||||
cp $(MANIFEST_FILE) dist/$(PLUGIN_ID)/
|
||||
cp -r ../webapp/pack dist/$(PLUGIN_ID)/
|
||||
ifneq ($(wildcard $(ASSETS_DIR)/.),)
|
||||
cp -r $(ASSETS_DIR) dist/$(PLUGIN_ID)/
|
||||
endif
|
||||
ifneq ($(HAS_PUBLIC),)
|
||||
cp -r public dist/$(PLUGIN_ID)/public/
|
||||
endif
|
||||
ifneq ($(HAS_SERVER),)
|
||||
mkdir -p dist/$(PLUGIN_ID)/server
|
||||
cp -r server/dist dist/$(PLUGIN_ID)/server/
|
||||
endif
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
mkdir -p dist/$(PLUGIN_ID)/webapp
|
||||
cp -r webapp/dist dist/$(PLUGIN_ID)/webapp/
|
||||
endif
|
||||
cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)
|
||||
|
||||
@echo plugin built at: dist/$(BUNDLE_NAME)
|
||||
|
||||
## Builds and bundles the plugin.
|
||||
.PHONY: dist
|
||||
dist: apply server webapp bundle
|
||||
|
||||
## Builds and installs the plugin to a server.
|
||||
.PHONY: deploy
|
||||
deploy: dist
|
||||
./build/bin/pluginctl deploy $(PLUGIN_ID) dist/$(BUNDLE_NAME)
|
||||
|
||||
## Builds and installs the plugin to a server, updating the webapp automatically when changed.
|
||||
.PHONY: watch
|
||||
watch: apply server bundle
|
||||
ifeq ($(MM_DEBUG),)
|
||||
cd webapp && $(NPM) run build:watch
|
||||
else
|
||||
cd webapp && $(NPM) run debug:watch
|
||||
endif
|
||||
|
||||
## Installs a previous built plugin with updated webpack assets to a server.
|
||||
.PHONY: deploy-from-watch
|
||||
deploy-from-watch: bundle
|
||||
./build/bin/pluginctl deploy $(PLUGIN_ID) dist/$(BUNDLE_NAME)
|
||||
|
||||
## Setup dlv for attaching, identifying the plugin PID for other targets.
|
||||
.PHONY: setup-attach
|
||||
setup-attach:
|
||||
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
|
||||
$(eval NUM_PID := $(shell echo -n ${PLUGIN_PID} | wc -w))
|
||||
|
||||
@if [ ${NUM_PID} -gt 2 ]; then \
|
||||
echo "** There is more than 1 plugin process running. Run 'make kill reset' to restart just one."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
## Check if setup-attach succeeded.
|
||||
.PHONY: check-attach
|
||||
check-attach:
|
||||
@if [ -z ${PLUGIN_PID} ]; then \
|
||||
echo "Could not find plugin PID; the plugin is not running. Exiting."; \
|
||||
exit 1; \
|
||||
else \
|
||||
echo "Located Plugin running with PID: ${PLUGIN_PID}"; \
|
||||
fi
|
||||
|
||||
## Attach dlv to an existing plugin instance.
|
||||
.PHONY: attach
|
||||
attach: setup-attach check-attach
|
||||
dlv attach ${PLUGIN_PID}
|
||||
|
||||
## Attach dlv to an existing plugin instance, exposing a headless instance on $DLV_DEBUG_PORT.
|
||||
.PHONY: attach-headless
|
||||
attach-headless: setup-attach check-attach
|
||||
dlv attach ${PLUGIN_PID} --listen :$(DLV_DEBUG_PORT) --headless=true --api-version=2 --accept-multiclient
|
||||
|
||||
## Detach dlv from an existing plugin instance, if previously attached.
|
||||
.PHONY: detach
|
||||
detach: setup-attach
|
||||
@DELVE_PID=$(shell ps aux | grep "dlv attach ${PLUGIN_PID}" | grep -v "grep" | awk -F " " '{print $$2}') && \
|
||||
if [ "$$DELVE_PID" -gt 0 ] > /dev/null 2>&1 ; then \
|
||||
echo "Located existing delve process running with PID: $$DELVE_PID. Killing." ; \
|
||||
kill -9 $$DELVE_PID ; \
|
||||
fi
|
||||
|
||||
## Runs any lints and unit tests defined for the server and webapp, if they exist.
|
||||
.PHONY: test
|
||||
test: export FOCALBOARD_UNIT_TESTING=1
|
||||
test: webapp/node_modules
|
||||
ifneq ($(HAS_SERVER),)
|
||||
$(GO) test -v $(GO_TEST_FLAGS) ./server/...
|
||||
endif
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
cd webapp && $(NPM) run test;
|
||||
endif
|
||||
ifneq ($(wildcard ./build/sync/plan/.),)
|
||||
cd ./build/sync && $(GO) test -v $(GO_TEST_FLAGS) ./...
|
||||
endif
|
||||
|
||||
## Creates a coverage report for the server code.
|
||||
.PHONY: coverage
|
||||
coverage: webapp/node_modules
|
||||
ifneq ($(HAS_SERVER),)
|
||||
$(GO) test $(GO_TEST_FLAGS) -coverprofile=server/coverage.txt ./server/...
|
||||
$(GO) tool cover -html=server/coverage.txt
|
||||
endif
|
||||
|
||||
## Extract strings for translation from the source code.
|
||||
.PHONY: i18n-extract
|
||||
i18n-extract:
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
ifeq ($(HAS_MM_UTILITIES),)
|
||||
@echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
|
||||
else
|
||||
cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-webapp --webapp-dir $(PWD)/webapp
|
||||
endif
|
||||
endif
|
||||
|
||||
## Disable the plugin.
|
||||
.PHONY: disable
|
||||
disable: detach
|
||||
./build/bin/pluginctl disable $(PLUGIN_ID)
|
||||
|
||||
## Enable the plugin.
|
||||
.PHONY: enable
|
||||
enable:
|
||||
./build/bin/pluginctl enable $(PLUGIN_ID)
|
||||
|
||||
## Reset the plugin, effectively disabling and re-enabling it on the server.
|
||||
.PHONY: reset
|
||||
reset: detach
|
||||
./build/bin/pluginctl reset $(PLUGIN_ID)
|
||||
|
||||
## Kill all instances of the plugin, detaching any existing dlv instance.
|
||||
.PHONY: kill
|
||||
kill: detach
|
||||
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
|
||||
|
||||
@for PID in ${PLUGIN_PID}; do \
|
||||
echo "Killing plugin pid $$PID"; \
|
||||
kill -9 $$PID; \
|
||||
done; \
|
||||
|
||||
## Clean removes all build artifacts.
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -fr dist/
|
||||
ifneq ($(HAS_SERVER),)
|
||||
rm -fr server/coverage.txt
|
||||
rm -fr server/dist
|
||||
endif
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
rm -fr webapp/junit.xml
|
||||
rm -fr webapp/dist
|
||||
rm -fr webapp/node_modules
|
||||
endif
|
||||
rm -fr build/bin/
|
||||
|
||||
## Sync directory with a starter template
|
||||
sync:
|
||||
ifndef STARTERTEMPLATE_PATH
|
||||
@echo STARTERTEMPLATE_PATH is not set.
|
||||
@echo Set STARTERTEMPLATE_PATH to a local clone of https://github.com/mattermost/mattermost-plugin-starter-template and retry.
|
||||
@exit 1
|
||||
endif
|
||||
cd ${STARTERTEMPLATE_PATH} && go run ./build/sync/main.go ./build/sync/plan.yml $(PWD)
|
||||
|
||||
## Watch webapp and server changes and redeploy locally using local filesystem (MM_SERVER_PATH)
|
||||
.PHONY: live-watch
|
||||
live-watch:
|
||||
make -j2 live-watch-server live-watch-webapp
|
||||
|
||||
## Watch server changes and redeploy locally using local filesystem (MM_SERVER_PATH)
|
||||
.PHONY: live-watch-server
|
||||
live-watch-server: apply
|
||||
cd ../ && modd -f mattermost-plugin/modd.conf
|
||||
|
||||
## Watch webapp changes and redeploy locally using local filesystem (MM_SERVER_PATH)
|
||||
.PHONY: live-watch-webapp
|
||||
live-watch-webapp: apply
|
||||
cd webapp && $(NPM) run live-watch
|
||||
|
||||
.PHONY: deploy-to-mattermost-directory
|
||||
deploy-to-mattermost-directory:
|
||||
./build/bin/pluginctl disable $(PLUGIN_ID)
|
||||
mkdir -p $(FOCALBOARD_PLUGIN_PATH)
|
||||
cp $(MANIFEST_FILE) $(FOCALBOARD_PLUGIN_PATH)/
|
||||
cp -r ../webapp/pack $(FOCALBOARD_PLUGIN_PATH)/
|
||||
cp -r $(ASSETS_DIR) $(FOCALBOARD_PLUGIN_PATH)/
|
||||
cp -r public $(FOCALBOARD_PLUGIN_PATH)/
|
||||
mkdir -p $(FOCALBOARD_PLUGIN_PATH)/server
|
||||
cp -r server/dist $(FOCALBOARD_PLUGIN_PATH)/server/
|
||||
mkdir -p $(FOCALBOARD_PLUGIN_PATH)/webapp
|
||||
cp -r webapp/dist $(FOCALBOARD_PLUGIN_PATH)/webapp/
|
||||
./build/bin/pluginctl enable $(PLUGIN_ID)
|
||||
@echo plugin built at: $(FOCALBOARD_PLUGIN_PATH)
|
||||
|
||||
# Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
@cat Makefile build/*.mk | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//" | sed -e "s/^## //" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort
|
@ -1,7 +0,0 @@
|
||||
# Mattermost Boards (Focalboard Plugin)
|
||||
|
||||
**[Mattermost Boards](https://mattermost.com/boards/)** is the Mattermost plugin version of Focalboard that combines project management tools with messaging and collaboration for teams of all sizes. To access and use **Mattermost Boards**, install or upgrade to Mattermost v6.0 or later as a [self-hosted server](https://docs.mattermost.com/guides/deployment.html?utm_source=focalboard&utm_campaign=focalboard) or [Cloud server](https://mattermost.com/get-started/?utm_source=focalboard&utm_campaign=focalboard). After logging into Mattermost, select the menu in the top left corner of Mattermost and select **Boards**.
|
||||
|
||||
***Mattermost Boards** is installed and enabled by default in Mattermost v6.0 and later.*
|
||||
|
||||
To build your own version of Matterboard Boards and upload it to your own Mattermost server, follow the instructions [here](https://developers.mattermost.com/contribute/focalboard/mattermost-boards-setup-guide/).
|
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="241px" height="240px" viewBox="0 0 241 240" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>blue-icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="06" transform="translate(-681.000000, -572.000000)" fill="#1875F0">
|
||||
<g id="Group-2" transform="translate(626.000000, 517.000000)">
|
||||
<path d="M216.908181,153.127705 C216.908181,153.127705 217.280588,169.452526 205.928754,180.543035 C194.57546,191.633544 180.631383,190.619887 171.560722,187.557072 C162.488602,184.494256 150.79503,176.85251 148.531381,161.16705 C146.269193,145.480133 156.508188,132.736607 156.508188,132.736607 L178.820463,105.066407 L191.815268,89.2629779 L202.969946,75.4912313 C202.969946,75.4912313 208.088713,68.6534193 209.547671,67.2421648 C209.836834,66.9625354 210.133299,66.7790286 210.423923,66.6377576 L210.635683,66.5299837 L210.673654,66.5154197 C211.28703,66.2518108 211.993873,66.195011 212.675888,66.4251227 C213.343299,66.6508652 213.860288,67.1081757 214.187421,67.6718037 L214.256061,67.7810339 L214.315938,67.9062846 C214.475124,68.2063036 214.608022,68.5485583 214.67082,68.9709151 C214.968745,70.976382 214.870897,79.5094471 214.870897,79.5094471 L215.342613,97.2047434 L216.039232,117.630795 L216.908181,153.127705 Z M245.790587,78.2043261 C287.057212,108.155253 305.982915,162.509669 288.774288,213.346872 C267.594104,275.911031 199.706245,309.46073 137.142925,288.281718 C74.5796048,267.10125 41.031812,199.213937 62.2105402,136.649778 C79.4482947,85.7295603 127.625459,54.0324057 178.690632,55.4145322 L162.322339,74.7541074 C132.028106,80.231639 105.87146,100.919843 95.5908489,131.290215 C80.2944535,176.475117 105.932628,225.982624 152.855846,241.866155 C199.777608,257.751142 250.216536,233.998666 265.512932,188.813764 C275.760046,158.543884 267.634882,126.336988 247.050359,103.595256 L245.790587,78.2043261 Z" id="blue-icon"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.2 KiB |
1
mattermost-plugin/build/.gitignore
vendored
1
mattermost-plugin/build/.gitignore
vendored
@ -1 +0,0 @@
|
||||
bin
|
@ -1 +0,0 @@
|
||||
# Include custom targets and environment variables here
|
@ -1,84 +0,0 @@
|
||||
module github.com/mattermost/mattermost-plugin-starter-template/build
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.8
|
||||
|
||||
require (
|
||||
github.com/go-git/go-git/v5 v5.1.0
|
||||
github.com/mattermost/mattermost/server/public v0.1.3
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
google.golang.org/grpc v1.62.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.0.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/graph-gophers/graphql-go v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.13 // indirect
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
|
||||
github.com/mattermost/logr/v2 v2.0.21 // indirect
|
||||
github.com/mattermost/mattermost/server/public v0.1.3
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.28 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pborman/uuid v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tinylib/msgp v1.1.9 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wiggin77/merror v1.0.5 // indirect
|
||||
github.com/wiggin77/srslog v1.0.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.2.1 // indirect
|
||||
golang.org/x/crypto v0.20.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
@ -1,424 +0,0 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64=
|
||||
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.2-0.20191121212151-29be175fc3a3/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
|
||||
github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk=
|
||||
github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.0.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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/graph-gophers/graphql-go v1.4.0 h1:JE9wveRTSXwJyjdRd6bOQ7Ob5bewTUQ58Jv4OiVdpdE=
|
||||
github.com/graph-gophers/graphql-go v1.4.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
|
||||
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
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.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
|
||||
github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.13 h1:1XxvOiqXZ8SULZUKim/wncr3wZ38H4yCuVDvKdK9OGs=
|
||||
github.com/klauspost/cpuid/v2 v2.0.13/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8=
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34=
|
||||
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d h1:/RJ/UV7M5c7L2TQ0KNm4yZxxFvC1nvRz/gY/Daa35aI=
|
||||
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ=
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI=
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI=
|
||||
github.com/mattermost/logr/v2 v2.0.15 h1:+WNbGcsc3dBao65eXlceB6dTILNJRIrvubnsTl3zBew=
|
||||
github.com/mattermost/logr/v2 v2.0.15/go.mod h1:mpPp935r5dIkFDo2y9Q87cQWhFR/4xXpNh0k/y8Hmwg=
|
||||
github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4=
|
||||
github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20220802151854-f07c31c5d933 h1:h7EibO8cwWeK8dLhC/A5tKGbkYSuJKZ0+2EXW7jDHoA=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20220802151854-f07c31c5d933/go.mod h1:otnBnKY9Y0eNkUKeD161de+BUBlESwANTnrkPT/392Y=
|
||||
github.com/mattermost/mattermost/server/public v0.1.3 h1:A3hQ3rNCwHfKAVxe7Hk3Zd9p2pyUKRmxtRPnkWP5SFM=
|
||||
github.com/mattermost/mattermost/server/public v0.1.3/go.mod h1:PDPb/iqzJJ5ZvK/m70oDF55AXN/cOvVFj96Yu4e6j+Q=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.28 h1:VMr3K5qGIEt+/KW3poopRh8mzi5RwuCjmrmstK196Fg=
|
||||
github.com/minio/minio-go/v7 v7.0.28/go.mod h1:x81+AX5gHSfCSqw7jxRKHvxUXMlE5uKX0Vb75Xk5yYg=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
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=
|
||||
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
|
||||
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
|
||||
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||
github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
|
||||
github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/wiggin77/merror v1.0.2/go.mod h1:uQTcIU0Z6jRK4OwqganPYerzQxSFJ4GSHM3aurxxQpg=
|
||||
github.com/wiggin77/merror v1.0.3 h1:8+ZHV+aSnJoYghE3EUThl15C6rvF2TYRSvOSBjdmNR8=
|
||||
github.com/wiggin77/merror v1.0.3/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0=
|
||||
github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME=
|
||||
github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0=
|
||||
github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8=
|
||||
github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
|
||||
go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
|
||||
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 h1:0qjDla5xICC2suMtyRH/QqX3B1btXTfNsIt/i4LFgO0=
|
||||
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098 h1:PgOr27OhUx2IRqGJ2RxAWI4dJQ7bi9cSrB82uzFzfUA=
|
||||
golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
|
||||
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
|
||||
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
@ -1,126 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const pluginIDGoFileTemplate = `// This file is automatically generated. Do not modify it manually.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
var manifest *model.Manifest
|
||||
|
||||
const manifestStr = ` + "`" + `
|
||||
%s
|
||||
` + "`" + `
|
||||
|
||||
func init() {
|
||||
_ = json.NewDecoder(strings.NewReader(manifestStr)).Decode(&manifest)
|
||||
}
|
||||
`
|
||||
|
||||
func main() {
|
||||
if len(os.Args) <= 1 {
|
||||
panic("no cmd specified")
|
||||
}
|
||||
|
||||
manifest, err := findManifest()
|
||||
if err != nil {
|
||||
panic("failed to find manifest: " + err.Error())
|
||||
}
|
||||
|
||||
cmd := os.Args[1]
|
||||
switch cmd {
|
||||
case "id":
|
||||
dumpPluginID(manifest)
|
||||
|
||||
case "version":
|
||||
dumpPluginVersion(manifest)
|
||||
|
||||
case "has_server":
|
||||
if manifest.HasServer() {
|
||||
fmt.Printf("true")
|
||||
}
|
||||
|
||||
case "has_webapp":
|
||||
if manifest.HasWebapp() {
|
||||
fmt.Printf("true")
|
||||
}
|
||||
|
||||
case "apply":
|
||||
if err := applyManifest(manifest); err != nil {
|
||||
panic("failed to apply manifest: " + err.Error())
|
||||
}
|
||||
|
||||
default:
|
||||
panic("unrecognized command: " + cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func findManifest() (*model.Manifest, error) {
|
||||
_, manifestFilePath, err := model.FindManifest(".")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find manifest in current working directory")
|
||||
}
|
||||
manifestFile, err := os.Open(manifestFilePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to open %s", manifestFilePath)
|
||||
}
|
||||
defer manifestFile.Close()
|
||||
|
||||
// Re-decode the manifest, disallowing unknown fields. When we write the manifest back out,
|
||||
// we don't want to accidentally clobber anything we won't preserve.
|
||||
var manifest model.Manifest
|
||||
decoder := json.NewDecoder(manifestFile)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err = decoder.Decode(&manifest); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse manifest")
|
||||
}
|
||||
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
// dumpPluginId writes the plugin id from the given manifest to standard out
|
||||
func dumpPluginID(manifest *model.Manifest) {
|
||||
fmt.Printf("%s", manifest.Id)
|
||||
}
|
||||
|
||||
// dumpPluginVersion writes the plugin version from the given manifest to standard out
|
||||
func dumpPluginVersion(manifest *model.Manifest) {
|
||||
fmt.Printf("%s", manifest.Version)
|
||||
}
|
||||
|
||||
// applyManifest propagates the plugin_id into the server and webapp folders, as necessary
|
||||
func applyManifest(manifest *model.Manifest) error {
|
||||
if manifest.HasServer() {
|
||||
// generate JSON representation of Manifest.
|
||||
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifestStr := string(manifestBytes)
|
||||
|
||||
// write generated code to file by using Go file template.
|
||||
if err := os.WriteFile(
|
||||
"server/manifest.go",
|
||||
[]byte(fmt.Sprintf(pluginIDGoFileTemplate, manifestStr)),
|
||||
0600,
|
||||
); err != nil {
|
||||
return errors.Wrap(err, "failed to write server/manifest.go")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
// main handles deployment of the plugin to a development server using the Client4 API.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
const helpText = `
|
||||
Usage:
|
||||
pluginctl deploy <plugin id> <bundle path>
|
||||
pluginctl disable <plugin id>
|
||||
pluginctl enable <plugin id>
|
||||
pluginctl reset <plugin id>
|
||||
`
|
||||
|
||||
func main() {
|
||||
err := pluginctl()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed: %s\n", err.Error())
|
||||
fmt.Print(helpText)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func pluginctl() error {
|
||||
if len(os.Args) < 3 {
|
||||
return errors.New("invalid number of arguments")
|
||||
}
|
||||
|
||||
client, err := getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "deploy":
|
||||
if len(os.Args) < 4 {
|
||||
return errors.New("invalid number of arguments")
|
||||
}
|
||||
return deploy(client, os.Args[2], os.Args[3])
|
||||
case "disable":
|
||||
return disablePlugin(client, os.Args[2])
|
||||
case "enable":
|
||||
return enablePlugin(client, os.Args[2])
|
||||
case "reset":
|
||||
return resetPlugin(client, os.Args[2])
|
||||
default:
|
||||
return errors.New("invalid second argument")
|
||||
}
|
||||
}
|
||||
|
||||
func getClient() (*model.Client4, error) {
|
||||
socketPath := os.Getenv("MM_LOCALSOCKETPATH")
|
||||
if socketPath == "" {
|
||||
socketPath = model.LocalModeSocketPath
|
||||
}
|
||||
|
||||
client, connected := getUnixClient(socketPath)
|
||||
if connected {
|
||||
log.Printf("Connecting using local mode over %s", socketPath)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
if os.Getenv("MM_LOCALSOCKETPATH") != "" {
|
||||
log.Printf("No socket found at %s for local mode deployment. Attempting to authenticate with credentials.", socketPath)
|
||||
}
|
||||
|
||||
siteURL := os.Getenv("MM_SERVICESETTINGS_SITEURL")
|
||||
adminToken := os.Getenv("MM_ADMIN_TOKEN")
|
||||
adminUsername := os.Getenv("MM_ADMIN_USERNAME")
|
||||
adminPassword := os.Getenv("MM_ADMIN_PASSWORD")
|
||||
|
||||
if siteURL == "" {
|
||||
return nil, errors.New("MM_SERVICESETTINGS_SITEURL is not set")
|
||||
}
|
||||
|
||||
client = model.NewAPIv4Client(siteURL)
|
||||
|
||||
if adminToken != "" {
|
||||
log.Printf("Authenticating using token against %s.", siteURL)
|
||||
client.SetToken(adminToken)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
if adminUsername != "" && adminPassword != "" {
|
||||
client := model.NewAPIv4Client(siteURL)
|
||||
log.Printf("Authenticating as %s against %s.", adminUsername, siteURL)
|
||||
ctx := context.Background()
|
||||
_, _, err := client.Login(ctx, adminUsername, adminPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to login as %s: %w", adminUsername, err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("one of MM_ADMIN_TOKEN or MM_ADMIN_USERNAME/MM_ADMIN_PASSWORD must be defined")
|
||||
}
|
||||
|
||||
func getUnixClient(socketPath string) (*model.Client4, bool) {
|
||||
_, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return model.NewAPIv4SocketClient(socketPath), true
|
||||
}
|
||||
|
||||
// deploy attempts to upload and enable a plugin via the Client4 API.
|
||||
// It will fail if plugin uploads are disabled.
|
||||
func deploy(client *model.Client4, pluginID, bundlePath string) error {
|
||||
pluginBundle, err := os.Open(bundlePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %s: %w", bundlePath, err)
|
||||
}
|
||||
defer pluginBundle.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
log.Print("Uploading plugin via API.")
|
||||
_, _, err = client.UploadPluginForced(ctx, pluginBundle)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload plugin bundle: %s", err)
|
||||
}
|
||||
|
||||
log.Print("Enabling plugin.")
|
||||
_, err = client.EnablePlugin(ctx, pluginID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable plugin: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// disablePlugin attempts to disable the plugin via the Client4 API.
|
||||
func disablePlugin(client *model.Client4, pluginID string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
log.Print("Disabling plugin.")
|
||||
_, err := client.DisablePlugin(ctx, pluginID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to disable plugin: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// enablePlugin attempts to enable the plugin via the Client4 API.
|
||||
func enablePlugin(client *model.Client4, pluginID string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
log.Print("Enabling plugin.")
|
||||
_, err := client.EnablePlugin(ctx, pluginID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable plugin: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetPlugin attempts to reset the plugin via the Client4 API.
|
||||
func resetPlugin(client *model.Client4, pluginID string) error {
|
||||
err := disablePlugin(client, pluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = enablePlugin(client, pluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
# Ensure that go is installed. Note that this is independent of whether or not a server is being
|
||||
# built, since the build script itself uses go.
|
||||
ifeq ($(GO),)
|
||||
$(error "go is not available: see https://golang.org/doc/install")
|
||||
endif
|
||||
|
||||
# Ensure that the build tools are compiled. Go's caching makes this quick.
|
||||
$(shell cd build/manifest && $(GO) build -o ../bin/manifest)
|
||||
|
||||
# Ensure that the deployment tools are compiled. Go's caching makes this quick.
|
||||
$(shell cd build/pluginctl && $(GO) build -o ../bin/pluginctl)
|
||||
|
||||
# Extract the plugin id from the manifest.
|
||||
PLUGIN_ID ?= $(shell build/bin/manifest id)
|
||||
ifeq ($(PLUGIN_ID),)
|
||||
$(error "Cannot parse id from $(MANIFEST_FILE)")
|
||||
endif
|
||||
|
||||
# Extract the plugin version from the manifest.
|
||||
PLUGIN_VERSION ?= $(shell build/bin/manifest version)
|
||||
ifeq ($(PLUGIN_VERSION),)
|
||||
$(error "Cannot parse version from $(MANIFEST_FILE)")
|
||||
endif
|
||||
|
||||
# Determine if a server is defined in the manifest.
|
||||
HAS_SERVER ?= $(shell build/bin/manifest has_server)
|
||||
|
||||
# Determine if a webapp is defined in the manifest.
|
||||
HAS_WEBAPP ?= $(shell build/bin/manifest has_webapp)
|
||||
|
||||
# Determine if a /public folder is in use
|
||||
HAS_PUBLIC ?= $(wildcard public/.)
|
||||
|
||||
# Determine if the mattermost-utilities repo is present
|
||||
HAS_MM_UTILITIES ?= $(wildcard $(MM_UTILITIES_DIR)/.)
|
||||
|
||||
# Store the current path for later use
|
||||
PWD ?= $(shell pwd)
|
||||
|
||||
# Ensure that npm (and thus node) is installed.
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
ifeq ($(NPM),)
|
||||
$(error "npm is not available: see https://www.npmjs.com/get-npm")
|
||||
endif
|
||||
endif
|
@ -1,113 +0,0 @@
|
||||
sync
|
||||
====
|
||||
|
||||
The sync tool is a proof-of-concept implementation of a tool for synchronizing mattermost plugin
|
||||
repositories with the mattermost-plugin-starter-template repo.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
At its core the tool is just a collection of checks and actions that are executed according to a
|
||||
synchronization plan (see [./build/sync/plan.yml](https://github.com/mattermost/mattermost-plugin-starter-template/blob/sync/build/sync/plan.yml)
|
||||
for an example). The plan defines a set of files
|
||||
and/or directories that need to be kept in sync between the plugin repository and the template (this
|
||||
repo).
|
||||
|
||||
For each set of paths, a set of actions to be performed is outlined. No more than one action of that set
|
||||
will be executed - the first one whose checks pass. Other actions are meant to act as fallbacks.
|
||||
The idea is to be able to e.g. overwrite a file if it has no local changes or apply a format-specific
|
||||
merge algorithm otherwise.
|
||||
|
||||
Before running each action, the tool will check if any checks are defined for that action. If there
|
||||
are any, they will be executed and their results examined. If all checks pass, the action will be executed.
|
||||
If there is a check failure, the tool will locate the next applicable action according to the plan and
|
||||
start over with it.
|
||||
|
||||
The synchronization plan can also run checks before running any actions, e.g. to check if the source and
|
||||
target worktrees are clean.
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
The tool can be executed from the root of this repository with a command:
|
||||
```
|
||||
$ go run ./build/sync/main.go ./build/sync/plan.yml ../mattermost-plugin-github
|
||||
```
|
||||
|
||||
(assuming `mattermost-plugin-github` is the target repository we want to synchronize with the source).
|
||||
|
||||
plan.yml
|
||||
---------
|
||||
|
||||
The `plan.yml` file (located in `build/sync/plan.yml`) consists of two parts:
|
||||
- checks
|
||||
- actions
|
||||
|
||||
The `checks` section defines tests to run before executing the plan itself. Currently the only available such check is `repo_is_clean` defined as:
|
||||
```
|
||||
type: repo_is_clean
|
||||
params:
|
||||
repo: source
|
||||
```
|
||||
The `repo` parameter takes one of two values:
|
||||
- `source` - the `mattermost-plugin-starter-template` repository
|
||||
- `target` - the repository of the plugin being updated.
|
||||
|
||||
The `actions` section defines actions to be run as part of the synchronization.
|
||||
Each entry in this section has the form:
|
||||
```
|
||||
paths:
|
||||
- path1
|
||||
- path2
|
||||
actions:
|
||||
- type: action_type
|
||||
params:
|
||||
action_parameter: value
|
||||
conditions:
|
||||
- type: check_type
|
||||
params:
|
||||
check_parameter: value
|
||||
```
|
||||
|
||||
`paths` is a list of file or directory paths (relative to the root of the repository)
|
||||
synchronization should be performed on.
|
||||
|
||||
Each action in the `actions` section is defined by its type. Currently supported action types are:
|
||||
- `overwrite_file` - overwrite the specified file in the `target` repository with the file in the `source` repository.
|
||||
- `overwrite_directory` - overwrite a directory.
|
||||
|
||||
Both actions accept a parameter called `create` which determines if the file or directory should be created if it does not exist in the target repository.
|
||||
|
||||
The `conditions` part of an action definition defines tests that need to pass for the
|
||||
action to be run. Available checks are:
|
||||
- `exists`
|
||||
- `file_unaltered`
|
||||
|
||||
The `exists` check takes a single parameter - `repo` (referencing either the source or target repository) and it passes only if the file or directory the action is about to be run on exists. If the repo parameter is not specified, it will default to `target`.
|
||||
|
||||
The `file_unaltered` check is only applicable to file paths. It passes if the file
|
||||
has not been altered - i.e. it is identical to some version of that same file in the reference repository (usually `source`). This check takes two parameters:
|
||||
- `in` - repository to check the file in, default `target`
|
||||
- `compared-to` - repository to check the file against, default `source`.
|
||||
|
||||
When multiple actions are specified for a set of paths, the `sync` tool will only
|
||||
execute a single action for each path. The first action in the list, whose conditions
|
||||
are all satisfied will be executed.
|
||||
|
||||
If an acton fails due to an error, the synchronization run will be aborted.
|
||||
|
||||
Caveat emptor
|
||||
-------------
|
||||
|
||||
This is a very basic proof-of-concept and there are many things that should be improved/implemented:
|
||||
(in no specific order)
|
||||
|
||||
1. Format-specific merge actions for `go.mod`, `go.sum`, `webapp/package.json` and other files should
|
||||
be implemented.
|
||||
2. Better logging should be implemented.
|
||||
3. Handling action dependencies should be investigated.
|
||||
e.g. if the `build` directory is overwritten, that will in some cases mean that the go.mod file also needs
|
||||
to be updated.
|
||||
4. Storing the tree-hash of the template repository that the plugin was synchronized with would allow
|
||||
improving the performance of the tool by restricting the search space when examining if a file
|
||||
has been altered in the plugin repository.
|
@ -1,84 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
|
||||
)
|
||||
|
||||
func main() {
|
||||
verbose := flag.Bool("verbose", false, "enable verbose output")
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Update a pluging directory with /mattermost-plugin-starter-template/.\n")
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "%s <plan.yml> <plugin_directory>\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
// TODO: implement proper command line parameter parsing.
|
||||
if len(os.Args) != 3 {
|
||||
fmt.Fprintf(os.Stderr, "running: \n $ sync [plan.yaml] [plugin path]\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
syncPlan, err := readPlan(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "coud not load plan: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
srcDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to get current directory: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
trgDir, err := filepath.Abs(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not determine target directory: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
srcRepo, err := plan.GetRepoSetup(srcDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
trgRepo, err := plan.GetRepoSetup(trgDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
planSetup := plan.Setup{
|
||||
Source: srcRepo,
|
||||
Target: trgRepo,
|
||||
VerboseLogging: *verbose,
|
||||
}
|
||||
err = syncPlan.Execute(planSetup)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func readPlan(path string) (*plan.Plan, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read plan file %q: %v", path, err)
|
||||
}
|
||||
|
||||
var p plan.Plan
|
||||
err = yaml.Unmarshal(raw, &p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal plan yaml: %v", err)
|
||||
}
|
||||
|
||||
return &p, err
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
checks:
|
||||
- type: repo_is_clean
|
||||
params:
|
||||
repo: source
|
||||
- type: repo_is_clean
|
||||
params:
|
||||
repo: target
|
||||
actions:
|
||||
- paths:
|
||||
- build/pluginctl
|
||||
- build/manifest
|
||||
actions:
|
||||
- type: overwrite_directory
|
||||
params:
|
||||
create: true
|
||||
- paths:
|
||||
- Makefile
|
||||
actions:
|
||||
- type: overwrite_file
|
||||
params:
|
||||
create: true
|
||||
- paths:
|
||||
- .editorconfig
|
||||
- .gitattributes
|
||||
- .gitignore
|
||||
- build/.gitignore
|
||||
- build/go.mod
|
||||
- build/go.sum
|
||||
- build/setup.mk
|
||||
- server/.gitignore
|
||||
- webapp/.eslintrc.json
|
||||
- webapp/.npmrc
|
||||
- webapp/babel.config.js
|
||||
- webapp/package.json
|
||||
- webapp/tsconfig.json
|
||||
- webapp/webpack.config.js
|
||||
- webapp/src/manifest.test.tsx
|
||||
- webapp/tests/setup.tsx
|
||||
actions:
|
||||
- type: overwrite_file
|
||||
params:
|
||||
create: true
|
||||
conditions:
|
||||
- type: file_unaltered
|
@ -1,214 +0,0 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ActionConditions adds condition support to actions.
|
||||
type ActionConditions struct {
|
||||
// Conditions are checkers run before executing the
|
||||
// action. If any one fails (returns an error), the action
|
||||
// itself is not executed.
|
||||
Conditions []Check
|
||||
}
|
||||
|
||||
// Check runs the conditions associated with the action and returns
|
||||
// the first error (if any).
|
||||
func (c ActionConditions) Check(path string, setup Setup) error {
|
||||
if len(c.Conditions) > 0 {
|
||||
setup.Logf("checking action conditions")
|
||||
}
|
||||
for _, condition := range c.Conditions {
|
||||
err := condition.Check(path, setup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OverwriteFileAction is used to overwrite a file.
|
||||
type OverwriteFileAction struct {
|
||||
ActionConditions
|
||||
Params struct {
|
||||
// Create determines whether the target directory
|
||||
// will be created if it does not exist.
|
||||
Create bool `json:"create"`
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements plan.Action.Run.
|
||||
func (a OverwriteFileAction) Run(path string, setup Setup) error {
|
||||
setup.Logf("overwriting file %q", path)
|
||||
src := setup.PathInRepo(SourceRepo, path)
|
||||
dst := setup.PathInRepo(TargetRepo, path)
|
||||
|
||||
dstInfo, err := os.Stat(dst)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
if !a.Params.Create {
|
||||
return fmt.Errorf("path %q does not exist, not creating", dst)
|
||||
}
|
||||
case err != nil:
|
||||
return fmt.Errorf("failed to check path %q: %v", dst, err)
|
||||
case dstInfo.IsDir():
|
||||
return fmt.Errorf("path %q is a directory", dst)
|
||||
}
|
||||
|
||||
srcInfo, err := os.Stat(src)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("file %q does not exist", src)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to check path %q: %v", src, err)
|
||||
}
|
||||
if srcInfo.IsDir() {
|
||||
return fmt.Errorf("path %q is a directory", src)
|
||||
}
|
||||
|
||||
srcF, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %q: %v", src, err)
|
||||
}
|
||||
defer srcF.Close()
|
||||
dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, srcInfo.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %q: %v", src, err)
|
||||
}
|
||||
defer dstF.Close()
|
||||
_, err = io.Copy(dstF, srcF)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file %q: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OverwriteDirectoryAction is used to completely overwrite directories.
|
||||
// If the target directory exists, it will be removed first.
|
||||
type OverwriteDirectoryAction struct {
|
||||
ActionConditions
|
||||
Params struct {
|
||||
// Create determines whether the target directory
|
||||
// will be created if it does not exist.
|
||||
Create bool `json:"create"`
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements plan.Action.Run.
|
||||
func (a OverwriteDirectoryAction) Run(path string, setup Setup) error {
|
||||
setup.Logf("overwriting directory %q", path)
|
||||
src := setup.PathInRepo(SourceRepo, path)
|
||||
dst := setup.PathInRepo(TargetRepo, path)
|
||||
|
||||
dstInfo, err := os.Stat(dst)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
if !a.Params.Create {
|
||||
return fmt.Errorf("path %q does not exist, not creating", dst)
|
||||
}
|
||||
case err != nil:
|
||||
return fmt.Errorf("failed to check path %q: %v", dst, err)
|
||||
default:
|
||||
if !dstInfo.IsDir() {
|
||||
return fmt.Errorf("path %q is not a directory", dst)
|
||||
}
|
||||
err = os.RemoveAll(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove directory %q: %v", dst, err)
|
||||
}
|
||||
}
|
||||
|
||||
srcInfo, err := os.Stat(src)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("directory %q does not exist", src)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to check path %q: %v", src, err)
|
||||
}
|
||||
if !srcInfo.IsDir() {
|
||||
return fmt.Errorf("path %q is not a directory", src)
|
||||
}
|
||||
|
||||
err = CopyDirectory(src, dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy path %q: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyDirectory copies the directory src to dst so that after
|
||||
// a successful operation the contents of src and dst are equal.
|
||||
func CopyDirectory(src, dst string) error {
|
||||
copier := dirCopier{dst: dst, src: src}
|
||||
return filepath.Walk(src, copier.Copy)
|
||||
}
|
||||
|
||||
type dirCopier struct {
|
||||
dst string
|
||||
src string
|
||||
}
|
||||
|
||||
// Convert a path in the source directory to a path in the destination
|
||||
// directory.
|
||||
func (d dirCopier) srcToDst(path string) (string, error) {
|
||||
suff := strings.TrimPrefix(path, d.src)
|
||||
if suff == path {
|
||||
return "", fmt.Errorf("path %q is not in %q", path, d.src)
|
||||
}
|
||||
return filepath.Join(d.dst, suff), nil
|
||||
}
|
||||
|
||||
// Copy is an implementation of filepatch.WalkFunc that copies the
|
||||
// source directory to target with all subdirectories.
|
||||
func (d dirCopier) Copy(srcPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy directory: %v", err)
|
||||
}
|
||||
trgPath, err := d.srcToDst(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
err = os.MkdirAll(trgPath, info.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory %q: %v", trgPath, err)
|
||||
}
|
||||
err = os.Chtimes(trgPath, info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory %q: %v", trgPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err = copyFile(srcPath, trgPath, info)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file %q: %v", srcPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, info os.FileInfo) error {
|
||||
srcF, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file %q: %v", src, err)
|
||||
}
|
||||
defer srcF.Close()
|
||||
dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, info.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open destination file %q: %v", dst, err)
|
||||
}
|
||||
_, err = io.Copy(dstF, srcF)
|
||||
if err != nil {
|
||||
dstF.Close()
|
||||
return fmt.Errorf("failed to copy file %q: %v", src, err)
|
||||
}
|
||||
if err = dstF.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close file %q: %v", dst, err)
|
||||
}
|
||||
err = os.Chtimes(dst, info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to adjust file modification time for %q: %v", dst, err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package plan_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
|
||||
)
|
||||
|
||||
func TestCopyDirectory(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Create a temporary directory to copy to.
|
||||
dir, err := os.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
assert.Nil(err)
|
||||
|
||||
srcDir := filepath.Join(wd, "testdata")
|
||||
err = plan.CopyDirectory(srcDir, dir)
|
||||
assert.Nil(err)
|
||||
|
||||
compareDirectories(t, dir, srcDir)
|
||||
}
|
||||
|
||||
func TestOverwriteFileAction(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Create a temporary directory to copy to.
|
||||
dir, err := os.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
assert.Nil(err)
|
||||
|
||||
setup := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Git: nil,
|
||||
Path: filepath.Join(wd, "testdata", "b"),
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Git: nil,
|
||||
Path: dir,
|
||||
},
|
||||
}
|
||||
action := plan.OverwriteFileAction{}
|
||||
action.Params.Create = true
|
||||
err = action.Run("c", setup)
|
||||
assert.Nil(err)
|
||||
|
||||
compareDirectories(t, dir, filepath.Join(wd, "testdata", "b"))
|
||||
}
|
||||
|
||||
func TestOverwriteDirectoryAction(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Create a temporary directory to copy to.
|
||||
dir, err := os.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
assert.Nil(err)
|
||||
|
||||
setup := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Git: nil,
|
||||
Path: wd,
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Git: nil,
|
||||
Path: dir,
|
||||
},
|
||||
}
|
||||
action := plan.OverwriteDirectoryAction{}
|
||||
action.Params.Create = true
|
||||
err = action.Run("testdata", setup)
|
||||
assert.Nil(err)
|
||||
|
||||
destDir := filepath.Join(dir, "testdata")
|
||||
srcDir := filepath.Join(wd, "testdata")
|
||||
compareDirectories(t, destDir, srcDir)
|
||||
}
|
||||
|
||||
func compareDirectories(t *testing.T, pathA, pathB string) {
|
||||
assert := assert.New(t)
|
||||
t.Helper()
|
||||
|
||||
aContents, err := os.ReadDir(pathA)
|
||||
assert.Nil(err)
|
||||
bContents, err := os.ReadDir(pathB)
|
||||
assert.Nil(err)
|
||||
assert.Len(aContents, len(bContents))
|
||||
|
||||
// Check the directory contents are equal.
|
||||
for i, aFInfo := range aContents {
|
||||
bFInfo := bContents[i]
|
||||
assert.Equal(aFInfo.Name(), bFInfo.Name())
|
||||
assert.Equal(aFInfo.Mode(), bFInfo.Mode())
|
||||
assert.Equal(aFInfo.IsDir(), bFInfo.IsDir())
|
||||
if !aFInfo.IsDir() {
|
||||
assert.Equal(aFInfo.Size(), bFInfo.Size())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan/git"
|
||||
)
|
||||
|
||||
// CheckFail is a custom error type used to indicate a
|
||||
// check that did not pass (but did not fail due to external
|
||||
// causes.
|
||||
// Use `IsCheckFail` to check if an error is a check failure.
|
||||
type CheckFail string
|
||||
|
||||
func (e CheckFail) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// CheckFailf creates an error with the specified message string.
|
||||
// The error will pass the IsCheckFail filter.
|
||||
func CheckFailf(msg string, args ...interface{}) CheckFail {
|
||||
if len(args) > 0 {
|
||||
msg = fmt.Sprintf(msg, args...)
|
||||
}
|
||||
return CheckFail(msg)
|
||||
}
|
||||
|
||||
// IsCheckFail determines if an error is a check fail error.
|
||||
func IsCheckFail(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := err.(CheckFail)
|
||||
return ok
|
||||
}
|
||||
|
||||
// RepoIsCleanChecker checks whether the git repository is clean.
|
||||
type RepoIsCleanChecker struct {
|
||||
Params struct {
|
||||
Repo RepoID
|
||||
}
|
||||
}
|
||||
|
||||
// Check implements the Checker interface.
|
||||
// The path parameter is ignored because this checker checks the state of a repository.
|
||||
func (r RepoIsCleanChecker) Check(_ string, ctx Setup) error {
|
||||
ctx.Logf("checking if repository %q is clean", r.Params.Repo)
|
||||
rc := ctx.GetRepo(r.Params.Repo)
|
||||
repo := rc.Git
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get worktree: %v", err)
|
||||
}
|
||||
status, err := worktree.Status()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get worktree status: %v", err)
|
||||
}
|
||||
if !status.IsClean() {
|
||||
return CheckFailf("%q repository is not clean", r.Params.Repo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PathExistsChecker checks whether the fle or directory with the
|
||||
// path exists. If it does not, an error is returned.
|
||||
type PathExistsChecker struct {
|
||||
Params struct {
|
||||
Repo RepoID
|
||||
}
|
||||
}
|
||||
|
||||
// Check implements the Checker interface.
|
||||
func (r PathExistsChecker) Check(path string, ctx Setup) error {
|
||||
repo := r.Params.Repo
|
||||
if repo == "" {
|
||||
repo = TargetRepo
|
||||
}
|
||||
ctx.Logf("checking if path %q exists in repo %q", path, repo)
|
||||
absPath := ctx.PathInRepo(repo, path)
|
||||
_, err := os.Stat(absPath)
|
||||
if os.IsNotExist(err) {
|
||||
return CheckFailf("path %q does not exist", path)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to stat path %q: %v", absPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileUnalteredChecker checks whether the file in Repo is
|
||||
// an unaltered version of that same file in ReferenceRepo.
|
||||
//
|
||||
// Its purpose is to check that a file has not been changed after forking a repository.
|
||||
// It could be an old unaltered version, so the git history of the file is traversed
|
||||
// until a matching version is found.
|
||||
//
|
||||
// If the repositories in the parameters are not specified,
|
||||
// reference will default to the source repository and repo - to the target.
|
||||
type FileUnalteredChecker struct {
|
||||
Params struct {
|
||||
SourceRepo RepoID `json:"compared-to"`
|
||||
TargetRepo RepoID `json:"in"`
|
||||
}
|
||||
}
|
||||
|
||||
// Check implements the Checker interface.
|
||||
func (f FileUnalteredChecker) Check(path string, setup Setup) error {
|
||||
setup.Logf("checking if file %q has not been altered", path)
|
||||
repo := f.Params.TargetRepo
|
||||
if repo == "" {
|
||||
repo = TargetRepo
|
||||
}
|
||||
source := f.Params.SourceRepo
|
||||
if source == "" {
|
||||
source = SourceRepo
|
||||
}
|
||||
trgPath := setup.PathInRepo(repo, path)
|
||||
srcPath := setup.PathInRepo(source, path)
|
||||
|
||||
fileHashes, err := git.FileHistory(path, setup.GetRepo(source).Git)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var srcDeleted bool
|
||||
srcInfo, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
srcDeleted = true
|
||||
} else {
|
||||
return fmt.Errorf("failed to get stat for %q: %v", trgPath, err)
|
||||
}
|
||||
} else if srcInfo.IsDir() {
|
||||
return fmt.Errorf("%q is a directory in source repository", path)
|
||||
}
|
||||
|
||||
trgInfo, err := os.Stat(trgPath)
|
||||
if os.IsNotExist(err) {
|
||||
if srcDeleted {
|
||||
// File has been deleted in target and source repositories.
|
||||
// Consider it unaltered.
|
||||
return nil
|
||||
}
|
||||
// Check if the file was ever in git history.
|
||||
_, err := git.FileHistory(path, setup.GetRepo(repo).Git)
|
||||
if errors.Is(err, git.ErrNotFound) {
|
||||
// This is a new file being introduced to the target repo.
|
||||
// Consider it unaltered.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
return CheckFailf("file %q has been deleted", trgPath)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get stat for %q: %v", trgPath, err)
|
||||
}
|
||||
if trgInfo.IsDir() {
|
||||
return fmt.Errorf("%q is a directory", trgPath)
|
||||
}
|
||||
|
||||
currentHash, err := git.GetFileHash(trgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(fileHashes)
|
||||
idx := sort.SearchStrings(fileHashes, currentHash)
|
||||
if idx < len(fileHashes) && fileHashes[idx] == currentHash {
|
||||
return nil
|
||||
}
|
||||
return CheckFailf("file %q has been altered", trgPath)
|
||||
}
|
@ -1,212 +0,0 @@
|
||||
package plan_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
|
||||
)
|
||||
|
||||
// Tests for the RepoIsClean checker.
|
||||
func TestRepoIsCleanChecker(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Create a git repository in a temporary dir.
|
||||
dir, err := os.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
repo, err := git.PlainInit(dir, false)
|
||||
assert.Nil(err)
|
||||
|
||||
// Repo should be clean.
|
||||
checker := plan.RepoIsCleanChecker{}
|
||||
checker.Params.Repo = plan.TargetRepo
|
||||
|
||||
ctx := plan.Setup{
|
||||
Target: plan.RepoSetup{
|
||||
Path: dir,
|
||||
Git: repo,
|
||||
},
|
||||
}
|
||||
assert.Nil(checker.Check("", ctx))
|
||||
|
||||
// Create a file in the repository.
|
||||
err = os.WriteFile(path.Join(dir, "data.txt"), []byte("lorem ipsum"), 0600)
|
||||
assert.Nil(err)
|
||||
err = checker.Check("", ctx)
|
||||
assert.EqualError(err, "\"target\" repository is not clean")
|
||||
assert.True(plan.IsCheckFail(err))
|
||||
}
|
||||
|
||||
func TestPathExistsChecker(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Set up a working directory.
|
||||
wd, err := os.TempDir("", "repo")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(wd)
|
||||
err = os.Mkdir(filepath.Join(wd, "t"), 0755)
|
||||
assert.Nil(err)
|
||||
err = os.WriteFile(filepath.Join(wd, "t", "test"), []byte("lorem ipsum"), 0644)
|
||||
assert.Nil(err)
|
||||
|
||||
checker := plan.PathExistsChecker{}
|
||||
checker.Params.Repo = plan.SourceRepo
|
||||
|
||||
ctx := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Path: wd,
|
||||
},
|
||||
}
|
||||
|
||||
// Check with existing directory.
|
||||
assert.Nil(checker.Check("t", ctx))
|
||||
|
||||
// Check with existing file.
|
||||
assert.Nil(checker.Check("t/test", ctx))
|
||||
|
||||
err = checker.Check("nosuchpath", ctx)
|
||||
assert.NotNil(err)
|
||||
assert.True(plan.IsCheckFail(err))
|
||||
}
|
||||
|
||||
func tempGitRepo(assert *assert.Assertions) (string, *git.Repository, func()) {
|
||||
// Setup repository.
|
||||
wd, err := os.TempDir("", "repo")
|
||||
assert.Nil(err)
|
||||
|
||||
// Initialize a repository.
|
||||
repo, err := git.PlainInit(wd, false)
|
||||
assert.Nil(err)
|
||||
w, err := repo.Worktree()
|
||||
assert.Nil(err)
|
||||
// Create repository files.
|
||||
err = os.WriteFile(filepath.Join(wd, "test"),
|
||||
[]byte("lorem ipsum"), 0644)
|
||||
assert.Nil(err)
|
||||
sig := &object.Signature{
|
||||
Name: "test",
|
||||
Email: "test@example.com",
|
||||
When: time.Now(),
|
||||
}
|
||||
_, err = w.Commit("initial commit", &git.CommitOptions{Author: sig})
|
||||
assert.Nil(err)
|
||||
pathA := "a.txt"
|
||||
err = os.WriteFile(filepath.Join(wd, pathA),
|
||||
[]byte("lorem ipsum"), 0644)
|
||||
assert.Nil(err)
|
||||
_, err = w.Add(pathA)
|
||||
assert.Nil(err)
|
||||
_, err = w.Commit("add files", &git.CommitOptions{Author: sig})
|
||||
assert.Nil(err)
|
||||
|
||||
return wd, repo, func() { os.RemoveAll(wd) }
|
||||
|
||||
}
|
||||
|
||||
func TestUnalteredCheckerSameFile(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
wd, repo, cleanup := tempGitRepo(assert)
|
||||
defer cleanup()
|
||||
|
||||
ctx := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Path: wd,
|
||||
Git: repo,
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Path: wd,
|
||||
},
|
||||
}
|
||||
|
||||
checker := plan.FileUnalteredChecker{}
|
||||
checker.Params.SourceRepo = plan.SourceRepo
|
||||
checker.Params.TargetRepo = plan.TargetRepo
|
||||
|
||||
// Check with the same file - check should succeed
|
||||
hashPath := "a.txt"
|
||||
err := checker.Check(hashPath, ctx)
|
||||
assert.Nil(err)
|
||||
}
|
||||
|
||||
func TestUnalteredCheckerDifferentContents(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
wd, repo, cleanup := tempGitRepo(assert)
|
||||
defer cleanup()
|
||||
|
||||
ctx := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Path: wd,
|
||||
Git: repo,
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Path: wd,
|
||||
},
|
||||
}
|
||||
|
||||
checker := plan.FileUnalteredChecker{}
|
||||
checker.Params.SourceRepo = plan.SourceRepo
|
||||
checker.Params.TargetRepo = plan.TargetRepo
|
||||
|
||||
// Create a file with the same suffix path, but different contents.
|
||||
tmpDir, err := os.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
err = os.WriteFile(filepath.Join(tmpDir, "a.txt"),
|
||||
[]byte("not lorem ipsum"), 0644)
|
||||
assert.Nil(err)
|
||||
|
||||
// Set the plugin path to the temporary directory.
|
||||
ctx.Target.Path = tmpDir
|
||||
err = checker.Check("a.txt", ctx)
|
||||
assert.True(plan.IsCheckFail(err))
|
||||
assert.EqualError(err, fmt.Sprintf("file %q has been altered", filepath.Join(tmpDir, "a.txt")))
|
||||
|
||||
}
|
||||
|
||||
// TestUnalteredCheckerNonExistant tests running the unaltered file checker
|
||||
// in the case where the target file does not exist. If the files has no history,
|
||||
// the checker should pass.
|
||||
func TestUnalteredCheckerNonExistant(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
hashPath := "a.txt"
|
||||
|
||||
wd, repo, cleanup := tempGitRepo(assert)
|
||||
defer cleanup()
|
||||
|
||||
// Temporary repo.
|
||||
tmpDir, err := os.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
trgRepo, err := git.PlainInit(tmpDir, false)
|
||||
assert.Nil(err)
|
||||
|
||||
ctx := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Path: wd,
|
||||
Git: repo,
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Path: tmpDir,
|
||||
Git: trgRepo,
|
||||
},
|
||||
}
|
||||
|
||||
checker := plan.FileUnalteredChecker{}
|
||||
checker.Params.SourceRepo = plan.SourceRepo
|
||||
checker.Params.TargetRepo = plan.TargetRepo
|
||||
|
||||
err = checker.Check(hashPath, ctx)
|
||||
assert.Nil(err)
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"crypto/sha1" //nolint
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrNotFound signifies the file was not found.
|
||||
var ErrNotFound = fmt.Errorf("not found")
|
||||
|
||||
// FileHistory will trace all the versions of a file in the git repository
|
||||
// and return a list of sha1 hashes of that file.
|
||||
func FileHistory(path string, repo *git.Repository) ([]string, error) {
|
||||
logOpts := git.LogOptions{
|
||||
FileName: &path,
|
||||
All: true,
|
||||
}
|
||||
commits, err := repo.Log(&logOpts)
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commits for path %q: %v", path, err)
|
||||
}
|
||||
defer commits.Close()
|
||||
hashHistory := []string{}
|
||||
cerr := commits.ForEach(func(c *object.Commit) error {
|
||||
root, err := repo.TreeObject(c.TreeHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get commit tree: %v", err)
|
||||
}
|
||||
f, err := traverseTree(root, path)
|
||||
if err == object.ErrFileNotFound || err == object.ErrDirectoryNotFound {
|
||||
// Ignoring file not found errors.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
sum, err := getReaderHash(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hashHistory = append(hashHistory, sum)
|
||||
return nil
|
||||
})
|
||||
if cerr != nil && cerr != io.EOF {
|
||||
return nil, cerr
|
||||
}
|
||||
if len(hashHistory) == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return hashHistory, nil
|
||||
}
|
||||
|
||||
func traverseTree(root *object.Tree, path string) (io.ReadCloser, error) {
|
||||
dirName, fileName := filepath.Split(path)
|
||||
var err error
|
||||
t := root
|
||||
if dirName != "" {
|
||||
t, err = root.Tree(filepath.Clean(dirName))
|
||||
if err == object.ErrDirectoryNotFound {
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to traverse tree to %q: %v", dirName, err)
|
||||
}
|
||||
}
|
||||
f, err := t.File(fileName)
|
||||
if err == object.ErrFileNotFound {
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to lookup file %q: %v", fileName, err)
|
||||
}
|
||||
reader, err := f.Reader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open %q: %v", path, err)
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func getReaderHash(r io.Reader) (string, error) {
|
||||
h := sha1.New() // nolint
|
||||
_, err := io.Copy(h, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// GetFileHash calculates the sha1 hash sum of the file.
|
||||
func GetFileHash(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
sum, err := getReaderHash(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sum, nil
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package git_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
gitutil "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan/git"
|
||||
)
|
||||
|
||||
var fileContents = []byte("abcdefg")
|
||||
|
||||
func TestFileHistory(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
dir, err := os.TempDir("", "repo")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Initialize a repository.
|
||||
repo, err := git.PlainInit(dir, false)
|
||||
assert.Nil(err)
|
||||
w, err := repo.Worktree()
|
||||
assert.Nil(err)
|
||||
// Create repository files.
|
||||
err = os.WriteFile(filepath.Join(dir, "test"), fileContents, 0644)
|
||||
assert.Nil(err)
|
||||
_, err = w.Add("test")
|
||||
assert.Nil(err)
|
||||
sig := &object.Signature{
|
||||
Name: "test",
|
||||
Email: "test@example.com",
|
||||
When: time.Now(),
|
||||
}
|
||||
_, err = w.Commit("initial commit", &git.CommitOptions{Author: sig})
|
||||
assert.Nil(err)
|
||||
pathA := "a.txt"
|
||||
err = os.WriteFile(filepath.Join(dir, pathA), fileContents, 0644)
|
||||
assert.Nil(err)
|
||||
pathB := "b.txt"
|
||||
err = os.WriteFile(filepath.Join(dir, pathB), fileContents, 0644)
|
||||
assert.Nil(err)
|
||||
_, err = w.Add(pathA)
|
||||
assert.Nil(err)
|
||||
_, err = w.Add(pathB)
|
||||
assert.Nil(err)
|
||||
_, err = w.Commit("add files", &git.CommitOptions{Author: sig})
|
||||
assert.Nil(err)
|
||||
// Delete one of the files.
|
||||
_, err = w.Remove(pathB)
|
||||
assert.Nil(err)
|
||||
_, err = w.Commit("remove file b.txt", &git.CommitOptions{
|
||||
Author: sig,
|
||||
All: true,
|
||||
})
|
||||
assert.Nil(err)
|
||||
|
||||
repo, err = git.PlainOpen(dir)
|
||||
assert.Nil(err)
|
||||
|
||||
// Call file history on an existing file.
|
||||
sums, err := gitutil.FileHistory("a.txt", repo)
|
||||
assert.Nil(err)
|
||||
assert.Equal([]string{"2fb5e13419fc89246865e7a324f476ec624e8740"}, sums)
|
||||
|
||||
// Calling with a non-existent file returns error.
|
||||
sums, err = gitutil.FileHistory(filepath.Join(dir, "nosuch_testfile.txt"), repo)
|
||||
assert.Equal(gitutil.ErrNotFound, err)
|
||||
assert.Nil(sums)
|
||||
|
||||
// Calling with a non-existent file that was in git history returns no error.
|
||||
_, err = gitutil.FileHistory(pathB, repo)
|
||||
assert.Nil(err)
|
||||
}
|
@ -1,245 +0,0 @@
|
||||
// Package plan handles the synchronization plan.
|
||||
//
|
||||
// Each synchronization plan is a set of checks and actions to perform on specified paths
|
||||
// that will result in the "plugin" repository being updated.
|
||||
package plan
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Plan defines the plan for synchronizing a target and a source directory.
|
||||
type Plan struct {
|
||||
Checks []Check `json:"checks"`
|
||||
// Each set of paths has multiple actions associated, each a fallback for the one
|
||||
// previous to it.
|
||||
Actions []ActionSet
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the `json.Unmarshaler` interface.
|
||||
func (p *Plan) UnmarshalJSON(raw []byte) error {
|
||||
var t jsonPlan
|
||||
if err := json.Unmarshal(raw, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Checks = make([]Check, len(t.Checks))
|
||||
for i, check := range t.Checks {
|
||||
c, err := parseCheck(check.Type, check.Params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse check %q: %v", check.Type, err)
|
||||
}
|
||||
p.Checks[i] = c
|
||||
}
|
||||
|
||||
if len(t.Actions) > 0 {
|
||||
p.Actions = make([]ActionSet, len(t.Actions))
|
||||
}
|
||||
for i, actionSet := range t.Actions {
|
||||
var err error
|
||||
pathActions := make([]Action, len(actionSet.Actions))
|
||||
for i, action := range actionSet.Actions {
|
||||
var actionConditions []Check
|
||||
if len(action.Conditions) > 0 {
|
||||
actionConditions = make([]Check, len(action.Conditions))
|
||||
}
|
||||
for j, check := range action.Conditions {
|
||||
actionConditions[j], err = parseCheck(check.Type, check.Params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pathActions[i], err = parseAction(action.Type, action.Params, actionConditions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.Actions[i] = ActionSet{
|
||||
Paths: actionSet.Paths,
|
||||
Actions: pathActions,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute executes the synchronization plan.
|
||||
func (p *Plan) Execute(c Setup) error {
|
||||
c.Logf("running pre-checks")
|
||||
for _, check := range p.Checks {
|
||||
err := check.Check("", c) // For pre-sync checks, the path is ignored.
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed check: %v", err)
|
||||
}
|
||||
}
|
||||
result := []pathResult{}
|
||||
c.Logf("running actions")
|
||||
for _, actions := range p.Actions {
|
||||
PATHS_LOOP:
|
||||
for _, path := range actions.Paths {
|
||||
c.Logf("syncing path %q", path)
|
||||
ACTIONS_LOOP:
|
||||
for i, action := range actions.Actions {
|
||||
c.Logf("running action for path %q", path)
|
||||
err := action.Check(path, c)
|
||||
if IsCheckFail(err) {
|
||||
c.Logf("check failed, not running action: %v", err)
|
||||
// If a check for an action fails, we switch to
|
||||
// the next action associated with the path.
|
||||
if i == len(actions.Actions)-1 { // no actions to fallback to.
|
||||
c.Logf("path %q not handled - no more fallbacks", path)
|
||||
result = append(result,
|
||||
pathResult{
|
||||
Path: path,
|
||||
Status: statusFailed,
|
||||
Message: fmt.Sprintf("check failed, %s", err.Error()),
|
||||
})
|
||||
}
|
||||
continue ACTIONS_LOOP
|
||||
} else if err != nil {
|
||||
c.LogErrorf("unexpected error when running check: %v", err)
|
||||
return fmt.Errorf("failed to run checks for action: %v", err)
|
||||
}
|
||||
err = action.Run(path, c)
|
||||
if err != nil {
|
||||
c.LogErrorf("action failed: %v", err)
|
||||
return fmt.Errorf("action failed: %v", err)
|
||||
}
|
||||
c.Logf("path %q sync'ed successfully", path)
|
||||
result = append(result,
|
||||
pathResult{
|
||||
Path: path,
|
||||
Status: statusUpdated,
|
||||
})
|
||||
|
||||
continue PATHS_LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print execution result.
|
||||
sort.SliceStable(result, func(i, j int) bool { return result[i].Path < result[j].Path })
|
||||
for _, res := range result {
|
||||
if res.Message != "" {
|
||||
fmt.Fprintf(os.Stdout, "%s\t%s: %s\n", res.Status, res.Path, res.Message)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stdout, "%s\t%s\n", res.Status, res.Path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check returns an error if the condition fails.
|
||||
type Check interface {
|
||||
Check(string, Setup) error
|
||||
}
|
||||
|
||||
// ActionSet is a set of actions along with a set of paths to
|
||||
// perform those actions on.
|
||||
type ActionSet struct {
|
||||
Paths []string
|
||||
Actions []Action
|
||||
}
|
||||
|
||||
// Action runs the defined action.
|
||||
type Action interface {
|
||||
// Run performs the action on the specified path.
|
||||
Run(string, Setup) error
|
||||
// Check runs checks associated with the action
|
||||
// before running it.
|
||||
Check(string, Setup) error
|
||||
}
|
||||
|
||||
// jsonPlan is used to unmarshal Plan structures.
|
||||
type jsonPlan struct {
|
||||
Checks []struct {
|
||||
Type string `json:"type"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
Actions []struct {
|
||||
Paths []string `json:"paths"`
|
||||
Actions []struct {
|
||||
Type string `json:"type"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
Conditions []struct {
|
||||
Type string `json:"type"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseCheck(checkType string, rawParams json.RawMessage) (Check, error) {
|
||||
var c Check
|
||||
|
||||
var params interface{}
|
||||
|
||||
switch checkType {
|
||||
case "repo_is_clean":
|
||||
tc := RepoIsCleanChecker{}
|
||||
params = &tc.Params
|
||||
c = &tc
|
||||
case "exists":
|
||||
tc := PathExistsChecker{}
|
||||
params = &tc.Params
|
||||
c = &tc
|
||||
case "file_unaltered":
|
||||
tc := FileUnalteredChecker{}
|
||||
params = &tc.Params
|
||||
c = &tc
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown checker type %q", checkType)
|
||||
}
|
||||
|
||||
if len(rawParams) > 0 {
|
||||
err := json.Unmarshal(rawParams, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal params for %s: %v", checkType, err)
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func parseAction(actionType string, rawParams json.RawMessage, checks []Check) (Action, error) {
|
||||
var a Action
|
||||
|
||||
var params interface{}
|
||||
|
||||
switch actionType {
|
||||
case "overwrite_file":
|
||||
ta := OverwriteFileAction{}
|
||||
ta.Conditions = checks
|
||||
params = &ta.Params
|
||||
a = &ta
|
||||
case "overwrite_directory":
|
||||
ta := OverwriteDirectoryAction{}
|
||||
ta.Conditions = checks
|
||||
params = &ta.Params
|
||||
a = &ta
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown action type %q", actionType)
|
||||
}
|
||||
|
||||
if len(rawParams) > 0 {
|
||||
err := json.Unmarshal(rawParams, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal params for %s: %v", actionType, err)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// pathResult contains the result of synchronizing a path.
|
||||
type pathResult struct {
|
||||
Path string
|
||||
Status status
|
||||
Message string
|
||||
}
|
||||
|
||||
type status string
|
||||
|
||||
const (
|
||||
statusUpdated status = "UPDATED"
|
||||
statusFailed status = "FAILED"
|
||||
)
|
@ -1,253 +0,0 @@
|
||||
package plan_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
|
||||
)
|
||||
|
||||
func TestUnmarshalPlan(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
rawJSON := []byte(`
|
||||
{
|
||||
"checks": [
|
||||
{"type": "repo_is_clean", "params": {"repo": "template"}}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"paths": ["abc"],
|
||||
"actions": [{
|
||||
"type": "overwrite_file",
|
||||
"params": {"create": true},
|
||||
"conditions": [{
|
||||
"type": "exists",
|
||||
"params": {"repo": "plugin"}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
var p plan.Plan
|
||||
err := json.Unmarshal(rawJSON, &p)
|
||||
assert.Nil(err)
|
||||
expectedCheck := plan.RepoIsCleanChecker{}
|
||||
expectedCheck.Params.Repo = "template"
|
||||
|
||||
expectedAction := plan.OverwriteFileAction{}
|
||||
expectedAction.Params.Create = true
|
||||
expectedActionCheck := plan.PathExistsChecker{}
|
||||
expectedActionCheck.Params.Repo = "plugin"
|
||||
expectedAction.Conditions = []plan.Check{&expectedActionCheck}
|
||||
expected := plan.Plan{
|
||||
Checks: []plan.Check{&expectedCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"abc"},
|
||||
Actions: []plan.Action{
|
||||
&expectedAction,
|
||||
},
|
||||
}},
|
||||
}
|
||||
assert.Equal(expected, p)
|
||||
}
|
||||
|
||||
type mockCheck struct {
|
||||
returnErr error
|
||||
calledWith string // Path parameter the check was called with.
|
||||
}
|
||||
|
||||
// Check implements the plan.Check interface.
|
||||
func (m *mockCheck) Check(path string, c plan.Setup) error {
|
||||
m.calledWith = path
|
||||
return m.returnErr
|
||||
}
|
||||
|
||||
type mockAction struct {
|
||||
runErr error // Error to be returned by Run.
|
||||
checkErr error // Error to be returned by Check.
|
||||
calledWith string
|
||||
}
|
||||
|
||||
// Check implements plan.Action interface.
|
||||
func (m *mockAction) Check(path string, c plan.Setup) error {
|
||||
return m.checkErr
|
||||
}
|
||||
|
||||
// Run implements plan.Action interface.
|
||||
func (m *mockAction) Run(path string, c plan.Setup) error {
|
||||
m.calledWith = path
|
||||
return m.runErr
|
||||
}
|
||||
|
||||
// TestRunPlanSuccessfully tests a successful execution of a sync plan.
|
||||
func TestRunPlanSuccessfully(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
preCheck := &mockCheck{}
|
||||
action1 := &mockAction{}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Checks: []plan.Check{preCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.Nil(err)
|
||||
|
||||
assert.Equal("", preCheck.calledWith)
|
||||
assert.Equal("somepath", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith) // second action was not called.
|
||||
}
|
||||
|
||||
// TestRunPlanPreCheckFail checks the scenario where a sync plan precheck
|
||||
// fails, aborting the whole operation.
|
||||
func TestRunPlanPreCheckFail(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
preCheck := &mockCheck{returnErr: plan.CheckFailf("check failed")}
|
||||
action1 := &mockAction{}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Checks: []plan.Check{preCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.EqualError(err, "failed check: check failed")
|
||||
|
||||
assert.Equal("", preCheck.calledWith)
|
||||
// None of the actions were executed.
|
||||
assert.Equal("", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith)
|
||||
}
|
||||
|
||||
// TestRunPlanActionCheckFails tests the situation where an action's
|
||||
// check returns a recoverable error, forcing the plan to execute the fallback action.
|
||||
func TestRunPlanActionCheckFails(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
action1 := &mockAction{checkErr: plan.CheckFailf("action check failed")}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.Nil(err)
|
||||
|
||||
assert.Equal("", action1.calledWith) // First action was not run.
|
||||
assert.Equal("somepath", action2.calledWith) // Second action was run.
|
||||
}
|
||||
|
||||
// TestRunPlanNoFallbacks tests the case where an action's check fails,
|
||||
// but there are not more fallback actions for that path.
|
||||
func TestRunPlanNoFallbacks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
action1 := &mockAction{checkErr: plan.CheckFailf("fail")}
|
||||
action2 := &mockAction{checkErr: plan.CheckFailf("fail")}
|
||||
|
||||
p := &plan.Plan{
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.Nil(err)
|
||||
|
||||
// both actions were not executed.
|
||||
assert.Equal("", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith)
|
||||
}
|
||||
|
||||
// TestRunPlanCheckError tests the scenario where a plan check fails with
|
||||
// an unexpected error. Plan execution is aborted.
|
||||
func TestRunPlanCheckError(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
preCheck := &mockCheck{returnErr: fmt.Errorf("fail")}
|
||||
action1 := &mockAction{}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Checks: []plan.Check{preCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.EqualError(err, "failed check: fail")
|
||||
|
||||
assert.Equal("", preCheck.calledWith)
|
||||
// Actions were not run.
|
||||
assert.Equal("", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith)
|
||||
}
|
||||
|
||||
// TestRunPlanActionError tests the scenario where an action fails,
|
||||
// aborting the whole sync process.
|
||||
func TestRunPlanActionError(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
preCheck := &mockCheck{}
|
||||
action1 := &mockAction{runErr: fmt.Errorf("fail")}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Checks: []plan.Check{preCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.EqualError(err, "action failed: fail")
|
||||
|
||||
assert.Equal("", preCheck.calledWith)
|
||||
assert.Equal("somepath", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith) // second action was not called.
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// RepoID identifies a repository - either plugin or template.
|
||||
type RepoID string
|
||||
|
||||
const (
|
||||
// SourceRepo is the id of the template repository (source).
|
||||
SourceRepo RepoID = "source"
|
||||
// TargetRepo is the id of the plugin repository (target).
|
||||
TargetRepo RepoID = "target"
|
||||
)
|
||||
|
||||
// Setup contains information about both parties
|
||||
// in the sync: the plugin repository being updated
|
||||
// and the source of the update - the template repo.
|
||||
type Setup struct {
|
||||
Source RepoSetup
|
||||
Target RepoSetup
|
||||
VerboseLogging bool
|
||||
}
|
||||
|
||||
// Logf logs the provided message.
|
||||
// If verbose output is not enabled, the message will not be printed.
|
||||
func (c Setup) Logf(tpl string, args ...interface{}) {
|
||||
if c.VerboseLogging {
|
||||
fmt.Fprintf(os.Stderr, tpl+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogErrorf logs the provided error message.
|
||||
func (c Setup) LogErrorf(tpl string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, tpl+"\n", args...)
|
||||
}
|
||||
|
||||
// GetRepo is a helper to get the required repo setup.
|
||||
// If the target parameter is not one of "plugin" or "template",
|
||||
// the function panics.
|
||||
func (c Setup) GetRepo(r RepoID) RepoSetup {
|
||||
switch r {
|
||||
case TargetRepo:
|
||||
return c.Target
|
||||
case SourceRepo:
|
||||
return c.Source
|
||||
default:
|
||||
panic(fmt.Sprintf("cannot get repository setup %q", r))
|
||||
}
|
||||
}
|
||||
|
||||
// PathInRepo returns the full path of a file in the specified repository.
|
||||
func (c Setup) PathInRepo(repo RepoID, path string) string {
|
||||
r := c.GetRepo(repo)
|
||||
return filepath.Join(r.Path, path)
|
||||
}
|
||||
|
||||
// RepoSetup contains relevant information
|
||||
// about a single repository (either source or target).
|
||||
type RepoSetup struct {
|
||||
Git *git.Repository
|
||||
Path string
|
||||
}
|
||||
|
||||
// GetRepoSetup returns the repository setup for the specified path.
|
||||
func GetRepoSetup(path string) (RepoSetup, error) {
|
||||
repo, err := git.PlainOpen(path)
|
||||
if err != nil {
|
||||
return RepoSetup{}, fmt.Errorf("failed to access git repository at %q: %v", path, err)
|
||||
}
|
||||
return RepoSetup{
|
||||
Git: repo,
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
1
mattermost-plugin/build/sync/plan/testdata/a
vendored
1
mattermost-plugin/build/sync/plan/testdata/a
vendored
@ -1 +0,0 @@
|
||||
a
|
@ -1 +0,0 @@
|
||||
c
|
@ -1,122 +0,0 @@
|
||||
module github.com/mattermost/focalboard/mattermost-plugin
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.8
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/mattermost/focalboard/server v0.0.0-20230104182634-f909c2552e37
|
||||
github.com/mattermost/mattermost/server/public v0.1.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/mattermost/mattermost/server/v8 v8.0.0-20240529104128-9d30a62c9471 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
|
||||
github.com/mattermost/logr/v2 v2.0.21 // indirect
|
||||
github.com/mattermost/morph v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.70 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/pborman/uuid v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.53.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/rudderlabs/analytics-go v3.3.3+incompatible // indirect
|
||||
github.com/segmentio/backo-go v1.1.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.18.2 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/gjson v1.17.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.1.9 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wiggin77/merror v1.0.5 // indirect
|
||||
github.com/wiggin77/srslog v1.0.1 // indirect
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
github.com/yuin/goldmark v1.7.1 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.50.9 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.29.10 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/mattermost/focalboard/server => ../server
|
@ -1,464 +0,0 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64=
|
||||
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
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-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
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.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI=
|
||||
github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94 h1:+AIlO01SKT9sfWU5CLWi0cfHc7dQwgGz3FhFRzXLoMg=
|
||||
github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94/go.mod h1:TcE3PIIkVWbP/HjhRAafgCjRKvDOi086iqp9VkNX/ng=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8=
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34=
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI=
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI=
|
||||
github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4=
|
||||
github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc=
|
||||
github.com/mattermost/mattermost/server/public v0.1.3 h1:A3hQ3rNCwHfKAVxe7Hk3Zd9p2pyUKRmxtRPnkWP5SFM=
|
||||
github.com/mattermost/mattermost/server/public v0.1.3/go.mod h1:PDPb/iqzJJ5ZvK/m70oDF55AXN/cOvVFj96Yu4e6j+Q=
|
||||
github.com/mattermost/mattermost/server/v8 v8.0.0-20240529104128-9d30a62c9471 h1:LxlvPGImhPoZ16qJtZHfooqfIG2UGsbcIRNiTqQ/5Is=
|
||||
github.com/mattermost/mattermost/server/v8 v8.0.0-20240529104128-9d30a62c9471/go.mod h1:qQjPPGKiugHw6Tunlmq3cVDkKFFbgtMxIvyNJoN+p3Y=
|
||||
github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw=
|
||||
github.com/mattermost/morph v1.1.0/go.mod h1:gD+EaqX2UMyyuzmF4PFh4r33XneQ8Nzi+0E8nXjMa3A=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJnn2g=
|
||||
github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
|
||||
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
|
||||
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
|
||||
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rudderlabs/analytics-go v3.3.3+incompatible h1:OG0XlKoXfr539e2t1dXtTB+Gr89uFW+OUNQBVhHIIBY=
|
||||
github.com/rudderlabs/analytics-go v3.3.3+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/segmentio/backo-go v1.1.0 h1:cJIfHQUdmLsd8t9IXqf5J8SdrOMn9vMa7cIvOavHAhc=
|
||||
github.com/segmentio/backo-go v1.1.0/go.mod h1:ckenwdf+v/qbyhVdNPWHnqh2YdJBED1O9cidYyM5J18=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
|
||||
github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME=
|
||||
github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0=
|
||||
github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8=
|
||||
github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls=
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck=
|
||||
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/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-20210630005230-0f9fa26af87c/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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
|
||||
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.17.8 h1:yyWBf2ipA0Y9GGz/MmCmi3EFpKgeS7ICrAFes+suEbs=
|
||||
modernc.org/ccgo/v4 v4.17.8/go.mod h1:buJnJ6Fn0tyAdP/dqePbrrvLyr6qslFfTbFrCuaYvtA=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8=
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.50.9 h1:hIWf1uz55lorXQhfoEoezdUHjxzuO6ceshET/yWjSjk=
|
||||
modernc.org/libc v1.50.9/go.mod h1:15P6ublJ9FJR8YQCGy8DeQ2Uwur7iW9Hserr/T3OFZE=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.29.10 h1:3u93dz83myFnMilBGCOLbr+HjklS6+5rJLx4q86RDAg=
|
||||
modernc.org/sqlite v1.29.10/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
@ -1,3 +0,0 @@
|
||||
server/**/*.go !server/**/*_test.go mattermost-plugin/server/**/*.go !mattermost-plugin/server/**/*_test.go {
|
||||
prep: cd mattermost-plugin; make server deploy-to-mattermost-directory
|
||||
}
|
6
mattermost-plugin/package-lock.json
generated
6
mattermost-plugin/package-lock.json
generated
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "mattermost-plugin",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
{
|
||||
"id": "focalboard",
|
||||
"name": "Mattermost Boards",
|
||||
"description": "The Mattermost Boards plugin",
|
||||
"homepage_url": "https://github.com/mattermost/focalboard",
|
||||
"support_url": "https://github.com/mattermost/focalboard/issues",
|
||||
"release_notes_url": "https://github.com/mattermost/focalboard/releases",
|
||||
"icon_path": "assets/starter-template-icon.svg",
|
||||
"version": "8.0.0",
|
||||
"min_server_version": "7.2.0",
|
||||
"server": {
|
||||
"executables": {
|
||||
"linux-amd64": "server/dist/plugin-linux-amd64",
|
||||
"linux-arm64": "server/dist/plugin-linux-arm64",
|
||||
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
||||
"darwin-arm64": "server/dist/plugin-darwin-arm64",
|
||||
"windows-amd64": "server/dist/plugin-windows-amd64.exe"
|
||||
}
|
||||
},
|
||||
"webapp": {
|
||||
"bundle_path": "webapp/dist/main.js"
|
||||
},
|
||||
"settings_schema": {
|
||||
"header": "",
|
||||
"footer": "",
|
||||
"settings": [{
|
||||
"key": "EnablePublicSharedBoards",
|
||||
"type": "bool",
|
||||
"display_name": "Enable Publicly-Shared Boards:",
|
||||
"default": false,
|
||||
"help_text": "This allows board editors to share boards that can be accessed by anyone with the link."
|
||||
}]
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
Hello from the static files public folder for the com.mattermost.plugin-starter-template plugin!
|
2
mattermost-plugin/server/.gitignore
vendored
2
mattermost-plugin/server/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
coverage.txt
|
||||
dist
|
@ -1,266 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
type storeService interface {
|
||||
GetMasterDB() (*sql.DB, error)
|
||||
}
|
||||
|
||||
// normalizeAppError returns a truly nil error if appErr is nil
|
||||
// See https://golang.org/doc/faq#nil_error for more details.
|
||||
func normalizeAppErr(appErr *mm_model.AppError) error {
|
||||
if appErr == nil {
|
||||
return nil
|
||||
}
|
||||
return appErr
|
||||
}
|
||||
|
||||
// pluginAPIAdapter is an adapter that ensures all Plugin API methods have the same signature as the
|
||||
// services API.
|
||||
// Note: this will be removed when plugin builds are no longer needed.
|
||||
type pluginAPIAdapter struct {
|
||||
api plugin.API
|
||||
storeService storeService
|
||||
logger mlog.LoggerIFace
|
||||
}
|
||||
|
||||
func newServiceAPIAdapter(api plugin.API, storeService storeService, logger mlog.LoggerIFace) *pluginAPIAdapter {
|
||||
return &pluginAPIAdapter{
|
||||
api: api,
|
||||
storeService: storeService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Channels service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) {
|
||||
channel, appErr := a.api.GetDirectChannel(userID1, userID2)
|
||||
return channel, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) {
|
||||
// plugin API's GetDirectChannel will create channel if it does not exist.
|
||||
channel, appErr := a.api.GetDirectChannel(userID1, userID2)
|
||||
return channel, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) GetChannelByID(channelID string) (*mm_model.Channel, error) {
|
||||
channel, appErr := a.api.GetChannel(channelID)
|
||||
return channel, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) {
|
||||
member, appErr := a.api.GetChannelMember(channelID, userID)
|
||||
return member, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) {
|
||||
channels, appErr := a.api.GetChannelsForTeamForUser(teamID, userID, includeDeleted)
|
||||
return channels, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Post service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) CreatePost(post *mm_model.Post) (*mm_model.Post, error) {
|
||||
post, appErr := a.api.CreatePost(post)
|
||||
return post, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// User service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetUserByID(userID string) (*mm_model.User, error) {
|
||||
user, appErr := a.api.GetUser(userID)
|
||||
return user, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) GetUserByUsername(name string) (*mm_model.User, error) {
|
||||
user, appErr := a.api.GetUserByUsername(name)
|
||||
return user, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) GetUserByEmail(email string) (*mm_model.User, error) {
|
||||
user, appErr := a.api.GetUserByEmail(email)
|
||||
return user, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) UpdateUser(user *mm_model.User) (*mm_model.User, error) {
|
||||
user, appErr := a.api.UpdateUser(user)
|
||||
return user, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) {
|
||||
users, appErr := a.api.GetUsers(options)
|
||||
return users, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Team service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) {
|
||||
member, appErr := a.api.GetTeamMember(teamID, userID)
|
||||
return member, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) {
|
||||
member, appErr := a.api.CreateTeamMember(teamID, userID)
|
||||
return member, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Permissions service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) HasPermissionTo(userID string, permission *mm_model.Permission) bool {
|
||||
return a.api.HasPermissionTo(userID, permission)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
|
||||
return a.api.HasPermissionToTeam(userID, teamID, permission)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool {
|
||||
return a.api.HasPermissionToChannel(askingUserID, channelID, permission)
|
||||
}
|
||||
|
||||
//
|
||||
// Bot service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
|
||||
return a.api.EnsureBotUser(bot)
|
||||
}
|
||||
|
||||
//
|
||||
// License service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetLicense() *mm_model.License {
|
||||
return a.api.GetLicense()
|
||||
}
|
||||
|
||||
//
|
||||
// FileInfoStore service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) {
|
||||
fi, appErr := a.api.GetFileInfo(fileID)
|
||||
return fi, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Cluster store.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) {
|
||||
a.api.PublishWebSocketEvent(event, payload, broadcast)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error {
|
||||
return a.api.PublishPluginClusterEvent(ev, opts)
|
||||
}
|
||||
|
||||
//
|
||||
// Config service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetConfig() *mm_model.Config {
|
||||
return a.api.GetUnsanitizedConfig()
|
||||
}
|
||||
|
||||
//
|
||||
// Logger service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetLogger() mlog.LoggerIFace {
|
||||
return a.logger
|
||||
}
|
||||
|
||||
//
|
||||
// KVStore service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) {
|
||||
b, appErr := a.api.KVSetWithOptions(key, value, options)
|
||||
return b, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Store service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetMasterDB() (*sql.DB, error) {
|
||||
return a.storeService.GetMasterDB()
|
||||
}
|
||||
|
||||
//
|
||||
// System service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetDiagnosticID() string {
|
||||
return a.api.GetDiagnosticId()
|
||||
}
|
||||
|
||||
//
|
||||
// Router service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) RegisterRouter(sub *mux.Router) {
|
||||
// NOOP for plugin
|
||||
}
|
||||
|
||||
//
|
||||
// Preferences service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
||||
preferences, appErr := a.api.GetPreferencesForUser(userID)
|
||||
if appErr != nil {
|
||||
return nil, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
boardsPreferences := mm_model.Preferences{}
|
||||
|
||||
// Mattermost API gives us all preferences.
|
||||
// We want just the Focalboard ones.
|
||||
for _, preference := range preferences {
|
||||
if preference.Category == model.PreferencesCategoryFocalboard {
|
||||
boardsPreferences = append(boardsPreferences, preference)
|
||||
}
|
||||
}
|
||||
|
||||
return boardsPreferences, nil
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||
appErr := a.api.UpdatePreferencesForUser(userID, preferences)
|
||||
return normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||
appErr := a.api.DeletePreferencesForUser(userID, preferences)
|
||||
return normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
// Ensure the adapter implements ServicesAPI.
|
||||
var _ model.ServicesAPI = &pluginAPIAdapter{}
|
@ -1,220 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package boards
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"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"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
"github.com/mattermost/mattermost/server/public/pluginapi/cluster"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
boardsFeatureFlagName = "BoardsFeatureFlags"
|
||||
PluginName = "focalboard"
|
||||
SharedBoardsName = "enablepublicsharedboards"
|
||||
|
||||
notifyFreqCardSecondsKey = "notify_freq_card_seconds"
|
||||
notifyFreqBoardSecondsKey = "notify_freq_board_seconds"
|
||||
)
|
||||
|
||||
type BoardsEmbed struct {
|
||||
OriginalPath string `json:"originalPath"`
|
||||
TeamID string `json:"teamID"`
|
||||
ViewID string `json:"viewID"`
|
||||
BoardID string `json:"boardID"`
|
||||
CardID string `json:"cardID"`
|
||||
ReadToken string `json:"readToken,omitempty"`
|
||||
}
|
||||
|
||||
type BoardsApp struct {
|
||||
// configurationLock synchronizes access to the configuration.
|
||||
configurationLock sync.RWMutex
|
||||
|
||||
// configuration is the active plugin configuration. Consult getConfiguration and
|
||||
// setConfiguration for usage.
|
||||
configuration *configuration
|
||||
|
||||
server *server.Server
|
||||
wsPluginAdapter ws.PluginAdapterInterface
|
||||
|
||||
servicesAPI model.ServicesAPI
|
||||
logger mlog.LoggerIFace
|
||||
}
|
||||
|
||||
func NewBoardsApp(api model.ServicesAPI) (*BoardsApp, error) {
|
||||
mmconfig := api.GetConfig()
|
||||
logger := api.GetLogger()
|
||||
|
||||
baseURL := ""
|
||||
if mmconfig.ServiceSettings.SiteURL != nil {
|
||||
baseURL = *mmconfig.ServiceSettings.SiteURL
|
||||
}
|
||||
serverID := api.GetDiagnosticID()
|
||||
cfg := createBoardsConfig(*mmconfig, baseURL, serverID)
|
||||
sqlDB, err := api.GetMasterDB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot access database while initializing Boards: %w", err)
|
||||
}
|
||||
|
||||
storeParams := sqlstore.Params{
|
||||
DBType: cfg.DBType,
|
||||
ConnectionString: cfg.DBConfigString,
|
||||
TablePrefix: cfg.DBTablePrefix,
|
||||
Logger: logger,
|
||||
DB: sqlDB,
|
||||
IsPlugin: true,
|
||||
NewMutexFn: func(name string) (*cluster.Mutex, error) {
|
||||
return cluster.NewMutex(&mutexAPIAdapter{api: api}, name)
|
||||
},
|
||||
ServicesAPI: api,
|
||||
ConfigFn: api.GetConfig,
|
||||
}
|
||||
|
||||
var db store.Store
|
||||
db, err = sqlstore.New(storeParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error initializing the DB: %w", err)
|
||||
}
|
||||
if cfg.AuthMode == server.MattermostAuthMod {
|
||||
layeredStore, err2 := mattermostauthlayer.New(cfg.DBType, sqlDB, db, logger, api, storeParams.TablePrefix)
|
||||
if err2 != nil {
|
||||
return nil, fmt.Errorf("error initializing the DB: %w", err2)
|
||||
}
|
||||
db = layeredStore
|
||||
}
|
||||
|
||||
permissionsService := mmpermissions.New(db, api, logger)
|
||||
|
||||
wsPluginAdapter := ws.NewPluginAdapter(api, auth.New(cfg, db, permissionsService), db, logger)
|
||||
|
||||
backendParams := notifyBackendParams{
|
||||
cfg: cfg,
|
||||
servicesAPI: api,
|
||||
appAPI: &appAPI{store: db},
|
||||
permissions: permissionsService,
|
||||
serverRoot: baseURL + "/boards",
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
var notifyBackends []notify.Backend
|
||||
|
||||
mentionsBackend, err := createMentionsNotifyBackend(backendParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating mention notifications backend: %w", err)
|
||||
}
|
||||
notifyBackends = append(notifyBackends, mentionsBackend)
|
||||
|
||||
subscriptionsBackend, err2 := createSubscriptionsNotifyBackend(backendParams)
|
||||
if err2 != nil {
|
||||
return nil, fmt.Errorf("error creating subscription notifications backend: %w", err2)
|
||||
}
|
||||
notifyBackends = append(notifyBackends, subscriptionsBackend)
|
||||
mentionsBackend.AddListener(subscriptionsBackend)
|
||||
|
||||
params := server.Params{
|
||||
Cfg: cfg,
|
||||
SingleUserToken: "",
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
ServerID: serverID,
|
||||
WSAdapter: wsPluginAdapter,
|
||||
NotifyBackends: notifyBackends,
|
||||
PermissionsService: permissionsService,
|
||||
IsPlugin: true,
|
||||
}
|
||||
|
||||
server, err := server.New(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error initializing the server: %w", err)
|
||||
}
|
||||
|
||||
backendParams.appAPI.init(db, server.App())
|
||||
|
||||
// ToDo: Cloud Limits have been disabled by design. We should
|
||||
// revisit the decision and update the related code accordingly
|
||||
/*
|
||||
if utils.IsCloudLicense(api.GetLicense()) {
|
||||
limits, err := api.GetCloudLimits()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching cloud limits when starting Boards: %w", err)
|
||||
}
|
||||
|
||||
if err := server.App().SetCloudLimits(limits); err != nil {
|
||||
return nil, fmt.Errorf("error setting cloud limits when starting Boards: %w", err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return &BoardsApp{
|
||||
server: server,
|
||||
wsPluginAdapter: wsPluginAdapter,
|
||||
servicesAPI: api,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BoardsApp) Start() error {
|
||||
if err := b.server.Start(); err != nil {
|
||||
return fmt.Errorf("error starting Boards server: %w", err)
|
||||
}
|
||||
|
||||
b.servicesAPI.RegisterRouter(b.server.GetRootRouter())
|
||||
|
||||
b.logger.Info("Boards product successfully started.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BoardsApp) Stop() error {
|
||||
return b.server.Shutdown()
|
||||
}
|
||||
|
||||
//
|
||||
// These callbacks are called automatically by the suite server.
|
||||
//
|
||||
|
||||
func (b *BoardsApp) MessageWillBePosted(_ *plugin.Context, post *mm_model.Post) (*mm_model.Post, string) {
|
||||
return postWithBoardsEmbed(post), ""
|
||||
}
|
||||
|
||||
func (b *BoardsApp) MessageWillBeUpdated(_ *plugin.Context, newPost, _ *mm_model.Post) (*mm_model.Post, string) {
|
||||
return postWithBoardsEmbed(newPost), ""
|
||||
}
|
||||
|
||||
func (b *BoardsApp) OnWebSocketConnect(webConnID, userID string) {
|
||||
b.wsPluginAdapter.OnWebSocketConnect(webConnID, userID)
|
||||
}
|
||||
|
||||
func (b *BoardsApp) OnWebSocketDisconnect(webConnID, userID string) {
|
||||
b.wsPluginAdapter.OnWebSocketDisconnect(webConnID, userID)
|
||||
}
|
||||
|
||||
func (b *BoardsApp) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) {
|
||||
b.wsPluginAdapter.WebSocketMessageHasBeenPosted(webConnID, userID, req)
|
||||
}
|
||||
|
||||
func (b *BoardsApp) OnPluginClusterEvent(_ *plugin.Context, ev mm_model.PluginClusterEvent) {
|
||||
b.wsPluginAdapter.HandleClusterEvent(ev)
|
||||
}
|
||||
|
||||
// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
|
||||
func (b *BoardsApp) ServeHTTP(_ *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||
router := b.server.GetRootRouter()
|
||||
router.ServeHTTP(w, r)
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package boards
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func TestSetConfiguration(t *testing.T) {
|
||||
boolTrue := true
|
||||
stringRef := ""
|
||||
|
||||
baseFeatureFlags := &model.FeatureFlags{}
|
||||
basePluginSettings := &model.PluginSettings{
|
||||
Directory: &stringRef,
|
||||
}
|
||||
driverName := "testDriver"
|
||||
dataSource := "testDirectory"
|
||||
baseSQLSettings := &model.SqlSettings{
|
||||
DriverName: &driverName,
|
||||
DataSource: &dataSource,
|
||||
}
|
||||
|
||||
directory := "testDirectory"
|
||||
baseFileSettings := &model.FileSettings{
|
||||
DriverName: &driverName,
|
||||
Directory: &directory,
|
||||
MaxFileSize: model.NewInt64(1024 * 1024),
|
||||
}
|
||||
|
||||
days := 365
|
||||
baseDataRetentionSettings := &model.DataRetentionSettings{
|
||||
BoardsRetentionDays: &days,
|
||||
}
|
||||
usernameRef := "username"
|
||||
baseTeamSettings := &model.TeamSettings{
|
||||
TeammateNameDisplay: &usernameRef,
|
||||
}
|
||||
|
||||
falseRef := false
|
||||
basePrivacySettings := &model.PrivacySettings{
|
||||
ShowEmailAddress: &falseRef,
|
||||
ShowFullName: &falseRef,
|
||||
}
|
||||
|
||||
baseConfig := &model.Config{
|
||||
FeatureFlags: baseFeatureFlags,
|
||||
PluginSettings: *basePluginSettings,
|
||||
SqlSettings: *baseSQLSettings,
|
||||
FileSettings: *baseFileSettings,
|
||||
DataRetentionSettings: *baseDataRetentionSettings,
|
||||
TeamSettings: *baseTeamSettings,
|
||||
PrivacySettings: *basePrivacySettings,
|
||||
}
|
||||
|
||||
t.Run("test boards feature flags", func(t *testing.T) {
|
||||
featureFlags := &model.FeatureFlags{
|
||||
TestFeature: "test",
|
||||
TestBoolFeature: boolTrue,
|
||||
}
|
||||
|
||||
mmConfig := baseConfig
|
||||
mmConfig.FeatureFlags = featureFlags
|
||||
|
||||
config := createBoardsConfig(*mmConfig, "", "")
|
||||
assert.Equal(t, "true", config.FeatureFlags["TestBoolFeature"])
|
||||
assert.Equal(t, "test", config.FeatureFlags["TestFeature"])
|
||||
})
|
||||
|
||||
t.Run("test enable telemetry", func(t *testing.T) {
|
||||
logSettings := &model.LogSettings{
|
||||
EnableDiagnostics: &boolTrue,
|
||||
}
|
||||
mmConfig := baseConfig
|
||||
mmConfig.LogSettings = *logSettings
|
||||
|
||||
config := createBoardsConfig(*mmConfig, "", "testId")
|
||||
assert.Equal(t, true, config.Telemetry)
|
||||
assert.Equal(t, "testId", config.TelemetryID)
|
||||
})
|
||||
|
||||
t.Run("test enable shared boards", func(t *testing.T) {
|
||||
mmConfig := baseConfig
|
||||
mmConfig.PluginSettings.Plugins = make(map[string]map[string]interface{})
|
||||
mmConfig.PluginSettings.Plugins[PluginName] = make(map[string]interface{})
|
||||
mmConfig.PluginSettings.Plugins[PluginName][SharedBoardsName] = true
|
||||
config := createBoardsConfig(*mmConfig, "", "")
|
||||
assert.Equal(t, true, config.EnablePublicSharedBoards)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
b := &BoardsApp{
|
||||
server: th.Server,
|
||||
logger: mlog.CreateConsoleTestLogger(t),
|
||||
}
|
||||
|
||||
assert := assert.New(t)
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/hello", nil)
|
||||
|
||||
b.ServeHTTP(nil, w, r)
|
||||
|
||||
result := w.Result()
|
||||
assert.NotNil(result)
|
||||
defer result.Body.Close()
|
||||
bodyBytes, err := io.ReadAll(result.Body)
|
||||
assert.Nil(err)
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
assert.Equal("Hello", bodyString)
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package boards
|
||||
|
||||
import (
|
||||
"math"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
const defaultS3Timeout = 60 * 1000 // 60 seconds
|
||||
|
||||
func createBoardsConfig(mmconfig mm_model.Config, baseURL string, serverID string) *config.Configuration {
|
||||
filesS3Config := config.AmazonS3Config{}
|
||||
if mmconfig.FileSettings.AmazonS3AccessKeyId != nil {
|
||||
filesS3Config.AccessKeyID = *mmconfig.FileSettings.AmazonS3AccessKeyId
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3SecretAccessKey != nil {
|
||||
filesS3Config.SecretAccessKey = *mmconfig.FileSettings.AmazonS3SecretAccessKey
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3Bucket != nil {
|
||||
filesS3Config.Bucket = *mmconfig.FileSettings.AmazonS3Bucket
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3PathPrefix != nil {
|
||||
filesS3Config.PathPrefix = *mmconfig.FileSettings.AmazonS3PathPrefix
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3Region != nil {
|
||||
filesS3Config.Region = *mmconfig.FileSettings.AmazonS3Region
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3Endpoint != nil {
|
||||
filesS3Config.Endpoint = *mmconfig.FileSettings.AmazonS3Endpoint
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3SSL != nil {
|
||||
filesS3Config.SSL = *mmconfig.FileSettings.AmazonS3SSL
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3SignV2 != nil {
|
||||
filesS3Config.SignV2 = *mmconfig.FileSettings.AmazonS3SignV2
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3SSE != nil {
|
||||
filesS3Config.SSE = *mmconfig.FileSettings.AmazonS3SSE
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3Trace != nil {
|
||||
filesS3Config.Trace = *mmconfig.FileSettings.AmazonS3Trace
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3RequestTimeoutMilliseconds != nil && *mmconfig.FileSettings.AmazonS3RequestTimeoutMilliseconds > 0 {
|
||||
filesS3Config.Timeout = *mmconfig.FileSettings.AmazonS3RequestTimeoutMilliseconds
|
||||
} else {
|
||||
filesS3Config.Timeout = defaultS3Timeout
|
||||
}
|
||||
|
||||
enableTelemetry := false
|
||||
if mmconfig.LogSettings.EnableDiagnostics != nil {
|
||||
enableTelemetry = *mmconfig.LogSettings.EnableDiagnostics
|
||||
}
|
||||
|
||||
enablePublicSharedBoards := false
|
||||
if mmconfig.PluginSettings.Plugins[PluginName][SharedBoardsName] == true {
|
||||
enablePublicSharedBoards = true
|
||||
}
|
||||
|
||||
enableBoardsDeletion := false
|
||||
if mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil {
|
||||
enableBoardsDeletion = true
|
||||
}
|
||||
|
||||
featureFlags := parseFeatureFlags(mmconfig.FeatureFlags.ToMap())
|
||||
|
||||
showEmailAddress := false
|
||||
if mmconfig.PrivacySettings.ShowEmailAddress != nil {
|
||||
showEmailAddress = *mmconfig.PrivacySettings.ShowEmailAddress
|
||||
}
|
||||
|
||||
showFullName := false
|
||||
if mmconfig.PrivacySettings.ShowFullName != nil {
|
||||
showFullName = *mmconfig.PrivacySettings.ShowFullName
|
||||
}
|
||||
|
||||
serverRoot := baseURL + "/plugins/focalboard"
|
||||
|
||||
return &config.Configuration{
|
||||
ServerRoot: serverRoot,
|
||||
Port: -1,
|
||||
DBType: *mmconfig.SqlSettings.DriverName,
|
||||
DBConfigString: *mmconfig.SqlSettings.DataSource,
|
||||
DBTablePrefix: "focalboard_",
|
||||
UseSSL: false,
|
||||
SecureCookie: true,
|
||||
WebPath: path.Join(*mmconfig.PluginSettings.Directory, "focalboard", "pack"),
|
||||
FilesDriver: *mmconfig.FileSettings.DriverName,
|
||||
FilesPath: *mmconfig.FileSettings.Directory,
|
||||
FilesS3Config: filesS3Config,
|
||||
MaxFileSize: *mmconfig.FileSettings.MaxFileSize,
|
||||
Telemetry: enableTelemetry,
|
||||
TelemetryID: serverID,
|
||||
WebhookUpdate: []string{},
|
||||
SessionExpireTime: 2592000,
|
||||
SessionRefreshTime: 18000,
|
||||
LocalOnly: false,
|
||||
EnableLocalMode: false,
|
||||
LocalModeSocketLocation: "",
|
||||
AuthMode: "mattermost",
|
||||
EnablePublicSharedBoards: enablePublicSharedBoards,
|
||||
FeatureFlags: featureFlags,
|
||||
NotifyFreqCardSeconds: getPluginSettingInt(mmconfig, notifyFreqCardSecondsKey, 120),
|
||||
NotifyFreqBoardSeconds: getPluginSettingInt(mmconfig, notifyFreqBoardSecondsKey, 86400),
|
||||
EnableDataRetention: enableBoardsDeletion,
|
||||
DataRetentionDays: *mmconfig.DataRetentionSettings.BoardsRetentionDays,
|
||||
TeammateNameDisplay: *mmconfig.TeamSettings.TeammateNameDisplay,
|
||||
ShowEmailAddress: showEmailAddress,
|
||||
ShowFullName: showFullName,
|
||||
}
|
||||
}
|
||||
|
||||
func parseFeatureFlags(configFeatureFlags map[string]string) map[string]string {
|
||||
featureFlags := make(map[string]string)
|
||||
for key, value := range configFeatureFlags {
|
||||
// Break out FeatureFlags and pass remaining
|
||||
if key == boardsFeatureFlagName {
|
||||
for _, flag := range strings.Split(value, "-") {
|
||||
featureFlags[flag] = "true"
|
||||
}
|
||||
} else {
|
||||
featureFlags[key] = value
|
||||
}
|
||||
}
|
||||
return featureFlags
|
||||
}
|
||||
|
||||
func getPluginSetting(mmConfig mm_model.Config, key string) (interface{}, bool) {
|
||||
plugin, ok := mmConfig.PluginSettings.Plugins[PluginName]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
val, ok := plugin[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return val, true
|
||||
}
|
||||
|
||||
func getPluginSettingInt(mmConfig mm_model.Config, key string, def int) int {
|
||||
val, ok := getPluginSetting(mmConfig, key)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
valFloat, ok := val.(float64)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
return int(math.Round(valFloat))
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
package boards
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// configuration captures the plugin's external configuration as exposed in the Mattermost server
|
||||
// configuration, as well as values computed from the configuration. Any public fields will be
|
||||
// deserialized from the Mattermost server configuration in OnConfigurationChange.
|
||||
//
|
||||
// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin
|
||||
// configuration can change at any time, access to the configuration must be synchronized. The
|
||||
// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire
|
||||
// struct whenever it changes. You may replace this with whatever strategy you choose.
|
||||
//
|
||||
// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
|
||||
// copy appropriate for your types.
|
||||
type configuration struct {
|
||||
EnablePublicSharedBoards bool
|
||||
}
|
||||
|
||||
// Clone shallow copies the configuration. Your implementation may require a deep copy if
|
||||
// your configuration has reference types.
|
||||
func (c *configuration) Clone() *configuration {
|
||||
var clone = *c
|
||||
return &clone
|
||||
}
|
||||
|
||||
// getConfiguration retrieves the active configuration under lock, making it safe to use
|
||||
// concurrently. The active configuration may change underneath the client of this method, but
|
||||
// the struct returned by this API call is considered immutable.
|
||||
func (b *BoardsApp) getConfiguration() *configuration {
|
||||
b.configurationLock.RLock()
|
||||
defer b.configurationLock.RUnlock()
|
||||
|
||||
if b.configuration == nil {
|
||||
return &configuration{}
|
||||
}
|
||||
|
||||
return b.configuration
|
||||
}
|
||||
|
||||
// setConfiguration replaces the active configuration under lock.
|
||||
//
|
||||
// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not
|
||||
// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a
|
||||
// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur.
|
||||
//
|
||||
// This method panics if setConfiguration is called with the existing configuration. This almost
|
||||
// certainly means that the configuration was modified without being cloned and may result in
|
||||
// an unsafe access.
|
||||
func (b *BoardsApp) setConfiguration(configuration *configuration) {
|
||||
b.configurationLock.Lock()
|
||||
defer b.configurationLock.Unlock()
|
||||
|
||||
if configuration != nil && b.configuration == configuration {
|
||||
// Ignore assignment if the configuration struct is empty. Go will optimize the
|
||||
// allocation for same to point at the same memory address, breaking the check
|
||||
// above.
|
||||
if reflect.ValueOf(*configuration).NumField() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
panic("setConfiguration called with the existing configuration")
|
||||
}
|
||||
|
||||
b.configuration = configuration
|
||||
}
|
||||
|
||||
// OnConfigurationChange is invoked when configuration changes may have been made.
|
||||
func (b *BoardsApp) OnConfigurationChange() error {
|
||||
// Have we been setup by OnActivate?
|
||||
if b.server == nil {
|
||||
return nil
|
||||
}
|
||||
mmconfig := b.servicesAPI.GetConfig()
|
||||
|
||||
enableShareBoards := false
|
||||
if mmconfig.PluginSettings.Plugins[PluginName][SharedBoardsName] == true {
|
||||
enableShareBoards = true
|
||||
}
|
||||
|
||||
configuration := &configuration{
|
||||
EnablePublicSharedBoards: enableShareBoards,
|
||||
}
|
||||
b.setConfiguration(configuration)
|
||||
b.server.Config().EnablePublicSharedBoards = enableShareBoards
|
||||
|
||||
// handle Data Retention settings
|
||||
enableBoardsDeletion := false
|
||||
if mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil {
|
||||
enableBoardsDeletion = true
|
||||
}
|
||||
b.server.Config().EnableDataRetention = enableBoardsDeletion
|
||||
b.server.Config().DataRetentionDays = *mmconfig.DataRetentionSettings.BoardsRetentionDays
|
||||
b.server.Config().TeammateNameDisplay = *mmconfig.TeamSettings.TeammateNameDisplay
|
||||
showEmailAddress := false
|
||||
if mmconfig.PrivacySettings.ShowEmailAddress != nil {
|
||||
showEmailAddress = *mmconfig.PrivacySettings.ShowEmailAddress
|
||||
}
|
||||
b.server.Config().ShowEmailAddress = showEmailAddress
|
||||
showFullName := false
|
||||
if mmconfig.PrivacySettings.ShowFullName != nil {
|
||||
showFullName = *mmconfig.PrivacySettings.ShowFullName
|
||||
}
|
||||
b.server.Config().ShowFullName = showFullName
|
||||
maxFileSize := int64(0)
|
||||
if mmconfig.FileSettings.MaxFileSize != nil {
|
||||
maxFileSize = *mmconfig.FileSettings.MaxFileSize
|
||||
}
|
||||
b.server.Config().MaxFileSize = maxFileSize
|
||||
|
||||
b.server.UpdateAppConfig()
|
||||
b.wsPluginAdapter.BroadcastConfigChange(*b.server.App().GetClientConfig())
|
||||
return nil
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package boards
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/integrationtests"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
||||
mockservicesapi "github.com/mattermost/focalboard/server/model/mocks"
|
||||
|
||||
serverModel "github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
Server *server.Server
|
||||
}
|
||||
|
||||
func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
th := &TestHelper{}
|
||||
th.Server = newTestServer()
|
||||
|
||||
err := th.Server.Start()
|
||||
require.NoError(t, err, "Server start should not error")
|
||||
|
||||
tearDown := func() {
|
||||
err := th.Server.Shutdown()
|
||||
require.NoError(t, err, "Server shutdown should not error")
|
||||
}
|
||||
return th, tearDown
|
||||
}
|
||||
|
||||
func newTestServer() *server.Server {
|
||||
return integrationtests.NewTestServerPluginMode()
|
||||
}
|
||||
func TestConfigurationNullConfiguration(t *testing.T) {
|
||||
boardsApp := &BoardsApp{}
|
||||
assert.NotNil(t, boardsApp.getConfiguration())
|
||||
}
|
||||
|
||||
func TestOnConfigurationChange(t *testing.T) {
|
||||
stringRef := ""
|
||||
|
||||
basePlugins := make(map[string]map[string]interface{})
|
||||
basePlugins[PluginName] = make(map[string]interface{})
|
||||
basePlugins[PluginName][SharedBoardsName] = true
|
||||
|
||||
basePluginSettings := &serverModel.PluginSettings{
|
||||
Directory: &stringRef,
|
||||
Plugins: basePlugins,
|
||||
}
|
||||
intRef := 365
|
||||
baseDataRetentionSettings := &serverModel.DataRetentionSettings{
|
||||
BoardsRetentionDays: &intRef,
|
||||
}
|
||||
usernameRef := "username"
|
||||
baseTeamSettings := &serverModel.TeamSettings{
|
||||
TeammateNameDisplay: &usernameRef,
|
||||
}
|
||||
|
||||
falseRef := false
|
||||
basePrivacySettings := &serverModel.PrivacySettings{
|
||||
ShowEmailAddress: &falseRef,
|
||||
ShowFullName: &falseRef,
|
||||
}
|
||||
|
||||
baseConfig := &serverModel.Config{
|
||||
PluginSettings: *basePluginSettings,
|
||||
DataRetentionSettings: *baseDataRetentionSettings,
|
||||
TeamSettings: *baseTeamSettings,
|
||||
PrivacySettings: *basePrivacySettings,
|
||||
}
|
||||
|
||||
t.Run("Test Load Plugin Success", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
api := mockservicesapi.NewMockServicesAPI(ctrl)
|
||||
api.EXPECT().GetConfig().Return(baseConfig)
|
||||
|
||||
b := &BoardsApp{
|
||||
server: th.Server,
|
||||
wsPluginAdapter: &FakePluginAdapter{},
|
||||
servicesAPI: api,
|
||||
logger: mlog.CreateConsoleTestLogger(&testing.T{}),
|
||||
}
|
||||
|
||||
err := b.OnConfigurationChange()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
|
||||
// make sure both App and Server got updated
|
||||
assert.True(t, b.server.Config().EnablePublicSharedBoards)
|
||||
assert.True(t, b.server.App().GetClientConfig().EnablePublicSharedBoards)
|
||||
})
|
||||
}
|
||||
|
||||
var count = 0
|
||||
|
||||
type FakePluginAdapter struct {
|
||||
ws.PluginAdapter
|
||||
}
|
||||
|
||||
func (c *FakePluginAdapter) BroadcastConfigChange(clientConfig model.ClientConfig) {
|
||||
count++
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
package boards
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrInsufficientLicense = errors.New("appropriate license required")
|
||||
|
||||
func (b *BoardsApp) RunDataRetention(nowTime, batchSize int64) (int64, error) {
|
||||
b.logger.Debug("Boards RunDataRetention")
|
||||
license := b.server.Store().GetLicense()
|
||||
if license == nil || !(*license.Features.DataRetention) {
|
||||
return 0, ErrInsufficientLicense
|
||||
}
|
||||
|
||||
if b.server.Config().EnableDataRetention {
|
||||
boardsRetentionDays := b.server.Config().DataRetentionDays
|
||||
endTimeBoards := convertDaysToCutoff(boardsRetentionDays, time.Unix(nowTime/1000, 0))
|
||||
return b.server.Store().RunDataRetention(endTimeBoards, batchSize)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func convertDaysToCutoff(days int, now time.Time) int64 {
|
||||
upToStartOfDay := now.AddDate(0, 0, -days)
|
||||
cutoffDate := time.Date(upToStartOfDay.Year(), upToStartOfDay.Month(), upToStartOfDay.Day(), 0, 0, 0, 0, time.Local)
|
||||
return cutoffDate.UnixNano() / int64(time.Millisecond)
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package boards
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"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/mockstore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
type TestHelperMockStore struct {
|
||||
Server *server.Server
|
||||
Store *mockstore.MockStore
|
||||
}
|
||||
|
||||
func SetupTestHelperMockStore(t *testing.T) (*TestHelperMockStore, func()) {
|
||||
th := &TestHelperMockStore{}
|
||||
|
||||
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
|
||||
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := mockstore.NewMockStore(ctrl)
|
||||
|
||||
tearDown := func() {
|
||||
defer ctrl.Finish()
|
||||
os.Setenv("FOCALBOARD_UNIT_TESTING", origUnitTesting)
|
||||
}
|
||||
|
||||
th.Server = newTestServerMock(mockStore)
|
||||
th.Store = mockStore
|
||||
|
||||
return th, tearDown
|
||||
}
|
||||
|
||||
func newTestServerMock(mockStore *mockstore.MockStore) *server.Server {
|
||||
config := &config.Configuration{
|
||||
EnableDataRetention: false,
|
||||
DataRetentionDays: 10,
|
||||
FilesDriver: "local",
|
||||
FilesPath: "./files",
|
||||
WebPath: "/",
|
||||
}
|
||||
|
||||
logger, _ := mlog.NewLogger()
|
||||
|
||||
mockStore.EXPECT().GetTeam(gomock.Any()).Return(nil, nil).AnyTimes()
|
||||
mockStore.EXPECT().UpsertTeamSignupToken(gomock.Any()).AnyTimes()
|
||||
mockStore.EXPECT().GetSystemSettings().AnyTimes()
|
||||
mockStore.EXPECT().SetSystemSetting(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
permissionsService := localpermissions.New(mockStore, logger)
|
||||
|
||||
srv, err := server.New(server.Params{
|
||||
Cfg: config,
|
||||
DBStore: mockStore,
|
||||
Logger: logger,
|
||||
PermissionsService: permissionsService,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
func TestRunDataRetention(t *testing.T) {
|
||||
th, tearDown := SetupTestHelperMockStore(t)
|
||||
defer tearDown()
|
||||
|
||||
logger, _ := mlog.NewLogger()
|
||||
b := &BoardsApp{
|
||||
server: th.Server,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
t.Run("test null license", func(t *testing.T) {
|
||||
th.Store.EXPECT().GetLicense().Return(nil)
|
||||
_, err := b.RunDataRetention(now, 10)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, ErrInsufficientLicense, err)
|
||||
})
|
||||
|
||||
t.Run("test invalid license", func(t *testing.T) {
|
||||
falseValue := false
|
||||
|
||||
th.Store.EXPECT().GetLicense().Return(
|
||||
&model.License{
|
||||
Features: &model.Features{
|
||||
DataRetention: &falseValue,
|
||||
},
|
||||
},
|
||||
)
|
||||
_, err := b.RunDataRetention(now, 10)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, ErrInsufficientLicense, err)
|
||||
})
|
||||
|
||||
t.Run("test valid license, invalid config", func(t *testing.T) {
|
||||
trueValue := true
|
||||
th.Store.EXPECT().GetLicense().Return(
|
||||
&model.License{
|
||||
Features: &model.Features{
|
||||
DataRetention: &trueValue,
|
||||
},
|
||||
})
|
||||
|
||||
count, err := b.RunDataRetention(now, 10)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), count)
|
||||
})
|
||||
|
||||
t.Run("test valid license, valid config", func(t *testing.T) {
|
||||
trueValue := true
|
||||
th.Store.EXPECT().GetLicense().Return(
|
||||
&model.License{
|
||||
Features: &model.Features{
|
||||
DataRetention: &trueValue,
|
||||
},
|
||||
})
|
||||
|
||||
th.Store.EXPECT().RunDataRetention(gomock.Any(), int64(10)).Return(int64(100), nil)
|
||||
b.server.Config().EnableDataRetention = true
|
||||
|
||||
count, err := b.RunDataRetention(now, 10)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(100), count)
|
||||
})
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package boards
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
)
|
||||
|
||||
type mutexAPIAdapter struct {
|
||||
api model.ServicesAPI
|
||||
}
|
||||
|
||||
func (m *mutexAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, *mm_model.AppError) {
|
||||
b, err := m.api.KVSetWithOptions(key, value, options)
|
||||
|
||||
var appErr *mm_model.AppError
|
||||
if err != nil {
|
||||
if !errors.As(err, &appErr) {
|
||||
appErr = mm_model.NewAppError("KVSetWithOptions", "", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
return b, appErr
|
||||
}
|
||||
|
||||
func (m *mutexAPIAdapter) LogError(msg string, keyValuePairs ...interface{}) {
|
||||
m.api.GetLogger().Error(msg, mlog.Array("kvpairs", keyValuePairs))
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
package boards
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/notify/notifymentions"
|
||||
"github.com/mattermost/focalboard/server/services/notify/notifysubscriptions"
|
||||
"github.com/mattermost/focalboard/server/services/notify/plugindelivery"
|
||||
"github.com/mattermost/focalboard/server/services/permissions"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
type notifyBackendParams struct {
|
||||
cfg *config.Configuration
|
||||
servicesAPI model.ServicesAPI
|
||||
permissions permissions.PermissionsService
|
||||
appAPI *appAPI
|
||||
serverRoot string
|
||||
logger mlog.LoggerIFace
|
||||
}
|
||||
|
||||
func createMentionsNotifyBackend(params notifyBackendParams) (*notifymentions.Backend, error) {
|
||||
delivery, err := createDelivery(params.servicesAPI, params.serverRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backendParams := notifymentions.BackendParams{
|
||||
AppAPI: params.appAPI,
|
||||
Permissions: params.permissions,
|
||||
Delivery: delivery,
|
||||
Logger: params.logger,
|
||||
}
|
||||
|
||||
backend := notifymentions.New(backendParams)
|
||||
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
func createSubscriptionsNotifyBackend(params notifyBackendParams) (*notifysubscriptions.Backend, error) {
|
||||
delivery, err := createDelivery(params.servicesAPI, params.serverRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backendParams := notifysubscriptions.BackendParams{
|
||||
ServerRoot: params.serverRoot,
|
||||
AppAPI: params.appAPI,
|
||||
Permissions: params.permissions,
|
||||
Delivery: delivery,
|
||||
Logger: params.logger,
|
||||
NotifyFreqCardSeconds: params.cfg.NotifyFreqCardSeconds,
|
||||
NotifyFreqBoardSeconds: params.cfg.NotifyFreqBoardSeconds,
|
||||
}
|
||||
backend := notifysubscriptions.New(backendParams)
|
||||
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
func createDelivery(servicesAPI model.ServicesAPI, serverRoot string) (*plugindelivery.PluginDelivery, error) {
|
||||
bot := model.FocalboardBot
|
||||
|
||||
botID, err := servicesAPI.EnsureBot(bot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure %s bot: %w", bot.DisplayName, err)
|
||||
}
|
||||
|
||||
return plugindelivery.New(botID, serverRoot, servicesAPI), nil
|
||||
}
|
||||
|
||||
type appIface interface {
|
||||
CreateSubscription(sub *model.Subscription) (*model.Subscription, error)
|
||||
AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error)
|
||||
}
|
||||
|
||||
// appAPI provides app and store APIs for notification services. Where appropriate calls are made to the
|
||||
// app layer to leverage the additional websocket notification logic present there, and other times the
|
||||
// store APIs are called directly.
|
||||
type appAPI struct {
|
||||
store store.Store
|
||||
app appIface
|
||||
}
|
||||
|
||||
func (a *appAPI) init(store store.Store, app appIface) {
|
||||
a.store = store
|
||||
a.app = app
|
||||
}
|
||||
|
||||
func (a *appAPI) GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) {
|
||||
return a.store.GetBlockHistory(blockID, opts)
|
||||
}
|
||||
|
||||
func (a *appAPI) GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
|
||||
return a.store.GetBlockHistoryNewestChildren(parentID, opts)
|
||||
}
|
||||
|
||||
func (a *appAPI) GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) {
|
||||
return a.store.GetBoardAndCardByID(blockID)
|
||||
}
|
||||
|
||||
func (a *appAPI) GetUserByID(userID string) (*model.User, error) {
|
||||
return a.store.GetUserByID(userID)
|
||||
}
|
||||
|
||||
func (a *appAPI) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) {
|
||||
return a.app.CreateSubscription(sub)
|
||||
}
|
||||
|
||||
func (a *appAPI) GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) {
|
||||
return a.store.GetSubscribersForBlock(blockID)
|
||||
}
|
||||
|
||||
func (a *appAPI) UpdateSubscribersNotifiedAt(blockID string, notifyAt int64) error {
|
||||
return a.store.UpdateSubscribersNotifiedAt(blockID, notifyAt)
|
||||
}
|
||||
|
||||
func (a *appAPI) UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) {
|
||||
return a.store.UpsertNotificationHint(hint, notificationFreq)
|
||||
}
|
||||
|
||||
func (a *appAPI) GetNextNotificationHint(remove bool) (*model.NotificationHint, error) {
|
||||
return a.store.GetNextNotificationHint(remove)
|
||||
}
|
||||
|
||||
func (a *appAPI) GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) {
|
||||
return a.store.GetMemberForBoard(boardID, userID)
|
||||
}
|
||||
|
||||
func (a *appAPI) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) {
|
||||
return a.app.AddMemberToBoard(member)
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package boards
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/markdown"
|
||||
)
|
||||
|
||||
func postWithBoardsEmbed(post *mm_model.Post) *mm_model.Post {
|
||||
if _, ok := post.GetProps()["boards"]; ok {
|
||||
post.AddProp("boards", nil)
|
||||
}
|
||||
|
||||
firstLink, newPostMessage := getFirstLinkAndShortenAllBoardsLink(post.Message)
|
||||
post.Message = newPostMessage
|
||||
|
||||
if firstLink == "" {
|
||||
return post
|
||||
}
|
||||
|
||||
u, err := url.Parse(firstLink)
|
||||
|
||||
if err != nil {
|
||||
return post
|
||||
}
|
||||
|
||||
// Trim away the first / because otherwise after we split the string, the first element in the array is a empty element
|
||||
urlPath := u.Path
|
||||
urlPath = strings.TrimPrefix(urlPath, "/")
|
||||
urlPath = strings.TrimSuffix(urlPath, "/")
|
||||
pathSplit := strings.Split(strings.ToLower(urlPath), "/")
|
||||
queryParams := u.Query()
|
||||
|
||||
if len(pathSplit) == 0 {
|
||||
return post
|
||||
}
|
||||
|
||||
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||
|
||||
if teamID != "" && boardID != "" && viewID != "" && cardID != "" {
|
||||
b, _ := json.Marshal(BoardsEmbed{
|
||||
TeamID: teamID,
|
||||
BoardID: boardID,
|
||||
ViewID: viewID,
|
||||
CardID: cardID,
|
||||
ReadToken: queryParams.Get("r"),
|
||||
OriginalPath: u.RequestURI(),
|
||||
})
|
||||
|
||||
BoardsPostEmbed := &mm_model.PostEmbed{
|
||||
Type: mm_model.PostEmbedBoards,
|
||||
Data: string(b),
|
||||
}
|
||||
|
||||
if post.Metadata == nil {
|
||||
post.Metadata = &mm_model.PostMetadata{}
|
||||
}
|
||||
|
||||
post.Metadata.Embeds = []*mm_model.PostEmbed{BoardsPostEmbed}
|
||||
post.AddProp("boards", string(b))
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
func getFirstLinkAndShortenAllBoardsLink(postMessage string) (firstLink, newPostMessage string) {
|
||||
newPostMessage = postMessage
|
||||
seenLinks := make(map[string]bool)
|
||||
markdown.Inspect(postMessage, func(blockOrInline interface{}) bool {
|
||||
if autoLink, ok := blockOrInline.(*markdown.Autolink); ok {
|
||||
link := autoLink.Destination()
|
||||
|
||||
if firstLink == "" {
|
||||
firstLink = link
|
||||
}
|
||||
|
||||
if seen := seenLinks[link]; !seen && isBoardsLink(link) {
|
||||
// TODO: Make sure that <Jump To Card> is Internationalized and translated to the Users Language preference
|
||||
markdownFormattedLink := fmt.Sprintf("[%s](%s)", "<Jump To Card>", link)
|
||||
newPostMessage = strings.ReplaceAll(newPostMessage, link, markdownFormattedLink)
|
||||
seenLinks[link] = true
|
||||
}
|
||||
}
|
||||
if inlineLink, ok := blockOrInline.(*markdown.InlineLink); ok {
|
||||
if link := inlineLink.Destination(); firstLink == "" {
|
||||
firstLink = link
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return firstLink, newPostMessage
|
||||
}
|
||||
|
||||
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++ {
|
||||
if pathArray[i] == "boards" || pathArray[i] == "plugins" {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == -1 {
|
||||
return teamID, boardID, viewID, cardID
|
||||
}
|
||||
|
||||
// If at index, the parameter in the path is boards,
|
||||
// then we've copied this directly as logged in user of that board
|
||||
|
||||
// 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/team/teamID/boardID/viewID/cardID
|
||||
|
||||
// For card links copied on a shared board, the path looks like
|
||||
// {...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] == "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] == "team" &&
|
||||
pathArray[index+4] == "shared" { // This is a shared board card link
|
||||
teamID = pathArray[index+3]
|
||||
boardID = pathArray[index+5]
|
||||
viewID = pathArray[index+6]
|
||||
cardID = pathArray[index+7]
|
||||
}
|
||||
return teamID, boardID, viewID, cardID
|
||||
}
|
||||
|
||||
func isBoardsLink(link string) bool {
|
||||
u, err := url.Parse(link)
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
urlPath := u.Path
|
||||
urlPath = strings.TrimPrefix(urlPath, "/")
|
||||
urlPath = strings.TrimSuffix(urlPath, "/")
|
||||
pathSplit := strings.Split(strings.ToLower(urlPath), "/")
|
||||
|
||||
if len(pathSplit) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||
return teamID != "" && boardID != "" && viewID != "" && cardID != ""
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
pluginapi "github.com/mattermost/mattermost/server/public/pluginapi"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginTargetType = "focalboard_plugin_adapter"
|
||||
)
|
||||
|
||||
// pluginTargetFactory creates a plugin log adapter when a custom target type appears in
|
||||
// the logging configuration.
|
||||
type pluginTargetFactory struct {
|
||||
logService *pluginapi.LogService
|
||||
}
|
||||
|
||||
func newPluginTargetFactory(logService *pluginapi.LogService) pluginTargetFactory {
|
||||
return pluginTargetFactory{
|
||||
logService: logService,
|
||||
}
|
||||
}
|
||||
|
||||
func (ptf pluginTargetFactory) createTarget(targetType string, options json.RawMessage) (mlog.Target, error) {
|
||||
if targetType != pluginTargetType {
|
||||
return nil, ErrInvalidTargetType{targetType}
|
||||
}
|
||||
return newPluginAdapterTarget(ptf.logService), nil
|
||||
}
|
||||
|
||||
// pluginLogAdapter is a simple log target that writes to the plugin API.
|
||||
type pluginLogAdapter struct {
|
||||
logService *pluginapi.LogService
|
||||
}
|
||||
|
||||
func newPluginAdapterTarget(logService *pluginapi.LogService) mlog.Target {
|
||||
return &pluginLogAdapter{
|
||||
logService: logService,
|
||||
}
|
||||
}
|
||||
|
||||
func (pla *pluginLogAdapter) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pla *pluginLogAdapter) Shutdown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pla *pluginLogAdapter) Write(p []byte, rec *mlog.LogRec) (int, error) {
|
||||
fields := rec.Fields()
|
||||
|
||||
args := make([]interface{}, 0, len(fields)*2)
|
||||
buf := &bytes.Buffer{}
|
||||
var err error
|
||||
for _, fld := range fields {
|
||||
err = fld.ValueString(buf, mlog.ShouldQuote)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
args = append(args, fld.Key, buf.String())
|
||||
buf.Reset()
|
||||
}
|
||||
|
||||
switch rec.Level() {
|
||||
case mlog.LvlDebug:
|
||||
pla.logService.Debug(rec.Msg(), args...)
|
||||
case mlog.LvlError:
|
||||
pla.logService.Error(rec.Msg(), args...)
|
||||
case mlog.LvlInfo:
|
||||
pla.logService.Info(rec.Msg(), args...)
|
||||
case mlog.LvlWarn:
|
||||
pla.logService.Warn(rec.Msg(), args...)
|
||||
case mlog.LvlCritical, mlog.LvlFatal:
|
||||
args = append(args, mlog.String("level", rec.Level().Name))
|
||||
pla.logService.Error(rec.Msg(), args...)
|
||||
default:
|
||||
args = append(args, mlog.String("level", rec.Level().Name))
|
||||
pla.logService.Info(rec.Msg(), args...)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// ErrInvalidTargetType is returned when a log config factory does not recognize the
|
||||
// target type.
|
||||
type ErrInvalidTargetType struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (e ErrInvalidTargetType) Error() string {
|
||||
return fmt.Sprintf("invalid log target type '%s'", e.name)
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&Plugin{})
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
// This file is automatically generated. Do not modify it manually.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
var manifest *model.Manifest
|
||||
|
||||
const manifestStr = `
|
||||
{
|
||||
"id": "focalboard",
|
||||
"name": "Mattermost Boards",
|
||||
"description": "The Mattermost Boards plugin",
|
||||
"homepage_url": "https://github.com/mattermost/focalboard",
|
||||
"support_url": "https://github.com/mattermost/focalboard/issues",
|
||||
"release_notes_url": "https://github.com/mattermost/focalboard/releases",
|
||||
"icon_path": "assets/starter-template-icon.svg",
|
||||
"version": "8.0.0",
|
||||
"min_server_version": "7.2.0",
|
||||
"server": {
|
||||
"executables": {
|
||||
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
||||
"darwin-arm64": "server/dist/plugin-darwin-arm64",
|
||||
"linux-amd64": "server/dist/plugin-linux-amd64",
|
||||
"linux-arm64": "server/dist/plugin-linux-arm64",
|
||||
"windows-amd64": "server/dist/plugin-windows-amd64.exe"
|
||||
},
|
||||
"executable": ""
|
||||
},
|
||||
"webapp": {
|
||||
"bundle_path": "webapp/dist/main.js"
|
||||
},
|
||||
"settings_schema": {
|
||||
"header": "",
|
||||
"footer": "",
|
||||
"settings": [
|
||||
{
|
||||
"key": "EnablePublicSharedBoards",
|
||||
"display_name": "Enable Publicly-Shared Boards:",
|
||||
"type": "bool",
|
||||
"help_text": "This allows board editors to share boards that can be accessed by anyone with the link.",
|
||||
"placeholder": "",
|
||||
"default": false,
|
||||
"hosting": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func init() {
|
||||
_ = json.NewDecoder(strings.NewReader(manifestStr)).Decode(&manifest)
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/focalboard/mattermost-plugin/server/boards"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
pluginapi "github.com/mattermost/mattermost/server/public/pluginapi"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
var ErrPluginNotAllowed = errors.New("boards plugin not allowed while Boards product enabled")
|
||||
|
||||
// Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes.
|
||||
type Plugin struct {
|
||||
plugin.MattermostPlugin
|
||||
boardsApp *boards.BoardsApp
|
||||
}
|
||||
|
||||
func (p *Plugin) OnActivate() error {
|
||||
client := pluginapi.NewClient(p.API, p.Driver)
|
||||
|
||||
logger, _ := mlog.NewLogger()
|
||||
pluginTargetFactory := newPluginTargetFactory(&client.Log)
|
||||
factories := &mlog.Factories{
|
||||
TargetFactory: pluginTargetFactory.createTarget,
|
||||
}
|
||||
cfgJSON := defaultLoggingConfig()
|
||||
err := logger.Configure("", cfgJSON, factories)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adapter := newServiceAPIAdapter(p.API, client.Store, logger)
|
||||
|
||||
boardsApp, err := boards.NewBoardsApp(adapter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot activate plugin: %w", err)
|
||||
}
|
||||
|
||||
model.LogServerInfo(logger)
|
||||
|
||||
p.boardsApp = boardsApp
|
||||
return p.boardsApp.Start()
|
||||
}
|
||||
|
||||
// OnConfigurationChange is invoked when configuration changes may have been made.
|
||||
func (p *Plugin) OnConfigurationChange() error {
|
||||
// Have we been setup by OnActivate?
|
||||
if p.boardsApp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.boardsApp.OnConfigurationChange()
|
||||
}
|
||||
|
||||
func (p *Plugin) OnWebSocketConnect(webConnID, userID string) {
|
||||
p.boardsApp.OnWebSocketConnect(webConnID, userID)
|
||||
}
|
||||
|
||||
func (p *Plugin) OnWebSocketDisconnect(webConnID, userID string) {
|
||||
p.boardsApp.OnWebSocketDisconnect(webConnID, userID)
|
||||
}
|
||||
|
||||
func (p *Plugin) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) {
|
||||
p.boardsApp.WebSocketMessageHasBeenPosted(webConnID, userID, req)
|
||||
}
|
||||
|
||||
func (p *Plugin) OnDeactivate() error {
|
||||
return p.boardsApp.Stop()
|
||||
}
|
||||
|
||||
func (p *Plugin) OnPluginClusterEvent(ctx *plugin.Context, ev mm_model.PluginClusterEvent) {
|
||||
p.boardsApp.OnPluginClusterEvent(ctx, ev)
|
||||
}
|
||||
|
||||
func (p *Plugin) MessageWillBePosted(ctx *plugin.Context, post *mm_model.Post) (*mm_model.Post, string) {
|
||||
return p.boardsApp.MessageWillBePosted(ctx, post)
|
||||
}
|
||||
|
||||
func (p *Plugin) MessageWillBeUpdated(ctx *plugin.Context, newPost, oldPost *mm_model.Post) (*mm_model.Post, string) {
|
||||
return p.boardsApp.MessageWillBeUpdated(ctx, newPost, oldPost)
|
||||
}
|
||||
|
||||
func (p *Plugin) RunDataRetention(nowTime, batchSize int64) (int64, error) {
|
||||
return p.boardsApp.RunDataRetention(nowTime, batchSize)
|
||||
}
|
||||
|
||||
// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
|
||||
func (p *Plugin) ServeHTTP(ctx *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||
p.boardsApp.ServeHTTP(ctx, w, r)
|
||||
}
|
||||
|
||||
func defaultLoggingConfig() string {
|
||||
return `
|
||||
{
|
||||
"def": {
|
||||
"type": "focalboard_plugin_adapter",
|
||||
"options": {},
|
||||
"format": "plain",
|
||||
"format_options": {
|
||||
"delim": " ",
|
||||
"min_level_len": 0,
|
||||
"min_msg_len": 0,
|
||||
"enable_color": false,
|
||||
"enable_caller": true
|
||||
},
|
||||
"levels": [
|
||||
{"id": 5, "name": "debug"},
|
||||
{"id": 4, "name": "info", "color": 36},
|
||||
{"id": 3, "name": "warn"},
|
||||
{"id": 2, "name": "error", "color": 31},
|
||||
{"id": 1, "name": "fatal", "stacktrace": true},
|
||||
{"id": 0, "name": "panic", "stacktrace": true}
|
||||
]
|
||||
},
|
||||
"errors_file": {
|
||||
"Type": "file",
|
||||
"Format": "plain",
|
||||
"Levels": [
|
||||
{"ID": 2, "Name": "error", "Stacktrace": true}
|
||||
],
|
||||
"Options": {
|
||||
"Compress": true,
|
||||
"Filename": "focalboard_errors.log",
|
||||
"MaxAgeDays": 0,
|
||||
"MaxBackups": 5,
|
||||
"MaxSizeMB": 10
|
||||
},
|
||||
"MaxQueueSize": 1000
|
||||
}
|
||||
}`
|
||||
}
|
@ -1 +0,0 @@
|
||||
node_modules/
|
@ -1,202 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:cypress/recommended",
|
||||
"plugin:jquery/deprecated"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"babel",
|
||||
"import",
|
||||
"cypress",
|
||||
"jquery",
|
||||
"no-only-tests"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"jest": true,
|
||||
"cypress/globals": true
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": "webpack",
|
||||
"react": {
|
||||
"pragma": "React",
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-expressions": 0,
|
||||
"babel/no-unused-expressions": [2, {"allowShortCircuit": true}],
|
||||
"eol-last": ["error", "always"],
|
||||
"import/no-unresolved": 2,
|
||||
"import/order": [
|
||||
2,
|
||||
{
|
||||
"newlines-between": "always-and-inside-groups",
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
[
|
||||
"internal",
|
||||
"parent"
|
||||
],
|
||||
"sibling",
|
||||
"index"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-undefined": 0,
|
||||
"react/jsx-filename-extension": 0,
|
||||
"react/prop-types": [
|
||||
2,
|
||||
{
|
||||
"ignore": [
|
||||
"location",
|
||||
"history",
|
||||
"component"
|
||||
]
|
||||
}
|
||||
],
|
||||
"react/no-string-refs": 2,
|
||||
"no-only-tests/no-only-tests": ["error", {"focus": ["only", "skip"]}],
|
||||
"max-nested-callbacks": ["error", {"max": 5}]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.tsx", "**/*.ts"],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"import/no-unresolved": 0, // ts handles this better
|
||||
"camelcase": 0,
|
||||
"semi": "off",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
2,
|
||||
{
|
||||
"selector": "function",
|
||||
"format": ["camelCase", "PascalCase"]
|
||||
},
|
||||
{
|
||||
"selector": "variable",
|
||||
"format": ["camelCase", "PascalCase", "UPPER_CASE"]
|
||||
},
|
||||
{
|
||||
"selector": "parameter",
|
||||
"format": ["camelCase", "PascalCase"],
|
||||
"leadingUnderscore": "allow"
|
||||
},
|
||||
{
|
||||
"selector": "typeLike",
|
||||
"format": ["PascalCase"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
2,
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "after-used"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/no-empty-function": 0,
|
||||
"@typescript-eslint/prefer-interface": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/semi": [2, "never"],
|
||||
"@typescript-eslint/indent": [
|
||||
2,
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 0
|
||||
}
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
2,
|
||||
{
|
||||
"classes": false,
|
||||
"functions": false,
|
||||
"variables": false
|
||||
}
|
||||
],
|
||||
"no-useless-constructor": 0,
|
||||
"@typescript-eslint/no-useless-constructor": 2,
|
||||
"react/jsx-filename-extension": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**", "**/*.test.*"],
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"func-names": 0,
|
||||
"global-require": 0,
|
||||
"new-cap": 0,
|
||||
"prefer-arrow-callback": 0,
|
||||
"no-import-assign": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["e2e/**"],
|
||||
"rules": {
|
||||
"func-names": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"max-nested-callbacks": 0,
|
||||
"no-process-env": 0,
|
||||
"babel/no-unused-expressions": 0,
|
||||
"no-unused-expressions": 0,
|
||||
"jquery/no-ajax": 0,
|
||||
"jquery/no-ajax-events": 0,
|
||||
"jquery/no-animate": 0,
|
||||
"jquery/no-attr": 0,
|
||||
"jquery/no-bind": 0,
|
||||
"jquery/no-class": 0,
|
||||
"jquery/no-clone": 0,
|
||||
"jquery/no-closest": 0,
|
||||
"jquery/no-css": 0,
|
||||
"jquery/no-data": 0,
|
||||
"jquery/no-deferred": 0,
|
||||
"jquery/no-delegate": 0,
|
||||
"jquery/no-each": 0,
|
||||
"jquery/no-extend": 0,
|
||||
"jquery/no-fade": 0,
|
||||
"jquery/no-filter": 0,
|
||||
"jquery/no-find": 0,
|
||||
"jquery/no-global-eval": 0,
|
||||
"jquery/no-grep": 0,
|
||||
"jquery/no-has": 0,
|
||||
"jquery/no-hide": 0,
|
||||
"jquery/no-html": 0,
|
||||
"jquery/no-in-array": 0,
|
||||
"jquery/no-is-array": 0,
|
||||
"jquery/no-is-function": 0,
|
||||
"jquery/no-is": 0,
|
||||
"jquery/no-load": 0,
|
||||
"jquery/no-map": 0,
|
||||
"jquery/no-merge": 0,
|
||||
"jquery/no-param": 0,
|
||||
"jquery/no-parent": 0,
|
||||
"jquery/no-parents": 0,
|
||||
"jquery/no-parse-html": 0,
|
||||
"jquery/no-prop": 0,
|
||||
"jquery/no-proxy": 0,
|
||||
"jquery/no-ready": 0,
|
||||
"jquery/no-serialize": 0,
|
||||
"jquery/no-show": 0,
|
||||
"jquery/no-size": 0,
|
||||
"jquery/no-sizzle": 0,
|
||||
"jquery/no-slide": 0,
|
||||
"jquery/no-submit": 0,
|
||||
"jquery/no-text": 0,
|
||||
"jquery/no-toggle": 0,
|
||||
"jquery/no-trigger": 0,
|
||||
"jquery/no-trim": 0,
|
||||
"jquery/no-val": 0,
|
||||
"jquery/no-when": 0,
|
||||
"jquery/no-wrap": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
3
mattermost-plugin/webapp/.gitignore
vendored
3
mattermost-plugin/webapp/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
.eslintcache
|
||||
junit.xml
|
||||
node_modules
|
@ -1 +0,0 @@
|
||||
save-exact=true
|
@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
const config = {
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: {
|
||||
chrome: 66,
|
||||
firefox: 60,
|
||||
edge: 42,
|
||||
safari: 12,
|
||||
},
|
||||
modules: false,
|
||||
corejs: 3,
|
||||
debug: false,
|
||||
useBuiltIns: 'usage',
|
||||
shippedProposals: true,
|
||||
}],
|
||||
['@babel/preset-react', {
|
||||
useBuiltIns: true,
|
||||
}],
|
||||
['@babel/typescript', {
|
||||
allExtensions: true,
|
||||
isTSX: true,
|
||||
}],
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/proposal-object-rest-spread',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'babel-plugin-typescript-to-proptypes',
|
||||
],
|
||||
};
|
||||
|
||||
// Jest needs module transformation
|
||||
config.env = {
|
||||
test: {
|
||||
presets: config.presets,
|
||||
plugins: config.plugins,
|
||||
},
|
||||
};
|
||||
config.env.test.presets[0][1].modules = 'auto';
|
||||
|
||||
module.exports = config;
|
@ -1 +0,0 @@
|
||||
{}
|
@ -1,23 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
function blockList(line) {
|
||||
return line.startsWith('.focalboard-body') ||
|
||||
line.startsWith('.GlobalHeaderComponent') ||
|
||||
line.startsWith('.boards-rhs-icon') ||
|
||||
line.startsWith('.focalboard-plugin-root') ||
|
||||
line.startsWith('.FocalboardUnfurl') ||
|
||||
line.startsWith('.CreateBoardFromTemplate');
|
||||
}
|
||||
|
||||
module.exports = function loader(source) {
|
||||
var newSource = [];
|
||||
source.split('\n').forEach((line) => {
|
||||
if ((line.startsWith('.') || line.startsWith('#')) && !blockList(line)) {
|
||||
newSource.push('.focalboard-body ' + line);
|
||||
} else {
|
||||
newSource.push(line);
|
||||
}
|
||||
});
|
||||
return newSource.join('\n');
|
||||
};
|
39450
mattermost-plugin/webapp/package-lock.json
generated
39450
mattermost-plugin/webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,142 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack --mode=production",
|
||||
"build:watch": "webpack --mode=production --watch",
|
||||
"debug": "webpack --mode=none",
|
||||
"debug:watch": "webpack --mode=development --watch",
|
||||
"live-watch": "webpack --mode=development --watch",
|
||||
"lint": "eslint --ignore-pattern node_modules --ignore-pattern dist --ext .js --ext .jsx --ext tsx --ext ts . --quiet --cache",
|
||||
"fix": "eslint --ignore-pattern node_modules --ignore-pattern dist --ext .js --ext .jsx --ext tsx --ext ts . --quiet --fix --cache",
|
||||
"test": "jest --forceExit --detectOpenHandles --verbose",
|
||||
"test:watch": "jest --watch",
|
||||
"test-ci": "jest --forceExit --detectOpenHandles --maxWorkers=2",
|
||||
"check-types": "tsc",
|
||||
"build:product": "webpack --mode=production",
|
||||
"start:product": "webpack serve --mode=development"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.17.6",
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.17.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/polyfill": "7.10.4",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/preset-react": "7.16.7",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@babel/runtime": "7.17.8",
|
||||
"@formatjs/ts-transformer": "3.9.2",
|
||||
"@testing-library/react": "11.2.7",
|
||||
"@testing-library/user-event": "14.2.1",
|
||||
"@types/enzyme": "3.10.11",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/lodash": "4.14.182",
|
||||
"@types/node": "17.0.23",
|
||||
"@types/react": "17.0.42",
|
||||
"@types/react-dom": "17.0.14",
|
||||
"@types/react-intl": "3.0.0",
|
||||
"@types/react-redux": "7.1.23",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-transition-group": "4.4.4",
|
||||
"@types/redux-mock-store": "1.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.16.0",
|
||||
"@typescript-eslint/parser": "5.16.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-jest": "27.5.1",
|
||||
"babel-loader": "8.2.4",
|
||||
"babel-plugin-typescript-to-proptypes": "2.0.0",
|
||||
"css-loader": "6.7.3",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-import-resolver-webpack": "0.13.2",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-cypress": "2.12.1",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-jquery": "1.5.1",
|
||||
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#46ad99355644a719bf32082f472048f526605181",
|
||||
"eslint-plugin-no-only-tests": "2.6.0",
|
||||
"eslint-plugin-react": "7.29.4",
|
||||
"eslint-plugin-react-hooks": "4.3.0",
|
||||
"file-loader": "6.2.0",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"image-webpack-loader": "8.1.0",
|
||||
"imagemin-gifsicle": "^7.0.0",
|
||||
"imagemin-mozjpeg": "^10.0.0",
|
||||
"imagemin-optipng": "^8.0.0",
|
||||
"imagemin-pngquant": "^9.0.2",
|
||||
"imagemin-svgo": "^10.0.1",
|
||||
"imagemin-webp": "7.0.0",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"jest": "27.5.1",
|
||||
"jest-canvas-mock": "2.4.0",
|
||||
"jest-junit": "15.0.0",
|
||||
"jest-mock": "27.5.1",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "13.2.0",
|
||||
"style-loader": "3.3.2",
|
||||
"ts-loader": "9.2.8",
|
||||
"typescript": "4.9.5",
|
||||
"webpack": "5.76.1",
|
||||
"webpack-cli": "5.0.1",
|
||||
"webpack-dev-server": "4.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.29.1",
|
||||
"glob-parent": "6.0.2",
|
||||
"marked": ">=4.0.12",
|
||||
"mattermost-redux": "5.33.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-intl": "^5.20.0",
|
||||
"react-redux": "8.0.5",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"trim-newlines": "4.0.2",
|
||||
"react-select": "^5.2.2"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom",
|
||||
"testPathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"/non_npm_dependencies/"
|
||||
],
|
||||
"clearMocks": true,
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"lcov",
|
||||
"text-summary"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/../../webapp/__mocks__/fileMock.js",
|
||||
"^.+\\.(scss|css)$": "<rootDir>/tests/style_mock.json",
|
||||
"^.*i18n.*\\.(json)$": "<rootDir>/tests/i18n_mock.json",
|
||||
"^bundle-loader\\?lazy\\!(.*)$": "$1",
|
||||
"^react$": "<rootDir>/../../webapp/node_modules/react",
|
||||
"^react-redux$": "<rootDir>/../../webapp/node_modules/react-redux",
|
||||
"^react-intl$": "<rootDir>/../../webapp/node_modules/react-intl"
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"",
|
||||
"node_modules",
|
||||
"non_npm_dependencies"
|
||||
],
|
||||
"reporters": [
|
||||
"default",
|
||||
"jest-junit"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!react-native|react-router|mattermost-webapp)"
|
||||
],
|
||||
"setupFiles": [
|
||||
"jest-canvas-mock"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/tests/setup.tsx"
|
||||
],
|
||||
"testURL": "http://localhost:8065"
|
||||
}
|
||||
}
|
@ -1,521 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/boardSelector escape button should unmount the component 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="Dialog dialog-back BoardSelector size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
/>
|
||||
<div
|
||||
class="wrapper"
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="toolbar"
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
class="dialog-title"
|
||||
>
|
||||
Link boards
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="toolbar--right"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--secondary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Create a board
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close dialog"
|
||||
class="IconButton dialog__close size--medium"
|
||||
title="Close dialog"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="BoardSelectorBody"
|
||||
>
|
||||
<div
|
||||
class="head"
|
||||
>
|
||||
<div
|
||||
class="queryWrapper"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<input
|
||||
class="searchQuery"
|
||||
maxlength="100"
|
||||
placeholder="Search for boards"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="searchResults"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
class="noResults introScreen"
|
||||
>
|
||||
<div
|
||||
class="iconWrapper"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
</div>
|
||||
<h4
|
||||
class="text-heading4"
|
||||
>
|
||||
Search for boards
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/boardSelector renders with no results 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="Dialog dialog-back BoardSelector size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
/>
|
||||
<div
|
||||
class="wrapper"
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="toolbar"
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
class="dialog-title"
|
||||
>
|
||||
Link boards
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="toolbar--right"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--secondary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Create a board
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close dialog"
|
||||
class="IconButton dialog__close size--medium"
|
||||
title="Close dialog"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="BoardSelectorBody"
|
||||
>
|
||||
<div
|
||||
class="head"
|
||||
>
|
||||
<div
|
||||
class="queryWrapper"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<input
|
||||
class="searchQuery"
|
||||
maxlength="100"
|
||||
placeholder="Search for boards"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="searchResults"
|
||||
>
|
||||
<div
|
||||
class="noResults"
|
||||
>
|
||||
<div
|
||||
class="iconWrapper"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
</div>
|
||||
<h4
|
||||
class="text-heading4"
|
||||
>
|
||||
No results for "test"
|
||||
</h4>
|
||||
<span>
|
||||
Check the spelling or try another search.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/boardSelector renders with some results 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="Dialog dialog-back BoardSelector size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
/>
|
||||
<div
|
||||
class="wrapper"
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="toolbar"
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
class="dialog-title"
|
||||
>
|
||||
Link boards
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="toolbar--right"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--secondary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Create a board
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close dialog"
|
||||
class="IconButton dialog__close size--medium"
|
||||
title="Close dialog"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="BoardSelectorBody"
|
||||
>
|
||||
<div
|
||||
class="head"
|
||||
>
|
||||
<div
|
||||
class="queryWrapper"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<input
|
||||
class="searchQuery"
|
||||
maxlength="100"
|
||||
placeholder="Search for boards"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="searchResults"
|
||||
>
|
||||
<div
|
||||
class="BoardSelectorItem"
|
||||
>
|
||||
<div
|
||||
class="BoardSelectorItem-info"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="resultTitle"
|
||||
>
|
||||
Untitled board
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resultDescription"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="linkUnlinkButton"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Link
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="BoardSelectorItem"
|
||||
>
|
||||
<div
|
||||
class="BoardSelectorItem-info"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="resultTitle"
|
||||
>
|
||||
Untitled board
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resultDescription"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="linkUnlinkButton"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Link
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="BoardSelectorItem"
|
||||
>
|
||||
<div
|
||||
class="BoardSelectorItem-info"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="resultTitle"
|
||||
>
|
||||
Untitled board
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resultDescription"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="linkUnlinkButton"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Link
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/boardSelector renders without start searching 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="Dialog dialog-back BoardSelector size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
/>
|
||||
<div
|
||||
class="wrapper"
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="toolbar"
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
class="dialog-title"
|
||||
>
|
||||
Link boards
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="toolbar--right"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--secondary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Create a board
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close dialog"
|
||||
class="IconButton dialog__close size--medium"
|
||||
title="Close dialog"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-close CloseIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="BoardSelectorBody"
|
||||
>
|
||||
<div
|
||||
class="head"
|
||||
>
|
||||
<div
|
||||
class="queryWrapper"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
<input
|
||||
class="searchQuery"
|
||||
maxlength="100"
|
||||
placeholder="Search for boards"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="searchResults"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
class="noResults introScreen"
|
||||
>
|
||||
<div
|
||||
class="iconWrapper"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-magnify MagnifyIcon"
|
||||
/>
|
||||
</div>
|
||||
<h4
|
||||
class="text-heading4"
|
||||
>
|
||||
Search for boards
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,133 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/boardSelectorItem renders board without title 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="BoardSelectorItem"
|
||||
>
|
||||
<div
|
||||
class="BoardSelectorItem-info"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="resultTitle"
|
||||
>
|
||||
Untitled board
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resultDescription"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="linkUnlinkButton"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--secondary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Unlink
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/boardSelectorItem renders linked board 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="BoardSelectorItem"
|
||||
>
|
||||
<div
|
||||
class="BoardSelectorItem-info"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="resultTitle"
|
||||
>
|
||||
Test title
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resultDescription"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="linkUnlinkButton"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--secondary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Unlink
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/boardSelectorItem renders not linked board 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="BoardSelectorItem"
|
||||
>
|
||||
<div
|
||||
class="BoardSelectorItem-info"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="resultTitle"
|
||||
>
|
||||
Test title
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resultDescription"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="linkUnlinkButton"
|
||||
>
|
||||
<button
|
||||
class="Button emphasis--primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Link
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,27 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/createBoardFromTemplate renders the Create Boards from template component and match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="CreateBoardFromTemplate"
|
||||
>
|
||||
<div
|
||||
class="add-board-to-channel"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
data-testid="add-board-to-channel-check"
|
||||
id="add-board-to-channel"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
Create a board for this channel
|
||||
</span>
|
||||
<i
|
||||
class="icon-information-outline"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,175 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/rhsChannelBoardItem render board 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Test board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
/>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 10, 2022 at 1:40 AM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoardItem render board with menu open 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
>
|
||||
i
|
||||
</span>
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
New board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper override menuOpened"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left "
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
aria-label="Unlink board"
|
||||
class="MenuOption TextOption menu-option menu-option--with-subtext menu-option--disabled"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="menu-option__icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-link-variant-off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Unlink board
|
||||
</div>
|
||||
<div
|
||||
class="menu-subtext text-75 mt-1"
|
||||
>
|
||||
You are not an admin of the board
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
>
|
||||
<p>
|
||||
<strong>
|
||||
Board
|
||||
</strong>
|
||||
with description
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 10, 2022 at 1:40 AM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,272 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/rhsChannelBoards renders the RHS for channel boards 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoards"
|
||||
>
|
||||
<div
|
||||
class="rhs-boards-header"
|
||||
>
|
||||
<span
|
||||
class="linked-boards"
|
||||
>
|
||||
Linked boards
|
||||
</span>
|
||||
<button
|
||||
class="Button emphasis--primary"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-plus AddIcon"
|
||||
/>
|
||||
<span>
|
||||
Add
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="rhs-boards-list"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Untitled board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
/>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 10, 2022 at 1:40 AM
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Untitled board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
/>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 10, 2022 at 1:40 AM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoards renders the RHS for channel boards, no add 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoards"
|
||||
>
|
||||
<div
|
||||
class="rhs-boards-header"
|
||||
>
|
||||
<span
|
||||
class="linked-boards"
|
||||
>
|
||||
Linked boards
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="rhs-boards-list"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Untitled board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
/>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 10, 2022 at 1:40 AM
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Untitled board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
/>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 10, 2022 at 1:40 AM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoards renders with empty list of boards 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoards empty"
|
||||
>
|
||||
<h2>
|
||||
No boards are linked to Channel Name yet
|
||||
</h2>
|
||||
<div
|
||||
class="empty-paragraph"
|
||||
>
|
||||
Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view.
|
||||
</div>
|
||||
<div
|
||||
class="boards-screenshots"
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="Button emphasis--primary size--medium"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Link boards to Channel Name
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoards renders with empty list of boards, cannot add 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoards empty"
|
||||
>
|
||||
<h2>
|
||||
No boards are linked to Channel Name yet
|
||||
</h2>
|
||||
<div
|
||||
class="empty-paragraph"
|
||||
>
|
||||
Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view.
|
||||
</div>
|
||||
<div
|
||||
class="boards-screenshots"
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,20 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/rhsChannelBoardsHeader renders the header 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<img
|
||||
class="boards-rhs-header-logo"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
<span>
|
||||
Boards
|
||||
</span>
|
||||
<span
|
||||
class="style--none sidebar--right__title__subtitle"
|
||||
>
|
||||
Channel Name
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,128 +0,0 @@
|
||||
.BoardSelector {
|
||||
color: rgba(var(--center-channel-color-rgb));
|
||||
.wrapper {
|
||||
.dialog {
|
||||
position: relative;
|
||||
width: 600px;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
.confirmation-dialog-box {
|
||||
.dialog {
|
||||
position: fixed;
|
||||
width: 500px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.BoardSelectorBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.head,
|
||||
.searchResults {
|
||||
padding: 0 32px 24px;
|
||||
}
|
||||
|
||||
.searchResults {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 16px 0;
|
||||
border-top: solid 1px rgba(var(--center-channel-color-rgb), 0.16);
|
||||
|
||||
.searchResult {
|
||||
height: 40px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 0 24px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
&.freesize {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
/*! align-content: center; */
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.CompassIcon.icon-magnify.MagnifyIcon {
|
||||
font-size: 72px !important;
|
||||
color: var(--button-bg);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
word-wrap: anywhere;
|
||||
margin-top: 30px;
|
||||
|
||||
.text-heading4 {
|
||||
line-height: 120%;
|
||||
}
|
||||
|
||||
&.introScreen {
|
||||
margin-top: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.queryWrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.MagnifyIcon {
|
||||
position: absolute;
|
||||
left: 13px;
|
||||
font-size: 18px;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
opacity: 0.48;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.searchQuery {
|
||||
height: 48px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
background: var(--center-channel-bg);
|
||||
color: var(--center-channel-color);
|
||||
padding: 0 40px;
|
||||
flex: 1;
|
||||
transition: border 0.15s ease-in;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--button-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {render, screen, act, fireEvent} from '@testing-library/react'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import octoClient from '../../../../webapp/src/octoClient'
|
||||
import {mockStateStore} from '../../../../webapp/src/testUtils'
|
||||
import {createBoard} from '../../../../webapp/src/blocks/board'
|
||||
import {wrapIntl} from '../../../../webapp/src/testUtils'
|
||||
|
||||
import BoardSelector from './boardSelector'
|
||||
|
||||
jest.mock('../../../../webapp/src/octoClient')
|
||||
const mockedOctoClient = mocked(octoClient, true)
|
||||
|
||||
const wait = (ms: number) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
|
||||
describe('components/boardSelector', () => {
|
||||
const team = {
|
||||
id: 'team-id',
|
||||
name: 'team',
|
||||
display_name: 'Team name',
|
||||
}
|
||||
const state = {
|
||||
teams: {
|
||||
allTeams: [team],
|
||||
current: team,
|
||||
currentId: team.id,
|
||||
},
|
||||
language: {
|
||||
value: 'en',
|
||||
},
|
||||
boards: {
|
||||
linkToChannel: 'channel-id',
|
||||
},
|
||||
}
|
||||
|
||||
it('renders without start searching', async () => {
|
||||
const store = mockStateStore([], state)
|
||||
const {container} = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<BoardSelector/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with no results', async () => {
|
||||
mockedOctoClient.searchLinkableBoards.mockResolvedValueOnce([])
|
||||
|
||||
const store = mockStateStore([], state)
|
||||
const {container} = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<BoardSelector/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
|
||||
await act(async () => {
|
||||
const inputElement = screen.getByPlaceholderText('Search for boards')
|
||||
await userEvent.type(inputElement, 'test')
|
||||
await wait(300)
|
||||
})
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with some results', async () => {
|
||||
mockedOctoClient.searchLinkableBoards.mockResolvedValueOnce([createBoard(), createBoard(), createBoard()])
|
||||
|
||||
const store = mockStateStore([], state)
|
||||
const {container} = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<BoardSelector/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
|
||||
await act(async () => {
|
||||
const inputElement = screen.getByPlaceholderText('Search for boards')
|
||||
await userEvent.type(inputElement, 'test')
|
||||
await wait(300)
|
||||
})
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("escape button should unmount the component", () => {
|
||||
mockedOctoClient.searchLinkableBoards.mockResolvedValueOnce([])
|
||||
|
||||
const store = mockStateStore([], state)
|
||||
const origDispatch = store.dispatch
|
||||
store.dispatch = jest.fn(origDispatch)
|
||||
const {container, getByText} = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<BoardSelector/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
|
||||
expect(getByText(/Link boards/i)).not.toBeNull()
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(0)
|
||||
|
||||
fireEvent.keyDown(getByText(/Link boards/i), {
|
||||
key: "Escape",
|
||||
code: "Escape",
|
||||
keyCode: 27,
|
||||
charCode: 27
|
||||
})
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(2)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -1,234 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useMemo, useCallback} from 'react'
|
||||
import {IntlProvider, useIntl, FormattedMessage} from 'react-intl'
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
import {getMessages} from '../../../../webapp/src/i18n'
|
||||
import {getLanguage} from '../../../../webapp/src/store/language'
|
||||
|
||||
import {useWebsockets} from '../../../../webapp/src/hooks/websockets'
|
||||
|
||||
import octoClient from '../../../../webapp/src/octoClient'
|
||||
import mutator from '../../../../webapp/src/mutator'
|
||||
import {getCurrentTeamId, getAllTeams, Team} from '../../../../webapp/src/store/teams'
|
||||
import {createBoard, Board} from '../../../../webapp/src/blocks/board'
|
||||
import {useAppSelector, useAppDispatch} from '../../../../webapp/src/store/hooks'
|
||||
import {EmptySearch, EmptyResults} from '../../../../webapp/src/components/searchDialog/searchDialog'
|
||||
import ConfirmationDialog from '../../../../webapp/src/components/confirmationDialogBox'
|
||||
import Dialog from '../../../../webapp/src/components/dialog'
|
||||
import SearchIcon from '../../../../webapp/src/widgets/icons/search'
|
||||
import Button from '../../../../webapp/src/widgets/buttons/button'
|
||||
import {getCurrentLinkToChannel, setLinkToChannel} from '../../../../webapp/src/store/boards'
|
||||
import {WSClient} from '../../../../webapp/src/wsclient'
|
||||
import {SuiteWindow} from '../../../../webapp/src/types/index'
|
||||
|
||||
import BoardSelectorItem from './boardSelectorItem'
|
||||
|
||||
const windowAny = (window as SuiteWindow)
|
||||
|
||||
import './boardSelector.scss'
|
||||
|
||||
const BoardSelector = () => {
|
||||
const teamsById:Record<string, Team> = {}
|
||||
useAppSelector(getAllTeams).forEach((t) => {
|
||||
teamsById[t.id] = t
|
||||
})
|
||||
const intl = useIntl()
|
||||
const teamId = useAppSelector(getCurrentTeamId)
|
||||
const currentChannel = useAppSelector(getCurrentLinkToChannel)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [results, setResults] = useState<Array<Board>>([])
|
||||
const [isSearching, setIsSearching] = useState<boolean>(false)
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [showLinkBoardConfirmation, setShowLinkBoardConfirmation] = useState<Board|null>(null)
|
||||
|
||||
const searchHandler = useCallback(async (query: string): Promise<void> => {
|
||||
setSearchQuery(query)
|
||||
|
||||
if (query.trim().length === 0 || !teamId) {
|
||||
return
|
||||
}
|
||||
const items = await octoClient.searchLinkableBoards(teamId, query)
|
||||
|
||||
setResults(items)
|
||||
setIsSearching(false)
|
||||
}, [teamId])
|
||||
|
||||
const debouncedSearchHandler = useMemo(() => debounce(searchHandler, 200), [searchHandler])
|
||||
|
||||
const emptyResult = results.length === 0 && !isSearching && searchQuery
|
||||
|
||||
useWebsockets(teamId, (wsClient: WSClient) => {
|
||||
const onChangeBoardHandler = (_: WSClient, boards: Board[]): void => {
|
||||
const newResults = [...results]
|
||||
let updated = false
|
||||
results.forEach((board, idx) => {
|
||||
for (const newBoard of boards) {
|
||||
if (newBoard.id == board.id) {
|
||||
newResults[idx] = newBoard
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
})
|
||||
if (updated) {
|
||||
setResults(newResults)
|
||||
}
|
||||
}
|
||||
|
||||
wsClient.addOnChange(onChangeBoardHandler, 'board')
|
||||
|
||||
return () => {
|
||||
wsClient.removeOnChange(onChangeBoardHandler, 'board')
|
||||
}
|
||||
}, [results])
|
||||
|
||||
|
||||
if (!teamId) {
|
||||
return null
|
||||
}
|
||||
if (!currentChannel) {
|
||||
return null
|
||||
}
|
||||
|
||||
const linkBoard = async (board: Board, confirmed?: boolean): Promise<void> => {
|
||||
if (!confirmed) {
|
||||
setShowLinkBoardConfirmation(board)
|
||||
return
|
||||
}
|
||||
const newBoard = createBoard({...board, channelId: currentChannel})
|
||||
await mutator.updateBoard(newBoard, board, 'linked channel')
|
||||
setShowLinkBoardConfirmation(null)
|
||||
dispatch(setLinkToChannel(''))
|
||||
setResults([])
|
||||
setIsSearching(false)
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
const unlinkBoard = async (board: Board): Promise<void> => {
|
||||
const newBoard = createBoard({...board, channelId: ''})
|
||||
await mutator.updateBoard(newBoard, board, 'unlinked channel')
|
||||
}
|
||||
|
||||
const newLinkedBoard = async (): Promise<void> => {
|
||||
window.open(`${windowAny.frontendBaseURL}/team/${teamId}/new/${currentChannel}`, '_blank', 'noopener')
|
||||
dispatch(setLinkToChannel(''))
|
||||
}
|
||||
|
||||
let confirmationSubText
|
||||
if (showLinkBoardConfirmation?.channelId !== '') {
|
||||
confirmationSubText = intl.formatMessage({
|
||||
id: 'boardSelector.confirm-link-board-subtext-with-other-channel',
|
||||
defaultMessage: 'When you link "{boardName}" to the channel, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests.{lineBreak} This board is currently linked to another channel. It will be unlinked if you choose to link it here.'
|
||||
}, {boardName: showLinkBoardConfirmation?.title, lineBreak: <p/>})
|
||||
} else {
|
||||
confirmationSubText = intl.formatMessage({
|
||||
id: 'boardSelector.confirm-link-board-subtext',
|
||||
defaultMessage: 'When you link "{boardName}" to the channel, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests. You can unlink a board from a channel at any time.'
|
||||
}, {boardName: showLinkBoardConfirmation?.title})
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
dispatch(setLinkToChannel(''))
|
||||
setResults([])
|
||||
setIsSearching(false)
|
||||
setSearchQuery('')
|
||||
setShowLinkBoardConfirmation(null)
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if(event.key == 'Escape') {
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className='focalboard-body'
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Dialog
|
||||
className='BoardSelector'
|
||||
onClose={closeDialog}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='boardSelector.title'
|
||||
defaultMessage='Link boards'
|
||||
/>
|
||||
}
|
||||
toolbar={
|
||||
<Button
|
||||
onClick={() => newLinkedBoard()}
|
||||
emphasis='secondary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='boardSelector.create-a-board'
|
||||
defaultMessage='Create a board'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{showLinkBoardConfirmation &&
|
||||
<ConfirmationDialog
|
||||
dialogBox={{
|
||||
heading: intl.formatMessage({id: 'boardSelector.confirm-link-board', defaultMessage: 'Link board to channel'}),
|
||||
subText: confirmationSubText,
|
||||
confirmButtonText: intl.formatMessage({id: 'boardSelector.confirm-link-board-button', defaultMessage: 'Yes, link board'}),
|
||||
destructive: showLinkBoardConfirmation?.channelId !== '',
|
||||
onConfirm: () => linkBoard(showLinkBoardConfirmation, true),
|
||||
onClose: () => setShowLinkBoardConfirmation(null),
|
||||
}}
|
||||
/>}
|
||||
<div className='BoardSelectorBody'>
|
||||
<div className='head'>
|
||||
<div className='queryWrapper'>
|
||||
<SearchIcon/>
|
||||
<input
|
||||
className='searchQuery'
|
||||
placeholder={intl.formatMessage({id: 'boardSelector.search-for-boards', defaultMessage:'Search for boards'})}
|
||||
type='text'
|
||||
onChange={(e) => debouncedSearchHandler(e.target.value)}
|
||||
autoFocus={true}
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='searchResults'>
|
||||
{/*When there are results to show*/}
|
||||
{searchQuery && results.length > 0 &&
|
||||
results.map((result) => (<BoardSelectorItem
|
||||
key={result.id}
|
||||
item={result}
|
||||
linkBoard={linkBoard}
|
||||
unlinkBoard={unlinkBoard}
|
||||
currentChannel={currentChannel}
|
||||
/>))}
|
||||
|
||||
{/*when user searched for something and there were no results*/}
|
||||
{emptyResult && <EmptyResults query={searchQuery}/>}
|
||||
|
||||
{/*default state, when user didn't search for anything. This is the initial screen*/}
|
||||
{!emptyResult && !searchQuery && <EmptySearch/>}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const IntlBoardSelector = () => {
|
||||
const language = useAppSelector<string>(getLanguage)
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={language.split(/[_]/)[0]}
|
||||
messages={getMessages(language)}
|
||||
>
|
||||
<BoardSelector/>
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntlBoardSelector
|
@ -1,55 +0,0 @@
|
||||
.BoardSelectorItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
padding: 10px 35px 10px 0;
|
||||
|
||||
.BoardSelectorItem-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
align-items: flex-start;
|
||||
margin-right: 10px;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.resultLine {
|
||||
flex-grow: 1;
|
||||
width: 80%;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.resultTitle {
|
||||
overflow: hidden;
|
||||
max-width: 60%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resultDescription {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-left: 28px;
|
||||
opacity: 0.64;
|
||||
}
|
||||
|
||||
.linkUnlinkButton {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {render, screen} from '@testing-library/react'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {createBoard} from '../../../../webapp/src/blocks/board'
|
||||
import {wrapIntl} from '../../../../webapp/src/testUtils'
|
||||
|
||||
import BoardSelectorItem from './boardSelectorItem'
|
||||
|
||||
describe('components/boardSelectorItem', () => {
|
||||
it('renders board without title', async () => {
|
||||
const board = createBoard()
|
||||
board.title = ""
|
||||
|
||||
const {container} = render(wrapIntl(
|
||||
<BoardSelectorItem
|
||||
item={board}
|
||||
currentChannel={board.channelId || ''}
|
||||
linkBoard={jest.fn()}
|
||||
unlinkBoard={jest.fn()}
|
||||
/>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders linked board', async () => {
|
||||
const board = createBoard()
|
||||
board.title = "Test title"
|
||||
|
||||
const {container} = render(wrapIntl(
|
||||
<BoardSelectorItem
|
||||
item={board}
|
||||
currentChannel={board.channelId || ''}
|
||||
linkBoard={jest.fn()}
|
||||
unlinkBoard={jest.fn()}
|
||||
/>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders not linked board', async () => {
|
||||
const board = createBoard()
|
||||
board.title = "Test title"
|
||||
|
||||
const {container} = render(wrapIntl(
|
||||
<BoardSelectorItem
|
||||
item={board}
|
||||
currentChannel={'other-channel'}
|
||||
linkBoard={jest.fn()}
|
||||
unlinkBoard={jest.fn()}
|
||||
/>,
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('call handler on link', async () => {
|
||||
const board = createBoard()
|
||||
|
||||
const linkBoard = jest.fn()
|
||||
const unlinkBoard = jest.fn()
|
||||
|
||||
render(wrapIntl(
|
||||
<BoardSelectorItem
|
||||
item={board}
|
||||
currentChannel={'other-channel'}
|
||||
linkBoard={linkBoard}
|
||||
unlinkBoard={unlinkBoard}
|
||||
/>,
|
||||
))
|
||||
|
||||
const buttonElement = screen.getByRole('button')
|
||||
await userEvent.click(buttonElement)
|
||||
expect(linkBoard).toBeCalledWith(board)
|
||||
expect(unlinkBoard).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('call handler on unlink', async () => {
|
||||
const board = createBoard()
|
||||
|
||||
const linkBoard = jest.fn()
|
||||
const unlinkBoard = jest.fn()
|
||||
|
||||
render(wrapIntl(
|
||||
<BoardSelectorItem
|
||||
item={board}
|
||||
currentChannel={board.channelId || ''}
|
||||
linkBoard={linkBoard}
|
||||
unlinkBoard={unlinkBoard}
|
||||
/>,
|
||||
))
|
||||
|
||||
const buttonElement = screen.getByRole('button')
|
||||
await userEvent.click(buttonElement)
|
||||
expect(unlinkBoard).toBeCalledWith(board)
|
||||
expect(linkBoard).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
|
@ -1,59 +0,0 @@
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {useIntl, FormattedMessage} from 'react-intl'
|
||||
|
||||
import {Board} from '../../../../webapp/src/blocks/board'
|
||||
import Button from '../../../../webapp/src/widgets/buttons/button'
|
||||
import CompassIcon from '../../../../webapp/src/widgets/icons/compassIcon'
|
||||
|
||||
import './boardSelectorItem.scss'
|
||||
|
||||
type Props = {
|
||||
item: Board
|
||||
currentChannel: string
|
||||
linkBoard: (board: Board) => void
|
||||
unlinkBoard: (board: Board) => void
|
||||
}
|
||||
|
||||
const BoardSelectorItem = (props: Props) => {
|
||||
const {item, currentChannel} = props
|
||||
const intl = useIntl()
|
||||
const untitledBoardTitle = intl.formatMessage({id: 'ViewTitle.untitled-board', defaultMessage: 'Untitled board'})
|
||||
const resultTitle = item.title || untitledBoardTitle
|
||||
return (
|
||||
<div className='BoardSelectorItem'>
|
||||
<div className='BoardSelectorItem-info'>
|
||||
<div className='d-flex'>
|
||||
<span className='icon'>{item.icon || <CompassIcon icon='product-boards'/>}</span>
|
||||
<div className='resultTitle'>{resultTitle}</div>
|
||||
</div>
|
||||
<div className='resultDescription'>{item.description}</div>
|
||||
</div>
|
||||
<div className='linkUnlinkButton'>
|
||||
{item.channelId === currentChannel &&
|
||||
<Button
|
||||
onClick={() => props.unlinkBoard(item)}
|
||||
emphasis='secondary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='boardSelector.unlink'
|
||||
defaultMessage='Unlink'
|
||||
/>
|
||||
</Button>}
|
||||
{item.channelId !== currentChannel &&
|
||||
<Button
|
||||
onClick={() => props.linkBoard(item)}
|
||||
emphasis='primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='boardSelector.link'
|
||||
defaultMessage='Link'
|
||||
/>
|
||||
</Button>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default BoardSelectorItem
|
@ -1,104 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/boardsUnfurl/BoardsUnfurl renders normally 1`] = `
|
||||
<div>
|
||||
<a
|
||||
class="FocalboardUnfurl"
|
||||
href="http://localhost:8065/test"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
/>
|
||||
<div
|
||||
class="information"
|
||||
>
|
||||
<span
|
||||
class="card_title"
|
||||
>
|
||||
test card
|
||||
</span>
|
||||
<span
|
||||
class="board_title"
|
||||
>
|
||||
test board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="body"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="footer"
|
||||
>
|
||||
<div
|
||||
class="avatar"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="timestamp_properties"
|
||||
>
|
||||
<div
|
||||
class="properties"
|
||||
/>
|
||||
<span
|
||||
class="post-preview__time"
|
||||
>
|
||||
Updated July 10, 2022 at 1:40 AM
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/boardsUnfurl/BoardsUnfurl renders when limited 1`] = `
|
||||
<div>
|
||||
<a
|
||||
class="FocalboardUnfurl"
|
||||
href="http://localhost:8065/test"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
/>
|
||||
<div
|
||||
class="information"
|
||||
>
|
||||
<span
|
||||
class="card_title"
|
||||
>
|
||||
test card
|
||||
</span>
|
||||
<span
|
||||
class="board_title"
|
||||
>
|
||||
test board
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
class="limited"
|
||||
>
|
||||
Additional details are hidden due to the card being archived
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/boardsUnfurl/BoardsUnfurl test invalid card, invalid block 1`] = `<div />`;
|
||||
|
||||
exports[`components/boardsUnfurl/BoardsUnfurl test invalid card, valid block 1`] = `<div />`;
|
||||
|
||||
exports[`components/boardsUnfurl/BoardsUnfurl test no card 1`] = `<div />`;
|
@ -1,162 +0,0 @@
|
||||
.FocalboardUnfurl {
|
||||
display: table;
|
||||
width: 100%;
|
||||
max-width: 425px;
|
||||
margin: 0;
|
||||
table-layout: fixed;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24) !important;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none !important;
|
||||
color: inherit !important;
|
||||
background: rgb(var(--center-channel-bg-rgb));
|
||||
box-shadow: var(--elevation-1);
|
||||
margin-top: 8px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
font-size: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.information {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.card_title {
|
||||
color: var(--center-channel-color);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.board_title {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 4px;
|
||||
margin-top: 16px;
|
||||
height: 145px;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
white-space: nowrap;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.limited {
|
||||
font-size: 14px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.6);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
margin-top: 16px;
|
||||
|
||||
.timestamp_properties {
|
||||
margin-left: 12px;
|
||||
max-width: 90%;
|
||||
|
||||
.properties {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
.remainder {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.48);
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.post-preview__time {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.property {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
margin-right: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: normal;
|
||||
height: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
|
||||
|
||||
&.propColorDefault {
|
||||
background-color: var(--prop-default);
|
||||
}
|
||||
|
||||
&.propColorGray {
|
||||
background-color: var(--prop-gray);
|
||||
}
|
||||
|
||||
&.propColorBrown {
|
||||
background-color: var(--prop-brown);
|
||||
}
|
||||
|
||||
&.propColorOrange {
|
||||
background-color: var(--prop-orange);
|
||||
}
|
||||
|
||||
&.propColorYellow {
|
||||
background-color: var(--prop-yellow);
|
||||
}
|
||||
|
||||
&.propColorGreen {
|
||||
background-color: var(--prop-green);
|
||||
}
|
||||
|
||||
&.propColorBlue {
|
||||
background-color: var(--prop-blue);
|
||||
}
|
||||
|
||||
&.propColorPurple {
|
||||
background-color: var(--prop-purple);
|
||||
}
|
||||
|
||||
&.propColorPink {
|
||||
background-color: var(--prop-pink);
|
||||
}
|
||||
|
||||
&.propColorRed {
|
||||
background-color: var(--prop-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,245 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {act, render} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {createBoardView} from '../../../../../webapp/src/blocks/boardView'
|
||||
|
||||
import {Utils} from '../../../../../webapp/src/utils'
|
||||
import {createCard} from '../../../../../webapp/src/blocks/card'
|
||||
import {createBoard} from '../../../../../webapp/src/blocks/board'
|
||||
import octoClient from '../../../../../webapp/src/octoClient'
|
||||
import {wrapIntl} from '../../../../../webapp/src/testUtils'
|
||||
|
||||
import BoardsUnfurl from './boardsUnfurl'
|
||||
|
||||
jest.mock('../../../../../webapp/src/octoClient')
|
||||
jest.mock('../../../../../webapp/src/utils')
|
||||
const mockedOctoClient = mocked(octoClient, true)
|
||||
const mockedUtils = mocked(Utils, true)
|
||||
mockedUtils.createGuid = jest.requireActual('../../../../../webapp/src/utils').Utils.createGuid
|
||||
mockedUtils.blockTypeToIDType = jest.requireActual('../../../../../webapp/src/utils').Utils.blockTypeToIDType
|
||||
mockedUtils.displayDateTime = jest.requireActual('../../../../../webapp/src/utils').Utils.displayDateTime
|
||||
|
||||
let mockDisplayDateTime: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
mockDisplayDateTime = jest.spyOn(Utils, 'displayDateTime').mockImplementation(() => 'July 10, 2022 at 1:40 AM')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockDisplayDateTime.mockRestore()
|
||||
})
|
||||
|
||||
describe('components/boardsUnfurl/BoardsUnfurl', () => {
|
||||
const team = {
|
||||
id: 'team-id',
|
||||
name: 'team',
|
||||
display_name: 'Team name',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// This is done to the websocket not to try to connect directly
|
||||
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders normally', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
language: {
|
||||
value: 'en',
|
||||
},
|
||||
teams: {
|
||||
allTeams: [team],
|
||||
current: team,
|
||||
},
|
||||
})
|
||||
|
||||
const cards = [{...createCard(), title: 'test card', updateAt: 12345}]
|
||||
const board = {...createBoard(), title: 'test board'}
|
||||
|
||||
mockedOctoClient.getBlocksWithBlockID.mockResolvedValueOnce(cards)
|
||||
mockedOctoClient.getBoard.mockResolvedValueOnce(board)
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<BoardsUnfurl
|
||||
embed={{data: JSON.stringify({workspaceID: "foo", cardID: cards[0].id, boardID: board.id, readToken: "abc", originalPath: "/test"})}}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
expect(mockedOctoClient.getBoard).toBeCalledWith(board.id)
|
||||
expect(mockedOctoClient.getBlocksWithBlockID).toBeCalledWith(cards[0].id, board.id, "abc")
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders when limited', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
language: {
|
||||
value: 'en',
|
||||
},
|
||||
teams: {
|
||||
allTeams: [team],
|
||||
current: team,
|
||||
},
|
||||
})
|
||||
|
||||
const cards = [{...createCard(), title: 'test card', limited: true, updateAt: 12345}]
|
||||
const board = {...createBoard(), title: 'test board'}
|
||||
|
||||
mockedOctoClient.getBlocksWithBlockID.mockResolvedValueOnce(cards)
|
||||
mockedOctoClient.getBoard.mockResolvedValueOnce(board)
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<BoardsUnfurl
|
||||
embed={{data: JSON.stringify({workspaceID: "foo", cardID: cards[0].id, boardID: board.id, readToken: "abc", originalPath: "/test"})}}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('test no card', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
language: {
|
||||
value: 'en',
|
||||
},
|
||||
teams: {
|
||||
allTeams: [team],
|
||||
current: team,
|
||||
},
|
||||
})
|
||||
|
||||
const board = {...createBoard(), title: 'test board'}
|
||||
// mockedOctoClient.getBoard.mockResolvedValueOnce(board)
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<BoardsUnfurl
|
||||
embed={{data: JSON.stringify({workspaceID: 'foo', cardID: '', boardID: board.id, readToken: 'abc', originalPath: '/test'})}}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('test invalid card, valid block', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
language: {
|
||||
value: 'en',
|
||||
},
|
||||
teams: {
|
||||
allTeams: [team],
|
||||
current: team,
|
||||
},
|
||||
})
|
||||
|
||||
const cards = [{...createBoardView(), title: 'test view', updateAt: 12345}]
|
||||
const board = {...createBoard(), title: 'test board'}
|
||||
|
||||
mockedOctoClient.getBlocksWithBlockID.mockResolvedValueOnce(cards)
|
||||
mockedOctoClient.getBoard.mockResolvedValueOnce(board)
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<BoardsUnfurl
|
||||
embed={{data: JSON.stringify({workspaceID: 'foo', cardID: cards[0].id, boardID: board.id, readToken: 'abc', originalPath: '/test'})}}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
expect(mockedOctoClient.getBoard).toBeCalledWith(board.id)
|
||||
expect(mockedOctoClient.getBlocksWithBlockID).toBeCalledWith(cards[0].id, board.id, 'abc')
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('test invalid card, invalid block', async () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
language: {
|
||||
value: 'en',
|
||||
},
|
||||
teams: {
|
||||
allTeams: [team],
|
||||
current: team,
|
||||
},
|
||||
})
|
||||
|
||||
const board = {...createBoard(), title: 'test board'}
|
||||
|
||||
mockedOctoClient.getBlocksWithBlockID.mockResolvedValueOnce([])
|
||||
mockedOctoClient.getBoard.mockResolvedValueOnce(board)
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
{wrapIntl(
|
||||
<BoardsUnfurl
|
||||
embed={{data: JSON.stringify({workspaceID: 'foo', cardID: 'invalidCard', boardID: board.id, readToken: 'abc', originalPath: '/test'})}}
|
||||
/>,
|
||||
)}
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
|
||||
await act(async () => {
|
||||
const result = render(component)
|
||||
container = result.container
|
||||
})
|
||||
expect(mockedOctoClient.getBoard).toBeCalledWith(board.id)
|
||||
expect(mockedOctoClient.getBlocksWithBlockID).toBeCalledWith('invalidCard', board.id, 'abc')
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
@ -1,288 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {IntlProvider, FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import WithWebSockets from '../../../../../webapp/src/components/withWebSockets'
|
||||
import {useWebsockets} from '../../../../../webapp/src/hooks/websockets'
|
||||
|
||||
import {getLanguage} from '../../../../../webapp/src/store/language'
|
||||
import {useAppSelector} from '../../../../../webapp/src/store/hooks'
|
||||
import {getCurrentTeamId} from '../../../../../webapp/src/store/teams'
|
||||
|
||||
import {WSClient, MMWebSocketClient} from '../../../../../webapp/src/wsclient'
|
||||
import manifest from '../../manifest'
|
||||
|
||||
import {getMessages} from './../../../../../webapp/src/i18n'
|
||||
import {Utils} from './../../../../../webapp/src/utils'
|
||||
import {Block} from './../../../../../webapp/src/blocks/block'
|
||||
import {Card} from './../../../../../webapp/src/blocks/card'
|
||||
import {Board} from './../../../../../webapp/src/blocks/board'
|
||||
import {ContentBlock} from './../../../../../webapp/src/blocks/contentBlock'
|
||||
|
||||
import octoClient from './../../../../../webapp/src/octoClient'
|
||||
|
||||
const noop = () => ''
|
||||
const Avatar = (window as any).Components?.Avatar || noop
|
||||
const imageURLForUser = (window as any).Components?.imageURLForUser || noop
|
||||
|
||||
import './boardsUnfurl.scss'
|
||||
import '../../../../../webapp/src/styles/labels.scss'
|
||||
|
||||
type Props = {
|
||||
embed: {
|
||||
data: string,
|
||||
},
|
||||
webSocketClient?: MMWebSocketClient,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export const BoardsUnfurl = (props: Props): JSX.Element => {
|
||||
if (!props.embed || !props.embed.data) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const {embed, webSocketClient} = props
|
||||
const focalboardInformation: FocalboardEmbeddedData = new FocalboardEmbeddedData(embed.data)
|
||||
const currentTeamId = useAppSelector(getCurrentTeamId)
|
||||
const {teamID, cardID, boardID, readToken, originalPath} = focalboardInformation
|
||||
const baseURL = window.location.origin
|
||||
|
||||
if (!teamID || !cardID || !boardID) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const [card, setCard] = useState<Card>()
|
||||
const [content, setContent] = useState<ContentBlock>()
|
||||
const [board, setBoard] = useState<Board>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const [cards, fetchedBoard] = await Promise.all(
|
||||
[
|
||||
octoClient.getBlocksWithBlockID(cardID, boardID, readToken),
|
||||
octoClient.getBoard(boardID),
|
||||
],
|
||||
)
|
||||
const [firstCard] = cards as Card[]
|
||||
if (!firstCard || !fetchedBoard || firstCard.type !== 'card') {
|
||||
setLoading(false)
|
||||
return null
|
||||
}
|
||||
setCard(firstCard)
|
||||
setBoard(fetchedBoard)
|
||||
|
||||
if (firstCard.fields.contentOrder.length) {
|
||||
let [firstContentBlockID] = firstCard.fields?.contentOrder
|
||||
|
||||
if (Array.isArray(firstContentBlockID)) {
|
||||
[firstContentBlockID] = firstContentBlockID
|
||||
}
|
||||
|
||||
const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, boardID, readToken) as ContentBlock[]
|
||||
const [firstContentBlock] = contentBlock
|
||||
if (!firstContentBlock) {
|
||||
setLoading(false)
|
||||
return null
|
||||
}
|
||||
setContent(firstContentBlock)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
return null
|
||||
}
|
||||
fetchData()
|
||||
}, [originalPath])
|
||||
|
||||
useWebsockets(currentTeamId, (wsClient: WSClient) => {
|
||||
const onChangeHandler = (_: WSClient, blocks: Block[]): void => {
|
||||
const cardBlock: Block|undefined = blocks.find(b => b.id === cardID)
|
||||
if (cardBlock && !cardBlock.deleteAt && cardBlock.type === 'card') {
|
||||
setCard(cardBlock as Card)
|
||||
}
|
||||
|
||||
const contentBlock: Block|undefined = blocks.find(b => b.id === content?.id)
|
||||
if (contentBlock && !contentBlock.deleteAt) {
|
||||
setContent(contentBlock)
|
||||
}
|
||||
}
|
||||
|
||||
wsClient.addOnChange(onChangeHandler, 'block')
|
||||
|
||||
return () => {
|
||||
wsClient.removeOnChange(onChangeHandler, 'block')
|
||||
}
|
||||
}, [cardID, content?.id])
|
||||
|
||||
let remainder = 0
|
||||
let html = ''
|
||||
const propertiesToDisplay: Array<Record<string, string>> = []
|
||||
if (card && board) {
|
||||
// Checkboxes need to be accounted for if they are off or on, if they are on they show up in the card properties so we don't want to count it twice
|
||||
// Therefore we keep track how many checkboxes there are and subtract it at the end
|
||||
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.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
|
||||
if (['createdTime', 'createdBy', 'updatedTime', 'updatedBy', 'checkbox'].includes(optionInBoard.type)) {
|
||||
if (valueToLookUp && optionInBoard.type === 'checkbox') {
|
||||
totalNumberOfCheckBoxes += 1
|
||||
}
|
||||
|
||||
remainder += 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Check to see if this property is set in the Card or if we have max properties to display
|
||||
if (propertiesToDisplay.length === 3 || !valueToLookUp) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (Array.isArray(valueToLookUp)) {
|
||||
valueToLookUp = valueToLookUp[0]
|
||||
}
|
||||
|
||||
const optionSelected = optionInBoard.options.find((option) => option.id === valueToLookUp)
|
||||
|
||||
if (!optionSelected) {
|
||||
continue
|
||||
}
|
||||
|
||||
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 || '')
|
||||
}
|
||||
|
||||
return (
|
||||
<WithWebSockets manifest={manifest} webSocketClient={webSocketClient}>
|
||||
{!loading && (!card || !board) && <></>}
|
||||
{!loading && card && board &&
|
||||
<a
|
||||
className='FocalboardUnfurl'
|
||||
href={`${baseURL}${originalPath}`}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
|
||||
{/* Header of the Card*/}
|
||||
<div className='header'>
|
||||
<span className='icon'>{card.fields?.icon}</span>
|
||||
<div className='information'>
|
||||
<span className='card_title'>{card.title}</span>
|
||||
<span className='board_title'>{board.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body of the Card*/}
|
||||
{!card.limited && html !== '' &&
|
||||
<div className='body'>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{__html: html}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{card.limited &&
|
||||
<p className='limited'>
|
||||
<FormattedMessage
|
||||
id='BoardsUnfurl.Limited'
|
||||
defaultMessage={'Additional details are hidden due to the card being archived'}
|
||||
/>
|
||||
</p>}
|
||||
|
||||
{/* Footer of the Card*/}
|
||||
{!card.limited &&
|
||||
<div className='footer'>
|
||||
<div className='avatar'>
|
||||
<Avatar
|
||||
size={'md'}
|
||||
url={imageURLForUser(card.createdBy)}
|
||||
className={'avatar-post-preview'}
|
||||
/>
|
||||
</div>
|
||||
<div className='timestamp_properties'>
|
||||
<div className='properties'>
|
||||
{propertiesToDisplay.map((property) => (
|
||||
<div
|
||||
key={property.optionValue}
|
||||
className={`property ${property.optionValueColour}`}
|
||||
title={`${property.optionName}`}
|
||||
style={{maxWidth: `${(1 / propertiesToDisplay.length) * 100}%`}}
|
||||
>
|
||||
{property.optionValue}
|
||||
</div>
|
||||
))}
|
||||
{remainder > 0 &&
|
||||
<span className='remainder'>
|
||||
<FormattedMessage
|
||||
id='BoardsUnfurl.Remainder'
|
||||
defaultMessage='+{remainder} more'
|
||||
values={{
|
||||
remainder,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<span className='post-preview__time'>
|
||||
<FormattedMessage
|
||||
id='BoardsUnfurl.Updated'
|
||||
defaultMessage='Updated {time}'
|
||||
values={{
|
||||
time: Utils.displayDateTime(new Date(card.updateAt), intl)
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>}
|
||||
</a>
|
||||
}
|
||||
{loading &&
|
||||
<div style={{height: '302px'}}/>
|
||||
}
|
||||
</WithWebSockets>
|
||||
)
|
||||
}
|
||||
|
||||
const IntlBoardsUnfurl = (props: Props) => {
|
||||
const language = useAppSelector<string>(getLanguage)
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={language.split(/[_]/)[0]}
|
||||
messages={getMessages(language)}
|
||||
>
|
||||
<BoardsUnfurl {...props}/>
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntlBoardsUnfurl
|
@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Post} from 'mattermost-redux/types/posts'
|
||||
|
||||
const PostTypeCloudUpgradeNudge = (props: {post: Post}): JSX.Element => {
|
||||
const ctaHandler = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const windowAny = (window as any)
|
||||
windowAny?.openPricingModal()({trackingLocation: 'boards > click_view_upgrade_options_nudge'})
|
||||
}
|
||||
|
||||
// custom post type doesn't support styling via CSS stylesheet.
|
||||
// Only styled components or react styles work there.
|
||||
const ctaContainerStyle = {
|
||||
padding: '12px',
|
||||
borderRadius: '0 4px 4px 0',
|
||||
border: '1px solid rgba(63, 67, 80, 0.16)',
|
||||
borderLeft: '6px solid var(--link-color)',
|
||||
width: 'max-content',
|
||||
margin: '10px 0',
|
||||
}
|
||||
|
||||
const ctaBtnStyle = {
|
||||
background: 'var(--link-color)',
|
||||
color: 'var(--center-channel-bg)',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
padding: '8px 20px',
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='PostTypeCloudUpgradeNudge'>
|
||||
<span>{props.post.message}</span>
|
||||
<div
|
||||
style={ctaContainerStyle}
|
||||
>
|
||||
<button
|
||||
onClick={ctaHandler}
|
||||
style={ctaBtnStyle}
|
||||
>
|
||||
{'View upgrade options'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PostTypeCloudUpgradeNudge
|
@ -1,78 +0,0 @@
|
||||
.CreateBoardFromTemplate {
|
||||
width: 100%;
|
||||
|
||||
.add-board-to-channel {
|
||||
display: flex;
|
||||
margin-top: 24px;
|
||||
padding-bottom: 5px;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.8);
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
|
||||
input[type=checkbox] {
|
||||
margin-top: 0 !important;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-o-appearance: none;
|
||||
content: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type=checkbox]::before {
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24);
|
||||
margin-right: 7px;
|
||||
background: var(--center-channel-bg);
|
||||
border-radius: 2px;
|
||||
color: transparent !important;
|
||||
content: "\f00c";
|
||||
font-family: "FontAwesome";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input[type=checkbox]:checked::before {
|
||||
background: var(--button-bg);
|
||||
color: var(--center-channel-bg) !important;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
i.icon-information-outline {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.7);
|
||||
margin-top: -3px;
|
||||
margin-left: 3px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.templates-selector {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.CreateBoardFromTemplate--templates-selector__menu-portal {
|
||||
&__option {
|
||||
&__icon {
|
||||
display: inline-block;
|
||||
width: 19px;
|
||||
}
|
||||
&__title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
&__description {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
margin-left: 29px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.5);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {render, screen, act} from '@testing-library/react'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mockStateStore} from '../../../../webapp/src/testUtils'
|
||||
import {wrapIntl} from '../../../../webapp/src/testUtils'
|
||||
|
||||
import CreateBoardFromTemplate from './createBoardFromTemplate'
|
||||
|
||||
jest.mock('../../../../webapp/src/hooks/useGetAllTemplates', () => ({
|
||||
useGetAllTemplates: () => [{id: 'id', title: 'title', description: 'description', icon: '🍔'}]
|
||||
}))
|
||||
|
||||
describe('components/createBoardFromTemplate', () => {
|
||||
const state = {
|
||||
language: {
|
||||
value: 'en',
|
||||
},
|
||||
}
|
||||
|
||||
it('renders the Create Boards from template component and match snapshot', async () => {
|
||||
const store = mockStateStore([], state)
|
||||
let container: Element | DocumentFragment | null = null
|
||||
const setCanCreate = jest.fn
|
||||
const setAction = jest.fn
|
||||
const newBoardInfoIcon = (<i className="icon-information-outline" />)
|
||||
|
||||
await act(async () => {
|
||||
const result = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CreateBoardFromTemplate
|
||||
setAction={setAction}
|
||||
setCanCreate={setCanCreate}
|
||||
newBoardInfoIcon={newBoardInfoIcon}
|
||||
/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('clicking checkbox toggles the templates selector', async () => {
|
||||
const store = mockStateStore([], state)
|
||||
const setCanCreate = jest.fn
|
||||
const setAction = jest.fn
|
||||
const newBoardInfoIcon = (<i className="icon-information-outline" />)
|
||||
|
||||
await act(async () => {
|
||||
render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<CreateBoardFromTemplate
|
||||
setAction={setAction}
|
||||
setCanCreate={setCanCreate}
|
||||
newBoardInfoIcon={newBoardInfoIcon}
|
||||
/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
})
|
||||
|
||||
// click to show the template selector
|
||||
let checkbox = screen.getByRole('checkbox', {checked: false})
|
||||
await act(async () => {
|
||||
await userEvent.click(checkbox)
|
||||
const templatesSelector = screen.queryByText('Select a template')
|
||||
expect(templatesSelector).toBeTruthy()
|
||||
})
|
||||
|
||||
// click to hide the template selector
|
||||
checkbox = screen.getByRole('checkbox', {checked: true})
|
||||
await act(async () => {
|
||||
await userEvent.click(checkbox)
|
||||
const templatesSelector = screen.queryByText('Select a template')
|
||||
expect(templatesSelector).toBeNull()
|
||||
})
|
||||
|
||||
})
|
||||
})
|
@ -1,261 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
|
||||
import {createIntl, createIntlCache, IntlProvider} from 'react-intl'
|
||||
|
||||
import Select from 'react-select/async'
|
||||
import {components, FormatOptionLabelMeta, GroupBase, PlaceholderProps} from 'react-select'
|
||||
import {SingleValue} from 'react-select'
|
||||
|
||||
import {CSSObject} from '@emotion/serialize'
|
||||
|
||||
import {getCurrentLanguage, getMessages} from '../../../../webapp/src/i18n'
|
||||
import {getLanguage} from '../../../../webapp/src/store/language'
|
||||
import CompassIcon from '../../../../webapp/src/widgets/icons/compassIcon'
|
||||
import {useAppSelector} from '../../../../webapp/src/store/hooks'
|
||||
import {mutator} from '../../../../webapp/src/mutator'
|
||||
import {useGetAllTemplates} from '../../../../webapp/src/hooks/useGetAllTemplates'
|
||||
|
||||
import './createBoardFromTemplate.scss'
|
||||
import {Board} from '../../../../webapp/src/blocks/board'
|
||||
|
||||
type Props = {
|
||||
setCanCreate: (canCreate: boolean) => void;
|
||||
setAction: (fn: () => (channelId: string, teamId: string) => Promise<Board | undefined>) => void;
|
||||
newBoardInfoIcon: React.ReactNode;
|
||||
}
|
||||
|
||||
type ReactSelectItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const EMPTY_BOARD = 'empty_board'
|
||||
const TEMPLATE_DESCRIPTION_LENGTH = 70
|
||||
|
||||
const cache = createIntlCache()
|
||||
const intl = createIntl({
|
||||
locale: getCurrentLanguage(),
|
||||
messages: getMessages(getCurrentLanguage())
|
||||
}, cache)
|
||||
|
||||
const {ValueContainer, Placeholder} = components
|
||||
|
||||
const CreateBoardFromTemplate = (props: Props) => {
|
||||
const {formatMessage} = intl
|
||||
|
||||
const [addBoard, setAddBoard] = useState(false)
|
||||
const allTemplates = useGetAllTemplates()
|
||||
const [selectedBoardTemplateId, setSelectedBoardTemplateId] = useState<string>('')
|
||||
|
||||
const addBoardRef = useRef(false)
|
||||
addBoardRef.current = addBoard
|
||||
const templateIdRef = useRef('')
|
||||
templateIdRef.current = selectedBoardTemplateId
|
||||
|
||||
|
||||
const showNewBoardTemplateSelector = async () => {
|
||||
setAddBoard((prev: boolean) => !prev)
|
||||
}
|
||||
|
||||
// CreateBoardFromTemplate
|
||||
const addBoardToChannel = async (channelId: string, teamId: string) => {
|
||||
if (!addBoardRef.current || !templateIdRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const ACTION_DESCRIPTION = 'board created from channel'
|
||||
const LINKED_CHANNEL = 'linked channel'
|
||||
const asTemplate = false
|
||||
|
||||
let boardsAndBlocks = undefined
|
||||
|
||||
if (templateIdRef.current === EMPTY_BOARD) {
|
||||
boardsAndBlocks = await mutator.addEmptyBoard(teamId, intl)
|
||||
} else {
|
||||
boardsAndBlocks = await mutator.duplicateBoard(templateIdRef.current as string, ACTION_DESCRIPTION, asTemplate, undefined, undefined, teamId)
|
||||
}
|
||||
|
||||
const board = boardsAndBlocks.boards[0]
|
||||
await mutator.updateBoard({...board, channelId: channelId}, board, LINKED_CHANNEL)
|
||||
return board
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
props.setAction(() => addBoardToChannel)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
props.setCanCreate(!addBoard || (addBoard && selectedBoardTemplateId !== ''))
|
||||
}, [addBoard, selectedBoardTemplateId])
|
||||
|
||||
const getSubstringWithCompleteWords = (str: string, len: number) => {
|
||||
if (str?.length <= len) {
|
||||
return str
|
||||
}
|
||||
|
||||
// get the final part of the string in order to find the next whitespace if any
|
||||
const finalStringPart = str.substring(len)
|
||||
const wordBreakingIndex = finalStringPart.indexOf(' ')
|
||||
|
||||
// if there is no whitespace is because the lenght in this case falls into an entire word and doesn't affect the display, so just return it
|
||||
if (wordBreakingIndex === -1) {
|
||||
return str
|
||||
}
|
||||
return `${str.substring(0, (len + wordBreakingIndex))}…`
|
||||
}
|
||||
|
||||
const formatOptionLabel = ({ id, title, icon, description }: ReactSelectItem, optionLabel: FormatOptionLabelMeta<ReactSelectItem>) => {
|
||||
const cssPrefix = 'CreateBoardFromTemplate--templates-selector__menu-portal__option'
|
||||
|
||||
const descriptionLabel = description ? getSubstringWithCompleteWords(description, TEMPLATE_DESCRIPTION_LENGTH) : 'ㅤ'
|
||||
|
||||
const templateDescription = (
|
||||
<span className={`${cssPrefix}__description`}>
|
||||
{descriptionLabel}
|
||||
</span>
|
||||
)
|
||||
|
||||
// do not show the description for the selected option so the input only show the icon and title of the template
|
||||
const selectedOption = id === optionLabel.selectValue[0]?.id
|
||||
return (
|
||||
<div key={id}>
|
||||
<span className={`${cssPrefix}__icon`}>
|
||||
{icon || <CompassIcon icon='product-boards'/>}
|
||||
</span>
|
||||
<span className={`${cssPrefix}__title`}>
|
||||
{title}
|
||||
</span>
|
||||
{!selectedOption && templateDescription}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomValueContainer = ({ children, ...props }: any) => {
|
||||
return (
|
||||
<ValueContainer {...props}>
|
||||
<Placeholder {...props}>
|
||||
{props.selectProps.placeholder}
|
||||
</Placeholder>
|
||||
{React.Children.map(children, (child) =>
|
||||
child && child.type !== Placeholder ? child : null
|
||||
)}
|
||||
</ValueContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const loadOptions = useCallback(async (value = '') => {
|
||||
let templates = allTemplates.map((template) => {
|
||||
return {
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
icon: template.icon,
|
||||
description: template.description,
|
||||
}
|
||||
})
|
||||
|
||||
const emptyBoard = {
|
||||
id: EMPTY_BOARD,
|
||||
title: formatMessage({id: 'new_channel_modal.create_board.empty_board_title', defaultMessage: 'Empty board'}),
|
||||
icon: '',
|
||||
description: formatMessage({id: 'new_channel_modal.create_board.empty_board_description', defaultMessage: 'Create a new empty board'}),
|
||||
}
|
||||
|
||||
templates.push(emptyBoard)
|
||||
|
||||
if (value !== '') {
|
||||
templates = templates.filter(template => template.title.toLowerCase().includes(value.toLowerCase()))
|
||||
}
|
||||
return templates
|
||||
}, [allTemplates])
|
||||
|
||||
const onChange = useCallback((item: SingleValue<ReactSelectItem>) => {
|
||||
if (item) {
|
||||
setSelectedBoardTemplateId(item.id)
|
||||
}
|
||||
}, [setSelectedBoardTemplateId])
|
||||
|
||||
const selectorStyles = {
|
||||
menu: (baseStyles: CSSObject): CSSObject => ({
|
||||
...baseStyles,
|
||||
height: '164px',
|
||||
}),
|
||||
menuList: (baseStyles: CSSObject): CSSObject => ({
|
||||
...baseStyles,
|
||||
height: '160px',
|
||||
}),
|
||||
menuPortal: (baseStyles: CSSObject): CSSObject => ({
|
||||
...baseStyles,
|
||||
zIndex: 9999,
|
||||
}),
|
||||
valueContainer: (baseStyles: CSSObject): CSSObject => ({
|
||||
...baseStyles,
|
||||
overflow: 'visible'
|
||||
}),
|
||||
placeholder: (baseStyles: CSSObject, state: PlaceholderProps<ReactSelectItem, false, GroupBase<ReactSelectItem>>): CSSObject => {
|
||||
const modifyPlaceholder = state.selectProps.menuIsOpen || (!state.selectProps.menuIsOpen && state.hasValue)
|
||||
return {
|
||||
...baseStyles,
|
||||
position: 'absolute',
|
||||
backgroundColor: 'var(--sys-center-channel-bg)',
|
||||
padding: '0 3px',
|
||||
top: modifyPlaceholder ? -15 : '18%',
|
||||
transition: 'top 0.5s, font-size 0.5s, color 0.5s',
|
||||
fontSize: modifyPlaceholder ? 10 : 16,
|
||||
color: modifyPlaceholder ? 'var(--sidebar-text-active-border)' : 'rgba(var(--center-channel-color-rgb), 0.42)',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='CreateBoardFromTemplate'>
|
||||
<div className='add-board-to-channel'>
|
||||
<label>
|
||||
<input
|
||||
type='checkbox'
|
||||
onChange={showNewBoardTemplateSelector}
|
||||
checked={addBoard}
|
||||
id={'add-board-to-channel'}
|
||||
data-testid='add-board-to-channel-check'
|
||||
/>
|
||||
<span>
|
||||
{formatMessage({id: 'new_channel_modal.create_board.title', defaultMessage: 'Create a board for this channel'})}
|
||||
</span>
|
||||
{props.newBoardInfoIcon}
|
||||
</label>
|
||||
{addBoard && <div className='templates-selector'>
|
||||
<Select
|
||||
classNamePrefix={'CreateBoardFromTemplate--templates-selector'}
|
||||
placeholder={formatMessage({id: 'new_channel_modal.create_board.select_template_placeholder', defaultMessage: 'Select a template'})}
|
||||
onChange={onChange}
|
||||
components={{IndicatorSeparator: () => null, ValueContainer: CustomValueContainer}}
|
||||
loadOptions={loadOptions}
|
||||
getOptionValue={(v) => v.id}
|
||||
getOptionLabel={(v) => v.title}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={selectorStyles}
|
||||
menuPortalTarget={document.body}
|
||||
defaultOptions={true}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const IntlCreateBoardFromTemplate = (props: Props) => {
|
||||
const language = useAppSelector<string>(getLanguage)
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={language.split(/[_]/)[0]}
|
||||
messages={getMessages(language)}
|
||||
>
|
||||
<CreateBoardFromTemplate {...props}/>
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntlCreateBoardFromTemplate
|
@ -1,42 +0,0 @@
|
||||
.RHSChannelBoardItem {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
box-shadow: var(--elevation-1);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
color: rgb(var(--center-channel-color-rgb));
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
opacity: 0.64;
|
||||
}
|
||||
|
||||
.board-info {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 4px 0;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {render, screen} from '@testing-library/react'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {createBoard} from '../../../../webapp/src/blocks/board'
|
||||
import {mockStateStore, wrapIntl} from '../../../../webapp/src/testUtils'
|
||||
|
||||
import {TestBlockFactory} from '../../../../webapp/src/test/testBlockFactory'
|
||||
|
||||
import {Utils} from '../../../../webapp/src/utils'
|
||||
|
||||
import RHSChannelBoardItem from './rhsChannelBoardItem'
|
||||
|
||||
let mockDisplayDateTime: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
mockDisplayDateTime = jest.spyOn(Utils, 'displayDateTime').mockImplementation(() => 'July 10, 2022 at 1:40 AM')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockDisplayDateTime.mockRestore()
|
||||
})
|
||||
|
||||
describe('components/rhsChannelBoardItem', () => {
|
||||
it('render board', async () => {
|
||||
const board = createBoard()
|
||||
const state = {
|
||||
teams: {
|
||||
current: {
|
||||
id: 'team-id',
|
||||
name: 'team',
|
||||
display_name: 'Team name',
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
myBoardMemberships: {
|
||||
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
board.updateAt = 1657311058157
|
||||
board.title = 'Test board'
|
||||
|
||||
const store = mockStateStore([], state)
|
||||
const {container} = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoardItem board={board} />
|
||||
</ReduxProvider>
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('render board with menu open', async () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
const state = {
|
||||
teams: {
|
||||
current: {
|
||||
id: 'team-id',
|
||||
name: 'team',
|
||||
display_name: 'Team name',
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
myBoardMemberships: {
|
||||
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
board.id = 'test_id'
|
||||
board.title = 'New board'
|
||||
board.description = '**Board** with description'
|
||||
board.updateAt = 1657311058157
|
||||
const store = mockStateStore([], state)
|
||||
const {container} = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoardItem board={board} />
|
||||
</ReduxProvider>
|
||||
))
|
||||
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
await userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -1,120 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import mutator from '../../../../webapp/src/mutator'
|
||||
import {Utils} from '../../../../webapp/src/utils'
|
||||
import {getCurrentTeam} from '../../../../webapp/src/store/teams'
|
||||
import {createBoard, Board} from '../../../../webapp/src/blocks/board'
|
||||
import {useAppSelector} from '../../../../webapp/src/store/hooks'
|
||||
import IconButton from '../../../../webapp/src/widgets/buttons/iconButton'
|
||||
import OptionsIcon from '../../../../webapp/src/widgets/icons/options'
|
||||
import Menu from '../../../../webapp/src/widgets/menu'
|
||||
import MenuWrapper from '../../../../webapp/src/widgets/menuWrapper'
|
||||
import {SuiteWindow} from '../../../../webapp/src/types/index'
|
||||
import CompassIcon from '../../../../webapp/src/widgets/icons/compassIcon'
|
||||
|
||||
import {Permission} from '../../../../webapp/src/constants'
|
||||
|
||||
import BoardPermissionGate from '../../../../webapp/src/components/permissions/boardPermissionGate'
|
||||
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../../../webapp/src/telemetry/telemetryClient'
|
||||
|
||||
import './rhsChannelBoardItem.scss'
|
||||
|
||||
const windowAny = (window as SuiteWindow)
|
||||
|
||||
type Props = {
|
||||
board: Board
|
||||
}
|
||||
|
||||
const RHSChannelBoardItem = (props: Props) => {
|
||||
const intl = useIntl()
|
||||
const board = props.board
|
||||
|
||||
const team = useAppSelector(getCurrentTeam)
|
||||
if (!team) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleBoardClicked = (boardID: string) => {
|
||||
// send the telemetry information for the clicked board
|
||||
const extraData = {teamID: team.id, board: boardID}
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelsRHSBoard, extraData)
|
||||
|
||||
window.open(`${windowAny.frontendBaseURL}/team/${team.id}/${boardID}`, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
const onUnlinkBoard = async (board: Board) => {
|
||||
const newBoard = createBoard(board)
|
||||
newBoard.channelId = ''
|
||||
mutator.updateBoard(newBoard, board, 'unlinked channel')
|
||||
}
|
||||
|
||||
const untitledBoardTitle = intl.formatMessage({id: 'ViewTitle.untitled-board', defaultMessage: 'Untitled board'})
|
||||
|
||||
const markdownHtml = Utils.htmlFromMarkdown(board.description)
|
||||
return (
|
||||
<div
|
||||
onClick={() => handleBoardClicked(board.id)}
|
||||
className='RHSChannelBoardItem'
|
||||
>
|
||||
<div className='board-info'>
|
||||
{board.icon && <span className='icon'>{board.icon}</span>}
|
||||
<span className='title'>{board.title || untitledBoardTitle}</span>
|
||||
<MenuWrapper stopPropagationOnToggle={true}>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu
|
||||
position='left'
|
||||
>
|
||||
<BoardPermissionGate
|
||||
boardId={board.id}
|
||||
teamId={team.id}
|
||||
permissions={[Permission.ManageBoardRoles]}
|
||||
>
|
||||
<Menu.Text
|
||||
key={`unlinkBoard-${board.id}`}
|
||||
id='unlinkBoard'
|
||||
name={intl.formatMessage({id: 'rhs-boards.unlink-board', defaultMessage: 'Unlink board'})}
|
||||
icon={<CompassIcon icon='link-variant-off'/>}
|
||||
onClick={() => {
|
||||
onUnlinkBoard(board)
|
||||
}}
|
||||
/>
|
||||
</BoardPermissionGate>
|
||||
<BoardPermissionGate
|
||||
boardId={board.id}
|
||||
teamId={team.id}
|
||||
permissions={[Permission.ManageBoardRoles]}
|
||||
invert={true}
|
||||
>
|
||||
<Menu.Text
|
||||
key={`unlinkBoard-${board.id}`}
|
||||
id='unlinkBoard'
|
||||
disabled={true}
|
||||
name={intl.formatMessage({id: 'rhs-boards.unlink-board1', defaultMessage: 'Unlink board'})}
|
||||
icon={<CompassIcon icon='link-variant-off'/>}
|
||||
onClick={() => {
|
||||
onUnlinkBoard(board)
|
||||
}}
|
||||
subText={intl.formatMessage({id: 'rhs-board-non-admin-msg', defaultMessage:'You are not an admin of the board'})}
|
||||
/>
|
||||
</BoardPermissionGate>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
<div className='description'
|
||||
dangerouslySetInnerHTML={{__html: markdownHtml}}
|
||||
/>
|
||||
<div className='date'>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.last-update-at'
|
||||
defaultMessage='Last update at: {datetime}'
|
||||
values={{datetime: Utils.displayDateTime(new Date(board.updateAt), intl as any)}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RHSChannelBoardItem
|
@ -1,67 +0,0 @@
|
||||
.RHSChannelBoards {
|
||||
padding: 16px 24px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&.empty {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
padding: 60px;
|
||||
|
||||
@media screen and (min-height: 800px) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.rhs-boards-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
>h2 {
|
||||
text-align: center;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
.empty-paragraph {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.boards-screenshots {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.linked-boards {
|
||||
flex-grow: 1;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rhs-boards-list {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.Button {
|
||||
width: auto;
|
||||
align-self: center;
|
||||
max-width: 100%;
|
||||
|
||||
span {
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {act, render, screen} from '@testing-library/react'
|
||||
import {mocked} from 'jest-mock'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
import octoClient from '../../../../webapp/src/octoClient'
|
||||
import {BoardMember, createBoard} from '../../../../webapp/src/blocks/board'
|
||||
import {mockStateStore, wrapIntl} from '../../../../webapp/src/testUtils'
|
||||
|
||||
import {Utils} from '../../../../webapp/src/utils'
|
||||
|
||||
import RHSChannelBoards from './rhsChannelBoards'
|
||||
|
||||
jest.mock('../../../../webapp/src/octoClient')
|
||||
const mockedOctoClient = mocked(octoClient, true)
|
||||
|
||||
let mockDisplayDateTime: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
mockDisplayDateTime = jest.spyOn(Utils, 'displayDateTime').mockImplementation(() => 'July 10, 2022 at 1:40 AM')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockDisplayDateTime.mockRestore()
|
||||
})
|
||||
|
||||
describe('components/rhsChannelBoards', () => {
|
||||
const board1 = createBoard()
|
||||
board1.updateAt = 1657311058157
|
||||
const board2 = createBoard()
|
||||
const board3 = createBoard()
|
||||
board3.updateAt = 1657311058157
|
||||
|
||||
board1.channelId = 'channel-id'
|
||||
board3.channelId = 'channel-id'
|
||||
|
||||
const boardMembership1 = {boardId: board1.id, userId: 'user-id'} as BoardMember
|
||||
const boardMembership2 = {boardId: board2.id, userId: 'user-id'} as BoardMember
|
||||
const boardMembership3 = {boardId: board3.id, userId: 'user-id'} as BoardMember
|
||||
|
||||
const team = {
|
||||
id: 'team-id',
|
||||
name: 'team',
|
||||
display_name: 'Team name',
|
||||
}
|
||||
const state = {
|
||||
teams: {
|
||||
allTeams: [team],
|
||||
current: team,
|
||||
currentId: team.id,
|
||||
},
|
||||
users: {
|
||||
me: {
|
||||
id: 'user-id',
|
||||
permissions: ['create_post']
|
||||
},
|
||||
},
|
||||
language: {
|
||||
value: 'en',
|
||||
},
|
||||
boards: {
|
||||
boards: {
|
||||
[board1.id]: board1,
|
||||
[board2.id]: board2,
|
||||
[board3.id]: board3,
|
||||
},
|
||||
myBoardMemberships: {
|
||||
[board1.id]: boardMembership1,
|
||||
[board2.id]: boardMembership2,
|
||||
[board3.id]: boardMembership3,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
current: {
|
||||
id: 'channel-id',
|
||||
name: 'channel',
|
||||
display_name: 'Channel Name',
|
||||
type: 'O',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockedOctoClient.getBoards.mockResolvedValue([board1, board2, board3])
|
||||
mockedOctoClient.getMyBoardMemberships.mockResolvedValue([boardMembership1, boardMembership2, boardMembership3])
|
||||
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders the RHS for channel boards', async () => {
|
||||
const store = mockStateStore([thunk], state)
|
||||
let container: Element | DocumentFragment | null = null
|
||||
await act(async () => {
|
||||
const result = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoards/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
const buttonElement = screen.queryByText('Add')
|
||||
expect(buttonElement).not.toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with empty list of boards', async () => {
|
||||
const localState = {...state, boards: {...state.boards, boards: {}}}
|
||||
const store = mockStateStore([thunk], localState)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
await act(async () => {
|
||||
const result = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoards/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Link boards to Channel Name')
|
||||
expect(buttonElement).not.toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders the RHS for channel boards, no add', async () => {
|
||||
const localState = {...state, users: {me:{id: 'user-id'}}}
|
||||
const store = mockStateStore([thunk], localState)
|
||||
let container: Element | DocumentFragment | null = null
|
||||
await act(async () => {
|
||||
const result = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoards/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Add')
|
||||
expect(buttonElement).toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with empty list of boards, cannot add', async () => {
|
||||
const localState = {...state, users: {me:{id: 'user-id'}}, boards: {...state.boards, boards: {}}}
|
||||
const store = mockStateStore([thunk], localState)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
await act(async () => {
|
||||
const result = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoards/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Link boards to Channel Name')
|
||||
expect(buttonElement).toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -1,186 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useEffect} from 'react'
|
||||
import {FormattedMessage, IntlProvider, useIntl} from 'react-intl'
|
||||
|
||||
import {getMessages} from '../../../../webapp/src/i18n'
|
||||
import {getLanguage} from '../../../../webapp/src/store/language'
|
||||
|
||||
import {useWebsockets} from '../../../../webapp/src/hooks/websockets'
|
||||
|
||||
import {Board, BoardMember} from '../../../../webapp/src/blocks/board'
|
||||
import {getCurrentTeamId} from '../../../../webapp/src/store/teams'
|
||||
import {IUser} from '../../../../webapp/src/user'
|
||||
import {getMe, fetchMe} from '../../../../webapp/src/store/users'
|
||||
import {loadBoards, loadMyBoardsMemberships} from '../../../../webapp/src/store/initialLoad'
|
||||
import {getCurrentChannel} from '../../../../webapp/src/store/channels'
|
||||
import {
|
||||
getMySortedBoards,
|
||||
setLinkToChannel,
|
||||
updateBoards,
|
||||
updateMembersEnsuringBoardsAndUsers,
|
||||
addMyBoardMemberships,
|
||||
} from '../../../../webapp/src/store/boards'
|
||||
import {useAppSelector, useAppDispatch} from '../../../../webapp/src/store/hooks'
|
||||
import AddIcon from '../../../../webapp/src/widgets/icons/add'
|
||||
import Button from '../../../../webapp/src/widgets/buttons/button'
|
||||
|
||||
import {Utils} from '../../../../webapp/src/utils'
|
||||
import {WSClient} from '../../../../webapp/src/wsclient'
|
||||
|
||||
import boardsScreenshots from '../../../../webapp/static/boards-screenshots.png'
|
||||
|
||||
import RHSChannelBoardItem from './rhsChannelBoardItem'
|
||||
|
||||
import './rhsChannelBoards.scss'
|
||||
|
||||
const RHSChannelBoards = () => {
|
||||
const boards = useAppSelector(getMySortedBoards)
|
||||
const teamId = useAppSelector(getCurrentTeamId)
|
||||
const currentChannel = useAppSelector(getCurrentChannel)
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const dispatch = useAppDispatch()
|
||||
const intl = useIntl()
|
||||
const [dataLoaded, setDataLoaded] = React.useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
dispatch(loadBoards()),
|
||||
dispatch(loadMyBoardsMemberships()),
|
||||
dispatch(fetchMe()),
|
||||
]).then(() => setDataLoaded(true))
|
||||
}, [currentChannel?.id])
|
||||
|
||||
useWebsockets(teamId || '', (wsClient: WSClient) => {
|
||||
const onChangeBoardHandler = (_: WSClient, boards: Board[]): void => {
|
||||
dispatch(updateBoards(boards))
|
||||
}
|
||||
const onChangeMemberHandler = (_: WSClient, members: BoardMember[]): void => {
|
||||
dispatch(updateMembersEnsuringBoardsAndUsers(members))
|
||||
|
||||
if (me) {
|
||||
const myBoardMemberships = members.filter((boardMember) => boardMember.userId === me.id)
|
||||
dispatch(addMyBoardMemberships(myBoardMemberships))
|
||||
}
|
||||
}
|
||||
|
||||
wsClient.addOnChange(onChangeBoardHandler, 'board')
|
||||
wsClient.addOnChange(onChangeMemberHandler, 'boardMembers')
|
||||
|
||||
return () => {
|
||||
wsClient.removeOnChange(onChangeBoardHandler, 'board')
|
||||
wsClient.removeOnChange(onChangeMemberHandler, 'boardMembers')
|
||||
}
|
||||
}, [me])
|
||||
|
||||
if (!boards) {
|
||||
return null
|
||||
}
|
||||
if (!teamId) {
|
||||
return null
|
||||
}
|
||||
if (!currentChannel) {
|
||||
return null
|
||||
}
|
||||
if (!dataLoaded) {
|
||||
return null
|
||||
}
|
||||
|
||||
const channelBoards = boards.filter((b) => b.channelId === currentChannel.id)
|
||||
|
||||
let channelName = currentChannel.display_name
|
||||
let headerChannelName = currentChannel.display_name
|
||||
|
||||
if (currentChannel.type === 'D') {
|
||||
channelName = intl.formatMessage({id: 'rhs-boards.dm', defaultMessage: 'DM'})
|
||||
headerChannelName = intl.formatMessage({id: 'rhs-boards.header.dm', defaultMessage: 'this Direct Message'})
|
||||
} else if (currentChannel.type === 'G') {
|
||||
channelName = intl.formatMessage({id: 'rhs-boards.gm', defaultMessage: 'GM'})
|
||||
headerChannelName = intl.formatMessage({id: 'rhs-boards.header.gm', defaultMessage: 'this Group Message'})
|
||||
}
|
||||
|
||||
if (channelBoards.length === 0) {
|
||||
return (
|
||||
<div className='focalboard-body'>
|
||||
<div className='RHSChannelBoards empty'>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.no-boards-linked-to-channel'
|
||||
defaultMessage='No boards are linked to {channelName} yet'
|
||||
values={{channelName: headerChannelName}}
|
||||
/>
|
||||
</h2>
|
||||
<div className='empty-paragraph'>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.no-boards-linked-to-channel-description'
|
||||
defaultMessage='Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view.'
|
||||
/>
|
||||
</div>
|
||||
<div className='boards-screenshots'><img src={Utils.buildURL(boardsScreenshots, true)}/></div>
|
||||
{me?.permissions?.find((s) => s === 'create_post') &&
|
||||
<Button
|
||||
onClick={() => dispatch(setLinkToChannel(currentChannel.id))}
|
||||
emphasis='primary'
|
||||
size='medium'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.link-boards-to-channel'
|
||||
defaultMessage='Link boards to {channelName}'
|
||||
values={{channelName: channelName}}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='focalboard-body'>
|
||||
<div className='RHSChannelBoards'>
|
||||
<div className='rhs-boards-header'>
|
||||
<span className='linked-boards'>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.linked-boards'
|
||||
defaultMessage='Linked boards'
|
||||
/>
|
||||
</span>
|
||||
{me?.permissions?.find((s) => s === 'create_post') &&
|
||||
<Button
|
||||
onClick={() => dispatch(setLinkToChannel(currentChannel.id))}
|
||||
icon={<AddIcon/>}
|
||||
emphasis='primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='rhs-boards.add'
|
||||
defaultMessage='Add'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
<div className='rhs-boards-list'>
|
||||
{channelBoards.map((b) => (
|
||||
<RHSChannelBoardItem
|
||||
key={b.id}
|
||||
board={b}
|
||||
/>))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const IntlRHSChannelBoards = () => {
|
||||
const language = useAppSelector<string>(getLanguage)
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={language.split(/[_]/)[0]}
|
||||
messages={getMessages(language)}
|
||||
>
|
||||
<RHSChannelBoards/>
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntlRHSChannelBoards
|
@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import {mockStateStore, wrapIntl} from '../../../../webapp/src/testUtils'
|
||||
|
||||
import RHSChannelBoardsHeader from './rhsChannelBoardsHeader'
|
||||
|
||||
describe('components/rhsChannelBoardsHeader', () => {
|
||||
it('renders the header', async () => {
|
||||
const state = {
|
||||
language: {
|
||||
value: 'en',
|
||||
},
|
||||
channels: {
|
||||
current: {
|
||||
id: 'channel-id',
|
||||
name: 'channel',
|
||||
display_name: 'Channel Name',
|
||||
type: 'O',
|
||||
},
|
||||
},
|
||||
}
|
||||
const store = mockStateStore([], state)
|
||||
const {container} = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoardsHeader/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {FormattedMessage, IntlProvider} from 'react-intl'
|
||||
|
||||
import {getMessages} from '../../../../webapp/src/i18n'
|
||||
import {getLanguage} from '../../../../webapp/src/store/language'
|
||||
import {getCurrentChannel} from '../../../../webapp/src/store/channels'
|
||||
import {useAppSelector} from '../../../../webapp/src/store/hooks'
|
||||
import {Utils} from '../../../../webapp/src/utils'
|
||||
|
||||
import appBarIcon from '../../../../webapp/static/app-bar-icon.png'
|
||||
|
||||
const RHSChannelBoardsHeader = () => {
|
||||
const currentChannel = useAppSelector(getCurrentChannel)
|
||||
const language = useAppSelector<string>(getLanguage)
|
||||
|
||||
if (!currentChannel) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={language.split(/[_]/)[0]}
|
||||
messages={getMessages(language)}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
className='boards-rhs-header-logo'
|
||||
src={Utils.buildURL(appBarIcon, true)}
|
||||
/>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='rhs-channel-boards-header.title'
|
||||
defaultMessage='Boards'
|
||||
/>
|
||||
</span>
|
||||
<span className='style--none sidebar--right__title__subtitle'>{currentChannel.display_name}</span>
|
||||
</div>
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default RHSChannelBoardsHeader
|
@ -1,46 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {Utils} from '../../../webapp/src/utils'
|
||||
|
||||
type State = {
|
||||
hasError: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
state = {hasError: false}
|
||||
msg = 'Redirecting to error page...'
|
||||
|
||||
handleError = (): void => {
|
||||
const url = Utils.getBaseURL() + '/error?id=unknown'
|
||||
Utils.log('error boundary redirecting to ' + url)
|
||||
window.location.replace(url)
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(/*error: Error*/): State {
|
||||
return {hasError: true}
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
Utils.logError(error + ': ' + errorInfo)
|
||||
}
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) {
|
||||
this.handleError()
|
||||
return <span>{this.msg}</span>
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
@ -1,459 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useEffect} from 'react'
|
||||
import {createIntl, createIntlCache} from 'react-intl'
|
||||
import {Store, Action} from 'redux'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {createBrowserHistory, History} from 'history'
|
||||
|
||||
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder'
|
||||
|
||||
import {GlobalState} from 'mattermost-redux/types/store'
|
||||
|
||||
import {selectTeam} from 'mattermost-redux/actions/teams'
|
||||
|
||||
import {SuiteWindow} from '../../../webapp/src/types/index'
|
||||
import {UserSettings} from '../../../webapp/src/userSettings'
|
||||
import {getMessages, getCurrentLanguage} from '../../../webapp/src/i18n'
|
||||
|
||||
const windowAny = (window as SuiteWindow)
|
||||
windowAny.baseURL = process.env.TARGET_IS_PRODUCT ? '/plugins/boards' : '/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 WithWebSockets from '../../../webapp/src/components/withWebSockets'
|
||||
import {setChannel} from '../../../webapp/src/store/channels'
|
||||
import {initialLoad} from '../../../webapp/src/store/initialLoad'
|
||||
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 TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient'
|
||||
|
||||
import '../../../webapp/src/styles/focalboard-variables.scss'
|
||||
import '../../../webapp/src/styles/main.scss'
|
||||
import '../../../webapp/src/styles/labels.scss'
|
||||
import octoClient from '../../../webapp/src/octoClient'
|
||||
import {Board} from '../../../webapp/src/blocks/board'
|
||||
|
||||
import appBarIcon from '../../../webapp/static/app-bar-icon.png'
|
||||
|
||||
import BoardsUnfurl from './components/boardsUnfurl/boardsUnfurl'
|
||||
import RHSChannelBoards from './components/rhsChannelBoards'
|
||||
import RHSChannelBoardsHeader from './components/rhsChannelBoardsHeader'
|
||||
import BoardSelector from './components/boardSelector'
|
||||
import wsClient, {
|
||||
MMWebSocketClient,
|
||||
ACTION_UPDATE_BLOCK,
|
||||
ACTION_UPDATE_CLIENT_CONFIG,
|
||||
ACTION_UPDATE_SUBSCRIPTION,
|
||||
ACTION_UPDATE_CARD_LIMIT_TIMESTAMP,
|
||||
ACTION_UPDATE_CATEGORY,
|
||||
ACTION_UPDATE_BOARD_CATEGORY,
|
||||
ACTION_UPDATE_BOARD,
|
||||
ACTION_REORDER_CATEGORIES,
|
||||
} from './../../../webapp/src/wsclient'
|
||||
|
||||
import manifest from './manifest'
|
||||
import ErrorBoundary from './error_boundary'
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import {PluginRegistry} from './types/mattermost-webapp'
|
||||
|
||||
import './plugin.scss'
|
||||
import CloudUpgradeNudge from "./components/cloudUpgradeNudge/cloudUpgradeNudge"
|
||||
import CreateBoardFromTemplate from './components/createBoardFromTemplate'
|
||||
|
||||
function getSubpath(siteURL: string): string {
|
||||
const url = new URL(siteURL)
|
||||
|
||||
// remove trailing slashes
|
||||
return url.pathname.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
const TELEMETRY_RUDDER_KEY = 'placeholder_rudder_key'
|
||||
const TELEMETRY_RUDDER_DATAPLANE_URL = 'placeholder_rudder_dataplane_url'
|
||||
const TELEMETRY_OPTIONS = {
|
||||
context: {
|
||||
ip: '0.0.0.0',
|
||||
},
|
||||
page: {
|
||||
path: '',
|
||||
referrer: '',
|
||||
search: '',
|
||||
title: '',
|
||||
url: '',
|
||||
},
|
||||
anonymousId: '00000000000000000000000000',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
webSocketClient: MMWebSocketClient
|
||||
}
|
||||
|
||||
function customHistory() {
|
||||
const history = createBrowserHistory({basename: Utils.getFrontendBaseURL()})
|
||||
|
||||
if (Utils.isDesktop()) {
|
||||
window.addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.origin !== windowAny.location.origin) {
|
||||
return
|
||||
}
|
||||
|
||||
const pathName = event.data.message?.pathName
|
||||
if (!pathName || !pathName.startsWith('/boards')) {
|
||||
return
|
||||
}
|
||||
|
||||
Utils.log(`Navigating Boards to ${pathName}`)
|
||||
history.replace(pathName.replace('/boards', ''))
|
||||
})
|
||||
}
|
||||
return {
|
||||
...history,
|
||||
push: (path: string, state?: unknown) => {
|
||||
if (Utils.isDesktop()) {
|
||||
windowAny.postMessage(
|
||||
{
|
||||
type: 'browser-history-push',
|
||||
message: {
|
||||
path: `${windowAny.frontendBaseURL}${path}`,
|
||||
},
|
||||
},
|
||||
windowAny.location.origin,
|
||||
)
|
||||
} else {
|
||||
history.push(path, state as Record<string, never>)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let browserHistory: History<unknown>
|
||||
|
||||
const MainApp = (props: Props) => {
|
||||
useEffect(() => {
|
||||
document.body.classList.add('focalboard-body')
|
||||
document.body.classList.add('app__body')
|
||||
const root = document.getElementById('root')
|
||||
if (root) {
|
||||
root.classList.add('focalboard-plugin-root')
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove('focalboard-body')
|
||||
document.body.classList.remove('app__body')
|
||||
if (root) {
|
||||
root.classList.remove('focalboard-plugin-root')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ReduxProvider store={store}>
|
||||
<WithWebSockets manifest={manifest} webSocketClient={props.webSocketClient}>
|
||||
<div id='focalboard-app'>
|
||||
<App history={browserHistory}/>
|
||||
</div>
|
||||
<div id='focalboard-root-portal'/>
|
||||
</WithWebSockets>
|
||||
</ReduxProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const HeaderComponent = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<GlobalHeader history={browserHistory}/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
export default class Plugin {
|
||||
channelHeaderButtonId?: string
|
||||
rhsId?: string
|
||||
boardSelectorId?: string
|
||||
registry?: PluginRegistry
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
async initialize(registry: PluginRegistry, mmStore: Store<GlobalState, Action<Record<string, unknown>>>): Promise<void> {
|
||||
const siteURL = mmStore.getState().entities.general.config.SiteURL
|
||||
const subpath = siteURL ? getSubpath(siteURL) : ''
|
||||
windowAny.frontendBaseURL = subpath + windowAny.frontendBaseURL
|
||||
windowAny.baseURL = subpath + windowAny.baseURL
|
||||
browserHistory = customHistory()
|
||||
const cache = createIntlCache()
|
||||
const intl = createIntl({
|
||||
// modeled after <IntlProvider> in webapp/src/app.tsx
|
||||
locale: getCurrentLanguage(),
|
||||
messages: getMessages(getCurrentLanguage())
|
||||
}, cache)
|
||||
|
||||
|
||||
this.registry = registry
|
||||
|
||||
UserSettings.nameFormat = mmStore.getState().entities.preferences?.myPreferences['display_settings--name_format']?.value || null
|
||||
let theme = mmStore.getState().entities.preferences.myPreferences.theme
|
||||
setMattermostTheme(theme)
|
||||
|
||||
const productID = process.env.TARGET_IS_PRODUCT ? 'boards' : manifest.id
|
||||
|
||||
// register websocket handlers
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${productID}_${ACTION_UPDATE_BOARD}`, (e: any) => wsClient.updateHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${productID}_${ACTION_UPDATE_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${productID}_${ACTION_UPDATE_BOARD_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${productID}_${ACTION_UPDATE_CLIENT_CONFIG}`, (e: any) => wsClient.updateClientConfigHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${productID}_${ACTION_UPDATE_CARD_LIMIT_TIMESTAMP}`, (e: any) => wsClient.updateCardLimitTimestampHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${productID}_${ACTION_UPDATE_SUBSCRIPTION}`, (e: any) => wsClient.updateSubscriptionHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${productID}_${ACTION_REORDER_CATEGORIES}`, (e) => wsClient.updateHandler(e.data))
|
||||
|
||||
this.registry?.registerWebSocketEventHandler('plugin_statuses_changed', (e: any) => wsClient.pluginStatusesChangedHandler(e.data))
|
||||
this.registry?.registerPostTypeComponent('custom_cloud_upgrade_nudge', CloudUpgradeNudge)
|
||||
this.registry?.registerWebSocketEventHandler('preferences_changed', (e: any) => {
|
||||
let preferences
|
||||
try {
|
||||
preferences = JSON.parse(e.data.preferences)
|
||||
} catch {
|
||||
preferences = []
|
||||
}
|
||||
if (preferences) {
|
||||
for (const preference of preferences) {
|
||||
if (preference.category === 'theme' && theme !== preference.value) {
|
||||
setMattermostTheme(JSON.parse(preference.value))
|
||||
theme = preference.value
|
||||
}
|
||||
if(preference.category === 'display_settings' && preference.name === 'name_format'){
|
||||
UserSettings.nameFormat = preference.value
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let lastViewedChannel = mmStore.getState().entities.channels.currentChannelId
|
||||
let prevTeamID: string
|
||||
|
||||
const currentChannel = mmStore.getState().entities.channels.currentChannelId
|
||||
const currentChannelObj = mmStore.getState().entities.channels.channels[currentChannel]
|
||||
store.dispatch(setChannel(currentChannelObj))
|
||||
|
||||
mmStore.subscribe(() => {
|
||||
const currentUserId = mmStore.getState().entities.users.currentUserId
|
||||
const currentChannel = mmStore.getState().entities.channels.currentChannelId
|
||||
if (lastViewedChannel !== currentChannel && currentChannel) {
|
||||
localStorage.setItem('focalboardLastViewedChannel:' + currentUserId, currentChannel)
|
||||
lastViewedChannel = currentChannel
|
||||
octoClient.channelId = currentChannel
|
||||
const currentChannelObj = mmStore.getState().entities.channels.channels[lastViewedChannel]
|
||||
store.dispatch(setChannel(currentChannelObj))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (prevTeamID && window.location.pathname.startsWith(windowAny.frontendBaseURL || '')) {
|
||||
// Don't re-push the URL if we're already on a URL for the current team
|
||||
if (!window.location.pathname.startsWith(`${(windowAny.frontendBaseURL || '')}/team/${currentTeamID}`))
|
||||
browserHistory.push(`/team/${currentTeamID}`)
|
||||
}
|
||||
prevTeamID = currentTeamID
|
||||
store.dispatch(setTeam(currentTeamID))
|
||||
octoClient.teamId = currentTeamID
|
||||
store.dispatch(initialLoad())
|
||||
}
|
||||
|
||||
if (currentTeamID && currentTeamID !== prevTeamID) {
|
||||
let theme = mmStore.getState().entities.preferences.myPreferences[`theme--${currentTeamID}`]
|
||||
if (!theme) {
|
||||
theme = mmStore.getState().entities.preferences.myPreferences['theme--'] || mmStore.getState().entities.preferences.myPreferences.theme
|
||||
}
|
||||
setMattermostTheme(theme)
|
||||
}
|
||||
})
|
||||
|
||||
let fbPrevTeamID = store.getState().teams.currentId
|
||||
store.subscribe(() => {
|
||||
const currentTeamID: string = store.getState().teams.currentId
|
||||
const currentUserId = mmStore.getState().entities.users.currentUserId
|
||||
if (currentTeamID !== fbPrevTeamID) {
|
||||
fbPrevTeamID = currentTeamID
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
mmStore.dispatch(selectTeam(currentTeamID))
|
||||
localStorage.setItem(`user_prev_team:${currentUserId}`, currentTeamID)
|
||||
}
|
||||
})
|
||||
|
||||
if (this.registry.registerProduct) {
|
||||
windowAny.frontendBaseURL = subpath + '/boards'
|
||||
|
||||
const {rhsId, toggleRHSPlugin} = this.registry.registerRightHandSidebarComponent(
|
||||
(props: {webSocketClient: MMWebSocketClient}) => (
|
||||
<ReduxProvider store={store}>
|
||||
<WithWebSockets manifest={manifest} webSocketClient={props.webSocketClient}>
|
||||
<RHSChannelBoards/>
|
||||
</WithWebSockets>
|
||||
</ReduxProvider>
|
||||
),
|
||||
<ErrorBoundary>
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoardsHeader/>
|
||||
</ReduxProvider>
|
||||
</ErrorBoundary>
|
||||
,
|
||||
)
|
||||
this.rhsId = rhsId
|
||||
|
||||
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, () => mmStore.dispatch(toggleRHSPlugin), 'Boards', 'Boards')
|
||||
|
||||
this.registry.registerProduct(
|
||||
'/boards',
|
||||
'product-boards',
|
||||
'Boards',
|
||||
'/boards',
|
||||
MainApp,
|
||||
HeaderComponent,
|
||||
() => null,
|
||||
true,
|
||||
)
|
||||
|
||||
const goToFocalboardTemplate = () => {
|
||||
const currentTeam = mmStore.getState().entities.teams.currentTeamId
|
||||
const currentChannel = mmStore.getState().entities.channels.currentChannelId
|
||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {teamID: currentTeam})
|
||||
window.open(`${windowAny.frontendBaseURL}/team/${currentTeam}/new/${currentChannel}`, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
if (registry.registerChannelIntroButtonAction) {
|
||||
this.channelHeaderButtonId = registry.registerChannelIntroButtonAction(<FocalboardIcon/>, goToFocalboardTemplate, intl.formatMessage({id: 'ChannelIntro.CreateBoard', defaultMessage: 'Create a board'}))
|
||||
}
|
||||
|
||||
if (this.registry.registerAppBarComponent) {
|
||||
this.registry.registerAppBarComponent(Utils.buildURL(appBarIcon, true), () => mmStore.dispatch(toggleRHSPlugin), intl.formatMessage({id: 'AppBar.Tooltip', defaultMessage: 'Toggle Linked Boards'}))
|
||||
}
|
||||
|
||||
if (this.registry.registerActionAfterChannelCreation) {
|
||||
this.registry.registerActionAfterChannelCreation((props: {
|
||||
setCanCreate: (canCreate: boolean) => void,
|
||||
setAction: (fn: () => (channelId: string, teamId: string) => Promise<Board | undefined>) => void,
|
||||
newBoardInfoIcon: React.ReactNode,
|
||||
}) => (
|
||||
<ReduxProvider store={store}>
|
||||
<CreateBoardFromTemplate
|
||||
setCanCreate={props.setCanCreate}
|
||||
setAction={props.setAction}
|
||||
newBoardInfoIcon={props.newBoardInfoIcon}
|
||||
/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
}
|
||||
|
||||
this.registry.registerPostWillRenderEmbedComponent(
|
||||
(embed) => embed.type === 'boards',
|
||||
(props: {embed: {data: string}, webSocketClient: MMWebSocketClient}) => (
|
||||
<ReduxProvider store={store}>
|
||||
<BoardsUnfurl
|
||||
embed={props.embed}
|
||||
webSocketClient={props.webSocketClient}
|
||||
/>
|
||||
</ReduxProvider>
|
||||
),
|
||||
false
|
||||
)
|
||||
|
||||
// Site statistics handler
|
||||
if (registry.registerSiteStatisticsHandler) {
|
||||
registry.registerSiteStatisticsHandler(async () => {
|
||||
const siteStats = await octoClient.getSiteStatistics()
|
||||
if(siteStats){
|
||||
return {
|
||||
boards_count: {
|
||||
name: intl.formatMessage({id: 'SiteStats.total_boards', defaultMessage: 'Total Boards'}),
|
||||
id: 'total_boards',
|
||||
icon: 'icon-product-boards',
|
||||
value: siteStats.board_count,
|
||||
},
|
||||
cards_count: {
|
||||
name: intl.formatMessage({id: 'SiteStats.total_cards', defaultMessage: 'Total Cards'}),
|
||||
id: 'total_cards',
|
||||
icon: 'icon-products',
|
||||
value: siteStats.card_count,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.boardSelectorId = this.registry.registerRootComponent((props: {webSocketClient: MMWebSocketClient}) => (
|
||||
<ReduxProvider store={store}>
|
||||
<WithWebSockets manifest={manifest} webSocketClient={props.webSocketClient}>
|
||||
<BoardSelector/>
|
||||
</WithWebSockets>
|
||||
</ReduxProvider>
|
||||
))
|
||||
|
||||
const config = await octoClient.getClientConfig()
|
||||
if (config?.telemetry) {
|
||||
let rudderKey = TELEMETRY_RUDDER_KEY
|
||||
let rudderUrl = TELEMETRY_RUDDER_DATAPLANE_URL
|
||||
|
||||
if (rudderKey.startsWith('placeholder') && rudderUrl.startsWith('placeholder')) {
|
||||
rudderKey = process.env.RUDDER_KEY as string //eslint-disable-line no-process-env
|
||||
rudderUrl = process.env.RUDDER_DATAPLANE_URL as string //eslint-disable-line no-process-env
|
||||
}
|
||||
|
||||
if (rudderKey !== '') {
|
||||
const rudderCfg = {} as {setCookieDomain: string}
|
||||
if (siteURL && siteURL !== '') {
|
||||
try {
|
||||
rudderCfg.setCookieDomain = new URL(siteURL).hostname
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (_) {}
|
||||
}
|
||||
rudderAnalytics.load(rudderKey, rudderUrl, rudderCfg)
|
||||
|
||||
rudderAnalytics.identify(config?.telemetryid, {}, TELEMETRY_OPTIONS)
|
||||
|
||||
rudderAnalytics.page('BoardsLoaded', '',
|
||||
TELEMETRY_OPTIONS.page,
|
||||
{
|
||||
context: TELEMETRY_OPTIONS.context,
|
||||
anonymousId: TELEMETRY_OPTIONS.anonymousId,
|
||||
})
|
||||
|
||||
rudderAnalytics.ready(() => {
|
||||
TelemetryClient.setTelemetryHandler(new RudderTelemetryHandler())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
windowAny.getCurrentTeamId = (): string => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return mmStore.getState().entities.teams.currentTeamId
|
||||
}
|
||||
}
|
||||
|
||||
uninitialize(): void {
|
||||
if (this.channelHeaderButtonId) {
|
||||
this.registry?.unregisterComponent(this.channelHeaderButtonId)
|
||||
}
|
||||
if (this.rhsId) {
|
||||
this.registry?.unregisterComponent(this.rhsId)
|
||||
}
|
||||
if (this.boardSelectorId) {
|
||||
this.registry?.unregisterComponent(this.boardSelectorId)
|
||||
}
|
||||
|
||||
// unregister websocket handlers
|
||||
this.registry?.unregisterWebSocketEventHandler(wsClient.clientPrefix + ACTION_UPDATE_BLOCK)
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import manifest, {id, version} from './manifest'
|
||||
|
||||
test('Plugin manifest, id and version are defined', () => {
|
||||
expect(manifest).toBeDefined()
|
||||
expect(manifest.id).toBeDefined()
|
||||
expect(manifest.version).toBeDefined()
|
||||
})
|
||||
|
||||
// To ease migration, verify separate export of id and version.
|
||||
test('Plugin id and version are defined', () => {
|
||||
expect(id).toBeDefined()
|
||||
expect(version).toBeDefined()
|
||||
})
|
@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import manifest from '../../plugin.json'
|
||||
|
||||
export default manifest
|
||||
export const id = manifest.id
|
||||
export const version = manifest.version
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user