From d8390a8bf3f70ef3eda214be26ee081a98509ce0 Mon Sep 17 00:00:00 2001 From: Bharat Date: Thu, 1 Apr 2021 03:27:51 +0530 Subject: [PATCH 01/35] Search in properties added --- webapp/src/viewModel/boardTree.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/webapp/src/viewModel/boardTree.ts b/webapp/src/viewModel/boardTree.ts index 0f7d83f98..b2f279b9f 100644 --- a/webapp/src/viewModel/boardTree.ts +++ b/webapp/src/viewModel/boardTree.ts @@ -198,7 +198,13 @@ class MutableBoardTree implements BoardTree { return cards.slice() } - return cards.filter((card) => card.title?.toLocaleLowerCase().indexOf(searchText) !== -1) + return cards.filter((card: Card) => { + const searchTextInCardTitle: boolean = card.title?.toLocaleLowerCase().includes(searchText) + const searchTextInProperties: boolean = Object.values(card.properties). + map((el: string) => el.toLowerCase().includes(searchText)). + some((el: boolean) => el) + return searchTextInCardTitle || searchTextInProperties + }) } private setGroupByProperty(propertyId: string) { From 0b02ddfc4bf7bb4a42ae510e20d196405d846232 Mon Sep 17 00:00:00 2001 From: Bharat Date: Thu, 1 Apr 2021 13:26:58 +0530 Subject: [PATCH 02/35] Returning early in searchFilterCards fn --- webapp/src/viewModel/boardTree.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/webapp/src/viewModel/boardTree.ts b/webapp/src/viewModel/boardTree.ts index b2f279b9f..0ccd86d55 100644 --- a/webapp/src/viewModel/boardTree.ts +++ b/webapp/src/viewModel/boardTree.ts @@ -200,10 +200,18 @@ class MutableBoardTree implements BoardTree { return cards.filter((card: Card) => { const searchTextInCardTitle: boolean = card.title?.toLocaleLowerCase().includes(searchText) - const searchTextInProperties: boolean = Object.values(card.properties). - map((el: string) => el.toLowerCase().includes(searchText)). - some((el: boolean) => el) - return searchTextInCardTitle || searchTextInProperties + if (searchTextInCardTitle) { + return true + } + for (const property in card.properties) { + if ( + Object.prototype.hasOwnProperty.call(card.properties, property) && + card.properties[property].toLowerCase().includes(searchText) + ) { + return true + } + } + return false }) } From b21191c578194ba119d369d8d28fb574e53bbce7 Mon Sep 17 00:00:00 2001 From: ziprandom Date: Thu, 1 Apr 2021 10:14:49 +0000 Subject: [PATCH 03/35] linux: find server executable relative to app this enables having config.json in a different dir than the executables --- linux/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/linux/main.go b/linux/main.go index cdabdca37..4c6ef9ad5 100644 --- a/linux/main.go +++ b/linux/main.go @@ -6,6 +6,7 @@ import ( "log" "os" "os/exec" + "path/filepath" "strconv" "github.com/google/uuid" @@ -13,9 +14,10 @@ import ( ) var sessionToken string = "su-" + uuid.New().String() +var serverExecutable string = filepath.Join(filepath.Dir(os.Executable()), "focalboard-server") func runServer(ctx context.Context) { - cmd := exec.CommandContext(ctx, "./focalboard-server", "--monitorpid", strconv.FormatInt(int64(os.Getpid()), 10), "-single-user") + cmd := exec.CommandContext(ctx, serverExecutable, "--monitorpid", strconv.FormatInt(int64(os.Getpid()), 10), "-single-user") cmd.Env = []string{fmt.Sprintf("FOCALBOARD_SINGLE_USER_TOKEN=%s", sessionToken)} cmd.Stdout = os.Stdout err := cmd.Run() From 530dc81a1cb0c82f55dcbdca3b32bae0d1a81846 Mon Sep 17 00:00:00 2001 From: ziprandom Date: Thu, 1 Apr 2021 10:36:56 +0000 Subject: [PATCH 04/35] server: additionally read config settings from env make the server app read configuration settings from execution environment to make server deployments more convenient. --- server/services/config/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/services/config/config.go b/server/services/config/config.go index 824406bf0..6f4da9822 100644 --- a/server/services/config/config.go +++ b/server/services/config/config.go @@ -41,6 +41,8 @@ func ReadConfigFile() (*Configuration, error) { viper.SetConfigName("config") // name of config file (without extension) viper.SetConfigType("json") // REQUIRED if the config file does not have the extension in the name viper.AddConfigPath(".") // optionally look for config in the working directory + viper.SetEnvPrefix("focalboard") + viper.AutomaticEnv() // read config values from env like FOCALBOARD_SERVERROOT=... viper.SetDefault("ServerRoot", DefaultServerRoot) viper.SetDefault("Port", DefaultPort) viper.SetDefault("DBType", "sqlite3") From 37adf0a081ef38e222b3ca92d15053940a577d18 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Thu, 1 Apr 2021 08:15:06 -0700 Subject: [PATCH 05/35] Fix #109: Use free port on Mac --- mac/Focalboard.xcodeproj/project.pbxproj | 4 ++ mac/Focalboard/AppDelegate.swift | 9 ++++ mac/Focalboard/Base.lproj/Main.storyboard | 7 +++ mac/Focalboard/PortUtils.swift | 52 +++++++++++++++++++++++ mac/Focalboard/ViewController.swift | 11 +++++ 5 files changed, 83 insertions(+) create mode 100644 mac/Focalboard/PortUtils.swift diff --git a/mac/Focalboard.xcodeproj/project.pbxproj b/mac/Focalboard.xcodeproj/project.pbxproj index a991d9d9c..9b930d12a 100644 --- a/mac/Focalboard.xcodeproj/project.pbxproj +++ b/mac/Focalboard.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 8014951C261598D600A51700 /* PortUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8014951B261598D600A51700 /* PortUtils.swift */; }; 80D6DEBB252E13CB00AEED9E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D6DEBA252E13CB00AEED9E /* AppDelegate.swift */; }; 80D6DEBD252E13CB00AEED9E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D6DEBC252E13CB00AEED9E /* ViewController.swift */; }; 80D6DEBF252E13CD00AEED9E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 80D6DEBE252E13CD00AEED9E /* Assets.xcassets */; }; @@ -35,6 +36,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 8014951B261598D600A51700 /* PortUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortUtils.swift; sourceTree = ""; }; 80D6DEB7252E13CB00AEED9E /* Focalboard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Focalboard.app; sourceTree = BUILT_PRODUCTS_DIR; }; 80D6DEBA252E13CB00AEED9E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 80D6DEBC252E13CB00AEED9E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -103,6 +105,7 @@ isa = PBXGroup; children = ( 80D6DEBA252E13CB00AEED9E /* AppDelegate.swift */, + 8014951B261598D600A51700 /* PortUtils.swift */, 80D6DEBC252E13CB00AEED9E /* ViewController.swift */, 80D6DF17252F9BDE00AEED9E /* AutoSaveWindowController.swift */, 80D6DEBE252E13CD00AEED9E /* Assets.xcassets */, @@ -283,6 +286,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8014951C261598D600A51700 /* PortUtils.swift in Sources */, 80D6DF18252F9BDE00AEED9E /* AutoSaveWindowController.swift in Sources */, 80D6DEBD252E13CB00AEED9E /* ViewController.swift in Sources */, 80D6DEBB252E13CB00AEED9E /* AppDelegate.swift in Sources */, diff --git a/mac/Focalboard/AppDelegate.swift b/mac/Focalboard/AppDelegate.swift index 6b9b0eff2..ea2f25adb 100644 --- a/mac/Focalboard/AppDelegate.swift +++ b/mac/Focalboard/AppDelegate.swift @@ -88,7 +88,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { return "su-" + randomNumber } + private func getFreePort() { + if PortUtils.isPortFree(in_port_t(serverPort)) { + return + } + + serverPort = Int(PortUtils.getFreePort()) + } + private func startServer() { + getFreePort() sessionToken = generateSessionToken() let cwdUrl = webFolder() diff --git a/mac/Focalboard/Base.lproj/Main.storyboard b/mac/Focalboard/Base.lproj/Main.storyboard index 1ac221a83..be5f285ce 100644 --- a/mac/Focalboard/Base.lproj/Main.storyboard +++ b/mac/Focalboard/Base.lproj/Main.storyboard @@ -578,6 +578,13 @@ + + + + + + + diff --git a/mac/Focalboard/PortUtils.swift b/mac/Focalboard/PortUtils.swift new file mode 100644 index 000000000..697f22ac4 --- /dev/null +++ b/mac/Focalboard/PortUtils.swift @@ -0,0 +1,52 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import Foundation + +class PortUtils { + static func isPortFree(_ port: in_port_t) -> Bool { + let socketFileDescriptor = socket(AF_INET, SOCK_STREAM, 0) + if socketFileDescriptor == -1 { + return false + } + + var addr = sockaddr_in() + let sizeOfSockkAddr = MemoryLayout.size + addr.sin_len = __uint8_t(sizeOfSockkAddr) + addr.sin_family = sa_family_t(AF_INET) + addr.sin_port = Int(OSHostByteOrder()) == OSLittleEndian ? _OSSwapInt16(port) : port + addr.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1")) + addr.sin_zero = (0, 0, 0, 0, 0, 0, 0, 0) + var bind_addr = sockaddr() + memcpy(&bind_addr, &addr, Int(sizeOfSockkAddr)) + + if Darwin.bind(socketFileDescriptor, &bind_addr, socklen_t(sizeOfSockkAddr)) == -1 { + release(socket: socketFileDescriptor) + return false + } + if listen(socketFileDescriptor, SOMAXCONN ) == -1 { + release(socket: socketFileDescriptor) + return false + } + release(socket: socketFileDescriptor) + return true + } + + private static func release(socket: Int32) { + Darwin.shutdown(socket, SHUT_RDWR) + close(socket) + } + + static func getFreePort() -> in_port_t { + var portNum: in_port_t = 0 + for i in 50000..<65000 { + let isFree = isPortFree(in_port_t(i)) + if isFree { + portNum = in_port_t(i) + return portNum + } + } + + return in_port_t(0) + } +} diff --git a/mac/Focalboard/ViewController.swift b/mac/Focalboard/ViewController.swift index 96010bf35..02e3d9957 100644 --- a/mac/Focalboard/ViewController.swift +++ b/mac/Focalboard/ViewController.swift @@ -45,6 +45,17 @@ class ViewController: WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes as! Set, modifiedSince: date, completionHandler:{ }) } + @IBAction func showDiagnosticsInfo(_ sender: NSObject) { + let appDelegate = NSApplication.shared.delegate as! AppDelegate + + let alert: NSAlert = NSAlert() + alert.messageText = "Diagnostics info" + alert.informativeText = "Port: \(appDelegate.serverPort)" + alert.alertStyle = .informational + alert.addButton(withTitle: "OK") + alert.runModal() + } + @objc func onServerStarted() { NSLog("onServerStarted") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { From e708f3e8f5621b151fe07731ad6ecd4a8457ec56 Mon Sep 17 00:00:00 2001 From: Chen-I Lim Date: Thu, 1 Apr 2021 09:39:50 -0700 Subject: [PATCH 06/35] Support Cmd+ alternative to Ctrl+ hotkeys --- webapp/src/components/centerPanel.tsx | 1 + webapp/src/components/viewHeader/viewHeaderSearch.tsx | 2 +- webapp/src/pages/boardPage.tsx | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/centerPanel.tsx b/webapp/src/components/centerPanel.tsx index 4f74f8aaa..fd02641b4 100644 --- a/webapp/src/components/centerPanel.tsx +++ b/webapp/src/components/centerPanel.tsx @@ -58,6 +58,7 @@ class CenterPanel extends React.Component { e.stopPropagation() } + // TODO: Might need a different hotkey, as Cmd+D is save bookmark on Chrome if (keyName === 'ctrl+d') { // CTRL+D: Duplicate selected cards this.duplicateSelectedCards() diff --git a/webapp/src/components/viewHeader/viewHeaderSearch.tsx b/webapp/src/components/viewHeader/viewHeaderSearch.tsx index 1ef3e007a..f58930e86 100644 --- a/webapp/src/components/viewHeader/viewHeaderSearch.tsx +++ b/webapp/src/components/viewHeader/viewHeaderSearch.tsx @@ -29,7 +29,7 @@ const ViewHeaderSearch = (props: Props) => { setSearchValue(boardTree.getSearchText()) }, [boardTree]) - useHotkeys('ctrl+shift+f', () => { + useHotkeys('ctrl+shift+f,cmd+shift+f', () => { setIsSearching(true) searchFieldRef.current?.focus(true) }) diff --git a/webapp/src/pages/boardPage.tsx b/webapp/src/pages/boardPage.tsx index ce24a532d..9745c7617 100644 --- a/webapp/src/pages/boardPage.tsx +++ b/webapp/src/pages/boardPage.tsx @@ -96,7 +96,7 @@ class BoardPage extends React.Component { return } - if (keyName === 'ctrl+z') { // Cmd+Z + if (keyName === 'ctrl+z' || keyName === 'cmd+z') { // Cmd+Z Utils.log('Undo') if (mutator.canUndo) { const description = mutator.undoDescription @@ -109,7 +109,7 @@ class BoardPage extends React.Component { } else { sendFlashMessage({content: 'Nothing to Undo', severity: 'low'}) } - } else if (keyName === 'shift+ctrl+z') { // Shift+Cmd+Z + } else if (keyName === 'shift+ctrl+z' || keyName === 'shift+cmd+z') { // Shift+Cmd+Z Utils.log('Redo') if (mutator.canRedo) { const description = mutator.redoDescription @@ -161,7 +161,7 @@ class BoardPage extends React.Component { return (
Date: Thu, 1 Apr 2021 10:24:04 -0700 Subject: [PATCH 07/35] Handle select properties in search --- webapp/src/viewModel/boardTree.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/webapp/src/viewModel/boardTree.ts b/webapp/src/viewModel/boardTree.ts index 0ccd86d55..1b2eb337d 100644 --- a/webapp/src/viewModel/boardTree.ts +++ b/webapp/src/viewModel/boardTree.ts @@ -203,14 +203,25 @@ class MutableBoardTree implements BoardTree { if (searchTextInCardTitle) { return true } - for (const property in card.properties) { - if ( - Object.prototype.hasOwnProperty.call(card.properties, property) && - card.properties[property].toLowerCase().includes(searchText) - ) { - return true + + // Search for text in properties + const {board} = this + for (const [propertyId, propertyValue] of Object.entries(card.properties)) { + // TODO: Refactor to a shared function that returns the display value of a property + const propertyTemplate = board.cardProperties.find((o) => o.id === propertyId) + if (propertyTemplate) { + if (propertyTemplate.type === 'select') { + // Look up the value of the select option + const option = propertyTemplate.options.find((o) => o.id === propertyValue) + if (option?.value.toLowerCase().includes(searchText)) { + return true + } + } else if (propertyValue.toLowerCase().includes(searchText)) { + return true + } } } + return false }) } From 6843290fab235a63da1ce771c9394ebd751dc7a1 Mon Sep 17 00:00:00 2001 From: YorimiMochida Date: Thu, 1 Apr 2021 17:15:18 +0200 Subject: [PATCH 08/35] Translated using Weblate (Japanese) Currently translated at 100.0% (139 of 139 strings) Co-authored-by: YorimiMochida Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/ja/ Translation: Focalboard/webapp --- webapp/i18n/ja.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/webapp/i18n/ja.json b/webapp/i18n/ja.json index 4c880443d..f7feb0fe9 100644 --- a/webapp/i18n/ja.json +++ b/webapp/i18n/ja.json @@ -1,7 +1,7 @@ { "BoardCard.delete": "削除", "BoardCard.duplicate": "複製", - "BoardCard.untitled": "Untitled", + "BoardCard.untitled": "名無し", "BoardComponent.add-a-group": "+ グループを追加する", "BoardComponent.delete": "削除", "BoardComponent.hidden-columns": "非表示", @@ -70,9 +70,9 @@ "Sidebar.add-board": "+ ボードを追加する", "Sidebar.add-template": "+ 新しいテンプレート", "Sidebar.changePassword": "パスワードを変更する", - "Sidebar.chinese": "Chinese", - "Sidebar.dark-theme": "Dark theme", - "Sidebar.default-theme": "Default theme", + "Sidebar.chinese": "中国語", + "Sidebar.dark-theme": "ダークテーマ", + "Sidebar.default-theme": "デフォルトテーマ", "Sidebar.delete-board": "ボードを削除", "Sidebar.delete-template": "削除", "Sidebar.duplicate-board": "ボードを複製する", @@ -86,7 +86,7 @@ "Sidebar.import-archive": "インポート", "Sidebar.invite-users": "ユーザーを招待する", "Sidebar.japanese": "日本語", - "Sidebar.light-theme": "Light theme", + "Sidebar.light-theme": "ライトテーマ", "Sidebar.logout": "ログアウト", "Sidebar.no-views-in-board": "ページがありません", "Sidebar.occitan": "Occitan", @@ -98,9 +98,9 @@ "Sidebar.spanish": "Spanish", "Sidebar.template-from-board": "ボードから新しいテンプレートを作成", "Sidebar.turkish": "Turkish", - "Sidebar.untitled": "ボード", - "Sidebar.untitled-board": "(Untitled Board)", - "Sidebar.untitled-view": "(Untitled View)", + "Sidebar.untitled": "名無し", + "Sidebar.untitled-board": "(名無しのボード)", + "Sidebar.untitled-view": "(名無しのビュー)", "TableComponent.add-icon": "アイコンを追加する", "TableComponent.name": "名前", "TableComponent.plus-new": "+ 新規", From c6d9ea3c2c33c7e07566d35c2898459350de6784 Mon Sep 17 00:00:00 2001 From: Edward Smirnov Date: Thu, 1 Apr 2021 17:15:18 +0200 Subject: [PATCH 09/35] Translated using Weblate (Russian) Currently translated at 100.0% (139 of 139 strings) Co-authored-by: Edward Smirnov Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/ru/ Translation: Focalboard/webapp --- webapp/i18n/ru.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json index 9f83cc3d2..4c700b4d6 100644 --- a/webapp/i18n/ru.json +++ b/webapp/i18n/ru.json @@ -70,6 +70,7 @@ "Sidebar.add-board": "+ Добавить доску", "Sidebar.add-template": "+ Новый шаблон", "Sidebar.changePassword": "Изменить пароль", + "Sidebar.chinese": "Китайский", "Sidebar.dark-theme": "Тёмная тема", "Sidebar.default-theme": "Тема по умолчанию", "Sidebar.delete-board": "Удалить доску", @@ -88,6 +89,7 @@ "Sidebar.light-theme": "Светлая тема", "Sidebar.logout": "Выйти", "Sidebar.no-views-in-board": "Внутри нет страниц", + "Sidebar.occitan": "Окситанский", "Sidebar.russian": "Русский", "Sidebar.select-a-template": "Выберите шаблон", "Sidebar.set-language": "Язык", @@ -95,6 +97,7 @@ "Sidebar.settings": "Настройки", "Sidebar.spanish": "Испанский", "Sidebar.template-from-board": "Новый шаблон из доски", + "Sidebar.turkish": "Турецкий", "Sidebar.untitled": "Без названия", "Sidebar.untitled-board": "(Доска без названия)", "Sidebar.untitled-view": "(Вид без названия)", @@ -115,7 +118,9 @@ "ViewHeader.delete-template": "Удалить", "ViewHeader.edit-template": "Редактировать", "ViewHeader.empty-card": "Очистить карточку", + "ViewHeader.export-complete": "Экспорт завершен!", "ViewHeader.export-csv": "Экспорт в CSV", + "ViewHeader.export-failed": "Ошибка экспорта!", "ViewHeader.filter": "Фильтр", "ViewHeader.group-by": "Сгруппировать по: {property}", "ViewHeader.new": "Создать", From 21529418741c8c44cbbf5ef01c7955f3c518b5ab Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 1 Apr 2021 17:15:18 +0200 Subject: [PATCH 10/35] Update translation files Updated by "Cleanup translation files" hook in Weblate. Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/ Translation: Focalboard/webapp --- webapp/i18n/de.json | 6 +----- webapp/i18n/es.json | 6 +----- webapp/i18n/fr.json | 6 +----- webapp/i18n/ja.json | 6 +----- webapp/i18n/nl.json | 6 +----- webapp/i18n/oc.json | 6 +----- webapp/i18n/ru.json | 6 +----- webapp/i18n/tr.json | 6 +----- webapp/i18n/zh_Hans.json | 6 +----- webapp/i18n/zh_Hant.json | 6 +----- 10 files changed, 10 insertions(+), 50 deletions(-) diff --git a/webapp/i18n/de.json b/webapp/i18n/de.json index 02a1a2e25..9beaaacc9 100644 --- a/webapp/i18n/de.json +++ b/webapp/i18n/de.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "Löschen", - "BoardCard.duplicate": "Duplizieren", - "BoardCard.untitled": "Unbenannt", "BoardComponent.add-a-group": "+ Hinzufügen einer Gruppe", "BoardComponent.delete": "Löschen", "BoardComponent.hidden-columns": "Versteckte Spalten", @@ -131,6 +128,5 @@ "ViewTitle.random-icon": "Zufällig", "ViewTitle.remove-icon": "Symbol entfernen", "ViewTitle.show-description": "Beschreibung anzeigen", - "ViewTitle.untitled-board": "Unbenanntes Board", - "WorkspaceComponent.editing-board-template": "Sie bearbeiten eine Board Vorlage" + "ViewTitle.untitled-board": "Unbenanntes Board" } diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index a2ad4038a..a36e7de8c 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "Borrar", - "BoardCard.duplicate": "Duplicar", - "BoardCard.untitled": "Sin título", "BoardComponent.add-a-group": "+ Añadir un grupo", "BoardComponent.delete": "Borrar", "BoardComponent.hidden-columns": "Columnas Ocultas", @@ -133,6 +130,5 @@ "ViewTitle.random-icon": "Aleatorio", "ViewTitle.remove-icon": "Quitar Icono", "ViewTitle.show-description": "mostrar descripción", - "ViewTitle.untitled-board": "Panel sin título", - "WorkspaceComponent.editing-board-template": "Está editando un plantilla de panel" + "ViewTitle.untitled-board": "Panel sin título" } diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json index c138e4889..a1bdefcd7 100644 --- a/webapp/i18n/fr.json +++ b/webapp/i18n/fr.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "Supprimer", - "BoardCard.duplicate": "Dupliquer", - "BoardCard.untitled": "Sans titre", "BoardComponent.add-a-group": "+ Ajouter un groupe", "BoardComponent.delete": "Supprimer", "BoardComponent.hidden-columns": "Colonnes cachées", @@ -132,6 +129,5 @@ "ViewTitle.random-icon": "Aléatoire", "ViewTitle.remove-icon": "Supprimer l'icône", "ViewTitle.show-description": "montrer la description", - "ViewTitle.untitled-board": "Tableau sans titre", - "WorkspaceComponent.editing-board-template": "Vous éditez un modèle de tableau" + "ViewTitle.untitled-board": "Tableau sans titre" } diff --git a/webapp/i18n/ja.json b/webapp/i18n/ja.json index f7feb0fe9..f92bb2e8b 100644 --- a/webapp/i18n/ja.json +++ b/webapp/i18n/ja.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "削除", - "BoardCard.duplicate": "複製", - "BoardCard.untitled": "名無し", "BoardComponent.add-a-group": "+ グループを追加する", "BoardComponent.delete": "削除", "BoardComponent.hidden-columns": "非表示", @@ -136,6 +133,5 @@ "ViewTitle.random-icon": "ランダム", "ViewTitle.remove-icon": "アイコンを削除する", "ViewTitle.show-description": "説明を表示", - "ViewTitle.untitled-board": "Untitled board", - "WorkspaceComponent.editing-board-template": "ボードテンプレートを編集しています" + "ViewTitle.untitled-board": "Untitled board" } diff --git a/webapp/i18n/nl.json b/webapp/i18n/nl.json index 801975c1f..0d73067d5 100644 --- a/webapp/i18n/nl.json +++ b/webapp/i18n/nl.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "Verwijderen", - "BoardCard.duplicate": "Kopiëren", - "BoardCard.untitled": "Zonder titel", "BoardComponent.add-a-group": "+ Een groep toevoegen", "BoardComponent.delete": "Verwijderen", "BoardComponent.hidden-columns": "Verborgen kolommen", @@ -136,6 +133,5 @@ "ViewTitle.random-icon": "Willekeurig", "ViewTitle.remove-icon": "Verwijder pictogram", "ViewTitle.show-description": "beschrijving tonen", - "ViewTitle.untitled-board": "Titelloze bord", - "WorkspaceComponent.editing-board-template": "Je bent een bordsjabloon aan het bewerken" + "ViewTitle.untitled-board": "Titelloze bord" } diff --git a/webapp/i18n/oc.json b/webapp/i18n/oc.json index 41d8f72a4..13530abe3 100644 --- a/webapp/i18n/oc.json +++ b/webapp/i18n/oc.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "Suprimir", - "BoardCard.duplicate": "Duplicar", - "BoardCard.untitled": "Sens títol", "BoardComponent.add-a-group": "+ Apondre un grop", "BoardComponent.delete": "Suprimir", "BoardComponent.hidden-columns": "Colomnas rescondudas", @@ -136,6 +133,5 @@ "ViewTitle.random-icon": "Aleatòria", "ViewTitle.remove-icon": "Suprimir l'icòna", "ViewTitle.show-description": "mostrar la descripcion", - "ViewTitle.untitled-board": "Tablèu sens títol", - "WorkspaceComponent.editing-board-template": "Sètz a modificar un modèl de tablèu" + "ViewTitle.untitled-board": "Tablèu sens títol" } diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json index 4c700b4d6..47b77908a 100644 --- a/webapp/i18n/ru.json +++ b/webapp/i18n/ru.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "Удалить", - "BoardCard.duplicate": "Дубликат", - "BoardCard.untitled": "Без названия", "BoardComponent.add-a-group": "+ Добавить группу", "BoardComponent.delete": "Удалить", "BoardComponent.hidden-columns": "Скрытые колонки", @@ -136,6 +133,5 @@ "ViewTitle.random-icon": "Случайным образом", "ViewTitle.remove-icon": "Убрать иконку", "ViewTitle.show-description": "показать описание", - "ViewTitle.untitled-board": "Доска без названия", - "WorkspaceComponent.editing-board-template": "Вы редактируете шаблон доски" + "ViewTitle.untitled-board": "Доска без названия" } diff --git a/webapp/i18n/tr.json b/webapp/i18n/tr.json index df35e638f..ea699c93e 100644 --- a/webapp/i18n/tr.json +++ b/webapp/i18n/tr.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "Sil", - "BoardCard.duplicate": "Kopya Oluştur", - "BoardCard.untitled": "Başlıksız", "BoardComponent.add-a-group": "+ Grup ekle", "BoardComponent.delete": "Sil", "BoardComponent.hidden-columns": "Gizli sütunlar", @@ -136,6 +133,5 @@ "ViewTitle.random-icon": "Rastgele", "ViewTitle.remove-icon": "Simgeyi kaldır", "ViewTitle.show-description": "açıklamayı göster", - "ViewTitle.untitled-board": "Başlıksız pano", - "WorkspaceComponent.editing-board-template": "Bir pano şablonunu düzenliyorsun" + "ViewTitle.untitled-board": "Başlıksız pano" } diff --git a/webapp/i18n/zh_Hans.json b/webapp/i18n/zh_Hans.json index 27937000c..8a67818c2 100644 --- a/webapp/i18n/zh_Hans.json +++ b/webapp/i18n/zh_Hans.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "删除", - "BoardCard.duplicate": "制作副本", - "BoardCard.untitled": "无标题", "BoardComponent.add-a-group": "+ 新增群组", "BoardComponent.delete": "删除", "BoardComponent.hidden-columns": "隐藏列", @@ -136,6 +133,5 @@ "ViewTitle.random-icon": "随机", "ViewTitle.remove-icon": "移除图标", "ViewTitle.show-description": "显示叙述", - "ViewTitle.untitled-board": "无标题版面", - "WorkspaceComponent.editing-board-template": "您正在编辑版面范本" + "ViewTitle.untitled-board": "无标题版面" } diff --git a/webapp/i18n/zh_Hant.json b/webapp/i18n/zh_Hant.json index 7ddf4f886..6ebace512 100644 --- a/webapp/i18n/zh_Hant.json +++ b/webapp/i18n/zh_Hant.json @@ -1,7 +1,4 @@ { - "BoardCard.delete": "刪除", - "BoardCard.duplicate": "製作副本", - "BoardCard.untitled": "無標題", "BoardComponent.add-a-group": "+ 新增群組", "BoardComponent.delete": "刪除", "BoardComponent.hidden-columns": "隱藏列", @@ -136,6 +133,5 @@ "ViewTitle.random-icon": "隨機", "ViewTitle.remove-icon": "移除圖標", "ViewTitle.show-description": "顯示敘述", - "ViewTitle.untitled-board": "無標題版面", - "WorkspaceComponent.editing-board-template": "您正在編輯版面範本" + "ViewTitle.untitled-board": "無標題版面" } From efc09dcf56f7a440fc842b2028ceedd36fd4484e Mon Sep 17 00:00:00 2001 From: Edward Smirnov Date: Thu, 1 Apr 2021 17:15:18 +0200 Subject: [PATCH 11/35] Translated using Weblate (Russian) Currently translated at 100.0% (160 of 160 strings) Translated using Weblate (Russian) Currently translated at 100.0% (149 of 149 strings) Co-authored-by: Edward Smirnov Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/ru/ Translation: Focalboard/webapp --- webapp/i18n/ru.json | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json index 47b77908a..c8910a1e8 100644 --- a/webapp/i18n/ru.json +++ b/webapp/i18n/ru.json @@ -20,20 +20,37 @@ "ContentBlock.Delete": "Удалить", "ContentBlock.DeleteAction": "удалить", "ContentBlock.addElement": "добавить {type}", + "ContentBlock.checkbox": "флажок", + "ContentBlock.divider": "разделитель", + "ContentBlock.editCardCheckbox": "помеченный флажок", + "ContentBlock.editCardCheckboxText": "редактировать текст карточки", + "ContentBlock.editCardText": "редактировать текст карточки", + "ContentBlock.editText": "Редактировать текст...", + "ContentBlock.image": "изображение", "ContentBlock.insertAbove": "Вставить выше", "ContentBlock.moveDown": "Опустить", "ContentBlock.moveUp": "Поднять", + "ContentBlock.text": "текст", + "Dialog.closeDialog": "Закрыть диалог", + "EmptyCenterPanel.no-content": "Добавьте или выберите доску на боковой панели, чтобы начать работу.", + "EmptyCenterPanel.workspace": "Это рабочее пространство для:", "Filter.includes": "содержит", "Filter.is-empty": "пусто", "Filter.is-not-empty": "не пусто", "Filter.not-includes": "не содержит", "FilterComponent.add-filter": "+ Добавить фильтр", "FilterComponent.delete": "Удалить", + "GalleryCard.delete": "Удалить", + "GalleryCard.duplicate": "Создать дубликат", + "KanbanCard.delete": "Удалить", + "KanbanCard.duplicate": "Создать дубликат", + "KanbanCard.untitled": "Без названия", "Mutator.duplicate-board": "сделать дубликат доски", "Mutator.new-board-from-template": "новая доска из шаблона", "Mutator.new-card-from-template": "новая карточка из шаблона", "Mutator.new-template-from-board": "новый шаблон из доски", "Mutator.new-template-from-card": "новый шаблон из карточки", + "PropertyMenu.Delete": "Удалить", "PropertyMenu.changeType": "Изменить тип свойства", "PropertyMenu.typeTitle": "Тип", "PropertyType.Checkbox": "Флажок", @@ -93,6 +110,7 @@ "Sidebar.set-theme": "Тема", "Sidebar.settings": "Настройки", "Sidebar.spanish": "Испанский", + "Sidebar.system-theme": "Системная тема", "Sidebar.template-from-board": "Новый шаблон из доски", "Sidebar.turkish": "Турецкий", "Sidebar.untitled": "Без названия", @@ -109,8 +127,14 @@ "TableHeaderMenu.sort-ascending": "Сортировать по возрастанию", "TableHeaderMenu.sort-descending": "Сортировать по убыванию", "TableRow.open": "Открыть", + "View.AddView": "Добавить вид", + "View.Board": "Доска", + "View.DeleteView": "Удалить вид", + "View.DuplicateView": "Создать дубликат вида", "View.NewBoardTitle": "Вид доски", + "View.NewGalleryTitle": "Представление \"галерея\"", "View.NewTableTitle": "Вид таблицы", + "View.Table": "Таблица", "ViewHeader.add-template": "+ Новый шаблон", "ViewHeader.delete-template": "Удалить", "ViewHeader.edit-template": "Редактировать", @@ -133,5 +157,6 @@ "ViewTitle.random-icon": "Случайным образом", "ViewTitle.remove-icon": "Убрать иконку", "ViewTitle.show-description": "показать описание", - "ViewTitle.untitled-board": "Доска без названия" + "ViewTitle.untitled-board": "Доска без названия", + "Workspace.editing-board-template": "Вы редактируете шаблон доски" } From abc79b385d857f9a10810ed5b935acb8e89ea5f6 Mon Sep 17 00:00:00 2001 From: Kaya Zeren Date: Thu, 1 Apr 2021 17:15:18 +0200 Subject: [PATCH 12/35] Translated using Weblate (Turkish) Currently translated at 100.0% (149 of 149 strings) Co-authored-by: Kaya Zeren Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/tr/ Translation: Focalboard/webapp --- webapp/i18n/tr.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/webapp/i18n/tr.json b/webapp/i18n/tr.json index ea699c93e..3dfdd5070 100644 --- a/webapp/i18n/tr.json +++ b/webapp/i18n/tr.json @@ -20,15 +20,26 @@ "ContentBlock.Delete": "Sil", "ContentBlock.DeleteAction": "sil", "ContentBlock.addElement": "{type} ekle", + "ContentBlock.checkbox": "işaret kutusu", + "ContentBlock.divider": "ayıraç", + "ContentBlock.editCardCheckbox": "değiştirilmiş işaret kutusu", + "ContentBlock.editCardCheckboxText": "kart metnini düzenle", + "ContentBlock.editCardText": "kart metnini düzenle", + "ContentBlock.editText": "Metni düzenle...", + "ContentBlock.image": "görsel", "ContentBlock.insertAbove": "Üste ekle", "ContentBlock.moveDown": "Alta taşı", "ContentBlock.moveUp": "Yukarı taşı", + "ContentBlock.text": "metin", "Filter.includes": "içerir", "Filter.is-empty": "boş", "Filter.is-not-empty": "boş değil", "Filter.not-includes": "içermez", "FilterComponent.add-filter": "+ Filtre ekle", "FilterComponent.delete": "Sil", + "KanbanCard.delete": "Sil", + "KanbanCard.duplicate": "Kopyala", + "KanbanCard.untitled": "Başlıksız", "Mutator.duplicate-board": "panonun kopyasını oluştur", "Mutator.new-board-from-template": "şablondan yeni pano oluştur", "Mutator.new-card-from-template": "şablondan yeni kart oluştur", @@ -93,6 +104,7 @@ "Sidebar.set-theme": "Tema ayarla", "Sidebar.settings": "Ayarlar", "Sidebar.spanish": "İspanyolca", + "Sidebar.system-theme": "Sistem teması", "Sidebar.template-from-board": "Panodan yeni şablon", "Sidebar.turkish": "Türkçe", "Sidebar.untitled": "Başlıksız", @@ -110,6 +122,7 @@ "TableHeaderMenu.sort-descending": "Azalan sıralama", "TableRow.open": "Aç", "View.NewBoardTitle": "Pano görünümü", + "View.NewGalleryTitle": "Galeri görünümü", "View.NewTableTitle": "Tablo görünümü", "ViewHeader.add-template": "+ Yeni şablon", "ViewHeader.delete-template": "Sil", @@ -133,5 +146,6 @@ "ViewTitle.random-icon": "Rastgele", "ViewTitle.remove-icon": "Simgeyi kaldır", "ViewTitle.show-description": "açıklamayı göster", - "ViewTitle.untitled-board": "Başlıksız pano" + "ViewTitle.untitled-board": "Başlıksız pano", + "Workspace.editing-board-template": "Bir tahta kalıbını düzenliyorsunuz" } From f13887c0443005cf259f32475b590be718e207a6 Mon Sep 17 00:00:00 2001 From: aeomin Date: Thu, 1 Apr 2021 17:15:19 +0200 Subject: [PATCH 13/35] Translated using Weblate (Chinese (Simplified)) Currently translated at 97.9% (146 of 149 strings) Co-authored-by: aeomin Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/zh_Hans/ Translation: Focalboard/webapp --- webapp/i18n/zh_Hans.json | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/webapp/i18n/zh_Hans.json b/webapp/i18n/zh_Hans.json index 8a67818c2..80476815e 100644 --- a/webapp/i18n/zh_Hans.json +++ b/webapp/i18n/zh_Hans.json @@ -20,15 +20,24 @@ "ContentBlock.Delete": "删除", "ContentBlock.DeleteAction": "删除", "ContentBlock.addElement": "新增 {type}", + "ContentBlock.checkbox": "复选框", + "ContentBlock.editCardCheckboxText": "编辑卡片文字", + "ContentBlock.editCardText": "编辑卡片文字", + "ContentBlock.editText": "编辑文字...", + "ContentBlock.image": "图片", "ContentBlock.insertAbove": "在上方插入", "ContentBlock.moveDown": "下移", "ContentBlock.moveUp": "上移", + "ContentBlock.text": "文字", "Filter.includes": "含有", - "Filter.is-empty": "爲空", - "Filter.is-not-empty": "不爲空", + "Filter.is-empty": "为空", + "Filter.is-not-empty": "不为空", "Filter.not-includes": "不包含", "FilterComponent.add-filter": "+ 增加过滤条件", "FilterComponent.delete": "删除", + "KanbanCard.delete": "删除", + "KanbanCard.duplicate": "复本", + "KanbanCard.untitled": "无标题", "Mutator.duplicate-board": "复制版面", "Mutator.new-board-from-template": "使用范本新增版面", "Mutator.new-card-from-template": "使用范本新增卡片", @@ -93,6 +102,7 @@ "Sidebar.set-theme": "设定佈景主题", "Sidebar.settings": "设定", "Sidebar.spanish": "西班牙文", + "Sidebar.system-theme": "系统主题风格", "Sidebar.template-from-board": "从版面新增范本", "Sidebar.turkish": "土耳其语", "Sidebar.untitled": "无标题", @@ -133,5 +143,6 @@ "ViewTitle.random-icon": "随机", "ViewTitle.remove-icon": "移除图标", "ViewTitle.show-description": "显示叙述", - "ViewTitle.untitled-board": "无标题版面" + "ViewTitle.untitled-board": "无标题版面", + "Workspace.editing-board-template": "您正在编辑板模板" } From c8f7d553fb5c0ad9067e0e999dd2112e059f2ace Mon Sep 17 00:00:00 2001 From: Tom De Moor Date: Thu, 1 Apr 2021 17:15:19 +0200 Subject: [PATCH 14/35] Translated using Weblate (Dutch) Currently translated at 100.0% (160 of 160 strings) Translated using Weblate (Dutch) Currently translated at 85.6% (137 of 160 strings) Co-authored-by: Tom De Moor Translate-URL: https://translate.mattermost.com/projects/focalboard/webapp/nl/ Translation: Focalboard/webapp --- webapp/i18n/nl.json | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/webapp/i18n/nl.json b/webapp/i18n/nl.json index 0d73067d5..d4823339d 100644 --- a/webapp/i18n/nl.json +++ b/webapp/i18n/nl.json @@ -20,23 +20,40 @@ "ContentBlock.Delete": "Verwijderen", "ContentBlock.DeleteAction": "verwijderen", "ContentBlock.addElement": "voeg {type} toe", + "ContentBlock.checkbox": "selectievakje", + "ContentBlock.divider": "verdeler", + "ContentBlock.editCardCheckbox": "Aangevinkt selectievakje", + "ContentBlock.editCardCheckboxText": "kaarttekst bewerken", + "ContentBlock.editCardText": "kaarttekst bewerken", + "ContentBlock.editText": "Tekst bewerken...", + "ContentBlock.image": "afbeelding", "ContentBlock.insertAbove": "Hierboven invoegen", "ContentBlock.moveDown": "Naar beneden verplaatsen", "ContentBlock.moveUp": "Naar boven verplaatsen", + "ContentBlock.text": "tekst", + "Dialog.closeDialog": "Dialoogvenster sluiten", + "EmptyCenterPanel.no-content": "Voeg een bord toe of selecteer een bord in de zijbalk om te beginnen.", + "EmptyCenterPanel.workspace": "Dit is de werkruimte voor:", "Filter.includes": "bevat", "Filter.is-empty": "is leeg", "Filter.is-not-empty": "is niet leeg", "Filter.not-includes": "bevat niet", "FilterComponent.add-filter": "+ Filter toevoegen", "FilterComponent.delete": "Verwijderen", + "GalleryCard.delete": "Verwijderen", + "GalleryCard.duplicate": "Kopiëren", + "KanbanCard.delete": "Verwijderen", + "KanbanCard.duplicate": "Kopiëren", + "KanbanCard.untitled": "Titelloos", "Mutator.duplicate-board": "bord kopiëren", "Mutator.new-board-from-template": "nieuw bord van sjabloon", "Mutator.new-card-from-template": "nieuwe kaart van sjabloon", "Mutator.new-template-from-board": "nieuw sjabloon van bord", "Mutator.new-template-from-card": "nieuw sjabloon van kaart", + "PropertyMenu.Delete": "Verwijderen", "PropertyMenu.changeType": "Type eigenschap wijzigen", "PropertyMenu.typeTitle": "Type", - "PropertyType.Checkbox": "Checkbox", + "PropertyType.Checkbox": "Selectievakje", "PropertyType.CreatedBy": "Gemaakt door", "PropertyType.CreatedTime": "Created Time", "PropertyType.Email": "E-mail", @@ -93,6 +110,7 @@ "Sidebar.set-theme": "Thema instellen", "Sidebar.settings": "Instellingen", "Sidebar.spanish": "Spaans", + "Sidebar.system-theme": "Systeemthema", "Sidebar.template-from-board": "Nieuw sjabloon van bord", "Sidebar.turkish": "Turks", "Sidebar.untitled": "Titelloos", @@ -109,8 +127,14 @@ "TableHeaderMenu.sort-ascending": "Sorteer oplopend", "TableHeaderMenu.sort-descending": "Aflopend sorteren", "TableRow.open": "Openen", + "View.AddView": "Weergave toevoegen", + "View.Board": "Bord", + "View.DeleteView": "Weergave verwijderen", + "View.DuplicateView": "Weergave kopiëren", "View.NewBoardTitle": "Bordweergave", + "View.NewGalleryTitle": "Galerie bekijken", "View.NewTableTitle": "Tabelweergave", + "View.Table": "Tabel", "ViewHeader.add-template": "+ Nieuw sjabloon", "ViewHeader.delete-template": "Verwijderen", "ViewHeader.edit-template": "Bewerken", @@ -133,5 +157,6 @@ "ViewTitle.random-icon": "Willekeurig", "ViewTitle.remove-icon": "Verwijder pictogram", "ViewTitle.show-description": "beschrijving tonen", - "ViewTitle.untitled-board": "Titelloze bord" + "ViewTitle.untitled-board": "Titelloze bord", + "Workspace.editing-board-template": "Je bent een bordsjabloon aan het bewerken" } From 8da9da698cc9edad05f9597bdf59ebf7e32c4822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 7 Apr 2021 19:07:14 +0200 Subject: [PATCH 15/35] Fixing the max-width of the board header columns inputs --- webapp/src/components/kanban/kanban.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/src/components/kanban/kanban.scss b/webapp/src/components/kanban/kanban.scss index 89094b023..1eba8c7da 100644 --- a/webapp/src/components/kanban/kanban.scss +++ b/webapp/src/components/kanban/kanban.scss @@ -37,6 +37,7 @@ } .Label{ + max-width: 170px; .Editable { background: transparent; } From 3aecc91fb558ed81285313963d972980e738ae3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 7 Apr 2021 19:13:06 +0200 Subject: [PATCH 16/35] Fixing the buttons in the column headers in the kanban --- webapp/src/components/kanban/kanban.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/kanban/kanban.scss b/webapp/src/components/kanban/kanban.scss index 1eba8c7da..d60acd542 100644 --- a/webapp/src/components/kanban/kanban.scss +++ b/webapp/src/components/kanban/kanban.scss @@ -37,11 +37,18 @@ } .Label{ - max-width: 170px; + max-width: 165px; + margin-right: 5px; .Editable { background: transparent; } } + >.Button { + &.IconButton { + cursor: pointer; + } + cursor: auto; + } } From 13dbecc8234d99d81dd919c7450c7ddb7938c717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 7 Apr 2021 15:51:19 +0200 Subject: [PATCH 17/35] Migrating all drag and drop into react-dnd --- webapp/package-lock.json | 57 ++- webapp/package.json | 2 + webapp/src/app.tsx | 142 +++---- webapp/src/components/kanban/kanban.tsx | 81 ++-- webapp/src/components/kanban/kanbanCard.tsx | 68 ++-- webapp/src/components/kanban/kanbanColumn.tsx | 44 +- .../components/kanban/kanbanColumnHeader.tsx | 63 +-- .../kanban/kanbanHiddenColumnItem.tsx | 49 +-- .../src/components/table/horizontalGrip.tsx | 59 +-- webapp/src/components/table/table.tsx | 382 ++++++------------ webapp/src/components/table/tableHeader.tsx | 90 +++++ 11 files changed, 500 insertions(+), 537 deletions(-) create mode 100644 webapp/src/components/table/tableHeader.tsx diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 524ab6fca..ed9aca251 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -1646,6 +1646,21 @@ "fastq": "^1.6.0" } }, + "@react-dnd/asap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", + "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==" + }, + "@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, "@samverschueren/stream-to-observable": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", @@ -4331,6 +4346,16 @@ "path-type": "^4.0.0" } }, + "dnd-core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.0.tgz", + "integrity": "sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA==", + "requires": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.0.5" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -10185,6 +10210,26 @@ "object-assign": "^4.1.1" } }, + "react-dnd": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.2.tgz", + "integrity": "sha512-JoEL78sBCg8SzjOKMlkR70GWaPORudhWuTNqJ56lb2P8Vq0eM2+er3ZrMGiSDhOmzaRPuA9SNBz46nHCrjn11A==", + "requires": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "14.0.0", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, + "react-dnd-html5-backend": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.0.tgz", + "integrity": "sha512-2wAQqRFC1hbRGmk6+dKhOXsyQQOn3cN8PSZyOUeOun9J8t3tjZ7PS2+aFu7CVu2ujMDwTJR3VTwZh8pj2kCv7g==", + "requires": { + "dnd-core": "14.0.0" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -10428,6 +10473,15 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", @@ -11677,8 +11731,7 @@ "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "optional": true + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, "symbol-tree": { "version": "3.2.4", diff --git a/webapp/package.json b/webapp/package.json index f5489d348..710c1a131 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -25,6 +25,8 @@ "marked": ">=2.0.1", "nanoevents": "^5.1.13", "react": "^17.0.2", + "react-dnd": "^14.0.2", + "react-dnd-html5-backend": "^14.0.0", "react-dom": "^17.0.2", "react-hot-keys": "^2.6.2", "react-hotkeys-hook": "^3.3.0", diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 530a6d80c..ad17a9190 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -8,6 +8,8 @@ import { Route, Switch, } from 'react-router-dom' +import {DndProvider} from 'react-dnd' +import {HTML5Backend} from 'react-dnd-html5-backend' import {FlashMessages} from './components/flashMessages' import {getCurrentLanguage, getMessages, storeLanguage} from './i18n' @@ -51,78 +53,80 @@ export default class App extends React.PureComponent { locale={this.state.language} messages={getMessages(this.state.language)} > - - - -
-
- - - - - - - - - - - - - - - + + + +
+
+ + + + + + + + + + + + + + + + + + {this.state.initialLoad && !this.state.user && } + + + { + return ( + + ) + }} /> - - - {this.state.initialLoad && !this.state.user && } - { + if (this.state.initialLoad && !this.state.user) { + const redirectUrl = `/workspace/${match.params.workspaceId}/` + const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}` + return + } + return ( + + ) + }} /> - - { - return ( - - ) - }} - /> - { - if (this.state.initialLoad && !this.state.user) { - const redirectUrl = `/workspace/${match.params.workspaceId}/` - const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}` - return - } - return ( - - ) - }} - /> - - {this.state.initialLoad && !this.state.user && } - - - + + {this.state.initialLoad && !this.state.user && } + + + +
-
- - + + + ) } diff --git a/webapp/src/components/kanban/kanban.tsx b/webapp/src/components/kanban/kanban.tsx index ba4a01a6a..b1b6bcab1 100644 --- a/webapp/src/components/kanban/kanban.tsx +++ b/webapp/src/components/kanban/kanban.tsx @@ -78,9 +78,6 @@ class Kanban extends React.Component { readonly={this.props.readonly} propertyNameChanged={this.propertyNameChanged} onDropToColumn={this.onDropToColumn} - setDraggedHeaderOption={(draggedHeaderOption?: IPropertyOption) => { - this.setState({draggedHeaderOption}) - }} /> ))} @@ -120,8 +117,7 @@ class Kanban extends React.Component { {visibleGroups.map((group) => ( this.onDropToColumn(group.option)} + onDrop={(card: Card) => this.onDropToColumn(group.option, card)} > {group.cards.map((card) => ( { onClick={(e) => { this.props.onCardClicked(e, card) }} - onDragStart={() => { - if (this.props.selectedCardIds.includes(card.id)) { - this.setState({draggedCards: this.props.selectedCardIds.map((id) => boardTree.allCards.find((o) => o.id === id)!)}) - } else { - this.setState({draggedCards: [card]}) - } - }} - onDragEnd={() => { - this.setState({draggedCards: []}) - }} - - isDropZone={isManualSort} - onDrop={() => { - this.onDropToCard(card) - }} + onDrop={this.onDropToCard} /> ))} {!this.props.readonly && @@ -176,8 +158,7 @@ class Kanban extends React.Component { boardTree={boardTree} intl={this.props.intl} readonly={this.props.readonly} - onDropToColumn={this.onDropToColumn} - hasDraggedCards={this.state.draggedCards.length > 0} + onDrop={(card: Card) => this.onDropToColumn(group.option, card)} /> ))}
} @@ -206,14 +187,24 @@ class Kanban extends React.Component { await mutator.insertPropertyOption(boardTree, boardTree.groupByProperty!, option, 'add group') } - private onDropToColumn = async (option: IPropertyOption) => { - const {boardTree} = this.props - const {draggedCards, draggedHeaderOption} = this.state + private onDropToColumn = async (option: IPropertyOption, card?: Card, dstOption?: IPropertyOption) => { + const {boardTree, selectedCardIds} = this.props const optionId = option ? option.id : undefined + let draggedCardIds = selectedCardIds + if (card) { + draggedCardIds = Array.from(new Set(selectedCardIds).add(card.id)) + } + Utils.assertValue(boardTree) - if (draggedCards.length > 0) { + if (draggedCardIds.length > 0) { + const orderedCards = boardTree.orderedCards() + const cardsById: {[key: string]: Card} = orderedCards.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 description = draggedCards.length > 1 ? `drag ${draggedCards.length} cards` : 'drag card' const awaits = [] @@ -226,14 +217,14 @@ class Kanban extends React.Component { } await Promise.all(awaits) }) - } else if (draggedHeaderOption) { - Utils.log(`ondrop. Header option: ${draggedHeaderOption.value}, column: ${option?.value}`) + } 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 {activeView} = boardTree - const srcIndex = visibleOptionIds.indexOf(draggedHeaderOption.id) + const srcIndex = visibleOptionIds.indexOf(dstOption.id) const destIndex = visibleOptionIds.indexOf(option.id) visibleOptionIds.splice(destIndex, 0, visibleOptionIds.splice(srcIndex, 1)[0]) @@ -242,28 +233,30 @@ class Kanban extends React.Component { } } - private async onDropToCard(card: Card) { - Utils.log(`onDropToCard: ${card.title}`) - const {boardTree} = this.props + private onDropToCard = async (srcCard: Card, dstCard: Card) => { + Utils.log(`onDropToCard: ${dstCard.title}`) + const {boardTree, selectedCardIds} = this.props const {activeView} = boardTree - const {draggedCards} = this.state - const optionId = card.properties[activeView.groupById!] + const optionId = dstCard.properties[activeView.groupById!] - if (draggedCards.length < 1 || draggedCards.includes(card)) { - return - } + const draggedCardIds = Array.from(new Set(selectedCardIds).add(srcCard.id)) - const description = draggedCards.length > 1 ? `drag ${draggedCards.length} cards` : 'drag card' + const description = draggedCardIds.length > 1 ? `drag ${draggedCardIds.length} cards` : 'drag card' - // Update card order - let cardOrder = boardTree.orderedCards().map((o) => o.id) - const draggedCardIds = draggedCards.map((o) => o.id) + // Update dstCard order + const orderedCards = boardTree.orderedCards() + const cardsById: {[key: string]: Card} = orderedCards.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 firstDraggedCard = draggedCards[0] - const isDraggingDown = cardOrder.indexOf(firstDraggedCard.id) <= cardOrder.indexOf(card.id) + const isDraggingDown = cardOrder.indexOf(firstDraggedCard.id) <= cardOrder.indexOf(dstCard.id) cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id)) - let destIndex = cardOrder.indexOf(card.id) + let destIndex = cardOrder.indexOf(dstCard.id) if (firstDraggedCard.properties[boardTree.groupByProperty!.id] === optionId && isDraggingDown) { - // If the cards are in the same column and dragging down, drop after the target card + // If the cards are in the same column and dragging down, drop after the target dstCard destIndex += 1 } cardOrder.splice(destIndex, 0, ...draggedCardIds) diff --git a/webapp/src/components/kanban/kanbanCard.tsx b/webapp/src/components/kanban/kanbanCard.tsx index 60e95d797..ea51837e8 100644 --- a/webapp/src/components/kanban/kanbanCard.tsx +++ b/webapp/src/components/kanban/kanbanCard.tsx @@ -1,7 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useState} from 'react' +import React, {useRef} from 'react' import {injectIntl, IntlShape} from 'react-intl' +import {useDrag, useDrop} from 'react-dnd' import {IPropertyTemplate} from '../../blocks/board' import {Card} from '../../blocks/card' @@ -20,60 +21,47 @@ type Props = { card: Card visiblePropertyTemplates: IPropertyTemplate[] isSelected: boolean - isDropZone?: boolean onClick?: (e: React.MouseEvent) => void - onDragStart: (e: React.DragEvent) => void - onDragEnd: (e: React.DragEvent) => void - onDrop: (e: React.DragEvent) => void intl: IntlShape readonly: boolean + onDrop: (srcCard: Card, dstCard: Card) => void } -const KanbanCard = (props: Props) => { - const [isDragged, setIsDragged] = useState(false) - const [isDragOver, setIsDragOver] = useState(false) - +const KanbanCard = React.memo((props: Props) => { + const cardRef = useRef(null) const {card, intl} = props + const [{isDragging}, drag] = useDrag(() => ({ + type: 'card', + item: card, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + })) + const [{isOver}, drop] = useDrop(() => ({ + accept: 'card', + collect: (monitor) => ({ + isOver: monitor.isOver(), + }), + drop: (item: Card) => { + props.onDrop(item, card) + }, + })) + const visiblePropertyTemplates = props.visiblePropertyTemplates || [] let className = props.isSelected ? 'KanbanCard selected' : 'KanbanCard' - if (props.isDropZone && isDragOver) { + if (isOver) { className += ' dragover' } + drop(drag(cardRef)) + return (
null : cardRef} className={className} draggable={!props.readonly} - style={{opacity: isDragged ? 0.5 : 1}} + style={{opacity: isDragging ? 0.5 : 1}} onClick={props.onClick} - onDragStart={(e) => { - setIsDragged(true) - props.onDragStart(e) - }} - onDragEnd={(e) => { - setIsDragged(false) - props.onDragEnd(e) - }} - - onDragOver={() => { - if (!isDragOver) { - setIsDragOver(true) - } - }} - onDragEnter={() => { - if (!isDragOver) { - setIsDragOver(true) - } - }} - onDragLeave={() => { - setIsDragOver(false) - }} - onDrop={(e) => { - setIsDragOver(false) - if (props.isDropZone) { - props.onDrop(e) - } - }} > {!props.readonly && { ))}
) -} +}) export default injectIntl(KanbanCard) diff --git a/webapp/src/components/kanban/kanbanColumn.tsx b/webapp/src/components/kanban/kanbanColumn.tsx index a6ba7c311..448ef097e 100644 --- a/webapp/src/components/kanban/kanbanColumn.tsx +++ b/webapp/src/components/kanban/kanbanColumn.tsx @@ -1,45 +1,35 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useState} from 'react' +import React from 'react' +import {useDrop} from 'react-dnd' +import {Card} from '../../blocks/card' type Props = { - onDrop: (e: React.DragEvent) => void - isDropZone: boolean + onDrop: (card: Card) => void children: React.ReactNode } const KanbanColumn = React.memo((props: Props) => { - const [isDragOver, setIsDragOver] = useState(false) + const [{isOver}, drop] = useDrop(() => ({ + accept: 'card', + collect: (monitor) => ({ + isOver: monitor.isOver({shallow: true}), + }), + drop: (item: Card, monitor) => { + if (monitor.isOver({shallow: true})) { + props.onDrop(item) + } + }, + })) let className = 'octo-board-column' - if (props.isDropZone && isDragOver) { + if (isOver) { className += ' dragover' } return (
{ - e.preventDefault() - if (!isDragOver) { - setIsDragOver(true) - } - }} - onDragEnter={(e) => { - e.preventDefault() - if (!isDragOver) { - setIsDragOver(true) - } - }} - onDragLeave={(e) => { - e.preventDefault() - setIsDragOver(false) - }} - onDrop={(e) => { - setIsDragOver(false) - if (props.isDropZone) { - props.onDrop(e) - } - }} > {props.children}
diff --git a/webapp/src/components/kanban/kanbanColumnHeader.tsx b/webapp/src/components/kanban/kanbanColumnHeader.tsx index 11354434b..bb8841ba8 100644 --- a/webapp/src/components/kanban/kanbanColumnHeader.tsx +++ b/webapp/src/components/kanban/kanbanColumnHeader.tsx @@ -1,11 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. /* eslint-disable max-lines */ -import React, {useState, useEffect} from 'react' +import React, {useState, useEffect, useRef} from 'react' import {FormattedMessage, IntlShape} from 'react-intl' +import {useDrop, useDrag} from 'react-dnd' import {Constants} from '../../constants' import {IPropertyOption} from '../../blocks/board' +import {Card} from '../../blocks/card' import mutator from '../../mutator' import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree' import Button from '../../widgets/buttons/button' @@ -26,8 +28,7 @@ type Props = { readonly: boolean addCard: (groupByOptionId?: string) => Promise propertyNameChanged: (option: IPropertyOption, text: string) => Promise - onDropToColumn: (option: IPropertyOption) => void - setDraggedHeaderOption: (draggedHeaderOption?: IPropertyOption) => void + onDropToColumn: (srcOption: IPropertyOption, card?: Card, dstOption?: IPropertyOption) => void } export default function KanbanColumnHeader(props: Props): JSX.Element { @@ -35,42 +36,42 @@ export default function KanbanColumnHeader(props: Props): JSX.Element { const {activeView} = boardTree const [groupTitle, setGroupTitle] = useState(group.option.value) + const headerRef = useRef(null) + + const [{isDragging}, drag] = useDrag(() => ({ + type: 'column', + item: group.option, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + })) + const [{isOver}, drop] = useDrop(() => ({ + accept: 'column', + collect: (monitor) => ({ + isOver: monitor.isOver(), + }), + drop: (item: IPropertyOption) => { + props.onDropToColumn(item, undefined, group.option) + }, + })) + useEffect(() => { setGroupTitle(group.option.value) }, [group.option.value]) - const ref = React.createRef() + drop(drag(headerRef)) + let className = 'octo-board-header-cell KanbanColumnHeader' + if (isOver) { + className += ' dragover' + } + return (
{ - props.setDraggedHeaderOption(group.option) - }} - onDragEnd={() => { - props.setDraggedHeaderOption(undefined) - }} - - onDragOver={(e) => { - ref.current?.classList.add('dragover') - e.preventDefault() - }} - onDragEnter={(e) => { - ref.current?.classList.add('dragover') - e.preventDefault() - }} - onDragLeave={(e) => { - ref.current?.classList.remove('dragover') - e.preventDefault() - }} - onDrop={(e) => { - ref.current?.classList.remove('dragover') - e.preventDefault() - props.onDropToColumn(group.option) - }} > {!group.option.id &&