Compare commits

...

69 commits

Author SHA1 Message Date
Joram Wilander
de5e5cc414
Merge pull request #5039 from mattermost-community/remove-plugin-from-readme
Remove references to boards plugin from README
2024-09-27 09:56:16 -04:00
Joram Wilander
a8897be8d7 Update disclaimer at top of README 2024-08-28 15:58:17 -04:00
Joram Wilander
4342b758b3 Remove references to boards plugin from README 2024-08-28 15:54:24 -04:00
Rajat Dabade
bfaa37fc24
[MM-59253]: Remove plugin code from focalboard repo (#5027)
* refactor: updated mysql docker image version

* refactor: removed isPlugin code from webapp

* refactor: removed isFocalboardPlugin from test

* refactor: removed package-lock.json from root

* removed unnecessary component

* nit: removed comments

* reverted the mysql docker version to original

* nit

* revert setting.json changes

* Removed `mattermost-plugin` folder (#5029)

* refactor: removed mattermost-plugin folder

* reverted the mysql image version

* Removed `mattermost-plugin` from make rules (#5030)

* removed mattermost-plugin from make rules

* removed: mattermost-plugin code from the repo

* updated snapshot and fix test (#5031)

* updated snapshot and fix test

* Updated snapshot and removed unnecessary tests

* chore: minor fix ci

* refactor: updated the mac-os version supported by github actions

* reverted: mac os version

* refactor: updated mac os version and also changed docker-compose to docker compose

* removed version from docker compose as no long needed

* reverted mysql docker version

* updated mysql version

* testing

* revert testing

* refactor: added version for mysql docker compose file

* test: test commit

* updated snapshot and fix test (#5032)

* removed mattermost-plugin from make rules

* removed: mattermost-plugin code from the repo

* updated snapshot and fix test

* Updated snapshot and removed unnecessary tests

* chore: minor fix ci

* refactor: removed isplugin code from server

* final attempt

* ci: Minor ci tweaks and upgrades

---------

Co-authored-by: Antonis Stamatiou <stamatiou.antonis@gmail.com>

* linter fixes

---------

Co-authored-by: Antonis Stamatiou <stamatiou.antonis@gmail.com>
2024-08-28 22:16:15 +05:30
Rajat Dabade
1932acb628
MM-58502 Error on crossing block limit on client side (#5015)
* refactor: error on crossing block limit

* fix: linter

* chore: updated snapshot

* refactor: updated mysql/mysql-server docker version

* chore: changed blob to text.length
2024-08-07 17:03:06 +05:30
Maria A Nunez
568a5f01b6
Merge pull request #5013 from mattermost/update-version-8-0
Update to version 8.0
2024-06-10 12:59:45 -04:00
maria.nunez
94e5ff5393 Update to version 8.0 2024-06-10 12:41:40 -04:00
Rajat Dabade
466ce56a90
[Refactor]: updated dependency for webapp (#5008)
* refactor: updated dependency for webapp

* refactor: updated test case and snapshot

* refactor: updated depedencies

* refactor: updated snapshot and test case

* fix: jest test case

* refactor: nvmrc version to match it with latest mm master

* chore: upgrade the node version in github flows

* refactor: plugin unit test and cardfilter test date issue

* refactor: mock date for rhs plugin mode

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2024-06-09 10:33:09 +05:30
Rajat Dabade
c8e729b6fe
[Refactor]: updated dependency for focalboard server (#5009)
* refactor: updated dependency for focalboard server

* chore: more dependency fixes

* refactor: removed the unless code

* refactor: added ctx for login and removed unnessary code

* refactor: bump up go version

* refactor: removed the commented code

* chore: upgraded golinter version

* fix: linter issue

* refactor: removed feature flg fix golinter

* refactor: removed feature flag from code

* revert: statistic and it's function

* refactor: removed ProductLimit related code

* refactor: removed isWithinViewsLimit implementation

* refactor: moved function GetUsedCardsCount to statistics.go from cloud.go

* refactor: removed insight code board

* refactor: removed limit dialog

* refactor: updated dependencies for linux

* chore: golinter fix

* chore: updated helper test function to use newLogger

* fix: go test

* refactor: db ping attempts from config

* revert: feature in action

* revert: feature flag in action

* revert: boardsEditor setting

---------

Co-authored-by: Rajat Dabade <rajat@Rajats-MacBook-Pro.local>
2024-06-07 23:30:08 +05:30
Joram Wilander
7a31925d8a
Merge pull request #4984 from azigler/azigler-fy25-messaging
📝 docs(README.md): Update project status and contribution guidelines
2024-02-22 11:53:38 -05:00
Andrew Zigler
17a44203fe 📝 docs(README.md): remove extra heading 2024-02-20 15:09:20 -08:00
Andrew Zigler
8a7db01249 📝 docs(CONTRIBUTING.md): Update contribution guidelines due to change in project maintenance
The contribution guidelines have been updated to reflect the recent changes in the project's maintenance. As of 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. The community is encouraged to fork this repository for continued development and contributions. This change is due to Mattermost's decision to focus developer resources on improving the platform’s performance and core features. The list of contributors has been updated to 'Past maintainers'.
2024-02-20 14:57:51 -08:00
Andrew Zigler
3bec6bbcbe 📝 docs(README.md): Update project status and contribution guidelines
🆕 docs: Add new README.md in docs folder with updated disclaimer
The README.md file has been updated to reflect the current status of the project. The project is no longer maintained by Mattermost and has transitioned to being fully community supported. The contribution guidelines have been updated to encourage the community to fork this repository for continued development and contributions. A new README.md file has been added in the docs folder to provide the updated disclaimer in a more prominent location. This change is necessary to keep the community informed about the project's status and how they can contribute moving forward.
2024-02-20 14:46:41 -08:00
Carrie Warner (Mattermost)
6bd7464372
Merge pull request #4914 from mattermost/focalboard-readme-legal
Update community supported README: legal disclaimer
2023-12-19 09:30:47 -05:00
Carrie Warner (Mattermost)
793dd73496
Update README.md 2023-12-08 12:28:57 -05:00
Carrie Warner (Mattermost)
d559157d28
Update README.md 2023-12-08 12:28:50 -05:00
Carrie Warner (Mattermost)
cbc71b3af7
Merge branch 'main' into focalboard-readme-legal 2023-11-01 08:38:31 -04:00
Tomer Ben-Rachel
28ec2b361a
Enhance Readme To Explain How To Contribute Translations (#4918)
* feature/translations-contribution adding steps on how to contribute a translation

* feature/translations-contribution added fruther detail of location of json file

* feature/translations-contribution updating snapshots

* feature/translations-contribution adding step to run updating snapshot command

* feature/translations-contribution reverting snapshot changes

* feature/translations-contribution reverting snapshot changes
2023-10-31 07:53:44 -06:00
Carrie Warner (Mattermost)
3269b67102 Incorporated reviewer feedback 2023-10-25 11:51:12 -04:00
Carrie Warner (Mattermost)
c584fae8df
Update community supported README
Added legal-approved disclaimer
2023-10-25 09:41:24 -04:00
Scott Bishel
134422df4d
MM-54366 Check guest access to other members (#4871)
* check guest access to other members

* lint fix
2023-09-25 08:19:53 -06:00
Asaad Mahmood
7226ed2cbb
MM-54013- Removing some mattermost references (#4865)
* MM-54013- Removing some mattermost references

* Updating css

* Updating logo

* Removing clients banner from website

* Update footer.html

* Updating links and text

* Updating image
2023-09-25 08:18:35 -06:00
Carrie Warner (Mattermost)
277ea9facc
Transitioning Mattermost Boards > Focalboards plugin docs (#4862)
* Transitioning Boards > Focalboards plugin docs

* Update docs/focalboard-plugin-end-user-guide.md

* Update docs/get-started-with-board-templates.md

* Update docs/get-started-with-board-templates.md

* Update docs/group-filter-sort-boards.md

* Update docs/group-filter-sort-boards.md

* Update docs/share-collaborate.md

* Update docs/import-export-backup-data.md

* Update docs/import-export-backup-data.md

* Fix MD table fomatting

* Added data collection content

* Added Focalboard import notes
2023-09-14 11:35:28 -06:00
Scott Bishel
a216e43fab
Merge pull request #4870 from esethna/patch-2
Update README.md
2023-09-11 17:48:16 -06:00
Eric Sethna
556a9f80fb
Update README.md
Clarify community support and link to forum post
2023-09-11 11:29:25 -07:00
Scott Bishel
d7cb5f8bd2
Merge pull request #4855 from esethna/esethna-patch-1
Update README.md
2023-08-17 11:16:05 -06:00
Eric Sethna
f6036cd662
Update README.md 2023-08-14 15:28:45 -07:00
Eric Sethna
a0639e7a71
Update README.md 2023-08-14 15:26:02 -07:00
Scott Bishel
4038d8471e
Merge pull request #4852 from mattermost/MM-41813_fix-font-urls
MM-41813 Fix path to fonts loaded by plugin
2023-08-10 12:07:44 -06:00
Harrison Healey
dd3be3e3ed MM-41813 Fix path to fonts loaded by plugin 2023-08-10 13:01:51 -04:00
Elias Nahum
3625c53527
Sanitize user following config for ShowFullName and ShowEmailAddress (#4820) 2023-07-26 08:42:22 -04:00
Miguel de la Cruz
257cc5f1fd
Adds a database migration to restore the fileinfos that are deleted (#4815) 2023-07-21 19:42:29 +02:00
Scott Bishel
e6b67af8b1
update version v7.12.0 (#4814) 2023-07-21 18:57:10 +02:00
Scott Bishel
9901557d99
update minimum dependencies (#4811)
* update minimum dependencies

* update minimum dependencies
2023-07-21 10:27:13 +02:00
Miguel de la Cruz
e0dbb380a3
Adds the parent ID filter when fetching child blocks to extract fileId and attachmentId (#4802) 2023-07-20 09:50:25 +02:00
Scott Bishel
a745c29cb1
update version to v7.11.1 (#4797) 2023-07-10 09:46:55 -06:00
Eric Sethna
8cf0ec3a61
Update README.md (#4794) 2023-07-05 08:39:26 -06:00
Milo Ivir
4e7416358e Translated using Weblate (Croatian)
Currently translated at 100.0% (460 of 460 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/hr/
2023-06-30 13:47:27 +03:00
Matthew Williams
1388d41a9b Translated using Weblate (English (Australia))
Currently translated at 100.0% (460 of 460 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/en_AU/
2023-06-30 13:47:27 +03:00
Kaya Zeren
236099f40e Translated using Weblate (Turkish)
Currently translated at 100.0% (460 of 460 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/tr/
2023-06-30 13:47:27 +03:00
Andrius Balsevičius
1381babf37 Added translation using Weblate (Lithuanian) 2023-06-30 13:47:27 +03:00
master7
77b8a8e14d Translated using Weblate (Polish)
Currently translated at 100.0% (460 of 460 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/pl/
2023-06-30 13:47:27 +03:00
leonambeez
4a6cab9233 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (460 of 460 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/pt_BR/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (450 of 450 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/pt_BR/
2023-06-30 13:47:27 +03:00
jprusch
56cc1b84c1 Translated using Weblate (German)
Currently translated at 100.0% (460 of 460 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/de/

Translated using Weblate (German)

Currently translated at 100.0% (450 of 450 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/de/
2023-06-30 13:47:27 +03:00
Hosted Weblate
d69c5bee0e Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/
2023-06-30 13:47:27 +03:00
Felipe Nogueira
5bea58c2c3 Translated using Weblate (Portuguese)
Currently translated at 20.4% (94 of 460 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/pt/

Translated using Weblate (Portuguese)

Currently translated at 11.5% (52 of 450 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/pt/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.1% (446 of 450 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/pt_BR/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.4% (434 of 450 strings)

Translation: Focalboard/webapp
Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/pt_BR/
2023-06-30 13:47:27 +03:00
Miguel de la Cruz
5dfd402e26
Unify and enhance block validation (#4790)
* Adds limit check for block titles

* Adds limit check for the aggregation of the fields

* Fix linter

* Adds tests

* Fix err check method order
2023-06-26 13:37:37 -06:00
Miguel de la Cruz
625526c3e7
Updates import to read with a scanner (#4788)
* Updates import to read with a scanner

* Fix linter
2023-06-22 11:31:08 +02:00
Scott Bishel
343b7bdc4b
check 'path' for 'empty' as well (#4785) 2023-06-20 11:36:13 +02:00
Miguel de la Cruz
c4454cac52
Update ubuntu version to use latest LTS (#4771) 2023-06-09 10:44:15 -06:00
Miguel de la Cruz
3b7954872e
Upgrade Scorecards action version (#4760) 2023-06-08 10:33:59 -06:00
Christopher Speller
d95d100d8c
Fix boards share dialog (#4761) 2023-06-07 16:39:44 -06:00
zhsj
a76ef9c168
Fix Chinese lang code (#4415)
zh-cn is for Simplified Chinese, and zh-tw is for Traditional Chinese.
And remove invalid zh-tx code.

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2023-06-01 10:37:43 -06:00
Miguel de la Cruz
e4630d1a84
Remove product references (#4744) 2023-05-31 15:52:44 -06:00
Christopher Speller
c3b1c82b1a
Fix cards not deleting properly. (#4746)
* Fix cards not deleting properly.

* Review feedback

* Test and lint fixes.

* Fix tests.
2023-05-31 08:26:19 -06:00
Scott Bishel
888c169910
Backport fixes for import/export attachments and fixes (#4741)
* Backport fixes for import/export attachments and fixes

* fix bad merge

* lint fixes

* Update server/app/boards.go

Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>

---------

Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
2023-05-22 10:31:24 -06:00
Scott Bishel
b7d94a8fe2
update to version v7.11.0 (#4742) 2023-05-22 13:11:06 +02:00
Scott Bishel
d10e4070ba
Fix public boards setting not applying properly (#4739) 2023-05-11 08:06:06 -06:00
Scott Bishel
0af70a0a4f
fix issue with card id being valid block but not a card (#4729) 2023-05-08 18:40:14 +02:00
Scott Bishel
252f2138ca
fix so dropping on card has latest cards objects (#4725)
Co-authored-by: Mattermost Build <build@mattermost.com>
2023-04-28 19:03:27 +02:00
Scott Bishel
c9acca6acb
check permissions for personal server (#4726)
* check permissions for personal server

* remove import
2023-04-28 18:56:32 +02:00
Miguel de la Cruz
799b8ead1c
Adds a retry count to the websocket re-open flow (#4722)
* Adds a retry count to the websocket re-open flow

* Fix linter
2023-04-26 22:41:27 +02:00
Miguel de la Cruz
acc9750d97
Add table_schema filter when checking for migrations table shape (#4717) 2023-04-26 22:01:47 +02:00
Winson Wu
7fd923b213
Merge pull request #4720 from mattermost/wuwinson-patch-2
Update README.md
2023-04-26 10:47:30 -04:00
Winson Wu
615b6ceee7
Update README.md
Updated announcement to make it more obvious.
2023-04-24 17:01:21 -04:00
Caleb Roseland
dc0e664df9
Backport: MM-51842: fix value-change detection in number properties (#4694) 2023-04-04 14:29:49 -05:00
Doug Lauder
2b9eec1bec
Avoid panic in file request handler on error (#4693)
Co-authored-by: Mattermost Build <build@mattermost.com>
2023-04-03 17:43:58 -04:00
Winson Wu
1d22070454
Merge pull request #4688 from mattermost/wuwinson-patch-2
Update README.md for Focalboard
2023-03-30 17:20:47 -04:00
Winson Wu
a015aa2cf2
Update README.md for Focalboard
Added details around sunsetting Personal Editions as well as changes to the plugin.
2023-03-30 16:03:45 -04:00
455 changed files with 4951 additions and 66885 deletions

View file

@ -5,7 +5,6 @@ node_modules
.github/
mac/
win-wpf/
mattermost-plugin/
website/
linux/
go.work

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -37,8 +37,7 @@
"linux/dist/**": true,
},
"editor.codeActionsOnSave": {
// "source.organizeImports": true,
"source.fixAll.eslint": true,
"source.fixAll.eslint": "explicit"
},
"[typescriptreact]": {
"editor.codeActionsOnSave": {

View file

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

View file

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

@ -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
![CI Status](https://github.com/mattermost/focalboard/actions/workflows/ci.yml/badge.svg)
![CodeQL](https://github.com/mattermost/focalboard/actions/workflows/codeql-analysis.yml/badge.svg)
![Dev Release](https://github.com/mattermost/focalboard/actions/workflows/dev-release.yml/badge.svg)
![Prod Release](https://github.com/mattermost/focalboard/actions/workflows/prod-release.yml/badge.svg)
<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:
![Focalboard](website/site/static/img/hero.jpg)
[![Focalboard](website/site/static/img/hero.jpg)](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)

View file

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

View file

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

View file

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

View file

@ -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
View 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 platforms 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
View 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 isnt missing).
- The sum of estimated developer days for features (to make sure your team isnt 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
View 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 boards 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**.

View file

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

View file

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

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

View 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 templates 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**.

View 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.
- **Doesnt 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.

View 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, thats 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 youd 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 youd 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.

View file

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

View 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
View 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 youre 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).

View 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**.

View 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
View 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 users 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 members role, open the **Share** dialog, select the role dropdown next to the users 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 users 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 channels 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.

View 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 youd 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 propertys 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 dont have a custom **Date** property, theyll be sorted and displayed by the card creation date (default). These cards cant be moved around the board until a custom **Date** property is added.
If your cards do have a **Date** property and youre 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
View 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 youre 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. Youll 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**. Youll 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 dont 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 its created, and isnt 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.

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
server/manifest.go linguist-generated=true

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
bin

View file

@ -1 +0,0 @@
# Include custom targets and environment variables here

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
a

View file

@ -1 +0,0 @@
c

View file

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

View file

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

View file

@ -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."
}]
}
}

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
Hello from the static files public folder for the com.mattermost.plugin-starter-template plugin!

View file

@ -1,2 +0,0 @@
coverage.txt
dist

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 != ""
}

View file

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

View file

@ -1,9 +0,0 @@
package main
import (
"github.com/mattermost/mattermost-server/v6/plugin"
)
func main() {
plugin.ClientMain(&Plugin{})
}

View file

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

View file

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

View file

@ -1 +0,0 @@
node_modules/

View file

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

View file

@ -1,3 +0,0 @@
.eslintcache
junit.xml
node_modules

View file

@ -1 +0,0 @@
save-exact=true

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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