mirror of
https://github.com/mattermost/focalboard.git
synced 2025-04-19 19:47:11 -04:00
Compare commits
69 commits
Author | SHA1 | Date | |
---|---|---|---|
|
de5e5cc414 | ||
|
a8897be8d7 | ||
|
4342b758b3 | ||
|
bfaa37fc24 | ||
|
1932acb628 | ||
|
568a5f01b6 | ||
|
94e5ff5393 | ||
|
466ce56a90 | ||
|
c8e729b6fe | ||
|
7a31925d8a | ||
|
17a44203fe | ||
|
8a7db01249 | ||
|
3bec6bbcbe | ||
|
6bd7464372 | ||
|
793dd73496 | ||
|
d559157d28 | ||
|
cbc71b3af7 | ||
|
28ec2b361a | ||
|
3269b67102 | ||
|
c584fae8df | ||
|
134422df4d | ||
|
7226ed2cbb | ||
|
277ea9facc | ||
|
a216e43fab | ||
|
556a9f80fb | ||
|
d7cb5f8bd2 | ||
|
f6036cd662 | ||
|
a0639e7a71 | ||
|
4038d8471e | ||
|
dd3be3e3ed | ||
|
3625c53527 | ||
|
257cc5f1fd | ||
|
e6b67af8b1 | ||
|
9901557d99 | ||
|
e0dbb380a3 | ||
|
a745c29cb1 | ||
|
8cf0ec3a61 | ||
|
4e7416358e | ||
|
1388d41a9b | ||
|
236099f40e | ||
|
1381babf37 | ||
|
77b8a8e14d | ||
|
4a6cab9233 | ||
|
56cc1b84c1 | ||
|
d69c5bee0e | ||
|
5bea58c2c3 | ||
|
5dfd402e26 | ||
|
625526c3e7 | ||
|
343b7bdc4b | ||
|
c4454cac52 | ||
|
3b7954872e | ||
|
d95d100d8c | ||
|
a76ef9c168 | ||
|
e4630d1a84 | ||
|
c3b1c82b1a | ||
|
888c169910 | ||
|
b7d94a8fe2 | ||
|
d10e4070ba | ||
|
0af70a0a4f | ||
|
252f2138ca | ||
|
c9acca6acb | ||
|
799b8ead1c | ||
|
acc9750d97 | ||
|
7fd923b213 | ||
|
615b6ceee7 | ||
|
dc0e664df9 | ||
|
2b9eec1bec | ||
|
1d22070454 | ||
|
a015aa2cf2 |
455 changed files with 4951 additions and 66885 deletions
|
@ -5,7 +5,6 @@ node_modules
|
|||
.github/
|
||||
mac/
|
||||
win-wpf/
|
||||
mattermost-plugin/
|
||||
website/
|
||||
linux/
|
||||
go.work
|
||||
|
|
104
.github/workflows/ci.yml
vendored
104
.github/workflows/ci.yml
vendored
|
@ -15,7 +15,7 @@ env:
|
|||
jobs:
|
||||
|
||||
ci-ubuntu-server:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
@ -27,68 +27,36 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
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-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
|
||||
- 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.19.5
|
||||
go-version-file: focalboard/server/go.mod
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.1.0
|
||||
node-version-file: focalboard/webapp/.nvmrc
|
||||
|
||||
- name: Build Linux server
|
||||
run: cd focalboard; make server-linux-package
|
||||
|
@ -115,35 +83,20 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
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:
|
||||
|
@ -152,29 +105,14 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version-file: focalboard/server/go.mod
|
||||
|
||||
- name: "Test server (minimum): ${{matrix['db']}}"
|
||||
run: cd focalboard; make server-test-mini-${{matrix['db']}}
|
||||
|
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
permissions:
|
||||
security-events: write # for github/codeql-action/autobuild to send a status report
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
158
.github/workflows/dev-release.yml
vendored
158
.github/workflows/dev-release.yml
vendored
|
@ -14,60 +14,39 @@ env:
|
|||
jobs:
|
||||
|
||||
ubuntu:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
|
||||
- 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.19.5
|
||||
go-version: 1.21
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.1.0
|
||||
node-version: 20.11.0
|
||||
|
||||
- name: apt-get update
|
||||
run: sudo apt-get update
|
||||
|
||||
- name: apt-get install libgtk-3-dev
|
||||
run: sudo apt-get install libgtk-3-dev
|
||||
- name: apt-get install -y libgtk-3-dev
|
||||
run: sudo apt-get install -y libgtk-3-dev
|
||||
|
||||
- name: apt-get install libwebkit2gtk-4.0-dev
|
||||
run: sudo apt-get install libwebkit2gtk-4.0-dev
|
||||
- name: apt-get install -y libwebkit2gtk-4.0-dev
|
||||
run: sudo apt-get install -y libwebkit2gtk-4.0-dev
|
||||
|
||||
- name: Build Linux server and app
|
||||
run: cd focalboard/; make server-linux-package linux-app
|
||||
|
@ -87,7 +66,7 @@ jobs:
|
|||
path: ${{ github.workspace }}/focalboard/linux/dist/focalboard-linux.tar.gz
|
||||
|
||||
macos:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
|
||||
|
@ -95,40 +74,19 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
- 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.19.5
|
||||
go-version: 1.21
|
||||
|
||||
- name: List Xcode versions
|
||||
run: ls -n /Applications/ | grep Xcode*
|
||||
|
@ -153,33 +111,9 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
- 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
|
||||
|
||||
|
@ -189,7 +123,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version: 1.21
|
||||
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@v1
|
||||
|
@ -215,71 +149,3 @@ jobs:
|
|||
with:
|
||||
name: focalboard-win.zip
|
||||
path: ${{ github.workspace }}/focalboard/win-wpf/dist/focalboard-win.zip
|
||||
|
||||
plugin:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
|
||||
- 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.19.5
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.1.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
|
||||
|
|
23
.github/workflows/lint-server.yml
vendored
23
.github/workflows/lint-server.yml
vendored
|
@ -13,7 +13,7 @@ env:
|
|||
|
||||
jobs:
|
||||
down-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -26,31 +26,16 @@ jobs:
|
|||
|
||||
golangci:
|
||||
name: plugin
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version: 1.21
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
- name: set up golangci-lint
|
||||
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.1
|
||||
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.59.0
|
||||
- name: lint
|
||||
run: |
|
||||
cd focalboard
|
||||
|
|
153
.github/workflows/prod-release.yml
vendored
153
.github/workflows/prod-release.yml
vendored
|
@ -9,62 +9,41 @@ env:
|
|||
jobs:
|
||||
|
||||
ubuntu:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
|
||||
- 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.19.5
|
||||
go-version: 1.21
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.1.0
|
||||
node-version: 20.11.0
|
||||
|
||||
- name: apt-get update
|
||||
run: sudo apt-get update
|
||||
|
||||
- name: apt-get install libgtk-3-dev
|
||||
run: sudo apt-get install libgtk-3-dev
|
||||
- name: apt-get install -y libgtk-3-dev
|
||||
run: sudo apt-get install -y libgtk-3-dev
|
||||
|
||||
- name: apt-get install libwebkit2gtk-4.0-dev
|
||||
run: sudo apt-get install libwebkit2gtk-4.0-dev
|
||||
- name: apt-get install -y libwebkit2gtk-4.0-dev
|
||||
run: sudo apt-get install -y libwebkit2gtk-4.0-dev
|
||||
|
||||
- name: Build Linux server and app
|
||||
run: cd focalboard; make server-linux-package linux-app
|
||||
|
@ -84,7 +63,7 @@ jobs:
|
|||
path: ${{ github.workspace }}/focalboard/linux/dist/focalboard-linux.tar.gz
|
||||
|
||||
macos:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
|
||||
|
@ -92,41 +71,20 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
|
||||
- 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.19.5
|
||||
go-version: 1.21
|
||||
|
||||
- name: List Xcode versions
|
||||
run: ls -n /Applications/ | grep Xcode*
|
||||
|
@ -151,34 +109,13 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
|
||||
- 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
|
||||
|
||||
|
@ -188,7 +125,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version: 1.21
|
||||
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@v1
|
||||
|
@ -214,72 +151,4 @@ jobs:
|
|||
with:
|
||||
name: focalboard-win.zip
|
||||
path: ${{ github.workspace }}/focalboard/win-wpf/dist/focalboard-win.zip
|
||||
|
||||
plugin-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: "focalboard"
|
||||
- id: "mattermostServer"
|
||||
uses: actions/checkout@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref: ${{ env.BRANCH_NAME }}
|
||||
- uses: actions/checkout@v3
|
||||
if: steps.mattermostServer.outcome == 'failure'
|
||||
with:
|
||||
repository: "mattermost/mattermost-server"
|
||||
fetch-depth: "20"
|
||||
path: "mattermost-server"
|
||||
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
|
||||
|
||||
- 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.19.5
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.1.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
|
||||
|
||||
|
|
4
.github/workflows/scorecards-analysis.yml
vendored
4
.github/workflows/scorecards-analysis.yml
vendored
|
@ -13,7 +13,7 @@ permissions: read-all
|
|||
jobs:
|
||||
analysis:
|
||||
name: Scorecards analysis
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
|
@ -27,7 +27,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@c8416b0b2bf627c349ca92fc8e3de51a64b005cf # v1.0.2
|
||||
uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v2.1.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -46,6 +46,7 @@ build/Release
|
|||
node_modules
|
||||
dist
|
||||
pack
|
||||
package
|
||||
bin
|
||||
debug
|
||||
__debug_bin
|
||||
|
@ -69,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": {
|
||||
|
|
|
@ -1,47 +1,20 @@
|
|||
# Code Contribution Guidelines
|
||||
# Disclaimer
|
||||
|
||||
Thank you for your interest in contributing! Please read the [Focalboard Contribution Guide](https://developers.mattermost.com/contribute/focalboard/) to learn the process for making code contributions, and [join our Focalboard community channel](https://community.mattermost.com/core/channels/focalboard) to get help from community members and the core team.
|
||||
> [!WARNING]
|
||||
> **Effective September 15th, 2023, Mattermost, Inc. staff are no longer reviewing or merging pull requests for either Focalboard or the Mattermost Boards plugin in this repository (`mattermost/focalboard`). We encourage the community to fork this repository for continued development and contributions.**
|
||||
>
|
||||
> The reason behind these changes is to focus Mattermost developer resources on improving the platform’s performance and core features to ensure Mattermost continues being resilient, stable, and best-in-breed for critical operations.
|
||||
>
|
||||
> ️💡 [Learn more](https://forum.mattermost.com/t/upcoming-product-changes-to-boards-and-various-plugins/16669)
|
||||
|
||||
When you submit a pull request, it goes through a code review process outlined [here](https://developers.mattermost.com/contribute/getting-started/code-review/).
|
||||
## Past maintainers
|
||||
|
||||
After a noteable bug fix or improvement is merged, submit a pull request to the [CHANGELOG](CHANGELOG.md) under the next release section.
|
||||
|
||||
## Bug Reports
|
||||
|
||||
Please file a [GitHub issue](https://github.com/mattermost/focalboard/issues) if anything isn't working the way you expect.
|
||||
|
||||
## Documentation
|
||||
|
||||
You can contribute to the [Mattermost Boards documentation](https://docs.mattermost.com/guides/boards.html). Read more about how the contribution process works in the repository's [README](https://github.com/mattermost/docs/blob/master/README.md). Visit the [Documentation Working Group channel](https://community.mattermost.com/core/channels/dwg-documentation-working-group) on our community server if you have any questions!
|
||||
|
||||
## Contributors
|
||||
|
||||
**Core Committers**: Maintains the Focalboard project and has merge access to the repositories. They are responsible for reviewing pull requests, cultivating the developer community, and guiding the technical vision of Focalboard. If you have a question or need some help, these are the people to ask.
|
||||
|
||||
- **<a name="scott.bishel">Scott Bishel</a>**
|
||||
- @scott.bishel on [community.mattermost.com](https://community.mattermost.com/core/messages/@scott.bishel) and [@sbishel](https://github.com/sbishel) on GitHub
|
||||
- **<a name="jesús.espino">Jesús Espino</a>**
|
||||
- @jesus.espino on [community.mattermost.com](https://community.mattermost.com/core/messages/@jesus.espino) and [@jespino](https://github.com/jespino) on GitHub
|
||||
- **<a name="doug.lauder">Doug Lauder</a>**
|
||||
- @doug.lauder on [community.mattermost.com](https://community.mattermost.com/core/messages/@doug.lauder) and [@wiggin77](https://github.com/wiggin77) on GitHub
|
||||
- **<a name="miguel.delacruz">Miguel de la Cruz</a>**
|
||||
- @miguel.delacruz on [community.mattermost.com](https://community.mattermost.com/core/messages/@miguel.delacruz) and [@mgdelacroix](https://github.com/mgdelacroix) on GitHub
|
||||
- **<a name="harshil.sharma">Harshil Sharma</a>**
|
||||
- @harshil.sharma on [community.mattermost.com](https://community.mattermost.com/core/messages/@harshil.sharma) and [@harshilsharma63](https://github.com/harshilsharma63) on GitHub
|
||||
- **<a name="chen.lim">Chen Lim</a>**
|
||||
- @chen-i.lim on [community.mattermost.com](https://community.mattermost.com/core/messages/@chen-i.lim) and [@chenilim](https://github.com/chenilim) on GitHub
|
||||
|
||||
**Quality Assurance**: Checks quality of code and verifies bug fixes.
|
||||
|
||||
- **<a name="ogi.marusic">Ogi Marušić</a>**
|
||||
- @ogi.marusic on [community.mattermost.com](https://community.mattermost.com/core/messages/@ogi.marusic) and [@ogi-m](https://github.com/ogi-m) on GitHub
|
||||
|
||||
**Community Organizers**: Responds with comments to bug reports, issues, and pull requests with tags, edits and mentions to core committers and contributors.
|
||||
|
||||
- **<a name="winson.wu">Winson Wu</a>**
|
||||
- @winson.wu on [community.mattermost.com](https://community.mattermost.com/core/messages/@winson.wu) and [@wuwinson](https://github.com/wuwinson) on GitHub
|
||||
|
||||
**Documentation**: Verifies documentation changes and updates documentation for new features.
|
||||
|
||||
- **<a name="justine.geffen">Justine Geffen</a>**
|
||||
- @justine.geffen on [community.mattermost.com](https://community.mattermost.com/core/messages/@justine.geffen) and [@justinegeffen ](https://github.com/justinegeffen) on GitHub
|
||||
- **Scott Bishel**: [@sbishel](https://github.com/sbishel)
|
||||
- **Jesús Espino**: [@jespino](https://github.com/jespino)
|
||||
- **Doug Lauder**: [@wiggin77](https://github.com/wiggin77)
|
||||
- **Miguel de la Cruz**: [@mgdelacroix](https://github.com/mgdelacroix)
|
||||
- **Harshil Sharma**: [@harshilsharma63](https://github.com/harshilsharma63)
|
||||
- **Chen Lim**: [@chenilim](https://github.com/chenilim)
|
||||
- **Ogi Marušić**: [@ogi-m](https://github.com/ogi-m)
|
||||
- **Winson Wu**: [@wuwinson](https://github.com/wuwinson)
|
||||
- **Justine Geffen**: [@justinegeffen](https://github.com/justinegeffen)
|
||||
|
|
74
Makefile
74
Makefile
|
@ -35,22 +35,17 @@ 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.
|
||||
|
||||
setup-go-work: export EXCLUDE_ENTERPRISE ?= true
|
||||
setup-go-work: ## Sets up a go.work file
|
||||
go run ./build/gowork/main.go
|
||||
|
||||
templates-archive: setup-go-work ## Build templates archive file
|
||||
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"
|
||||
|
||||
server: setup-go-work ## Build server for local environment.
|
||||
server: ## Build server for local environment.
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=dev")
|
||||
cd server; go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/focalboard-server ./main
|
||||
|
||||
server-mac: setup-go-work ## Build server for Mac.
|
||||
server-mac: ## Build server for Mac.
|
||||
mkdir -p bin/mac
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=mac")
|
||||
ifeq ($(FB_PROD),)
|
||||
|
@ -60,21 +55,21 @@ else
|
|||
cd server; env GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/mac/focalboard-server ./main
|
||||
endif
|
||||
|
||||
server-linux: setup-go-work ## Build server for Linux.
|
||||
server-linux: ## Build server for Linux.
|
||||
mkdir -p bin/linux
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=linux")
|
||||
cd server; env GOOS=linux GOARCH=$(arch) go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/linux/focalboard-server ./main
|
||||
|
||||
server-docker: setup-go-work ## Build server for Docker Architectures.
|
||||
server-docker: ## Build server for Docker Architectures.
|
||||
mkdir -p bin/docker
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=linux")
|
||||
cd server; env GOOS=$(os) GOARCH=$(arch) go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/docker/focalboard-server ./main
|
||||
|
||||
server-win: setup-go-work ## Build server for Windows.
|
||||
server-win: ## Build server for Windows.
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win")
|
||||
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/win/focalboard-server.exe ./main
|
||||
|
||||
server-dll: setup-go-work ## Build server as Windows DLL.
|
||||
server-dll: ## Build server as Windows DLL.
|
||||
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win")
|
||||
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -buildmode=c-shared -o ../bin/win-dll/focalboard-server.dll ./main
|
||||
|
||||
|
@ -84,7 +79,6 @@ server-linux-package: server-linux webapp
|
|||
cp bin/linux/focalboard-server package/${PACKAGE_FOLDER}/bin
|
||||
cp -R webapp/pack package/${PACKAGE_FOLDER}/pack
|
||||
cp server-config.json package/${PACKAGE_FOLDER}/config.json
|
||||
cp build/MIT-COMPILED-LICENSE.md package/${PACKAGE_FOLDER}
|
||||
cp NOTICE.txt package/${PACKAGE_FOLDER}
|
||||
cp webapp/NOTICE.txt package/${PACKAGE_FOLDER}/webapp-NOTICE.txt
|
||||
mkdir -p dist
|
||||
|
@ -97,7 +91,6 @@ server-linux-package-docker:
|
|||
cp bin/linux/focalboard-server package/${PACKAGE_FOLDER}/bin
|
||||
cp -R webapp/pack package/${PACKAGE_FOLDER}/pack
|
||||
cp server-config.json package/${PACKAGE_FOLDER}/config.json
|
||||
cp build/MIT-COMPILED-LICENSE.md package/${PACKAGE_FOLDER}
|
||||
cp NOTICE.txt package/${PACKAGE_FOLDER}
|
||||
cp webapp/NOTICE.txt package/${PACKAGE_FOLDER}/webapp-NOTICE.txt
|
||||
mkdir -p dist
|
||||
|
@ -108,13 +101,12 @@ generate: ## Install and run code generators.
|
|||
cd server; go install github.com/golang/mock/mockgen@v1.6.0
|
||||
cd server; go generate ./...
|
||||
|
||||
server-lint: setup-go-work ## Run linters on server code.
|
||||
server-lint: ## Run linters on server code.
|
||||
@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;
|
||||
cd server; golangci-lint run ./...
|
||||
cd mattermost-plugin; golangci-lint run ./...
|
||||
|
||||
modd-precheck:
|
||||
@if ! [ -x "$$(command -v modd)" ]; then \
|
||||
|
@ -135,28 +127,26 @@ server-test: server-test-sqlite server-test-mysql server-test-mariadb server-tes
|
|||
|
||||
server-test-sqlite: export FOCALBOARD_UNIT_TESTING=1
|
||||
|
||||
server-test-sqlite: setup-go-work ## Run server tests using sqlite
|
||||
server-test-sqlite: ## Run server tests using sqlite
|
||||
cd server; go test -tags '$(BUILD_TAGS)' -race -v -coverpkg=./... -coverprofile=server-sqlite-profile.coverage -count=1 -timeout=30m ./...
|
||||
cd server; go tool cover -func server-sqlite-profile.coverage
|
||||
|
||||
server-test-mini-sqlite: export FOCALBOARD_UNIT_TESTING=1
|
||||
|
||||
server-test-mini-sqlite: setup-go-work ## Run server tests using sqlite
|
||||
server-test-mini-sqlite: ## Run server tests using sqlite
|
||||
cd server/integrationtests; go test -tags '$(BUILD_TAGS)' $(RACE) -v -count=1 -timeout=30m ./...
|
||||
|
||||
server-test-mysql: export FOCALBOARD_UNIT_TESTING=1
|
||||
server-test-mysql: export FOCALBOARD_STORE_TEST_DB_TYPE=mysql
|
||||
server-test-mysql: export FOCALBOARD_STORE_TEST_DOCKER_PORT=44446
|
||||
|
||||
server-test-mysql: setup-go-work ## Run server tests using mysql
|
||||
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
|
||||
|
@ -164,55 +154,35 @@ 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
|
||||
server-test-postgres: export FOCALBOARD_STORE_TEST_DOCKER_PORT=44447
|
||||
|
||||
server-test-postgres: setup-go-work ## Run server tests using postgres
|
||||
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
|
||||
|
||||
.PHONY: build-product
|
||||
build-product: ## Builds the product as something the Mattermost server will pull files from when packaging a release
|
||||
cd mattermost-plugin; make build-product
|
||||
|
||||
.PHONY: watch-product
|
||||
watch-product: ## Run the product as something the Mattermost web app will watch for
|
||||
cd mattermost-plugin; make watch-product
|
||||
|
||||
mac-app: server-mac webapp ## Build Mac application.
|
||||
rm -rf mac/temp
|
||||
rm -rf mac/dist
|
||||
|
@ -228,7 +198,6 @@ mac-app: server-mac webapp ## Build Mac application.
|
|||
mkdir -p mac/dist
|
||||
cp -R mac/temp/focalboard.xcarchive/Products/Applications/Focalboard.app mac/dist/
|
||||
# xcodebuild -exportArchive -archivePath mac/temp/focalboard.xcarchive -exportPath mac/dist -exportOptionsPlist mac/export.plist
|
||||
cp build/MIT-COMPILED-LICENSE.md mac/dist
|
||||
cp NOTICE.txt mac/dist
|
||||
cp webapp/NOTICE.txt mac/dist/webapp-NOTICE.txt
|
||||
cd mac/dist; zip -r focalboard-mac.zip Focalboard.app MIT-COMPILED-LICENSE.md NOTICE.txt webapp-NOTICE.txt
|
||||
|
@ -244,7 +213,6 @@ linux-app: webapp ## Build Linux application.
|
|||
mkdir -p linux/dist
|
||||
mkdir -p linux/temp/focalboard-app
|
||||
cp app-config.json linux/temp/focalboard-app/config.json
|
||||
cp build/MIT-COMPILED-LICENSE.md linux/temp/focalboard-app/
|
||||
cp NOTICE.txt linux/temp/focalboard-app/
|
||||
cp webapp/NOTICE.txt linux/temp/focalboard-app/webapp-NOTICE.txt
|
||||
cp -R webapp/pack linux/temp/focalboard-app/pack
|
||||
|
|
108
README.md
108
README.md
|
@ -1,37 +1,28 @@
|
|||
> [!WARNING]
|
||||
> This repository is currently not maintained. If you're interested in becoming a maintainer please [let us know here](https://github.com/mattermost-community/focalboard/issues/5038).
|
||||
>
|
||||
> This repository only contains standalone Focalboard. If you're looking for the Mattermost plugin please see [mattermost/mattermost-plugin-boards](https://github.com/mattermost/mattermost-plugin-boards).
|
||||
>
|
||||
|
||||
# Focalboard
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
<a href="https://translate.mattermost.com/engage/focalboard/">
|
||||
<img src="https://translate.mattermost.com/widgets/focalboard/-/svg-badge.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
Like what you see? :eyes: Give us a GitHub Star! :star:
|
||||

|
||||
|
||||
[](https://www.focalboard.com)
|
||||
Focalboard is an open source, multilingual, self-hosted project management tool that's an alternative to Trello, Notion, and Asana.
|
||||
|
||||
[Focalboard](https://www.focalboard.com) is an open source, multilingual, self-hosted project management tool that's an alternative to Trello, Notion, and Asana.
|
||||
It helps define, organize, track and manage work across individuals and teams. Focalboard comes in two editions:
|
||||
|
||||
It helps define, organize, track and manage work across individuals and teams. Focalboard comes in two main editions:
|
||||
* **[Personal Desktop](https://www.focalboard.com/docs/personal-edition/desktop/)**: A standalone, single-user [macOS](https://apps.apple.com/app/apple-store/id1556908618?pt=2114704&ct=website&mt=8), [Windows](https://www.microsoft.com/store/apps/9NLN2T0SX9VF?cid=website), or [Linux](https://www.focalboard.com/download/personal-edition/desktop/#linux-desktop) desktop app for your own todos and personal projects.
|
||||
|
||||
* **[Mattermost Boards](https://www.focalboard.com/download/mattermost/)**: A self-hosted or **[free cloud server](https://mattermost.com/sign-up/?utm_source=github&utm_campaign=focalboard)** for your team to plan and collaborate.
|
||||
|
||||
* **[Personal Desktop](https://www.focalboard.com/download/personal-edition/desktop/)**: A standalone, single-user [macOS](https://apps.apple.com/app/apple-store/id1556908618?pt=2114704&ct=website&mt=8), [Windows](https://www.microsoft.com/store/apps/9NLN2T0SX9VF?cid=website), or [Linux](https://www.focalboard.com/download/personal-edition/desktop/#linux-desktop) desktop app for your own todos and personal projects.
|
||||
|
||||
Focalboard can also be installed as a standalone **[Personal Server](https://www.focalboard.com/download/personal-edition/ubuntu/)** for development and personal use.
|
||||
* **[Personal Server](https://www.focalboard.com/download/personal-edition/ubuntu/)**: A standalone, multi-user server for development and personal use.
|
||||
|
||||
## Try Focalboard
|
||||
|
||||
### Mattermost Boards - [now available as a free cloud server](https://mattermost.com/sign-up/?utm_source=github&utm_campaign=focalboard)
|
||||
|
||||
**Mattermost Boards** 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=github&utm_campaign=focalboard) or [Cloud server](https://mattermost.com/sign-up/?utm_source=github&utm_campaign=focalboard). After logging into Mattermost, select the menu in the top left corner and select **Boards**.
|
||||
|
||||
***Mattermost Boards** is installed and enabled by default in Mattermost v6.0 and later.*
|
||||
|
||||
See the [plugin setup guide](https://www.focalboard.com/download/mattermost/) for more details.
|
||||
|
||||
### Personal Desktop (Windows, Mac or Linux Desktop)
|
||||
|
||||
* **Windows**: Download from the [Windows App Store](https://www.microsoft.com/store/productId/9NLN2T0SX9VF) or download `focalboard-win.zip` from the [latest release](https://github.com/mattermost/focalboard/releases), unpack, and run `Focalboard.exe`.
|
||||
|
@ -44,17 +35,11 @@ See the [plugin setup guide](https://www.focalboard.com/download/mattermost/) fo
|
|||
|
||||
### API Docs
|
||||
|
||||
Boards API docs can be found over at https://htmlpreview.github.io/?https://github.com/mattermost/focalboard/blob/main/server/swagger/docs/html/index.html
|
||||
|
||||
## Contribute to Focalboard
|
||||
|
||||
Contribute code, bug reports, and ideas to the future of the Focalboard project. We welcome your input! Please see [CONTRIBUTING](CONTRIBUTING.md) for details on how to get involved.
|
||||
Boards API docs can be found over at <https://htmlpreview.github.io/?https://github.com/mattermost/focalboard/blob/main/server/swagger/docs/html/index.html>
|
||||
|
||||
### Getting started
|
||||
|
||||
Our [developer guide](https://developers.mattermost.com/contribute/focalboard/personal-server-setup-guide) has detailed instructions on how to set up your development environment for the **Personal Server**. It also provides more information about contributing to our open source community.
|
||||
|
||||
Clone [mattermost-server](https://github.com/mattermost/mattermost-server) into sibling directory.
|
||||
Our [developer guide](https://developers.mattermost.com/contribute/focalboard/personal-server-setup-guide) has detailed instructions on how to set up your development environment for the **Personal Server**. You can also join the [~Focalboard community channel](https://community.mattermost.com/core/channels/focalboard) to connect with other developers.
|
||||
|
||||
Create an `.env` file in the focalboard directory that contains:
|
||||
|
||||
|
@ -84,38 +69,38 @@ Once the server is running, you can rebuild just the web app via `make webapp` i
|
|||
You can build standalone apps that package the server to run locally against SQLite:
|
||||
|
||||
* **Windows**:
|
||||
* *Requires Windows 10, [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) 10.0.19041.0, and .NET 4.8 developer pack*
|
||||
* Open a `git-bash` prompt.
|
||||
* Run `make prebuild`
|
||||
* The above prebuild step needs to be run only when you make changes to or want to install your npm dependencies, etc.
|
||||
* Once the prebuild is completed, you can keep repeating the below steps to build the app & see the changes.
|
||||
* Run `make win-wpf-app`
|
||||
* Run `cd win-wpf/msix && focalboard.exe`
|
||||
* *Requires Windows 10, [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) 10.0.19041.0, and .NET 4.8 developer pack*
|
||||
* Open a `git-bash` prompt.
|
||||
* Run `make prebuild`
|
||||
* The above prebuild step needs to be run only when you make changes to or want to install your npm dependencies, etc.
|
||||
* Once the prebuild is completed, you can keep repeating the below steps to build the app & see the changes.
|
||||
* Run `make win-wpf-app`
|
||||
* Run `cd win-wpf/msix && focalboard.exe`
|
||||
* **Mac**:
|
||||
* *Requires macOS 11.3+ and Xcode 13.2.1+*
|
||||
* Run `make prebuild`
|
||||
* The above prebuild step needs to be run only when you make changes to or want to install your npm dependencies, etc.
|
||||
* Once the prebuild is completed, you can keep repeating the below steps to build the app & see the changes.
|
||||
* Run `make mac-app`
|
||||
* Run `open mac/dist/Focalboard.app`
|
||||
* *Requires macOS 11.3+ and Xcode 13.2.1+*
|
||||
* Run `make prebuild`
|
||||
* The above prebuild step needs to be run only when you make changes to or want to install your npm dependencies, etc.
|
||||
* Once the prebuild is completed, you can keep repeating the below steps to build the app & see the changes.
|
||||
* Run `make mac-app`
|
||||
* Run `open mac/dist/Focalboard.app`
|
||||
* **Linux**:
|
||||
* *Tested on Ubuntu 18.04*
|
||||
* Install `webgtk` dependencies
|
||||
* Run `sudo apt-get install libgtk-3-dev`
|
||||
* Run `sudo apt-get install libwebkit2gtk-4.0-dev`
|
||||
* Run `make prebuild`
|
||||
* The above prebuild step needs to be run only when you make changes to or want to install your npm dependencies, etc.
|
||||
* Once the prebuild is completed, you can keep repeating the below steps to build the app & see the changes.
|
||||
* Run `make linux-app`
|
||||
* Uncompress `linux/dist/focalboard-linux.tar.gz` to a directory of your choice
|
||||
* Run `focalboard-app` from the directory you have chosen
|
||||
* *Tested on Ubuntu 18.04*
|
||||
* Install `webgtk` dependencies
|
||||
* Run `sudo apt-get install libgtk-3-dev`
|
||||
* Run `sudo apt-get install libwebkit2gtk-4.0-dev`
|
||||
* Run `make prebuild`
|
||||
* The above prebuild step needs to be run only when you make changes to or want to install your npm dependencies, etc.
|
||||
* Once the prebuild is completed, you can keep repeating the below steps to build the app & see the changes.
|
||||
* Run `make linux-app`
|
||||
* Uncompress `linux/dist/focalboard-linux.tar.gz` to a directory of your choice
|
||||
* Run `focalboard-app` from the directory you have chosen
|
||||
* **Docker**:
|
||||
* To run it locally from offical image:
|
||||
* `docker run -it -p 80:8000 mattermost/focalboard`
|
||||
* To build it for your current architecture:
|
||||
* `docker build -f docker/Dockerfile .`
|
||||
* To build it for a custom architecture (experimental):
|
||||
* `docker build -f docker/Dockerfile --platform linux/arm64 .`
|
||||
* To run it locally from offical image:
|
||||
* `docker run -it -p 80:8000 mattermost/focalboard`
|
||||
* To build it for your current architecture:
|
||||
* `docker build -f docker/Dockerfile .`
|
||||
* To build it for a custom architecture (experimental):
|
||||
* `docker build -f docker/Dockerfile --platform linux/arm64 .`
|
||||
|
||||
Cross-compilation currently isn't fully supported, so please build on the appropriate platform. Refer to the GitHub Actions workflows (`build-mac.yml`, `build-win.yml`, `build-ubuntu.yml`) for the detailed list of steps on each platform.
|
||||
|
||||
|
@ -128,15 +113,8 @@ Before checking in commits, run `make ci`, which is similar to the `.gitlab-ci.y
|
|||
* **Web app unit tests**: `cd webapp; npm run test`
|
||||
* **Web app UI tests**: `cd webapp; npm run cypress:ci`
|
||||
|
||||
### Translating
|
||||
|
||||
Help translate Focalboard! The app is already translated into several languages. We welcome corrections and new language translations! You can add new languages or improve existing translations at [Weblate](https://translate.mattermost.com/engage/focalboard/).
|
||||
|
||||
### Staying informed
|
||||
|
||||
Are you interested in influencing the future of the Focalboard open source project? Here's how you can get involved:
|
||||
|
||||
* **Changes**: See the [CHANGELOG](CHANGELOG.md) for the latest updates
|
||||
* **GitHub Discussions**: Join the [Developer Discussion](https://github.com/mattermost/focalboard/discussions) board
|
||||
* **Bug Reports**: [File a bug report](https://github.com/mattermost/focalboard/issues/new?assignees=&labels=bug&template=bug_report.md&title=)
|
||||
* **Chat**: Join the [Focalboard community channel](https://community.mattermost.com/core/channels/focalboard)
|
||||
* **Chat**: Join the [~Focalboard community channel](https://community.mattermost.com/core/channels/focalboard)
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
Mattermost MIT Compiled License
|
||||
|
||||
**Note: This license does not cover source code; for information on source code licensing see LICENSE.txt in the source code from which this project was compiled.**
|
||||
|
||||
Copyright (c) 2016-present Mattermost, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software;
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,101 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
filename = "go.work"
|
||||
)
|
||||
|
||||
func main() {
|
||||
force := false
|
||||
if len(os.Args) == 2 && strings.ToLower(os.Args[1]) == "-f" {
|
||||
force = true
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filename); err == nil && !force {
|
||||
// go.work already exists and force flag not specified
|
||||
fmt.Fprintln(os.Stdout, "go.work already exists and -f (force) not specified; nothing to do.")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error creating %s: %s", filename, err.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
isCI := isCI()
|
||||
content := makeGoWork(isCI)
|
||||
|
||||
_, err = f.WriteString(content)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error writing %s: %s", filename, err.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stdout, "go.work written successfully.")
|
||||
fmt.Fprintln(os.Stdout, content)
|
||||
}
|
||||
|
||||
func makeGoWork(ci bool) string {
|
||||
repos := map[string]string{
|
||||
"../mattermost-server": "EXCLUDE_SERVER",
|
||||
"../enterprise": "EXCLUDE_ENTERPRISE",
|
||||
"./mattermost-plugin": "EXCLUDE_PLUGIN",
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("go 1.19\n\n")
|
||||
b.WriteString("use ./server\n")
|
||||
|
||||
for repo, envVarName := range repos {
|
||||
if !isEnvVarTrue(envVarName, true) {
|
||||
b.WriteString(fmt.Sprintf("use %s\n", repo))
|
||||
}
|
||||
}
|
||||
|
||||
if ci {
|
||||
b.WriteString("use ./linux\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func isCI() bool {
|
||||
vars := map[string]bool{
|
||||
// var name: must_be_true (false means being defined is enough)
|
||||
"CIRCLECI": true,
|
||||
"GITHUB_ACTIONS": true,
|
||||
"GITLAB_CI": false,
|
||||
"TRAVIS": true,
|
||||
}
|
||||
|
||||
for name, mustBeTrue := range vars {
|
||||
if isEnvVarTrue(name, mustBeTrue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEnvVarTrue(name string, mustBeTrue bool) bool {
|
||||
val, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !mustBeTrue {
|
||||
return true
|
||||
}
|
||||
|
||||
switch strings.ToLower(val) {
|
||||
case "t", "1", "true", "y", "yes":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
"port": 8000,
|
||||
"dbtype": "sqlite3",
|
||||
"dbconfig": "./focalboard.db?_busy_timeout=5000",
|
||||
"dbpingattempts": 5,
|
||||
"dbtableprefix": "",
|
||||
"postgres_dbconfig": "dbname=focalboard sslmode=disable",
|
||||
"useSSL": false,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
version: '2.4'
|
||||
services:
|
||||
mysql:
|
||||
image: "mysql/mysql-server:5.7.12"
|
||||
image: "mysql/mysql-server:8.0.32"
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_HOST: "%"
|
||||
|
|
8
docs/README.md
Normal file
8
docs/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Disclaimer
|
||||
|
||||
> [!WARNING]
|
||||
> **Effective September 15th, 2023, Mattermost, Inc. staff are no longer reviewing or merging pull requests for either Focalboard or the Mattermost Boards plugin in this repository (`mattermost/focalboard`). We encourage the community to fork this repository for continued development and contributions.**
|
||||
>
|
||||
> The reason behind these changes is to focus Mattermost developer resources on improving the platform’s performance and core features to ensure Mattermost continues being resilient, stable, and best-in-breed for critical operations.
|
||||
>
|
||||
> ️💡 [Learn more](https://forum.mattermost.com/t/upcoming-product-changes-to-boards-and-various-plugins/16669)
|
55
docs/board-metrics.md
Normal file
55
docs/board-metrics.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Board metrics
|
||||
|
||||
When you view a board in table or board view, you can use calculations to answer basic metric questions without needing to create complex reports. Hover over the bottom of a column to display the **Calculate** feature, then select the arrow to open the menu options.
|
||||
|
||||
You can use calculations to quickly see:
|
||||
|
||||
- How many story points are planned for a release.
|
||||
- How many tasks have been assigned or not assigned.
|
||||
- How long has the oldest bug been sitting in the backlog.
|
||||
- The count of cards where particular properties are empty (useful to make sure important info isn’t missing).
|
||||
- The sum of estimated developer days for features (to make sure your team isn’t overloaded).
|
||||
- The range of estimated dates (to make sure your milestones all line up).
|
||||
|
||||
The calculation options are:
|
||||
|
||||
* **Count**: Counts the total number of rows in table view or total number of cards in a column in Board view. Applies to any property type.
|
||||
* **Count Empty**: Applies to any property type.
|
||||
|
||||
- Table View: Counts the total number of empty rows per column selected.
|
||||
- Board View: Counts the total number of empty values per property specified within the same column.
|
||||
|
||||
* **Count Not Empty**: Applies to any property type.
|
||||
|
||||
- Table View: Counts the total number of rows with non-empty cells per column selected.
|
||||
- Board View: Counts the total number of non-empty values per property specified within the same column.
|
||||
|
||||
* **Percent Empty**: Applies to any property type.
|
||||
|
||||
- Table View: Percentage of empty rows per column selected.
|
||||
- Board View: Percentage of empty values per property specified within the same column.
|
||||
|
||||
* **Percent Not Empty**: Applies to any property type.
|
||||
|
||||
- Table View: Percentage of rows with non-empty cells per column selected.
|
||||
- Board View: Percentage of non-empty values per property specified within the same column.
|
||||
|
||||
* **Count Value**: Applies to any property type.
|
||||
|
||||
- Table View: Counts the total number of values within the column (helpful for multi-select properties).
|
||||
- Board View: Counts the total number of values per property specified within the same column.
|
||||
|
||||
* **Count Unique Values**: Applies to any property type.
|
||||
|
||||
- Table View: Counts the total number of rows with unique values within the column, omitting any duplicates from the count.
|
||||
- Board View: Counts the total number of unique values per property specified within the same column, omitting any duplicates from the count.
|
||||
|
||||
* **Sum**: The sum of any specified number property within the same column.
|
||||
* **Average**: The average of any specified number property within the same column.
|
||||
* **Median**: The median of any specified number property within the same column.
|
||||
* **Min**: The lowest number of any specified number property within the same column.
|
||||
* **Max**: The highest number of any specified number property within the same column.
|
||||
* **Range**: Displays the lowest and highest number. Requires a number property.
|
||||
* **Earliest Date**: Displays the oldest date. Requires any custom date property or the included "Created time" or "Last updated time".
|
||||
* **Latest Date**: Displays the most recent date. Requires any custom date property or the included "Created time" or "Last updated time".
|
||||
* **Date Range**: The difference between the most recent date and oldest date within the same column. In Table View, it's labeled simply as "Range" for any date property/column. Requires any custom date property or the included "Created time" or "Last updated time".
|
17
docs/create-new-board.md
Normal file
17
docs/create-new-board.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Create a new board
|
||||
|
||||
If none of the standard templates suit your requirements, you can create a blank board. Select the plus icon at the top of the sidebar, then select **Create New Board** to open the template picker and select the **Create empty board** option.
|
||||
|
||||
## Manage board details
|
||||
|
||||
If you've created a board, you can edit that board any time. To name or rename a board, select the title area to edit it.
|
||||
|
||||
To display board description, hover above the board’s title and select **Show description** to activate the show/hide toggle. Once the description field is displayed, select **Add a description** right below the board title to add or edit the description.
|
||||
|
||||
Boards and cards are created with random icons by default. To change or remove icons, select the icon then choose the appropriate action.
|
||||
|
||||
All changes you make to boards and cards are saved immediately.
|
||||
|
||||
## Create a template from a board
|
||||
|
||||
To turn an existing board into a template, hover over the board title in the sidebar. Select the options menu, then select **New template from board**.
|
|
@ -1,6 +1,8 @@
|
|||
# Developer Tips and Tricks
|
||||
|
||||
These tips and tricks apply to developing the standalone Personal Server of Focalboard. For most features, this is the easiest way to get started working against code that ships across editions. For working with Mattermost Boards, refer to the [Mattermost Boards developer's guide](mattermost-boards-dev-guide.md).
|
||||
These tips and tricks apply to developing the standalone Personal Server of Focalboard. For most features, this is the easiest way to get started working against code that ships across editions.
|
||||
|
||||
For working with the Focalboard plugin, refer to the [Focalboard Plugin Developer's Guide](focalboard-dev-guide.md).
|
||||
|
||||
## Installation prerequisites
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Mattermost Boards Developer's Guide
|
||||
# Focalboard Plugin Developer's Guide
|
||||
|
||||
[Mattermost Boards](https://mattermost.com/boards/) is the Focalboard (aka Boards) plugin running in Mattermost. It is pre-packaed, and runs out of the box with Mattermost 6.0 and later.
|
||||
**Important**: Effective September 15th, 2023, Mattermost Boards transitions to being fully community supported as the Focalboard Plugin. Mattermost will no longer be maintaining this plugin - this includes bug fixes and feature additions. Instead, the plugin is open-sourced and made available indefinitely for community contributions in GitHub.
|
||||
|
||||
To build your own version of it:
|
||||
1. Build [mattermost-plugin](https://github.com/mattermost/focalboard/tree/main/mattermost-plugin) in the [Focalboard repo](https://github.com/mattermost/focalboard)
|
||||
|
@ -8,7 +8,7 @@ To build your own version of it:
|
|||
|
||||
Here are the steps in more detail:
|
||||
|
||||
### Building the Boards plugin
|
||||
### Building the Focalboard plugin
|
||||
|
||||
Fork the [Focalboard repo](https://github.com/mattermost/focalboard), clone it locally, and follow the steps in the readme to set up your dev environment.
|
||||
|
||||
|
@ -55,7 +55,7 @@ First, build and run Mattermost locally:
|
|||
5. Add an ENV var `MM_SERVICESETTINGS_SITEURL` with the same site URL used in the config
|
||||
6. Run `make run-server` in Mattermost
|
||||
|
||||
Now, to build and deploy the Boards plugin:
|
||||
Now, to build and deploy the plugin:
|
||||
1. Clone / fork [mattermost/focalboard](https://github.com/mattermost/focalboard)
|
||||
2. Install the dependencies (see above)
|
||||
3. Run:
|
23
docs/focalboard-plugin-end-user-guide.md
Normal file
23
docs/focalboard-plugin-end-user-guide.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Focalboard Plugin End User's Guide
|
||||
|
||||
The Focalboard plugin is a deliverable and task management solution to help teams achieve project milestones using a familiar kanban board structure.
|
||||
|
||||
This user guide is for anyone looking to use Focalboard to:
|
||||
- Align work across the organization and drive milestone achievement such as project planning, execution, and task management.
|
||||
- Keep everyone in your team and organization in the loop to stay on schedule with clearly defined tasks, owners, checklists, and deadlines.
|
||||
- Increase transparency and keep all resources available including documents, images, and important hyperlinks.
|
||||
- Track tasks for sprints and features in roadmap planning.
|
||||
|
||||
The following end user documentation is available:
|
||||
|
||||
- [Get started with board templates](get-started-with-board-templates.md)
|
||||
- [Create a new board](create-new-board.md)
|
||||
- [Link boards to Mattermost channels](link-boards-to-mattermost-channels.md)
|
||||
- [Manage boards](manage-boards.md)
|
||||
- [Work with cards](work-with-cards.md)
|
||||
- [Work with board views](work-with-board-views.md)
|
||||
- [Group, filter, and sort boards](group-filter-sort-boards.md)
|
||||
- [Board metrics](board-metrics.md)
|
||||
- [Share and collaborate](share-collaborate.md)
|
||||
- [Import, export, and back up boards data](import-export-backup-data.md)
|
||||
- [Manage plugin preferences](manage-plugin-preferences.md)
|
47
docs/get-started-with-board-templates.md
Normal file
47
docs/get-started-with-board-templates.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Get started with board templates
|
||||
|
||||
## What's a board?
|
||||
|
||||
A board is a collection of cards to help you manage your projects, organize tasks, and collaborate with your team all in one place. A board contains cards, which typically track tasks or topics, and views, which define how to display the cards, or a subset of them.
|
||||
|
||||
Boards can be displayed and filtered in different views such as kanban, table, calendar, and gallery views to help you visualize work items in the format that makes most sense to you.
|
||||
|
||||
## Start from a board template
|
||||
|
||||
To create a new board, we strongly recommend starting from a standard board template.
|
||||
|
||||
Board templates provide you with a predefined structure so that you can get started quickly. Each template has a different function, but can be customized to suit your use case. When you create a new board from the template picker, select each template’s name to preview it and make sure it suits your requirements. Alternatively, you can create your own board templates.
|
||||
|
||||
Select the plus icon at the top of the sidebar, then select **Create New Board** to open the template picker and select a template or a blank board.
|
||||
|
||||
Standard board templates include:
|
||||
|
||||
- **Content Calendar**: Plan and organize your content creation and publication schedule.
|
||||
- **Company Goals & OKRs**: Plan your company goals and objectives more efficiently.
|
||||
- **Competitive Analysis**: Track and stay ahead of the competition.
|
||||
- **Meeting Agenda**: Use this template for recurring meetings. Queue up items, organize discussions, and plan what to revisit later.
|
||||
- **Personal Goals**: Categorize and plan your personal goals.
|
||||
- **Personal Tasks**: Organize your life and track your personal tasks.
|
||||
- **Project Tasks**: Stay on top of your project tasks, track progress, and set priorities.
|
||||
- **Roadmap**: Plan your roadmap and manage your releases more efficiently.
|
||||
- **Sprint Planner**: Plan your sprints and releases more efficiently.
|
||||
- **Team Retrospective**: Identify what worked well and what can be improved for the future.
|
||||
- **User Research Sessions**: Manage and keep track of all your user research sessions.
|
||||
- **Welcome to Boards!**: Onboarding template with guided tour points to help you quickly ramp up on Focalboard.
|
||||
|
||||
### Edit board templates
|
||||
|
||||
To open the template editor for a specific template, go to the template picker then hover over the custom template and select the pencil icon. Any changes made on the template editor will be automatically saved and visible to team members who have access to the template. If you don't see the pencil icon when hovering over the template, then you don't have the appropriate permissions to edit the template.
|
||||
|
||||
**Notes**:
|
||||
- From v7.2 of the Focalboard plugin, only admins and editors of a custom template can edit the template.
|
||||
- Prior to v7.2 of the plugin, any member of the channel workspace can edit a custom template in the channel. To limit access to the template, create or export the template to a private channel.
|
||||
- Custom templates are fully editable, but standard templates cannot be edited or deleted.
|
||||
|
||||
## Create a template
|
||||
|
||||
To create a new board template select the plus icon at the top of the sidebar to open the template picker, select **Create New Board** and then select **+ New template**.
|
||||
|
||||
## Turn a board into a template
|
||||
|
||||
To turn an existing board into a template, hover over the board title in the sidebar. Select the options menu, then select **New template from board**.
|
79
docs/group-filter-sort-boards.md
Normal file
79
docs/group-filter-sort-boards.md
Normal file
|
@ -0,0 +1,79 @@
|
|||
# Group, filter, and sort boards
|
||||
|
||||
Your boards can be grouped, filtered, and sorted into different views using a range of properties. This gives you a powerful way to track work from various perspectives. For example, easily find tasks assigned to you or a team member using the person or multi-person filters, and keep track of upcoming tasks with date filters.
|
||||
|
||||
## Group cards
|
||||
|
||||
You can group cards on your board if they utilize the **Select** or **Person** property.
|
||||
|
||||
Card grouping is only available in board and table views and you must have at least one **Select** or **Person** property on your board for grouping to work.
|
||||
|
||||
### Apply a group
|
||||
|
||||
To apply a group, select the **Group by** option at the top of the board, then select any available **Select** or **Person** property to group your cards by.
|
||||
|
||||
- In the boards view, cards are automatically grouped into columns by the values from the specified property.
|
||||
- In the table view, grouped cards will appear in individual sections based on the values from the specified property. Select the arrow to the left of the group name to expand or collapse cards in the group.
|
||||
|
||||
### Hide and unhide groups
|
||||
|
||||
- To hide a group, select the options menu **(...)** to the right of any group name, then select **Hide**. Additionally, in table view only, you can hide empty groups by selecting the **Group by** option at the top of the board, then selecting **Hide empty groups**.
|
||||
- To unhide a group, go to the hidden column section towards the right of a board view, select the group you want to unhide, then select **Show**. On table view, select the **Group by** option at the top of the board, then select **Show hidden groups**.
|
||||
|
||||
### Ungroup cards
|
||||
|
||||
To ungroup cards on table view, select the **Group by** option at the top of the board, then select **Ungroup** from the top of the menu. This will return your table to its default state. Cards can be ungrouped in table view only. Ungrouping is not possible on board view since groups are used to determine what to display.
|
||||
|
||||
## Filter cards
|
||||
|
||||
You can filter cards on your board if they utilize any of the following property types:
|
||||
|
||||
- Select
|
||||
- Text
|
||||
- Email
|
||||
- Phone
|
||||
- URL
|
||||
- Date
|
||||
- Person
|
||||
- Multi-person
|
||||
- Created time
|
||||
- Created by
|
||||
- Last updated time
|
||||
- Last updated by
|
||||
|
||||
You must have at least one of the property types listed above on your board for filtering to work.
|
||||
|
||||
To use filters, you must have the above property types already added to your board. Go to **Filter > Add filter**, and select the property you wish to filter by. You can use the modifiers to get even more granular results.
|
||||
|
||||
### Add filters
|
||||
|
||||
To add a filter, select the **Filter** option at the top of the board, then select **+ Add filter**. To change the property to filter by, select the name of the first property, then select another property (if available) from the menu.
|
||||
|
||||
**Specify the filtering criteria**
|
||||
|
||||
- **Includes**: Display cards with any of the specified values.
|
||||
- **Doesn’t include**: Display all cards without any of the specified values.
|
||||
- **Is empty**: Display cards with no values assigned to a property.
|
||||
- **Is not empty**: Display cards with any value assigned to a property.
|
||||
|
||||
To add another filtering layer, repeat the steps above with another property to refine your filtering results. Adding another layer will display cards that only match the criteria from the first layer and the second layer.
|
||||
|
||||
### Delete filters
|
||||
|
||||
To delete a filter, select the **Filter** option at the top of the board, then select **Delete** to the right of each filtering layer. Delete all filtering layers to completely remove filters from the board.
|
||||
|
||||
## Sort cards
|
||||
|
||||
Sort cards by the card name or by any property available on the card.
|
||||
|
||||
Sorting is only available in boards, table, and gallery views.
|
||||
|
||||
### Apply sorting
|
||||
|
||||
To apply a sort, select the **Sort** option at the top of the board, then select an option from the menu. The cards will be sorted in ascending order by default based on the selected option and the **Sort** menu will display an upward pointing arrow next to the selected option.
|
||||
|
||||
To change the sort order to descending order, select the same option again from the **Sort** menu. The cards will now be sorted in descending order and the menu will display a downward pointing arrow next to the selected option.
|
||||
|
||||
### Clear sorting
|
||||
|
||||
To clear a sort, select the **Sort** option at the top of the board, then select the **Manual** option from the top of the menu.
|
147
docs/import-export-backup-data.md
Normal file
147
docs/import-export-backup-data.md
Normal file
|
@ -0,0 +1,147 @@
|
|||
# Import, export, and back up data
|
||||
|
||||
## Import data into Focalboard
|
||||
|
||||
You can import data from other tools to use with Focalboard.
|
||||
|
||||
### Import from Asana
|
||||
|
||||
This node app converts an Asana JSON archive into a ``.boardarchive`` file. The script imports all cards from a single board, including their section (column) membership, names, and notes.
|
||||
|
||||
1. Log into your Asana account.
|
||||
2. Select the drop-down menu next to the Asana board's name. Then select **Export/Print > JSON**. This will create an archive file.
|
||||
3. Save the file locally, e.g. to ``asana.json``.
|
||||
4. Open a terminal window on your local machine and clone the focalboard repository to a local directory, e.g. to ``focalboard``: ``git clone https://github.com/mattermost/focalboard focalboard``
|
||||
5. Navigate to ``focalboard/webapp``.
|
||||
6. Run ``npm install``.
|
||||
7. Change directory to ``focalboard/import/asana``.
|
||||
8. Run ``npm install``.
|
||||
9. From within the same folder, run ``npx ts-node importAsana.ts -i <asana.json> -o archive.boardarchive``. This generates the following data:
|
||||
|
||||
```
|
||||
My-MacbookPro:asana macbook$ npx ts-node importAsana.ts -i ~/Downloads/asana.json -o archive.boardarchive
|
||||
Board: 1:1 Meeting Agenda Test
|
||||
Card: [READ ME] Instructions for using this project
|
||||
Card: [EXAMPLE TASK] Feedback on design team presentation
|
||||
Card: [EXAMPLE TASK] Finalize monthly staffing plan
|
||||
Card: [EXAMPLE TASK] Review Q2 launch video outline
|
||||
Card: [EXAMPLE TASK] Mentor a peer
|
||||
|
||||
Found 5 card(s).
|
||||
Exported to archive.boardarchive
|
||||
```
|
||||
|
||||
10. In Focalboard, open the board you want to use for the export.
|
||||
11. Select **Settings > Import archive** and select ``archive.boardarchive``.
|
||||
12. Select **Upload**.
|
||||
13. Return to your board and confirm that your Asana data is now displaying.
|
||||
|
||||
If you don't see your Asana data, an error should be displayed. You can also check log files for errors.
|
||||
|
||||
### Import from Notion
|
||||
|
||||
This node app converts a Notion CSV and markdown export into a ``.boardarchive`` file. The script imports all cards from a single board, including their properties and markdown content.
|
||||
|
||||
**Note**: The Notion export format does not preserve property types, so the script currently imports all card properties as a Select type. You can change the type after importing into Focalboard.
|
||||
|
||||
1. From a Notion Board, open the **...** menu at the top right corner of the board.
|
||||
2. Select `Export` and pick `Markdown & CSV` as the export format.
|
||||
3. Save the generated file locally, and unzip the folder.
|
||||
4. Open a terminal window on your local machine and clone the focalboard repository to a local directory, e.g. to ``focalboard``: ``git clone https://github.com/mattermost/focalboard focalboard``
|
||||
5. Navigate to ``focalboard/webapp``.
|
||||
6. Run ``npm install``.
|
||||
7. Change directory to ``focalboard/import/notion``.
|
||||
8. Run ``npm install``.
|
||||
9. From within the same folder, run ``npx ts-node importNotion.ts -i <path to the notion-export folder> -o archive.boardarchive``.
|
||||
10. In Focalboard, open the board you want to use for the export.
|
||||
11. Select **Settings > Import archive** and select ``archive.boardarchive``.
|
||||
12. Select **Upload**.
|
||||
13. Return to your board and confirm that your Notion data is now displaying.
|
||||
|
||||
### Import from Jira
|
||||
|
||||
This node app converts a Jira ``.XML`` export into a ``.boardarchive`` file. The script imports each item as a card into a single board. Users are imported as Select properties, with the name of the user.
|
||||
|
||||
**Notes**:
|
||||
- Jira ``.XML`` export is limited to 1000 issues at a time.
|
||||
- The following aren't currently imported: custom properties, comments, and embedded files.
|
||||
|
||||
1. Open Jira advanced search, and search for all the items to export.
|
||||
2. Select **Export > Export XML**.
|
||||
3. Save the generated file locally, e.g. to ``jira_export.xml``.
|
||||
4. Open a terminal window on your local machine and clone the focalboard repository to a local directory, e.g. to ``focalboard``: ``git clone https://github.com/mattermost/focalboard focalboard``
|
||||
5. Navigate to ``focalboard/webapp``.
|
||||
6. Run ``npm install``.
|
||||
7. Change directory to ``focalboard/import/jira`.
|
||||
8. Run ``npm install``.
|
||||
9. From within the same folder, run ``npx ts-node importJira.ts -i <path-to-jira.xml> -o archive.boardarchive``.
|
||||
10. In Focalboard, open the board you want to use for the export.
|
||||
11. Select **Settings > Import archive** and select ``archive.boardarchive``.
|
||||
12. Select **Upload**.
|
||||
13. Return to your board and confirm that your Jira data is now displaying.
|
||||
|
||||
### Import from Trello
|
||||
|
||||
This node app converts a Trello ``.json`` archive into a ``.boardarchive`` file. The script imports all cards from a single board, including their list (column) membership, names, and descriptions.
|
||||
|
||||
1. From the Trello Board Menu, select **...Show Menu**.
|
||||
2. Select **More > Print and Export > Export to JSON**.
|
||||
3. Save the generated file locally, e.g. to ``trello.json``.
|
||||
4. Open a terminal window on your local machine and clone the focalboard repository to a local directory, e.g. to ``focalboard``: ``git clone https://github.com/mattermost/focalboard focalboard``
|
||||
5. Navigate to ``focalboard/webapp``.
|
||||
6. Run ``npm install``.
|
||||
7. Change directory to ``focalboard/import/trello``.
|
||||
8. Run ``npm install``.
|
||||
9. From within the same folder, run ``npx ts-node importTrello.ts -i <path-to-trello.json> -o archive.boardarchive``.
|
||||
10. In Focalboard, open the board you want to use for the export.
|
||||
11. Select **Settings > Import archive** and select ``archive.boardarchive``.
|
||||
12. Select **Upload**.
|
||||
13. Return to your board and confirm that your Trello data is now displaying.
|
||||
|
||||
### Import from Todoist
|
||||
|
||||
This node app converts a Todoist ``.json`` archive into a ``.boardarchive`` file.
|
||||
|
||||
1. Visit the open source Todoist data export service at https://darekkay.com/todoist-export/.
|
||||
2. From the **Options** menu, select **Export As > JSON (all data)**.
|
||||
3. Uncheck the **Archived** option if checked.
|
||||
4. Select **Authorize and Backup**. This will take you to your Todoist account. Follow the instructions on screen.
|
||||
5. Note the name and location of the downloaded ``.json`` file.
|
||||
6. Open a terminal window on your local machine and clone the focalboard repository to a local directory, e.g. to ``focalboard``: ``git clone https://github.com/mattermost/focalboard focalboard``
|
||||
7. Navigate to ``focalboard/webapp``.
|
||||
8. Run ``npm install``.
|
||||
9. Change directory to ``focalboard/import/todoist``.
|
||||
10. Run ``npm install``.
|
||||
11. From within the same folder, run ``npx ts-node importTodoist.ts -i <path-to-todoist.json> -o archive.boardarchive``.
|
||||
12. In Focalboard, open the board you want to use for the export.
|
||||
13. Select **Settings > Import archive** and select ``archive.boardarchive``.
|
||||
14. Select **Upload**.
|
||||
15. Return to your board and confirm that your Todoist data is now displaying.
|
||||
|
||||
## Export from Focalboard
|
||||
|
||||
You can export your boards data as a CSV file.
|
||||
|
||||
1. Select the options menu to the left of the **New** button at the top of any board.
|
||||
2. Select **Export to CSV**.
|
||||
3. Import the CSV file to your tool of choice. The CSV file contains all the cards in that board and their associated properties.
|
||||
|
||||
**Notes**:
|
||||
|
||||
- If you only see a single entry in the CSV export when the board contains multiple cards, you may have a specific card in context when you exported the file because you were performing a card search. If you have searched for a card, and that card is in context, that’s the only card that will be exported into the CSV file. Clear your search and try exporting to CSV again.
|
||||
- After importing CSV Focalboard data from one Mattermost instance into another (such as during a migration from Mattermost Cloud to self-hosted), card timestamps will be updated based on the import date, and cards won't correctly identify users whose user IDs differ across Mattermost instances.
|
||||
|
||||
## Back up your Focalboard data
|
||||
|
||||
If you’d like to back up a board, you can export it as an archive file. You can import that board to another Mattermost team within the same Mattermost instance. Exported and imported board archives include all card content such as properties, comments, descriptions, and image attachments.
|
||||
|
||||
1. Select the options menu Options icon to the left of the **New** button at the top of the board
|
||||
2. Select **Export board archive**.
|
||||
3. Download the archive file.
|
||||
4. Navigate to the team or channel workspace where you’d like to add the exported board.
|
||||
5. Select the Gear icon next to your profile picture, then choose **Import archive**. The board you exported will be added to this team or channel workspace.
|
||||
|
||||
**Notes**:
|
||||
|
||||
- If you're using a version of the Focalboard plugin older than v6.4, backing up a board results in a ``.focalboard`` file, rather than a ``.boardarchive`` file. When importing a board backup, select the **Select all files** option to select ``.focalboard`` files.
|
||||
- After importing a Focalboard backup from one Mattermost instance into another (such as during a migration from Mattermost Cloud to self-hosted), card timestamps will be updated based on the import date, and cards won't correctly identify users whose user IDs differ across Mattermost instances.
|
|
@ -1,10 +1,30 @@
|
|||
# Focalboard / Mattermost Boards Contributors Guide
|
||||
# Focalboard Plugin Documentation
|
||||
|
||||
Welcome to the [Focalboard](https://www.focalboard.com) / [Mattermost Boards](https://mattermost.com/boards/?utm_source=focalboard) project!
|
||||
Welcome to the Focalboard plugin project! We're very glad you want to check it out and perhaps contribute code to this project in GitHub.
|
||||
|
||||
We're very glad you want to check it out and perhaps contribute code our repository in GitHub.
|
||||
## Install the plugin
|
||||
|
||||
Our goal is to make your experience as great as possible. Follow these simple steps to contribute:
|
||||
Visit the [Mattermost Developer Documentation](https://developers.mattermost.com/integrate/plugins/using-and-managing-plugins/#custom-plugins) for details on how to install and enable the Focalboard plugin in your self-hosted Mattermost instance.
|
||||
|
||||
## Enable the plugin
|
||||
|
||||
Once you've installed the Focalboard plugin, you can enable the plugin in the Mattermost System Console by going to **Plugins > Plugin Management**, and selecting the **Enable** option for the Focalboard plugin.
|
||||
|
||||
## Learn what Focalboard plugin data is being collected
|
||||
|
||||
See the [plugin data being collected documentation](plugin-data-being-collected.md) for details.
|
||||
|
||||
## Use the plugin
|
||||
|
||||
See the [Focalboard plugin end user guide](focalboard-plugin-end-user-guide.md) for details on getting started with and using the plugin.
|
||||
|
||||
## Manage plugin preferences
|
||||
|
||||
See the [manage plugin preferences documentation](manage-plugin-preferences.md) for details.
|
||||
|
||||
## Contribute to the Focalboard plugin project
|
||||
|
||||
Follow these simple steps to contribute:
|
||||
|
||||
1. [Fork the Focalboard repo](https://github.com/mattermost/focalboard), clone it locally, and follow the steps in the README to build. Read the [developer tips & tricks](dev-tips.md) to get started.
|
||||
|
||||
|
|
24
docs/link-boards-to-mattermost-channels.md
Normal file
24
docs/link-boards-to-mattermost-channels.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Link boards to Mattermost channels
|
||||
|
||||
## Link a board to a channel
|
||||
|
||||
Boards can be linked to channels and accessed from the channel Apps Bar.
|
||||
|
||||
1. Select the **Focalboard** icon from the Apps Bar in a channel to open a right-hand sidebar (RHS).
|
||||
2. Search for and link boards to the channel.
|
||||
3. Select **Add** button to open the link boards dialog and search for a board to link.
|
||||
|
||||
Once a board is linked to a channel, it's listed in the right-hand pane. Linking a board to a channel automatically grants all channel members access to the board, with the exception of guest accounts. Select a linked board to navigate directly to the board.
|
||||
|
||||
**Notes**:
|
||||
- A channel can be linked to multiple boards, but each individual board can only be linked to one channel at a time.
|
||||
- Linking the same board to another channel will automatically replaces the link to the previous channel with the new channel.
|
||||
- Channel members can only search and link boards within the team where they are a board admin.
|
||||
- If you're using a Focalboard plugin older than v7.2, you won't be able to link a board to a channel. We recommend upgrading to the latest version of the plugin to take full advantage of all plugin features and functionality.
|
||||
- After upgrading to version v7.2 or later of the Focalboard plugin, your boards automatically appear in the right-hand side pane for easy access.
|
||||
|
||||
## Unlink a board from a channel
|
||||
|
||||
If you're a board admin, and want to unlink a board from a channel you're in, open linked board, select the options menu, and select **Unlink**.
|
||||
|
||||
Alternatively, you can open the **Share** dialog on the board, open the **Role** drop-down menu next to the channel's name and select **Unlink**.
|
39
docs/manage-boards.md
Normal file
39
docs/manage-boards.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Manage boards
|
||||
|
||||
## Access your boards
|
||||
|
||||
Open the Boards tab via the product menu in the top left corner of Mattermost to view all the boards for your team. You can select the **Focalboard** icon in the Apps Bar to open the right-hand panel, and display boards linked to the channel or message that you're in.
|
||||
|
||||
If you don't see the Apps Bar and your boards layout looks different to what's described, you may be using an older version of Mattermost and/or the Focalboard plugin.
|
||||
|
||||
## Find a board
|
||||
|
||||
From the top of the boards left hand sidebar, select the **Find Boards** field (CMD+K/CTRL+K) to open the board switcher, and start typing the name of the board you’re looking for.
|
||||
|
||||
## Manage sidebar categories
|
||||
|
||||
From Focalboard plugin v7.2, you can organize your boards in the left-hand sidebar using custom categories. By default, all boards will appear under the **Boards** category. To manage your categories, open the Options menu next to the category to create, delete, or rename a category. With the exception to the default **Boards** category, all other categories can be renamed or deleted.
|
||||
|
||||
After creating categories, you can move your boards to those categories by opening the Options menu next to the board and selecting **Move To…** to select the category where you want the board to be moved.
|
||||
|
||||
If you delete a category with boards in it, then those boards will return to the default **Boards** category.
|
||||
|
||||
Categories are organized per-user, so you can arrange your boards under categories that make sense to you without impacting boards or categories for other users. If a board is moved to a custom category, then the board will appear under that category for you only. Other users who are members of the board will continue to see the board in their own categories.
|
||||
|
||||
### Organize using drag and drop
|
||||
|
||||
You can organize both sidebar categories and boards to change the order of both to suit your preference. You can:
|
||||
|
||||
- Set the position of a board within a category.
|
||||
- Drag a board out of one category and drop it into another category.
|
||||
|
||||
To do this, select and hold the cursor over the category or board name. Then move the category or board around as needed. Boards moved into a category are sorted to the top of the category by default unless you specifically position the board before releasing the cursor.
|
||||
|
||||
### Manage boards in the sidebar
|
||||
|
||||
In addition to moving boards to other categories, from the Options menu next to each board name, you can perform the following actions:
|
||||
|
||||
- **Delete board**: If you're an admin of the board, you will see an option to delete the board. Deleting the board permanently removes the board from the sidebar of all board members.
|
||||
- **Duplicate board**: Creates a copy of the board and all the cards on the board. The duplicated board will appear under the same category as the original board. Board members and comments from the original board aren't migrated to the new board.
|
||||
- **New template from board**: Creates a custom board template of the board and all the cards on the board.
|
||||
- **Hide board**: Hides the board from your sidebar only. The board will still remain visible on the sidebar for other board members. You can add the board back to your sidebar using the search box (CMD+K/CTRL+K).
|
5
docs/manage-plugin-preferences.md
Normal file
5
docs/manage-plugin-preferences.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Manage plugin preferences
|
||||
|
||||
## Disable emojis on cards
|
||||
|
||||
You can enable or disable random emoji icons for your board and cards by selecting the Gear icon next to your profile picture, then toggling **Random icons on or off**.
|
46
docs/plugin-data-being-collected.md
Normal file
46
docs/plugin-data-being-collected.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Plugin data being collected
|
||||
|
||||
Boards metadata is collected and sent to Mattermost every 24 hours. Visit the [Focalboard telemetry file](https://github.com/mattermost/focalboard/blob/main/webapp/src/telemetry/telemetryClient.ts) for information about the action and event data collected.
|
||||
|
||||
Other telemetry information that Mattermost collects includes:
|
||||
|
||||
## Server telemetry
|
||||
|
||||
### Boards Plugin Information
|
||||
|
||||
- Boards Version and Build Number
|
||||
- Boards Edition
|
||||
- Operating System for Boards server
|
||||
- The server diagnostic ID
|
||||
|
||||
### Configuration Information
|
||||
|
||||
- ServerRoot is default server root (``true``/``false``)
|
||||
- Port is default port (``true``/``false``)
|
||||
- UseSSL (``true``/``false``)
|
||||
- Database Type
|
||||
- Single User (``true``/``false``)
|
||||
|
||||
### User Count Information
|
||||
|
||||
- Registered User Count
|
||||
- Daily Active User Count
|
||||
- Weekly Active User Count
|
||||
- Monthly Active User Count
|
||||
|
||||
### Block Count Information
|
||||
|
||||
- Block Counts By Type
|
||||
|
||||
### Workspace Information
|
||||
|
||||
- Workspace Count
|
||||
|
||||
## Web app event activity
|
||||
|
||||
### Load Board View
|
||||
|
||||
- ``UserID``: Unique identifier of the server.
|
||||
- ``UserActualID``: Unique identifier of the user who initiated the action.
|
||||
- ``Event``: Type of the event. Only the ``view`` event is currently monitored.
|
||||
- ``View Type`` (``board``, ``table``, ``gallery``).
|
93
docs/share-collaborate.md
Normal file
93
docs/share-collaborate.md
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Share and collaborate
|
||||
|
||||
You can share boards with your Mattermost teams and within your Mattermost channel conversations.
|
||||
|
||||
## Share a board internally
|
||||
|
||||
To share a board with team members internally, select **Share** in the top-right corner of the board, then select **Copy link** from the **Share** tab below. Paste the copied link in a channel or direct message to share the board with other team members. Only team members who have permissions to the board will be able to open the board from the shared link.
|
||||
|
||||
## Share cards in channel conversations
|
||||
|
||||
Cards can be linked and shared with team members directly with Mattermost Channels. When you share a link to a card within a channel, the card details are automatically displayed in a preview. This preview highlights what the card is about at a glance without having to navigate to it.
|
||||
|
||||
To share a card, you'll need to copy the card link first:
|
||||
|
||||
- Open a card and select the options menu **(...)** at the top right of the card, then select **Copy link**.
|
||||
- Alternatively, you can open the board view and hover your mouse over any card to access the options menu **(...)** for the card and select **Copy link** from there.
|
||||
|
||||
After you've copied the link, paste it into any channel or direct message to share the card. A preview of the card will display within the channel with a link back to the card on the board.
|
||||
|
||||
## Control access to boards
|
||||
|
||||
Boards belong to teams, and any member of a team can be granted access to a board.
|
||||
|
||||
**Note**: If you're using a Focalboard plugin version prior to v7.2, boards are tied to channel workspaces and board membership is determined by channel membership. In this case, roles and permissions information on this page won't be applicable to you.
|
||||
|
||||
### Board roles
|
||||
|
||||
The level of access to a board is determined by a user’s assigned board role. Individual board membership always gets precedence, followed by highest (most permissive) group role.
|
||||
|
||||
- **Admin**: Can modify the board, its contents, and its permissions. By default, board creators are also admins of the board.
|
||||
- **Editor**: Can modify the board and its contents.
|
||||
- **Commenter**: Can add comments to cards.
|
||||
- **Viewer**: Can view the board and its contents but can't comment or edit the board.
|
||||
|
||||
| **Board permissions** | **Admin** | **Editor** | **Commenter** | **Viewer** |
|
||||
|------------------------------------|-----------|------------|---------------|------------|
|
||||
| Modify permissions | X | | | |
|
||||
| Delete a board | X | | | |
|
||||
| Rename a board | X | X | | |
|
||||
| Add, edit, and delete views | X | X | | |
|
||||
| Add, edit, and delete cards | X | X | | |
|
||||
| Comment and delete my own comments | X | X | X | |
|
||||
| Delete any comment | X | | | |
|
||||
| View a board | X | X | X | X |
|
||||
|
||||
## System admin access
|
||||
|
||||
System admins can access any board across the server provided they have the board's URL without having to request permission or be manually added. When a system admin joins a board, their default role is admin. System admins will have an **Admin** label assigned to their name on the participants list.
|
||||
|
||||
## Team admin access
|
||||
|
||||
Team admins can access any board within their team provided they have the board's URL without having to request permission or be manually added. When a system admin joins a board, their default role is admin. Team admins will have a **Team admin** label assigned to their name on the participants list.
|
||||
|
||||
## Manage team access
|
||||
|
||||
Board admins can manage team access to their board by selecting **Share** in the top-right corner of the board. On the dropdown next to **Everyone at… Team** option, select a minimum board role for everyone on the team. You can also easily assign the new roles to the entire team and/or to individual team members.
|
||||
|
||||
Minimum default board roles reduce permission ambiguity and prevent security loopholes. The minimum default role means that board admins can't assign individual board members a role lower than the team role. If the team role is set to **Editor** then the board admin will only be able to assign the **Editor** or **Admin** role to individual team members. Lower roles will not be available for selection unless the admin changes the minimum board role.
|
||||
|
||||
Depending on the role selected, everyone on the team will have access to the board with a minimum of the permissions from the role selected. Users can get elevated permissions based on their individual board membership. The default team access for a newly created board is **None**, which means nobody on the team has access to the board.
|
||||
|
||||
## Manage individual board membership
|
||||
|
||||
Only board admins can manage user permissions on a board, including adding, changing, and removing members.
|
||||
|
||||
To add individual users from the team as explicit members of the board, open the **Share** dialog on the board, search for individual team members, then assign a role to set their permissions for the board. The role for individual board members overrides any role specified for team access.
|
||||
|
||||
- To change a board member’s role, open the **Share** dialog, select the role dropdown next to the user’s name, then select another role from the list.
|
||||
- To remove a member from a board, open the **Share** dialog, select the role dropdown next to the user’s name, then select **Remove member**.
|
||||
|
||||
Board admins can also add individual members using the autocomplete list from @mentions and the person properties. To add an individual from the autocomplete list, type their username in an @mention or in the **Person** or **Multi-person** properties, then assign a role to the user from the confirmation dialog, and select **Add to board**.
|
||||
|
||||
On boards with team access, board members with **Editor** or **Commenter** roles can also add individuals to the board from the autocomplete list. Board members added in this manner will be assigned the default minimum board role.
|
||||
|
||||
## Channel role groups
|
||||
|
||||
Board admins can add a channel to a board to grant all its members Editor access. To do this, select **Share** in the top-right corner of the board, search for the channel name, and add it to the board as a user. The default role is Editor. Doing so also [links the board back to the channel](link-boards-to-mattermost-channels) where the board will appear on the channel RHS.
|
||||
|
||||
To unlink the channel from the board, open the **Share** dialog, select the role dropdown next to the channel’s name, then select **Unlink**.
|
||||
|
||||
Remember, a board can only be linked to one channel at a time. Linking another channel to the same board will automatically remove the link from the previous channel.
|
||||
|
||||
## Guest accounts
|
||||
|
||||
From version v7.4 of the Focalboard plugin, [Mattermost guest accounts](https://docs.mattermost.com/onboard/guest-accounts.html#guest-accounts) are supported. If you're not able to access this functionality, you may be on an earlier version of the Focalboard plugin.
|
||||
|
||||
Guests can:
|
||||
|
||||
- Access boards where they're added as an explicit member of the board, but can't manage team access or add channels to boards.
|
||||
- Access existing boards, but can't create new boards. Guests also don't have access to the template picker and can't duplicate an existing board.
|
||||
- Search for boards where they're currently an explicit member.
|
||||
- Be assigned the Viewer, Commenter, or Editor roles, but not the Admin role.
|
||||
- Only @mention current members on the board.
|
35
docs/work-with-board-views.md
Normal file
35
docs/work-with-board-views.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Work with board views
|
||||
|
||||
Views display your cards in a board, table, calendar, or gallery layout, optionally filtered and grouped by a property (e.g., priority, status, etc).
|
||||
|
||||
To add a new view to a board:
|
||||
|
||||
1. From the board header, select the menu next to the current view name.
|
||||
2. Scroll down and select **+ Add view**.
|
||||
3. select the new visualization you’d like to use.
|
||||
|
||||
The following board views are available.
|
||||
|
||||
## Board view
|
||||
|
||||
This is a kanban view where cards are grouped into columns. Column groups only work with the **Select** or **Person** properties and display all cards that share the same value from the specified property. The column names are editable, and any changes to the column names are also applied to the value from the property. Cards can be dragged between columns, which will automatically update the property’s assigned value on the card.
|
||||
|
||||
## Table view
|
||||
|
||||
Displays cards in a table format with rows and columns. Use this view to get an overview of all your project tasks. Easily view and compare the state of all properties across all cards without needing to open individual cards. Each column corresponds to a card property. You can edit cells directly or you can select **Open** to open the card view for that row.
|
||||
|
||||
## Gallery view
|
||||
|
||||
Displays cards in a gallery format, so you can manage and organize cards with image attachments. Gallery view displays a preview of the first image attached on the card. For cards with no image attachments, a preview of the first description block will be displayed instead.
|
||||
|
||||
## Calendar view
|
||||
|
||||
To use this view, cards need to have the **Date** property added.
|
||||
|
||||
If cards don’t have a custom **Date** property, they’ll be sorted and displayed by the card creation date (default). These cards can’t be moved around the board until a custom **Date** property is added.
|
||||
|
||||
If your cards do have a **Date** property and you’re not able to move them around, you may be displaying them by **Created Time** or **Last Updated Time**.
|
||||
|
||||
- To add a new card, select the **+** option in the top-left corner of the relevant date.
|
||||
- To create a date range event, select a start date and then drag to the end date to create a card for that date range event.
|
||||
- To add a date range to an existing card, hover over the side of the card to display the arrow and drag to the left or right to create a date range.
|
164
docs/work-with-cards.md
Normal file
164
docs/work-with-cards.md
Normal file
|
@ -0,0 +1,164 @@
|
|||
# Work with cards
|
||||
|
||||
## What's a card?
|
||||
|
||||
Cards are used on a board to track individual work items. Cards are customizable and can have a number of properties added to them, which are then used as a way to tag, sort, and filter the cards.
|
||||
|
||||
A card consists of:
|
||||
|
||||
- **A set of properties**: Properties are common to all cards in a board. Board views can group cards by “Select” type properties into different columns.
|
||||
- **A list of comments**: Comments are useful for noting important changes or milestones.
|
||||
- **A set of content**: The content of a card can consist of Markdown text, checkboxes, and images. Use this to record detailed specs or design decisions for an item for example.
|
||||
|
||||
When working with cards, you can manage properties, add descriptions, attach images, assign them to team members, mention team members, add comments, and so on.
|
||||
|
||||
Standard board templates provide some default card properties that can be customized or removed. In the Roadmap template, there's a **Type** property, whereas in the Project Tasks template, there's an **Estimated Hours** property. These properties are not exclusive to any template and can be easily re-created in any of the templates provided.
|
||||
|
||||
## Add card descriptions
|
||||
|
||||
Card descriptions can include text with Markdown formatting, checkboxes, and visual elements such as images or GIFs, and can be separated into blocks of content. To add a description, open a card, select **Add a description** below the **Comments** section, and start typing in your content.
|
||||
|
||||
To add a new content block in the description section, hover over the section and select **Add content**. Then choose from any of the following options:
|
||||
|
||||
- **Text**: Adds a new text block that can be formatted with Markdown.
|
||||
- **Image**: Select and embed an image file into the content block. The following image formats are currently supported: GIF, JPEG, and PNG.
|
||||
- **Divider**: Adds a divider content block below the previous block.
|
||||
- **Checkbox**: Adds a checkbox content block. Press Enter/Return after typing in content for your checkbox to add another checkbox within the same block. Please note, Markdown formatting isn't supported within the **Checkbox** content block.
|
||||
|
||||
To manage the description content blocks on a card, hover over any existing block and select the options menu |options-icon| to move the block up or down, insert a new block above, or delete the current block. Alternatively, you can hover over any existing block, then select and hold the grid button to drag and drop it to a new position within the description section.
|
||||
|
||||
## Attach files to cards
|
||||
|
||||
From Focalboard plugin version v7.7, you can attach files to your cards, which other board members can download. There are no limitations to the file types that you can upload.
|
||||
|
||||
To upload a file to a card, select **Attach** in the top-right corner of the card. Then select the file you'd like to upload. When your file has been uploaded, you can find it in the **Attachments** section of the card. Select the **+** sign to add additional files to your card.
|
||||
|
||||
To delete a file attachment, hover over it and select the 3-dot menu, then select **Delete**. To download the file, select the download icon.
|
||||
|
||||
## Add card badges
|
||||
|
||||
Card badges are a quick way to view card details without opening up a card. To add them, select **Properties > Comments and Description**. Icons related to the card description, comments, and checkboxes will be displayed on cards with the respective content. Open the card to view the details.
|
||||
|
||||
- The description icon indicates that a card has a text description.
|
||||
- The comment icon displays a number indicating how many comments have been added to a card. When a new comment is added, that number is updated.
|
||||
- The checkbox icon displays the number of items checked off relative to the total number of checkboxes within the card. When an item is checked off, the icon is automatically updated.
|
||||
|
||||
## Comment on a card
|
||||
|
||||
Comments allow you to provide feedback and ask questions relevant to the specific work item on the card.
|
||||
|
||||
To add a comment, select a card to open the card view, then click on **Add a comment…** to type in your comment, and press **Send** to save the comment to the card. All team members who are `following the card </boards/work-with-cards.html#receive-updates>`_ will receive a notification with a preview of your comment in Mattermost Channels.
|
||||
|
||||
From Focalboard plugin v7.4, only board members with the *Commenter* role or higher can comment on a card. Board members assigned the *Viewer* role can view, but not comment on, a card.
|
||||
|
||||
## Mention people on cards
|
||||
|
||||
You can include a team member on a card by `mentioning them on a card </channels/mention-people.html>`__ the same way you would in Channels. Mentions are supported in the `Comments </boards/work-with-cards.html#comment-on-a-card>`_ and `Description </boards/work-with-cards.html#card-descriptions>`_ sections within a card. The team member you mention will receive a direct message notification from the boards bot with a link to the card you mentioned them on. To mention multiple team members, separate each name with a comma.
|
||||
|
||||
## Follow card updates
|
||||
|
||||
When you create a card, you automatically follow it. You can @mention someone on a card to add them as a follower. This can be a card you've created or someone else's card. Lastly, you can also follow cards manually using the **Follow** option on the top-right corner of a card. To unfollow a card, select **Following**.
|
||||
|
||||
When updates are made to a card you're following, you'll receive a direct message from the boards bot with a summary of the change (e.g. Bob changed status from **In progress** to **Done**) and a link to the card for more detailed information.
|
||||
|
||||
You won't get a notification of your own changes made to a card, even if you're following that card.
|
||||
|
||||
## Search for cards
|
||||
|
||||
You can search through all the cards on a board to find what you’re looking for. Open the board you want to search, then select the **Search cards** field in the top-right of the board.
|
||||
|
||||
## Manage card properties
|
||||
|
||||
Cards can contain different data fields depending on the purpose of the board. Using card properties, you can customize these data fields to fit your needs and track the information most important to you. For example, in a **Roadmap** board, cards include a **Type** field where you can add categories such as **Bug**, **Epic**, etc. In a **Project Task** board, cards include the **Estimated Hours** field instead.
|
||||
|
||||
Properties are displayed in the order they were created and can't be re-ordered.
|
||||
|
||||
## Create card properties
|
||||
|
||||
To create a new property field open a card and select **Add a property**. Then select the type of property from the drop-down menu. The property type specifies the type of data you plan to capture within that field. When you create new card properties, they're added to all new and all existing cards on the current board.
|
||||
|
||||
Properties are automatically added to the board filter list at the top of the page, so ensure you customize all property names to make it easy to filter your board by specific properties later.
|
||||
|
||||
## Work with property types
|
||||
|
||||
The Focalboard plugin supports a wide range of fully customizable property types:
|
||||
|
||||
- **Text** can be used to add short notes to a card. An advantage of the text property over card descriptions is that it can be `shown on the board <https://docs.mattermost.com/boards/work-with-cards.html#toggle-properties-shown-on-a-board>`_ without needing to open the card.
|
||||
- **Numbers** are useful to capture metrics such as task sizing or effort estimates. Use in conjunction with calculations to get the most out of the number property type.
|
||||
- **Email** and **Phone** can be used to record contact information.
|
||||
- **URL** can be used to provide a link to a pull request or relevant website. Clicking on the box of a URL property will automatically open the link in a new tab on your browser. Hover over the box to surface options to copy or edit the URL.
|
||||
- **Select** and **Multi-select** allows you to create a predefined list of options that can be color-coded and displayed as badges on the card to indicate things like status and priority.
|
||||
- **Dates** are useful to set and track due dates or milestones. Use the date property to make a card appear on the `Calendar view <https://docs.mattermost.com/boards/work-with-views.html#calendar-view>`_. Set a single date or toggle on the **End date** to set a date range.
|
||||
- **Person** and **Multi-person** provides a quick way to capture user assignments. Note that this is not available in Personal Desktop.
|
||||
- **Checkbox** is a toggle property that can be used for assigning simple binary options on a card such as True/False or Yes/No.
|
||||
- **Created time/Created by/Last updated time/Last updated by** are predefined system properties to help you audit changes on a card. The names of these properties are customizable, but the values are not.
|
||||
|
||||
### Rename a property type
|
||||
|
||||
The default name for a new property is the name of the property type (e.g. **Date**, **URL**).
|
||||
To rename a property field, open up a card and select the property name to open an editable field. Enter the new name in the field provided. The change is saved immediately and applied across all cards on the current board.
|
||||
|
||||
### Change a property type
|
||||
|
||||
To change a property type, select the property then open the **Type** menu and choose a new property type. You’ll be asked to confirm the change from every card on the current board. Changing the type for an existing property will affect values across all cards on the board and may result in data loss.
|
||||
|
||||
### Delete a property
|
||||
|
||||
To delete properties you no longer need, select the property, then choose **Delete**. You’ll be asked to confirm that you want to remove that property from every card on the current board.
|
||||
|
||||
### Define a "Select" or "Multi-select" property
|
||||
|
||||
The options on a **Select** and **Multi-select** property type appear as color-coded tags on a card. Options in a **Select** or **Multi-Select** property list are sorted in the order they were created and can't be re-ordered or renamed.
|
||||
|
||||
To add and configure the options on these types:
|
||||
|
||||
1. Select a card to open the card view.
|
||||
2. Add a new property, give it a name, and set its type to **Select** (or **Multi-Select**).
|
||||
3. Select the field box for the property, and start typing the name of a new option. Press Enter to accept. Repeat this step to add additional options.
|
||||
- To assign a color to or delete an option, select the value and select the options menu **(...)** next to each option name.
|
||||
- To select an option on the property, select the box and choose one of the values from the menu.
|
||||
- To remove an option on the property, select the box and chooose the `X` next to the option name you want to remove.
|
||||
|
||||
Alternatively, you can also add new options directly from a board:
|
||||
|
||||
1. Open a board view.
|
||||
2. Group by a **Select** property.
|
||||
3. Scroll to the right of the board and select **+ Add a group**.
|
||||
|
||||
This will add a new column, which corresponds to a new value option for the Select property.
|
||||
|
||||
### Control what properties are shown on a board
|
||||
|
||||
Once you have card properties defined, you have full control over which properties are shown on the board as a preview without having to open the card. Select **Properties** at the top of the board, then enable all properties you want to see at a glance, and hide all properties you don’t want to see.
|
||||
|
||||
## Create card templates
|
||||
|
||||
Card templates can help reduce repetitive manual input for similar types of work items. Each board can have any number of card templates. To create a new card template:
|
||||
|
||||
1. Open the board where you want to add the card template.
|
||||
2. Select the drop-down arrow next to **New**, then select **New template**.
|
||||
3. Add a title to the card template.
|
||||
4. Then assign values to any properties and add a description you wish to have pre-populated when a card is created from the template.
|
||||
5. Close the card using the **X** in the top left corner.
|
||||
6. Select the drop-down arrow next to **New**, then select the template you just created.
|
||||
|
||||
Alternatively, you can turn any existing card into a template:
|
||||
|
||||
1. Open the card you want to use as a template.
|
||||
2. Select the options menu |options-icon| in the top-right corner of the card.
|
||||
3. Select **New template from card**.
|
||||
4. Edit the card as needed, including a helpful name.
|
||||
5. Close the card using the **X** in the top left corner.
|
||||
6. Select the drop-down arrow next to **New**, then select the template you just created.
|
||||
|
||||
To set a default card template for all new cards created on the board:
|
||||
|
||||
1. Select the drop-down arrow next to **New**.
|
||||
2. Open the options menu |options-icon| next to the card template of your choosing.
|
||||
3. Select **Set as default**.
|
||||
|
||||
**Notes**:
|
||||
|
||||
- The card template is applicable only to the board in which it’s created, and isn’t available in other boards within your team workspace.
|
||||
- Comments on a template don't get populated on to new cards.
|
||||
- Additionally, properties can't be hidden from a card template at this time. All cards on a board share the same properties, so adding or deleting a property on a template will also apply to all cards on a board.
|
163
linux/go.mod
163
linux/go.mod
|
@ -1,123 +1,118 @@
|
|||
module github.com/mattermost/focalboard/linux
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.8
|
||||
|
||||
replace github.com/mattermost/focalboard/server => ../server
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/mattermost/focalboard/server v0.0.0-00010101000000-000000000000
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20221214122404-8d90c7042f93
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattermost/focalboard/server v0.0.0-20230104182634-f909c2552e37
|
||||
github.com/mattermost/mattermost/server/public v0.1.3
|
||||
github.com/webview/webview v0.0.0-20220314230258-a2b7746141c3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.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.13.0 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/golang-migrate/migrate/v4 v4.15.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/graph-gophers/graphql-go v1.4.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.3.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.4.6 // 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/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.1 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.15.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.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.7 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // 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-20201202150706-ee0e6284187d // indirect
|
||||
github.com/mattermost/logr/v2 v2.0.15 // indirect
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb // indirect
|
||||
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e // indirect
|
||||
github.com/mattermost/squirrel v0.2.0 // 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/v8 v8.0.0-20240529104128-9d30a62c9471 // 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.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.43 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // 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.4.3 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // 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.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.12.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.33.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/rs/xid v1.4.0 // 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.0.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/segmentio/backo-go v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // 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.10.1 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/testify v1.8.1 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.3 // indirect
|
||||
github.com/spf13/viper v1.18.2 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // 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.6 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // 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.4 // 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.5.3 // indirect
|
||||
golang.org/x/crypto v0.2.0 // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/tools v0.3.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
github.com/yuin/goldmark v1.7.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // 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/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // 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.0.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
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.36.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.6 // indirect
|
||||
modernc.org/libc v1.16.7 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/sqlite v1.18.0 // indirect
|
||||
modernc.org/strutil v1.1.1 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // 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
|
||||
)
|
||||
|
|
2232
linux/go.sum
2232
linux/go.sum
File diff suppressed because it is too large
Load diff
|
@ -16,7 +16,7 @@ import (
|
|||
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
|
||||
"github.com/webview/webview"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
var sessionToken string = "su-" + uuid.New().String()
|
||||
|
@ -71,13 +71,13 @@ func runServer(port int) (*server.Server, error) {
|
|||
permissionsService := localpermissions.New(db, logger)
|
||||
|
||||
params := server.Params{
|
||||
Cfg: config,
|
||||
SingleUserToken: sessionToken,
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
ServerID: "",
|
||||
WSAdapter: nil,
|
||||
NotifyBackends: nil,
|
||||
Cfg: config,
|
||||
SingleUserToken: sessionToken,
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
ServerID: "",
|
||||
WSAdapter: nil,
|
||||
NotifyBackends: nil,
|
||||
PermissionsService: permissionsService,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,87 +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
|
||||
- deadcode
|
||||
- ineffassign
|
||||
- structcheck
|
||||
- varcheck
|
||||
- unparam
|
||||
- errcheck
|
||||
- govet
|
||||
- bodyclose
|
||||
- durationcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- gosec
|
||||
- makezero
|
||||
- staticcheck
|
||||
- prealloc
|
||||
- asciicheck
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- goconst
|
||||
- gocritic
|
||||
- godot
|
||||
- goerr113
|
||||
- goheader
|
||||
- revive
|
||||
- nakedret
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosimple
|
||||
- lll
|
||||
- misspell
|
||||
- nolintlint
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- whitespace
|
||||
- gocyclo
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: server/manifest.go
|
||||
linters:
|
||||
- deadcode
|
||||
- unused
|
||||
- varcheck
|
||||
- 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,339 +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
|
||||
|
||||
setup-go-work: ## Sets up a go.work file
|
||||
cd ..; go run ./build/gowork/main.go
|
||||
|
||||
## 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: setup-go-work ## 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)
|
||||
|
||||
.PHONY: build-product
|
||||
build-product: apply
|
||||
cd webapp && npm run build:product
|
||||
|
||||
.PHONY: watch-product
|
||||
watch-product: apply
|
||||
cd webapp && npm run start:product
|
||||
|
||||
## 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,63 +0,0 @@
|
|||
module github.com/mattermost/mattermost-plugin-starter-template/build
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/go-git/go-git/v5 v5.1.0
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20220802151854-f07c31c5d933
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.7.2
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
||||
|
||||
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.4 // 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.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // 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-20201202150706-ee0e6284187d // indirect
|
||||
github.com/mattermost/logr/v2 v2.0.15 // indirect
|
||||
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.1 // 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.8.1 // indirect
|
||||
github.com/tinylib/msgp v1.1.6 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wiggin77/merror v1.0.3 // indirect
|
||||
github.com/wiggin77/srslog v1.0.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.2.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
||||
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 // indirect
|
||||
golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // 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,343 +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/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/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-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/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.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/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/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/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/logr/v2 v2.0.15 h1:+WNbGcsc3dBao65eXlceB6dTILNJRIrvubnsTl3zBew=
|
||||
github.com/mattermost/logr/v2 v2.0.15/go.mod h1:mpPp935r5dIkFDo2y9Q87cQWhFR/4xXpNh0k/y8Hmwg=
|
||||
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/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/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/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/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/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.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/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/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/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/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/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/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-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-20211216021012-1d35b9e2eb4e/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/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/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/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=
|
||||
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/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,127 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
const pluginIDGoFileTemplate = `// This file is automatically generated. Do not modify it manually.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/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,172 +0,0 @@
|
|||
// main handles deployment of the plugin to a development server using the Client4 API.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/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)
|
||||
_, _, err := client.Login(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()
|
||||
|
||||
log.Print("Uploading plugin via API.")
|
||||
_, _, err = client.UploadPluginForced(pluginBundle)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload plugin bundle: %s", err)
|
||||
}
|
||||
|
||||
log.Print("Enabling plugin.")
|
||||
_, err = client.EnablePlugin(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 {
|
||||
log.Print("Disabling plugin.")
|
||||
_, err := client.DisablePlugin(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 {
|
||||
log.Print("Enabling plugin.")
|
||||
_, err := client.EnablePlugin(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.19
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/mattermost/focalboard/server v0.0.0-20220818150333-feb49eaf197a
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20221214122404-8d90c7042f93
|
||||
github.com/stretchr/testify v1.8.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // 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/fatih/color v1.13.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/golang-migrate/migrate/v4 v4.15.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/graph-gophers/graphql-go v1.4.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.3.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.4.6 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.15.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.1 // 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.7 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
|
||||
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect
|
||||
github.com/mattermost/logr/v2 v2.0.15 // indirect
|
||||
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e // indirect
|
||||
github.com/mattermost/squirrel v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.43 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // 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.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.12.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.33.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/rudderlabs/analytics-go v3.3.3+incompatible // indirect
|
||||
github.com/segmentio/backo-go v1.0.1 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.10.1 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.1.6 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wiggin77/merror v1.0.4 // indirect
|
||||
github.com/wiggin77/srslog v1.0.1 // indirect
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
github.com/yuin/goldmark v1.5.3 // indirect
|
||||
golang.org/x/crypto v0.2.0 // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/tools v0.3.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.36.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.6 // indirect
|
||||
modernc.org/libc v1.16.7 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/sqlite v1.18.0 // indirect
|
||||
modernc.org/strutil v1.1.1 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
)
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
}
|
|
@ -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": "7.10.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,257 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package product
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// serviceAPIAdapter is an adapter that flattens the APIs provided by suite services so they can
|
||||
// be used as per the Plugin API.
|
||||
// Note: when supporting a plugin build is no longer needed this adapter may be removed as the Boards app
|
||||
// can be modified to use the services in modular fashion.
|
||||
type serviceAPIAdapter struct {
|
||||
api *boardsProduct
|
||||
ctx *request.Context
|
||||
}
|
||||
|
||||
func newServiceAPIAdapter(api *boardsProduct) *serviceAPIAdapter {
|
||||
return &serviceAPIAdapter{
|
||||
api: api,
|
||||
ctx: request.EmptyContext(api.logger),
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Channels service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) {
|
||||
channel, appErr := a.api.channelService.GetDirectChannel(userID1, userID2)
|
||||
return channel, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) {
|
||||
channel, appErr := a.api.channelService.GetDirectChannelOrCreate(userID1, userID2)
|
||||
return channel, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) GetChannelByID(channelID string) (*mm_model.Channel, error) {
|
||||
channel, appErr := a.api.channelService.GetChannelByID(channelID)
|
||||
return channel, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) {
|
||||
member, appErr := a.api.channelService.GetChannelMember(channelID, userID)
|
||||
return member, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) {
|
||||
opts := &mm_model.ChannelSearchOpts{
|
||||
IncludeDeleted: includeDeleted,
|
||||
}
|
||||
channels, appErr := a.api.channelService.GetChannelsForTeamForUser(teamID, userID, opts)
|
||||
return channels, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Post service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) CreatePost(post *mm_model.Post) (*mm_model.Post, error) {
|
||||
post, appErr := a.api.postService.CreatePost(a.ctx, post)
|
||||
return post, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// User service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetUserByID(userID string) (*mm_model.User, error) {
|
||||
user, appErr := a.api.userService.GetUser(userID)
|
||||
return user, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) GetUserByUsername(name string) (*mm_model.User, error) {
|
||||
user, appErr := a.api.userService.GetUserByUsername(name)
|
||||
return user, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) GetUserByEmail(email string) (*mm_model.User, error) {
|
||||
user, appErr := a.api.userService.GetUserByEmail(email)
|
||||
return user, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) UpdateUser(user *mm_model.User) (*mm_model.User, error) {
|
||||
user, appErr := a.api.userService.UpdateUser(a.ctx, user, true)
|
||||
return user, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) {
|
||||
user, appErr := a.api.userService.GetUsersFromProfiles(options)
|
||||
return user, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Team service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) {
|
||||
member, appErr := a.api.teamService.GetMember(teamID, userID)
|
||||
return member, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) {
|
||||
member, appErr := a.api.teamService.CreateMember(a.ctx, teamID, userID)
|
||||
return member, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Permissions service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) HasPermissionTo(userID string, permission *mm_model.Permission) bool {
|
||||
return a.api.permissionsService.HasPermissionTo(userID, permission)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
|
||||
return a.api.permissionsService.HasPermissionToTeam(userID, teamID, permission)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool {
|
||||
return a.api.permissionsService.HasPermissionToChannel(askingUserID, channelID, permission)
|
||||
}
|
||||
|
||||
//
|
||||
// Bot service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
|
||||
return a.api.botService.EnsureBot(a.ctx, boardsProductID, bot)
|
||||
}
|
||||
|
||||
//
|
||||
// License service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetLicense() *mm_model.License {
|
||||
return a.api.licenseService.GetLicense()
|
||||
}
|
||||
|
||||
//
|
||||
// FileInfoStore service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) {
|
||||
fi, appErr := a.api.fileInfoStoreService.GetFileInfo(fileID)
|
||||
return fi, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Cluster store.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) {
|
||||
a.api.clusterService.PublishWebSocketEvent(boardsProductName, event, payload, broadcast)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error {
|
||||
return a.api.clusterService.PublishPluginClusterEvent(boardsProductName, ev, opts)
|
||||
}
|
||||
|
||||
//
|
||||
// Cloud service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
|
||||
return a.api.cloudService.GetCloudLimits()
|
||||
}
|
||||
|
||||
//
|
||||
// Config service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetConfig() *mm_model.Config {
|
||||
return a.api.configService.Config()
|
||||
}
|
||||
|
||||
//
|
||||
// Logger service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetLogger() mlog.LoggerIFace {
|
||||
return a.api.logger
|
||||
}
|
||||
|
||||
//
|
||||
// KVStore service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) {
|
||||
b, appErr := a.api.kvStoreService.SetPluginKeyWithOptions(boardsProductName, key, value, options)
|
||||
return b, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
//
|
||||
// Store service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) {
|
||||
return a.api.storeService.GetMasterDB(), nil
|
||||
}
|
||||
|
||||
//
|
||||
// System service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetDiagnosticID() string {
|
||||
return a.api.systemService.GetDiagnosticId()
|
||||
}
|
||||
|
||||
//
|
||||
// Router service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) {
|
||||
a.api.routerService.RegisterRouter(boardsProductName, sub)
|
||||
}
|
||||
|
||||
//
|
||||
// Preferences service.
|
||||
//
|
||||
|
||||
func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
||||
p, appErr := a.api.preferencesService.GetPreferencesForUser(userID)
|
||||
return p, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||
appErr := a.api.preferencesService.UpdatePreferencesForUser(userID, preferences)
|
||||
return normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||
appErr := a.api.preferencesService.DeletePreferencesForUser(userID, preferences)
|
||||
return normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
// Ensure the adapter implements ServicesAPI.
|
||||
var _ model.ServicesAPI = &serviceAPIAdapter{}
|
|
@ -1,331 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package product
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/focalboard/mattermost-plugin/server/boards"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/product"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
boardsProductName = "boards"
|
||||
boardsProductID = "com.mattermost.boards"
|
||||
)
|
||||
|
||||
var errServiceTypeAssert = errors.New("type assertion failed")
|
||||
|
||||
func init() {
|
||||
product.RegisterProduct(boardsProductName, product.Manifest{
|
||||
Initializer: newBoardsProduct,
|
||||
Dependencies: map[product.ServiceKey]struct{}{
|
||||
product.TeamKey: {},
|
||||
product.ChannelKey: {},
|
||||
product.UserKey: {},
|
||||
product.PostKey: {},
|
||||
product.PermissionsKey: {},
|
||||
product.BotKey: {},
|
||||
product.ClusterKey: {},
|
||||
product.ConfigKey: {},
|
||||
product.LogKey: {},
|
||||
product.LicenseKey: {},
|
||||
product.FilestoreKey: {},
|
||||
product.FileInfoStoreKey: {},
|
||||
product.RouterKey: {},
|
||||
product.CloudKey: {},
|
||||
product.KVStoreKey: {},
|
||||
product.StoreKey: {},
|
||||
product.SystemKey: {},
|
||||
product.PreferencesKey: {},
|
||||
product.HooksKey: {},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type boardsProduct struct {
|
||||
teamService product.TeamService
|
||||
channelService product.ChannelService
|
||||
userService product.UserService
|
||||
postService product.PostService
|
||||
permissionsService product.PermissionService
|
||||
botService product.BotService
|
||||
clusterService product.ClusterService
|
||||
configService product.ConfigService
|
||||
logger mlog.LoggerIFace
|
||||
licenseService product.LicenseService
|
||||
filestoreService product.FilestoreService
|
||||
fileInfoStoreService product.FileInfoStoreService
|
||||
routerService product.RouterService
|
||||
cloudService product.CloudService
|
||||
kvStoreService product.KVStoreService
|
||||
storeService product.StoreService
|
||||
systemService product.SystemService
|
||||
preferencesService product.PreferencesService
|
||||
hooksService product.HooksService
|
||||
|
||||
boardsApp *boards.BoardsApp
|
||||
}
|
||||
|
||||
func newBoardsProduct(services map[product.ServiceKey]interface{}) (product.Product, error) {
|
||||
boardsProd := &boardsProduct{}
|
||||
|
||||
if err := populateServices(boardsProd, services); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boardsProd.logger.Info("Creating boards service")
|
||||
|
||||
adapter := newServiceAPIAdapter(boardsProd)
|
||||
boardsApp, err := boards.NewBoardsApp(adapter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Boards service: %w", err)
|
||||
}
|
||||
|
||||
boardsProd.boardsApp = boardsApp
|
||||
|
||||
// Add the Boards services API to the services map so other products can access Boards functionality.
|
||||
boardsAPI := boards.NewBoardsServiceAPI(boardsApp)
|
||||
services[product.BoardsKey] = boardsAPI
|
||||
|
||||
return boardsProd, nil
|
||||
}
|
||||
|
||||
// populateServices populates the boardProduct with all the services needed from the suite.
|
||||
func populateServices(boardsProd *boardsProduct, services map[product.ServiceKey]interface{}) error {
|
||||
for key, service := range services {
|
||||
switch key {
|
||||
case product.TeamKey:
|
||||
teamService, ok := service.(product.TeamService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.teamService = teamService
|
||||
case product.ChannelKey:
|
||||
channelService, ok := service.(product.ChannelService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.channelService = channelService
|
||||
case product.UserKey:
|
||||
userService, ok := service.(product.UserService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.userService = userService
|
||||
case product.PostKey:
|
||||
postService, ok := service.(product.PostService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.postService = postService
|
||||
case product.PermissionsKey:
|
||||
permissionsService, ok := service.(product.PermissionService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.permissionsService = permissionsService
|
||||
case product.BotKey:
|
||||
botService, ok := service.(product.BotService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.botService = botService
|
||||
case product.ClusterKey:
|
||||
clusterService, ok := service.(product.ClusterService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.clusterService = clusterService
|
||||
case product.ConfigKey:
|
||||
configService, ok := service.(product.ConfigService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.configService = configService
|
||||
case product.LogKey:
|
||||
logger, ok := service.(mlog.LoggerIFace)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.logger = logger.With(mlog.String("product", boardsProductName))
|
||||
case product.LicenseKey:
|
||||
licenseService, ok := service.(product.LicenseService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.licenseService = licenseService
|
||||
case product.FilestoreKey:
|
||||
filestoreService, ok := service.(product.FilestoreService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.filestoreService = filestoreService
|
||||
case product.FileInfoStoreKey:
|
||||
fileInfoStoreService, ok := service.(product.FileInfoStoreService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.fileInfoStoreService = fileInfoStoreService
|
||||
case product.RouterKey:
|
||||
routerService, ok := service.(product.RouterService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.routerService = routerService
|
||||
case product.CloudKey:
|
||||
cloudService, ok := service.(product.CloudService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.cloudService = cloudService
|
||||
case product.KVStoreKey:
|
||||
kvStoreService, ok := service.(product.KVStoreService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.kvStoreService = kvStoreService
|
||||
case product.StoreKey:
|
||||
storeService, ok := service.(product.StoreService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.storeService = storeService
|
||||
case product.SystemKey:
|
||||
systemService, ok := service.(product.SystemService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.systemService = systemService
|
||||
case product.PreferencesKey:
|
||||
preferencesService, ok := service.(product.PreferencesService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.preferencesService = preferencesService
|
||||
case product.HooksKey:
|
||||
hooksService, ok := service.(product.HooksService)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boardsProd.hooksService = hooksService
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) Start() error {
|
||||
if !bp.configService.Config().FeatureFlags.BoardsProduct {
|
||||
bp.logger.Info("Boards product disabled via feature flag")
|
||||
return nil
|
||||
}
|
||||
|
||||
bp.logger.Info("Starting boards service")
|
||||
|
||||
adapter := newServiceAPIAdapter(bp)
|
||||
boardsApp, err := boards.NewBoardsApp(adapter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Boards service: %w", err)
|
||||
}
|
||||
|
||||
model.LogServerInfo(bp.logger)
|
||||
|
||||
if err := bp.hooksService.RegisterHooks(boardsProductName, bp); err != nil {
|
||||
return fmt.Errorf("failed to register hooks: %w", err)
|
||||
}
|
||||
|
||||
bp.boardsApp = boardsApp
|
||||
if err := bp.boardsApp.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start Boards service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) Stop() error {
|
||||
bp.logger.Info("Stopping boards service")
|
||||
|
||||
if bp.boardsApp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := bp.boardsApp.Stop(); err != nil {
|
||||
return fmt.Errorf("error while stopping Boards service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// These callbacks are called by the suite automatically
|
||||
//
|
||||
|
||||
func (bp *boardsProduct) OnConfigurationChange() error {
|
||||
if bp.boardsApp == nil {
|
||||
return nil
|
||||
}
|
||||
return bp.boardsApp.OnConfigurationChange()
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) OnWebSocketConnect(webConnID, userID string) {
|
||||
if bp.boardsApp == nil {
|
||||
return
|
||||
}
|
||||
bp.boardsApp.OnWebSocketConnect(webConnID, userID)
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) OnWebSocketDisconnect(webConnID, userID string) {
|
||||
if bp.boardsApp == nil {
|
||||
return
|
||||
}
|
||||
bp.boardsApp.OnWebSocketDisconnect(webConnID, userID)
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) {
|
||||
if bp.boardsApp == nil {
|
||||
return
|
||||
}
|
||||
bp.boardsApp.WebSocketMessageHasBeenPosted(webConnID, userID, req)
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) OnPluginClusterEvent(ctx *plugin.Context, ev mm_model.PluginClusterEvent) {
|
||||
if bp.boardsApp == nil {
|
||||
return
|
||||
}
|
||||
bp.boardsApp.OnPluginClusterEvent(ctx, ev)
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) MessageWillBePosted(ctx *plugin.Context, post *mm_model.Post) (*mm_model.Post, string) {
|
||||
if bp.boardsApp == nil {
|
||||
return post, ""
|
||||
}
|
||||
return bp.boardsApp.MessageWillBePosted(ctx, post)
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) MessageWillBeUpdated(ctx *plugin.Context, newPost, oldPost *mm_model.Post) (*mm_model.Post, string) {
|
||||
if bp.boardsApp == nil {
|
||||
return newPost, ""
|
||||
}
|
||||
return bp.boardsApp.MessageWillBeUpdated(ctx, newPost, oldPost)
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) OnCloudLimitsUpdated(limits *mm_model.ProductLimits) {
|
||||
if bp.boardsApp == nil {
|
||||
return
|
||||
}
|
||||
bp.boardsApp.OnCloudLimitsUpdated(limits)
|
||||
}
|
||||
|
||||
func (bp *boardsProduct) RunDataRetention(nowTime, batchSize int64) (int64, error) {
|
||||
if bp.boardsApp == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return bp.boardsApp.RunDataRetention(nowTime, batchSize)
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package imports
|
||||
|
||||
import (
|
||||
// Needed to ensure the init() method in the FocalBoard product is run.
|
||||
// This file is copied to the mmserver imports package via makefile.
|
||||
_ "github.com/mattermost/focalboard/mattermost-plugin/product"
|
||||
)
|
|
@ -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,274 +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/v6/plugin"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/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)
|
||||
}
|
||||
|
||||
//
|
||||
// Cloud service.
|
||||
//
|
||||
|
||||
func (a *pluginAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
|
||||
return a.api.GetCloudLimits()
|
||||
}
|
||||
|
||||
//
|
||||
// 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,84 +0,0 @@
|
|||
package boards
|
||||
|
||||
import (
|
||||
"github.com/mattermost/focalboard/server/app"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/product"
|
||||
)
|
||||
|
||||
// boardsServiceAPI provides a service API for other products such as Channels.
|
||||
type boardsServiceAPI struct {
|
||||
app *app.App
|
||||
}
|
||||
|
||||
func NewBoardsServiceAPI(app *BoardsApp) *boardsServiceAPI {
|
||||
return &boardsServiceAPI{
|
||||
app: app.server.App(),
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) GetTemplates(teamID string, userID string) ([]*model.Board, error) {
|
||||
return bs.app.GetTemplateBoards(teamID, userID)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) GetBoard(boardID string) (*model.Board, error) {
|
||||
return bs.app.GetBoard(boardID)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) CreateBoard(board *model.Board, userID string, addmember bool) (*model.Board, error) {
|
||||
return bs.app.CreateBoard(board, userID, addmember)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) PatchBoard(boardPatch *model.BoardPatch, boardID string, userID string) (*model.Board, error) {
|
||||
return bs.app.PatchBoard(boardPatch, boardID, userID)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) DeleteBoard(boardID string, userID string) error {
|
||||
return bs.app.DeleteBoard(boardID, userID)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) SearchBoards(searchTerm string, searchField model.BoardSearchField,
|
||||
userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
return bs.app.SearchBoardsForUser(searchTerm, searchField, userID, includePublicBoards)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) LinkBoardToChannel(boardID string, channelID string, userID string) (*model.Board, error) {
|
||||
patch := &model.BoardPatch{
|
||||
ChannelID: &channelID,
|
||||
}
|
||||
return bs.app.PatchBoard(patch, boardID, userID)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) GetCards(boardID string) ([]*model.Card, error) {
|
||||
return bs.app.GetCardsForBoard(boardID, 0, 0)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) GetCard(cardID string) (*model.Card, error) {
|
||||
return bs.app.GetCardByID(cardID)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) CreateCard(card *model.Card, boardID string, userID string) (*model.Card, error) {
|
||||
return bs.app.CreateCard(card, boardID, userID, false)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) PatchCard(cardPatch *model.CardPatch, cardID string, userID string) (*model.Card, error) {
|
||||
return bs.app.PatchCard(cardPatch, cardID, userID, false)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) DeleteCard(cardID string, userID string) error {
|
||||
return bs.app.DeleteBlock(cardID, userID)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool {
|
||||
return bs.app.HasPermissionToBoard(userID, boardID, permission)
|
||||
}
|
||||
|
||||
func (bs *boardsServiceAPI) DuplicateBoard(boardID string, userID string,
|
||||
toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
|
||||
return bs.app.DuplicateBoard(boardID, userID, toTeam, asTemplate)
|
||||
}
|
||||
|
||||
// Ensure boardsServiceAPI implements product.BoardsService interface.
|
||||
var _ product.BoardsService = (*boardsServiceAPI)(nil)
|
|
@ -1,227 +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"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-api/cluster"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (b *BoardsApp) OnCloudLimitsUpdated(limits *mm_model.ProductLimits) {
|
||||
if err := b.server.App().SetCloudLimits(limits); err != nil {
|
||||
b.logger.Error("Error setting the cloud limits for Boards", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 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,128 +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/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/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 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)
|
||||
})
|
||||
|
||||
t.Run("test boards feature flags", func(t *testing.T) {
|
||||
featureFlags := &model.FeatureFlags{
|
||||
TestFeature: "test",
|
||||
TestBoolFeature: boolTrue,
|
||||
BoardsFeatureFlags: "hello_world-myTest",
|
||||
}
|
||||
|
||||
mmConfig := baseConfig
|
||||
mmConfig.FeatureFlags = featureFlags
|
||||
|
||||
config := createBoardsConfig(*mmConfig, "", "")
|
||||
assert.Equal(t, "true", config.FeatureFlags["TestBoolFeature"])
|
||||
assert.Equal(t, "test", config.FeatureFlags["TestFeature"])
|
||||
|
||||
assert.Equal(t, "true", config.FeatureFlags["hello_world"])
|
||||
assert.Equal(t, "true", config.FeatureFlags["myTest"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
b := &BoardsApp{
|
||||
server: th.Server,
|
||||
logger: mlog.CreateConsoleTestLogger(true, mlog.LvlError),
|
||||
}
|
||||
|
||||
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,159 +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/v6/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"
|
||||
if mmconfig.FeatureFlags.BoardsProduct {
|
||||
serverRoot = baseURL + "/boards"
|
||||
}
|
||||
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 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))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -1,122 +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()
|
||||
|
||||
// handle plugin configuration settings
|
||||
enableShareBoards := false
|
||||
if mmconfig.PluginSettings.Plugins[PluginName][SharedBoardsName] == true {
|
||||
enableShareBoards = true
|
||||
}
|
||||
if mmconfig.ProductSettings.EnablePublicSharedBoards != nil {
|
||||
enableShareBoards = *mmconfig.ProductSettings.EnablePublicSharedBoards
|
||||
}
|
||||
configuration := &configuration{
|
||||
EnablePublicSharedBoards: enableShareBoards,
|
||||
}
|
||||
b.setConfiguration(configuration)
|
||||
b.server.Config().EnablePublicSharedBoards = enableShareBoards
|
||||
|
||||
// handle feature flags
|
||||
b.server.Config().FeatureFlags = parseFeatureFlags(mmconfig.FeatureFlags.ToMap())
|
||||
|
||||
// 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,124 +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/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/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
|
||||
|
||||
baseFeatureFlags := &serverModel.FeatureFlags{
|
||||
BoardsFeatureFlags: "Feature1-Feature2",
|
||||
}
|
||||
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{
|
||||
FeatureFlags: baseFeatureFlags,
|
||||
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(true, mlog.LvlError),
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
assert.Equal(t, "true", b.server.Config().FeatureFlags["Feature1"])
|
||||
assert.Equal(t, "true", b.server.Config().FeatureFlags["Feature2"])
|
||||
assert.Equal(t, "", b.server.Config().FeatureFlags["Feature3"])
|
||||
})
|
||||
}
|
||||
|
||||
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,142 +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/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/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.CreateConsoleTestLogger(true, mlog.LvlDebug)
|
||||
|
||||
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()
|
||||
|
||||
b := &BoardsApp{
|
||||
server: th.Server,
|
||||
logger: mlog.CreateConsoleTestLogger(true, mlog.LvlError),
|
||||
}
|
||||
|
||||
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/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/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/v6/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/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/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-plugin-api"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/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/v6/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&Plugin{})
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
// This file is automatically generated. Do not modify it manually.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/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": "7.10.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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func init() {
|
||||
_ = json.NewDecoder(strings.NewReader(manifestStr)).Decode(&manifest)
|
||||
}
|
|
@ -1,152 +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-plugin-api"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/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 {
|
||||
if p.API.GetConfig().FeatureFlags.BoardsProduct {
|
||||
p.API.LogError(ErrPluginNotAllowed.Error())
|
||||
return ErrPluginNotAllowed
|
||||
}
|
||||
|
||||
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) OnCloudLimitsUpdated(limits *mm_model.ProductLimits) {
|
||||
p.boardsApp.OnCloudLimitsUpdated(limits)
|
||||
}
|
||||
|
||||
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,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');
|
||||
};
|
39134
mattermost-plugin/webapp/package-lock.json
generated
39134
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.1",
|
||||
"eslint": "8.11.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.3.1",
|
||||
"jest-junit": "13.0.0",
|
||||
"jest-mock": "27.5.1",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"style-loader": "3.3.1",
|
||||
"ts-loader": "9.2.8",
|
||||
"typescript": "4.6.2",
|
||||
"webpack": "5.70.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-dev-server": "4.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.21.1",
|
||||
"glob-parent": "6.0.2",
|
||||
"marked": ">=4.0.12",
|
||||
"mattermost-redux": "5.33.1",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-intl": "^5.20.0",
|
||||
"react-redux": "^7.2.0",
|
||||
"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>
|
||||
`;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue