You've already forked focalboard
mirror of
https://github.com/mattermost/focalboard.git
synced 2025-07-15 23:54:29 +02:00
Migrate webapp global state to redux (#737)
* Migrating workspace tree to redux * More changes for use the redux store for boads and views * Taking into account the templates on websocket event updates * Fixing bug on boardTree maintenance * Websocket client now connects once and subscribe/desubscribe on the fly * Including usage of the new websocket client * More work around migrating to redux * WIP * WIP * WIP * WIP * WIP * WIP * Fixing some things * WIP * WIP * Another small fix * Restoring filtering, sorting and grouping * Fixing some other bugs * Add search text reducer * Fixing another drag and drop problem * Improve store names and api * Fixing small bgus * Some small fixes * fixing login * Fixing register page * Some other improvements * Removing unneeded old files * Removing the need of userCache * Fixing comments and fixing content ordering * Fixing sort * Fixing some TODOs * Fixing tests * Fixing snapshot * Fixing cypress tests * Fix eslint * Fixing server tests * Updating the add cards actions * Fixing some tiny navigation problems * Mocking the api calls to pass the tests * Migrating a new test to redux * Adding the card right after the insert of the block (not wait for ws event) * Showing the ws disconnect banner only after 5 seconds of disconnection * Fixing share view * Fix eslint * Fixing problem with sort/groupby modifications * Fixing some details on redirections and templates creation * Fixing small bugs around undo * Fix update properties on click outside the dialog * Improving the column resize look and feel * Removing the class based objects from the store (now they are all plain objects * Fix eslint * Fixing tests * Removing unneeded code
This commit is contained in:
@ -226,6 +226,7 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
parentID := query.Get("parent_id")
|
||||
blockType := query.Get("type")
|
||||
all := query.Get("all")
|
||||
container, err := a.getContainer(r)
|
||||
if err != nil {
|
||||
a.noContainerErrorResponse(w, r.URL.Path, err)
|
||||
@ -236,12 +237,22 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("parentID", parentID)
|
||||
auditRec.AddMeta("blockType", blockType)
|
||||
auditRec.AddMeta("all", all)
|
||||
|
||||
blocks, err := a.app.GetBlocks(*container, parentID, blockType)
|
||||
var blocks []model.Block
|
||||
if all != "" {
|
||||
blocks, err = a.app.GetAllBlocks(*container)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
blocks, err = a.app.GetBlocks(*container, parentID, blockType)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
a.logger.Debug("GetBlocks",
|
||||
mlog.String("parentID", parentID),
|
||||
|
@ -6,11 +6,11 @@ import (
|
||||
)
|
||||
|
||||
func (a *App) GetBlocks(c store.Container, parentID string, blockType string) ([]model.Block, error) {
|
||||
if len(blockType) > 0 && len(parentID) > 0 {
|
||||
if blockType != "" && parentID != "" {
|
||||
return a.store.GetBlocksWithParentAndType(c, parentID, blockType)
|
||||
}
|
||||
|
||||
if len(blockType) > 0 {
|
||||
if blockType != "" {
|
||||
return a.store.GetBlocksWithType(c, blockType)
|
||||
}
|
||||
|
||||
|
297
webapp/package-lock.json
generated
297
webapp/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.8.1",
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"easymde": "^2.15.0",
|
||||
"emoji-mart": "^3.0.1",
|
||||
"imagemin-gifsicle": "^7.0.0",
|
||||
"imagemin-mozjpeg": "^9.0.0",
|
||||
@ -32,7 +33,7 @@
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^4.3.0",
|
||||
"react-simplemde-editor": "^4.1.3"
|
||||
"react-simplemde-editor": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^3.2.0",
|
||||
@ -1480,18 +1481,6 @@
|
||||
"redux": "^4.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17.0.0",
|
||||
"react-redux": "^7.2.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@samverschueren/stream-to-observable": {
|
||||
@ -1630,9 +1619,6 @@
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@testing-library/dom": ">=7.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
@ -1683,9 +1669,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "0.0.88",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.88.tgz",
|
||||
"integrity": "sha512-FI9BvlO+SIEmKoIdrS9uphasiHJ/JbeUsAbVTdklBOcmnr/bQpJ6QaCw540FY98LGdDNl1Nyn7erGxo0eD2gOg==",
|
||||
"version": "0.0.109",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.109.tgz",
|
||||
"integrity": "sha512-cSdiHeeLjvGn649lRTNeYrVCDOgDrtP+bDDSFDd1TF+i0jKGPDRozno2NOJ9lTniso+taiv4kiVS8dgM8Jm5lg==",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
@ -1832,10 +1818,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/marked": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.0.tgz",
|
||||
"integrity": "sha512-kSOVa3R6HJvFdd3UIbTYvrSBTPHjXhNErh7/8oSCKOwqdOkk4Oj8N77n+f6dsgd1jW3j3SU5EhnmRxPhNKOmtQ==",
|
||||
"dev": true
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.4.tgz",
|
||||
"integrity": "sha512-L9VRSe0Id8xbPL99mUo/4aKgD7ZoRwFZqUQScNKHi2pFjF9ZYSMNShUHD6VlMT6J/prQq0T1mxuU25m3R7dFzg=="
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "3.0.4",
|
||||
@ -1991,9 +1976,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.3.tgz",
|
||||
"integrity": "sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==",
|
||||
"version": "0.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz",
|
||||
"integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
@ -4145,9 +4130,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "5.59.4",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.59.4.tgz",
|
||||
"integrity": "sha512-achw5JBgx8QPcACDDn+EUUXmCYzx/zxEtOGXyjvLEvYY8GleUrnfm5D+Zb+UjShHggXKDT9AXrbkBZX6a0YSQg=="
|
||||
"version": "5.62.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.2.tgz",
|
||||
"integrity": "sha512-tVFMUa4J3Q8JUd1KL9yQzQB0/BJt7ZYZujZmTPgo/54Lpuq3ez4C8x/ATUY/wv7b7X3AUq8o3Xd+2C5ZrCGWHw=="
|
||||
},
|
||||
"node_modules/codemirror-spell-checker": {
|
||||
"version": "1.1.2",
|
||||
@ -4374,12 +4359,7 @@
|
||||
"version": "3.12.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.12.1.tgz",
|
||||
"integrity": "sha512-Ne9DKPHTObRuB09Dru5AjwKjY4cJHVGu+y5f7coGn1E9Grkc3p2iBwE9AI/nJzsE29mQF7oq+mhYYRqOMFN1Bw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-js-pure": {
|
||||
"version": "3.9.1",
|
||||
@ -4755,7 +4735,6 @@
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cwebp-bin/-/cwebp-bin-5.1.0.tgz",
|
||||
"integrity": "sha512-BsPKStaNr98zfxwejWWLIGELbPERULJoD2v5ijvpeutSAGsegX7gmABgnkRK7MUucCPROXXfaPqkLAwI509JzA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bin-build": "^3.0.0",
|
||||
"bin-wrapper": "^4.0.1",
|
||||
@ -5521,13 +5500,26 @@
|
||||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
|
||||
},
|
||||
"node_modules/easymde": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.14.0.tgz",
|
||||
"integrity": "sha512-yQh3EF1amknaxDhXE1L28kwknREU8S19o01ki0t6Q8ThECCipXTOM3E/LL32Ia5D3AsCBRbC1/fT5tpLniVGuw==",
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.15.0.tgz",
|
||||
"integrity": "sha512-9jMRIVvKt1d0UjRN45yotUYECAM4xvw0TTAQw8sYDONP++keWJVnd8Xrn+V+vQEN/v9/X0SWEoo1rFSgCooGpw==",
|
||||
"dependencies": {
|
||||
"codemirror": "^5.59.2",
|
||||
"@types/codemirror": "0.0.109",
|
||||
"@types/marked": "^2.0.2",
|
||||
"codemirror": "^5.61.0",
|
||||
"codemirror-spell-checker": "1.1.2",
|
||||
"marked": "^2.0.0"
|
||||
"marked": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/easymde/node_modules/marked": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz",
|
||||
"integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==",
|
||||
"bin": {
|
||||
"marked": "bin/marked"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
@ -7051,10 +7043,6 @@
|
||||
"integrity": "sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==",
|
||||
"bin": {
|
||||
"xml2js": "cli.js"
|
||||
},
|
||||
"funding": {
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/naturalintelligence"
|
||||
}
|
||||
},
|
||||
"node_modules/fastest-levenshtein": {
|
||||
@ -7108,18 +7096,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "charity",
|
||||
"url": "https://www.justgiving.com/refugee-support-europe"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"node-fetch": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"node-fetch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-mock-jest": {
|
||||
@ -7132,18 +7108,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "charity",
|
||||
"url": "https://www.justgiving.com/refugee-support-europe"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"node-fetch": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"node-fetch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-mock/node_modules/path-to-regexp": {
|
||||
@ -7544,7 +7508,6 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-5.2.0.tgz",
|
||||
"integrity": "sha512-vOIS3j0XoTCxq9pkGj43gEix82RkI5FveNgaFZutjbaui/HH+4fR8Y56dwXDuxYo8hR4xOo6/j2h1WHoQW6XLw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bin-build": "^3.0.0",
|
||||
"bin-wrapper": "^4.0.0",
|
||||
@ -7556,9 +7519,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/imagemin/gisicle-bin?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/gifsicle/node_modules/cross-spawn": {
|
||||
@ -7591,9 +7551,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/gifsicle/node_modules/get-stream": {
|
||||
@ -7602,9 +7559,6 @@
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gifsicle/node_modules/human-signals": {
|
||||
@ -8237,17 +8191,15 @@
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"imagemin": "^7.0.1",
|
||||
"loader-utils": "^2.0.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"schema-utils": "^2.7.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"imagemin-gifsicle": "^7.0.0",
|
||||
"imagemin-mozjpeg": "^9.0.0",
|
||||
"imagemin-optipng": "^8.0.0",
|
||||
"imagemin-pngquant": "^9.0.1",
|
||||
"imagemin-svgo": "^8.0.0",
|
||||
"imagemin-webp": "^6.0.0"
|
||||
"imagemin-webp": "^6.0.0",
|
||||
"loader-utils": "^2.0.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"schema-utils": "^2.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/image-webpack-loader/node_modules/loader-utils": {
|
||||
@ -8276,10 +8228,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/imagemin": {
|
||||
@ -8311,9 +8259,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/imagemin/imagemin-gifsicle?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/imagemin-mozjpeg": {
|
||||
@ -8359,9 +8304,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/imagemin-mozjpeg/node_modules/get-stream": {
|
||||
@ -8373,9 +8315,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/imagemin-mozjpeg/node_modules/is-stream": {
|
||||
@ -8496,9 +8435,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/imagemin-pngquant/node_modules/get-stream": {
|
||||
@ -8510,9 +8446,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/imagemin-pngquant/node_modules/is-stream": {
|
||||
@ -8585,9 +8518,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/imagemin-svgo?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/imagemin-webp": {
|
||||
@ -8625,11 +8555,7 @@
|
||||
"node_modules/immer": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.3.tgz",
|
||||
"integrity": "sha512-mONgeNSMuyjIe0lkQPa9YhdmTv8P19IeHV0biYhcXhbd5dhdB9HSK93zBpyKjp6wersSUgT5QyU0skmejUVP2A==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
"integrity": "sha512-mONgeNSMuyjIe0lkQPa9YhdmTv8P19IeHV0biYhcXhbd5dhdB9HSK93zBpyKjp6wersSUgT5QyU0skmejUVP2A=="
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
@ -8993,9 +8919,6 @@
|
||||
"integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
@ -9101,10 +9024,7 @@
|
||||
"node_modules/is-object": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA=="
|
||||
},
|
||||
"node_modules/is-observable": {
|
||||
"version": "1.1.0",
|
||||
@ -9218,9 +9138,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-symbol": {
|
||||
@ -11392,7 +11309,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mozjpeg/-/mozjpeg-7.0.0.tgz",
|
||||
"integrity": "sha512-mH7atSbIusVTO3A4H43sEdmveN3aWn54k6V0edefzCEvOsTrbjg5murY2TsNznaztWnIgaRbWxeLVp4IgKdedQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bin-build": "^3.0.0",
|
||||
"bin-wrapper": "^4.0.0",
|
||||
@ -11849,9 +11765,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object.pick": {
|
||||
@ -11920,7 +11833,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/optipng-bin/-/optipng-bin-7.0.0.tgz",
|
||||
"integrity": "sha512-mesUAwfedu5p9gRQwlYgD6Svw5IH3VUIWDJj/9cNpP3yFNbbEVqkTMWYhrIEn/cxmbGA3LpZrdoV2Yl8OfmnIA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bin-build": "^3.0.0",
|
||||
"bin-wrapper": "^4.0.0",
|
||||
@ -11965,9 +11877,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ow/node_modules/type-fest": {
|
||||
@ -11976,9 +11885,6 @@
|
||||
"integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
@ -12088,9 +11994,6 @@
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-reduce": {
|
||||
@ -12367,7 +12270,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngquant-bin/-/pngquant-bin-6.0.0.tgz",
|
||||
"integrity": "sha512-oXWAS9MQ9iiDAJRdAZ9KO1mC5UwhzKkJsmetiu0iqIjJuW7JsuLhmc4JdRm7uJkIWRzIAou/Vq2VcjfJwz30Ow==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bin-build": "^3.0.0",
|
||||
"bin-wrapper": "^4.0.1",
|
||||
@ -12411,9 +12313,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pngquant-bin/node_modules/get-stream": {
|
||||
@ -12425,9 +12324,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pngquant-bin/node_modules/is-stream": {
|
||||
@ -12511,10 +12407,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-modules": {
|
||||
@ -12986,17 +12878,6 @@
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.13.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.3 || ^17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
@ -13045,20 +12926,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-simplemde-editor": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-simplemde-editor/-/react-simplemde-editor-4.1.3.tgz",
|
||||
"integrity": "sha512-MJ3SDYfYsNnEcmLzQCqPERDaarllwbxR06oyOQ+jJn0517HYIcQCfFoOIT4uewRY14g05n/Ux1Nka88Bocrdcg==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-simplemde-editor/-/react-simplemde-editor-5.0.1.tgz",
|
||||
"integrity": "sha512-qFWxcBo9P6B8vfdPDyuaOlfwM9+ExLXplWAR2r/Drc1wGftQbwCDlSzgVlp8R0vsIBFdm+OtR8fxk4mJJKn0OA==",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "^0.0.88",
|
||||
"@types/marked": "^0.7.4",
|
||||
"easymde": "^2.10.1"
|
||||
"@types/codemirror": "0.0.109",
|
||||
"@types/marked": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-simplemde-editor/node_modules/@types/marked": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.7.4.tgz",
|
||||
"integrity": "sha512-fdg0NO4qpuHWtZk6dASgsrBggY+8N4dWthl1bAQG9ceKUNKFjqpHaDKCAhRUI6y8vavG7hLSJ4YBwJtZyZEXqw=="
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||
@ -14377,6 +14252,11 @@
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"sshpk-conv": "bin/sshpk-conv",
|
||||
"sshpk-sign": "bin/sshpk-sign",
|
||||
"sshpk-verify": "bin/sshpk-verify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -15649,20 +15529,6 @@
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
@ -15848,9 +15714,6 @@
|
||||
"es-abstract": "^1.17.2",
|
||||
"has-symbols": "^1.0.1",
|
||||
"object.getownpropertydescriptors": "^2.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/util/node_modules/inherits": {
|
||||
@ -17969,9 +17832,9 @@
|
||||
}
|
||||
},
|
||||
"@types/codemirror": {
|
||||
"version": "0.0.88",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.88.tgz",
|
||||
"integrity": "sha512-FI9BvlO+SIEmKoIdrS9uphasiHJ/JbeUsAbVTdklBOcmnr/bQpJ6QaCw540FY98LGdDNl1Nyn7erGxo0eD2gOg==",
|
||||
"version": "0.0.109",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.109.tgz",
|
||||
"integrity": "sha512-cSdiHeeLjvGn649lRTNeYrVCDOgDrtP+bDDSFDd1TF+i0jKGPDRozno2NOJ9lTniso+taiv4kiVS8dgM8Jm5lg==",
|
||||
"requires": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
@ -18118,10 +17981,9 @@
|
||||
}
|
||||
},
|
||||
"@types/marked": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.0.tgz",
|
||||
"integrity": "sha512-kSOVa3R6HJvFdd3UIbTYvrSBTPHjXhNErh7/8oSCKOwqdOkk4Oj8N77n+f6dsgd1jW3j3SU5EhnmRxPhNKOmtQ==",
|
||||
"dev": true
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.4.tgz",
|
||||
"integrity": "sha512-L9VRSe0Id8xbPL99mUo/4aKgD7ZoRwFZqUQScNKHi2pFjF9ZYSMNShUHD6VlMT6J/prQq0T1mxuU25m3R7dFzg=="
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.4",
|
||||
@ -18277,9 +18139,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/tern": {
|
||||
"version": "0.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.3.tgz",
|
||||
"integrity": "sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==",
|
||||
"version": "0.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz",
|
||||
"integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==",
|
||||
"requires": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
@ -20132,9 +19994,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.59.4",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.59.4.tgz",
|
||||
"integrity": "sha512-achw5JBgx8QPcACDDn+EUUXmCYzx/zxEtOGXyjvLEvYY8GleUrnfm5D+Zb+UjShHggXKDT9AXrbkBZX6a0YSQg=="
|
||||
"version": "5.62.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.2.tgz",
|
||||
"integrity": "sha512-tVFMUa4J3Q8JUd1KL9yQzQB0/BJt7ZYZujZmTPgo/54Lpuq3ez4C8x/ATUY/wv7b7X3AUq8o3Xd+2C5ZrCGWHw=="
|
||||
},
|
||||
"codemirror-spell-checker": {
|
||||
"version": "1.1.2",
|
||||
@ -21273,13 +21135,22 @@
|
||||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
|
||||
},
|
||||
"easymde": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.14.0.tgz",
|
||||
"integrity": "sha512-yQh3EF1amknaxDhXE1L28kwknREU8S19o01ki0t6Q8ThECCipXTOM3E/LL32Ia5D3AsCBRbC1/fT5tpLniVGuw==",
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.15.0.tgz",
|
||||
"integrity": "sha512-9jMRIVvKt1d0UjRN45yotUYECAM4xvw0TTAQw8sYDONP++keWJVnd8Xrn+V+vQEN/v9/X0SWEoo1rFSgCooGpw==",
|
||||
"requires": {
|
||||
"codemirror": "^5.59.2",
|
||||
"@types/codemirror": "0.0.109",
|
||||
"@types/marked": "^2.0.2",
|
||||
"codemirror": "^5.61.0",
|
||||
"codemirror-spell-checker": "1.1.2",
|
||||
"marked": "^2.0.0"
|
||||
"marked": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"marked": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz",
|
||||
"integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
@ -27286,20 +27157,12 @@
|
||||
}
|
||||
},
|
||||
"react-simplemde-editor": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-simplemde-editor/-/react-simplemde-editor-4.1.3.tgz",
|
||||
"integrity": "sha512-MJ3SDYfYsNnEcmLzQCqPERDaarllwbxR06oyOQ+jJn0517HYIcQCfFoOIT4uewRY14g05n/Ux1Nka88Bocrdcg==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-simplemde-editor/-/react-simplemde-editor-5.0.1.tgz",
|
||||
"integrity": "sha512-qFWxcBo9P6B8vfdPDyuaOlfwM9+ExLXplWAR2r/Drc1wGftQbwCDlSzgVlp8R0vsIBFdm+OtR8fxk4mJJKn0OA==",
|
||||
"requires": {
|
||||
"@types/codemirror": "^0.0.88",
|
||||
"@types/marked": "^0.7.4",
|
||||
"easymde": "^2.10.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/marked": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.7.4.tgz",
|
||||
"integrity": "sha512-fdg0NO4qpuHWtZk6dASgsrBggY+8N4dWthl1bAQG9ceKUNKFjqpHaDKCAhRUI6y8vavG7hLSJ4YBwJtZyZEXqw=="
|
||||
}
|
||||
"@types/codemirror": "0.0.109",
|
||||
"@types/marked": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
|
@ -25,6 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"easymde": "^2.15.0",
|
||||
"emoji-mart": "^3.0.1",
|
||||
"imagemin-gifsicle": "^7.0.0",
|
||||
"imagemin-mozjpeg": "^9.0.0",
|
||||
@ -48,7 +49,7 @@
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^4.3.0",
|
||||
"react-simplemde-editor": "^4.1.3"
|
||||
"react-simplemde-editor": "^5.0.1"
|
||||
},
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import React, {useEffect} from 'react'
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Redirect,
|
||||
@ -20,28 +20,25 @@ import DashboardPage from './pages/dashboardPage'
|
||||
import ErrorPage from './pages/errorPage'
|
||||
import LoginPage from './pages/loginPage'
|
||||
import RegisterPage from './pages/registerPage'
|
||||
import {IUser} from './user'
|
||||
import {Utils} from './utils'
|
||||
import wsClient from './wsclient'
|
||||
import {importNativeAppSettings} from './nativeApp'
|
||||
import {fetchCurrentUser, getCurrentUser} from './store/currentUser'
|
||||
import {fetchMe, getLoggedIn} from './store/users'
|
||||
import {getLanguage, fetchLanguage} from './store/language'
|
||||
import {setGlobalError, getGlobalError} from './store/globalError'
|
||||
import {useAppSelector, useAppDispatch} from './store/hooks'
|
||||
|
||||
const App = React.memo((): JSX.Element => {
|
||||
importNativeAppSettings()
|
||||
|
||||
const language = useAppSelector<string>(getLanguage)
|
||||
|
||||
const user = useAppSelector<IUser|null>(getCurrentUser)
|
||||
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
|
||||
const globalError = useAppSelector<string>(getGlobalError)
|
||||
const dispatch = useAppDispatch()
|
||||
const [initialLoad, setInitialLoad] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchLanguage())
|
||||
dispatch(fetchCurrentUser()).then(() => {
|
||||
setInitialLoad(true)
|
||||
})
|
||||
dispatch(fetchMe())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@ -51,6 +48,12 @@ const App = React.memo((): JSX.Element => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
let globalErrorRedirect = null
|
||||
if (globalError) {
|
||||
globalErrorRedirect = <Route path='/*'><Redirect to={`/error?id=${globalError}`}/></Route>
|
||||
setTimeout(() => dispatch(setGlobalError('')), 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={language.split(/[_]/)[0]}
|
||||
@ -62,6 +65,7 @@ const App = React.memo((): JSX.Element => {
|
||||
<div id='frame'>
|
||||
<div id='main'>
|
||||
<Switch>
|
||||
{globalErrorRedirect}
|
||||
<Route path='/error'>
|
||||
<ErrorPage/>
|
||||
</Route>
|
||||
@ -78,8 +82,8 @@ const App = React.memo((): JSX.Element => {
|
||||
<BoardPage readonly={true}/>
|
||||
</Route>
|
||||
<Route path='/board/:boardId?/:viewId?'>
|
||||
{initialLoad && !user && <Redirect to='/login'/>}
|
||||
<BoardPage/>
|
||||
{loggedIn === false && <Redirect to='/login'/>}
|
||||
{loggedIn === true && <BoardPage/>}
|
||||
</Route>
|
||||
<Route path='/workspace/:workspaceId/shared/:boardId?/:viewId?'>
|
||||
<BoardPage readonly={true}/>
|
||||
@ -87,17 +91,19 @@ const App = React.memo((): JSX.Element => {
|
||||
<Route
|
||||
path='/workspace/:workspaceId/:boardId?/:viewId?'
|
||||
render={({match}) => {
|
||||
if (initialLoad && !user) {
|
||||
if (loggedIn === false) {
|
||||
let redirectUrl = '/' + Utils.buildURL(`/workspace/${match.params.workspaceId}/`)
|
||||
if (redirectUrl.indexOf('//') === 0) {
|
||||
redirectUrl = redirectUrl.slice(1)
|
||||
}
|
||||
const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}`
|
||||
return <Redirect to={loginUrl}/>
|
||||
}
|
||||
} else if (loggedIn === true) {
|
||||
return (
|
||||
<BoardPage/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
@ -107,8 +113,8 @@ const App = React.memo((): JSX.Element => {
|
||||
<DashboardPage/>
|
||||
</Route>
|
||||
<Route path='/:boardId?/:viewId?'>
|
||||
{initialLoad && !user && <Redirect to='/login'/>}
|
||||
<BoardPage/>
|
||||
{loggedIn === false && <Redirect to='/login'/>}
|
||||
{loggedIn === true && <BoardPage/>}
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
|
@ -1,15 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {ArchiveUtils, IArchiveHeader, IArchiveLine, IBlockArchiveLine} from './blocks/archive'
|
||||
import {IBlock} from './blocks/block'
|
||||
import {ArchiveUtils, ArchiveHeader, ArchiveLine, BlockArchiveLine} from './blocks/archive'
|
||||
import {Block} from './blocks/block'
|
||||
import {Board} from './blocks/board'
|
||||
import {LineReader} from './lineReader'
|
||||
import mutator from './mutator'
|
||||
import {Utils} from './utils'
|
||||
import {BoardTree} from './viewModel/boardTree'
|
||||
|
||||
class Archiver {
|
||||
static async exportBoardArchive(boardTree: BoardTree): Promise<void> {
|
||||
const blocks = await mutator.exportArchive(boardTree.board.id)
|
||||
static async exportBoardArchive(board: Board): Promise<void> {
|
||||
const blocks = await mutator.exportArchive(board.id)
|
||||
this.exportArchive(blocks)
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ class Archiver {
|
||||
this.exportArchive(blocks)
|
||||
}
|
||||
|
||||
private static exportArchive(blocks: readonly IBlock[]): void {
|
||||
private static exportArchive(blocks: readonly Block[]): void {
|
||||
const content = ArchiveUtils.buildBlockArchive(blocks)
|
||||
|
||||
const date = new Date()
|
||||
@ -45,7 +45,7 @@ class Archiver {
|
||||
private static async importBlocksFromFile(file: File): Promise<void> {
|
||||
let blockCount = 0
|
||||
const maxBlocksPerImport = 1000
|
||||
let blocks: IBlock[] = []
|
||||
let blocks: Block[] = []
|
||||
|
||||
let isFirstLine = true
|
||||
return new Promise<void>((resolve) => {
|
||||
@ -62,20 +62,20 @@ class Archiver {
|
||||
|
||||
if (isFirstLine) {
|
||||
isFirstLine = false
|
||||
const header = JSON.parse(line) as IArchiveHeader
|
||||
const header = JSON.parse(line) as ArchiveHeader
|
||||
if (header.date && header.version >= 1) {
|
||||
const date = new Date(header.date)
|
||||
Utils.log(`Import archive, version: ${header.version}, date/time: ${date.toLocaleString()}.`)
|
||||
}
|
||||
} else {
|
||||
const row = JSON.parse(line) as IArchiveLine
|
||||
const row = JSON.parse(line) as ArchiveLine
|
||||
if (!row || !row.type || !row.data) {
|
||||
Utils.logError('importFullArchive ERROR parsing line')
|
||||
return
|
||||
}
|
||||
switch (row.type) {
|
||||
case 'block': {
|
||||
const blockLine = row as IBlockArchiveLine
|
||||
const blockLine = row as BlockArchiveLine
|
||||
const block = blockLine.data
|
||||
if (Archiver.isValidBlock(block)) {
|
||||
blocks.push(block)
|
||||
@ -94,7 +94,7 @@ class Archiver {
|
||||
})
|
||||
}
|
||||
|
||||
static isValidBlock(block: IBlock): boolean {
|
||||
static isValidBlock(block: Block): boolean {
|
||||
if (!block.id || !block.rootId) {
|
||||
return false
|
||||
}
|
||||
|
@ -3,10 +3,10 @@
|
||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||
|
||||
import {ArchiveUtils} from './archive'
|
||||
import {IBlock} from './block'
|
||||
import {Block} from './block'
|
||||
|
||||
test('archive: archive and unarchive', async () => {
|
||||
const blocks: IBlock[] = []
|
||||
const blocks: Block[] = []
|
||||
|
||||
const board = TestBlockFactory.createBoard()
|
||||
blocks.push(board)
|
||||
|
@ -1,26 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IBlock} from './block'
|
||||
import {Block} from './block'
|
||||
|
||||
interface IArchiveHeader {
|
||||
interface ArchiveHeader {
|
||||
version: number
|
||||
date: number
|
||||
}
|
||||
|
||||
interface IArchiveLine {
|
||||
interface ArchiveLine {
|
||||
type: string,
|
||||
data: any,
|
||||
}
|
||||
|
||||
// This schema allows the expansion of additional line types in the future
|
||||
interface IBlockArchiveLine extends IArchiveLine {
|
||||
interface BlockArchiveLine extends ArchiveLine {
|
||||
type: 'block',
|
||||
data: IBlock
|
||||
data: Block
|
||||
}
|
||||
|
||||
class ArchiveUtils {
|
||||
static buildBlockArchive(blocks: readonly IBlock[]): string {
|
||||
const header: IArchiveHeader = {
|
||||
static buildBlockArchive(blocks: readonly Block[]): string {
|
||||
const header: ArchiveHeader = {
|
||||
version: 1,
|
||||
date: Date.now(),
|
||||
}
|
||||
@ -28,7 +28,7 @@ class ArchiveUtils {
|
||||
const headerString = JSON.stringify(header)
|
||||
let content = headerString + '\n'
|
||||
for (const block of blocks) {
|
||||
const line: IBlockArchiveLine = {
|
||||
const line: BlockArchiveLine = {
|
||||
type: 'block',
|
||||
data: block,
|
||||
}
|
||||
@ -40,12 +40,12 @@ class ArchiveUtils {
|
||||
return content
|
||||
}
|
||||
|
||||
static parseBlockArchive(contents: string): IBlock[] {
|
||||
const blocks: IBlock[] = []
|
||||
static parseBlockArchive(contents: string): Block[] {
|
||||
const blocks: Block[] = []
|
||||
const allLineStrings = contents.split('\n')
|
||||
if (allLineStrings.length >= 2) {
|
||||
const headerString = allLineStrings[0]
|
||||
const header = JSON.parse(headerString) as IArchiveHeader
|
||||
const header = JSON.parse(headerString) as ArchiveHeader
|
||||
if (header.date && header.version >= 1) {
|
||||
const lineStrings = allLineStrings.slice(1)
|
||||
let lineNum = 2
|
||||
@ -54,13 +54,13 @@ class ArchiveUtils {
|
||||
// Ignore empty lines, e.g. last line
|
||||
continue
|
||||
}
|
||||
const line = JSON.parse(lineString) as IArchiveLine
|
||||
const line = JSON.parse(lineString) as ArchiveLine
|
||||
if (!line || !line.type || !line.data) {
|
||||
throw new Error(`ERROR parsing line ${lineNum}`)
|
||||
}
|
||||
switch (line.type) {
|
||||
case 'block': {
|
||||
const blockLine = line as IBlockArchiveLine
|
||||
const blockLine = line as BlockArchiveLine
|
||||
const block = blockLine.data
|
||||
blocks.push(block)
|
||||
break
|
||||
@ -78,4 +78,4 @@ class ArchiveUtils {
|
||||
}
|
||||
}
|
||||
|
||||
export {IArchiveHeader, IArchiveLine, IBlockArchiveLine, ArchiveUtils}
|
||||
export {ArchiveHeader, ArchiveLine, BlockArchiveLine, ArchiveUtils}
|
||||
|
@ -1,85 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||
|
||||
import {MutableBoard} from './board'
|
||||
import {MutableBoardView} from './boardView'
|
||||
import {MutableCard} from './card'
|
||||
import {MutableCommentBlock} from './commentBlock'
|
||||
import {MutableDividerBlock} from './dividerBlock'
|
||||
import {MutableImageBlock} from './imageBlock'
|
||||
import {MutableTextBlock} from './textBlock'
|
||||
|
||||
test('block: clone board', async () => {
|
||||
const boardA = TestBlockFactory.createBoard()
|
||||
boardA.isTemplate = true
|
||||
const boardB = new MutableBoard(boardA)
|
||||
|
||||
expect(boardB).toEqual(boardA)
|
||||
|
||||
expect(boardB.icon).toBe(boardA.icon)
|
||||
expect(boardB.isTemplate).toBe(boardA.isTemplate)
|
||||
expect(boardB.description).toBe(boardA.description)
|
||||
expect(boardB.showDescription).toBe(boardA.showDescription)
|
||||
})
|
||||
|
||||
test('block: clone view', async () => {
|
||||
const viewA = TestBlockFactory.createBoardView()
|
||||
const viewB = new MutableBoardView(viewA)
|
||||
|
||||
expect(viewB).toEqual(viewA)
|
||||
expect(viewB.groupById).toBe(viewA.groupById)
|
||||
expect(viewB.hiddenOptionIds).toEqual(viewA.hiddenOptionIds)
|
||||
expect(viewB.visiblePropertyIds).toEqual(viewA.visiblePropertyIds)
|
||||
expect(viewB.visibleOptionIds).toEqual(viewA.visibleOptionIds)
|
||||
expect(viewB.filter).toEqual(viewA.filter)
|
||||
expect(viewB.sortOptions).toEqual(viewA.sortOptions)
|
||||
expect(viewB.cardOrder).toEqual(viewA.cardOrder)
|
||||
expect(viewB.columnWidths).toEqual(viewA.columnWidths)
|
||||
})
|
||||
|
||||
test('block: clone card', async () => {
|
||||
const cardA = TestBlockFactory.createCard()
|
||||
cardA.isTemplate = true
|
||||
const cardB = new MutableCard(cardA)
|
||||
|
||||
expect(cardB).toEqual(cardA)
|
||||
expect(cardB.icon).toBe(cardA.icon)
|
||||
expect(cardB.isTemplate).toBe(cardA.isTemplate)
|
||||
expect(cardB.contentOrder).toEqual(cardA.contentOrder)
|
||||
})
|
||||
|
||||
test('block: clone comment', async () => {
|
||||
const card = TestBlockFactory.createCard()
|
||||
const blockA = TestBlockFactory.createComment(card)
|
||||
const blockB = new MutableCommentBlock(blockA)
|
||||
|
||||
expect(blockB).toEqual(blockA)
|
||||
})
|
||||
|
||||
test('block: clone text', async () => {
|
||||
const card = TestBlockFactory.createCard()
|
||||
const blockA = TestBlockFactory.createText(card)
|
||||
const blockB = new MutableTextBlock(blockA)
|
||||
|
||||
expect(blockB).toEqual(blockA)
|
||||
})
|
||||
|
||||
test('block: clone image', async () => {
|
||||
const card = TestBlockFactory.createCard()
|
||||
const blockA = TestBlockFactory.createImage(card)
|
||||
const blockB = new MutableImageBlock(blockA)
|
||||
|
||||
expect(blockB).toEqual(blockA)
|
||||
expect(blockB.fileId.length).toBeGreaterThan(0)
|
||||
expect(blockB.fileId).toEqual(blockA.fileId)
|
||||
})
|
||||
|
||||
test('block: clone divider', async () => {
|
||||
const card = TestBlockFactory.createCard()
|
||||
const blockA = TestBlockFactory.createDivider(card)
|
||||
const blockB = new MutableDividerBlock(blockA)
|
||||
|
||||
expect(blockB).toEqual(blockA)
|
||||
})
|
@ -1,30 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Utils} from '../utils'
|
||||
|
||||
const contentBlockTypes = ['text', 'image', 'divider', 'checkbox'] as const
|
||||
const blockTypes = [...contentBlockTypes, 'board', 'view', 'card', 'comment'] as const
|
||||
const blockTypes = [...contentBlockTypes, 'board', 'view', 'card', 'comment', 'unknown'] as const
|
||||
type ContentBlockTypes = typeof contentBlockTypes[number]
|
||||
type BlockTypes = typeof blockTypes[number]
|
||||
|
||||
interface IBlock {
|
||||
readonly id: string
|
||||
readonly parentId: string
|
||||
readonly rootId: string
|
||||
readonly createdBy: string
|
||||
readonly modifiedBy: string
|
||||
|
||||
readonly schema: number
|
||||
readonly type: BlockTypes
|
||||
readonly title: string
|
||||
readonly fields: Readonly<Record<string, any>>
|
||||
|
||||
readonly createAt: number
|
||||
readonly updateAt: number
|
||||
readonly deleteAt: number
|
||||
}
|
||||
|
||||
interface IMutableBlock extends IBlock {
|
||||
interface Block {
|
||||
id: string
|
||||
parentId: string
|
||||
rootId: string
|
||||
@ -41,40 +25,23 @@ interface IMutableBlock extends IBlock {
|
||||
deleteAt: number
|
||||
}
|
||||
|
||||
class MutableBlock implements IMutableBlock {
|
||||
id: string = Utils.createGuid()
|
||||
schema: number
|
||||
parentId: string
|
||||
rootId: string
|
||||
createdBy: string
|
||||
modifiedBy: string
|
||||
type: BlockTypes
|
||||
title: string
|
||||
fields: Record<string, any> = {}
|
||||
createAt: number = Date.now()
|
||||
updateAt = 0
|
||||
deleteAt = 0
|
||||
|
||||
constructor(block: any = {}) {
|
||||
this.id = block.id || Utils.createGuid()
|
||||
this.schema = 1
|
||||
this.parentId = block.parentId || ''
|
||||
this.rootId = block.rootId || ''
|
||||
this.createdBy = block.createdBy || ''
|
||||
this.modifiedBy = block.modifiedBy || ''
|
||||
this.type = block.type || ''
|
||||
|
||||
// Shallow copy here. Derived classes must make deep copies of their known properties in their constructors.
|
||||
this.fields = block.fields ? {...block.fields} : {}
|
||||
|
||||
this.title = block.title || ''
|
||||
|
||||
function createBlock(block?: Block): Block {
|
||||
const now = Date.now()
|
||||
this.createAt = block.createAt || now
|
||||
this.updateAt = block.updateAt || now
|
||||
this.deleteAt = block.deleteAt || 0
|
||||
return {
|
||||
id: block?.id || Utils.createGuid(),
|
||||
schema: 1,
|
||||
parentId: block?.parentId || '',
|
||||
rootId: block?.rootId || '',
|
||||
createdBy: block?.createdBy || '',
|
||||
modifiedBy: block?.modifiedBy || '',
|
||||
type: block?.type || 'unknown',
|
||||
fields: block?.fields ? {...block?.fields} : {},
|
||||
title: block?.title || '',
|
||||
createAt: block?.createAt || now,
|
||||
updateAt: block?.updateAt || now,
|
||||
deleteAt: block?.deleteAt || 0,
|
||||
}
|
||||
}
|
||||
|
||||
export type {ContentBlockTypes, BlockTypes}
|
||||
export {blockTypes, contentBlockTypes, IBlock, IMutableBlock, MutableBlock}
|
||||
export {blockTypes, contentBlockTypes, Block, createBlock}
|
||||
|
@ -2,17 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {Utils} from '../utils'
|
||||
|
||||
import {IBlock, MutableBlock} from './block'
|
||||
import {Block, createBlock} from './block'
|
||||
import {Card} from './card'
|
||||
|
||||
type PropertyType = 'text' | 'number' | 'select' | 'multiSelect' | 'date' | 'person' | 'file' | 'checkbox' | 'url' | 'email' | 'phone' | 'createdTime' | 'createdBy' | 'updatedTime' | 'updatedBy'
|
||||
|
||||
interface IPropertyOption {
|
||||
readonly id: string
|
||||
readonly value: string
|
||||
readonly color: string
|
||||
}
|
||||
|
||||
interface IMutablePropertyOption {
|
||||
id: string
|
||||
value: string
|
||||
color: string
|
||||
@ -20,73 +15,40 @@ interface IMutablePropertyOption {
|
||||
|
||||
// A template for card properties attached to a board
|
||||
interface IPropertyTemplate {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly type: PropertyType
|
||||
readonly options: IPropertyOption[]
|
||||
}
|
||||
|
||||
interface IMutablePropertyTemplate extends IPropertyTemplate {
|
||||
id: string
|
||||
name: string
|
||||
type: PropertyType
|
||||
options: IMutablePropertyOption[]
|
||||
options: IPropertyOption[]
|
||||
}
|
||||
|
||||
interface Board extends IBlock {
|
||||
readonly icon: string
|
||||
readonly description: string
|
||||
readonly showDescription: boolean
|
||||
readonly isTemplate: boolean
|
||||
readonly cardProperties: readonly IPropertyTemplate[]
|
||||
duplicate(): MutableBoard
|
||||
type BoardFields = {
|
||||
icon: string
|
||||
description: string
|
||||
showDescription?: boolean
|
||||
isTemplate?: boolean
|
||||
cardProperties: IPropertyTemplate[]
|
||||
}
|
||||
|
||||
class MutableBoard extends MutableBlock implements Board {
|
||||
get icon(): string {
|
||||
return this.fields.icon as string
|
||||
}
|
||||
set icon(value: string) {
|
||||
this.fields.icon = value
|
||||
type Board = Block & {
|
||||
fields: BoardFields
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.fields.description as string
|
||||
function createBoard(block?: Block): Board {
|
||||
let cardProperties: IPropertyTemplate[] = []
|
||||
const selectProperties = cardProperties.find((o) => o.type === 'select')
|
||||
if (!selectProperties) {
|
||||
const property: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: 'Status',
|
||||
type: 'select',
|
||||
options: [],
|
||||
}
|
||||
set description(value: string) {
|
||||
this.fields.description = value
|
||||
cardProperties.push(property)
|
||||
}
|
||||
|
||||
get showDescription(): boolean {
|
||||
return Boolean(this.fields.showDescription)
|
||||
}
|
||||
set showDescription(value: boolean) {
|
||||
this.fields.showDescription = value
|
||||
}
|
||||
|
||||
get isTemplate(): boolean {
|
||||
return Boolean(this.fields.isTemplate)
|
||||
}
|
||||
set isTemplate(value: boolean) {
|
||||
this.fields.isTemplate = value
|
||||
}
|
||||
|
||||
get cardProperties(): IMutablePropertyTemplate[] {
|
||||
return this.fields.cardProperties as IPropertyTemplate[]
|
||||
}
|
||||
set cardProperties(value: IMutablePropertyTemplate[]) {
|
||||
this.fields.cardProperties = value
|
||||
}
|
||||
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'board'
|
||||
|
||||
this.icon = block.fields?.icon || ''
|
||||
this.description = block.fields?.description || ''
|
||||
if (block.fields?.cardProperties) {
|
||||
if (block?.fields.cardProperties) {
|
||||
// Deep clone of card properties and their options
|
||||
this.cardProperties = block.fields.cardProperties.map((o: IPropertyTemplate) => {
|
||||
cardProperties = block?.fields.cardProperties.map((o: IPropertyTemplate) => {
|
||||
return {
|
||||
id: o.id,
|
||||
name: o.name,
|
||||
@ -94,16 +56,24 @@ class MutableBoard extends MutableBlock implements Board {
|
||||
options: o.options ? o.options.map((option) => ({...option})) : [],
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.cardProperties = []
|
||||
}
|
||||
|
||||
return {
|
||||
...createBlock(block),
|
||||
type: 'board',
|
||||
fields: {
|
||||
showDescription: block?.fields.showDescription || false,
|
||||
description: block?.fields.description || '',
|
||||
icon: block?.fields.icon || '',
|
||||
isTemplate: block?.fields.isTemplate || false,
|
||||
cardProperties,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
duplicate(): MutableBoard {
|
||||
const board = new MutableBoard(this)
|
||||
board.id = Utils.createGuid()
|
||||
return board
|
||||
}
|
||||
type BoardGroup = {
|
||||
option: IPropertyOption
|
||||
cards: Card[]
|
||||
}
|
||||
|
||||
export {Board, MutableBoard, PropertyType, IPropertyOption, IPropertyTemplate}
|
||||
export {Board, PropertyType, IPropertyOption, IPropertyTemplate, BoardGroup, createBoard}
|
||||
|
@ -1,12 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MutableBoardView, sortBoardViewsAlphabetically} from './boardView'
|
||||
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||
|
||||
import {sortBoardViewsAlphabetically} from './boardView'
|
||||
|
||||
test('boardView: sort with ASCII', async () => {
|
||||
const view1 = new MutableBoardView()
|
||||
const view1 = TestBlockFactory.createBoardView()
|
||||
view1.title = 'Maybe'
|
||||
const view2 = new MutableBoardView()
|
||||
const view2 = TestBlockFactory.createBoardView()
|
||||
view2.title = 'Active'
|
||||
|
||||
const views = [view1, view2]
|
||||
@ -15,9 +17,9 @@ test('boardView: sort with ASCII', async () => {
|
||||
})
|
||||
|
||||
test('boardView: sort with leading emoji', async () => {
|
||||
const view1 = new MutableBoardView()
|
||||
const view1 = TestBlockFactory.createBoardView()
|
||||
view1.title = '🤔 Maybe'
|
||||
const view2 = new MutableBoardView()
|
||||
const view2 = TestBlockFactory.createBoardView()
|
||||
view2.title = '🚀 Active'
|
||||
|
||||
const views = [view1, view2]
|
||||
@ -26,9 +28,9 @@ test('boardView: sort with leading emoji', async () => {
|
||||
})
|
||||
|
||||
test('boardView: sort with non-latin characters', async () => {
|
||||
const view1 = new MutableBoardView()
|
||||
const view1 = TestBlockFactory.createBoardView()
|
||||
view1.title = 'zebra'
|
||||
const view2 = new MutableBoardView()
|
||||
const view2 = TestBlockFactory.createBoardView()
|
||||
view2.title = 'ñu'
|
||||
|
||||
const views = [view1, view2]
|
||||
|
@ -1,122 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {Utils} from '../utils'
|
||||
|
||||
import {IBlock, MutableBlock} from './block'
|
||||
import {FilterGroup} from './filterGroup'
|
||||
import {Block, createBlock} from './block'
|
||||
import {FilterGroup, createFilterGroup} from './filterGroup'
|
||||
|
||||
type IViewType = 'board' | 'table' | 'gallery' // | 'calendar' | 'list'
|
||||
type ISortOption = { propertyId: '__title' | string, reversed: boolean }
|
||||
|
||||
interface BoardView extends IBlock {
|
||||
readonly viewType: IViewType
|
||||
readonly groupById?: string
|
||||
readonly sortOptions: readonly ISortOption[]
|
||||
readonly visiblePropertyIds: readonly string[]
|
||||
readonly visibleOptionIds: readonly string[]
|
||||
readonly hiddenOptionIds: readonly string[]
|
||||
readonly collapsedOptionIds: readonly string[]
|
||||
readonly filter: FilterGroup
|
||||
readonly cardOrder: readonly string[]
|
||||
readonly columnWidths: Readonly<Record<string, number>>
|
||||
|
||||
duplicate(): MutableBoardView
|
||||
type BoardViewFields = {
|
||||
viewType: IViewType
|
||||
groupById?: string
|
||||
sortOptions: ISortOption[]
|
||||
visiblePropertyIds: string[]
|
||||
visibleOptionIds: string[]
|
||||
hiddenOptionIds: string[]
|
||||
collapsedOptionIds: string[]
|
||||
filter: FilterGroup
|
||||
cardOrder: string[]
|
||||
columnWidths: Record<string, number>
|
||||
}
|
||||
|
||||
class MutableBoardView extends MutableBlock implements BoardView {
|
||||
get viewType(): IViewType {
|
||||
return this.fields.viewType
|
||||
}
|
||||
set viewType(value: IViewType) {
|
||||
this.fields.viewType = value
|
||||
type BoardView = Block & {
|
||||
fields: BoardViewFields
|
||||
}
|
||||
|
||||
get groupById(): string | undefined {
|
||||
return this.fields.groupById
|
||||
}
|
||||
set groupById(value: string | undefined) {
|
||||
this.fields.groupById = value
|
||||
}
|
||||
|
||||
get sortOptions(): ISortOption[] {
|
||||
return this.fields.sortOptions
|
||||
}
|
||||
set sortOptions(value: ISortOption[]) {
|
||||
this.fields.sortOptions = value
|
||||
}
|
||||
|
||||
get visiblePropertyIds(): string[] {
|
||||
return this.fields.visiblePropertyIds
|
||||
}
|
||||
set visiblePropertyIds(value: string[]) {
|
||||
this.fields.visiblePropertyIds = value
|
||||
}
|
||||
|
||||
get visibleOptionIds(): string[] {
|
||||
return this.fields.visibleOptionIds
|
||||
}
|
||||
set visibleOptionIds(value: string[]) {
|
||||
this.fields.visibleOptionIds = value
|
||||
}
|
||||
|
||||
get hiddenOptionIds(): string[] {
|
||||
return this.fields.hiddenOptionIds
|
||||
}
|
||||
set hiddenOptionIds(value: string[]) {
|
||||
this.fields.hiddenOptionIds = value
|
||||
}
|
||||
|
||||
get collapsedOptionIds(): string[] {
|
||||
return this.fields.collapsedOptionIds
|
||||
}
|
||||
set collapsedOptionIds(value: string[]) {
|
||||
this.fields.collapsedOptionIds = value
|
||||
}
|
||||
|
||||
get filter(): FilterGroup {
|
||||
return this.fields.filter
|
||||
}
|
||||
set filter(value: FilterGroup) {
|
||||
this.fields.filter = value
|
||||
}
|
||||
|
||||
get cardOrder(): string[] {
|
||||
return this.fields.cardOrder
|
||||
}
|
||||
set cardOrder(value: string[]) {
|
||||
this.fields.cardOrder = value
|
||||
}
|
||||
|
||||
get columnWidths(): Record<string, number> {
|
||||
return this.fields.columnWidths as Record<string, number>
|
||||
}
|
||||
set columnWidths(value: Record<string, number>) {
|
||||
this.fields.columnWidths = value
|
||||
}
|
||||
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
|
||||
this.type = 'view'
|
||||
|
||||
this.sortOptions = block.fields?.sortOptions?.map((o: ISortOption) => ({...o})) || [] // Deep clone
|
||||
this.visiblePropertyIds = block.fields?.visiblePropertyIds?.slice() || []
|
||||
this.visibleOptionIds = block.fields?.visibleOptionIds?.slice() || []
|
||||
this.hiddenOptionIds = block.fields?.hiddenOptionIds?.slice() || []
|
||||
this.collapsedOptionIds = block.fields?.collapsedOptionIds?.slice() || []
|
||||
this.filter = new FilterGroup(block.fields?.filter)
|
||||
this.cardOrder = block.fields?.cardOrder?.slice() || []
|
||||
this.columnWidths = {...(block.fields?.columnWidths || {})}
|
||||
|
||||
if (!this.viewType) {
|
||||
this.viewType = 'board'
|
||||
}
|
||||
}
|
||||
|
||||
duplicate(): MutableBoardView {
|
||||
const view = new MutableBoardView(this)
|
||||
view.id = Utils.createGuid()
|
||||
return view
|
||||
function createBoardView(block?: Block): BoardView {
|
||||
return {
|
||||
...createBlock(block),
|
||||
type: 'view',
|
||||
fields: {
|
||||
viewType: block?.fields.viewType || 'board',
|
||||
groupById: block?.fields.groupById,
|
||||
sortOptions: block?.fields.sortOptions?.map((o: ISortOption) => ({...o})) || [],
|
||||
visiblePropertyIds: block?.fields.visiblePropertyIds?.slice() || [],
|
||||
visibleOptionIds: block?.fields.visibleOptionIds?.slice() || [],
|
||||
hiddenOptionIds: block?.fields.hiddenOptionIds?.slice() || [],
|
||||
collapsedOptionIds: block?.fields.collapsedOptionIds?.slice() || [],
|
||||
filter: createFilterGroup(block?.fields.filter),
|
||||
cardOrder: block?.fields.cardOrder?.slice() || [],
|
||||
columnWidths: {...(block?.fields.columnWidths || {})},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,4 +50,4 @@ function sortBoardViewsAlphabetically(views: BoardView[]): BoardView[] {
|
||||
}).sort((v1, v2) => v1.title.localeCompare(v2.title)).map((v) => v.view)
|
||||
}
|
||||
|
||||
export {BoardView, MutableBoardView, IViewType, ISortOption, sortBoardViewsAlphabetically}
|
||||
export {BoardView, IViewType, ISortOption, sortBoardViewsAlphabetically, createBoardView}
|
||||
|
@ -1,61 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {Utils} from '../utils'
|
||||
|
||||
import {IBlock, MutableBlock} from './block'
|
||||
import {Block, createBlock} from './block'
|
||||
|
||||
interface Card extends IBlock {
|
||||
readonly icon: string
|
||||
readonly isTemplate: boolean
|
||||
readonly properties: Readonly<Record<string, string | string[]>>
|
||||
readonly contentOrder: Readonly<Array<string | string[]>>
|
||||
|
||||
duplicate(): MutableCard
|
||||
type CardFields = {
|
||||
icon?: string
|
||||
isTemplate?: boolean
|
||||
properties: Record<string, string | string[]>
|
||||
contentOrder: Array<string | string[]>
|
||||
}
|
||||
|
||||
class MutableCard extends MutableBlock implements Card {
|
||||
get icon(): string {
|
||||
return this.fields.icon as string
|
||||
}
|
||||
set icon(value: string) {
|
||||
this.fields.icon = value
|
||||
type Card = Block & {
|
||||
fields: CardFields
|
||||
}
|
||||
|
||||
get isTemplate(): boolean {
|
||||
return Boolean(this.fields.isTemplate)
|
||||
function createCard(block?: Block): Card {
|
||||
const contentOrder: Array<string|string[]> = []
|
||||
if (block?.fields.contentOrder) {
|
||||
for (const contentId of block.fields.contentOrder) {
|
||||
if (typeof contentId === 'string') {
|
||||
contentOrder.push(contentId)
|
||||
} else {
|
||||
contentOrder.push(contentId.slice())
|
||||
}
|
||||
set isTemplate(value: boolean) {
|
||||
this.fields.isTemplate = value
|
||||
}
|
||||
|
||||
get properties(): Record<string, string | string[]> {
|
||||
return this.fields.properties as Record<string, string | string[]>
|
||||
}
|
||||
set properties(value: Record<string, string | string[]>) {
|
||||
this.fields.properties = value
|
||||
}
|
||||
|
||||
get contentOrder(): Array<string | string[]> {
|
||||
return this.fields.contentOrder
|
||||
}
|
||||
set contentOrder(value: Array<string | string[]>) {
|
||||
this.fields.contentOrder = value
|
||||
}
|
||||
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'card'
|
||||
|
||||
this.icon = block.fields?.icon || ''
|
||||
this.properties = {...(block.fields?.properties || {})}
|
||||
this.contentOrder = block.fields?.contentOrder?.slice() || []
|
||||
}
|
||||
|
||||
duplicate(): MutableCard {
|
||||
const card = new MutableCard(this)
|
||||
card.id = Utils.createGuid()
|
||||
return card
|
||||
return {
|
||||
...createBlock(block),
|
||||
type: 'card',
|
||||
fields: {
|
||||
icon: block?.fields.icon || '',
|
||||
properties: {...(block?.fields.properties || {})},
|
||||
contentOrder,
|
||||
isTemplate: block?.fields.isTemplate || false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export {MutableCard, Card}
|
||||
export {Card, createCard}
|
||||
|
@ -1,14 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IContentBlock, MutableContentBlock} from './contentBlock'
|
||||
import {ContentBlock} from './contentBlock'
|
||||
import {Block, createBlock} from './block'
|
||||
|
||||
type CheckboxBlock = IContentBlock
|
||||
type CheckboxBlock = ContentBlock & {
|
||||
type: 'checkbox'
|
||||
}
|
||||
|
||||
class MutableCheckboxBlock extends MutableContentBlock implements CheckboxBlock {
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'checkbox'
|
||||
function createCheckboxBlock(block?: Block): CheckboxBlock {
|
||||
return {
|
||||
...createBlock(block),
|
||||
type: 'checkbox',
|
||||
}
|
||||
}
|
||||
|
||||
export {CheckboxBlock, MutableCheckboxBlock}
|
||||
export {CheckboxBlock, createCheckboxBlock}
|
||||
|
@ -1,14 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IBlock, MutableBlock} from './block'
|
||||
import {Block, createBlock} from './block'
|
||||
|
||||
type CommentBlock = IBlock
|
||||
type CommentBlock = Block & {
|
||||
type: 'comment'
|
||||
}
|
||||
|
||||
class MutableCommentBlock extends MutableBlock implements CommentBlock {
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'comment'
|
||||
function createCommentBlock(block?: Block): CommentBlock {
|
||||
return {
|
||||
...createBlock(block),
|
||||
type: 'comment',
|
||||
}
|
||||
}
|
||||
|
||||
export {CommentBlock, MutableCommentBlock}
|
||||
export {CommentBlock, createCommentBlock}
|
||||
|
@ -1,11 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IBlock, MutableBlock} from './block'
|
||||
import {Block, createBlock} from './block'
|
||||
|
||||
type IContentBlock = IBlock
|
||||
type IContentBlockWithCords = {block: IBlock, cords: {x: number, y?: number, z?: number}}
|
||||
|
||||
class MutableContentBlock extends MutableBlock implements IContentBlock {
|
||||
type IContentBlockWithCords = {
|
||||
block: Block,
|
||||
cords: {x: number, y?: number, z?: number}
|
||||
}
|
||||
|
||||
export {IContentBlock, IContentBlockWithCords, MutableContentBlock}
|
||||
type ContentBlock = Block
|
||||
|
||||
const createContentBlock = createBlock
|
||||
|
||||
export {ContentBlock, IContentBlockWithCords, createContentBlock}
|
||||
|
@ -1,14 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IContentBlock, MutableContentBlock} from './contentBlock'
|
||||
import {Block, createBlock} from './block'
|
||||
import {ContentBlock} from './contentBlock'
|
||||
|
||||
type DividerBlock = IContentBlock
|
||||
type DividerBlock = ContentBlock & {
|
||||
type: 'divider'
|
||||
}
|
||||
|
||||
class MutableDividerBlock extends MutableContentBlock implements DividerBlock {
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'divider'
|
||||
function createDividerBlock(block?: Block): DividerBlock {
|
||||
return {
|
||||
...createBlock(block),
|
||||
type: 'divider',
|
||||
}
|
||||
}
|
||||
|
||||
export {DividerBlock, MutableDividerBlock}
|
||||
export {DividerBlock, createDividerBlock}
|
||||
|
@ -4,24 +4,26 @@ import {Utils} from '../utils'
|
||||
|
||||
type FilterCondition = 'includes' | 'notIncludes' | 'isEmpty' | 'isNotEmpty'
|
||||
|
||||
class FilterClause {
|
||||
type FilterClause = {
|
||||
propertyId: string
|
||||
condition: FilterCondition
|
||||
values: string[]
|
||||
|
||||
constructor(o: any = {}) {
|
||||
this.propertyId = o.propertyId || ''
|
||||
this.condition = o.condition || 'includes'
|
||||
this.values = o.values?.slice() || []
|
||||
}
|
||||
|
||||
isEqual(o: FilterClause): boolean {
|
||||
function createFilterClause(o?: FilterClause): FilterClause {
|
||||
return {
|
||||
propertyId: o?.propertyId || '',
|
||||
condition: o?.condition || 'includes',
|
||||
values: o?.values?.slice() || [],
|
||||
}
|
||||
}
|
||||
|
||||
function areEqual(a: FilterClause, b: FilterClause): boolean {
|
||||
return (
|
||||
this.propertyId === o.propertyId &&
|
||||
this.condition === o.condition &&
|
||||
Utils.arraysEqual(this.values, o.values)
|
||||
a.propertyId === b.propertyId &&
|
||||
a.condition === b.condition &&
|
||||
Utils.arraysEqual(a.values, b.values)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export {FilterClause, FilterCondition}
|
||||
export {FilterClause, FilterCondition, createFilterClause, areEqual}
|
||||
|
@ -1,31 +1,33 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {FilterClause} from './filterClause'
|
||||
import {FilterClause, createFilterClause} from './filterClause'
|
||||
|
||||
type FilterGroupOperation = 'and' | 'or'
|
||||
|
||||
// A FilterGroup has 2 forms: (A or B or C) OR (A and B and C)
|
||||
class FilterGroup {
|
||||
operation: FilterGroupOperation = 'and'
|
||||
filters: (FilterClause | FilterGroup)[] = []
|
||||
type FilterGroup = {
|
||||
operation: FilterGroupOperation
|
||||
filters: (FilterClause | FilterGroup)[]
|
||||
}
|
||||
|
||||
static isAnInstanceOf(object: any): object is FilterGroup {
|
||||
function isAFilterGroupInstance(object: (FilterClause | FilterGroup)): object is FilterGroup {
|
||||
return 'innerOperation' in object && 'filters' in object
|
||||
}
|
||||
|
||||
constructor(o: any = {}) {
|
||||
this.operation = o.operation || 'and'
|
||||
if (o.filters) {
|
||||
this.filters = o.filters.map((p: any) => {
|
||||
if (FilterGroup.isAnInstanceOf(p)) {
|
||||
return new FilterGroup(p)
|
||||
function createFilterGroup(o?: FilterGroup): FilterGroup {
|
||||
let filters: (FilterClause | FilterGroup)[] = []
|
||||
if (o?.filters) {
|
||||
filters = o.filters.map((p: (FilterClause | FilterGroup)) => {
|
||||
if (isAFilterGroupInstance(p)) {
|
||||
return createFilterGroup(p)
|
||||
}
|
||||
return new FilterClause(p)
|
||||
return createFilterClause(p)
|
||||
})
|
||||
} else {
|
||||
this.filters = []
|
||||
}
|
||||
return {
|
||||
operation: o?.operation || 'and',
|
||||
filters,
|
||||
}
|
||||
}
|
||||
|
||||
export {FilterGroup, FilterGroupOperation}
|
||||
export {FilterGroup, FilterGroupOperation, createFilterGroup, isAFilterGroupInstance}
|
||||
|
@ -1,24 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IContentBlock, MutableContentBlock} from './contentBlock'
|
||||
import {Block, createBlock} from './block'
|
||||
import {ContentBlock} from './contentBlock'
|
||||
|
||||
interface ImageBlock extends IContentBlock {
|
||||
readonly fileId: string
|
||||
type ImageBlockFields = {
|
||||
fileId: string
|
||||
}
|
||||
|
||||
class MutableImageBlock extends MutableContentBlock implements ImageBlock {
|
||||
get fileId(): string {
|
||||
return this.fields.fileId as string
|
||||
}
|
||||
set fileId(value: string) {
|
||||
this.fields.fileId = value
|
||||
type ImageBlock = ContentBlock & {
|
||||
type: 'image'
|
||||
fields: ImageBlockFields
|
||||
}
|
||||
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'image'
|
||||
this.fileId = block.fields?.fileId || ''
|
||||
function createImageBlock(block?: Block): ImageBlock {
|
||||
return {
|
||||
...createBlock(block),
|
||||
type: 'image',
|
||||
fields: {
|
||||
fileId: block?.fields.fileId || '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export {ImageBlock, MutableImageBlock}
|
||||
export {ImageBlock, createImageBlock}
|
||||
|
@ -1,14 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IContentBlock, MutableContentBlock} from './contentBlock'
|
||||
import {ContentBlock} from './contentBlock'
|
||||
import {Block, createBlock} from './block'
|
||||
|
||||
type TextBlock = IContentBlock
|
||||
type TextBlock = ContentBlock & {
|
||||
type: 'text'
|
||||
}
|
||||
|
||||
class MutableTextBlock extends MutableContentBlock implements TextBlock {
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'text'
|
||||
function createTextBlock(block?: Block): TextBlock {
|
||||
return {
|
||||
...createBlock(block),
|
||||
type: 'text',
|
||||
}
|
||||
}
|
||||
|
||||
export {TextBlock, MutableTextBlock}
|
||||
export {TextBlock, createTextBlock}
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {IPropertyTemplate} from './blocks/board'
|
||||
import {Card} from './blocks/card'
|
||||
import {FilterClause} from './blocks/filterClause'
|
||||
import {FilterGroup} from './blocks/filterGroup'
|
||||
import {FilterGroup, isAFilterGroupInstance} from './blocks/filterGroup'
|
||||
import {Utils} from './utils'
|
||||
|
||||
class CardFilter {
|
||||
@ -20,7 +20,7 @@ class CardFilter {
|
||||
|
||||
if (filterGroup.operation === 'or') {
|
||||
for (const filter of filters) {
|
||||
if (FilterGroup.isAnInstanceOf(filter)) {
|
||||
if (isAFilterGroupInstance(filter)) {
|
||||
if (this.isFilterGroupMet(filter, templates, card)) {
|
||||
return true
|
||||
}
|
||||
@ -32,7 +32,7 @@ class CardFilter {
|
||||
}
|
||||
Utils.assert(filterGroup.operation === 'and')
|
||||
for (const filter of filters) {
|
||||
if (FilterGroup.isAnInstanceOf(filter)) {
|
||||
if (isAFilterGroupInstance(filter)) {
|
||||
if (!this.isFilterGroupMet(filter, templates, card)) {
|
||||
return false
|
||||
}
|
||||
@ -44,7 +44,7 @@ class CardFilter {
|
||||
}
|
||||
|
||||
static isClauseMet(filter: FilterClause, templates: readonly IPropertyTemplate[], card: Card): boolean {
|
||||
const value = card.properties[filter.propertyId]
|
||||
const value = card.fields.properties[filter.propertyId]
|
||||
switch (filter.condition) {
|
||||
case 'includes': {
|
||||
if (filter.values?.length < 1) {
|
||||
@ -77,7 +77,7 @@ class CardFilter {
|
||||
return {}
|
||||
}
|
||||
|
||||
const filters = filterGroup.filters.filter((o) => !FilterGroup.isAnInstanceOf(o))
|
||||
const filters = filterGroup.filters.filter((o) => !isAFilterGroupInstance(o))
|
||||
if (filters.length < 1) {
|
||||
return {}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ type Props = {
|
||||
const AddContentMenuItem = React.memo((props:Props): JSX.Element => {
|
||||
const {card, type, cords} = props
|
||||
const index = cords.x
|
||||
const contentOrder = card.contentOrder.slice()
|
||||
const contentOrder = card.fields.contentOrder.slice()
|
||||
const intl = useIntl()
|
||||
|
||||
const handler = contentRegistry.getHandler(type)
|
||||
|
@ -29,7 +29,7 @@ const BlockIconSelector = React.memo((props: Props) => {
|
||||
document.body.click()
|
||||
}, [])
|
||||
|
||||
if (!block.icon) {
|
||||
if (!block.fields.icon) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ const BlockIconSelector = React.memo((props: Props) => {
|
||||
if (props.readonly) {
|
||||
className += ' readonly'
|
||||
}
|
||||
const iconElement = <div className={className}><span>{block.icon}</span></div>
|
||||
const iconElement = <div className={className}><span>{block.fields.icon}</span></div>
|
||||
|
||||
return (
|
||||
<div className='BlockIconSelector'>
|
||||
|
@ -1,12 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useRef, useEffect} from 'react'
|
||||
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {BlockIcons} from '../../blockIcons'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import {Board} from '../../blocks/board'
|
||||
import {CommentBlock} from '../../blocks/commentBlock'
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {CardTree} from '../../viewModel/cardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import {Focusable} from '../../widgets/editable'
|
||||
import EditableArea from '../../widgets/editableArea'
|
||||
@ -22,18 +25,29 @@ import CardDetailProperties from './cardDetailProperties'
|
||||
import './cardDetail.scss'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
cardTree: CardTree
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
views: BoardView[]
|
||||
cards: Card[]
|
||||
card: Card
|
||||
comments: CommentBlock[]
|
||||
contents: Array<ContentBlock|ContentBlock[]>
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
const CardDetail = (props: Props): JSX.Element|null => {
|
||||
const {cardTree} = props
|
||||
const {card, comments} = cardTree
|
||||
const [title, setTitle] = useState(cardTree.card.title)
|
||||
const {card, comments} = props
|
||||
const [title, setTitle] = useState(card.title)
|
||||
const [serverTitle, setServerTitle] = useState(card.title)
|
||||
const titleRef = useRef<Focusable>(null)
|
||||
const titleValueRef = useRef(title)
|
||||
titleValueRef.current = title
|
||||
const saveTitle = useCallback(() => {
|
||||
if (title !== card.title) {
|
||||
mutator.changeTitle(card, title)
|
||||
}
|
||||
}, [card.title, title])
|
||||
|
||||
const saveTitleRef = useRef<() => void>(saveTitle)
|
||||
saveTitleRef.current = saveTitle
|
||||
|
||||
useEffect(() => {
|
||||
if (!title) {
|
||||
@ -42,14 +56,19 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (titleValueRef.current !== cardTree?.card.title) {
|
||||
mutator.changeTitle(card, titleValueRef.current)
|
||||
if (serverTitle === title) {
|
||||
setTitle(card.title)
|
||||
}
|
||||
}
|
||||
}, [cardTree])
|
||||
setServerTitle(card.title)
|
||||
}, [card.title, title])
|
||||
|
||||
if (!cardTree) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveTitleRef.current && saveTitleRef.current()
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!card) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -61,7 +80,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
||||
size='l'
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
{!props.readonly && !card.icon &&
|
||||
{!props.readonly && !card.fields.icon &&
|
||||
<div className='add-buttons'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@ -84,12 +103,8 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
||||
placeholderText='Untitled'
|
||||
onChange={(newTitle: string) => setTitle(newTitle)}
|
||||
saveOnEsc={true}
|
||||
onSave={() => {
|
||||
if (title !== props.cardTree.card.title) {
|
||||
mutator.changeTitle(card, title)
|
||||
}
|
||||
}}
|
||||
onCancel={() => setTitle(props.cardTree.card.title)}
|
||||
onSave={saveTitle}
|
||||
onCancel={() => setTitle(props.card.title)}
|
||||
readonly={props.readonly}
|
||||
spellCheck={true}
|
||||
/>
|
||||
@ -97,8 +112,13 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
||||
{/* Property list */}
|
||||
|
||||
<CardDetailProperties
|
||||
boardTree={props.boardTree}
|
||||
cardTree={props.cardTree}
|
||||
board={props.board}
|
||||
card={props.card}
|
||||
contents={props.contents}
|
||||
comments={props.comments}
|
||||
cards={props.cards}
|
||||
activeView={props.activeView}
|
||||
views={props.views}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
|
||||
@ -121,13 +141,14 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
||||
|
||||
<div className='CardDetail content fullwidth'>
|
||||
<CardDetailContents
|
||||
cardTree={props.cardTree}
|
||||
card={props.card}
|
||||
contents={props.contents}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!props.readonly &&
|
||||
<CardDetailContentsMenu card={props.cardTree.card}/>
|
||||
<CardDetailContentsMenu card={props.card}/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
|
@ -3,11 +3,10 @@
|
||||
import React from 'react'
|
||||
import {useIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {IContentBlockWithCords, IContentBlock} from '../../blocks/contentBlock'
|
||||
import {MutableTextBlock} from '../../blocks/textBlock'
|
||||
import mutator from '../../mutator'
|
||||
import {CardTree} from '../../viewModel/cardTree'
|
||||
import {IContentBlockWithCords, ContentBlock as ContentBlockType} from '../../blocks/contentBlock'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {createTextBlock} from '../../blocks/textBlock'
|
||||
import mutator from '../../mutator'
|
||||
import {useSortableWithGrip} from '../../hooks/sortable'
|
||||
|
||||
import ContentBlock from '../contentBlock'
|
||||
@ -16,17 +15,18 @@ import {MarkdownEditor} from '../markdownEditor'
|
||||
export type Position = 'left' | 'right' | 'above' | 'below' | 'aboveRow' | 'belowRow'
|
||||
|
||||
type Props = {
|
||||
cardTree: CardTree
|
||||
card: Card
|
||||
contents: Array<ContentBlockType|ContentBlockType[]>
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
function addTextBlock(card: Card, intl: IntlShape, text: string): void {
|
||||
const block = new MutableTextBlock()
|
||||
const block = createTextBlock()
|
||||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
block.title = text
|
||||
|
||||
const contentOrder = card.contentOrder.slice()
|
||||
const contentOrder = card.fields.contentOrder.slice()
|
||||
contentOrder.push(block.id)
|
||||
mutator.performAsUndoGroup(async () => {
|
||||
const description = intl.formatMessage({id: 'CardDetail.addCardText', defaultMessage: 'add card text'})
|
||||
@ -36,7 +36,16 @@ function addTextBlock(card: Card, intl: IntlShape, text: string): void {
|
||||
}
|
||||
|
||||
function moveBlock(card: Card, srcBlock: IContentBlockWithCords, dstBlock: IContentBlockWithCords, intl: IntlShape, moveTo: Position): void {
|
||||
const contentOrder = card.contentOrder.slice()
|
||||
const contentOrder: Array<string|string[]> = []
|
||||
if (card.fields.contentOrder) {
|
||||
for (const contentId of card.fields.contentOrder) {
|
||||
if (typeof contentId === 'string') {
|
||||
contentOrder.push(contentId)
|
||||
} else {
|
||||
contentOrder.push(contentId.slice())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const srcBlockId = srcBlock.block.id
|
||||
const dstBlockId = dstBlock.block.id
|
||||
@ -55,7 +64,7 @@ function moveBlock(card: Card, srcBlock: IContentBlockWithCords, dstBlock: ICont
|
||||
if (srcBlockY > -1) {
|
||||
(contentOrder[srcBlockX] as string[]).splice(srcBlockY, 1)
|
||||
|
||||
if (contentOrder[srcBlockX].length === 1) {
|
||||
if (contentOrder[srcBlockX].length === 1 && srcBlockX !== dstBlockX) {
|
||||
contentOrder.splice(srcBlockX, 1, contentOrder[srcBlockX][0])
|
||||
}
|
||||
} else {
|
||||
@ -99,10 +108,10 @@ function moveBlock(card: Card, srcBlock: IContentBlockWithCords, dstBlock: ICont
|
||||
}
|
||||
|
||||
type ContentBlockWithDragAndDropProps = {
|
||||
block: IContentBlock | IContentBlock[],
|
||||
block: ContentBlockType | ContentBlockType[],
|
||||
x: number,
|
||||
card: Card,
|
||||
cardTree: CardTree,
|
||||
contents: Array<ContentBlockType|ContentBlockType[]>,
|
||||
intl: IntlShape,
|
||||
readonly: boolean,
|
||||
}
|
||||
@ -129,13 +138,13 @@ const ContentBlockWithDragAndDrop = (props: ContentBlockWithDragAndDropProps) =>
|
||||
block={b}
|
||||
card={props.card}
|
||||
readonly={props.readonly}
|
||||
width={(1 / (props.block as IContentBlock[]).length) * 100}
|
||||
width={(1 / (props.block as ContentBlockType[]).length) * 100}
|
||||
onDrop={(src, dst, moveTo) => moveBlock(props.card, src, dst, props.intl, moveTo)}
|
||||
cords={{x: props.x, y}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{props.x === props.cardTree.contents.length - 1 && (
|
||||
{props.x === props.contents.length - 1 && (
|
||||
<div
|
||||
ref={itemRef2}
|
||||
className={`addToRow ${isOver2 ? 'dragover' : ''}`}
|
||||
@ -162,7 +171,7 @@ const ContentBlockWithDragAndDrop = (props: ContentBlockWithDragAndDropProps) =>
|
||||
onDrop={(src, dst, moveTo) => moveBlock(props.card, src, dst, props.intl, moveTo)}
|
||||
cords={{x: props.x}}
|
||||
/>
|
||||
{props.x === props.cardTree.contents.length - 1 && (
|
||||
{props.x === props.contents.length - 1 && (
|
||||
<div
|
||||
ref={itemRef2}
|
||||
className={`addToRow ${isOver2 ? 'dragover' : ''}`}
|
||||
@ -176,22 +185,21 @@ const ContentBlockWithDragAndDrop = (props: ContentBlockWithDragAndDropProps) =>
|
||||
|
||||
const CardDetailContents = React.memo((props: Props) => {
|
||||
const intl = useIntl()
|
||||
const {cardTree} = props
|
||||
if (!cardTree) {
|
||||
const {contents, card} = props
|
||||
if (!contents) {
|
||||
return null
|
||||
}
|
||||
const {card} = cardTree
|
||||
if (cardTree.contents.length > 0) {
|
||||
if (contents.length > 0) {
|
||||
return (
|
||||
<div className='octo-content'>
|
||||
{cardTree.contents.map((block, x) =>
|
||||
{contents.map((block, x) =>
|
||||
(
|
||||
<ContentBlockWithDragAndDrop
|
||||
key={x}
|
||||
block={block}
|
||||
x={x}
|
||||
card={card}
|
||||
cardTree={cardTree}
|
||||
contents={contents}
|
||||
intl={intl}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
|
@ -38,7 +38,7 @@ async function addBlock(card: Card, intl: IntlShape, handler: ContentHandler) {
|
||||
newBlock.parentId = card.id
|
||||
newBlock.rootId = card.rootId
|
||||
|
||||
const contentOrder = card.contentOrder.slice()
|
||||
const contentOrder = card.fields.contentOrder.slice()
|
||||
contentOrder.push(newBlock.id)
|
||||
const typeName = handler.getDisplayText(intl)
|
||||
const description = intl.formatMessage({id: 'ContentBlock.addElement', defaultMessage: 'add {type}'}, {type: typeName})
|
||||
|
@ -6,12 +6,10 @@ import {fireEvent, render} from '@testing-library/react'
|
||||
import {IntlProvider} from 'react-intl'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {MutableBoardTree} from '../../viewModel/boardTree'
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
|
||||
import 'isomorphic-fetch'
|
||||
import {MutableCardTree} from '../../viewModel/cardTree'
|
||||
|
||||
import CardDetailProperties from './cardDetailProperties'
|
||||
|
||||
@ -25,7 +23,7 @@ const wrapIntl = (children: any) => <IntlProvider locale='en'>{children}</IntlPr
|
||||
|
||||
describe('components/cardDetail/CardDetailProperties', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
board.cardProperties = [
|
||||
board.fields.cardProperties = [
|
||||
{
|
||||
id: 'property_id_1',
|
||||
name: 'Owner',
|
||||
@ -51,27 +49,26 @@ describe('components/cardDetail/CardDetailProperties', () => {
|
||||
]
|
||||
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
view.sortOptions = []
|
||||
view.groupById = undefined
|
||||
view.hiddenOptionIds = []
|
||||
view.fields.sortOptions = []
|
||||
view.fields.groupById = undefined
|
||||
view.fields.hiddenOptionIds = []
|
||||
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
card.properties.property_id_1 = 'property_value_id_1'
|
||||
card.fields.properties.property_id_1 = 'property_value_id_1'
|
||||
|
||||
const cardTemplate = TestBlockFactory.createCard(board)
|
||||
cardTemplate.isTemplate = true
|
||||
|
||||
const cardTree = new MutableCardTree(card)
|
||||
cardTemplate.fields.isTemplate = true
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, card, cardTemplate])))
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).not.toBeUndefined()
|
||||
|
||||
const componet = wrapIntl((
|
||||
<CardDetailProperties
|
||||
boardTree={boardTree!}
|
||||
cardTree={cardTree}
|
||||
board={board!}
|
||||
card={card}
|
||||
cards={[card]}
|
||||
contents={[]}
|
||||
comments={[]}
|
||||
activeView={view}
|
||||
views={[view]}
|
||||
readonly={false}
|
||||
/>
|
||||
))
|
||||
@ -81,14 +78,15 @@ describe('components/cardDetail/CardDetailProperties', () => {
|
||||
})
|
||||
|
||||
test('rename select property', async () => {
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, card, cardTemplate])))
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).not.toBeUndefined()
|
||||
|
||||
const component = wrapIntl((
|
||||
<CardDetailProperties
|
||||
boardTree={boardTree!}
|
||||
cardTree={cardTree}
|
||||
board={board!}
|
||||
card={card}
|
||||
cards={[card]}
|
||||
contents={[]}
|
||||
comments={[]}
|
||||
activeView={view}
|
||||
views={[view]}
|
||||
readonly={false}
|
||||
/>
|
||||
))
|
||||
@ -103,15 +101,13 @@ describe('components/cardDetail/CardDetailProperties', () => {
|
||||
userEvent.type(propertyNameInput!, 'Owner - Renamed{enter}')
|
||||
fireEvent.click(propertyLabel!)
|
||||
|
||||
// should be called twice,
|
||||
// one to sync card tree,
|
||||
// and once on renaming the property
|
||||
expect(FetchMock.fn).toBeCalledTimes(2)
|
||||
// should be called once on renaming the property
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
// Verify API call was made with renamed property
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const lastAPICallPayload = JSON.parse(FetchMock.fn.mock.calls[1][1].body)
|
||||
const lastAPICallPayload = JSON.parse(FetchMock.fn.mock.calls[0][1].body)
|
||||
expect(lastAPICallPayload[0].fields.cardProperties[0].name).toBe('Owner - Renamed')
|
||||
expect(lastAPICallPayload[0].fields.cardProperties[0].options.length).toBe(3)
|
||||
expect(lastAPICallPayload[0].fields.cardProperties[0].options[0].value).toBe('Jean-Luc Picard')
|
||||
|
@ -3,10 +3,12 @@
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {PropertyType} from '../../blocks/board'
|
||||
import {Board, PropertyType, IPropertyTemplate} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
import {CommentBlock} from '../../blocks/commentBlock'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {CardTree} from '../../viewModel/cardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import PropertyMenu from '../../widgets/propertyMenu'
|
||||
@ -14,20 +16,23 @@ import PropertyMenu from '../../widgets/propertyMenu'
|
||||
import PropertyValueElement from '../propertyValueElement'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
cardTree: CardTree
|
||||
board: Board
|
||||
card: Card
|
||||
cards: Card[]
|
||||
contents: Array<ContentBlock|ContentBlock[]>
|
||||
comments: CommentBlock[]
|
||||
activeView: BoardView
|
||||
views: BoardView[]
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
const CardDetailProperties = React.memo((props: Props) => {
|
||||
const {boardTree, cardTree} = props
|
||||
const {board} = boardTree
|
||||
const {card} = cardTree
|
||||
const {board, card, cards, views, activeView, contents, comments} = props
|
||||
|
||||
return (
|
||||
<div className='octo-propertylist CardDetailProperties'>
|
||||
{board.cardProperties.map((propertyTemplate) => {
|
||||
const propertyValue = card.properties[propertyTemplate.id]
|
||||
{board.fields.cardProperties.map((propertyTemplate: IPropertyTemplate) => {
|
||||
const propertyValue = card.fields.properties[propertyTemplate.id]
|
||||
return (
|
||||
<div
|
||||
key={propertyTemplate.id + '-' + propertyTemplate.type + '-' + propertyValue}
|
||||
@ -41,16 +46,17 @@ const CardDetailProperties = React.memo((props: Props) => {
|
||||
propertyId={propertyTemplate.id}
|
||||
propertyName={propertyTemplate.name}
|
||||
propertyType={propertyTemplate.type}
|
||||
onTypeAndNameChanged={(newType: PropertyType, newName: string) => mutator.changePropertyTypeAndName(boardTree, propertyTemplate, newType, newName)}
|
||||
onDelete={(id: string) => mutator.deleteProperty(boardTree, id)}
|
||||
onTypeAndNameChanged={(newType: PropertyType, newName: string) => mutator.changePropertyTypeAndName(board, cards, propertyTemplate, newType, newName)}
|
||||
onDelete={(id: string) => mutator.deleteProperty(board, views, cards, id)}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
}
|
||||
<PropertyValueElement
|
||||
readOnly={props.readonly}
|
||||
card={card}
|
||||
boardTree={boardTree}
|
||||
cardTree={cardTree}
|
||||
board={board}
|
||||
contents={contents}
|
||||
comments={comments}
|
||||
propertyTemplate={propertyTemplate}
|
||||
emptyDisplayValue='Empty'
|
||||
/>
|
||||
@ -63,7 +69,7 @@ const CardDetailProperties = React.memo((props: Props) => {
|
||||
<Button
|
||||
onClick={async () => {
|
||||
// TODO: Show UI
|
||||
await mutator.insertPropertyTemplate(boardTree)
|
||||
await mutator.insertPropertyTemplate(board, activeView)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {FC, useEffect, useState} from 'react'
|
||||
import React, {FC} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {IBlock} from '../../blocks/block'
|
||||
import {Block} from '../../blocks/block'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
@ -11,12 +11,13 @@ import DeleteIcon from '../../widgets/icons/delete'
|
||||
import OptionsIcon from '../../widgets/icons/options'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import {UserCache} from '../../userCache'
|
||||
import {getUser} from '../../store/users'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
|
||||
import './comment.scss'
|
||||
|
||||
type Props = {
|
||||
comment: IBlock
|
||||
comment: Block
|
||||
userId: string
|
||||
userImageUrl: string
|
||||
}
|
||||
@ -25,15 +26,7 @@ const Comment: FC<Props> = (props: Props) => {
|
||||
const {comment, userId, userImageUrl} = props
|
||||
const intl = useIntl()
|
||||
const html = Utils.htmlFromMarkdown(comment.title)
|
||||
|
||||
const [username, setUsername] = useState('')
|
||||
useEffect(() => {
|
||||
UserCache.shared.getUser(userId).then((user) => {
|
||||
if (user) {
|
||||
setUsername(user.username)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
const user = useAppSelector(getUser(userId))
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -45,7 +38,7 @@ const Comment: FC<Props> = (props: Props) => {
|
||||
className='comment-avatar'
|
||||
src={userImageUrl}
|
||||
/>
|
||||
<div className='comment-username'>{username}</div>
|
||||
<div className='comment-username'>{user?.username}</div>
|
||||
<div className='comment-date'>
|
||||
{Utils.displayDateTime(new Date(comment.createAt), intl)}
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
import React, {useState} from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import {CommentBlock, MutableCommentBlock} from '../../blocks/commentBlock'
|
||||
import {CommentBlock, createCommentBlock} from '../../blocks/commentBlock'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
@ -29,7 +29,7 @@ const CommentsList = React.memo((props: Props) => {
|
||||
Utils.log(`Send comment: ${commentText}`)
|
||||
Utils.assertValue(cardId)
|
||||
|
||||
const comment = new MutableCommentBlock()
|
||||
const comment = createCommentBlock()
|
||||
comment.parentId = cardId
|
||||
comment.rootId = rootId
|
||||
comment.title = commentText
|
||||
|
@ -1,22 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import React from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
import {CardTree, CardTreeContext, MutableCardTree} from '../viewModel/cardTree'
|
||||
import {BoardView} from '../blocks/boardView'
|
||||
import {Board} from '../blocks/board'
|
||||
import {Card} from '../blocks/card'
|
||||
import DeleteIcon from '../widgets/icons/delete'
|
||||
import Menu from '../widgets/menu'
|
||||
|
||||
import useCardListener from '../hooks/cardListener'
|
||||
import {useAppSelector} from '../store/hooks'
|
||||
import {getCard} from '../store/cards'
|
||||
import {getCardContents} from '../store/contents'
|
||||
import {getCardComments} from '../store/comments'
|
||||
|
||||
import CardDetail from './cardDetail/cardDetail'
|
||||
import Dialog from './dialog'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
views: BoardView[]
|
||||
cards: Card[]
|
||||
cardId: string
|
||||
onClose: () => void
|
||||
showCard: (cardId?: string) => void
|
||||
@ -24,39 +31,20 @@ type Props = {
|
||||
}
|
||||
|
||||
const CardDialog = (props: Props) => {
|
||||
const [syncComplete, setSyncComplete] = useState(false)
|
||||
const [cardTree, setCardTree] = useState<CardTree>()
|
||||
const {board, activeView, cards, views} = props
|
||||
const card = useAppSelector(getCard(props.cardId))
|
||||
const contents = useAppSelector(getCardContents(props.cardId))
|
||||
const comments = useAppSelector(getCardComments(props.cardId))
|
||||
const intl = useIntl()
|
||||
useEffect(() => {
|
||||
MutableCardTree.sync(props.cardId).then((ct) => {
|
||||
setCardTree(ct)
|
||||
setSyncComplete(true)
|
||||
})
|
||||
}, [props.boardTree])
|
||||
|
||||
useCardListener(
|
||||
async (blocks) => {
|
||||
Utils.log(`cardListener.onChanged: ${blocks.length}`)
|
||||
const newCardTree = cardTree ? MutableCardTree.incrementalUpdate(cardTree, blocks) : await MutableCardTree.sync(props.cardId)
|
||||
setCardTree(newCardTree)
|
||||
setSyncComplete(true)
|
||||
},
|
||||
async () => {
|
||||
Utils.log('cardListener.onReconnect')
|
||||
const newCardTree = await MutableCardTree.sync(props.cardId)
|
||||
setCardTree(newCardTree)
|
||||
setSyncComplete(true)
|
||||
},
|
||||
)
|
||||
|
||||
const makeTemplateClicked = async () => {
|
||||
if (!cardTree) {
|
||||
Utils.assertFailure('cardTree')
|
||||
if (!card) {
|
||||
Utils.assertFailure('card')
|
||||
return
|
||||
}
|
||||
|
||||
await mutator.duplicateCard(
|
||||
cardTree.card.id,
|
||||
props.cardId,
|
||||
intl.formatMessage({id: 'Mutator.new-template-from-card', defaultMessage: 'new template from card'}),
|
||||
true,
|
||||
async (newCardId) => {
|
||||
@ -75,7 +63,6 @@ const CardDialog = (props: Props) => {
|
||||
icon={<DeleteIcon/>}
|
||||
name='Delete'
|
||||
onClick={async () => {
|
||||
const card = cardTree?.card
|
||||
if (!card) {
|
||||
Utils.assertFailure()
|
||||
return
|
||||
@ -84,7 +71,7 @@ const CardDialog = (props: Props) => {
|
||||
props.onClose()
|
||||
}}
|
||||
/>
|
||||
{(cardTree && !cardTree.card.isTemplate) &&
|
||||
{(card && !card.fields.isTemplate) &&
|
||||
<Menu.Text
|
||||
id='makeTemplate'
|
||||
name='New template from card'
|
||||
@ -98,31 +85,33 @@ const CardDialog = (props: Props) => {
|
||||
onClose={props.onClose}
|
||||
toolsMenu={!props.readonly && menu}
|
||||
>
|
||||
{(cardTree?.card.isTemplate) &&
|
||||
{card && card.fields.isTemplate &&
|
||||
<div className='banner'>
|
||||
<FormattedMessage
|
||||
id='CardDialog.editing-template'
|
||||
defaultMessage="You're editing a template"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{cardTree &&
|
||||
<CardTreeContext.Provider value={cardTree}>
|
||||
</div>}
|
||||
|
||||
{card &&
|
||||
<CardDetail
|
||||
boardTree={props.boardTree}
|
||||
cardTree={cardTree}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
views={views}
|
||||
cards={cards}
|
||||
card={card}
|
||||
contents={contents}
|
||||
comments={comments}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
</CardTreeContext.Provider>
|
||||
}
|
||||
{(!cardTree && syncComplete) &&
|
||||
/>}
|
||||
|
||||
{!card &&
|
||||
<div className='banner error'>
|
||||
<FormattedMessage
|
||||
id='CardDialog.nocard'
|
||||
defaultMessage="This card doesn't exist or is inaccessible"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
@ -3,15 +3,18 @@
|
||||
/* eslint-disable max-lines */
|
||||
import React from 'react'
|
||||
import {injectIntl, IntlShape} from 'react-intl'
|
||||
import {connect} from 'react-redux'
|
||||
import Hotkeys from 'react-hot-keys'
|
||||
|
||||
import {BlockIcons} from '../blockIcons'
|
||||
import {Card, MutableCard} from '../blocks/card'
|
||||
import {Card, createCard} from '../blocks/card'
|
||||
import {Board, IPropertyTemplate, IPropertyOption, BoardGroup} from '../blocks/board'
|
||||
import {BoardView} from '../blocks/boardView'
|
||||
import {CardFilter} from '../cardFilter'
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
import {UserSettings} from '../userSettings'
|
||||
import {addCard, addTemplate} from '../store/cards'
|
||||
|
||||
import './centerPanel.scss'
|
||||
|
||||
@ -25,10 +28,15 @@ import Table from './table/table'
|
||||
import Gallery from './gallery/gallery'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
setSearchText: (text?: string) => void
|
||||
board: Board
|
||||
cards: Card[]
|
||||
activeView: BoardView
|
||||
views: BoardView[]
|
||||
groupByProperty?: IPropertyTemplate
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
addCard: (card: Card) => void
|
||||
addTemplate: (template: Card) => void
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -94,16 +102,8 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const {boardTree} = this.props
|
||||
const {groupByProperty} = boardTree
|
||||
const {activeView} = boardTree
|
||||
|
||||
if (!groupByProperty && activeView.viewType === 'board') {
|
||||
Utils.assertFailure('Board views must have groupByProperty set')
|
||||
return <div/>
|
||||
}
|
||||
|
||||
const {board} = boardTree
|
||||
const {groupByProperty, activeView, board, views, cards} = this.props
|
||||
const {visible: visibleGroups, hidden: hiddenGroups} = this.getVisibleAndHiddenGroups(cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty)
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -120,8 +120,11 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
{this.state.shownCardId &&
|
||||
<RootPortal>
|
||||
<CardDialog
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
views={views}
|
||||
cards={cards}
|
||||
key={this.state.shownCardId}
|
||||
boardTree={boardTree}
|
||||
cardId={this.state.shownCardId}
|
||||
onClose={() => this.showCard(undefined)}
|
||||
showCard={(cardId) => this.showCard(cardId)}
|
||||
@ -137,8 +140,11 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
readonly={this.props.readonly}
|
||||
/>
|
||||
<ViewHeader
|
||||
boardTree={boardTree}
|
||||
setSearchText={this.props.setSearchText}
|
||||
board={this.props.board}
|
||||
activeView={this.props.activeView}
|
||||
cards={this.props.cards}
|
||||
views={this.props.views}
|
||||
groupByProperty={this.props.groupByProperty}
|
||||
addCard={() => this.addCard('', true)}
|
||||
addCardFromTemplate={this.addCardFromTemplate}
|
||||
addCardTemplate={this.addCardTemplate}
|
||||
@ -147,9 +153,14 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{activeView.viewType === 'board' &&
|
||||
{activeView.fields.viewType === 'board' &&
|
||||
<Kanban
|
||||
boardTree={boardTree}
|
||||
board={this.props.board}
|
||||
activeView={this.props.activeView}
|
||||
cards={this.props.cards}
|
||||
groupByProperty={this.props.groupByProperty}
|
||||
visibleGroups={visibleGroups}
|
||||
hiddenGroups={hiddenGroups}
|
||||
selectedCardIds={this.state.selectedCardIds}
|
||||
readonly={this.props.readonly}
|
||||
onCardClicked={this.cardClicked}
|
||||
@ -157,9 +168,14 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
showCard={this.showCard}
|
||||
/>}
|
||||
|
||||
{activeView.viewType === 'table' &&
|
||||
{activeView.fields.viewType === 'table' &&
|
||||
<Table
|
||||
boardTree={boardTree}
|
||||
board={this.props.board}
|
||||
activeView={this.props.activeView}
|
||||
cards={this.props.cards}
|
||||
groupByProperty={this.props.groupByProperty}
|
||||
views={this.props.views}
|
||||
visibleGroups={visibleGroups}
|
||||
selectedCardIds={this.state.selectedCardIds}
|
||||
readonly={this.props.readonly}
|
||||
cardIdToFocusOnRender={this.state.cardIdToFocusOnRender}
|
||||
@ -168,9 +184,11 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
onCardClicked={this.cardClicked}
|
||||
/>}
|
||||
|
||||
{activeView.viewType === 'gallery' &&
|
||||
{activeView.fields.viewType === 'gallery' &&
|
||||
<Gallery
|
||||
boardTree={boardTree}
|
||||
board={this.props.board}
|
||||
cards={this.props.cards}
|
||||
activeView={this.props.activeView}
|
||||
readonly={this.props.readonly}
|
||||
onCardClicked={this.cardClicked}
|
||||
selectedCardIds={this.state.selectedCardIds}
|
||||
@ -203,30 +221,30 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
addCard = async (groupByOptionId?: string, show = false): Promise<void> => {
|
||||
const {boardTree} = this.props
|
||||
const {activeView, board} = boardTree
|
||||
const {activeView, board, groupByProperty} = this.props
|
||||
|
||||
const card = new MutableCard()
|
||||
const card = createCard()
|
||||
|
||||
card.parentId = boardTree.board.id
|
||||
card.rootId = boardTree.board.rootId
|
||||
const propertiesThatMeetFilters = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties)
|
||||
if ((activeView.viewType === 'board' || activeView.viewType === 'table') && boardTree.groupByProperty) {
|
||||
card.parentId = board.id
|
||||
card.rootId = board.rootId
|
||||
const propertiesThatMeetFilters = CardFilter.propertiesThatMeetFilterGroup(activeView.fields.filter, board.fields.cardProperties)
|
||||
if ((activeView.fields.viewType === 'board' || activeView.fields.viewType === 'table') && groupByProperty) {
|
||||
if (groupByOptionId) {
|
||||
propertiesThatMeetFilters[boardTree.groupByProperty.id] = groupByOptionId
|
||||
propertiesThatMeetFilters[groupByProperty.id] = groupByOptionId
|
||||
} else {
|
||||
delete propertiesThatMeetFilters[boardTree.groupByProperty.id]
|
||||
delete propertiesThatMeetFilters[groupByProperty.id]
|
||||
}
|
||||
}
|
||||
card.properties = {...card.properties, ...propertiesThatMeetFilters}
|
||||
if (!card.icon && UserSettings.prefillRandomIcons) {
|
||||
card.icon = BlockIcons.shared.randomIcon()
|
||||
card.fields.properties = {...card.fields.properties, ...propertiesThatMeetFilters}
|
||||
if (!card.fields.icon && UserSettings.prefillRandomIcons) {
|
||||
card.fields.icon = BlockIcons.shared.randomIcon()
|
||||
}
|
||||
await mutator.insertBlock(
|
||||
card,
|
||||
'add card',
|
||||
async () => {
|
||||
if (show) {
|
||||
this.props.addCard(card)
|
||||
this.showCard(card.id)
|
||||
} else {
|
||||
// Focus on this card's title inline on next render
|
||||
@ -241,16 +259,17 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
private addCardTemplate = async () => {
|
||||
const {boardTree} = this.props
|
||||
const {board} = this.props
|
||||
|
||||
const cardTemplate = new MutableCard()
|
||||
cardTemplate.isTemplate = true
|
||||
cardTemplate.parentId = boardTree.board.id
|
||||
cardTemplate.rootId = boardTree.board.rootId
|
||||
const cardTemplate = createCard()
|
||||
cardTemplate.fields.isTemplate = true
|
||||
cardTemplate.parentId = board.id
|
||||
cardTemplate.rootId = board.rootId
|
||||
await mutator.insertBlock(
|
||||
cardTemplate,
|
||||
'add card template',
|
||||
async () => {
|
||||
this.props.addTemplate(cardTemplate)
|
||||
this.showCard(cardTemplate.id)
|
||||
}, async () => {
|
||||
this.showCard(undefined)
|
||||
@ -263,14 +282,13 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
cardClicked = (e: React.MouseEvent, card: Card): void => {
|
||||
const {boardTree} = this.props
|
||||
const {activeView} = boardTree
|
||||
const {activeView, cards} = this.props
|
||||
|
||||
if (e.shiftKey) {
|
||||
let selectedCardIds = this.state.selectedCardIds.slice()
|
||||
if (selectedCardIds.length > 0 && (e.metaKey || e.ctrlKey)) {
|
||||
// Cmd+Shift+Click: Extend the selection
|
||||
const orderedCardIds = boardTree.orderedCards().map((o) => o.id)
|
||||
const orderedCardIds = cards.map((o) => o.id)
|
||||
const lastCardId = selectedCardIds[selectedCardIds.length - 1]
|
||||
const srcIndex = orderedCardIds.indexOf(lastCardId)
|
||||
const destIndex = orderedCardIds.indexOf(card.id)
|
||||
@ -290,7 +308,7 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
}
|
||||
this.setState({selectedCardIds})
|
||||
}
|
||||
} else if (activeView.viewType === 'board' || activeView.viewType === 'gallery') {
|
||||
} else if (activeView.fields.viewType === 'board' || activeView.fields.viewType === 'gallery') {
|
||||
this.showCard(card.id)
|
||||
}
|
||||
|
||||
@ -310,7 +328,7 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
|
||||
mutator.performAsUndoGroup(async () => {
|
||||
for (const cardId of selectedCardIds) {
|
||||
const card = this.props.boardTree.allCards.find((o) => o.id === cardId)
|
||||
const card = this.props.cards.find((o) => o.id === cardId)
|
||||
if (card) {
|
||||
mutator.deleteBlock(card, selectedCardIds.length > 1 ? `delete ${selectedCardIds.length} cards` : 'delete card')
|
||||
} else {
|
||||
@ -330,7 +348,7 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
|
||||
mutator.performAsUndoGroup(async () => {
|
||||
for (const cardId of selectedCardIds) {
|
||||
const card = this.props.boardTree.allCards.find((o) => o.id === cardId)
|
||||
const card = this.props.cards.find((o) => o.id === cardId)
|
||||
if (card) {
|
||||
mutator.duplicateCard(cardId)
|
||||
} else {
|
||||
@ -341,6 +359,55 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
|
||||
this.setState({selectedCardIds: []})
|
||||
}
|
||||
private groupCardsByOptions(cards: Card[], optionIds: string[], groupByProperty?: IPropertyTemplate): BoardGroup[] {
|
||||
const groups = []
|
||||
for (const optionId of optionIds) {
|
||||
if (optionId) {
|
||||
const option = groupByProperty?.options.find((o) => o.id === optionId)
|
||||
if (option) {
|
||||
const c = cards.filter((o) => optionId === o.fields.properties[groupByProperty!.id])
|
||||
const group: BoardGroup = {
|
||||
option,
|
||||
cards: c,
|
||||
}
|
||||
groups.push(group)
|
||||
} else {
|
||||
Utils.logError(`groupCardsByOptions: Missing option with id: ${optionId}`)
|
||||
}
|
||||
} else {
|
||||
// Empty group
|
||||
const emptyGroupCards = cards.filter((card) => {
|
||||
const groupByOptionId = card.fields.properties[groupByProperty?.id || '']
|
||||
return !groupByOptionId || !groupByProperty?.options.find((option) => option.id === groupByOptionId)
|
||||
})
|
||||
const group: BoardGroup = {
|
||||
option: {id: '', value: `No ${groupByProperty?.name}`, color: ''},
|
||||
cards: emptyGroupCards,
|
||||
}
|
||||
groups.push(group)
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
export default injectIntl(CenterPanel)
|
||||
private getVisibleAndHiddenGroups(cards: Card[], visibleOptionIds: string[], hiddenOptionIds: string[], groupByProperty?: IPropertyTemplate): {visible: BoardGroup[], hidden: BoardGroup[]} {
|
||||
let unassignedOptionIds: string[] = []
|
||||
if (groupByProperty) {
|
||||
unassignedOptionIds = groupByProperty.options.
|
||||
filter((o: IPropertyOption) => !visibleOptionIds.includes(o.id) && !hiddenOptionIds.includes(o.id)).
|
||||
map((o: IPropertyOption) => o.id)
|
||||
}
|
||||
const allVisibleOptionIds = [...visibleOptionIds, ...unassignedOptionIds]
|
||||
|
||||
// If the empty group positon is not explicitly specified, make it the first visible column
|
||||
if (!allVisibleOptionIds.includes('') && !hiddenOptionIds.includes('')) {
|
||||
allVisibleOptionIds.unshift('')
|
||||
}
|
||||
|
||||
const visibleGroups = this.groupCardsByOptions(cards, allVisibleOptionIds, groupByProperty)
|
||||
const hiddenGroups = this.groupCardsByOptions(cards, hiddenOptionIds, groupByProperty)
|
||||
return {visible: visibleGroups, hidden: hiddenGroups}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(undefined, {addCard, addTemplate})(injectIntl(CenterPanel))
|
||||
|
@ -6,7 +6,7 @@ import {fireEvent, render} from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import {IntlProvider} from 'react-intl'
|
||||
|
||||
import {IContentBlock} from '../../blocks/contentBlock'
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
|
||||
import CheckboxElement from './checkboxElement'
|
||||
|
||||
@ -15,7 +15,7 @@ const fetchMock = require('fetch-mock-jest')
|
||||
const wrapIntl = (children: any) => <IntlProvider locale='en'>{children}</IntlProvider>
|
||||
|
||||
describe('components/content/CheckboxElement', () => {
|
||||
const defaultBlock: IContentBlock = {
|
||||
const defaultBlock: ContentBlock = {
|
||||
id: 'test-id',
|
||||
parentId: '',
|
||||
rootId: '',
|
||||
|
@ -3,8 +3,8 @@
|
||||
import React, {useState} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {MutableCheckboxBlock} from '../../blocks/checkboxBlock'
|
||||
import {IContentBlock} from '../../blocks/contentBlock'
|
||||
import {createCheckboxBlock} from '../../blocks/checkboxBlock'
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
import CheckIcon from '../../widgets/icons/check'
|
||||
import mutator from '../../mutator'
|
||||
import Editable from '../../widgets/editable'
|
||||
@ -13,7 +13,7 @@ import {contentRegistry} from './contentRegistry'
|
||||
import './checkboxElement.scss'
|
||||
|
||||
type Props = {
|
||||
block: IContentBlock
|
||||
block: ContentBlock
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ const CheckboxElement = React.memo((props: Props) => {
|
||||
value={active ? 'on' : 'off'}
|
||||
onChange={(e) => {
|
||||
e.preventDefault()
|
||||
const newBlock = new MutableCheckboxBlock(block)
|
||||
const newBlock = createCheckboxBlock(block)
|
||||
newBlock.fields.value = !active
|
||||
newBlock.title = title
|
||||
setActive(newBlock.fields.value)
|
||||
@ -46,7 +46,7 @@ const CheckboxElement = React.memo((props: Props) => {
|
||||
placeholderText={intl.formatMessage({id: 'ContentBlock.editText', defaultMessage: 'Edit text...'})}
|
||||
onChange={setTitle}
|
||||
onSave={() => {
|
||||
const newBlock = new MutableCheckboxBlock(block)
|
||||
const newBlock = createCheckboxBlock(block)
|
||||
newBlock.title = title
|
||||
newBlock.fields.value = active
|
||||
mutator.updateBlock(newBlock, block, intl.formatMessage({id: 'ContentBlock.editCardCheckboxText', defaultMessage: 'edit card text'}))
|
||||
@ -63,7 +63,7 @@ contentRegistry.registerContentType({
|
||||
getDisplayText: (intl) => intl.formatMessage({id: 'ContentBlock.checkbox', defaultMessage: 'checkbox'}),
|
||||
getIcon: () => <CheckIcon/>,
|
||||
createBlock: async () => {
|
||||
return new MutableCheckboxBlock()
|
||||
return createCheckboxBlock()
|
||||
},
|
||||
createComponent: (block, readonly) => {
|
||||
return (
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IContentBlock} from '../../blocks/contentBlock'
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
import {contentRegistry} from './contentRegistry'
|
||||
@ -14,7 +14,7 @@ import './dividerElement'
|
||||
import './checkboxElement'
|
||||
|
||||
type Props = {
|
||||
block: IContentBlock
|
||||
block: ContentBlock
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
|
@ -4,15 +4,15 @@
|
||||
import {IntlShape} from 'react-intl'
|
||||
|
||||
import {BlockTypes} from '../../blocks/block'
|
||||
import {IContentBlock, MutableContentBlock} from '../../blocks/contentBlock'
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
type ContentHandler = {
|
||||
type: BlockTypes,
|
||||
getDisplayText: (intl: IntlShape) => string,
|
||||
getIcon: () => JSX.Element,
|
||||
createBlock: (rootId: string) => Promise<MutableContentBlock>,
|
||||
createComponent: (block: IContentBlock, readonly: boolean) => JSX.Element,
|
||||
createBlock: (rootId: string) => Promise<ContentBlock>,
|
||||
createComponent: (block: ContentBlock, readonly: boolean) => JSX.Element,
|
||||
}
|
||||
|
||||
class ContentRegistry {
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {MutableDividerBlock} from '../../blocks/dividerBlock'
|
||||
import {DividerBlock, createDividerBlock} from '../../blocks/dividerBlock'
|
||||
import DividerIcon from '../../widgets/icons/divider'
|
||||
|
||||
import {contentRegistry} from './contentRegistry'
|
||||
@ -14,8 +14,8 @@ contentRegistry.registerContentType({
|
||||
type: 'divider',
|
||||
getDisplayText: (intl) => intl.formatMessage({id: 'ContentBlock.divider', defaultMessage: 'divider'}),
|
||||
getIcon: () => <DividerIcon/>,
|
||||
createBlock: async () => {
|
||||
return new MutableDividerBlock()
|
||||
createBlock: async (): Promise<DividerBlock> => {
|
||||
return createDividerBlock()
|
||||
},
|
||||
createComponent: () => <DividerElement/>,
|
||||
})
|
||||
|
@ -2,8 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useEffect, useState} from 'react'
|
||||
|
||||
import {IContentBlock} from '../../blocks/contentBlock'
|
||||
import {MutableImageBlock} from '../../blocks/imageBlock'
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
import {ImageBlock, createImageBlock} from '../../blocks/imageBlock'
|
||||
import octoClient from '../../octoClient'
|
||||
import {Utils} from '../../utils'
|
||||
import ImageIcon from '../../widgets/icons/image'
|
||||
@ -11,7 +11,7 @@ import ImageIcon from '../../widgets/icons/image'
|
||||
import {contentRegistry} from './contentRegistry'
|
||||
|
||||
type Props = {
|
||||
block: IContentBlock
|
||||
block: ContentBlock
|
||||
}
|
||||
|
||||
const ImageElement = React.memo((props: Props): JSX.Element|null => {
|
||||
@ -47,20 +47,20 @@ contentRegistry.registerContentType({
|
||||
getDisplayText: (intl) => intl.formatMessage({id: 'ContentBlock.image', defaultMessage: 'image'}),
|
||||
getIcon: () => <ImageIcon/>,
|
||||
createBlock: async (rootId: string) => {
|
||||
return new Promise<MutableImageBlock>(
|
||||
return new Promise<ImageBlock>(
|
||||
(resolve) => {
|
||||
Utils.selectLocalFile(async (file) => {
|
||||
const fileId = await octoClient.uploadFile(rootId, file)
|
||||
|
||||
const block = new MutableImageBlock()
|
||||
block.fileId = fileId || ''
|
||||
const block = createImageBlock()
|
||||
block.fields.fileId = fileId || ''
|
||||
resolve(block)
|
||||
},
|
||||
'.jpg,.jpeg,.png')
|
||||
},
|
||||
)
|
||||
|
||||
// return new MutableImageBlock()
|
||||
// return new ImageBlock()
|
||||
},
|
||||
createComponent: (block) => <ImageElement block={block}/>,
|
||||
})
|
||||
|
@ -3,8 +3,8 @@
|
||||
import React from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {IContentBlock} from '../../blocks/contentBlock'
|
||||
import {MutableTextBlock} from '../../blocks/textBlock'
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
import {createTextBlock} from '../../blocks/textBlock'
|
||||
import mutator from '../../mutator'
|
||||
import TextIcon from '../../widgets/icons/text'
|
||||
import {MarkdownEditor} from '../markdownEditor'
|
||||
@ -12,7 +12,7 @@ import {MarkdownEditor} from '../markdownEditor'
|
||||
import {contentRegistry} from './contentRegistry'
|
||||
|
||||
type Props = {
|
||||
block: IContentBlock
|
||||
block: ContentBlock
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ contentRegistry.registerContentType({
|
||||
getDisplayText: (intl) => intl.formatMessage({id: 'ContentBlock.text', defaultMessage: 'text'}),
|
||||
getIcon: () => <TextIcon/>,
|
||||
createBlock: async () => {
|
||||
return new MutableTextBlock()
|
||||
return createTextBlock()
|
||||
},
|
||||
createComponent: (block, readonly) => {
|
||||
return (
|
||||
|
@ -5,7 +5,7 @@ import React from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {Card} from '../blocks/card'
|
||||
import {IContentBlock, IContentBlockWithCords} from '../blocks/contentBlock'
|
||||
import {ContentBlock as ContentBlockType, IContentBlockWithCords} from '../blocks/contentBlock'
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
import IconButton from '../widgets/buttons/iconButton'
|
||||
@ -26,7 +26,7 @@ import {contentRegistry} from './content/contentRegistry'
|
||||
import './contentBlock.scss'
|
||||
|
||||
type Props = {
|
||||
block: IContentBlock
|
||||
block: ContentBlockType
|
||||
card: Card
|
||||
readonly: boolean
|
||||
onDrop: (srctBlock: IContentBlockWithCords, dstBlock: IContentBlockWithCords, position: Position) => void
|
||||
@ -43,7 +43,7 @@ const ContentBlock = React.memo((props: Props): JSX.Element => {
|
||||
|
||||
const index = cords.x
|
||||
const colIndex = (cords.y || cords.y === 0) && cords.y > -1 ? cords.y : -1
|
||||
const contentOrder = card.contentOrder.slice()
|
||||
const contentOrder = card.fields.contentOrder.slice()
|
||||
|
||||
const className = 'ContentBlock octo-block'
|
||||
return (
|
||||
|
@ -3,15 +3,12 @@
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {IWorkspace} from '../blocks/workspace'
|
||||
import {getWorkspace} from '../store/workspace'
|
||||
import {useAppSelector} from '../store/hooks'
|
||||
import './emptyCenterPanel.scss'
|
||||
|
||||
type Props = {
|
||||
workspace?: IWorkspace
|
||||
}
|
||||
|
||||
const EmptyCenterPanel = React.memo((props: Props) => {
|
||||
const {workspace} = props
|
||||
const EmptyCenterPanel = React.memo(() => {
|
||||
const workspace = useAppSelector(getWorkspace)
|
||||
|
||||
return (
|
||||
<div className='EmptyCenterPanel'>
|
||||
|
@ -1,21 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {Constants} from '../../constants'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {CardTree, MutableCardTree} from '../../viewModel/cardTree'
|
||||
import useCardListener from '../../hooks/cardListener'
|
||||
|
||||
import './gallery.scss'
|
||||
import GalleryCard from './galleryCard'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
board: Board
|
||||
cards: Card[]
|
||||
activeView: BoardView
|
||||
readonly: boolean
|
||||
addCard: (show: boolean) => Promise<void>
|
||||
selectedCardIds: string[]
|
||||
@ -23,11 +24,9 @@ type Props = {
|
||||
}
|
||||
|
||||
const Gallery = (props: Props): JSX.Element => {
|
||||
const {boardTree} = props
|
||||
const {cards, activeView} = boardTree
|
||||
const visiblePropertyTemplates = boardTree.board.cardProperties.filter((template) => boardTree.activeView.visiblePropertyIds.includes(template.id))
|
||||
const [cardTrees, setCardTrees] = useState<{[key: string]: CardTree | undefined}>({})
|
||||
const isManualSort = activeView.sortOptions.length === 0
|
||||
const {activeView, board, cards} = props
|
||||
const visiblePropertyTemplates = board.fields.cardProperties.filter((template: IPropertyTemplate) => activeView.fields.visiblePropertyIds.includes(template.id))
|
||||
const isManualSort = activeView.fields.sortOptions.length === 0
|
||||
|
||||
const onDropToCard = (srcCard: Card, dstCard: Card) => {
|
||||
Utils.log(`onDropToCard: ${dstCard.title}`)
|
||||
@ -37,7 +36,7 @@ const Gallery = (props: Props): JSX.Element => {
|
||||
const description = draggedCardIds.length > 1 ? `drag ${draggedCardIds.length} cards` : 'drag card'
|
||||
|
||||
// Update dstCard order
|
||||
let cardOrder = Array.from(new Set([...activeView.cardOrder, ...boardTree.cards.map((o) => o.id)]))
|
||||
let cardOrder = Array.from(new Set([...activeView.fields.cardOrder, ...cards.map((o) => o.id)]))
|
||||
const isDraggingDown = cardOrder.indexOf(srcCard.id) <= cardOrder.indexOf(dstCard.id)
|
||||
cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id))
|
||||
let destIndex = cardOrder.indexOf(dstCard.id)
|
||||
@ -51,40 +50,16 @@ const Gallery = (props: Props): JSX.Element => {
|
||||
})
|
||||
}
|
||||
|
||||
const visibleTitle = boardTree.activeView.visiblePropertyIds.includes(Constants.titleColumnId)
|
||||
|
||||
useCardListener(
|
||||
async (blocks) => {
|
||||
cards.forEach(async (c) => {
|
||||
const cardTree = cardTrees[c.id]
|
||||
const newCardTree = cardTree ? MutableCardTree.incrementalUpdate(cardTree, blocks) : await MutableCardTree.sync(c.id)
|
||||
setCardTrees((oldTree) => ({...oldTree, [c.id]: newCardTree}))
|
||||
})
|
||||
},
|
||||
async () => {
|
||||
cards.forEach(async (c) => {
|
||||
const newCardTree = await MutableCardTree.sync(c.id)
|
||||
setCardTrees((oldTree) => ({...oldTree, [c.id]: newCardTree}))
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
cards.forEach(async (c) => {
|
||||
const newCardTree = await MutableCardTree.sync(c.id)
|
||||
setCardTrees((oldTree) => ({...oldTree, [c.id]: newCardTree}))
|
||||
})
|
||||
}, [cards])
|
||||
const visibleTitle = activeView.fields.visiblePropertyIds.includes(Constants.titleColumnId)
|
||||
|
||||
return (
|
||||
<div className='octo-table-body Gallery'>
|
||||
{cards.map((card) => {
|
||||
const cardTree = cardTrees[card.id]
|
||||
if (cardTree) {
|
||||
{cards.filter((c) => c.parentId === board.id).map((card) => {
|
||||
return (
|
||||
<GalleryCard
|
||||
key={card.id + card.updateAt}
|
||||
cardTree={cardTree}
|
||||
card={card}
|
||||
board={board}
|
||||
onClick={props.onCardClicked}
|
||||
visiblePropertyTemplates={visiblePropertyTemplates}
|
||||
visibleTitle={visibleTitle}
|
||||
@ -94,7 +69,6 @@ const Gallery = (props: Props): JSX.Element => {
|
||||
isManualSort={isManualSort}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
|
||||
|
@ -3,10 +3,9 @@
|
||||
import React from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {CardTree} from '../../viewModel/cardTree'
|
||||
import {IContentBlock} from '../../blocks/contentBlock'
|
||||
import {ContentBlock} from '../../blocks/contentBlock'
|
||||
import mutator from '../../mutator'
|
||||
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
@ -21,11 +20,15 @@ import ImageElement from '../content/imageElement'
|
||||
import ContentElement from '../content/contentElement'
|
||||
import PropertyValueElement from '../propertyValueElement'
|
||||
import Tooltip from '../../widgets/tooltip'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getCardContents} from '../../store/contents'
|
||||
import {getCardComments} from '../../store/comments'
|
||||
|
||||
import './galleryCard.scss'
|
||||
|
||||
type Props = {
|
||||
cardTree: CardTree
|
||||
board: Board
|
||||
card: Card
|
||||
onClick: (e: React.MouseEvent, card: Card) => void
|
||||
visiblePropertyTemplates: IPropertyTemplate[]
|
||||
visibleTitle: boolean
|
||||
@ -36,18 +39,20 @@ type Props = {
|
||||
}
|
||||
|
||||
const GalleryCard = React.memo((props: Props) => {
|
||||
const {cardTree} = props
|
||||
const {card, board} = props
|
||||
const intl = useIntl()
|
||||
const [isDragging, isOver, cardRef] = useSortable('card', cardTree.card, props.isManualSort && !props.readonly, props.onDrop)
|
||||
const [isDragging, isOver, cardRef] = useSortable('card', card, props.isManualSort && !props.readonly, props.onDrop)
|
||||
const contents = useAppSelector(getCardContents(card.id))
|
||||
const comments = useAppSelector(getCardComments(card.id))
|
||||
|
||||
const visiblePropertyTemplates = props.visiblePropertyTemplates || []
|
||||
|
||||
let image: IContentBlock | undefined
|
||||
for (let i = 0; i < cardTree.contents.length; ++i) {
|
||||
if (Array.isArray(cardTree.contents[i])) {
|
||||
image = (cardTree.contents[i] as IContentBlock[]).find((c) => c.type === 'image')
|
||||
} else if ((cardTree.contents[i] as IContentBlock).type === 'image') {
|
||||
image = cardTree.contents[i] as IContentBlock
|
||||
let image: ContentBlock | undefined
|
||||
for (let i = 0; i < contents.length; ++i) {
|
||||
if (Array.isArray(contents[i])) {
|
||||
image = (contents[i] as ContentBlock[]).find((c) => c.type === 'image')
|
||||
} else if ((contents[i] as ContentBlock).type === 'image') {
|
||||
image = contents[i] as ContentBlock
|
||||
}
|
||||
|
||||
if (image) {
|
||||
@ -63,7 +68,7 @@ const GalleryCard = React.memo((props: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={(e: React.MouseEvent) => props.onClick(e, cardTree.card)}
|
||||
onClick={(e: React.MouseEvent) => props.onClick(e, card)}
|
||||
style={{opacity: isDragging ? 0.5 : 1}}
|
||||
ref={cardRef}
|
||||
>
|
||||
@ -78,14 +83,14 @@ const GalleryCard = React.memo((props: Props) => {
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'GalleryCard.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deleteBlock(cardTree.card, 'delete card')}
|
||||
onClick={() => mutator.deleteBlock(card, 'delete card')}
|
||||
/>
|
||||
<Menu.Text
|
||||
icon={<DuplicateIcon/>}
|
||||
id='duplicate'
|
||||
name={intl.formatMessage({id: 'GalleryCard.duplicate', defaultMessage: 'Duplicate'})}
|
||||
onClick={() => {
|
||||
mutator.duplicateCard(cardTree.card.id)
|
||||
mutator.duplicateCard(card.id)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
@ -98,7 +103,7 @@ const GalleryCard = React.memo((props: Props) => {
|
||||
</div>}
|
||||
{!image &&
|
||||
<div className='gallery-item'>
|
||||
{cardTree?.contents.map((block) => {
|
||||
{contents.map((block) => {
|
||||
if (Array.isArray(block)) {
|
||||
return block.map((b) => (
|
||||
<ContentElement
|
||||
@ -120,9 +125,9 @@ const GalleryCard = React.memo((props: Props) => {
|
||||
</div>}
|
||||
{props.visibleTitle &&
|
||||
<div className='gallery-title'>
|
||||
{ cardTree.card.icon ? <div className='octo-icon'>{cardTree.card.icon}</div> : undefined }
|
||||
{ card.fields.icon ? <div className='octo-icon'>{card.fields.icon}</div> : undefined }
|
||||
<div key='__title'>
|
||||
{cardTree.card.title ||
|
||||
{card.title ||
|
||||
<FormattedMessage
|
||||
id='KanbanCard.untitled'
|
||||
defaultMessage='Untitled'
|
||||
@ -138,9 +143,11 @@ const GalleryCard = React.memo((props: Props) => {
|
||||
placement='top'
|
||||
>
|
||||
<PropertyValueElement
|
||||
contents={contents}
|
||||
comments={comments}
|
||||
board={board}
|
||||
readOnly={true}
|
||||
card={cardTree.card}
|
||||
cardTree={cardTree}
|
||||
card={card}
|
||||
propertyTemplate={template}
|
||||
emptyDisplayValue=''
|
||||
/>
|
||||
|
@ -1,20 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
/* eslint-disable max-lines */
|
||||
import React, {useRef, useState} from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {IPropertyOption} from '../../blocks/board'
|
||||
import {Board, IPropertyOption, IPropertyTemplate, BoardGroup} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
|
||||
import {CardTree, MutableCardTree} from '../../viewModel/cardTree'
|
||||
|
||||
import useCardListener from '../../hooks/cardListener'
|
||||
|
||||
import KanbanCard from './kanbanCard'
|
||||
import KanbanColumn from './kanbanColumn'
|
||||
import KanbanColumnHeader from './kanbanColumnHeader'
|
||||
@ -23,7 +19,12 @@ import KanbanHiddenColumnItem from './kanbanHiddenColumnItem'
|
||||
import './kanban.scss'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
cards: Card[]
|
||||
groupByProperty?: IPropertyTemplate
|
||||
visibleGroups: BoardGroup[]
|
||||
hiddenGroups: BoardGroup[]
|
||||
selectedCardIds: string[]
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
@ -33,8 +34,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const Kanban = (props: Props) => {
|
||||
const {boardTree} = props
|
||||
const {cards, groupByProperty} = boardTree
|
||||
const {board, activeView, cards, groupByProperty, visibleGroups, hiddenGroups} = props
|
||||
|
||||
if (!groupByProperty) {
|
||||
Utils.assertFailure('Board views must have groupByProperty set')
|
||||
@ -44,36 +44,14 @@ const Kanban = (props: Props) => {
|
||||
const propertyValues = groupByProperty.options || []
|
||||
Utils.log(`${propertyValues.length} propertyValues`)
|
||||
|
||||
const {board, activeView, visibleGroups, hiddenGroups} = boardTree
|
||||
const visiblePropertyTemplates = board.cardProperties.filter((template) => activeView.visiblePropertyIds.includes(template.id))
|
||||
const isManualSort = activeView.sortOptions.length === 0
|
||||
const visiblePropertyTemplates = board.fields.cardProperties.filter((template: IPropertyTemplate) => activeView.fields.visiblePropertyIds.includes(template.id))
|
||||
const isManualSort = activeView.fields.sortOptions.length === 0
|
||||
|
||||
const [cardTrees, setCardTrees] = useState<{[key: string]: CardTree | undefined}>({})
|
||||
const cardTreeRef = useRef<{[key: string]: CardTree | undefined}>()
|
||||
cardTreeRef.current = cardTrees
|
||||
const propertyNameChanged = useCallback(async (option: IPropertyOption, text: string): Promise<void> => {
|
||||
await mutator.changePropertyOptionValue(board, groupByProperty!, option, text)
|
||||
}, [board, groupByProperty])
|
||||
|
||||
useCardListener(
|
||||
async (blocks) => {
|
||||
for (const block of blocks) {
|
||||
const cardTree = cardTreeRef.current && cardTreeRef.current[block.parentId]
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const newCardTree = cardTree ? MutableCardTree.incrementalUpdate(cardTree, blocks) : await MutableCardTree.sync(block.parentId)
|
||||
setCardTrees((oldTree) => ({...oldTree, [block.parentId]: newCardTree}))
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
cards.forEach(async (c) => {
|
||||
const newCardTree = await MutableCardTree.sync(c.id)
|
||||
setCardTrees((oldTree) => ({...oldTree, [c.id]: newCardTree}))
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const propertyNameChanged = async (option: IPropertyOption, text: string): Promise<void> => {
|
||||
await mutator.changePropertyOptionValue(boardTree, boardTree.groupByProperty!, option, text)
|
||||
}
|
||||
|
||||
const addGroupClicked = async () => {
|
||||
const addGroupClicked = useCallback(async () => {
|
||||
Utils.log('onAddGroupClicked')
|
||||
|
||||
const option: IPropertyOption = {
|
||||
@ -82,12 +60,12 @@ const Kanban = (props: Props) => {
|
||||
color: 'propColorDefault',
|
||||
}
|
||||
|
||||
await mutator.insertPropertyOption(boardTree, boardTree.groupByProperty!, option, 'add group')
|
||||
}
|
||||
await mutator.insertPropertyOption(board, groupByProperty!, option, 'add group')
|
||||
}, [board, groupByProperty])
|
||||
|
||||
const orderAfterMoveToColumn = (cardIds: string[], columnId?: string): string[] => {
|
||||
let cardOrder = activeView.cardOrder.slice()
|
||||
const columnGroup = boardTree.visibleGroups.find((g) => g.option.id === columnId)
|
||||
const orderAfterMoveToColumn = useCallback((cardIds: string[], columnId?: string): string[] => {
|
||||
let cardOrder = activeView.fields.cardOrder.slice()
|
||||
const columnGroup = visibleGroups.find((g) => g.option.id === columnId)
|
||||
const columnCards = columnGroup?.cards
|
||||
if (!columnCards || columnCards.length === 0) {
|
||||
return cardOrder
|
||||
@ -98,9 +76,9 @@ const Kanban = (props: Props) => {
|
||||
const lastCardIndex = cardOrder.indexOf(lastCardId)
|
||||
cardOrder.splice(lastCardIndex + 1, 0, ...cardIds)
|
||||
return cardOrder
|
||||
}
|
||||
}, [activeView, visibleGroups])
|
||||
|
||||
const onDropToColumn = async (option: IPropertyOption, card?: Card, dstOption?: IPropertyOption) => {
|
||||
const onDropToColumn = useCallback(async (option: IPropertyOption, card?: Card, dstOption?: IPropertyOption) => {
|
||||
const {selectedCardIds} = props
|
||||
const optionId = option ? option.id : undefined
|
||||
|
||||
@ -109,34 +87,31 @@ const Kanban = (props: Props) => {
|
||||
draggedCardIds = Array.from(new Set(selectedCardIds).add(card.id))
|
||||
}
|
||||
|
||||
Utils.assertValue(boardTree)
|
||||
|
||||
if (draggedCardIds.length > 0) {
|
||||
const orderedCards = boardTree.orderedCards()
|
||||
const cardsById: { [key: string]: Card } = orderedCards.reduce((acc: { [key: string]: Card }, c: Card): { [key: string]: Card } => {
|
||||
await mutator.performAsUndoGroup(async () => {
|
||||
const cardsById: { [key: string]: Card } = cards.reduce((acc: { [key: string]: Card }, c: Card): { [key: string]: Card } => {
|
||||
acc[c.id] = c
|
||||
return acc
|
||||
}, {})
|
||||
const draggedCards: Card[] = draggedCardIds.map((o: string) => cardsById[o])
|
||||
await mutator.performAsUndoGroup(async () => {
|
||||
const draggedCards: Card[] = draggedCardIds.map((o: string) => cardsById[o]).filter((c) => c)
|
||||
const description = draggedCards.length > 1 ? `drag ${draggedCards.length} cards` : 'drag card'
|
||||
const awaits = []
|
||||
for (const draggedCard of draggedCards) {
|
||||
Utils.log(`ondrop. Card: ${draggedCard.title}, column: ${optionId}`)
|
||||
const oldValue = draggedCard.properties[boardTree.groupByProperty!.id]
|
||||
const oldValue = draggedCard.fields.properties[groupByProperty!.id]
|
||||
if (optionId !== oldValue) {
|
||||
awaits.push(mutator.changePropertyValue(draggedCard, boardTree.groupByProperty!.id, optionId, description))
|
||||
awaits.push(mutator.changePropertyValue(draggedCard, groupByProperty!.id, optionId, description))
|
||||
}
|
||||
}
|
||||
const newOrder = orderAfterMoveToColumn(draggedCardIds, optionId)
|
||||
awaits.push(mutator.changeViewCardOrder(boardTree.activeView, newOrder, description))
|
||||
awaits.push(mutator.changeViewCardOrder(activeView, newOrder, description))
|
||||
await Promise.all(awaits)
|
||||
})
|
||||
} else if (dstOption) {
|
||||
Utils.log(`ondrop. Header option: ${dstOption.value}, column: ${option?.value}`)
|
||||
|
||||
// Move option to new index
|
||||
const visibleOptionIds = boardTree.visibleGroups.map((o) => o.option.id)
|
||||
const visibleOptionIds = visibleGroups.map((o) => o.option.id)
|
||||
|
||||
const srcIndex = visibleOptionIds.indexOf(dstOption.id)
|
||||
const destIndex = visibleOptionIds.indexOf(option.id)
|
||||
@ -146,32 +121,31 @@ const Kanban = (props: Props) => {
|
||||
|
||||
await mutator.changeViewVisibleOptionIds(activeView, visibleOptionIds)
|
||||
}
|
||||
}
|
||||
}, [cards, visibleGroups, activeView, groupByProperty, props.selectedCardIds])
|
||||
|
||||
const onDropToCard = async (srcCard: Card, dstCard: Card) => {
|
||||
const onDropToCard = useCallback(async (srcCard: Card, dstCard: Card) => {
|
||||
if (srcCard.id === dstCard.id) {
|
||||
return
|
||||
}
|
||||
Utils.log(`onDropToCard: ${dstCard.title}`)
|
||||
const {selectedCardIds} = props
|
||||
const optionId = dstCard.properties[activeView.groupById!]
|
||||
const optionId = dstCard.fields.properties[groupByProperty.id]
|
||||
|
||||
const draggedCardIds = Array.from(new Set(selectedCardIds).add(srcCard.id))
|
||||
|
||||
const description = draggedCardIds.length > 1 ? `drag ${draggedCardIds.length} cards` : 'drag card'
|
||||
|
||||
// Update dstCard order
|
||||
const orderedCards = boardTree.orderedCards()
|
||||
const cardsById: { [key: string]: Card } = orderedCards.reduce((acc: { [key: string]: Card }, card: Card): { [key: string]: Card } => {
|
||||
const cardsById: { [key: string]: Card } = cards.reduce((acc: { [key: string]: Card }, card: Card): { [key: string]: Card } => {
|
||||
acc[card.id] = card
|
||||
return acc
|
||||
}, {})
|
||||
const draggedCards: Card[] = draggedCardIds.map((o: string) => cardsById[o])
|
||||
let cardOrder = orderedCards.map((o) => o.id)
|
||||
const draggedCards: Card[] = draggedCardIds.map((o: string) => cardsById[o]).filter((c) => c)
|
||||
let cardOrder = cards.map((o) => o.id)
|
||||
const isDraggingDown = cardOrder.indexOf(srcCard.id) <= cardOrder.indexOf(dstCard.id)
|
||||
cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id))
|
||||
let destIndex = cardOrder.indexOf(dstCard.id)
|
||||
if (srcCard.properties[boardTree.groupByProperty!.id] === optionId && isDraggingDown) {
|
||||
if (srcCard.fields.properties[groupByProperty!.id] === optionId && isDraggingDown) {
|
||||
// If the cards are in the same column and dragging down, drop after the target dstCard
|
||||
destIndex += 1
|
||||
}
|
||||
@ -182,15 +156,15 @@ const Kanban = (props: Props) => {
|
||||
const awaits = []
|
||||
for (const draggedCard of draggedCards) {
|
||||
Utils.log(`draggedCard: ${draggedCard.title}, column: ${optionId}`)
|
||||
const oldOptionId = draggedCard.properties[boardTree.groupByProperty!.id]
|
||||
const oldOptionId = draggedCard.fields.properties[groupByProperty!.id]
|
||||
if (optionId !== oldOptionId) {
|
||||
awaits.push(mutator.changePropertyValue(draggedCard, boardTree.groupByProperty!.id, optionId, description))
|
||||
awaits.push(mutator.changePropertyValue(draggedCard, groupByProperty!.id, optionId, description))
|
||||
}
|
||||
}
|
||||
await Promise.all(awaits)
|
||||
await mutator.changeViewCardOrder(activeView, cardOrder, description)
|
||||
})
|
||||
}
|
||||
}, [cards, activeView, groupByProperty, props.selectedCardIds])
|
||||
|
||||
return (
|
||||
<div className='Kanban'>
|
||||
@ -204,8 +178,10 @@ const Kanban = (props: Props) => {
|
||||
<KanbanColumnHeader
|
||||
key={group.option.id}
|
||||
group={group}
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
intl={props.intl}
|
||||
groupByProperty={groupByProperty}
|
||||
addCard={props.addCard}
|
||||
readonly={props.readonly}
|
||||
propertyNameChanged={propertyNameChanged}
|
||||
@ -254,7 +230,7 @@ const Kanban = (props: Props) => {
|
||||
{group.cards.map((card) => (
|
||||
<KanbanCard
|
||||
card={card}
|
||||
cardTree={cardTrees[card.id]}
|
||||
board={board}
|
||||
visiblePropertyTemplates={visiblePropertyTemplates}
|
||||
key={card.id}
|
||||
readonly={props.readonly}
|
||||
@ -290,7 +266,7 @@ const Kanban = (props: Props) => {
|
||||
<KanbanHiddenColumnItem
|
||||
key={group.option.id}
|
||||
group={group}
|
||||
boardTree={boardTree}
|
||||
activeView={activeView}
|
||||
intl={props.intl}
|
||||
readonly={props.readonly}
|
||||
onDrop={(card: Card) => onDropToColumn(group.option, card)}
|
||||
|
@ -3,7 +3,7 @@
|
||||
import React from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
import mutator from '../../mutator'
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
@ -13,15 +13,17 @@ import OptionsIcon from '../../widgets/icons/options'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import {useSortable} from '../../hooks/sortable'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getCardContents} from '../../store/contents'
|
||||
import {getCardComments} from '../../store/comments'
|
||||
|
||||
import './kanbanCard.scss'
|
||||
import PropertyValueElement from '../propertyValueElement'
|
||||
import {CardTree} from '../../viewModel/cardTree'
|
||||
import Tooltip from '../../widgets/tooltip'
|
||||
|
||||
type Props = {
|
||||
card: Card
|
||||
cardTree?: CardTree
|
||||
board: Board
|
||||
visiblePropertyTemplates: IPropertyTemplate[]
|
||||
isSelected: boolean
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
@ -32,7 +34,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const KanbanCard = React.memo((props: Props) => {
|
||||
const {card} = props
|
||||
const {card, board} = props
|
||||
const intl = useIntl()
|
||||
const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly, props.onDrop)
|
||||
const visiblePropertyTemplates = props.visiblePropertyTemplates || []
|
||||
@ -41,6 +43,9 @@ const KanbanCard = React.memo((props: Props) => {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
const contents = useAppSelector(getCardContents(card.id))
|
||||
const comments = useAppSelector(getCardComments(card.id))
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={props.readonly ? () => null : cardRef}
|
||||
@ -85,7 +90,7 @@ const KanbanCard = React.memo((props: Props) => {
|
||||
}
|
||||
|
||||
<div className='octo-icontitle'>
|
||||
{ card.icon ? <div className='octo-icon'>{card.icon}</div> : undefined }
|
||||
{ card.fields.icon ? <div className='octo-icon'>{card.fields.icon}</div> : undefined }
|
||||
<div key='__title'>{card.title || intl.formatMessage({id: 'KanbanCard.untitled', defaultMessage: 'Untitled'})}</div>
|
||||
</div>
|
||||
{visiblePropertyTemplates.map((template) => (
|
||||
@ -94,9 +99,11 @@ const KanbanCard = React.memo((props: Props) => {
|
||||
title={template.name}
|
||||
>
|
||||
<PropertyValueElement
|
||||
board={board}
|
||||
readOnly={true}
|
||||
card={card}
|
||||
cardTree={props.cardTree}
|
||||
contents={contents}
|
||||
comments={comments}
|
||||
propertyTemplate={template}
|
||||
emptyDisplayValue=''
|
||||
/>
|
||||
|
@ -6,10 +6,10 @@ import {FormattedMessage, IntlShape} from 'react-intl'
|
||||
import {useDrop, useDrag} from 'react-dnd'
|
||||
|
||||
import {Constants} from '../../constants'
|
||||
import {IPropertyOption} from '../../blocks/board'
|
||||
import {IPropertyOption, IPropertyTemplate, Board, BoardGroup} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import {Card} from '../../blocks/card'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
import AddIcon from '../../widgets/icons/add'
|
||||
@ -22,8 +22,10 @@ import Editable from '../../widgets/editable'
|
||||
import Label from '../../widgets/label'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
group: BoardTreeGroup
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
group: BoardGroup
|
||||
groupByProperty?: IPropertyTemplate
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
addCard: (groupByOptionId?: string) => Promise<void>
|
||||
@ -32,8 +34,7 @@ type Props = {
|
||||
}
|
||||
|
||||
export default function KanbanColumnHeader(props: Props): JSX.Element {
|
||||
const {boardTree, intl, group} = props
|
||||
const {activeView} = boardTree
|
||||
const {board, activeView, intl, group, groupByProperty} = props
|
||||
const [groupTitle, setGroupTitle] = useState(group.option.value)
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null)
|
||||
@ -78,13 +79,13 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
|
||||
title={intl.formatMessage({
|
||||
id: 'BoardComponent.no-property-title',
|
||||
defaultMessage: 'Items with an empty {property} property will go here. This column cannot be removed.',
|
||||
}, {property: boardTree.groupByProperty!.name})}
|
||||
}, {property: groupByProperty!.name})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='BoardComponent.no-property'
|
||||
defaultMessage='No {property}'
|
||||
values={{
|
||||
property: boardTree.groupByProperty!.name,
|
||||
property: groupByProperty!.name,
|
||||
}}
|
||||
/>
|
||||
</Label>}
|
||||
@ -126,7 +127,7 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
|
||||
id='delete'
|
||||
icon={<DeleteIcon/>}
|
||||
name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)}
|
||||
onClick={() => mutator.deletePropertyOption(board, groupByProperty!, group.option)}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
{Object.entries(Constants.menuColors).map(([key, color]) => (
|
||||
@ -134,7 +135,7 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
|
||||
key={key}
|
||||
id={key}
|
||||
name={color}
|
||||
onClick={() => mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, key)}
|
||||
onClick={() => mutator.changePropertyOptionColor(board, groupByProperty!, group.option, key)}
|
||||
/>
|
||||
))}
|
||||
</>}
|
||||
|
@ -6,25 +6,25 @@ import {IntlShape} from 'react-intl'
|
||||
import {useDrop} from 'react-dnd'
|
||||
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import ShowIcon from '../../widgets/icons/show'
|
||||
import Label from '../../widgets/label'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {BoardGroup} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
group: BoardTreeGroup
|
||||
activeView: BoardView
|
||||
group: BoardGroup
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
onDrop: (card: Card) => void
|
||||
}
|
||||
|
||||
export default function KanbanHiddenColumnItem(props: Props): JSX.Element {
|
||||
const {boardTree, intl, group} = props
|
||||
const {activeView} = boardTree
|
||||
const {activeView, intl, group} = props
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: 'card',
|
||||
collect: (monitor) => ({
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useRef, useMemo} from 'react'
|
||||
import EasyMDE from 'easymde'
|
||||
import SimpleMDE from 'react-simplemde-editor'
|
||||
|
||||
import {Utils} from '../utils'
|
||||
@ -10,7 +9,6 @@ import './markdownEditor.scss'
|
||||
type Props = {
|
||||
text?: string
|
||||
placeholderText?: string
|
||||
uniqueId?: string
|
||||
className?: string
|
||||
readonly?: boolean
|
||||
|
||||
@ -21,11 +19,29 @@ type Props = {
|
||||
}
|
||||
|
||||
const MarkdownEditor = (props: Props): JSX. Element => {
|
||||
const {placeholderText, uniqueId, onFocus, onBlur, onChange, text} = props
|
||||
const {placeholderText, onFocus, onBlur, onChange, text} = props
|
||||
const [uniqueId] = useState(Utils.createGuid())
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [active, setActive] = useState(false)
|
||||
const [editorInstance, setEditorInstance] = useState<EasyMDE>()
|
||||
const [editorInstance, setEditorInstance] = useState<any>()
|
||||
const editorOptions = useMemo(() => ({
|
||||
autoDownloadFontAwesome: true,
|
||||
toolbar: false,
|
||||
status: false,
|
||||
autofocus: true,
|
||||
spellChecker: true,
|
||||
nativeSpellcheck: true,
|
||||
minHeight: '10px',
|
||||
shortcuts: {
|
||||
toggleStrikethrough: 'Cmd-.',
|
||||
togglePreview: null,
|
||||
drawImage: null,
|
||||
drawLink: null,
|
||||
toggleSideBySide: null,
|
||||
toggleFullScreen: null,
|
||||
},
|
||||
}), [])
|
||||
|
||||
const showEditor = (): void => {
|
||||
const cm = editorInstance?.codemirror
|
||||
@ -52,20 +68,35 @@ const MarkdownEditor = (props: Props): JSX. Element => {
|
||||
const stateAndPropsRef = useRef(stateAndPropsValue)
|
||||
stateAndPropsRef.current = stateAndPropsValue
|
||||
|
||||
const mdeOptions = useMemo(() => ({
|
||||
autoDownloadFontAwesome: true,
|
||||
toolbar: false,
|
||||
status: false,
|
||||
spellChecker: true,
|
||||
nativeSpellcheck: true,
|
||||
minHeight: '10px',
|
||||
shortcuts: {
|
||||
toggleStrikethrough: 'Cmd-.',
|
||||
togglePreview: null,
|
||||
drawImage: null,
|
||||
drawLink: null,
|
||||
toggleSideBySide: null,
|
||||
toggleFullScreen: null,
|
||||
const editorEvents = useMemo(() => ({
|
||||
change: (instance: any) => {
|
||||
if (stateAndPropsRef.current.isEditing) {
|
||||
const newText = instance.getValue()
|
||||
stateAndPropsRef.current.onChange?.(newText)
|
||||
}
|
||||
},
|
||||
blur: (instance: any) => {
|
||||
const newText = instance.getValue()
|
||||
const oldText = text || ''
|
||||
if (newText !== oldText && stateAndPropsRef.current.onChange) {
|
||||
stateAndPropsRef.current.onChange(newText)
|
||||
}
|
||||
|
||||
stateAndPropsRef.current.setActive(false)
|
||||
|
||||
if (stateAndPropsRef.current.onBlur) {
|
||||
stateAndPropsRef.current.onBlur(newText)
|
||||
}
|
||||
|
||||
stateAndPropsRef.current.setIsEditing(false)
|
||||
},
|
||||
focus: () => {
|
||||
stateAndPropsRef.current.setActive(true)
|
||||
stateAndPropsRef.current.setIsEditing(true)
|
||||
|
||||
if (stateAndPropsRef.current.onFocus) {
|
||||
stateAndPropsRef.current.onFocus()
|
||||
}
|
||||
},
|
||||
}), [])
|
||||
|
||||
@ -106,50 +137,10 @@ const MarkdownEditor = (props: Props): JSX. Element => {
|
||||
>
|
||||
<SimpleMDE
|
||||
id={uniqueId}
|
||||
getMdeInstance={(instance) => {
|
||||
setEditorInstance(instance)
|
||||
|
||||
// BUGBUG: This breaks auto-lists
|
||||
// instance.codemirror.setOption("extraKeys", {
|
||||
// "Ctrl-Enter": (cm) => {
|
||||
// cm.getInputField().blur()
|
||||
// }
|
||||
// })
|
||||
}}
|
||||
getMdeInstance={setEditorInstance}
|
||||
value={text}
|
||||
|
||||
events={{
|
||||
change: (instance: any) => {
|
||||
if (stateAndPropsRef.current.isEditing) {
|
||||
const newText = instance.getValue()
|
||||
stateAndPropsRef.current.onChange?.(newText)
|
||||
}
|
||||
},
|
||||
blur: (instance: any) => {
|
||||
const newText = instance.getValue()
|
||||
const oldText = text || ''
|
||||
if (newText !== oldText && stateAndPropsRef.current.onChange) {
|
||||
stateAndPropsRef.current.onChange(newText)
|
||||
}
|
||||
|
||||
stateAndPropsRef.current.setActive(false)
|
||||
|
||||
if (stateAndPropsRef.current.onBlur) {
|
||||
stateAndPropsRef.current.onBlur(newText)
|
||||
}
|
||||
|
||||
stateAndPropsRef.current.setIsEditing(false)
|
||||
},
|
||||
focus: () => {
|
||||
stateAndPropsRef.current.setActive(true)
|
||||
stateAndPropsRef.current.setIsEditing(true)
|
||||
|
||||
if (stateAndPropsRef.current.onFocus) {
|
||||
stateAndPropsRef.current.onFocus()
|
||||
}
|
||||
},
|
||||
}}
|
||||
options={mdeOptions}
|
||||
events={editorEvents}
|
||||
options={editorOptions}
|
||||
/>
|
||||
</div>)
|
||||
|
||||
|
@ -8,19 +8,19 @@ import {render} from '@testing-library/react'
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {IUser} from '../../../user'
|
||||
import {MutableCard} from '../../../blocks/card'
|
||||
import {createCard} from '../../../blocks/card'
|
||||
|
||||
import CreatedBy from './createdBy'
|
||||
|
||||
describe('components/properties/createdBy', () => {
|
||||
test('should match snapshot', () => {
|
||||
const card = new MutableCard()
|
||||
const card = createCard()
|
||||
card.createdBy = 'user-id-1'
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
currentWorkspaceUsers: {
|
||||
byId: {
|
||||
users: {
|
||||
workspaceUsers: {
|
||||
'user-id-1': {username: 'username_1'} as IUser,
|
||||
},
|
||||
},
|
||||
|
@ -5,7 +5,7 @@ exports[`componnets/properties/lastModifiedAt should match snapshot 1`] = `
|
||||
<div
|
||||
class="LastModifiedAt octo-propertyvalue"
|
||||
>
|
||||
June 10, 4:22 PM
|
||||
June 15, 4:22 PM
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -3,16 +3,10 @@
|
||||
|
||||
import React from 'react'
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import {IntlProvider} from 'react-intl'
|
||||
|
||||
import {CardTree, CardTreeContext, MutableCardTree} from '../../../viewModel/cardTree'
|
||||
|
||||
import {MutableCard} from '../../../blocks/card'
|
||||
|
||||
import {MutableBoardTree} from '../../../viewModel/boardTree'
|
||||
import {MutableBoard} from '../../../blocks/board'
|
||||
import {MutableBlock} from '../../../blocks/block'
|
||||
import {createCard} from '../../../blocks/card'
|
||||
import {createCommentBlock} from '../../../blocks/commentBlock'
|
||||
|
||||
import LastModifiedAt from './lastModifiedAt'
|
||||
|
||||
@ -20,35 +14,22 @@ const wrapIntl = (children: any) => <IntlProvider locale='en'>{children}</IntlPr
|
||||
|
||||
describe('componnets/properties/lastModifiedAt', () => {
|
||||
test('should match snapshot', () => {
|
||||
const cardTree = new MutableCardTree(
|
||||
new MutableCard({
|
||||
updateAt: Date.parse('15 Jun 2021 16:22:00'),
|
||||
}),
|
||||
)
|
||||
|
||||
const card = new MutableCard()
|
||||
const card = createCard()
|
||||
card.id = 'card-id-1'
|
||||
card.modifiedBy = 'user-id-1'
|
||||
card.updateAt = Date.parse('10 Jun 2021 16:22:00')
|
||||
|
||||
const boardTree = new MutableBoardTree(new MutableBoard([]), {})
|
||||
const block = new MutableBlock()
|
||||
block.modifiedBy = 'user-id-1'
|
||||
block.parentId = 'card-id-1'
|
||||
block.type = 'comment'
|
||||
block.updateAt = Date.parse('15 Jun 2021 16:22:00')
|
||||
boardTree.rawBlocks.push(block)
|
||||
|
||||
const cardTrees:{ [key: string]: CardTree | undefined } = {}
|
||||
cardTrees[card.id] = new MutableCardTree(card)
|
||||
const comment = createCommentBlock()
|
||||
comment.modifiedBy = 'user-id-1'
|
||||
comment.parentId = 'card-id-1'
|
||||
comment.updateAt = Date.parse('15 Jun 2021 16:22:00')
|
||||
|
||||
const component = wrapIntl(
|
||||
<CardTreeContext.Provider value={cardTree}>
|
||||
<LastModifiedAt
|
||||
card={card}
|
||||
cardTree={cardTree}
|
||||
/>
|
||||
</CardTreeContext.Provider>,
|
||||
contents={[]}
|
||||
comments={[comment]}
|
||||
/>,
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
|
@ -6,23 +6,24 @@ import React from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {Card} from '../../../blocks/card'
|
||||
import {CardTree} from '../../../viewModel/cardTree'
|
||||
import {IBlock} from '../../../blocks/block'
|
||||
import {Block} from '../../../blocks/block'
|
||||
import {ContentBlock} from '../../../blocks/contentBlock'
|
||||
import {CommentBlock} from '../../../blocks/commentBlock'
|
||||
import {Utils} from '../../../utils'
|
||||
|
||||
type Props = {
|
||||
card: Card,
|
||||
cardTree?: CardTree
|
||||
contents: Array<ContentBlock|ContentBlock[]>
|
||||
comments: CommentBlock[]
|
||||
}
|
||||
|
||||
const LastModifiedAt = (props: Props): JSX.Element => {
|
||||
const intl = useIntl()
|
||||
|
||||
let latestBlock: IBlock = props.card
|
||||
if (props.cardTree) {
|
||||
const sortedBlocks = props.cardTree.allBlocks.
|
||||
filter((block) => block.parentId === props.card.id || block.id === props.card.id).
|
||||
sort((a, b) => b.updateAt - a.updateAt)
|
||||
let latestBlock: Block = props.card
|
||||
if (props.card) {
|
||||
const allBlocks = [props.card, ...props.contents.flat(), ...props.comments]
|
||||
const sortedBlocks = allBlocks.sort((a, b) => b.updateAt - a.updateAt)
|
||||
|
||||
latestBlock = sortedBlocks.length > 0 ? sortedBlocks[0] : latestBlock
|
||||
}
|
||||
|
@ -7,45 +7,30 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {render} from '@testing-library/react'
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {MutableCardTree, CardTreeContext} from '../../../viewModel/cardTree'
|
||||
import {MutableCard} from '../../../blocks/card'
|
||||
import {createCard} from '../../../blocks/card'
|
||||
import {IUser} from '../../../user'
|
||||
|
||||
import {MutableBoardTree} from '../../../viewModel/boardTree'
|
||||
import {createBoard} from '../../../blocks/board'
|
||||
|
||||
import {MutableBoard} from '../../../blocks/board'
|
||||
|
||||
import {MutableBlock} from '../../../blocks/block'
|
||||
import {createCommentBlock} from '../../../blocks/commentBlock'
|
||||
|
||||
import LastModifiedBy from './lastModifiedBy'
|
||||
|
||||
describe('components/properties/lastModifiedBy', () => {
|
||||
test('should match snapshot', () => {
|
||||
const cardTree = new MutableCardTree(
|
||||
new MutableCard({
|
||||
updateAt: Date.parse('15 Jun 2021 16:22:00 +05:30'),
|
||||
modifiedBy: 'user-id-1',
|
||||
}),
|
||||
|
||||
)
|
||||
|
||||
const card = new MutableCard()
|
||||
const card = createCard()
|
||||
card.id = 'card-id-1'
|
||||
card.modifiedBy = 'user-id-1'
|
||||
|
||||
const boardTree = new MutableBoardTree(new MutableBoard([]), {
|
||||
'user-id-1': {username: 'username_1'} as IUser,
|
||||
})
|
||||
const block = new MutableBlock()
|
||||
block.modifiedBy = 'user-id-1'
|
||||
block.parentId = 'card-id-1'
|
||||
block.type = 'comment'
|
||||
boardTree.rawBlocks.push(block)
|
||||
const board = createBoard()
|
||||
const comment = createCommentBlock()
|
||||
comment.modifiedBy = 'user-id-1'
|
||||
comment.parentId = 'card-id-1'
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
currentWorkspaceUsers: {
|
||||
byId: {
|
||||
users: {
|
||||
workspaceUsers: {
|
||||
'user-id-1': {username: 'username_1'} as IUser,
|
||||
},
|
||||
},
|
||||
@ -53,12 +38,12 @@ describe('components/properties/lastModifiedBy', () => {
|
||||
|
||||
const component = (
|
||||
<ReduxProvider store={store}>
|
||||
<CardTreeContext.Provider value={cardTree}>
|
||||
<LastModifiedBy
|
||||
card={card}
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
contents={[]}
|
||||
comments={[comment]}
|
||||
/>
|
||||
</CardTreeContext.Provider>
|
||||
</ReduxProvider>
|
||||
)
|
||||
|
||||
|
@ -5,24 +5,27 @@ import React from 'react'
|
||||
|
||||
import {IUser} from '../../../user'
|
||||
import {Card} from '../../../blocks/card'
|
||||
import {BoardTree} from '../../../viewModel/boardTree'
|
||||
import {IBlock} from '../../../blocks/block'
|
||||
import {getCurrentWorkspaceUsersById} from '../../../store/currentWorkspaceUsers'
|
||||
import {ContentBlock} from '../../../blocks/contentBlock'
|
||||
import {CommentBlock} from '../../../blocks/commentBlock'
|
||||
import {Board} from '../../../blocks/board'
|
||||
import {Block} from '../../../blocks/block'
|
||||
import {getWorkspaceUsers} from '../../../store/users'
|
||||
import {useAppSelector} from '../../../store/hooks'
|
||||
|
||||
type Props = {
|
||||
card: Card,
|
||||
boardTree?: BoardTree,
|
||||
board?: Board,
|
||||
contents: Array<ContentBlock|ContentBlock[]>
|
||||
comments: CommentBlock[],
|
||||
}
|
||||
|
||||
const LastModifiedBy = (props: Props): JSX.Element => {
|
||||
const workspaceUsersById = useAppSelector<{[key:string]: IUser}>(getCurrentWorkspaceUsersById)
|
||||
const workspaceUsersById = useAppSelector<{[key:string]: IUser}>(getWorkspaceUsers)
|
||||
|
||||
let latestBlock: IBlock = props.card
|
||||
if (props.boardTree) {
|
||||
const sortedBlocks = props.boardTree?.allBlocks.
|
||||
filter((block) => block.parentId === props.card.id || block.id === props.card.id).
|
||||
sort((a, b) => b.updateAt - a.updateAt)
|
||||
let latestBlock: Block = props.card
|
||||
if (props.board) {
|
||||
const allBlocks: Block[] = [props.card, ...props.contents.flat(), ...props.comments]
|
||||
const sortedBlocks = allBlocks.sort((a, b) => b.updateAt - a.updateAt)
|
||||
|
||||
latestBlock = sortedBlocks.length > 0 ? sortedBlocks[0] : latestBlock
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ exports[`components/properties/user user dropdown open 1`] = `
|
||||
<span
|
||||
id="aria-context"
|
||||
>
|
||||
Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
option username-1 selected, 1 of 1. 1 result available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
@ -284,9 +284,11 @@ exports[`components/properties/user user dropdown open 1`] = `
|
||||
class=" css-g29tl0-MenuList"
|
||||
>
|
||||
<div
|
||||
class=" css-gg45go-NoOptionsMessage"
|
||||
class=" css-1pe17ep-option"
|
||||
id="react-select-4-option-0"
|
||||
tabindex="-1"
|
||||
>
|
||||
No options
|
||||
username-1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,9 +17,9 @@ const wrapIntl = (children: any) => <IntlProvider locale='en'>{children}</IntlPr
|
||||
|
||||
describe('components/properties/user', () => {
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
currentWorkspaceUsers: {
|
||||
byId: {
|
||||
const state = {
|
||||
users: {
|
||||
workspaceUsers: {
|
||||
'user-id-1': {
|
||||
id: 'user-id-1',
|
||||
username: 'username-1',
|
||||
@ -31,9 +31,10 @@ describe('components/properties/user', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
test('not readonly not existing user', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<UserProperty
|
||||
@ -56,6 +57,7 @@ describe('components/properties/user', () => {
|
||||
})
|
||||
|
||||
test('not readonly', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<UserProperty
|
||||
@ -78,6 +80,7 @@ describe('components/properties/user', () => {
|
||||
})
|
||||
|
||||
test('readonly view', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<UserProperty
|
||||
@ -100,6 +103,7 @@ describe('components/properties/user', () => {
|
||||
})
|
||||
|
||||
test('user dropdown open', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<UserProperty
|
||||
|
@ -5,7 +5,7 @@ import React from 'react'
|
||||
import Select from 'react-select'
|
||||
|
||||
import {IUser} from '../../../user'
|
||||
import {getCurrentWorkspaceUsers, getCurrentWorkspaceUsersById} from '../../../store/currentWorkspaceUsers'
|
||||
import {getWorkspaceUsersList, getWorkspaceUsers} from '../../../store/users'
|
||||
import {useAppSelector} from '../../../store/hooks'
|
||||
|
||||
import './user.scss'
|
||||
@ -18,8 +18,8 @@ type Props = {
|
||||
}
|
||||
|
||||
const UserProperty = (props: Props): JSX.Element => {
|
||||
const workspaceUsers = useAppSelector<IUser[]>(getCurrentWorkspaceUsers)
|
||||
const workspaceUsersById = useAppSelector<{[key:string]: IUser}>(getCurrentWorkspaceUsersById)
|
||||
const workspaceUsers = useAppSelector<IUser[]>(getWorkspaceUsersList)
|
||||
const workspaceUsersById = useAppSelector<{[key:string]: IUser}>(getWorkspaceUsers)
|
||||
|
||||
if (props.readonly) {
|
||||
return (<div className='UserProperty octo-propertyvalue readonly'>{workspaceUsersById[props.value]?.username || props.value}</div>)
|
||||
|
@ -4,12 +4,13 @@
|
||||
import React, {useState, useCallback, useEffect, useRef} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {IPropertyOption, IPropertyTemplate, PropertyType} from '../blocks/board'
|
||||
import {Board, IPropertyOption, IPropertyTemplate, PropertyType} from '../blocks/board'
|
||||
import {Card} from '../blocks/card'
|
||||
import {ContentBlock} from '../blocks/contentBlock'
|
||||
import {CommentBlock} from '../blocks/commentBlock'
|
||||
import mutator from '../mutator'
|
||||
import {OctoUtils} from '../octoUtils'
|
||||
import {Utils} from '../utils'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
import Editable from '../widgets/editable'
|
||||
import ValueSelector from '../widgets/valueSelector'
|
||||
|
||||
@ -18,8 +19,6 @@ import Label from '../widgets/label'
|
||||
import EditableDayPicker from '../widgets/editableDayPicker'
|
||||
import Switch from '../widgets/switch'
|
||||
|
||||
import {CardTree} from '../viewModel/cardTree'
|
||||
|
||||
import UserProperty from './properties/user/user'
|
||||
import MultiSelectProperty from './properties/multiSelect'
|
||||
import URLProperty from './properties/link/link'
|
||||
@ -29,21 +28,22 @@ import CreatedAt from './properties/createdAt/createdAt'
|
||||
import CreatedBy from './properties/createdBy/createdBy'
|
||||
|
||||
type Props = {
|
||||
boardTree?: BoardTree
|
||||
cardTree?: CardTree
|
||||
board: Board
|
||||
readOnly: boolean
|
||||
card: Card
|
||||
contents: Array<ContentBlock|ContentBlock[]>
|
||||
comments: CommentBlock[]
|
||||
propertyTemplate: IPropertyTemplate
|
||||
emptyDisplayValue: string
|
||||
}
|
||||
|
||||
const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
const [value, setValue] = useState(props.card.properties[props.propertyTemplate.id] || '')
|
||||
const [value, setValue] = useState(props.card.fields.properties[props.propertyTemplate.id] || '')
|
||||
const [serverValue, setServerValue] = useState(props.card.fields.properties[props.propertyTemplate.id] || '')
|
||||
|
||||
const {card, propertyTemplate, readOnly, emptyDisplayValue, boardTree, cardTree} = props
|
||||
const {card, propertyTemplate, readOnly, emptyDisplayValue, board, contents, comments} = props
|
||||
const intl = useIntl()
|
||||
const propertyValue = card.properties[propertyTemplate.id]
|
||||
const propertyValue = card.fields.properties[propertyTemplate.id]
|
||||
const displayValue = OctoUtils.propertyDisplayValue(card, propertyValue, propertyTemplate, intl)
|
||||
const finalDisplayValue = displayValue || emptyDisplayValue
|
||||
const [open, setOpen] = useState(false)
|
||||
@ -101,13 +101,13 @@ const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
if (propertyTemplate.type === 'multiSelect') {
|
||||
return (
|
||||
<MultiSelectProperty
|
||||
isEditable={!readOnly && Boolean(boardTree)}
|
||||
isEditable={!readOnly && Boolean(board)}
|
||||
emptyValue={emptyDisplayValue}
|
||||
propertyTemplate={propertyTemplate}
|
||||
propertyValue={propertyValue}
|
||||
onChange={(newValue) => mutator.changePropertyValue(card, propertyTemplate.id, newValue)}
|
||||
onChangeColor={(option: IPropertyOption, colorId: string) => mutator.changePropertyOptionColor(boardTree!.board, propertyTemplate, option, colorId)}
|
||||
onDeleteOption={(option: IPropertyOption) => mutator.deletePropertyOption(boardTree!, propertyTemplate, option)}
|
||||
onChangeColor={(option: IPropertyOption, colorId: string) => mutator.changePropertyOptionColor(board, propertyTemplate, option, colorId)}
|
||||
onDeleteOption={(option: IPropertyOption) => mutator.deletePropertyOption(board, propertyTemplate, option)}
|
||||
onCreate={
|
||||
async (newValue, currentValues) => {
|
||||
const option: IPropertyOption = {
|
||||
@ -116,7 +116,7 @@ const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
color: 'propColorDefault',
|
||||
}
|
||||
currentValues.push(option)
|
||||
await mutator.insertPropertyOption(boardTree!, propertyTemplate, option, 'add property option')
|
||||
await mutator.insertPropertyOption(board, propertyTemplate, option, 'add property option')
|
||||
mutator.changePropertyValue(card, propertyTemplate.id, currentValues.map((v) => v.id))
|
||||
}
|
||||
}
|
||||
@ -132,7 +132,7 @@ const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
propertyColorCssClassName = cardPropertyValue.color
|
||||
}
|
||||
|
||||
if (readOnly || !boardTree || !open) {
|
||||
if (readOnly || !board || !open) {
|
||||
return (
|
||||
<div
|
||||
className='octo-propertyvalue'
|
||||
@ -152,10 +152,10 @@ const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
mutator.changePropertyValue(card, propertyTemplate.id, newValue)
|
||||
}}
|
||||
onChangeColor={(option: IPropertyOption, colorId: string): void => {
|
||||
mutator.changePropertyOptionColor(boardTree.board, propertyTemplate, option, colorId)
|
||||
mutator.changePropertyOptionColor(board, propertyTemplate, option, colorId)
|
||||
}}
|
||||
onDeleteOption={(option: IPropertyOption): void => {
|
||||
mutator.deletePropertyOption(boardTree, propertyTemplate, option)
|
||||
mutator.deletePropertyOption(board, propertyTemplate, option)
|
||||
}}
|
||||
onCreate={
|
||||
async (newValue) => {
|
||||
@ -164,7 +164,7 @@ const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
value: newValue,
|
||||
color: 'propColorDefault',
|
||||
}
|
||||
await mutator.insertPropertyOption(boardTree, propertyTemplate, option, 'add property option')
|
||||
await mutator.insertPropertyOption(board, propertyTemplate, option, 'add property option')
|
||||
mutator.changePropertyValue(card, propertyTemplate.id, option.id)
|
||||
}
|
||||
}
|
||||
@ -218,7 +218,9 @@ const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
return (
|
||||
<LastModifiedBy
|
||||
card={card}
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
contents={contents}
|
||||
comments={comments}
|
||||
/>
|
||||
)
|
||||
} else if (propertyTemplate.type === 'createdTime') {
|
||||
@ -229,7 +231,8 @@ const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
return (
|
||||
<LastModifiedAt
|
||||
card={card}
|
||||
cardTree={cardTree}
|
||||
contents={contents}
|
||||
comments={comments}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
import {useRouteMatch} from 'react-router'
|
||||
import {generatePath, useRouteMatch} from 'react-router'
|
||||
|
||||
import {ISharing} from '../blocks/sharing'
|
||||
|
||||
@ -26,7 +26,7 @@ const ShareBoardComponent = React.memo((props: Props): JSX.Element => {
|
||||
const [wasCopied, setWasCopied] = useState(false)
|
||||
const [sharing, setSharing] = useState<ISharing|undefined>(undefined)
|
||||
const intl = useIntl()
|
||||
const match = useRouteMatch<{workspaceId?: string}>()
|
||||
const match = useRouteMatch<{workspaceId?: string, boardId: string, viewId: string}>()
|
||||
|
||||
const loadData = async () => {
|
||||
const newSharing = await client.getSharing(props.boardId)
|
||||
@ -76,9 +76,18 @@ const ShareBoardComponent = React.memo((props: Props): JSX.Element => {
|
||||
shareUrl.searchParams.set('r', readToken)
|
||||
|
||||
if (match.params.workspaceId) {
|
||||
shareUrl.pathname = Utils.buildURL(`/workspace/${match.params.workspaceId}/shared`)
|
||||
const newPath = generatePath('/workspace/:workspaceId/shared/:boardId/:viewId', {
|
||||
boardId: match.params.boardId,
|
||||
viewId: match.params.viewId,
|
||||
workspaceId: match.params.workspaceId,
|
||||
})
|
||||
shareUrl.pathname = newPath
|
||||
} else {
|
||||
shareUrl.pathname = Utils.buildURL('/shared')
|
||||
const newPath = generatePath('/shared/:boardId/:viewId', {
|
||||
boardId: match.params.boardId,
|
||||
viewId: match.params.viewId,
|
||||
})
|
||||
shareUrl.pathname = newPath
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -47,12 +47,12 @@ const BoardTemplateMenuItem = React.memo((props: Props) => {
|
||||
|
||||
return (
|
||||
<Menu.Text
|
||||
key={boardTemplate.id}
|
||||
id={boardTemplate.id}
|
||||
key={boardTemplate.id || ''}
|
||||
id={boardTemplate.id || ''}
|
||||
name={displayName}
|
||||
icon={<div className='Icon'>{boardTemplate.fields.icon}</div>}
|
||||
onClick={() => {
|
||||
addBoardFromTemplate(intl, props.showBoard, boardTemplate.id, activeBoardId, isGlobal)
|
||||
addBoardFromTemplate(intl, props.showBoard, boardTemplate.id || '', activeBoardId, isGlobal)
|
||||
}}
|
||||
rightIcon={!isGlobal &&
|
||||
<MenuWrapper stopPropagationOnToggle={true}>
|
||||
@ -63,7 +63,7 @@ const BoardTemplateMenuItem = React.memo((props: Props) => {
|
||||
id='edit'
|
||||
name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})}
|
||||
onClick={() => {
|
||||
props.showBoard(boardTemplate.id)
|
||||
props.showBoard(boardTemplate.id || '')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
|
@ -8,7 +8,7 @@ import {sendFlashMessage} from '../../components/flashMessages'
|
||||
import client from '../../octoClient'
|
||||
import {Utils} from '../../utils'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import {getCurrentWorkspace, fetchCurrentWorkspace} from '../../store/currentWorkspace'
|
||||
import {getWorkspace, fetchWorkspace} from '../../store/workspace'
|
||||
import {useAppSelector, useAppDispatch} from '../../store/hooks'
|
||||
|
||||
import Modal from '../modal'
|
||||
@ -22,13 +22,13 @@ type Props = {
|
||||
const RegistrationLink = React.memo((props: Props) => {
|
||||
const {onClose} = props
|
||||
const intl = useIntl()
|
||||
const workspace = useAppSelector<IWorkspace|null>(getCurrentWorkspace)
|
||||
const workspace = useAppSelector<IWorkspace|null>(getWorkspace)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [wasCopied, setWasCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCurrentWorkspace())
|
||||
dispatch(fetchWorkspace())
|
||||
}, [])
|
||||
|
||||
const regenerateToken = async () => {
|
||||
@ -36,7 +36,7 @@ const RegistrationLink = React.memo((props: Props) => {
|
||||
const accept = window.confirm(intl.formatMessage({id: 'RegistrationLink.confirmRegenerateToken', defaultMessage: 'This will invalidate previously shared links. Continue?'}))
|
||||
if (accept) {
|
||||
await client.regenerateWorkspaceSignupToken()
|
||||
await dispatch(fetchCurrentWorkspace())
|
||||
await dispatch(fetchWorkspace())
|
||||
setWasCopied(false)
|
||||
|
||||
const description = intl.formatMessage({id: 'RegistrationLink.tokenRegenerated', defaultMessage: 'Registration link regenerated'})
|
||||
|
@ -2,13 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useEffect, useState} from 'react'
|
||||
|
||||
import {IWorkspace} from '../../blocks/workspace'
|
||||
import {getActiveThemeName, loadTheme} from '../../theme'
|
||||
import {WorkspaceTree} from '../../viewModel/workspaceTree'
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
import HamburgerIcon from '../../widgets/icons/hamburger'
|
||||
import HideSidebarIcon from '../../widgets/icons/hideSidebar'
|
||||
import ShowSidebarIcon from '../../widgets/icons/showSidebar'
|
||||
import {getSortedBoards} from '../../store/boards'
|
||||
import {getSortedViews} from '../../store/views'
|
||||
import {getWorkspace} from '../../store/workspace'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
|
||||
import './sidebar.scss'
|
||||
|
||||
@ -18,8 +20,6 @@ import SidebarSettingsMenu from './sidebarSettingsMenu'
|
||||
import SidebarUserMenu from './sidebarUserMenu'
|
||||
|
||||
type Props = {
|
||||
workspace?: IWorkspace
|
||||
workspaceTree: WorkspaceTree,
|
||||
activeBoardId?: string
|
||||
activeViewId?: string
|
||||
}
|
||||
@ -27,6 +27,8 @@ type Props = {
|
||||
const Sidebar = React.memo((props: Props) => {
|
||||
const [isHidden, setHidden] = useState(false)
|
||||
const [whiteLogo, setWhiteLogo] = useState(false)
|
||||
const boards = useAppSelector(getSortedBoards)
|
||||
const views = useAppSelector(getSortedViews)
|
||||
|
||||
useEffect(() => {
|
||||
const theme = loadTheme()
|
||||
@ -36,13 +38,11 @@ const Sidebar = React.memo((props: Props) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const {workspace, workspaceTree} = props
|
||||
if (!workspaceTree) {
|
||||
const workspace = useAppSelector(getWorkspace)
|
||||
if (!boards) {
|
||||
return <div/>
|
||||
}
|
||||
|
||||
const {boards, views} = workspaceTree
|
||||
|
||||
if (isHidden) {
|
||||
return (
|
||||
<div className='Sidebar octo-sidebar hidden'>
|
||||
@ -98,7 +98,7 @@ const Sidebar = React.memo((props: Props) => {
|
||||
board={board}
|
||||
activeBoardId={props.activeBoardId}
|
||||
activeViewId={props.activeViewId}
|
||||
nextBoardId={nextBoardId}
|
||||
nextBoardId={board.id === props.activeBoardId ? nextBoardId : undefined}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@ -108,7 +108,6 @@ const Sidebar = React.memo((props: Props) => {
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
<SidebarAddBoardMenu
|
||||
workspaceTree={props.workspaceTree}
|
||||
activeBoardId={props.activeBoardId}
|
||||
/>
|
||||
|
||||
|
@ -4,35 +4,34 @@ import React, {useEffect, useCallback} from 'react'
|
||||
import {FormattedMessage, useIntl, IntlShape} from 'react-intl'
|
||||
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
|
||||
|
||||
import {MutableBoard} from '../../blocks/board'
|
||||
import {MutableBoardView} from '../../blocks/boardView'
|
||||
import {Board, createBoard} from '../../blocks/board'
|
||||
import {createBoardView} from '../../blocks/boardView'
|
||||
import mutator from '../../mutator'
|
||||
import octoClient from '../../octoClient'
|
||||
import {WorkspaceTree} from '../../viewModel/workspaceTree'
|
||||
import AddIcon from '../../widgets/icons/add'
|
||||
import BoardIcon from '../../widgets/icons/board'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import {useAppDispatch, useAppSelector} from '../../store/hooks'
|
||||
import {getGlobalTemplates, fetchGlobalTemplates} from '../../store/globalTemplates'
|
||||
import {getSortedTemplates} from '../../store/boards'
|
||||
|
||||
import BoardTemplateMenuItem from './boardTemplateMenuItem'
|
||||
|
||||
import './sidebarAddBoardMenu.scss'
|
||||
|
||||
type Props = {
|
||||
workspaceTree: WorkspaceTree,
|
||||
activeBoardId?: string
|
||||
}
|
||||
|
||||
const addBoardClicked = async (showBoard: (id: string) => void, intl: IntlShape, activeBoardId?: string) => {
|
||||
const oldBoardId = activeBoardId
|
||||
|
||||
const board = new MutableBoard()
|
||||
const board = createBoard()
|
||||
board.rootId = board.id
|
||||
|
||||
const view = new MutableBoardView()
|
||||
view.viewType = 'board'
|
||||
const view = createBoardView()
|
||||
view.fields.viewType = 'board'
|
||||
view.parentId = board.id
|
||||
view.rootId = board.rootId
|
||||
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board view'})
|
||||
@ -51,12 +50,19 @@ const addBoardClicked = async (showBoard: (id: string) => void, intl: IntlShape,
|
||||
)
|
||||
}
|
||||
|
||||
const addBoardTemplateClicked = async (showBoard: (id: string) => void, activeBoardId?: string) => {
|
||||
const boardTemplate = new MutableBoard()
|
||||
const addBoardTemplateClicked = async (showBoard: (id: string) => void, intl: IntlShape, activeBoardId?: string) => {
|
||||
const boardTemplate = createBoard()
|
||||
boardTemplate.rootId = boardTemplate.id
|
||||
boardTemplate.isTemplate = true
|
||||
await mutator.insertBlock(
|
||||
boardTemplate,
|
||||
boardTemplate.fields.isTemplate = true
|
||||
|
||||
const view = createBoardView()
|
||||
view.fields.viewType = 'board'
|
||||
view.parentId = boardTemplate.id
|
||||
view.rootId = boardTemplate.rootId
|
||||
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board view'})
|
||||
|
||||
await mutator.insertBlocks(
|
||||
[boardTemplate, view],
|
||||
'add board template',
|
||||
async () => {
|
||||
showBoard(boardTemplate.id)
|
||||
@ -69,13 +75,15 @@ const addBoardTemplateClicked = async (showBoard: (id: string) => void, activeBo
|
||||
}
|
||||
|
||||
const SidebarAddBoardMenu = (props: Props): JSX.Element => {
|
||||
const globalTemplates = useAppSelector<MutableBoard[]>(getGlobalTemplates)
|
||||
const globalTemplates = useAppSelector<Board[]>(getGlobalTemplates)
|
||||
const dispatch = useAppDispatch()
|
||||
const history = useHistory()
|
||||
const match = useRouteMatch()
|
||||
const match = useRouteMatch<{boardId: string, viewId?: string}>()
|
||||
|
||||
const showBoard = useCallback((boardId) => {
|
||||
const newPath = generatePath(match.path, {...match.params, boardId: boardId || ''})
|
||||
const params = {...match.params, boardId: boardId || ''}
|
||||
delete params.viewId
|
||||
const newPath = generatePath(match.path, params)
|
||||
history.push(newPath)
|
||||
}, [match, history])
|
||||
|
||||
@ -85,10 +93,10 @@ const SidebarAddBoardMenu = (props: Props): JSX.Element => {
|
||||
}
|
||||
}, [octoClient.workspaceId])
|
||||
|
||||
const {workspaceTree} = props
|
||||
const intl = useIntl()
|
||||
const templates = useAppSelector(getSortedTemplates)
|
||||
|
||||
if (!workspaceTree) {
|
||||
if (!templates) {
|
||||
return <div/>
|
||||
}
|
||||
|
||||
@ -102,7 +110,7 @@ const SidebarAddBoardMenu = (props: Props): JSX.Element => {
|
||||
/>
|
||||
</div>
|
||||
<Menu position='top'>
|
||||
{workspaceTree.boardTemplates.length > 0 && <>
|
||||
{templates.length > 0 && <>
|
||||
<Menu.Label>
|
||||
<b>
|
||||
<FormattedMessage
|
||||
@ -115,7 +123,7 @@ const SidebarAddBoardMenu = (props: Props): JSX.Element => {
|
||||
<Menu.Separator/>
|
||||
</>}
|
||||
|
||||
{workspaceTree.boardTemplates.map((boardTemplate) => (
|
||||
{templates.map((boardTemplate) => (
|
||||
<BoardTemplateMenuItem
|
||||
key={boardTemplate.id}
|
||||
boardTemplate={boardTemplate}
|
||||
@ -125,7 +133,7 @@ const SidebarAddBoardMenu = (props: Props): JSX.Element => {
|
||||
/>
|
||||
))}
|
||||
|
||||
{globalTemplates.map((boardTemplate: MutableBoard) => (
|
||||
{globalTemplates.map((boardTemplate: Board) => (
|
||||
<BoardTemplateMenuItem
|
||||
key={boardTemplate.id}
|
||||
boardTemplate={boardTemplate}
|
||||
@ -146,7 +154,7 @@ const SidebarAddBoardMenu = (props: Props): JSX.Element => {
|
||||
icon={<AddIcon/>}
|
||||
id='add-template'
|
||||
name={intl.formatMessage({id: 'Sidebar.add-template', defaultMessage: 'New template'})}
|
||||
onClick={() => addBoardTemplateClicked(showBoard, props.activeBoardId)}
|
||||
onClick={() => addBoardTemplateClicked(showBoard, intl, props.activeBoardId)}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
@ -21,7 +21,7 @@ import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import './sidebarBoardItem.scss'
|
||||
|
||||
type Props = {
|
||||
views: readonly BoardView[]
|
||||
views: BoardView[]
|
||||
board: Board
|
||||
activeBoardId?: string
|
||||
activeViewId?: string
|
||||
@ -107,7 +107,7 @@ const SidebarBoardItem = React.memo((props: Props) => {
|
||||
className='octo-sidebar-title'
|
||||
title={displayTitle}
|
||||
>
|
||||
{board.icon ? `${board.icon} ${displayTitle}` : displayTitle}
|
||||
{board.fields.icon ? `${board.fields.icon} ${displayTitle}` : displayTitle}
|
||||
</div>
|
||||
<MenuWrapper stopPropagationOnToggle={true}>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
@ -140,7 +140,7 @@ const SidebarBoardItem = React.memo((props: Props) => {
|
||||
name={intl.formatMessage({id: 'Sidebar.duplicate-board', defaultMessage: 'Duplicate board'})}
|
||||
icon={<DuplicateIcon/>}
|
||||
onClick={() => {
|
||||
duplicateBoard(board.id)
|
||||
duplicateBoard(board.id || '')
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -148,7 +148,7 @@ const SidebarBoardItem = React.memo((props: Props) => {
|
||||
id='templateFromBoard'
|
||||
name={intl.formatMessage({id: 'Sidebar.template-from-board', defaultMessage: 'New template from board'})}
|
||||
onClick={() => {
|
||||
addTemplateFromBoard(board.id)
|
||||
addTemplateFromBoard(board.id || '')
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
@ -167,7 +167,7 @@ const SidebarBoardItem = React.memo((props: Props) => {
|
||||
className={`octo-sidebar-item subitem ${view.id === props.activeViewId ? 'active' : ''}`}
|
||||
onClick={() => showView(view.id, board.id)}
|
||||
>
|
||||
{iconForViewType(view.viewType)}
|
||||
{iconForViewType(view.fields.viewType)}
|
||||
<div
|
||||
className='octo-sidebar-title'
|
||||
title={view.title || intl.formatMessage({id: 'Sidebar.untitled-view', defaultMessage: '(Untitled View)'})}
|
||||
|
@ -11,7 +11,7 @@ import LogoWithNameIcon from '../../widgets/icons/logoWithName'
|
||||
import LogoWithNameWhiteIcon from '../../widgets/icons/logoWithNameWhite'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import {getCurrentUser} from '../../store/currentUser'
|
||||
import {getMe} from '../../store/users'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
|
||||
import ModalWrapper from '../modalWrapper'
|
||||
@ -30,7 +30,7 @@ const SidebarUserMenu = React.memo((props: Props) => {
|
||||
const history = useHistory()
|
||||
const [showRegistrationLinkDialog, setShowRegistrationLinkDialog] = useState(false)
|
||||
const {whiteLogo, showVersionBadge, showAccountActions} = props
|
||||
const user = useAppSelector<IUser|null>(getCurrentUser)
|
||||
const user = useAppSelector<IUser|null>(getMe)
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<div className='SidebarUserMenu'>
|
||||
|
@ -864,7 +864,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedAt 1`
|
||||
<div
|
||||
class="LastModifiedAt octo-propertyvalue"
|
||||
>
|
||||
June 20, 12:22 PM
|
||||
June 22, 11:23 AM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -118,7 +118,7 @@ exports[`should match snapshot with Group 1`] = `
|
||||
exports[`should match snapshot, add new 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="octo-group-header-cell"
|
||||
class="octo-group-header-cell expanded"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
@ -184,7 +184,7 @@ exports[`should match snapshot, add new 1`] = `
|
||||
exports[`should match snapshot, edit title 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="octo-group-header-cell"
|
||||
class="octo-group-header-cell expanded"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
@ -250,7 +250,7 @@ exports[`should match snapshot, edit title 1`] = `
|
||||
exports[`should match snapshot, hide group 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="octo-group-header-cell expanded"
|
||||
class="octo-group-header-cell"
|
||||
draggable="true"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
|
@ -15,7 +15,7 @@ import {HTML5Backend} from 'react-dnd-html5-backend'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
import {MutableBoardTree} from '../../viewModel/boardTree'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
|
||||
import {IUser} from '../../user'
|
||||
|
||||
@ -40,70 +40,90 @@ const wrapProviders = (children: any) => {
|
||||
describe('components/table/Table', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
view.viewType = 'table'
|
||||
view.groupById = undefined
|
||||
view.visiblePropertyIds = ['property1', 'property2']
|
||||
view.fields.viewType = 'table'
|
||||
view.fields.groupById = undefined
|
||||
view.fields.visiblePropertyIds = ['property1', 'property2']
|
||||
|
||||
const view2 = TestBlockFactory.createBoardView(board)
|
||||
view2.sortOptions = []
|
||||
view2.fields.sortOptions = []
|
||||
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const cardTemplate = TestBlockFactory.createCard(board)
|
||||
cardTemplate.isTemplate = true
|
||||
cardTemplate.fields.isTemplate = true
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).not.toBeUndefined()
|
||||
if (!boardTree) {
|
||||
fail('sync')
|
||||
const state = {
|
||||
users: {
|
||||
workspaceUsers: {
|
||||
'user-id-1': {username: 'username_1'} as IUser,
|
||||
'user-id-2': {username: 'username_2'} as IUser,
|
||||
'user-id-3': {username: 'username_3'} as IUser,
|
||||
'user-id-4': {username: 'username_4'} as IUser,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
contents: {
|
||||
contents: {},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
expect(boardTree.cards).toBeDefined()
|
||||
expect(boardTree.cards).toEqual([card])
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
const callback = jest.fn()
|
||||
const addCard = jest.fn()
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore(state)
|
||||
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<Table
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
visibleGroups={[]}
|
||||
cards={[card]}
|
||||
views={[view, view2]}
|
||||
selectedCardIds={[]}
|
||||
readonly={false}
|
||||
cardIdToFocusOnRender=''
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot, read-only', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const callback = jest.fn()
|
||||
const addCard = jest.fn()
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore(state)
|
||||
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<Table
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
visibleGroups={[]}
|
||||
cards={[card]}
|
||||
views={[view, view2]}
|
||||
selectedCardIds={[]}
|
||||
readonly={true}
|
||||
cardIdToFocusOnRender=''
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
@ -111,33 +131,34 @@ describe('components/table/Table', () => {
|
||||
})
|
||||
|
||||
test('should match snapshot with GroupBy', async () => {
|
||||
// Sync
|
||||
view.groupById = 'property1'
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).not.toBeUndefined()
|
||||
if (!boardTree) {
|
||||
fail('sync')
|
||||
}
|
||||
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
expect(boardTree.cards).toBeDefined()
|
||||
expect(boardTree.cards).toEqual([card])
|
||||
|
||||
const callback = jest.fn()
|
||||
const addCard = jest.fn()
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore(state)
|
||||
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<Table
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={{...view, fields: {...view.fields, groupById: 'property1'}} as BoardView}
|
||||
visibleGroups={[{option: {id: '', value: 'test', color: ''}, cards: []}]}
|
||||
groupByProperty={{
|
||||
id: '',
|
||||
name: 'Property 1',
|
||||
type: 'text',
|
||||
options: [{id: 'property1', value: 'Property 1', color: ''}],
|
||||
}}
|
||||
cards={[card]}
|
||||
views={[view, view2]}
|
||||
selectedCardIds={[]}
|
||||
readonly={false}
|
||||
cardIdToFocusOnRender=''
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
@ -145,11 +166,31 @@ describe('components/table/Table', () => {
|
||||
})
|
||||
|
||||
describe('components/table/Table extended', () => {
|
||||
const state = {
|
||||
users: {
|
||||
workspaceUsers: {
|
||||
'user-id-1': {username: 'username_1'} as IUser,
|
||||
'user-id-2': {username: 'username_2'} as IUser,
|
||||
'user-id-3': {username: 'username_3'} as IUser,
|
||||
'user-id-4': {username: 'username_4'} as IUser,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
contents: {
|
||||
contents: {},
|
||||
},
|
||||
cards: {
|
||||
cards: {},
|
||||
},
|
||||
}
|
||||
|
||||
test('should match snapshot with CreatedBy', async () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
|
||||
const dateCreatedId = Utils.createGuid()
|
||||
board.cardProperties.push({
|
||||
board.fields.cardProperties.push({
|
||||
id: dateCreatedId,
|
||||
name: 'Date Created',
|
||||
type: 'createdTime',
|
||||
@ -163,27 +204,20 @@ describe('components/table/Table extended', () => {
|
||||
card2.createAt = Date.parse('15 Jun 2021 16:22:00')
|
||||
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
view.viewType = 'table'
|
||||
view.groupById = undefined
|
||||
view.visiblePropertyIds = ['property1', 'property2', dateCreatedId]
|
||||
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, card1, card2, view])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).not.toBeUndefined()
|
||||
if (!boardTree) {
|
||||
fail('sync')
|
||||
}
|
||||
view.fields.viewType = 'table'
|
||||
view.fields.groupById = undefined
|
||||
view.fields.visiblePropertyIds = ['property1', 'property2', dateCreatedId]
|
||||
|
||||
const callback = jest.fn()
|
||||
const addCard = jest.fn()
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
currentWorkspaceUsers: {
|
||||
byId: {
|
||||
'user-id-1': {username: 'username_1'} as IUser,
|
||||
'user-id-2': {username: 'username_2'} as IUser,
|
||||
...state,
|
||||
cards: {
|
||||
cards: {
|
||||
[card1.id]: card1,
|
||||
[card2.id]: card2,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -191,7 +225,11 @@ describe('components/table/Table extended', () => {
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<Table
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
visibleGroups={[]}
|
||||
cards={[card1, card2]}
|
||||
views={[view]}
|
||||
selectedCardIds={[]}
|
||||
readonly={false}
|
||||
cardIdToFocusOnRender=''
|
||||
@ -209,7 +247,7 @@ describe('components/table/Table extended', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
|
||||
const dateUpdatedId = Utils.createGuid()
|
||||
board.cardProperties.push({
|
||||
board.fields.cardProperties.push({
|
||||
id: dateUpdatedId,
|
||||
name: 'Date Updated',
|
||||
type: 'updatedTime',
|
||||
@ -232,32 +270,53 @@ describe('components/table/Table extended', () => {
|
||||
card2Text.type = 'text'
|
||||
card2Text.updateAt = Date.parse('22 Jun 2021 11:23:00')
|
||||
|
||||
card2.fields.contentOrder = [card2Text.id]
|
||||
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
view.viewType = 'table'
|
||||
view.groupById = undefined
|
||||
view.visiblePropertyIds = ['property1', 'property2', dateUpdatedId]
|
||||
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, card1, card2, view, card2Comment, card2Text])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).not.toBeUndefined()
|
||||
if (!boardTree) {
|
||||
fail('sync')
|
||||
}
|
||||
view.fields.viewType = 'table'
|
||||
view.fields.groupById = undefined
|
||||
view.fields.visiblePropertyIds = ['property1', 'property2', dateUpdatedId]
|
||||
|
||||
const callback = jest.fn()
|
||||
const addCard = jest.fn()
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
...state,
|
||||
comments: {
|
||||
comments: {
|
||||
[card2Comment.id]: card2Comment,
|
||||
},
|
||||
},
|
||||
contents: {
|
||||
contents: {
|
||||
[card2Text.id]: card2Text,
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card1.id]: card1,
|
||||
[card2.id]: card2,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<Table
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
visibleGroups={[]}
|
||||
cards={[card1, card2]}
|
||||
views={[view]}
|
||||
selectedCardIds={[]}
|
||||
readonly={false}
|
||||
cardIdToFocusOnRender=''
|
||||
showCard={callback}
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
@ -267,7 +326,7 @@ describe('components/table/Table extended', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
|
||||
const createdById = Utils.createGuid()
|
||||
board.cardProperties.push({
|
||||
board.fields.cardProperties.push({
|
||||
id: createdById,
|
||||
name: 'Created By',
|
||||
type: 'createdBy',
|
||||
@ -281,27 +340,20 @@ describe('components/table/Table extended', () => {
|
||||
card2.createdBy = 'user-id-2'
|
||||
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
view.viewType = 'table'
|
||||
view.groupById = undefined
|
||||
view.visiblePropertyIds = ['property1', 'property2', createdById]
|
||||
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, card1, card2, view])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).not.toBeUndefined()
|
||||
if (!boardTree) {
|
||||
fail('sync')
|
||||
}
|
||||
view.fields.viewType = 'table'
|
||||
view.fields.groupById = undefined
|
||||
view.fields.visiblePropertyIds = ['property1', 'property2', createdById]
|
||||
|
||||
const callback = jest.fn()
|
||||
const addCard = jest.fn()
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
currentWorkspaceUsers: {
|
||||
byId: {
|
||||
'user-id-1': {username: 'username_1'} as IUser,
|
||||
'user-id-2': {username: 'username_2'} as IUser,
|
||||
...state,
|
||||
cards: {
|
||||
cards: {
|
||||
[card1.id]: card1,
|
||||
[card2.id]: card2,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -309,7 +361,11 @@ describe('components/table/Table extended', () => {
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<Table
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
visibleGroups={[]}
|
||||
cards={[card1, card2]}
|
||||
views={[view]}
|
||||
selectedCardIds={[]}
|
||||
readonly={false}
|
||||
cardIdToFocusOnRender=''
|
||||
@ -328,7 +384,7 @@ describe('components/table/Table extended', () => {
|
||||
const board = TestBlockFactory.createBoard()
|
||||
|
||||
const modifiedById = Utils.createGuid()
|
||||
board.cardProperties.push({
|
||||
board.fields.cardProperties.push({
|
||||
id: modifiedById,
|
||||
name: 'Last Modified By',
|
||||
type: 'updatedBy',
|
||||
@ -345,6 +401,8 @@ describe('components/table/Table extended', () => {
|
||||
card1Text.modifiedBy = 'user-id-4'
|
||||
card1Text.updateAt = Date.parse('16 Jun 2021 16:22:00')
|
||||
|
||||
card1.fields.contentOrder = [card1Text.id]
|
||||
|
||||
const card2 = TestBlockFactory.createCard(board)
|
||||
card2.modifiedBy = 'user-id-2'
|
||||
card2.updateAt = Date.parse('15 Jun 2021 16:22:00')
|
||||
@ -356,27 +414,30 @@ describe('components/table/Table extended', () => {
|
||||
card2.updateAt = Date.parse('16 Jun 2021 16:22:00')
|
||||
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
view.viewType = 'table'
|
||||
view.groupById = undefined
|
||||
view.visiblePropertyIds = ['property1', 'property2', modifiedById]
|
||||
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, card1, card2, view, card2Comment, card1Text])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).not.toBeUndefined()
|
||||
if (!boardTree) {
|
||||
fail('sync')
|
||||
}
|
||||
view.fields.viewType = 'table'
|
||||
view.fields.groupById = undefined
|
||||
view.fields.visiblePropertyIds = ['property1', 'property2', modifiedById]
|
||||
|
||||
const callback = jest.fn()
|
||||
const addCard = jest.fn()
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore({
|
||||
currentWorkspaceUsers: {
|
||||
byId: {
|
||||
'user-id-3': {username: 'username_3'} as IUser,
|
||||
'user-id-4': {username: 'username_4'} as IUser,
|
||||
...state,
|
||||
comments: {
|
||||
comments: {
|
||||
[card2Comment.id]: card2Comment,
|
||||
},
|
||||
},
|
||||
contents: {
|
||||
contents: {
|
||||
[card1Text.id]: card1Text,
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card1.id]: card1,
|
||||
[card2.id]: card2,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -384,7 +445,11 @@ describe('components/table/Table extended', () => {
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<Table
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
visibleGroups={[]}
|
||||
cards={[card1, card2]}
|
||||
views={[view]}
|
||||
selectedCardIds={[]}
|
||||
readonly={false}
|
||||
cardIdToFocusOnRender=''
|
||||
|
@ -1,33 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useRef, useState} from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
import {useDragLayer, useDrop} from 'react-dnd'
|
||||
|
||||
import {IPropertyOption, IPropertyTemplate} from '../../blocks/board'
|
||||
import {MutableBoardView} from '../../blocks/boardView'
|
||||
import {IPropertyOption, IPropertyTemplate, Board, BoardGroup} from '../../blocks/board'
|
||||
import {createBoardView, BoardView, ISortOption} from '../../blocks/boardView'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {Constants} from '../../constants'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {useAppDispatch} from '../../store/hooks'
|
||||
import {updateView} from '../../store/views'
|
||||
|
||||
import {OctoUtils} from '../../octoUtils'
|
||||
|
||||
import './table.scss'
|
||||
import {CardTree, MutableCardTree} from '../../viewModel/cardTree'
|
||||
|
||||
import useCardListener from '../../hooks/cardListener'
|
||||
|
||||
import TableHeader from './tableHeader'
|
||||
import TableRows from './tableRows'
|
||||
import TableGroup from './tableGroup'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
selectedCardIds: string[]
|
||||
board: Board
|
||||
cards: Card[]
|
||||
activeView: BoardView
|
||||
views: BoardView[]
|
||||
visibleGroups: BoardGroup[]
|
||||
groupByProperty?: IPropertyTemplate
|
||||
readonly: boolean
|
||||
cardIdToFocusOnRender: string
|
||||
showCard: (cardId?: string) => void
|
||||
@ -35,38 +37,11 @@ type Props = {
|
||||
onCardClicked: (e: React.MouseEvent, card: Card) => void
|
||||
}
|
||||
|
||||
const Table = (props: Props) => {
|
||||
const {boardTree} = props
|
||||
const {board, cards, activeView, visibleGroups} = boardTree
|
||||
const isManualSort = activeView.sortOptions.length === 0
|
||||
const Table = (props: Props): JSX.Element => {
|
||||
const {board, cards, activeView, visibleGroups, groupByProperty, views} = props
|
||||
const isManualSort = activeView.fields.sortOptions?.length === 0
|
||||
const intl = useIntl()
|
||||
|
||||
const [cardTrees, setCardTrees] = useState<{[key: string]: CardTree | undefined}>({a: undefined})
|
||||
const cardTreeRef = useRef<{[key: string]: CardTree | undefined}>()
|
||||
cardTreeRef.current = cardTrees
|
||||
|
||||
useCardListener(
|
||||
async (blocks) => {
|
||||
for (const block of blocks) {
|
||||
const cardTree = cardTreeRef.current && cardTreeRef.current[block.parentId]
|
||||
if (cardTree) {
|
||||
const newCardTree = MutableCardTree.incrementalUpdate(cardTree, blocks)
|
||||
setCardTrees((oldTree) => ({...oldTree, [block.parentId]: newCardTree}))
|
||||
} else {
|
||||
MutableCardTree.sync(block.parentId).
|
||||
then((newCardTree) => {
|
||||
setCardTrees((oldTree) => ({...oldTree, [block.parentId]: newCardTree}))
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
cards.forEach(async (c) => {
|
||||
const newCardTree = await MutableCardTree.sync(c.id)
|
||||
setCardTrees((oldTree) => ({...oldTree, [c.id]: newCardTree}))
|
||||
})
|
||||
},
|
||||
)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const {offset, resizingColumn} = useDragLayer((monitor) => {
|
||||
if (monitor.getItemType() === 'horizontalGrip') {
|
||||
@ -85,23 +60,28 @@ const Table = (props: Props) => {
|
||||
|
||||
const [, drop] = useDrop(() => ({
|
||||
accept: 'horizontalGrip',
|
||||
drop: (item: { id: string }, monitor) => {
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
drop: async (item: { id: string }, monitor) => {
|
||||
const columnWidths = {...activeView.fields.columnWidths}
|
||||
const finalOffset = monitor.getDifferenceFromInitialOffset()?.x || 0
|
||||
const newWidth = Math.max(Constants.minColumnWidth, (columnWidths[item.id] || 0) + (finalOffset || 0))
|
||||
if (newWidth !== columnWidths[item.id]) {
|
||||
columnWidths[item.id] = newWidth
|
||||
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.columnWidths = columnWidths
|
||||
mutator.updateBlock(newView, activeView, 'resize column')
|
||||
const newView = createBoardView(activeView)
|
||||
newView.fields.columnWidths = columnWidths
|
||||
try {
|
||||
dispatch(updateView(newView))
|
||||
await mutator.updateBlock(newView, activeView, 'resize column')
|
||||
} catch {
|
||||
dispatch(updateView(activeView))
|
||||
}
|
||||
}
|
||||
},
|
||||
}), [activeView])
|
||||
|
||||
const onAutoSizeColumn = ((columnID: string, headerWidth: number) => {
|
||||
const onAutoSizeColumn = useCallback((columnID: string, headerWidth: number) => {
|
||||
let longestSize = headerWidth
|
||||
const visibleProperties = board.cardProperties.filter(() => activeView.visiblePropertyIds.includes(columnID))
|
||||
const visibleProperties = board.fields.cardProperties.filter(() => activeView.fields.visiblePropertyIds.includes(columnID)) || []
|
||||
const columnRef = columnRefs.get(columnID)
|
||||
if (!columnRef?.current) {
|
||||
return
|
||||
@ -111,11 +91,11 @@ const Table = (props: Props) => {
|
||||
cards.forEach((card) => {
|
||||
let displayValue = card.title
|
||||
if (columnID !== Constants.titleColumnId) {
|
||||
const template = visibleProperties.find((t) => t.id === columnID)
|
||||
const template = visibleProperties.find((t: IPropertyTemplate) => t.id === columnID)
|
||||
if (!template) {
|
||||
return
|
||||
}
|
||||
displayValue = (OctoUtils.propertyDisplayValue(card, card.properties[columnID], template, intl) || '') as string
|
||||
displayValue = (OctoUtils.propertyDisplayValue(card, card.fields.properties[columnID], template, intl) || '') as string
|
||||
if (template.type === 'select') {
|
||||
displayValue = displayValue.toUpperCase()
|
||||
}
|
||||
@ -126,43 +106,43 @@ const Table = (props: Props) => {
|
||||
}
|
||||
})
|
||||
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
const columnWidths = {...activeView.fields.columnWidths}
|
||||
columnWidths[columnID] = longestSize
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.columnWidths = columnWidths
|
||||
const newView = createBoardView(activeView)
|
||||
newView.fields.columnWidths = columnWidths
|
||||
mutator.updateBlock(newView, activeView, 'autosize column')
|
||||
})
|
||||
}, [activeView, board, cards])
|
||||
|
||||
const hideGroup = (groupById: string): void => {
|
||||
const index: number = activeView.collapsedOptionIds.indexOf(groupById)
|
||||
const newValue: string[] = [...activeView.collapsedOptionIds]
|
||||
const hideGroup = useCallback((groupById: string): void => {
|
||||
const index: number = activeView.fields.collapsedOptionIds.indexOf(groupById)
|
||||
const newValue: string[] = [...activeView.fields.collapsedOptionIds]
|
||||
if (index > -1) {
|
||||
newValue.splice(index, 1)
|
||||
} else if (groupById !== '') {
|
||||
newValue.push(groupById)
|
||||
}
|
||||
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.collapsedOptionIds = newValue
|
||||
const newView = createBoardView(activeView)
|
||||
newView.fields.collapsedOptionIds = newValue
|
||||
mutator.performAsUndoGroup(async () => {
|
||||
await mutator.updateBlock(newView, activeView, 'hide group')
|
||||
})
|
||||
}
|
||||
}, [activeView])
|
||||
|
||||
const onDropToColumn = async (template: IPropertyTemplate, container: IPropertyTemplate) => {
|
||||
const onDropToColumn = useCallback(async (template: IPropertyTemplate, container: IPropertyTemplate) => {
|
||||
Utils.log(`ondrop. Source column: ${template.name}, dest column: ${container.name}`)
|
||||
|
||||
// Move template to new index
|
||||
const destIndex = container ? board.cardProperties.indexOf(container) : 0
|
||||
const destIndex = container ? board.fields.cardProperties.indexOf(container) : 0
|
||||
await mutator.changePropertyTemplateOrder(board, template, destIndex >= 0 ? destIndex : 0)
|
||||
}
|
||||
}, [board])
|
||||
|
||||
const onDropToGroupHeader = async (option: IPropertyOption, dstOption?: IPropertyOption) => {
|
||||
const onDropToGroupHeader = useCallback(async (option: IPropertyOption, dstOption?: IPropertyOption) => {
|
||||
if (dstOption) {
|
||||
Utils.log(`ondrop. Header target: ${dstOption.value}, source: ${option?.value}`)
|
||||
|
||||
// Move option to new index
|
||||
const visibleOptionIds = boardTree.visibleGroups.map((o) => o.option.id)
|
||||
const visibleOptionIds = visibleGroups.map((o) => o.option.id)
|
||||
const srcIndex = visibleOptionIds.indexOf(dstOption.id)
|
||||
const destIndex = visibleOptionIds.indexOf(option.id)
|
||||
|
||||
@ -171,23 +151,22 @@ const Table = (props: Props) => {
|
||||
|
||||
await mutator.changeViewVisibleOptionIds(activeView, visibleOptionIds)
|
||||
}
|
||||
}
|
||||
}, [activeView, visibleGroups])
|
||||
|
||||
const onDropToCard = (srcCard: Card, dstCard: Card) => {
|
||||
const onDropToCard = useCallback((srcCard: Card, dstCard: Card) => {
|
||||
Utils.log(`onDropToCard: ${dstCard.title}`)
|
||||
onDropToGroup(srcCard, dstCard.properties[activeView.groupById!] as string, dstCard.id)
|
||||
}
|
||||
onDropToGroup(srcCard, dstCard.fields.properties[activeView.fields.groupById!] as string, dstCard.id)
|
||||
}, [activeView])
|
||||
|
||||
const onDropToGroup = (srcCard: Card, groupID: string, dstCardID: string) => {
|
||||
const onDropToGroup = useCallback((srcCard: Card, groupID: string, dstCardID: string) => {
|
||||
Utils.log(`onDropToGroup: ${srcCard.title}`)
|
||||
const {selectedCardIds} = props
|
||||
|
||||
const draggedCardIds = Array.from(new Set(selectedCardIds).add(srcCard.id))
|
||||
const description = draggedCardIds.length > 1 ? `drag ${draggedCardIds.length} cards` : 'drag card'
|
||||
|
||||
if (activeView.groupById !== undefined) {
|
||||
const orderedCards = boardTree.orderedCards()
|
||||
const cardsById: { [key: string]: Card } = orderedCards.reduce((acc: { [key: string]: Card }, card: Card): { [key: string]: Card } => {
|
||||
if (activeView.fields.groupById !== undefined) {
|
||||
const cardsById: { [key: string]: Card } = cards.reduce((acc: { [key: string]: Card }, card: Card): { [key: string]: Card } => {
|
||||
acc[card.id] = card
|
||||
return acc
|
||||
}, {})
|
||||
@ -197,13 +176,13 @@ const Table = (props: Props) => {
|
||||
// Update properties of dragged cards
|
||||
const awaits = []
|
||||
for (const draggedCard of draggedCards) {
|
||||
Utils.log(`draggedCard: ${draggedCard.title}, column: ${draggedCard.properties}`)
|
||||
Utils.log(`draggedCard: ${draggedCard.title}, column: ${draggedCard.fields.properties}`)
|
||||
Utils.log(`droppedColumn: ${groupID}`)
|
||||
const oldOptionId = draggedCard.properties[boardTree.groupByProperty!.id]
|
||||
const oldOptionId = draggedCard.fields.properties[groupByProperty!.id]
|
||||
Utils.log(`ondrop. oldValue: ${oldOptionId}`)
|
||||
|
||||
if (groupID !== oldOptionId) {
|
||||
awaits.push(mutator.changePropertyValue(draggedCard, boardTree.groupByProperty!.id, groupID, description))
|
||||
awaits.push(mutator.changePropertyValue(draggedCard, groupByProperty!.id, groupID, description))
|
||||
}
|
||||
}
|
||||
await Promise.all(awaits)
|
||||
@ -212,7 +191,7 @@ const Table = (props: Props) => {
|
||||
|
||||
// Update dstCard order
|
||||
if (isManualSort) {
|
||||
let cardOrder = Array.from(new Set([...activeView.cardOrder, ...boardTree.cards.map((o) => o.id)]))
|
||||
let cardOrder = Array.from(new Set([...activeView.fields.cardOrder, ...cards.map((o) => o.id)]))
|
||||
if (dstCardID) {
|
||||
const isDraggingDown = cardOrder.indexOf(srcCard.id) <= cardOrder.indexOf(dstCardID)
|
||||
cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id))
|
||||
@ -223,7 +202,7 @@ const Table = (props: Props) => {
|
||||
cardOrder.splice(destIndex, 0, ...draggedCardIds)
|
||||
} else {
|
||||
// Find index of first group item
|
||||
const firstCard = boardTree.orderedCards().find((card) => card.properties[activeView.groupById!] === groupID)
|
||||
const firstCard = cards.find((card) => card.fields.properties[activeView.fields.groupById!] === groupID)
|
||||
if (firstCard) {
|
||||
const destIndex = cardOrder.indexOf(firstCard.id)
|
||||
cardOrder.splice(destIndex, 0, ...draggedCardIds)
|
||||
@ -237,13 +216,13 @@ const Table = (props: Props) => {
|
||||
await mutator.changeViewCardOrder(activeView, cardOrder, description)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [activeView, cards, props.selectedCardIds, groupByProperty])
|
||||
|
||||
const propertyNameChanged = async (option: IPropertyOption, text: string): Promise<void> => {
|
||||
await mutator.changePropertyOptionValue(boardTree, boardTree.groupByProperty!, option, text)
|
||||
}
|
||||
const propertyNameChanged = useCallback(async (option: IPropertyOption, text: string): Promise<void> => {
|
||||
await mutator.changePropertyOptionValue(board, groupByProperty!, option, text)
|
||||
}, [board, groupByProperty])
|
||||
|
||||
const titleSortOption = activeView.sortOptions.find((o) => o.propertyId === Constants.titleColumnId)
|
||||
const titleSortOption = activeView.fields.sortOptions?.find((o) => o.propertyId === Constants.titleColumnId)
|
||||
let titleSorted: 'up' | 'down' | 'none' = 'none'
|
||||
if (titleSortOption) {
|
||||
titleSorted = titleSortOption.reversed ? 'down' : 'up'
|
||||
@ -269,7 +248,10 @@ const Table = (props: Props) => {
|
||||
}
|
||||
sorted={titleSorted}
|
||||
readonly={props.readonly}
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
cards={cards}
|
||||
views={views}
|
||||
template={{id: Constants.titleColumnId, name: 'title', type: 'text', options: []}}
|
||||
offset={resizingColumn === Constants.titleColumnId ? offset : 0}
|
||||
onDrop={onDropToColumn}
|
||||
@ -278,9 +260,9 @@ const Table = (props: Props) => {
|
||||
|
||||
{/* Table header row */}
|
||||
|
||||
{board.cardProperties.filter((template) => activeView.visiblePropertyIds.includes(template.id)).map((template) => {
|
||||
{board.fields.cardProperties.filter((template: IPropertyTemplate) => activeView.fields.visiblePropertyIds.includes(template.id)).map((template: IPropertyTemplate) => {
|
||||
let sorted: 'up' | 'down' | 'none' = 'none'
|
||||
const sortOption = activeView.sortOptions.find((o) => o.propertyId === template.id)
|
||||
const sortOption = activeView.fields.sortOptions.find((o: ISortOption) => o.propertyId === template.id)
|
||||
if (sortOption) {
|
||||
sorted = sortOption.reversed ? 'down' : 'up'
|
||||
}
|
||||
@ -290,7 +272,10 @@ const Table = (props: Props) => {
|
||||
name={template.name}
|
||||
sorted={sorted}
|
||||
readonly={props.readonly}
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
cards={cards}
|
||||
views={views}
|
||||
template={template}
|
||||
key={template.id}
|
||||
offset={resizingColumn === template.id ? offset : 0}
|
||||
@ -303,13 +288,14 @@ const Table = (props: Props) => {
|
||||
|
||||
{/* Table header row */}
|
||||
<div className='table-row-container'>
|
||||
{activeView.groupById &&
|
||||
{activeView.fields.groupById &&
|
||||
visibleGroups.map((group) => {
|
||||
return (
|
||||
<TableGroup
|
||||
key={group.option.id}
|
||||
boardTree={boardTree}
|
||||
cardTrees={cardTrees}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
groupByProperty={groupByProperty}
|
||||
group={group}
|
||||
readonly={props.readonly}
|
||||
columnRefs={columnRefs}
|
||||
@ -328,12 +314,12 @@ const Table = (props: Props) => {
|
||||
}
|
||||
|
||||
{/* No Grouping, Rows, one per card */}
|
||||
{!activeView.groupById &&
|
||||
{!activeView.fields.groupById &&
|
||||
<TableRows
|
||||
boardTree={boardTree}
|
||||
cardTrees={cardTrees}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
columnRefs={columnRefs}
|
||||
cards={boardTree.cards}
|
||||
cards={cards}
|
||||
selectedCardIds={props.selectedCardIds}
|
||||
readonly={props.readonly}
|
||||
cardIdToFocusOnRender={props.cardIdToFocusOnRender}
|
||||
@ -347,7 +333,7 @@ const Table = (props: Props) => {
|
||||
|
||||
{/* Add New row */}
|
||||
<div className='octo-table-footer'>
|
||||
{!props.readonly && !activeView.groupById &&
|
||||
{!props.readonly && !activeView.fields.groupById &&
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
onClick={() => {
|
||||
|
@ -5,19 +5,18 @@ import React from 'react'
|
||||
|
||||
import {useDrop} from 'react-dnd'
|
||||
|
||||
import {IPropertyOption} from '../../blocks/board'
|
||||
import {Board, IPropertyOption, IPropertyTemplate, BoardGroup} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree'
|
||||
|
||||
import {CardTree} from '../../viewModel/cardTree'
|
||||
|
||||
import TableGroupHeaderRow from './tableGroupHeaderRow'
|
||||
import TableRows from './tableRows'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
cardTrees: { [key: string]: CardTree | undefined }
|
||||
group: BoardTreeGroup
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
groupByProperty?: IPropertyTemplate
|
||||
group: BoardGroup
|
||||
readonly: boolean
|
||||
columnRefs: Map<string, React.RefObject<HTMLDivElement>>
|
||||
selectedCardIds: string[]
|
||||
@ -33,7 +32,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const TableGroup = React.memo((props: Props): JSX.Element => {
|
||||
const {boardTree, group, onDropToGroup} = props
|
||||
const {board, activeView, group, onDropToGroup, groupByProperty} = props
|
||||
const groupId = group.option.id
|
||||
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
@ -61,7 +60,9 @@ const TableGroup = React.memo((props: Props): JSX.Element => {
|
||||
>
|
||||
<TableGroupHeaderRow
|
||||
group={group}
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
groupByProperty={groupByProperty}
|
||||
hideGroup={props.hideGroup}
|
||||
addCard={props.addCard}
|
||||
readonly={props.readonly}
|
||||
@ -71,8 +72,8 @@ const TableGroup = React.memo((props: Props): JSX.Element => {
|
||||
|
||||
{(group.cards.length > 0) &&
|
||||
<TableRows
|
||||
boardTree={boardTree}
|
||||
cardTrees={props.cardTrees}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
columnRefs={props.columnRefs}
|
||||
cards={group.cards}
|
||||
selectedCardIds={props.selectedCardIds}
|
||||
|
@ -16,17 +16,9 @@ import {act} from 'react-dom/test-utils'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
import {MutableBoardTree} from '../../viewModel/boardTree'
|
||||
|
||||
import TableGroupHeaderRowElement from './tableGroupHeaderRow'
|
||||
|
||||
global.fetch = FetchMock.fn
|
||||
|
||||
beforeEach(() => {
|
||||
FetchMock.fn.mockReset()
|
||||
})
|
||||
|
||||
const wrapProviders = (children: any) => {
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
@ -39,11 +31,7 @@ const board = TestBlockFactory.createBoard()
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
|
||||
const view2 = TestBlockFactory.createBoardView(board)
|
||||
view2.sortOptions = []
|
||||
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const cardTemplate = TestBlockFactory.createCard(board)
|
||||
cardTemplate.isTemplate = true
|
||||
view2.fields.sortOptions = []
|
||||
|
||||
const boardTreeNoGroup = {
|
||||
option: {
|
||||
@ -64,21 +52,22 @@ const boardTreeGroup = {
|
||||
}
|
||||
|
||||
test('should match snapshot, no groups', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const component = wrapProviders(
|
||||
<TableGroupHeaderRowElement
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
group={boardTreeNoGroup}
|
||||
readonly={false}
|
||||
hideGroup={jest.fn()}
|
||||
addCard={jest.fn()}
|
||||
propertyNameChanged={jest.fn()}
|
||||
onDrop={jest.fn()}
|
||||
groupByProperty={{
|
||||
id: '',
|
||||
name: 'Property 1',
|
||||
type: 'text',
|
||||
options: [{id: 'property1', value: 'Property 1', color: ''}],
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
@ -86,16 +75,10 @@ test('should match snapshot, no groups', async () => {
|
||||
})
|
||||
|
||||
test('should match snapshot with Group', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const component = wrapProviders(
|
||||
<TableGroupHeaderRowElement
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
group={boardTreeGroup}
|
||||
readonly={false}
|
||||
hideGroup={jest.fn()}
|
||||
@ -109,16 +92,10 @@ test('should match snapshot with Group', async () => {
|
||||
})
|
||||
|
||||
test('should match snapshot on read only', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const component = wrapProviders(
|
||||
<TableGroupHeaderRowElement
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
group={boardTreeGroup}
|
||||
readonly={true}
|
||||
hideGroup={jest.fn()}
|
||||
@ -132,19 +109,15 @@ test('should match snapshot on read only', async () => {
|
||||
})
|
||||
|
||||
test('should match snapshot, hide group', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const hideGroup = jest.fn()
|
||||
|
||||
view.collapsedOptionIds = [boardTreeGroup.option.id]
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
const collapsedOptionsView = TestBlockFactory.createBoardView(board)
|
||||
collapsedOptionsView.fields.collapsedOptionIds = [boardTreeGroup.option.id]
|
||||
|
||||
const component = wrapProviders(
|
||||
<TableGroupHeaderRowElement
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={collapsedOptionsView}
|
||||
group={boardTreeGroup}
|
||||
readonly={false}
|
||||
hideGroup={hideGroup}
|
||||
@ -166,18 +139,12 @@ test('should match snapshot, hide group', async () => {
|
||||
})
|
||||
|
||||
test('should match snapshot, add new', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const addNew = jest.fn()
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const component = wrapProviders(
|
||||
<TableGroupHeaderRowElement
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
group={boardTreeGroup}
|
||||
readonly={false}
|
||||
hideGroup={jest.fn()}
|
||||
@ -200,16 +167,10 @@ test('should match snapshot, add new', async () => {
|
||||
})
|
||||
|
||||
test('should match snapshot, edit title', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const component = wrapProviders(
|
||||
<TableGroupHeaderRowElement
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
group={boardTreeGroup}
|
||||
readonly={false}
|
||||
hideGroup={jest.fn()}
|
||||
|
@ -5,10 +5,10 @@ import React, {useState, useEffect} from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import {Constants} from '../../constants'
|
||||
import {IPropertyOption} from '../../blocks/board'
|
||||
import {IPropertyOption, Board, IPropertyTemplate, BoardGroup} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import {useSortable} from '../../hooks/sortable'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
import AddIcon from '../../widgets/icons/add'
|
||||
@ -22,8 +22,10 @@ import Editable from '../../widgets/editable'
|
||||
import Label from '../../widgets/label'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
group: BoardTreeGroup
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
group: BoardGroup
|
||||
groupByProperty?: IPropertyTemplate
|
||||
readonly: boolean
|
||||
hideGroup: (groupByOptionId: string) => void
|
||||
addCard: (groupByOptionId?: string) => Promise<void>
|
||||
@ -32,8 +34,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const TableGroupHeaderRow = React.memo((props: Props): JSX.Element => {
|
||||
const {boardTree, group} = props
|
||||
const {activeView} = boardTree
|
||||
const {board, activeView, group, groupByProperty} = props
|
||||
const [groupTitle, setGroupTitle] = useState(group.option.value)
|
||||
|
||||
const [isDragging, isOver, groupHeaderRef] = useSortable('groupHeader', group.option, !props.readonly, props.onDrop)
|
||||
@ -46,12 +47,12 @@ const TableGroupHeaderRow = React.memo((props: Props): JSX.Element => {
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
if (activeView.collapsedOptionIds.indexOf(group.option.id || 'undefined') < 0) {
|
||||
if (activeView.fields.collapsedOptionIds.indexOf(group.option.id || 'undefined') < 0) {
|
||||
className += ' expanded'
|
||||
}
|
||||
|
||||
const columnWidth = (templateId: string): number => {
|
||||
return Math.max(Constants.minColumnWidth, props.boardTree.activeView.columnWidths[templateId] || 0)
|
||||
return Math.max(Constants.minColumnWidth, props.activeView.fields.columnWidths[templateId] || 0)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -76,13 +77,13 @@ const TableGroupHeaderRow = React.memo((props: Props): JSX.Element => {
|
||||
title={intl.formatMessage({
|
||||
id: 'BoardComponent.no-property-title',
|
||||
defaultMessage: 'Items with an empty {property} property will go here. This column cannot be removed.',
|
||||
}, {property: boardTree.groupByProperty!.name})}
|
||||
}, {property: groupByProperty?.name})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='BoardComponent.no-property'
|
||||
defaultMessage='No {property}'
|
||||
values={{
|
||||
property: boardTree.groupByProperty!.name,
|
||||
property: groupByProperty?.name,
|
||||
}}
|
||||
/>
|
||||
</Label>}
|
||||
@ -124,7 +125,7 @@ const TableGroupHeaderRow = React.memo((props: Props): JSX.Element => {
|
||||
id='delete'
|
||||
icon={<DeleteIcon/>}
|
||||
name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)}
|
||||
onClick={() => mutator.deletePropertyOption(board, groupByProperty!, group.option)}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
{Object.entries(Constants.menuColors).map(([key, color]) => (
|
||||
@ -132,7 +133,7 @@ const TableGroupHeaderRow = React.memo((props: Props): JSX.Element => {
|
||||
key={key}
|
||||
id={key}
|
||||
name={color}
|
||||
onClick={() => mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, key)}
|
||||
onClick={() => mutator.changePropertyOptionColor(board, groupByProperty!, group.option, key)}
|
||||
/>
|
||||
))}
|
||||
</>}
|
||||
|
@ -11,17 +11,9 @@ import {DndProvider} from 'react-dnd'
|
||||
import {HTML5Backend} from 'react-dnd-html5-backend'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
import {MutableBoardTree} from '../../viewModel/boardTree'
|
||||
|
||||
import TableHeader from './tableHeader'
|
||||
|
||||
global.fetch = FetchMock.fn
|
||||
|
||||
beforeEach(() => {
|
||||
FetchMock.fn.mockReset()
|
||||
})
|
||||
|
||||
const wrapProviders = (children: any) => {
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
@ -35,27 +27,20 @@ describe('components/table/TableHeaderMenu', () => {
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
|
||||
const view2 = TestBlockFactory.createBoardView(board)
|
||||
view2.sortOptions = []
|
||||
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const cardTemplate = TestBlockFactory.createCard(board)
|
||||
cardTemplate.isTemplate = true
|
||||
view2.fields.sortOptions = []
|
||||
|
||||
test('should match snapshot, title column', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
const onAutoSizeColumn = jest.fn()
|
||||
const component = wrapProviders(
|
||||
<TableHeader
|
||||
readonly={false}
|
||||
sorted={'none'}
|
||||
name={'my Name'}
|
||||
boardTree={boardTree!}
|
||||
template={board.cardProperties[0]}
|
||||
board={board}
|
||||
activeView={view}
|
||||
cards={[]}
|
||||
views={[view, view2]}
|
||||
template={board.fields.cardProperties[0]}
|
||||
offset={0}
|
||||
onDrop={jest.fn()}
|
||||
onAutoSizeColumn={onAutoSizeColumn}
|
||||
|
@ -2,9 +2,10 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {Constants} from '../../constants'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import SortDownIcon from '../../widgets/icons/sortDown'
|
||||
import SortUpIcon from '../../widgets/icons/sortUp'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
@ -21,7 +22,10 @@ type Props = {
|
||||
readonly: boolean
|
||||
sorted: 'up'|'down'|'none'
|
||||
name: React.ReactNode
|
||||
boardTree: BoardTree
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
cards: Card[]
|
||||
views: BoardView[]
|
||||
template: IPropertyTemplate
|
||||
offset: number
|
||||
onDrop: (template: IPropertyTemplate, container: IPropertyTemplate) => void
|
||||
@ -32,7 +36,7 @@ const TableHeader = React.memo((props: Props): JSX.Element => {
|
||||
const [isDragging, isOver, columnRef] = useSortable('column', props.template, !props.readonly, props.onDrop)
|
||||
|
||||
const columnWidth = (templateId: string): number => {
|
||||
return Math.max(Constants.minColumnWidth, (props.boardTree.activeView.columnWidths[templateId] || 0) + props.offset)
|
||||
return Math.max(Constants.minColumnWidth, (props.activeView.fields.columnWidths[templateId] || 0) + props.offset)
|
||||
}
|
||||
|
||||
const onAutoSizeColumn = (templateId: string) => {
|
||||
@ -63,7 +67,10 @@ const TableHeader = React.memo((props: Props): JSX.Element => {
|
||||
{props.sorted === 'down' && <SortDownIcon/>}
|
||||
</Label>
|
||||
<TableHeaderMenu
|
||||
boardTree={props.boardTree}
|
||||
board={props.board}
|
||||
activeView={props.activeView}
|
||||
views={props.views}
|
||||
cards={props.cards}
|
||||
templateId={props.template.id}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
|
@ -13,7 +13,6 @@ import mutator from '../../mutator'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
import {MutableBoardTree} from '../../viewModel/boardTree'
|
||||
|
||||
import TableHeaderMenu from './tableHeaderMenu'
|
||||
|
||||
@ -41,23 +40,16 @@ describe('components/table/TableHeaderMenu', () => {
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
|
||||
const view2 = TestBlockFactory.createBoardView(board)
|
||||
view2.sortOptions = []
|
||||
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const cardTemplate = TestBlockFactory.createCard(board)
|
||||
cardTemplate.isTemplate = true
|
||||
view2.fields.sortOptions = []
|
||||
|
||||
test('should match snapshot, title column', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
const component = wrapIntl(
|
||||
<TableHeaderMenu
|
||||
templateId={Constants.titleColumnId}
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
views={[view, view2]}
|
||||
cards={[]}
|
||||
/>,
|
||||
)
|
||||
const {container, getByText} = render(component)
|
||||
@ -78,16 +70,13 @@ describe('components/table/TableHeaderMenu', () => {
|
||||
})
|
||||
|
||||
test('should match snapshot, other column', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
const component = wrapIntl(
|
||||
<TableHeaderMenu
|
||||
templateId={'property 1'}
|
||||
boardTree={boardTree!}
|
||||
board={board}
|
||||
activeView={view}
|
||||
views={[view, view2]}
|
||||
cards={[]}
|
||||
/>,
|
||||
)
|
||||
const {container, getByText} = render(component)
|
||||
|
@ -5,18 +5,22 @@ import React, {FC} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {Constants} from '../../constants'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import {Card} from '../../blocks/card'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import Menu from '../../widgets/menu'
|
||||
|
||||
type Props = {
|
||||
templateId: string
|
||||
boardTree: BoardTree
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
views: BoardView[]
|
||||
cards: Card[]
|
||||
}
|
||||
|
||||
const TableHeaderMenu: FC<Props> = (props: Props): JSX.Element => {
|
||||
const {boardTree, templateId} = props
|
||||
const {board, activeView} = boardTree
|
||||
const {board, activeView, templateId, views, cards} = props
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<Menu>
|
||||
@ -37,8 +41,8 @@ const TableHeaderMenu: FC<Props> = (props: Props): JSX.Element => {
|
||||
if (props.templateId === Constants.titleColumnId) {
|
||||
// TODO: Handle name column
|
||||
} else {
|
||||
const index = board.cardProperties.findIndex((o) => o.id === templateId)
|
||||
mutator.insertPropertyTemplate(boardTree, index)
|
||||
const index = board.fields.cardProperties.findIndex((o: IPropertyTemplate) => o.id === templateId)
|
||||
mutator.insertPropertyTemplate(board, activeView, index)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@ -49,8 +53,8 @@ const TableHeaderMenu: FC<Props> = (props: Props): JSX.Element => {
|
||||
if (templateId === Constants.titleColumnId) {
|
||||
// TODO: Handle title column
|
||||
} else {
|
||||
const index = board.cardProperties.findIndex((o) => o.id === templateId) + 1
|
||||
mutator.insertPropertyTemplate(boardTree, index)
|
||||
const index = board.fields.cardProperties.findIndex((o: IPropertyTemplate) => o.id === templateId) + 1
|
||||
mutator.insertPropertyTemplate(board, activeView, index)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@ -59,17 +63,17 @@ const TableHeaderMenu: FC<Props> = (props: Props): JSX.Element => {
|
||||
<Menu.Text
|
||||
id='hide'
|
||||
name={intl.formatMessage({id: 'TableHeaderMenu.hide', defaultMessage: 'Hide'})}
|
||||
onClick={() => mutator.changeViewVisibleProperties(activeView, activeView.visiblePropertyIds.filter((o) => o !== templateId))}
|
||||
onClick={() => mutator.changeViewVisibleProperties(activeView, activeView.fields.visiblePropertyIds.filter((o: string) => o !== templateId))}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='duplicate'
|
||||
name={intl.formatMessage({id: 'TableHeaderMenu.duplicate', defaultMessage: 'Duplicate'})}
|
||||
onClick={() => mutator.duplicatePropertyTemplate(boardTree, templateId)}
|
||||
onClick={() => mutator.duplicatePropertyTemplate(board, activeView, templateId)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='delete'
|
||||
name={intl.formatMessage({id: 'TableHeaderMenu.delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => mutator.deleteProperty(boardTree, templateId)}
|
||||
onClick={() => mutator.deleteProperty(board, views, cards, templateId)}
|
||||
/>
|
||||
</>}
|
||||
</Menu>
|
||||
|
@ -2,7 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {render} from '@testing-library/react'
|
||||
import configureStore from 'redux-mock-store'
|
||||
import '@testing-library/jest-dom'
|
||||
import {IntlProvider} from 'react-intl'
|
||||
|
||||
@ -12,19 +14,9 @@ import {DndProvider} from 'react-dnd'
|
||||
import {HTML5Backend} from 'react-dnd-html5-backend'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
import {MutableBoardTree} from '../../viewModel/boardTree'
|
||||
|
||||
import {MutableCardTree} from '../../viewModel/cardTree'
|
||||
|
||||
import TableRow from './tableRow'
|
||||
|
||||
global.fetch = FetchMock.fn
|
||||
|
||||
beforeEach(() => {
|
||||
FetchMock.fn.mockReset()
|
||||
})
|
||||
|
||||
const wrapProviders = (children: any) => {
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
@ -38,26 +30,36 @@ describe('components/table/TableRow', () => {
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
|
||||
const view2 = TestBlockFactory.createBoardView(board)
|
||||
view2.sortOptions = []
|
||||
view2.fields.sortOptions = []
|
||||
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const cardTemplate = TestBlockFactory.createCard(board)
|
||||
cardTemplate.isTemplate = true
|
||||
cardTemplate.fields.isTemplate = true
|
||||
|
||||
const state = {
|
||||
users: {},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
contents: {
|
||||
contents: {},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mockStore = configureStore([])
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const cardTree = new MutableCardTree(card)
|
||||
|
||||
const store = mockStore(state)
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<TableRow
|
||||
boardTree={boardTree!}
|
||||
cardTree={cardTree}
|
||||
board={board}
|
||||
activeView={view}
|
||||
card={card}
|
||||
isSelected={false}
|
||||
focusOnMount={false}
|
||||
@ -69,27 +71,21 @@ describe('components/table/TableRow', () => {
|
||||
resizingColumn={''}
|
||||
columnRefs={new Map()}
|
||||
onDrop={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot, read-only', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const cardTree = new MutableCardTree(card)
|
||||
|
||||
const store = mockStore(state)
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<TableRow
|
||||
boardTree={boardTree!}
|
||||
cardTree={cardTree}
|
||||
board={board}
|
||||
card={card}
|
||||
activeView={view}
|
||||
isSelected={false}
|
||||
focusOnMount={false}
|
||||
onSaveWithEnter={jest.fn()}
|
||||
@ -100,27 +96,21 @@ describe('components/table/TableRow', () => {
|
||||
resizingColumn={''}
|
||||
columnRefs={new Map()}
|
||||
onDrop={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot, isSelected', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const cardTree = new MutableCardTree(card)
|
||||
|
||||
const store = mockStore(state)
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<TableRow
|
||||
boardTree={boardTree!}
|
||||
cardTree={cardTree}
|
||||
board={board}
|
||||
card={card}
|
||||
activeView={view}
|
||||
isSelected={true}
|
||||
focusOnMount={false}
|
||||
onSaveWithEnter={jest.fn()}
|
||||
@ -131,29 +121,24 @@ describe('components/table/TableRow', () => {
|
||||
resizingColumn={''}
|
||||
columnRefs={new Map()}
|
||||
onDrop={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot, collapsed tree', async () => {
|
||||
// Sync
|
||||
view.collapsedOptionIds = ['value1']
|
||||
view.hiddenOptionIds = []
|
||||
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
const cardTree = new MutableCardTree(card)
|
||||
view.fields.collapsedOptionIds = ['value1']
|
||||
view.fields.hiddenOptionIds = []
|
||||
|
||||
const store = mockStore(state)
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<TableRow
|
||||
boardTree={boardTree!}
|
||||
cardTree={cardTree}
|
||||
board={board}
|
||||
card={card}
|
||||
activeView={view}
|
||||
isSelected={false}
|
||||
focusOnMount={false}
|
||||
onSaveWithEnter={jest.fn()}
|
||||
@ -164,27 +149,23 @@ describe('components/table/TableRow', () => {
|
||||
resizingColumn={''}
|
||||
columnRefs={new Map()}
|
||||
onDrop={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot, display properties', async () => {
|
||||
// Sync
|
||||
view.visiblePropertyIds = ['property1', 'property2']
|
||||
view.fields.visiblePropertyIds = ['property1', 'property2']
|
||||
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
const cardTree = new MutableCardTree(card)
|
||||
const store = mockStore(state)
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<TableRow
|
||||
boardTree={boardTree!}
|
||||
cardTree={cardTree}
|
||||
board={board}
|
||||
card={card}
|
||||
activeView={view}
|
||||
isSelected={false}
|
||||
focusOnMount={false}
|
||||
onSaveWithEnter={jest.fn()}
|
||||
@ -194,27 +175,23 @@ describe('components/table/TableRow', () => {
|
||||
resizingColumn={''}
|
||||
columnRefs={new Map()}
|
||||
onDrop={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot, resizing column', async () => {
|
||||
// Sync
|
||||
view.visiblePropertyIds = ['property1', 'property2']
|
||||
view.fields.visiblePropertyIds = ['property1', 'property2']
|
||||
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
const cardTree = new MutableCardTree(card)
|
||||
const store = mockStore(state)
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<TableRow
|
||||
boardTree={boardTree!}
|
||||
cardTree={cardTree}
|
||||
board={board}
|
||||
card={card}
|
||||
activeView={view}
|
||||
isSelected={false}
|
||||
focusOnMount={false}
|
||||
onSaveWithEnter={jest.fn()}
|
||||
@ -224,7 +201,8 @@ describe('components/table/TableRow', () => {
|
||||
resizingColumn={'property1'}
|
||||
columnRefs={new Map()}
|
||||
onDrop={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const {container} = render(component)
|
||||
expect(container).toMatchSnapshot()
|
||||
|
@ -4,21 +4,24 @@ import React, {useState, useRef, useEffect} from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {Card} from '../../blocks/card'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import {Constants} from '../../constants'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import Editable from '../../widgets/editable'
|
||||
import {useSortable} from '../../hooks/sortable'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getCardContents} from '../../store/contents'
|
||||
import {getCardComments} from '../../store/comments'
|
||||
|
||||
import PropertyValueElement from '../propertyValueElement'
|
||||
import './tableRow.scss'
|
||||
import {CardTree} from '../../viewModel/cardTree'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
card: Card
|
||||
cardTree?: CardTree
|
||||
isSelected: boolean
|
||||
focusOnMount: boolean
|
||||
onSaveWithEnter: () => void
|
||||
@ -32,14 +35,14 @@ type Props = {
|
||||
}
|
||||
|
||||
const TableRow = React.memo((props: Props) => {
|
||||
const {boardTree, onSaveWithEnter, columnRefs} = props
|
||||
const {board, activeView} = boardTree
|
||||
const {board, activeView, onSaveWithEnter, columnRefs, card} = props
|
||||
const contents = useAppSelector(getCardContents(card.id || ''))
|
||||
const comments = useAppSelector(getCardComments(card.id))
|
||||
|
||||
const titleRef = useRef<{focus(selectAll?: boolean): void}>(null)
|
||||
const [title, setTitle] = useState(props.card.title)
|
||||
const {card} = props
|
||||
const isManualSort = activeView.sortOptions.length === 0
|
||||
const isGrouped = Boolean(activeView.groupById)
|
||||
const [title, setTitle] = useState(props.card.title || '')
|
||||
const isManualSort = activeView.fields.sortOptions.length === 0
|
||||
const isGrouped = Boolean(activeView.fields.groupById)
|
||||
const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly && (isManualSort || isGrouped), props.onDrop)
|
||||
|
||||
useEffect(() => {
|
||||
@ -50,9 +53,9 @@ const TableRow = React.memo((props: Props) => {
|
||||
|
||||
const columnWidth = (templateId: string): number => {
|
||||
if (props.resizingColumn === templateId) {
|
||||
return Math.max(Constants.minColumnWidth, (props.boardTree.activeView.columnWidths[templateId] || 0) + props.offset)
|
||||
return Math.max(Constants.minColumnWidth, (props.activeView.fields.columnWidths[templateId] || 0) + props.offset)
|
||||
}
|
||||
return Math.max(Constants.minColumnWidth, props.boardTree.activeView.columnWidths[templateId] || 0)
|
||||
return Math.max(Constants.minColumnWidth, props.activeView.fields.columnWidths[templateId] || 0)
|
||||
}
|
||||
|
||||
let className = props.isSelected ? 'TableRow octo-table-row selected' : 'TableRow octo-table-row'
|
||||
@ -60,9 +63,9 @@ const TableRow = React.memo((props: Props) => {
|
||||
className += ' dragover'
|
||||
}
|
||||
if (isGrouped) {
|
||||
const groupID = activeView.groupById || ''
|
||||
const groupValue = card.properties[groupID] as string || 'undefined'
|
||||
if (activeView.collapsedOptionIds.indexOf(groupValue) > -1) {
|
||||
const groupID = activeView.fields.groupById || ''
|
||||
const groupValue = card.fields.properties[groupID] as string || 'undefined'
|
||||
if (activeView.fields.collapsedOptionIds.indexOf(groupValue) > -1) {
|
||||
className += ' hidden'
|
||||
}
|
||||
}
|
||||
@ -87,7 +90,7 @@ const TableRow = React.memo((props: Props) => {
|
||||
ref={columnRefs.get(Constants.titleColumnId)}
|
||||
>
|
||||
<div className='octo-icontitle'>
|
||||
<div className='octo-icon'>{card.icon}</div>
|
||||
<div className='octo-icon'>{card.fields.icon}</div>
|
||||
<Editable
|
||||
ref={titleRef}
|
||||
value={title}
|
||||
@ -99,14 +102,14 @@ const TableRow = React.memo((props: Props) => {
|
||||
onSaveWithEnter()
|
||||
}
|
||||
}}
|
||||
onCancel={() => setTitle(card.title)}
|
||||
onCancel={() => setTitle(card.title || '')}
|
||||
readonly={props.readonly}
|
||||
spellCheck={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='open-button'>
|
||||
<Button onClick={() => props.showCard(props.card.id)}>
|
||||
<Button onClick={() => props.showCard(props.card.id || '')}>
|
||||
<FormattedMessage
|
||||
id='TableRow.open'
|
||||
defaultMessage='Open'
|
||||
@ -117,9 +120,9 @@ const TableRow = React.memo((props: Props) => {
|
||||
|
||||
{/* Columns, one per property */}
|
||||
|
||||
{board.cardProperties.
|
||||
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
||||
map((template) => {
|
||||
{board.fields.cardProperties.
|
||||
filter((template: IPropertyTemplate) => activeView.fields.visiblePropertyIds.includes(template.id)).
|
||||
map((template: IPropertyTemplate) => {
|
||||
if (!columnRefs.get(template.id)) {
|
||||
columnRefs.set(template.id, React.createRef())
|
||||
}
|
||||
@ -133,8 +136,9 @@ const TableRow = React.memo((props: Props) => {
|
||||
<PropertyValueElement
|
||||
readOnly={props.readonly}
|
||||
card={card}
|
||||
cardTree={props.cardTree}
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
contents={contents}
|
||||
comments={comments}
|
||||
propertyTemplate={template}
|
||||
emptyDisplayValue=''
|
||||
/>
|
||||
|
@ -2,7 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {fireEvent, render} from '@testing-library/react'
|
||||
import configureStore from 'redux-mock-store'
|
||||
import '@testing-library/jest-dom'
|
||||
import {IntlProvider} from 'react-intl'
|
||||
|
||||
@ -17,9 +19,6 @@ import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
import {MutableBoardTree} from '../../viewModel/boardTree'
|
||||
|
||||
import {CardTree, MutableCardTree} from '../../viewModel/cardTree'
|
||||
|
||||
import TableRows from './tableRows'
|
||||
|
||||
@ -42,30 +41,41 @@ describe('components/table/TableRows', () => {
|
||||
const view = TestBlockFactory.createBoardView(board)
|
||||
|
||||
const view2 = TestBlockFactory.createBoardView(board)
|
||||
view2.sortOptions = []
|
||||
view2.fields.sortOptions = []
|
||||
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
const cardTemplate = TestBlockFactory.createCard(board)
|
||||
cardTemplate.isTemplate = true
|
||||
cardTemplate.fields.isTemplate = true
|
||||
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
users: {},
|
||||
comments: {
|
||||
comments: {},
|
||||
},
|
||||
contents: {
|
||||
contents: {},
|
||||
},
|
||||
cards: {
|
||||
cards: {
|
||||
[card.id]: card,
|
||||
},
|
||||
templates: {
|
||||
[cardTemplate.id]: cardTemplate,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test('should match snapshot, fire events', async () => {
|
||||
// Sync
|
||||
FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate])))
|
||||
|
||||
const boardTree = await MutableBoardTree.sync(board.id, view.id, {})
|
||||
expect(boardTree).toBeDefined()
|
||||
expect(FetchMock.fn).toBeCalledTimes(1)
|
||||
|
||||
const callback = jest.fn()
|
||||
const addCard = jest.fn()
|
||||
|
||||
const cardTrees:{ [key: string]: CardTree | undefined } = {}
|
||||
cardTrees[card.id] = new MutableCardTree(card)
|
||||
|
||||
const store = mockStore(state)
|
||||
const component = wrapProviders(
|
||||
<ReduxProvider store={store}>
|
||||
<TableRows
|
||||
boardTree={boardTree!}
|
||||
cardTrees={cardTrees}
|
||||
board={board}
|
||||
activeView={view}
|
||||
columnRefs={new Map()}
|
||||
cards={[card]}
|
||||
selectedCardIds={[]}
|
||||
@ -75,7 +85,8 @@ describe('components/table/TableRows', () => {
|
||||
addCard={addCard}
|
||||
onCardClicked={jest.fn()}
|
||||
onDrop={jest.fn()}
|
||||
/>,
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const {container, getByTitle, getByText} = render(<DndProvider backend={HTML5Backend}>{component}</DndProvider>)
|
||||
|
@ -4,17 +4,16 @@ import React from 'react'
|
||||
import {useDragLayer} from 'react-dnd'
|
||||
|
||||
import {Card} from '../../blocks/card'
|
||||
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {Board} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
|
||||
import './table.scss'
|
||||
import {CardTree} from '../../viewModel/cardTree'
|
||||
|
||||
import TableRow from './tableRow'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
cardTrees: { [key: string]: CardTree | undefined }
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
columnRefs: Map<string, React.RefObject<HTMLDivElement>>
|
||||
cards: readonly Card[]
|
||||
selectedCardIds: string[]
|
||||
@ -27,8 +26,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const TableRows = (props: Props) => {
|
||||
const {boardTree, cards} = props
|
||||
const {activeView} = boardTree
|
||||
const {board, cards, activeView} = props
|
||||
|
||||
const {offset, resizingColumn} = useDragLayer((monitor) => {
|
||||
if (monitor.getItemType() === 'horizontalGrip') {
|
||||
@ -49,14 +47,14 @@ const TableRows = (props: Props) => {
|
||||
const tableRow = (
|
||||
<TableRow
|
||||
key={card.id + card.updateAt}
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
card={card}
|
||||
cardTree={props.cardTrees[card.id]}
|
||||
isSelected={props.selectedCardIds.includes(card.id)}
|
||||
focusOnMount={props.cardIdToFocusOnRender === card.id}
|
||||
onSaveWithEnter={() => {
|
||||
if (cards.length > 0 && cards[cards.length - 1] === card) {
|
||||
props.addCard(activeView.groupById ? card.properties[activeView.groupById!] as string : '')
|
||||
props.addCard(activeView.fields.groupById ? card.fields.properties[activeView.fields.groupById!] as string : '')
|
||||
}
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
|
@ -3,11 +3,12 @@
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {FilterClause, FilterCondition} from '../../blocks/filterClause'
|
||||
import {FilterGroup} from '../../blocks/filterGroup'
|
||||
import {FilterClause, FilterCondition, createFilterClause} from '../../blocks/filterClause'
|
||||
import {createFilterGroup, isAFilterGroupInstance} from '../../blocks/filterGroup'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
|
||||
import Modal from '../modal'
|
||||
@ -17,53 +18,51 @@ import FilterEntry from './filterEntry'
|
||||
import './filterComponent.scss'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const FilterComponent = React.memo((props: Props): JSX.Element => {
|
||||
const conditionClicked = (optionId: string, filter: FilterClause): void => {
|
||||
const {boardTree} = props
|
||||
const {activeView: view} = boardTree
|
||||
const {activeView} = props
|
||||
|
||||
const filterIndex = view.filter.filters.indexOf(filter)
|
||||
const filterIndex = activeView.fields.filter.filters.indexOf(filter)
|
||||
Utils.assert(filterIndex >= 0, "Can't find filter")
|
||||
|
||||
const filterGroup = new FilterGroup(view.filter)
|
||||
const filterGroup = createFilterGroup(activeView.fields.filter)
|
||||
const newFilter = filterGroup.filters[filterIndex] as FilterClause
|
||||
|
||||
Utils.assert(newFilter, `No filter at index ${filterIndex}`)
|
||||
if (newFilter.condition !== optionId) {
|
||||
newFilter.condition = optionId as FilterCondition
|
||||
mutator.changeViewFilter(view, filterGroup)
|
||||
mutator.changeViewFilter(activeView, filterGroup)
|
||||
}
|
||||
}
|
||||
|
||||
const addFilterClicked = () => {
|
||||
const {boardTree} = props
|
||||
const {board, activeView: view} = boardTree
|
||||
const {board, activeView} = props
|
||||
|
||||
const filters = view.filter?.filters.filter((o) => !FilterGroup.isAnInstanceOf(o)) as FilterClause[] || []
|
||||
const filterGroup = new FilterGroup(view.filter)
|
||||
const filter = new FilterClause()
|
||||
const filters = activeView.fields.filter?.filters.filter((o) => !isAFilterGroupInstance(o)) as FilterClause[] || []
|
||||
const filterGroup = createFilterGroup(activeView.fields.filter)
|
||||
const filter = createFilterClause()
|
||||
|
||||
// Pick the first select property that isn't already filtered on
|
||||
const selectProperty = board.cardProperties.
|
||||
filter((o) => !filters.find((f) => f.propertyId === o.id)).
|
||||
find((o) => o.type === 'select' || o.type === 'multiSelect')
|
||||
const selectProperty = board.fields.cardProperties.
|
||||
filter((o: IPropertyTemplate) => !filters.find((f) => f.propertyId === o.id)).
|
||||
find((o: IPropertyTemplate) => o.type === 'select' || o.type === 'multiSelect')
|
||||
if (selectProperty) {
|
||||
filter.propertyId = selectProperty.id
|
||||
}
|
||||
filterGroup.filters.push(filter)
|
||||
|
||||
mutator.changeViewFilter(view, filterGroup)
|
||||
mutator.changeViewFilter(activeView, filterGroup)
|
||||
}
|
||||
|
||||
const {boardTree} = props
|
||||
const {board, activeView} = boardTree
|
||||
const {board, activeView} = props
|
||||
|
||||
// TODO: Handle FilterGroups (compound filter statements)
|
||||
const filters: FilterClause[] = activeView.filter?.filters.filter((o) => !FilterGroup.isAnInstanceOf(o)) as FilterClause[] || []
|
||||
const filters: FilterClause[] = activeView.fields.filter?.filters.filter((o) => !isAFilterGroupInstance(o)) as FilterClause[] || []
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -3,12 +3,12 @@
|
||||
import React from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import {FilterClause} from '../../blocks/filterClause'
|
||||
import {FilterGroup} from '../../blocks/filterGroup'
|
||||
import {FilterClause, areEqual as areFilterClausesEqual} from '../../blocks/filterClause'
|
||||
import {createFilterGroup, isAFilterGroupInstance} from '../../blocks/filterGroup'
|
||||
import mutator from '../../mutator'
|
||||
import {OctoUtils} from '../../octoUtils'
|
||||
import {Utils} from '../../utils'
|
||||
import {Board} from '../../blocks/board'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import Menu from '../../widgets/menu'
|
||||
@ -29,7 +29,7 @@ const FilterEntry = React.memo((props: Props): JSX.Element => {
|
||||
const {board, view, filter} = props
|
||||
const intl = useIntl()
|
||||
|
||||
const template = board.cardProperties.find((o) => o.id === filter.propertyId)
|
||||
const template = board.fields.cardProperties.find((o: IPropertyTemplate) => o.id === filter.propertyId)
|
||||
const propertyName = template ? template.name : '(unknown)' // TODO: Handle error
|
||||
const key = `${filter.propertyId}-${filter.condition}-${filter.values.join(',')}`
|
||||
return (
|
||||
@ -40,15 +40,15 @@ const FilterEntry = React.memo((props: Props): JSX.Element => {
|
||||
<MenuWrapper>
|
||||
<Button>{propertyName}</Button>
|
||||
<Menu>
|
||||
{board.cardProperties.filter((o) => o.type === 'select' || o.type === 'multiSelect').map((o) => (
|
||||
{board.fields.cardProperties.filter((o: IPropertyTemplate) => o.type === 'select' || o.type === 'multiSelect').map((o: IPropertyTemplate) => (
|
||||
<Menu.Text
|
||||
key={o.id}
|
||||
id={o.id}
|
||||
name={o.name}
|
||||
onClick={(optionId: string) => {
|
||||
const filterIndex = view.filter.filters.indexOf(filter)
|
||||
const filterIndex = view.fields.filter.filters.indexOf(filter)
|
||||
Utils.assert(filterIndex >= 0, "Can't find filter")
|
||||
const filterGroup = new FilterGroup(view.filter)
|
||||
const filterGroup = createFilterGroup(view.fields.filter)
|
||||
const newFilter = filterGroup.filters[filterIndex] as FilterClause
|
||||
Utils.assert(newFilter, `No filter at index ${filterIndex}`)
|
||||
if (newFilter.propertyId !== optionId) {
|
||||
@ -94,8 +94,8 @@ const FilterEntry = React.memo((props: Props): JSX.Element => {
|
||||
<div className='octo-spacer'/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const filterGroup = new FilterGroup(view.filter)
|
||||
filterGroup.filters = filterGroup.filters.filter((o) => FilterGroup.isAnInstanceOf(o) || !o.isEqual(filter))
|
||||
const filterGroup = createFilterGroup(view.fields.filter)
|
||||
filterGroup.filters = filterGroup.filters.filter((o) => isAFilterGroupInstance(o) || areFilterClausesEqual(o, filter))
|
||||
mutator.changeViewFilter(view, filterGroup)
|
||||
}}
|
||||
>
|
||||
|
@ -4,7 +4,7 @@ import React from 'react'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {FilterClause} from '../../blocks/filterClause'
|
||||
import {FilterGroup} from '../../blocks/filterGroup'
|
||||
import {createFilterGroup} from '../../blocks/filterGroup'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
@ -47,10 +47,10 @@ const filterValue = (props: Props): JSX.Element|null => {
|
||||
name={o.value}
|
||||
isOn={filter.values.includes(o.id)}
|
||||
onClick={(optionId) => {
|
||||
const filterIndex = view.filter.filters.indexOf(filter)
|
||||
const filterIndex = view.fields.filter.filters.indexOf(filter)
|
||||
Utils.assert(filterIndex >= 0, "Can't find filter")
|
||||
|
||||
const filterGroup = new FilterGroup(view.filter)
|
||||
const filterGroup = createFilterGroup(view.fields.filter)
|
||||
const newFilter = filterGroup.filters[filterIndex] as FilterClause
|
||||
Utils.assert(newFilter, `No filter at index ${filterIndex}`)
|
||||
if (filter.values.includes(o.id)) {
|
||||
|
@ -4,16 +4,17 @@
|
||||
import React from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {Card} from '../../blocks/card'
|
||||
import ButtonWithMenu from '../../widgets/buttons/buttonWithMenu'
|
||||
import CardIcon from '../../widgets/icons/card'
|
||||
import AddIcon from '../../widgets/icons/add'
|
||||
import Menu from '../../widgets/menu'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getCurrentBoardTemplates} from '../../store/cards'
|
||||
|
||||
import NewCardButtonTemplateItem from './newCardButtonTemplateItem'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
addCard: () => void
|
||||
addCardFromTemplate: (cardTemplateId: string) => void
|
||||
addCardTemplate: () => void
|
||||
@ -21,7 +22,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const NewCardButton = React.memo((props: Props): JSX.Element => {
|
||||
const {boardTree} = props
|
||||
const cardTemplates: Card[] = useAppSelector(getCurrentBoardTemplates)
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
@ -37,7 +38,7 @@ const NewCardButton = React.memo((props: Props): JSX.Element => {
|
||||
)}
|
||||
>
|
||||
<Menu position='left'>
|
||||
{boardTree.cardTemplates.length > 0 && <>
|
||||
{cardTemplates.length > 0 && <>
|
||||
<Menu.Label>
|
||||
<b>
|
||||
<FormattedMessage
|
||||
@ -50,7 +51,7 @@ const NewCardButton = React.memo((props: Props): JSX.Element => {
|
||||
<Menu.Separator/>
|
||||
</>}
|
||||
|
||||
{boardTree.cardTemplates.map((cardTemplate) => (
|
||||
{cardTemplates.map((cardTemplate) => (
|
||||
<NewCardButtonTemplateItem
|
||||
key={cardTemplate.id}
|
||||
cardTemplate={cardTemplate}
|
||||
|
@ -28,7 +28,7 @@ const NewCardButtonTemplateItem = React.memo((props: Props) => {
|
||||
key={cardTemplate.id}
|
||||
id={cardTemplate.id}
|
||||
name={displayName}
|
||||
icon={<div className='Icon'>{cardTemplate.icon}</div>}
|
||||
icon={<div className='Icon'>{cardTemplate.fields.icon}</div>}
|
||||
onClick={() => {
|
||||
props.addCardFromTemplate(cardTemplate.id)
|
||||
}}
|
||||
|
@ -5,7 +5,9 @@ import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import ViewMenu from '../../components/viewMenu'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {Board, IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import {Card} from '../../blocks/card'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
import DropdownIcon from '../../widgets/icons/dropdown'
|
||||
@ -25,8 +27,11 @@ import FilterComponent from './filterComponent'
|
||||
import './viewHeader.scss'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
setSearchText: (text?: string) => void
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
views: BoardView[]
|
||||
cards: Card[]
|
||||
groupByProperty?: IPropertyTemplate
|
||||
addCard: () => void
|
||||
addCardFromTemplate: (cardTemplateId: string) => void
|
||||
addCardTemplate: () => void
|
||||
@ -37,10 +42,9 @@ type Props = {
|
||||
const ViewHeader = React.memo((props: Props) => {
|
||||
const [showFilter, setShowFilter] = useState(false)
|
||||
|
||||
const {boardTree} = props
|
||||
const {board, activeView} = boardTree
|
||||
const {board, activeView, views, groupByProperty, cards} = props
|
||||
|
||||
const withGroupBy = activeView.viewType === 'board' || activeView.viewType === 'table'
|
||||
const withGroupBy = activeView.fields.viewType === 'board' || activeView.fields.viewType === 'table'
|
||||
|
||||
const [viewTitle, setViewTitle] = useState(activeView.title)
|
||||
|
||||
@ -48,7 +52,7 @@ const ViewHeader = React.memo((props: Props) => {
|
||||
setViewTitle(activeView.title)
|
||||
}, [activeView.title])
|
||||
|
||||
const hasFilter = activeView.filter && activeView.filter.filters?.length > 0
|
||||
const hasFilter = activeView.fields.filter && activeView.fields.filter.filters?.length > 0
|
||||
|
||||
return (
|
||||
<div className='ViewHeader'>
|
||||
@ -70,7 +74,8 @@ const ViewHeader = React.memo((props: Props) => {
|
||||
<IconButton icon={<DropdownIcon/>}/>
|
||||
<ViewMenu
|
||||
board={board}
|
||||
boardTree={boardTree}
|
||||
activeView={activeView}
|
||||
views={views}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
@ -82,7 +87,7 @@ const ViewHeader = React.memo((props: Props) => {
|
||||
{/* Card properties */}
|
||||
|
||||
<ViewHeaderPropertiesMenu
|
||||
properties={board.cardProperties}
|
||||
properties={board.fields.cardProperties}
|
||||
activeView={activeView}
|
||||
/>
|
||||
|
||||
@ -90,9 +95,9 @@ const ViewHeader = React.memo((props: Props) => {
|
||||
|
||||
{withGroupBy &&
|
||||
<ViewHeaderGroupByMenu
|
||||
properties={board.cardProperties}
|
||||
properties={board.fields.cardProperties}
|
||||
activeView={activeView}
|
||||
groupByPropertyName={boardTree.groupByProperty?.name}
|
||||
groupByPropertyName={groupByProperty?.name}
|
||||
/>}
|
||||
|
||||
{/* Filter */}
|
||||
@ -109,7 +114,8 @@ const ViewHeader = React.memo((props: Props) => {
|
||||
</Button>
|
||||
{showFilter &&
|
||||
<FilterComponent
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
onClose={() => setShowFilter(false)}
|
||||
/>}
|
||||
</ModalWrapper>
|
||||
@ -117,32 +123,30 @@ const ViewHeader = React.memo((props: Props) => {
|
||||
{/* Sort */}
|
||||
|
||||
<ViewHeaderSortMenu
|
||||
properties={board.cardProperties}
|
||||
properties={board.fields.cardProperties}
|
||||
activeView={activeView}
|
||||
orderedCards={boardTree.orderedCards()}
|
||||
orderedCards={cards}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
{/* Search */}
|
||||
|
||||
<ViewHeaderSearch
|
||||
boardTree={boardTree}
|
||||
setSearchText={props.setSearchText}
|
||||
/>
|
||||
<ViewHeaderSearch/>
|
||||
|
||||
{/* Options menu */}
|
||||
|
||||
{!props.readonly &&
|
||||
<>
|
||||
<ViewHeaderActionsMenu
|
||||
boardTree={boardTree}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
cards={cards}
|
||||
/>
|
||||
|
||||
{/* New card button */}
|
||||
|
||||
<NewCardButton
|
||||
boardTree={boardTree}
|
||||
addCard={props.addCard}
|
||||
addCardFromTemplate={props.addCardFromTemplate}
|
||||
addCardTemplate={props.addCardTemplate}
|
||||
|
@ -6,12 +6,14 @@ import {useIntl, IntlShape} from 'react-intl'
|
||||
import {CsvExporter} from '../../csvExporter'
|
||||
import {Archiver} from '../../archiver'
|
||||
import {IUser} from '../../user'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {Board} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import {Card} from '../../blocks/card'
|
||||
import IconButton from '../../widgets/buttons/iconButton'
|
||||
import OptionsIcon from '../../widgets/icons/options'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import {getCurrentUser} from '../../store/currentUser'
|
||||
import {getMe} from '../../store/users'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
|
||||
import ModalWrapper from '../modalWrapper'
|
||||
@ -19,29 +21,32 @@ import ShareBoardComponent from '../shareBoardComponent'
|
||||
import {sendFlashMessage} from '../flashMessages'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
cards: Card[]
|
||||
}
|
||||
|
||||
// async function testAddCards(boardTree: BoardTree, count: number) {
|
||||
// const {board, activeView} = boardTree
|
||||
|
||||
// const startCount = boardTree.cards.length
|
||||
// import {mutator} from '../../mutator'
|
||||
// import {CardFilter} from '../../cardFilter'
|
||||
// import {BlockIcons} from '../../blockIcons'
|
||||
// async function testAddCards(board: Board, activeView: BoardView, startCount: number, count: number) {
|
||||
// let optionIndex = 0
|
||||
|
||||
// mutator.performAsUndoGroup(async () => {
|
||||
// for (let i = 0; i < count; i++) {
|
||||
// const card = new MutableCard()
|
||||
// card.parentId = boardTree.board.id
|
||||
// card.rootId = boardTree.board.rootId
|
||||
// card.properties = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties)
|
||||
// const card = new Card()
|
||||
// card.parentId = board.id
|
||||
// card.rootId = board.rootId
|
||||
// card.fields.properties = CardFilter.propertiesThatMeetFilterGroup(activeView.fields.filter, board.fields.cardProperties)
|
||||
// card.title = `Test Card ${startCount + i + 1}`
|
||||
// card.icon = BlockIcons.shared.randomIcon()
|
||||
// card.fields.icon = BlockIcons.shared.randomIcon()
|
||||
|
||||
// if (boardTree.groupByProperty && boardTree.groupByProperty.options.length > 0) {
|
||||
// const groupByProperty = board.fields.cardProperties.find((o) => o.id === activeView.fields.groupById)
|
||||
// if (groupByProperty && groupByProperty.options.length > 0) {
|
||||
// // Cycle through options
|
||||
// const option = boardTree.groupByProperty.options[optionIndex]
|
||||
// optionIndex = (optionIndex + 1) % boardTree.groupByProperty.options.length
|
||||
// card.properties[boardTree.groupByProperty.id] = option.id
|
||||
// const option = groupByProperty.options[optionIndex]
|
||||
// optionIndex = (optionIndex + 1) % groupByProperty.options.length
|
||||
// card.fields.properties[groupByProperty.id] = option.id
|
||||
// }
|
||||
// mutator.insertBlock(card, 'test add card')
|
||||
// }
|
||||
@ -56,7 +61,7 @@ type Props = {
|
||||
// // Cycle through options
|
||||
// const option = boardTree.groupByProperty.options[optionIndex]
|
||||
// optionIndex = (optionIndex + 1) % boardTree.groupByProperty.options.length
|
||||
// const newCard = new MutableCard(card)
|
||||
// const newCard = new Card(card)
|
||||
// if (newCard.properties[boardTree.groupByProperty.id] !== option.id) {
|
||||
// newCard.properties[boardTree.groupByProperty.id] = option.id
|
||||
// mutator.updateBlock(newCard, card, 'test distribute cards')
|
||||
@ -74,9 +79,9 @@ type Props = {
|
||||
// })
|
||||
// }
|
||||
|
||||
function onExportCsvTrigger(boardTree: BoardTree, intl: IntlShape) {
|
||||
function onExportCsvTrigger(board: Board, activeView: BoardView, cards: Card[], intl: IntlShape) {
|
||||
try {
|
||||
CsvExporter.exportTableCsv(boardTree, intl)
|
||||
CsvExporter.exportTableCsv(board, activeView, cards, intl)
|
||||
const exportCompleteMessage = intl.formatMessage({
|
||||
id: 'ViewHeader.export-complete',
|
||||
defaultMessage: 'Export complete!',
|
||||
@ -94,8 +99,8 @@ function onExportCsvTrigger(boardTree: BoardTree, intl: IntlShape) {
|
||||
const ViewHeaderActionsMenu = React.memo((props: Props) => {
|
||||
const [showShareDialog, setShowShareDialog] = useState(false)
|
||||
|
||||
const {boardTree} = props
|
||||
const user = useAppSelector<IUser|null>(getCurrentUser)
|
||||
const {board, activeView, cards} = props
|
||||
const user = useAppSelector<IUser|null>(getMe)
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
@ -106,12 +111,12 @@ const ViewHeaderActionsMenu = React.memo((props: Props) => {
|
||||
<Menu.Text
|
||||
id='exportCsv'
|
||||
name={intl.formatMessage({id: 'ViewHeader.export-csv', defaultMessage: 'Export to CSV'})}
|
||||
onClick={() => onExportCsvTrigger(boardTree, intl)}
|
||||
onClick={() => onExportCsvTrigger(board, activeView, cards, intl)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='exportBoardArchive'
|
||||
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export board archive'})}
|
||||
onClick={() => Archiver.exportBoardArchive(boardTree)}
|
||||
onClick={() => Archiver.exportBoardArchive(board)}
|
||||
/>
|
||||
{user && user.id !== 'single-user' &&
|
||||
<Menu.Text
|
||||
@ -122,18 +127,17 @@ const ViewHeaderActionsMenu = React.memo((props: Props) => {
|
||||
}
|
||||
|
||||
{/*
|
||||
|
||||
<Menu.Separator/>
|
||||
|
||||
<Menu.Text
|
||||
id='testAdd100Cards'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-add-100-cards', defaultMessage: 'TEST: Add 100 cards'})}
|
||||
onClick={() => testAddCards(100)}
|
||||
onClick={() => testAddCards(board, activeView, cards.length, 100)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='testAdd1000Cards'
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-add-1000-cards', defaultMessage: 'TEST: Add 1,000 cards'})}
|
||||
onClick={() => testAddCards(1000)}
|
||||
onClick={() => testAddCards(board, activeView, cards.length, 1000)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='testDistributeCards'
|
||||
@ -145,13 +149,12 @@ const ViewHeaderActionsMenu = React.memo((props: Props) => {
|
||||
name={intl.formatMessage({id: 'ViewHeader.test-randomize-icons', defaultMessage: 'TEST: Randomize icons'})}
|
||||
onClick={() => testRandomizeIcons()}
|
||||
/>
|
||||
|
||||
*/}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
{showShareDialog &&
|
||||
<ShareBoardComponent
|
||||
boardId={boardTree.board.id}
|
||||
boardId={board.id || ''}
|
||||
onClose={() => setShowShareDialog(false)}
|
||||
/>
|
||||
}
|
||||
|
@ -40,15 +40,15 @@ const ViewHeaderGroupByMenu = React.memo((props: Props) => {
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{activeView.viewType === 'table' && activeView.groupById &&
|
||||
{activeView.fields.viewType === 'table' && activeView.fields.groupById &&
|
||||
<>
|
||||
<Menu.Text
|
||||
key={'ungroup'}
|
||||
id={''}
|
||||
name={intl.formatMessage({id: 'GroupBy.ungroup', defaultMessage: 'Ungroup'})}
|
||||
rightIcon={activeView.groupById === '' ? <CheckIcon/> : undefined}
|
||||
rightIcon={activeView.fields.groupById === '' ? <CheckIcon/> : undefined}
|
||||
onClick={(id) => {
|
||||
if (activeView.groupById === id) {
|
||||
if (activeView.fields.groupById === id) {
|
||||
return
|
||||
}
|
||||
mutator.changeViewGroupById(activeView, id)
|
||||
@ -56,14 +56,14 @@ const ViewHeaderGroupByMenu = React.memo((props: Props) => {
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
</>}
|
||||
{properties.filter((o: IPropertyTemplate) => o.type === 'select').map((option: IPropertyTemplate) => (
|
||||
{properties?.filter((o: IPropertyTemplate) => o.type === 'select').map((option: IPropertyTemplate) => (
|
||||
<Menu.Text
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
rightIcon={activeView.groupById === option.id ? <CheckIcon/> : undefined}
|
||||
rightIcon={activeView.fields.groupById === option.id ? <CheckIcon/> : undefined}
|
||||
onClick={(id) => {
|
||||
if (activeView.groupById === id) {
|
||||
if (activeView.fields.groupById === id) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -27,34 +27,34 @@ const ViewHeaderPropertiesMenu = React.memo((props: Props) => {
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{activeView.viewType === 'gallery' &&
|
||||
{activeView.fields.viewType === 'gallery' &&
|
||||
<Menu.Switch
|
||||
key={Constants.titleColumnId}
|
||||
id={Constants.titleColumnId}
|
||||
name={intl.formatMessage({id: 'default-properties.title', defaultMessage: 'Title'})}
|
||||
isOn={activeView.visiblePropertyIds.includes(Constants.titleColumnId)}
|
||||
isOn={activeView.fields.visiblePropertyIds.includes(Constants.titleColumnId)}
|
||||
onClick={(propertyId: string) => {
|
||||
let newVisiblePropertyIds = []
|
||||
if (activeView.visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
if (activeView.fields.visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = activeView.fields.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
} else {
|
||||
newVisiblePropertyIds = [...activeView.visiblePropertyIds, propertyId]
|
||||
newVisiblePropertyIds = [...activeView.fields.visiblePropertyIds, propertyId]
|
||||
}
|
||||
mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds)
|
||||
}}
|
||||
/>}
|
||||
{properties.map((option: IPropertyTemplate) => (
|
||||
{properties?.map((option: IPropertyTemplate) => (
|
||||
<Menu.Switch
|
||||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
isOn={activeView.visiblePropertyIds.includes(option.id)}
|
||||
isOn={activeView.fields.visiblePropertyIds.includes(option.id)}
|
||||
onClick={(propertyId: string) => {
|
||||
let newVisiblePropertyIds = []
|
||||
if (activeView.visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
if (activeView.fields.visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = activeView.fields.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
} else {
|
||||
newVisiblePropertyIds = [...activeView.visiblePropertyIds, propertyId]
|
||||
newVisiblePropertyIds = [...activeView.fields.visiblePropertyIds, propertyId]
|
||||
}
|
||||
mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds)
|
||||
}}
|
||||
|
@ -4,22 +4,20 @@ import React, {useState, useRef, useEffect} from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
import {useHotkeys} from 'react-hotkeys-hook'
|
||||
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
import Editable from '../../widgets/editable'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
setSearchText: (text?: string) => void
|
||||
}
|
||||
import {useAppSelector, useAppDispatch} from '../../store/hooks'
|
||||
import {getSearchText, setSearchText} from '../../store/searchText'
|
||||
|
||||
const ViewHeaderSearch = (props: Props) => {
|
||||
const {boardTree, setSearchText} = props
|
||||
const ViewHeaderSearch = () => {
|
||||
const searchText = useAppSelector<string>(getSearchText)
|
||||
const dispatch = useAppDispatch()
|
||||
const intl = useIntl()
|
||||
|
||||
const searchFieldRef = useRef<{focus(selectAll?: boolean): void}>(null)
|
||||
const [isSearching, setIsSearching] = useState(Boolean(boardTree.getSearchText()))
|
||||
const [searchValue, setSearchValue] = useState(boardTree.getSearchText())
|
||||
const [isSearching, setIsSearching] = useState(Boolean(searchText))
|
||||
const [searchValue, setSearchValue] = useState(searchText)
|
||||
|
||||
useEffect(() => {
|
||||
searchFieldRef.current?.focus()
|
||||
@ -40,13 +38,13 @@ const ViewHeaderSearch = (props: Props) => {
|
||||
onCancel={() => {
|
||||
setSearchValue('')
|
||||
setIsSearching(false)
|
||||
setSearchText('')
|
||||
dispatch(setSearchText(''))
|
||||
}}
|
||||
onSave={() => {
|
||||
if (searchValue === '') {
|
||||
setIsSearching(false)
|
||||
}
|
||||
setSearchText(searchValue)
|
||||
dispatch(setSearchText(searchValue))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
@ -1,10 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView, MutableBoardView, ISortOption} from '../../blocks/boardView'
|
||||
import {BoardView, ISortOption} from '../../blocks/boardView'
|
||||
import {Constants} from '../../constants'
|
||||
import {Card} from '../../blocks/card'
|
||||
import mutator from '../../mutator'
|
||||
@ -21,9 +21,37 @@ type Props = {
|
||||
}
|
||||
const ViewHeaderSortMenu = React.memo((props: Props) => {
|
||||
const {properties, activeView, orderedCards} = props
|
||||
const hasSort = activeView.sortOptions.length > 0
|
||||
const sortDisplayOptions = properties.map((o) => ({id: o.id, name: o.name}))
|
||||
sortDisplayOptions.unshift({id: Constants.titleColumnId, name: 'Name'})
|
||||
const hasSort = activeView.fields.sortOptions?.length > 0
|
||||
const sortDisplayOptions = properties?.map((o) => ({id: o.id, name: o.name}))
|
||||
sortDisplayOptions?.unshift({id: Constants.titleColumnId, name: 'Name'})
|
||||
|
||||
const sortChanged = useCallback((propertyId: string) => {
|
||||
let newSortOptions: ISortOption[] = []
|
||||
if (activeView.fields.sortOptions && activeView.fields.sortOptions[0] && activeView.fields.sortOptions[0].propertyId === propertyId) {
|
||||
// Already sorting by name, so reverse it
|
||||
newSortOptions = [
|
||||
{propertyId, reversed: !activeView.fields.sortOptions[0].reversed},
|
||||
]
|
||||
} else {
|
||||
newSortOptions = [
|
||||
{propertyId, reversed: false},
|
||||
]
|
||||
}
|
||||
mutator.changeViewSortOptions(activeView, newSortOptions)
|
||||
}, [activeView])
|
||||
|
||||
const onManualSort = useCallback(() => {
|
||||
// This sets the manual card order to the currently displayed order
|
||||
// Note: Perform this as a single update to change both properties correctly
|
||||
const newView = {...activeView, fields: {...activeView.fields}}
|
||||
newView.fields.cardOrder = orderedCards.map((o) => o.id || '') || []
|
||||
newView.fields.sortOptions = []
|
||||
mutator.updateBlock(newView, activeView, 'reorder')
|
||||
}, [activeView, orderedCards])
|
||||
|
||||
const onRevertSort = useCallback(() => {
|
||||
mutator.changeViewSortOptions(activeView, [])
|
||||
}, [activeView])
|
||||
|
||||
return (
|
||||
<MenuWrapper>
|
||||
@ -34,37 +62,28 @@ const ViewHeaderSortMenu = React.memo((props: Props) => {
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{(activeView.sortOptions.length > 0) &&
|
||||
{(activeView.fields.sortOptions?.length > 0) &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='manual'
|
||||
name='Manual'
|
||||
onClick={() => {
|
||||
// This sets the manual card order to the currently displayed order
|
||||
// Note: Perform this as a single update to change both properties correctly
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.cardOrder = orderedCards.map((o) => o.id)
|
||||
newView.sortOptions = []
|
||||
mutator.updateBlock(newView, activeView, 'reorder')
|
||||
}}
|
||||
onClick={onManualSort}
|
||||
/>
|
||||
|
||||
<Menu.Text
|
||||
id='revert'
|
||||
name='Revert'
|
||||
onClick={() => {
|
||||
mutator.changeViewSortOptions(activeView, [])
|
||||
}}
|
||||
onClick={onRevertSort}
|
||||
/>
|
||||
|
||||
<Menu.Separator/>
|
||||
</>
|
||||
}
|
||||
|
||||
{sortDisplayOptions.map((option) => {
|
||||
{sortDisplayOptions?.map((option) => {
|
||||
let rightIcon: JSX.Element | undefined
|
||||
if (activeView.sortOptions.length > 0) {
|
||||
const sortOption = activeView.sortOptions[0]
|
||||
if (activeView.fields.sortOptions?.length > 0) {
|
||||
const sortOption = activeView.fields.sortOptions[0]
|
||||
if (sortOption.propertyId === option.id) {
|
||||
rightIcon = sortOption.reversed ? <SortDownIcon/> : <SortUpIcon/>
|
||||
}
|
||||
@ -75,20 +94,7 @@ const ViewHeaderSortMenu = React.memo((props: Props) => {
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
rightIcon={rightIcon}
|
||||
onClick={(propertyId: string) => {
|
||||
let newSortOptions: ISortOption[] = []
|
||||
if (activeView.sortOptions[0] && activeView.sortOptions[0].propertyId === propertyId) {
|
||||
// Already sorting by name, so reverse it
|
||||
newSortOptions = [
|
||||
{propertyId, reversed: !activeView.sortOptions[0].reversed},
|
||||
]
|
||||
} else {
|
||||
newSortOptions = [
|
||||
{propertyId, reversed: false},
|
||||
]
|
||||
}
|
||||
mutator.changeViewSortOptions(activeView, newSortOptions)
|
||||
}}
|
||||
onClick={sortChanged}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
@ -4,12 +4,11 @@ import React, {useCallback} from 'react'
|
||||
import {injectIntl, IntlShape} from 'react-intl'
|
||||
import {generatePath, useHistory, useRouteMatch} from 'react-router-dom'
|
||||
|
||||
import {Board} from '../blocks/board'
|
||||
import {BoardView, IViewType, MutableBoardView} from '../blocks/boardView'
|
||||
import {Board, IPropertyTemplate} from '../blocks/board'
|
||||
import {IViewType, BoardView, createBoardView} from '../blocks/boardView'
|
||||
import {Constants} from '../constants'
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
import AddIcon from '../widgets/icons/add'
|
||||
import BoardIcon from '../widgets/icons/board'
|
||||
import DeleteIcon from '../widgets/icons/delete'
|
||||
@ -19,8 +18,9 @@ import GalleryIcon from '../widgets/icons/gallery'
|
||||
import Menu from '../widgets/menu'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
board: Board,
|
||||
activeView: BoardView,
|
||||
views: BoardView[],
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
}
|
||||
@ -35,11 +35,12 @@ const ViewMenu = React.memo((props: Props) => {
|
||||
}, [match, history])
|
||||
|
||||
const handleDuplicateView = useCallback(() => {
|
||||
const {boardTree} = props
|
||||
const {activeView} = props
|
||||
Utils.log('duplicateView')
|
||||
const currentViewId = boardTree.activeView.id
|
||||
const newView = boardTree.activeView.duplicate()
|
||||
newView.title = `${boardTree.activeView.title} copy`
|
||||
const currentViewId = activeView.id
|
||||
const newView = createBoardView(activeView)
|
||||
newView.title = `${activeView.title} copy`
|
||||
newView.id = Utils.createGuid()
|
||||
mutator.insertBlock(
|
||||
newView,
|
||||
'duplicate view',
|
||||
@ -53,39 +54,39 @@ const ViewMenu = React.memo((props: Props) => {
|
||||
showView(currentViewId)
|
||||
},
|
||||
)
|
||||
}, [props.boardTree, showView])
|
||||
}, [props.activeView, showView])
|
||||
|
||||
const handleDeleteView = useCallback(() => {
|
||||
const {boardTree} = props
|
||||
const {activeView, views} = props
|
||||
Utils.log('deleteView')
|
||||
const view = boardTree.activeView
|
||||
const nextView = boardTree.views.find((o) => o !== view)
|
||||
const view = activeView
|
||||
const nextView = views.find((o) => o !== view)
|
||||
mutator.deleteBlock(view, 'delete view')
|
||||
if (nextView) {
|
||||
showView(nextView.id)
|
||||
}
|
||||
}, [props.boardTree, showView])
|
||||
}, [props.views, props.activeView, showView])
|
||||
|
||||
const handleViewClick = useCallback((id: string) => {
|
||||
const {boardTree} = props
|
||||
const {views} = props
|
||||
Utils.log('view ' + id)
|
||||
const view = boardTree.views.find((o) => o.id === id)
|
||||
const view = views.find((o) => o.id === id)
|
||||
Utils.assert(view, `view not found: ${id}`)
|
||||
if (view) {
|
||||
showView(view.id)
|
||||
}
|
||||
}, [props.boardTree, showView])
|
||||
}, [props.views, showView])
|
||||
|
||||
const handleAddViewBoard = useCallback(() => {
|
||||
const {board, boardTree, intl} = props
|
||||
const {board, activeView, intl} = props
|
||||
Utils.log('addview-board')
|
||||
const view = new MutableBoardView()
|
||||
const view = createBoardView()
|
||||
view.title = intl.formatMessage({id: 'View.NewBoardTitle', defaultMessage: 'Board view'})
|
||||
view.viewType = 'board'
|
||||
view.fields.viewType = 'board'
|
||||
view.parentId = board.id
|
||||
view.rootId = board.rootId
|
||||
|
||||
const oldViewId = boardTree.activeView.id
|
||||
const oldViewId = activeView.id
|
||||
|
||||
mutator.insertBlock(
|
||||
view,
|
||||
@ -99,22 +100,22 @@ const ViewMenu = React.memo((props: Props) => {
|
||||
async () => {
|
||||
showView(oldViewId)
|
||||
})
|
||||
}, [props.boardTree, props.board, props.intl, showView])
|
||||
}, [props.activeView, props.board, props.intl, showView])
|
||||
|
||||
const handleAddViewTable = useCallback(() => {
|
||||
const {board, boardTree, intl} = props
|
||||
const {board, activeView, intl} = props
|
||||
|
||||
Utils.log('addview-table')
|
||||
const view = new MutableBoardView()
|
||||
const view = createBoardView()
|
||||
view.title = intl.formatMessage({id: 'View.NewTableTitle', defaultMessage: 'Table view'})
|
||||
view.viewType = 'table'
|
||||
view.fields.viewType = 'table'
|
||||
view.parentId = board.id
|
||||
view.rootId = board.rootId
|
||||
view.visiblePropertyIds = board.cardProperties.map((o) => o.id)
|
||||
view.columnWidths = {}
|
||||
view.columnWidths[Constants.titleColumnId] = Constants.defaultTitleColumnWidth
|
||||
view.fields.visiblePropertyIds = board.fields.cardProperties.map((o: IPropertyTemplate) => o.id)
|
||||
view.fields.columnWidths = {}
|
||||
view.fields.columnWidths[Constants.titleColumnId] = Constants.defaultTitleColumnWidth
|
||||
|
||||
const oldViewId = boardTree.activeView.id
|
||||
const oldViewId = activeView.id
|
||||
|
||||
mutator.insertBlock(
|
||||
view,
|
||||
@ -129,20 +130,20 @@ const ViewMenu = React.memo((props: Props) => {
|
||||
async () => {
|
||||
showView(oldViewId)
|
||||
})
|
||||
}, [props.boardTree, props.board, props.intl, showView])
|
||||
}, [props.activeView, props.board, props.intl, showView])
|
||||
|
||||
const handleAddViewGallery = useCallback(() => {
|
||||
const {board, boardTree, intl} = props
|
||||
const {board, activeView, intl} = props
|
||||
|
||||
Utils.log('addview-gallery')
|
||||
const view = new MutableBoardView()
|
||||
const view = createBoardView()
|
||||
view.title = intl.formatMessage({id: 'View.NewGalleryTitle', defaultMessage: 'Gallery view'})
|
||||
view.viewType = 'gallery'
|
||||
view.fields.viewType = 'gallery'
|
||||
view.parentId = board.id
|
||||
view.rootId = board.rootId
|
||||
view.visiblePropertyIds = [Constants.titleColumnId]
|
||||
view.fields.visiblePropertyIds = [Constants.titleColumnId]
|
||||
|
||||
const oldViewId = boardTree.activeView.id
|
||||
const oldViewId = activeView.id
|
||||
|
||||
mutator.insertBlock(
|
||||
view,
|
||||
@ -157,9 +158,9 @@ const ViewMenu = React.memo((props: Props) => {
|
||||
async () => {
|
||||
showView(oldViewId)
|
||||
})
|
||||
}, [props.board, props.boardTree, props.intl, showView])
|
||||
}, [props.board, props.activeView, props.intl, showView])
|
||||
|
||||
const {boardTree, intl} = props
|
||||
const {views, intl} = props
|
||||
|
||||
const duplicateViewText = intl.formatMessage({
|
||||
id: 'View.DuplicateView',
|
||||
@ -193,12 +194,12 @@ const ViewMenu = React.memo((props: Props) => {
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
{boardTree.views.map((view: BoardView) => (
|
||||
{views.map((view: BoardView) => (
|
||||
<Menu.Text
|
||||
key={view.id}
|
||||
id={view.id}
|
||||
name={view.title}
|
||||
icon={iconForViewType(view.viewType)}
|
||||
icon={iconForViewType(view.fields.viewType)}
|
||||
onClick={handleViewClick}
|
||||
/>))}
|
||||
<Menu.Separator/>
|
||||
@ -210,7 +211,7 @@ const ViewMenu = React.memo((props: Props) => {
|
||||
onClick={handleDuplicateView}
|
||||
/>
|
||||
}
|
||||
{!props.readonly && boardTree.views.length > 1 &&
|
||||
{!props.readonly && views.length > 1 &&
|
||||
<Menu.Text
|
||||
id='__deleteView'
|
||||
name={deleteViewText}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState} from 'react'
|
||||
import React, {useState, useCallback} from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
|
||||
import {BlockIcons} from '../blockIcons'
|
||||
@ -22,15 +22,19 @@ type Props = {
|
||||
}
|
||||
|
||||
const ViewTitle = React.memo((props: Props) => {
|
||||
const [title, setTitle] = useState(props.board.title)
|
||||
|
||||
const {board} = props
|
||||
|
||||
const [title, setTitle] = useState(board.title)
|
||||
const onEditTitleSave = useCallback(() => mutator.changeTitle(board, title), [board, title])
|
||||
const onEditTitleCancel = useCallback(() => setTitle(board.title), [board])
|
||||
const onDescriptionBlur = useCallback((text) => mutator.changeDescription(board, text), [board])
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<div className='ViewTitle'>
|
||||
<div className='add-buttons add-visible'>
|
||||
{!props.readonly && !board.icon &&
|
||||
{!props.readonly && !board.fields.icon &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
@ -44,7 +48,7 @@ const ViewTitle = React.memo((props: Props) => {
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
{!props.readonly && board.showDescription &&
|
||||
{!props.readonly && board.fields.showDescription &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
mutator.showDescription(board, false)
|
||||
@ -57,7 +61,7 @@ const ViewTitle = React.memo((props: Props) => {
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
{!props.readonly && !board.showDescription &&
|
||||
{!props.readonly && !board.fields.showDescription &&
|
||||
<Button
|
||||
onClick={() => {
|
||||
mutator.showDescription(board, true)
|
||||
@ -80,21 +84,19 @@ const ViewTitle = React.memo((props: Props) => {
|
||||
placeholderText={intl.formatMessage({id: 'ViewTitle.untitled-board', defaultMessage: 'Untitled board'})}
|
||||
onChange={(newTitle) => setTitle(newTitle)}
|
||||
saveOnEsc={true}
|
||||
onSave={() => mutator.changeTitle(board, title)}
|
||||
onCancel={() => setTitle(props.board.title)}
|
||||
onSave={onEditTitleSave}
|
||||
onCancel={onEditTitleCancel}
|
||||
readonly={props.readonly}
|
||||
spellCheck={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{board.showDescription &&
|
||||
{board.fields.showDescription &&
|
||||
<div className='description'>
|
||||
<MarkdownEditor
|
||||
text={board.description}
|
||||
text={board.fields.description}
|
||||
placeholderText='Add a description...'
|
||||
onBlur={(text) => {
|
||||
mutator.changeDescription(board, text)
|
||||
}}
|
||||
onBlur={onDescriptionBlur}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,12 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {useRouteMatch} from 'react-router-dom'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {IWorkspace} from '../blocks/workspace'
|
||||
import {Utils} from '../utils'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
import {WorkspaceTree} from '../viewModel/workspaceTree'
|
||||
import {getCurrentBoard} from '../store/boards'
|
||||
import {getCurrentViewCardsSortedFilteredAndGrouped} from '../store/cards'
|
||||
import {getView, getCurrentBoardViews, getCurrentViewGroupBy, getCurrentView} from '../store/views'
|
||||
import {useAppSelector} from '../store/hooks'
|
||||
|
||||
import CenterPanel from './centerPanel'
|
||||
import EmptyCenterPanel from './emptyCenterPanel'
|
||||
@ -14,56 +15,60 @@ import Sidebar from './sidebar/sidebar'
|
||||
import './workspace.scss'
|
||||
|
||||
type Props = {
|
||||
workspace?: IWorkspace
|
||||
workspaceTree: WorkspaceTree
|
||||
boardTree?: BoardTree
|
||||
setSearchText: (text?: string) => void
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
function centerContent(props: Props) {
|
||||
const {workspace, boardTree, setSearchText} = props
|
||||
const {activeView} = boardTree || {}
|
||||
function CenterContent(props: Props) {
|
||||
const match = useRouteMatch<{boardId: string, viewId: string}>()
|
||||
const board = useAppSelector(getCurrentBoard)
|
||||
const cards = useAppSelector(getCurrentViewCardsSortedFilteredAndGrouped)
|
||||
const activeView = useAppSelector(getView(match.params.viewId))
|
||||
const views = useAppSelector(getCurrentBoardViews)
|
||||
const groupByProperty = useAppSelector(getCurrentViewGroupBy)
|
||||
|
||||
if (boardTree && activeView) {
|
||||
if (board && activeView) {
|
||||
let property = groupByProperty
|
||||
if ((!property || property.type !== 'select') && activeView.fields.viewType === 'board') {
|
||||
property = board?.fields.cardProperties.find((o) => o.type === 'select')
|
||||
}
|
||||
return (
|
||||
<CenterPanel
|
||||
boardTree={boardTree}
|
||||
setSearchText={setSearchText}
|
||||
readonly={props.readonly}
|
||||
board={board}
|
||||
cards={cards}
|
||||
activeView={activeView}
|
||||
groupByProperty={property}
|
||||
views={views}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyCenterPanel workspace={workspace}/>
|
||||
<EmptyCenterPanel/>
|
||||
)
|
||||
}
|
||||
|
||||
const Workspace = React.memo((props: Props) => {
|
||||
const {workspace, boardTree, workspaceTree} = props
|
||||
|
||||
Utils.assert(workspaceTree || !props.readonly)
|
||||
const board = useAppSelector(getCurrentBoard)
|
||||
const view = useAppSelector(getCurrentView)
|
||||
|
||||
return (
|
||||
<div className='Workspace'>
|
||||
{!props.readonly &&
|
||||
<Sidebar
|
||||
workspace={workspace}
|
||||
workspaceTree={workspaceTree}
|
||||
activeBoardId={boardTree?.board.id}
|
||||
activeViewId={boardTree?.activeView.id}
|
||||
activeBoardId={board?.id}
|
||||
activeViewId={view?.id}
|
||||
/>
|
||||
}
|
||||
<div className='mainFrame'>
|
||||
{(boardTree?.board.isTemplate) &&
|
||||
{(board?.fields.isTemplate) &&
|
||||
<div className='banner'>
|
||||
<FormattedMessage
|
||||
id='Workspace.editing-board-template'
|
||||
defaultMessage="You're editing a board template"
|
||||
/>
|
||||
</div>}
|
||||
{centerContent(props)}
|
||||
<CenterContent readonly={props.readonly}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -3,20 +3,20 @@
|
||||
import {IntlShape} from 'react-intl'
|
||||
|
||||
import {BoardView} from './blocks/boardView'
|
||||
import {BoardTree} from './viewModel/boardTree'
|
||||
import {Board, IPropertyTemplate} from './blocks/board'
|
||||
import {Card} from './blocks/card'
|
||||
import {OctoUtils} from './octoUtils'
|
||||
import {Utils} from './utils'
|
||||
|
||||
class CsvExporter {
|
||||
static exportTableCsv(boardTree: BoardTree, intl: IntlShape, view?: BoardView): void {
|
||||
const {activeView} = boardTree
|
||||
static exportTableCsv(board: Board, activeView: BoardView, cards: Card[], intl: IntlShape, view?: BoardView): void {
|
||||
const viewToExport = view ?? activeView
|
||||
|
||||
if (!viewToExport) {
|
||||
return
|
||||
}
|
||||
|
||||
const rows = CsvExporter.generateTableArray(boardTree, viewToExport, intl)
|
||||
const rows = CsvExporter.generateTableArray(board, cards, viewToExport, intl)
|
||||
|
||||
let csvContent = 'data:text/csv;charset=utf-8,'
|
||||
|
||||
@ -47,16 +47,14 @@ class CsvExporter {
|
||||
return text.replace(/"/g, '""')
|
||||
}
|
||||
|
||||
private static generateTableArray(boardTree: BoardTree, viewToExport: BoardView, intl: IntlShape): string[][] {
|
||||
const {board, cards} = boardTree
|
||||
|
||||
private static generateTableArray(board: Board, cards: Card[], viewToExport: BoardView, intl: IntlShape): string[][] {
|
||||
const rows: string[][] = []
|
||||
const visibleProperties = board.cardProperties.filter((template) => viewToExport.visiblePropertyIds.includes(template.id))
|
||||
const visibleProperties = board.fields.cardProperties.filter((template: IPropertyTemplate) => viewToExport.fields.visiblePropertyIds.includes(template.id))
|
||||
|
||||
{
|
||||
// Header row
|
||||
const row: string[] = ['Title']
|
||||
visibleProperties.forEach((template) => {
|
||||
visibleProperties.forEach((template: IPropertyTemplate) => {
|
||||
row.push(template.name)
|
||||
})
|
||||
rows.push(row)
|
||||
@ -65,8 +63,8 @@ class CsvExporter {
|
||||
cards.forEach((card) => {
|
||||
const row: string[] = []
|
||||
row.push(`"${this.encodeText(card.title)}"`)
|
||||
visibleProperties.forEach((template) => {
|
||||
const propertyValue = card.properties[template.id]
|
||||
visibleProperties.forEach((template: IPropertyTemplate) => {
|
||||
const propertyValue = card.fields.properties[template.id]
|
||||
const displayValue = (OctoUtils.propertyDisplayValue(card, propertyValue, template, intl) || '') as string
|
||||
if (template.type === 'number') {
|
||||
const numericValue = propertyValue ? Number(propertyValue).toString() : ''
|
||||
|
@ -2,12 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {useEffect} from 'react'
|
||||
|
||||
import {IBlock} from '../blocks/block'
|
||||
import {Block} from '../blocks/block'
|
||||
import wsClient, {WSClient} from '../wsclient'
|
||||
|
||||
export default function useCardListener(onChange: (blocks: IBlock[]) => void, onReconnect: () => void): void {
|
||||
export default function useCardListener(onChange: (blocks: Block[]) => void, onReconnect: () => void): void {
|
||||
useEffect(() => {
|
||||
const onChangeHandler = (_: WSClient, blocks: IBlock[]) => onChange(blocks)
|
||||
const onChangeHandler = (_: WSClient, blocks: Block[]) => onChange(blocks)
|
||||
wsClient.addOnChange(onChangeHandler)
|
||||
wsClient.addOnReconnect(onReconnect)
|
||||
return () => {
|
||||
|
@ -1,16 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {BlockIcons} from './blockIcons'
|
||||
import {IBlock, MutableBlock} from './blocks/block'
|
||||
import {Board, IPropertyOption, IPropertyTemplate, MutableBoard, PropertyType} from './blocks/board'
|
||||
import {BoardView, ISortOption, MutableBoardView} from './blocks/boardView'
|
||||
import {Card, MutableCard} from './blocks/card'
|
||||
import {Block, createBlock} from './blocks/block'
|
||||
import {Board, IPropertyOption, IPropertyTemplate, PropertyType, createBoard} from './blocks/board'
|
||||
import {BoardView, ISortOption, createBoardView} from './blocks/boardView'
|
||||
import {Card, createCard} from './blocks/card'
|
||||
import {FilterGroup} from './blocks/filterGroup'
|
||||
import octoClient, {OctoClient} from './octoClient'
|
||||
import {OctoUtils} from './octoUtils'
|
||||
import undoManager from './undomanager'
|
||||
import {Utils} from './utils'
|
||||
import {BoardTree} from './viewModel/boardTree'
|
||||
|
||||
//
|
||||
// The Mutator is used to make all changes to server state
|
||||
@ -48,7 +47,7 @@ class Mutator {
|
||||
}
|
||||
}
|
||||
|
||||
async updateBlock(newBlock: IBlock, oldBlock: IBlock, description: string): Promise<void> {
|
||||
async updateBlock(newBlock: Block, oldBlock: Block, description: string): Promise<void> {
|
||||
await undoManager.perform(
|
||||
async () => {
|
||||
await octoClient.updateBlock(newBlock)
|
||||
@ -61,7 +60,7 @@ class Mutator {
|
||||
)
|
||||
}
|
||||
|
||||
private async updateBlocks(newBlocks: IBlock[], oldBlocks: IBlock[], description: string): Promise<void> {
|
||||
private async updateBlocks(newBlocks: Block[], oldBlocks: Block[], description: string): Promise<void> {
|
||||
await undoManager.perform(
|
||||
async () => {
|
||||
await octoClient.updateBlocks(newBlocks)
|
||||
@ -74,7 +73,7 @@ class Mutator {
|
||||
)
|
||||
}
|
||||
|
||||
async insertBlock(block: IBlock, description = 'add', afterRedo?: () => Promise<void>, beforeUndo?: () => Promise<void>) {
|
||||
async insertBlock(block: Block, description = 'add', afterRedo?: () => Promise<void>, beforeUndo?: () => Promise<void>) {
|
||||
await undoManager.perform(
|
||||
async () => {
|
||||
await octoClient.insertBlock(block)
|
||||
@ -89,7 +88,7 @@ class Mutator {
|
||||
)
|
||||
}
|
||||
|
||||
async insertBlocks(blocks: IBlock[], description = 'add', afterRedo?: () => Promise<void>, beforeUndo?: () => Promise<void>) {
|
||||
async insertBlocks(blocks: Block[], description = 'add', afterRedo?: () => Promise<void>, beforeUndo?: () => Promise<void>) {
|
||||
await undoManager.perform(
|
||||
async () => {
|
||||
await octoClient.insertBlocks(blocks)
|
||||
@ -108,7 +107,7 @@ class Mutator {
|
||||
)
|
||||
}
|
||||
|
||||
async deleteBlock(block: IBlock, description?: string, beforeRedo?: () => Promise<void>, afterUndo?: () => Promise<void>) {
|
||||
async deleteBlock(block: Block, description?: string, beforeRedo?: () => Promise<void>, afterUndo?: () => Promise<void>) {
|
||||
const actualDescription = description || `delete ${block.type}`
|
||||
|
||||
await undoManager.perform(
|
||||
@ -125,24 +124,25 @@ class Mutator {
|
||||
)
|
||||
}
|
||||
|
||||
async changeTitle(block: IBlock, title: string, description = 'change title') {
|
||||
const newBlock = new MutableBlock(block)
|
||||
async changeTitle(block: Block, title: string, description = 'change title') {
|
||||
const newBlock = createBlock(block)
|
||||
newBlock.title = title
|
||||
await this.updateBlock(newBlock, block, description)
|
||||
}
|
||||
|
||||
async changeIcon(block: Card | Board, icon: string, description = 'change icon') {
|
||||
let newBlock: IBlock
|
||||
let newBlock: Block
|
||||
switch (block.type) {
|
||||
case 'card': {
|
||||
const card = new MutableCard(block)
|
||||
card.icon = icon
|
||||
const card = createCard(block)
|
||||
card.fields.icon = icon
|
||||
newBlock = card
|
||||
break
|
||||
}
|
||||
case 'board': {
|
||||
const board = new MutableBoard(block)
|
||||
board.icon = icon
|
||||
const board = createBoard(block)
|
||||
|
||||
board.fields.icon = icon
|
||||
newBlock = board
|
||||
break
|
||||
}
|
||||
@ -155,15 +155,15 @@ class Mutator {
|
||||
await this.updateBlock(newBlock, block, description)
|
||||
}
|
||||
|
||||
async changeDescription(block: IBlock, boardDescription: string, description = 'change description') {
|
||||
const newBoard = new MutableBoard(block)
|
||||
newBoard.description = boardDescription
|
||||
async changeDescription(block: Block, boardDescription: string, description = 'change description') {
|
||||
const newBoard = createBoard(block)
|
||||
newBoard.fields.description = boardDescription
|
||||
await this.updateBlock(newBoard, block, description)
|
||||
}
|
||||
|
||||
async showDescription(board: Board, showDescription = true, description?: string) {
|
||||
const newBoard = new MutableBoard(board)
|
||||
newBoard.showDescription = showDescription
|
||||
const newBoard = createBoard(board)
|
||||
newBoard.fields.showDescription = showDescription
|
||||
let actionDescription = description
|
||||
if (!actionDescription) {
|
||||
actionDescription = showDescription ? 'show description' : 'hide description'
|
||||
@ -172,15 +172,14 @@ class Mutator {
|
||||
}
|
||||
|
||||
async changeCardContentOrder(card: Card, contentOrder: Array<string | string[]>, description = 'reorder'): Promise<void> {
|
||||
const newCard = new MutableCard(card)
|
||||
newCard.contentOrder = contentOrder
|
||||
const newCard = createCard(card)
|
||||
newCard.fields.contentOrder = contentOrder
|
||||
await this.updateBlock(newCard, card, description)
|
||||
}
|
||||
|
||||
// Property Templates
|
||||
|
||||
async insertPropertyTemplate(boardTree: BoardTree, index = -1, template?: IPropertyTemplate) {
|
||||
const {board, activeView} = boardTree
|
||||
async insertPropertyTemplate(board: Board, activeView: BoardView, index = -1, template?: IPropertyTemplate) {
|
||||
if (!activeView) {
|
||||
Utils.assertFailure('insertPropertyTemplate: no activeView')
|
||||
return
|
||||
@ -193,20 +192,20 @@ class Mutator {
|
||||
options: [],
|
||||
}
|
||||
|
||||
const oldBlocks: IBlock[] = [board]
|
||||
const oldBlocks: Block[] = [board]
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const startIndex = (index >= 0) ? index : board.cardProperties.length
|
||||
newBoard.cardProperties.splice(startIndex, 0, newTemplate)
|
||||
const changedBlocks: IBlock[] = [newBoard]
|
||||
const newBoard = createBoard(board)
|
||||
const startIndex = (index >= 0) ? index : board.fields.cardProperties.length
|
||||
newBoard.fields.cardProperties.splice(startIndex, 0, newTemplate)
|
||||
const changedBlocks: Block[] = [newBoard]
|
||||
|
||||
let description = 'add property'
|
||||
|
||||
if (activeView.viewType === 'table') {
|
||||
if (activeView.fields.viewType === 'table') {
|
||||
oldBlocks.push(activeView)
|
||||
|
||||
const newActiveView = new MutableBoardView(activeView)
|
||||
newActiveView.visiblePropertyIds.push(newTemplate.id)
|
||||
const newActiveView = createBoardView(activeView)
|
||||
newActiveView.fields.visiblePropertyIds.push(newTemplate.id)
|
||||
changedBlocks.push(newActiveView)
|
||||
|
||||
description = 'add column'
|
||||
@ -215,37 +214,36 @@ class Mutator {
|
||||
await this.updateBlocks(changedBlocks, oldBlocks, description)
|
||||
}
|
||||
|
||||
async duplicatePropertyTemplate(boardTree: BoardTree, propertyId: string) {
|
||||
const {board, activeView} = boardTree
|
||||
async duplicatePropertyTemplate(board: Board, activeView: BoardView, propertyId: string) {
|
||||
if (!activeView) {
|
||||
Utils.assertFailure('duplicatePropertyTemplate: no activeView')
|
||||
return
|
||||
}
|
||||
|
||||
const oldBlocks: IBlock[] = [board]
|
||||
const oldBlocks: Block[] = [board]
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const changedBlocks: IBlock[] = [newBoard]
|
||||
const index = newBoard.cardProperties.findIndex((o) => o.id === propertyId)
|
||||
const newBoard = createBoard(board)
|
||||
const changedBlocks: Block[] = [newBoard]
|
||||
const index = newBoard.fields.cardProperties.findIndex((o: IPropertyTemplate) => o.id === propertyId)
|
||||
if (index === -1) {
|
||||
Utils.assertFailure(`Cannot find template with id: ${propertyId}`)
|
||||
return
|
||||
}
|
||||
const srcTemplate = newBoard.cardProperties[index]
|
||||
const srcTemplate = newBoard.fields.cardProperties[index]
|
||||
const newTemplate: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: `${srcTemplate.name} copy`,
|
||||
type: srcTemplate.type,
|
||||
options: srcTemplate.options.slice(),
|
||||
}
|
||||
newBoard.cardProperties.splice(index + 1, 0, newTemplate)
|
||||
newBoard.fields.cardProperties.splice(index + 1, 0, newTemplate)
|
||||
|
||||
let description = 'duplicate property'
|
||||
if (activeView.viewType === 'table') {
|
||||
if (activeView.fields.viewType === 'table') {
|
||||
oldBlocks.push(activeView)
|
||||
|
||||
const newActiveView = new MutableBoardView(activeView)
|
||||
newActiveView.visiblePropertyIds.push(newTemplate.id)
|
||||
const newActiveView = createBoardView(activeView)
|
||||
newActiveView.fields.visiblePropertyIds.push(newTemplate.id)
|
||||
changedBlocks.push(newActiveView)
|
||||
|
||||
description = 'duplicate column'
|
||||
@ -255,43 +253,41 @@ class Mutator {
|
||||
}
|
||||
|
||||
async changePropertyTemplateOrder(board: Board, template: IPropertyTemplate, destIndex: number) {
|
||||
const templates = board.cardProperties
|
||||
const templates = board.fields.cardProperties
|
||||
const newValue = templates.slice()
|
||||
|
||||
const srcIndex = templates.indexOf(template)
|
||||
Utils.log(`srcIndex: ${srcIndex}, destIndex: ${destIndex}`)
|
||||
newValue.splice(destIndex, 0, newValue.splice(srcIndex, 1)[0])
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
newBoard.cardProperties = newValue
|
||||
const newBoard = createBoard(board)
|
||||
newBoard.fields.cardProperties = newValue
|
||||
|
||||
await this.updateBlock(newBoard, board, 'reorder properties')
|
||||
}
|
||||
|
||||
async deleteProperty(boardTree: BoardTree, propertyId: string) {
|
||||
const {board, views, cards} = boardTree
|
||||
async deleteProperty(board: Board, views: BoardView[], cards: Card[], propertyId: string) {
|
||||
const oldBlocks: Block[] = [board]
|
||||
|
||||
const oldBlocks: IBlock[] = [board]
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const changedBlocks: IBlock[] = [newBoard]
|
||||
newBoard.cardProperties = board.cardProperties.filter((o) => o.id !== propertyId)
|
||||
const newBoard = createBoard(board)
|
||||
const changedBlocks: Block[] = [newBoard]
|
||||
newBoard.fields.cardProperties = board.fields.cardProperties.filter((o: IPropertyTemplate) => o.id !== propertyId)
|
||||
|
||||
views.forEach((view) => {
|
||||
if (view.visiblePropertyIds.includes(propertyId)) {
|
||||
if (view.fields.visiblePropertyIds.includes(propertyId)) {
|
||||
oldBlocks.push(view)
|
||||
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.visiblePropertyIds = view.visiblePropertyIds.filter((o) => o !== propertyId)
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.visiblePropertyIds = view.fields.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
changedBlocks.push(newView)
|
||||
}
|
||||
})
|
||||
cards.forEach((card) => {
|
||||
if (card.properties[propertyId]) {
|
||||
if (card.fields.properties[propertyId]) {
|
||||
oldBlocks.push(card)
|
||||
|
||||
const newCard = new MutableCard(card)
|
||||
delete newCard.properties[propertyId]
|
||||
const newCard = createCard(card)
|
||||
delete newCard.fields.properties[propertyId]
|
||||
changedBlocks.push(newCard)
|
||||
}
|
||||
})
|
||||
@ -301,23 +297,19 @@ class Mutator {
|
||||
|
||||
// Properties
|
||||
|
||||
async insertPropertyOption(boardTree: BoardTree, template: IPropertyTemplate, option: IPropertyOption, description = 'add option') {
|
||||
const {board} = boardTree
|
||||
async insertPropertyOption(board: Board, template: IPropertyTemplate, option: IPropertyOption, description = 'add option') {
|
||||
Utils.assert(board.fields.cardProperties.includes(template))
|
||||
|
||||
Utils.assert(board.cardProperties.includes(template))
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === template.id)!
|
||||
const newBoard = createBoard(board)
|
||||
const newTemplate = newBoard.fields.cardProperties.find((o: IPropertyTemplate) => o.id === template.id)!
|
||||
newTemplate.options.push(option)
|
||||
|
||||
await this.updateBlock(newBoard, board, description)
|
||||
}
|
||||
|
||||
async deletePropertyOption(boardTree: BoardTree, template: IPropertyTemplate, option: IPropertyOption) {
|
||||
const {board} = boardTree
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === template.id)!
|
||||
async deletePropertyOption(board: Board, template: IPropertyTemplate, option: IPropertyOption) {
|
||||
const newBoard = createBoard(board)
|
||||
const newTemplate = newBoard.fields.cardProperties.find((o: IPropertyTemplate) => o.id === template.id)!
|
||||
newTemplate.options = newTemplate.options.filter((o) => o.id !== option.id)
|
||||
|
||||
await this.updateBlock(newBoard, board, 'delete option')
|
||||
@ -327,23 +319,21 @@ class Mutator {
|
||||
const srcIndex = template.options.indexOf(option)
|
||||
Utils.log(`srcIndex: ${srcIndex}, destIndex: ${destIndex}`)
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === template.id)!
|
||||
const newBoard = createBoard(board)
|
||||
const newTemplate = newBoard.fields.cardProperties.find((o: IPropertyTemplate) => o.id === template.id)!
|
||||
newTemplate.options.splice(destIndex, 0, newTemplate.options.splice(srcIndex, 1)[0])
|
||||
|
||||
await this.updateBlock(newBoard, board, 'reorder options')
|
||||
}
|
||||
|
||||
async changePropertyOptionValue(boardTree: BoardTree, propertyTemplate: IPropertyTemplate, option: IPropertyOption, value: string) {
|
||||
const {board} = boardTree
|
||||
async changePropertyOptionValue(board: Board, propertyTemplate: IPropertyTemplate, option: IPropertyOption, value: string) {
|
||||
const oldBlocks: Block[] = [board]
|
||||
|
||||
const oldBlocks: IBlock[] = [board]
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === propertyTemplate.id)!
|
||||
const newBoard = createBoard(board)
|
||||
const newTemplate = newBoard.fields.cardProperties.find((o: IPropertyTemplate) => o.id === propertyTemplate.id)!
|
||||
const newOption = newTemplate.options.find((o) => o.id === option.id)!
|
||||
newOption.value = value
|
||||
const changedBlocks: IBlock[] = [newBoard]
|
||||
const changedBlocks: Block[] = [newBoard]
|
||||
|
||||
await this.updateBlocks(changedBlocks, oldBlocks, 'rename option')
|
||||
|
||||
@ -351,32 +341,30 @@ class Mutator {
|
||||
}
|
||||
|
||||
async changePropertyOptionColor(board: Board, template: IPropertyTemplate, option: IPropertyOption, color: string) {
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === template.id)!
|
||||
const newBoard = createBoard(board)
|
||||
const newTemplate = newBoard.fields.cardProperties.find((o: IPropertyTemplate) => o.id === template.id)!
|
||||
const newOption = newTemplate.options.find((o) => o.id === option.id)!
|
||||
newOption.color = color
|
||||
await this.updateBlock(newBoard, board, 'change option color')
|
||||
}
|
||||
|
||||
async changePropertyValue(card: Card, propertyId: string, value?: string | string[], description = 'change property') {
|
||||
const newCard = new MutableCard(card)
|
||||
const newCard = createCard(card)
|
||||
if (value) {
|
||||
newCard.properties[propertyId] = value
|
||||
newCard.fields.properties[propertyId] = value
|
||||
} else {
|
||||
delete newCard.properties[propertyId]
|
||||
delete newCard.fields.properties[propertyId]
|
||||
}
|
||||
await this.updateBlock(newCard, card, description)
|
||||
}
|
||||
|
||||
async changePropertyTypeAndName(boardTree: BoardTree, propertyTemplate: IPropertyTemplate, newType: PropertyType, newName: string) {
|
||||
async changePropertyTypeAndName(board: Board, cards: Card[], propertyTemplate: IPropertyTemplate, newType: PropertyType, newName: string) {
|
||||
if (propertyTemplate.type === newType && propertyTemplate.name === newName) {
|
||||
return
|
||||
}
|
||||
|
||||
const {board} = boardTree
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === propertyTemplate.id)!
|
||||
const newBoard = createBoard(board)
|
||||
const newTemplate = newBoard.fields.cardProperties.find((o: IPropertyTemplate) => o.id === propertyTemplate.id)!
|
||||
|
||||
if (propertyTemplate.type !== newType) {
|
||||
newTemplate.options = []
|
||||
@ -385,25 +373,24 @@ class Mutator {
|
||||
newTemplate.type = newType
|
||||
newTemplate.name = newName
|
||||
|
||||
const oldBlocks: IBlock[] = [board]
|
||||
const newBlocks: IBlock[] = [newBoard]
|
||||
const oldBlocks: Block[] = [board]
|
||||
const newBlocks: Block[] = [newBoard]
|
||||
|
||||
if (propertyTemplate.type !== newType) {
|
||||
if (propertyTemplate.type === 'select' || propertyTemplate.type === 'multiSelect') { // If the old type was either select or multiselect
|
||||
const isNewTypeSelectOrMulti = newType === 'select' || newType === 'multiSelect'
|
||||
|
||||
for (const card of boardTree.allCards) {
|
||||
const oldValue = Array.isArray(card.properties[propertyTemplate.id]) ? (card.properties[propertyTemplate.id].length > 0 && card.properties[propertyTemplate.id][0]) : card.properties[propertyTemplate.id]
|
||||
|
||||
for (const card of cards) {
|
||||
const oldValue = Array.isArray(card.fields.properties[propertyTemplate.id]) ? (card.fields.properties[propertyTemplate.id].length > 0 && card.fields.properties[propertyTemplate.id][0]) : card.fields.properties[propertyTemplate.id]
|
||||
if (oldValue) {
|
||||
const newValue = isNewTypeSelectOrMulti ? propertyTemplate.options.find((o) => o.id === oldValue)?.id : propertyTemplate.options.find((o) => o.id === oldValue)?.value
|
||||
const newCard = new MutableCard(card)
|
||||
const newCard = createCard(card)
|
||||
|
||||
if (newValue) {
|
||||
newCard.properties[propertyTemplate.id] = newType === 'multiSelect' ? [newValue] : newValue
|
||||
newCard.fields.properties[propertyTemplate.id] = newType === 'multiSelect' ? [newValue] : newValue
|
||||
} else {
|
||||
// This was an invalid select option, so delete it
|
||||
delete newCard.properties[propertyTemplate.id]
|
||||
delete newCard.fields.properties[propertyTemplate.id]
|
||||
}
|
||||
|
||||
newBlocks.push(newCard)
|
||||
@ -416,10 +403,10 @@ class Mutator {
|
||||
}
|
||||
} else if (newType === 'select' || newType === 'multiSelect') { // if the new type is either select or multiselect
|
||||
// Map values to new template option IDs
|
||||
for (const card of boardTree.allCards) {
|
||||
const oldValue = card.properties[propertyTemplate.id] as string
|
||||
for (const card of cards) {
|
||||
const oldValue = card.fields.properties[propertyTemplate.id] as string
|
||||
if (oldValue) {
|
||||
let option = newTemplate.options.find((o) => o.value === oldValue)
|
||||
let option = newTemplate.options.find((o: IPropertyOption) => o.value === oldValue)
|
||||
if (!option) {
|
||||
option = {
|
||||
id: Utils.createGuid(),
|
||||
@ -429,8 +416,8 @@ class Mutator {
|
||||
newTemplate.options.push(option)
|
||||
}
|
||||
|
||||
const newCard = new MutableCard(card)
|
||||
newCard.properties[propertyTemplate.id] = newType === 'multiSelect' ? [option.id] : option.id
|
||||
const newCard = createCard(card)
|
||||
newCard.fields.properties[propertyTemplate.id] = newType === 'multiSelect' ? [option.id] : option.id
|
||||
|
||||
newBlocks.push(newCard)
|
||||
oldBlocks.push(card)
|
||||
@ -445,71 +432,71 @@ class Mutator {
|
||||
// Views
|
||||
|
||||
async changeViewSortOptions(view: BoardView, sortOptions: ISortOption[]): Promise<void> {
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.sortOptions = sortOptions
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.sortOptions = sortOptions
|
||||
await this.updateBlock(newView, view, 'sort')
|
||||
}
|
||||
|
||||
async changeViewFilter(view: BoardView, filter: FilterGroup): Promise<void> {
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.filter = filter
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.filter = filter
|
||||
await this.updateBlock(newView, view, 'filter')
|
||||
}
|
||||
|
||||
async changeViewGroupById(view: BoardView, groupById: string): Promise<void> {
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.groupById = groupById
|
||||
newView.hiddenOptionIds = []
|
||||
newView.visibleOptionIds = []
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.groupById = groupById
|
||||
newView.fields.hiddenOptionIds = []
|
||||
newView.fields.visibleOptionIds = []
|
||||
await this.updateBlock(newView, view, 'group by')
|
||||
}
|
||||
|
||||
async changeViewVisibleProperties(view: BoardView, visiblePropertyIds: string[], description = 'show / hide property'): Promise<void> {
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.visiblePropertyIds = visiblePropertyIds
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.visiblePropertyIds = visiblePropertyIds
|
||||
await this.updateBlock(newView, view, description)
|
||||
}
|
||||
|
||||
async changeViewVisibleOptionIds(view: BoardView, visibleOptionIds: string[], description = 'reorder'): Promise<void> {
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.visibleOptionIds = visibleOptionIds
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.visibleOptionIds = visibleOptionIds
|
||||
await this.updateBlock(newView, view, description)
|
||||
}
|
||||
|
||||
async changeViewHiddenOptionIds(view: BoardView, hiddenOptionIds: string[], description = 'reorder'): Promise<void> {
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.hiddenOptionIds = hiddenOptionIds
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.hiddenOptionIds = hiddenOptionIds
|
||||
await this.updateBlock(newView, view, description)
|
||||
}
|
||||
|
||||
async hideViewColumn(view: BoardView, columnOptionId: string): Promise<void> {
|
||||
if (view.hiddenOptionIds.includes(columnOptionId)) {
|
||||
if (view.fields.hiddenOptionIds.includes(columnOptionId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.visibleOptionIds = newView.visibleOptionIds.filter((o) => o !== columnOptionId)
|
||||
newView.hiddenOptionIds.push(columnOptionId)
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.visibleOptionIds = newView.fields.visibleOptionIds.filter((o) => o !== columnOptionId)
|
||||
newView.fields.hiddenOptionIds = [...newView.fields.hiddenOptionIds, columnOptionId]
|
||||
await this.updateBlock(newView, view, 'hide column')
|
||||
}
|
||||
|
||||
async unhideViewColumn(view: BoardView, columnOptionId: string): Promise<void> {
|
||||
if (!view.hiddenOptionIds.includes(columnOptionId)) {
|
||||
if (!view.fields.hiddenOptionIds.includes(columnOptionId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.hiddenOptionIds = newView.hiddenOptionIds.filter((o) => o !== columnOptionId)
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.hiddenOptionIds = newView.fields.hiddenOptionIds.filter((o) => o !== columnOptionId)
|
||||
|
||||
// Put the column at the end of the visible list
|
||||
newView.visibleOptionIds = newView.visibleOptionIds.filter((o) => o !== columnOptionId)
|
||||
newView.visibleOptionIds.push(columnOptionId)
|
||||
newView.fields.visibleOptionIds = newView.fields.visibleOptionIds.filter((o) => o !== columnOptionId)
|
||||
newView.fields.visibleOptionIds = [...newView.fields.visibleOptionIds, columnOptionId]
|
||||
await this.updateBlock(newView, view, 'show column')
|
||||
}
|
||||
|
||||
async changeViewCardOrder(view: BoardView, cardOrder: string[], description = 'reorder'): Promise<void> {
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.cardOrder = cardOrder
|
||||
const newView = createBoardView(view)
|
||||
newView.fields.cardOrder = cardOrder
|
||||
await this.updateBlock(newView, view, description)
|
||||
}
|
||||
|
||||
@ -521,12 +508,12 @@ class Mutator {
|
||||
asTemplate = false,
|
||||
afterRedo?: (newCardId: string) => Promise<void>,
|
||||
beforeUndo?: () => Promise<void>,
|
||||
): Promise<[IBlock[], string]> {
|
||||
): Promise<[Block[], string]> {
|
||||
const blocks = await octoClient.getSubtree(cardId, 2)
|
||||
const [newBlocks1, newCard] = OctoUtils.duplicateBlockTree(blocks, cardId) as [IBlock[], MutableCard, Record<string, string>]
|
||||
const [newBlocks1, newCard] = OctoUtils.duplicateBlockTree(blocks, cardId) as [Block[], Card, Record<string, string>]
|
||||
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
|
||||
Utils.log(`duplicateCard: duplicating ${newBlocks.length} blocks`)
|
||||
if (asTemplate === newCard.isTemplate) {
|
||||
if (asTemplate === newCard.fields.isTemplate) {
|
||||
// Copy template
|
||||
newCard.title = `${newCard.title} copy`
|
||||
} else if (asTemplate) {
|
||||
@ -537,11 +524,11 @@ class Mutator {
|
||||
newCard.title = ''
|
||||
|
||||
// If the template doesn't specify an icon, initialize it to a random one
|
||||
if (!newCard.icon) {
|
||||
newCard.icon = BlockIcons.shared.randomIcon()
|
||||
if (!newCard.fields.icon) {
|
||||
newCard.fields.icon = BlockIcons.shared.randomIcon()
|
||||
}
|
||||
}
|
||||
newCard.isTemplate = asTemplate
|
||||
newCard.fields.isTemplate = asTemplate
|
||||
await this.insertBlocks(
|
||||
newBlocks,
|
||||
description,
|
||||
@ -559,13 +546,13 @@ class Mutator {
|
||||
asTemplate = false,
|
||||
afterRedo?: (newBoardId: string) => Promise<void>,
|
||||
beforeUndo?: () => Promise<void>,
|
||||
): Promise<[IBlock[], string]> {
|
||||
): Promise<[Block[], string]> {
|
||||
const blocks = await octoClient.getSubtree(boardId, 3)
|
||||
const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId) as [IBlock[], MutableBoard, Record<string, string>]
|
||||
const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId) as [Block[], Board, Record<string, string>]
|
||||
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
|
||||
Utils.log(`duplicateBoard: duplicating ${newBlocks.length} blocks`)
|
||||
|
||||
if (asTemplate === newBoard.isTemplate) {
|
||||
if (asTemplate === newBoard.fields.isTemplate) {
|
||||
newBoard.title = `${newBoard.title} copy`
|
||||
} else if (asTemplate) {
|
||||
// Template from board
|
||||
@ -573,7 +560,7 @@ class Mutator {
|
||||
} else {
|
||||
// Board from template
|
||||
}
|
||||
newBoard.isTemplate = asTemplate
|
||||
newBoard.fields.isTemplate = asTemplate
|
||||
await this.insertBlocks(
|
||||
newBlocks,
|
||||
description,
|
||||
@ -591,14 +578,14 @@ class Mutator {
|
||||
asTemplate = false,
|
||||
afterRedo?: (newBoardId: string) => Promise<void>,
|
||||
beforeUndo?: () => Promise<void>,
|
||||
): Promise<[IBlock[], string]> {
|
||||
): Promise<[Block[], string]> {
|
||||
const rootClient = new OctoClient(octoClient.serverUrl, '0')
|
||||
const blocks = await rootClient.getSubtree(boardId, 3)
|
||||
const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId) as [IBlock[], MutableBoard, Record<string, string>]
|
||||
const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId) as [Block[], Board, Record<string, string>]
|
||||
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
|
||||
Utils.log(`duplicateBoard: duplicating ${newBlocks.length} blocks`)
|
||||
|
||||
if (asTemplate === newBoard.isTemplate) {
|
||||
if (asTemplate === newBoard.fields.isTemplate) {
|
||||
newBoard.title = `${newBoard.title} copy`
|
||||
} else if (asTemplate) {
|
||||
// Template from board
|
||||
@ -606,7 +593,7 @@ class Mutator {
|
||||
} else {
|
||||
// Board from template
|
||||
}
|
||||
newBoard.isTemplate = asTemplate
|
||||
newBoard.fields.isTemplate = asTemplate
|
||||
await this.insertBlocks(
|
||||
newBlocks,
|
||||
description,
|
||||
@ -621,12 +608,12 @@ class Mutator {
|
||||
// Other methods
|
||||
|
||||
// Not a mutator, but convenient to put here since Mutator wraps OctoClient
|
||||
async exportArchive(boardID?: string): Promise<IBlock[]> {
|
||||
async exportArchive(boardID?: string): Promise<Block[]> {
|
||||
return octoClient.exportArchive(boardID)
|
||||
}
|
||||
|
||||
// Not a mutator, but convenient to put here since Mutator wraps OctoClient
|
||||
async importFullArchive(blocks: readonly IBlock[]): Promise<Response> {
|
||||
async importFullArchive(blocks: readonly Block[]): Promise<Response> {
|
||||
return octoClient.importFullArchive(blocks)
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,8 @@
|
||||
// Disable console log
|
||||
console.log = jest.fn()
|
||||
|
||||
import {IBlock} from './blocks/block'
|
||||
import {MutableBoard} from './blocks/board'
|
||||
import {Block} from './blocks/block'
|
||||
import {createBoard} from './blocks/board'
|
||||
import octoClient from './octoClient'
|
||||
import 'isomorphic-fetch'
|
||||
import {FetchMock} from './test/fetchMock'
|
||||
@ -69,11 +69,11 @@ test('OctoClient: importFullArchive', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
function createBoards(): IBlock[] {
|
||||
function createBoards(): Block[] {
|
||||
const blocks = []
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const board = new MutableBoard()
|
||||
const board = createBoard()
|
||||
board.id = `board${i + 1}`
|
||||
blocks.push(board)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user