mirror of
https://github.com/mattermost/focalboard.git
synced 2025-02-19 19:59:59 +02:00
Merge branch 'main' into feat/add_url_prop_frontend
This commit is contained in:
commit
1640923001
@ -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()
|
||||
|
@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
80D6DEBC252E13CB00AEED9E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
@ -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()
|
||||
|
@ -578,6 +578,13 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/>
|
||||
<menuItem title="Diagnostics Info" id="oQB-FY-dcB">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="showDiagnosticsInfo:" target="Ady-hI-5gd" id="hkZ-Xn-3T1"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="dRF-4R-egy"/>
|
||||
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
|
52
mac/Focalboard/PortUtils.swift
Normal file
52
mac/Focalboard/PortUtils.swift
Normal file
@ -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<sockaddr_in>.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)
|
||||
}
|
||||
}
|
@ -45,6 +45,17 @@ class ViewController:
|
||||
WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes as! Set<String>, 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) {
|
||||
|
@ -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")
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -158,5 +158,6 @@
|
||||
"ViewTitle.remove-icon": "Remove icon",
|
||||
"ViewTitle.show-description": "show description",
|
||||
"ViewTitle.untitled-board": "Untitled board",
|
||||
"Workspace.editing-board-template": "You're editing a board template"
|
||||
"Workspace.editing-board-template": "You're editing a board template",
|
||||
"default-properties.title": "Title"
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
{
|
||||
"BoardCard.delete": "削除",
|
||||
"BoardCard.duplicate": "複製",
|
||||
"BoardCard.untitled": "Untitled",
|
||||
"BoardComponent.add-a-group": "+ グループを追加する",
|
||||
"BoardComponent.delete": "削除",
|
||||
"BoardComponent.hidden-columns": "非表示",
|
||||
@ -70,9 +67,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 +83,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 +95,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": "+ 新規",
|
||||
@ -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"
|
||||
}
|
||||
|
@ -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",
|
||||
@ -23,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",
|
||||
@ -96,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",
|
||||
@ -112,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",
|
||||
@ -137,5 +158,5 @@
|
||||
"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"
|
||||
"Workspace.editing-board-template": "Je bent een bordsjabloon aan het bewerken"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
{
|
||||
"BoardCard.delete": "Удалить",
|
||||
"BoardCard.duplicate": "Дубликат",
|
||||
"BoardCard.untitled": "Без названия",
|
||||
"BoardComponent.add-a-group": "+ Добавить группу",
|
||||
"BoardComponent.delete": "Удалить",
|
||||
"BoardComponent.hidden-columns": "Скрытые колонки",
|
||||
@ -23,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": "Флажок",
|
||||
@ -70,6 +84,7 @@
|
||||
"Sidebar.add-board": "+ Добавить доску",
|
||||
"Sidebar.add-template": "+ Новый шаблон",
|
||||
"Sidebar.changePassword": "Изменить пароль",
|
||||
"Sidebar.chinese": "Китайский",
|
||||
"Sidebar.dark-theme": "Тёмная тема",
|
||||
"Sidebar.default-theme": "Тема по умолчанию",
|
||||
"Sidebar.delete-board": "Удалить доску",
|
||||
@ -88,13 +103,16 @@
|
||||
"Sidebar.light-theme": "Светлая тема",
|
||||
"Sidebar.logout": "Выйти",
|
||||
"Sidebar.no-views-in-board": "Внутри нет страниц",
|
||||
"Sidebar.occitan": "Окситанский",
|
||||
"Sidebar.russian": "Русский",
|
||||
"Sidebar.select-a-template": "Выберите шаблон",
|
||||
"Sidebar.set-language": "Язык",
|
||||
"Sidebar.set-theme": "Тема",
|
||||
"Sidebar.settings": "Настройки",
|
||||
"Sidebar.spanish": "Испанский",
|
||||
"Sidebar.system-theme": "Системная тема",
|
||||
"Sidebar.template-from-board": "Новый шаблон из доски",
|
||||
"Sidebar.turkish": "Турецкий",
|
||||
"Sidebar.untitled": "Без названия",
|
||||
"Sidebar.untitled-board": "(Доска без названия)",
|
||||
"Sidebar.untitled-view": "(Вид без названия)",
|
||||
@ -109,13 +127,21 @@
|
||||
"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": "Редактировать",
|
||||
"ViewHeader.empty-card": "Очистить карточку",
|
||||
"ViewHeader.export-complete": "Экспорт завершен!",
|
||||
"ViewHeader.export-csv": "Экспорт в CSV",
|
||||
"ViewHeader.export-failed": "Ошибка экспорта!",
|
||||
"ViewHeader.filter": "Фильтр",
|
||||
"ViewHeader.group-by": "Сгруппировать по: {property}",
|
||||
"ViewHeader.new": "Создать",
|
||||
@ -132,5 +158,5 @@
|
||||
"ViewTitle.remove-icon": "Убрать иконку",
|
||||
"ViewTitle.show-description": "показать описание",
|
||||
"ViewTitle.untitled-board": "Доска без названия",
|
||||
"WorkspaceComponent.editing-board-template": "Вы редактируете шаблон доски"
|
||||
"Workspace.editing-board-template": "Вы редактируете шаблон доски"
|
||||
}
|
||||
|
@ -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",
|
||||
@ -23,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",
|
||||
@ -96,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",
|
||||
@ -113,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",
|
||||
@ -137,5 +147,5 @@
|
||||
"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"
|
||||
"Workspace.editing-board-template": "Bir tahta kalıbını düzenliyorsunuz"
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
{
|
||||
"BoardCard.delete": "删除",
|
||||
"BoardCard.duplicate": "制作副本",
|
||||
"BoardCard.untitled": "无标题",
|
||||
"BoardComponent.add-a-group": "+ 新增群组",
|
||||
"BoardComponent.delete": "删除",
|
||||
"BoardComponent.hidden-columns": "隐藏列",
|
||||
@ -23,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": "使用范本新增卡片",
|
||||
@ -96,6 +102,7 @@
|
||||
"Sidebar.set-theme": "设定佈景主题",
|
||||
"Sidebar.settings": "设定",
|
||||
"Sidebar.spanish": "西班牙文",
|
||||
"Sidebar.system-theme": "系统主题风格",
|
||||
"Sidebar.template-from-board": "从版面新增范本",
|
||||
"Sidebar.turkish": "土耳其语",
|
||||
"Sidebar.untitled": "无标题",
|
||||
@ -137,5 +144,5 @@
|
||||
"ViewTitle.remove-icon": "移除图标",
|
||||
"ViewTitle.show-description": "显示叙述",
|
||||
"ViewTitle.untitled-board": "无标题版面",
|
||||
"WorkspaceComponent.editing-board-template": "您正在编辑版面范本"
|
||||
"Workspace.editing-board-template": "您正在编辑板模板"
|
||||
}
|
||||
|
@ -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": "無標題版面"
|
||||
}
|
||||
|
66
webapp/package-lock.json
generated
66
webapp/package-lock.json
generated
@ -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,35 @@
|
||||
"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-dnd-touch-backend": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-14.0.0.tgz",
|
||||
"integrity": "sha512-fNt3isf9h0xgjj86dIXhBi3dJ7OhC88vKUYdEvsOGypdp3LOFD+TobBAuBu00v26WmJ6II6xqbkhOx+KOcyHxQ==",
|
||||
"requires": {
|
||||
"@react-dnd/invariant": "^2.0.0",
|
||||
"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 +10482,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 +11740,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",
|
||||
|
@ -25,6 +25,9 @@
|
||||
"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-dnd-touch-backend": "^14.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hot-keys": "^2.6.2",
|
||||
"react-hotkeys-hook": "^3.3.0",
|
||||
|
@ -8,6 +8,9 @@ import {
|
||||
Route,
|
||||
Switch,
|
||||
} from 'react-router-dom'
|
||||
import {DndProvider} from 'react-dnd'
|
||||
import {HTML5Backend} from 'react-dnd-html5-backend'
|
||||
import {TouchBackend} from 'react-dnd-touch-backend'
|
||||
|
||||
import {FlashMessages} from './components/flashMessages'
|
||||
import {getCurrentLanguage, getMessages, storeLanguage} from './i18n'
|
||||
@ -18,6 +21,7 @@ import ErrorPage from './pages/errorPage'
|
||||
import LoginPage from './pages/loginPage'
|
||||
import RegisterPage from './pages/registerPage'
|
||||
import {IUser, UserContext} from './user'
|
||||
import {Utils} from './utils'
|
||||
|
||||
type State = {
|
||||
language: string,
|
||||
@ -51,78 +55,80 @@ export default class App extends React.PureComponent<unknown, State> {
|
||||
locale={this.state.language}
|
||||
messages={getMessages(this.state.language)}
|
||||
>
|
||||
<UserContext.Provider value={this.state.user}>
|
||||
<FlashMessages milliseconds={2000}/>
|
||||
<Router forceRefresh={true}>
|
||||
<div id='frame'>
|
||||
<div id='main'>
|
||||
<Switch>
|
||||
<Route path='/error'>
|
||||
<ErrorPage/>
|
||||
</Route>
|
||||
<Route path='/login'>
|
||||
<LoginPage/>
|
||||
</Route>
|
||||
<Route path='/register'>
|
||||
<RegisterPage/>
|
||||
</Route>
|
||||
<Route path='/change_password'>
|
||||
<ChangePasswordPage/>
|
||||
</Route>
|
||||
<Route path='/shared'>
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
readonly={true}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
<DndProvider backend={Utils.isMobile() ? TouchBackend : HTML5Backend}>
|
||||
<UserContext.Provider value={this.state.user}>
|
||||
<FlashMessages milliseconds={2000}/>
|
||||
<Router forceRefresh={true}>
|
||||
<div id='frame'>
|
||||
<div id='main'>
|
||||
<Switch>
|
||||
<Route path='/error'>
|
||||
<ErrorPage/>
|
||||
</Route>
|
||||
<Route path='/login'>
|
||||
<LoginPage/>
|
||||
</Route>
|
||||
<Route path='/register'>
|
||||
<RegisterPage/>
|
||||
</Route>
|
||||
<Route path='/change_password'>
|
||||
<ChangePasswordPage/>
|
||||
</Route>
|
||||
<Route path='/shared'>
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
readonly={true}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
</Route>
|
||||
<Route path='/board'>
|
||||
{this.state.initialLoad && !this.state.user && <Redirect to='/login'/>}
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path='/workspace/:workspaceId/shared'
|
||||
render={({match}) => {
|
||||
return (
|
||||
<BoardPage
|
||||
workspaceId={match.params.workspaceId}
|
||||
readonly={true}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route path='/board'>
|
||||
{this.state.initialLoad && !this.state.user && <Redirect to='/login'/>}
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
<Route
|
||||
path='/workspace/:workspaceId/'
|
||||
render={({match}) => {
|
||||
if (this.state.initialLoad && !this.state.user) {
|
||||
const redirectUrl = `/workspace/${match.params.workspaceId}/`
|
||||
const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}`
|
||||
return <Redirect to={loginUrl}/>
|
||||
}
|
||||
return (
|
||||
<BoardPage
|
||||
workspaceId={match.params.workspaceId}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path='/workspace/:workspaceId/shared'
|
||||
render={({match}) => {
|
||||
return (
|
||||
<BoardPage
|
||||
workspaceId={match.params.workspaceId}
|
||||
readonly={true}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path='/workspace/:workspaceId/'
|
||||
render={({match}) => {
|
||||
if (this.state.initialLoad && !this.state.user) {
|
||||
const redirectUrl = `/workspace/${match.params.workspaceId}/`
|
||||
const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}`
|
||||
return <Redirect to={loginUrl}/>
|
||||
}
|
||||
return (
|
||||
<BoardPage
|
||||
workspaceId={match.params.workspaceId}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/'>
|
||||
{this.state.initialLoad && !this.state.user && <Redirect to='/login'/>}
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Route path='/'>
|
||||
{this.state.initialLoad && !this.state.user && <Redirect to='/login'/>}
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
</UserContext.Provider>
|
||||
</Router>
|
||||
</UserContext.Provider>
|
||||
</DndProvider>
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
import React from 'react'
|
||||
import {injectIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {IContentBlock} from '../../blocks/contentBlock'
|
||||
import {MutableTextBlock} from '../../blocks/textBlock'
|
||||
import mutator from '../../mutator'
|
||||
import {CardTree} from '../../viewModel/cardTree'
|
||||
@ -32,6 +33,22 @@ function addTextBlock(card: Card, intl: IntlShape, text: string): void {
|
||||
})
|
||||
}
|
||||
|
||||
function moveBlock(card: Card, srcBlock: IContentBlock, dstBlock: IContentBlock, intl: IntlShape): void {
|
||||
let contentOrder = card.contentOrder.slice()
|
||||
const isDraggingDown = contentOrder.indexOf(srcBlock.id) <= contentOrder.indexOf(dstBlock.id)
|
||||
contentOrder = contentOrder.filter((id) => srcBlock.id !== id)
|
||||
let destIndex = contentOrder.indexOf(dstBlock.id)
|
||||
if (isDraggingDown) {
|
||||
destIndex += 1
|
||||
}
|
||||
contentOrder.splice(destIndex, 0, srcBlock.id)
|
||||
|
||||
mutator.performAsUndoGroup(async () => {
|
||||
const description = intl.formatMessage({id: 'CardDetail.moveContent', defaultMessage: 'move card content'})
|
||||
await mutator.changeCardContentOrder(card, contentOrder, description)
|
||||
})
|
||||
}
|
||||
|
||||
const CardDetailContents = React.memo((props: Props) => {
|
||||
const {cardTree} = props
|
||||
if (!cardTree) {
|
||||
@ -49,6 +66,7 @@ const CardDetailContents = React.memo((props: Props) => {
|
||||
card={card}
|
||||
contents={cardTree.contents}
|
||||
readonly={props.readonly}
|
||||
onDrop={(src, dst) => moveBlock(card, src, dst, props.intl)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -58,6 +58,7 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
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()
|
||||
|
@ -13,4 +13,7 @@
|
||||
> .octo-block-margin {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.ImageElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import SortDownIcon from '../widgets/icons/sortDown'
|
||||
import SortUpIcon from '../widgets/icons/sortUp'
|
||||
import Menu from '../widgets/menu'
|
||||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
import useSortable from '../hooks/sortable'
|
||||
|
||||
import ContentElement from './content/contentElement'
|
||||
import AddContentMenuItem from './addContentMenuItem'
|
||||
@ -28,14 +29,24 @@ type Props = {
|
||||
contents: readonly IContentBlock[]
|
||||
readonly: boolean
|
||||
intl: IntlShape
|
||||
onDrop: (srctBlock: IContentBlock, dstBlock: IContentBlock) => void
|
||||
}
|
||||
|
||||
const ContentBlock = React.memo((props: Props): JSX.Element => {
|
||||
const {intl, card, contents, block, readonly} = props
|
||||
const [isDragging, isOver, contentRef] = useSortable('content', block, true, props.onDrop)
|
||||
|
||||
const index = contents.indexOf(block)
|
||||
let className = 'ContentBlock octo-block'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
return (
|
||||
<div className='ContentBlock octo-block'>
|
||||
<div
|
||||
className={className}
|
||||
style={{opacity: isDragging ? 0.5 : 1}}
|
||||
ref={contentRef}
|
||||
>
|
||||
<div className='octo-block-margin'>
|
||||
{!props.readonly &&
|
||||
<MenuWrapper>
|
||||
|
@ -8,7 +8,7 @@
|
||||
color: rgba(var(--body-color), 0.3);
|
||||
cursor: pointer;
|
||||
width: 280px;
|
||||
min-height: 200px;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -3,7 +3,10 @@
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {Constants} from '../../constants'
|
||||
import {Card} from '../../blocks/card'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import {CardTree, MutableCardTree} from '../../viewModel/cardTree'
|
||||
import useCardListener from '../../hooks/cardListener'
|
||||
@ -21,9 +24,34 @@ type Props = {
|
||||
|
||||
const Gallery = (props: Props): JSX.Element => {
|
||||
const {boardTree} = props
|
||||
const {cards} = boardTree
|
||||
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 < 1
|
||||
|
||||
const onDropToCard = (srcCard: Card, dstCard: Card) => {
|
||||
Utils.log(`onDropToCard: ${dstCard.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'
|
||||
|
||||
// Update dstCard order
|
||||
let cardOrder = Array.from(new Set([...activeView.cardOrder, ...boardTree.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 (isDraggingDown) {
|
||||
destIndex += 1
|
||||
}
|
||||
cardOrder.splice(destIndex, 0, ...draggedCardIds)
|
||||
|
||||
mutator.performAsUndoGroup(async () => {
|
||||
await mutator.changeViewCardOrder(activeView, cardOrder, description)
|
||||
})
|
||||
}
|
||||
|
||||
const visibleTitle = boardTree.activeView.visiblePropertyIds.includes(Constants.titleColumnId)
|
||||
|
||||
useCardListener(
|
||||
cards.map((c) => c.id),
|
||||
@ -60,8 +88,11 @@ const Gallery = (props: Props): JSX.Element => {
|
||||
cardTree={cardTree}
|
||||
onClick={props.onCardClicked}
|
||||
visiblePropertyTemplates={visiblePropertyTemplates}
|
||||
visibleTitle={visibleTitle}
|
||||
isSelected={props.selectedCardIds.includes(card.id)}
|
||||
readonly={props.readonly}
|
||||
onDrop={onDropToCard}
|
||||
isManualSort={isManualSort}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -56,6 +56,7 @@
|
||||
overflow: hidden;
|
||||
max-height: 160px;
|
||||
min-height: 160px;
|
||||
pointer-events: none;
|
||||
|
||||
.ImageElement {
|
||||
width: 100%;
|
||||
|
@ -15,6 +15,7 @@ import DuplicateIcon from '../../widgets/icons/duplicate'
|
||||
import OptionsIcon from '../../widgets/icons/options'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import useSortable from '../../hooks/sortable'
|
||||
|
||||
import ImageElement from '../content/imageElement'
|
||||
import ContentElement from '../content/contentElement'
|
||||
@ -26,23 +27,33 @@ type Props = {
|
||||
cardTree: CardTree
|
||||
onClick: (e: React.MouseEvent, card: Card) => void
|
||||
visiblePropertyTemplates: IPropertyTemplate[]
|
||||
visibleTitle: boolean
|
||||
isSelected: boolean
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
isManualSort: boolean
|
||||
onDrop: (srcCard: Card, dstCard: Card) => void
|
||||
}
|
||||
|
||||
const GalleryCard = React.memo((props: Props) => {
|
||||
const {cardTree} = props
|
||||
const [isDragging, isOver, cardRef] = useSortable('card', cardTree.card, props.isManualSort, props.onDrop)
|
||||
|
||||
const visiblePropertyTemplates = props.visiblePropertyTemplates || []
|
||||
|
||||
let images: IContentBlock[] = []
|
||||
images = cardTree.contents.filter((content) => content.type === 'image')
|
||||
let className = props.isSelected ? 'GalleryCard selected' : 'GalleryCard'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`GalleryCard ${props.isSelected ? 'selected' : ''}`}
|
||||
className={className}
|
||||
onClick={(e: React.MouseEvent) => props.onClick(e, cardTree.card)}
|
||||
style={{opacity: isDragging ? 0.5 : 1}}
|
||||
ref={cardRef}
|
||||
>
|
||||
{!props.readonly &&
|
||||
<MenuWrapper
|
||||
@ -83,16 +94,17 @@ const GalleryCard = React.memo((props: Props) => {
|
||||
/>
|
||||
))}
|
||||
</div>}
|
||||
<div className='gallery-title'>
|
||||
{ cardTree.card.icon ? <div className='octo-icon'>{cardTree.card.icon}</div> : undefined }
|
||||
<div key='__title'>
|
||||
{cardTree.card.title ||
|
||||
<FormattedMessage
|
||||
id='KanbanCard.untitled'
|
||||
defaultMessage='Untitled'
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
{props.visibleTitle &&
|
||||
<div className='gallery-title'>
|
||||
{ cardTree.card.icon ? <div className='octo-icon'>{cardTree.card.icon}</div> : undefined }
|
||||
<div key='__title'>
|
||||
{cardTree.card.title ||
|
||||
<FormattedMessage
|
||||
id='KanbanCard.untitled'
|
||||
defaultMessage='Untitled'
|
||||
/>}
|
||||
</div>
|
||||
</div>}
|
||||
{visiblePropertyTemplates.length > 0 &&
|
||||
<div className='gallery-props'>
|
||||
{visiblePropertyTemplates.map((template) => (
|
||||
|
@ -37,10 +37,18 @@
|
||||
}
|
||||
|
||||
.Label{
|
||||
max-width: 165px;
|
||||
margin-right: 5px;
|
||||
.Editable {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
>.Button {
|
||||
&.IconButton {
|
||||
cursor: pointer;
|
||||
}
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -78,9 +78,6 @@ class Kanban extends React.Component<Props, State> {
|
||||
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<Props, State> {
|
||||
{visibleGroups.map((group) => (
|
||||
<KanbanColumn
|
||||
key={group.option.id || 'empty'}
|
||||
isDropZone={!isManualSort || group.cards.length < 1}
|
||||
onDrop={() => this.onDropToColumn(group.option)}
|
||||
onDrop={(card: Card) => this.onDropToColumn(group.option, card)}
|
||||
>
|
||||
{group.cards.map((card) => (
|
||||
<KanbanCard
|
||||
@ -133,21 +129,8 @@ class Kanban extends React.Component<Props, State> {
|
||||
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}
|
||||
isManualSort={isManualSort}
|
||||
/>
|
||||
))}
|
||||
{!this.props.readonly &&
|
||||
@ -176,8 +159,7 @@ class Kanban extends React.Component<Props, State> {
|
||||
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)}
|
||||
/>
|
||||
))}
|
||||
</div>}
|
||||
@ -206,14 +188,24 @@ class Kanban extends React.Component<Props, State> {
|
||||
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 +218,14 @@ class Kanban extends React.Component<Props, State> {
|
||||
}
|
||||
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 +234,29 @@ class Kanban extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
const firstDraggedCard = draggedCards[0]
|
||||
const isDraggingDown = cardOrder.indexOf(firstDraggedCard.id) <= cardOrder.indexOf(card.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 isDraggingDown = cardOrder.indexOf(srcCard.id) <= cardOrder.indexOf(dstCard.id)
|
||||
cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id))
|
||||
let destIndex = cardOrder.indexOf(card.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
|
||||
let destIndex = cardOrder.indexOf(dstCard.id)
|
||||
if (srcCard.properties[boardTree.groupByProperty!.id] === optionId && isDraggingDown) {
|
||||
// If the cards are in the same column and dragging down, drop after the target dstCard
|
||||
destIndex += 1
|
||||
}
|
||||
cardOrder.splice(destIndex, 0, ...draggedCardIds)
|
||||
|
@ -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 from 'react'
|
||||
import {injectIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
@ -12,6 +12,7 @@ import DuplicateIcon from '../../widgets/icons/duplicate'
|
||||
import OptionsIcon from '../../widgets/icons/options'
|
||||
import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import useSortable from '../../hooks/sortable'
|
||||
|
||||
import './kanbanCard.scss'
|
||||
import PropertyValueElement from '../propertyValueElement'
|
||||
@ -20,60 +21,29 @@ type Props = {
|
||||
card: Card
|
||||
visiblePropertyTemplates: IPropertyTemplate[]
|
||||
isSelected: boolean
|
||||
isDropZone?: boolean
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
onDragStart: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
onDragEnd: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
onDrop: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
onDrop: (srcCard: Card, dstCard: Card) => void
|
||||
isManualSort: boolean
|
||||
}
|
||||
|
||||
const KanbanCard = (props: Props) => {
|
||||
const [isDragged, setIsDragged] = useState(false)
|
||||
const [isDragOver, setIsDragOver] = useState(false)
|
||||
|
||||
const KanbanCard = React.memo((props: Props) => {
|
||||
const {card, intl} = props
|
||||
const [isDragging, isOver, cardRef] = useSortable('card', card, props.isManualSort, props.onDrop)
|
||||
const visiblePropertyTemplates = props.visiblePropertyTemplates || []
|
||||
let className = props.isSelected ? 'KanbanCard selected' : 'KanbanCard'
|
||||
if (props.isDropZone && isDragOver) {
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={props.readonly ? () => 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 &&
|
||||
<MenuWrapper
|
||||
@ -115,6 +85,6 @@ const KanbanCard = (props: Props) => {
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default injectIntl(KanbanCard)
|
||||
|
@ -1,45 +1,36 @@
|
||||
// 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<HTMLDivElement>) => 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 (
|
||||
<div
|
||||
ref={drop}
|
||||
className={className}
|
||||
onDragOver={(e) => {
|
||||
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}
|
||||
</div>
|
||||
|
@ -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<void>
|
||||
propertyNameChanged: (option: IPropertyOption, text: string) => Promise<void>
|
||||
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<HTMLDivElement>(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<HTMLDivElement>()
|
||||
drop(drag(headerRef))
|
||||
let className = 'octo-board-header-cell KanbanColumnHeader'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={group.option.id || 'empty'}
|
||||
ref={ref}
|
||||
className='octo-board-header-cell KanbanColumnHeader'
|
||||
|
||||
ref={headerRef}
|
||||
style={{opacity: isDragging ? 0.5 : 1}}
|
||||
className={className}
|
||||
draggable={!props.readonly}
|
||||
onDragStart={() => {
|
||||
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 &&
|
||||
<Label
|
||||
|
@ -1,10 +1,10 @@
|
||||
// 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 from 'react'
|
||||
import {IntlShape} from 'react-intl'
|
||||
import {useDrop} from 'react-dnd'
|
||||
|
||||
import {IPropertyOption} from '../../blocks/board'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
@ -12,53 +12,39 @@ 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'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
group: BoardTreeGroup
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
onDropToColumn: (option: IPropertyOption) => void
|
||||
hasDraggedCards: boolean
|
||||
onDrop: (card: Card) => void
|
||||
}
|
||||
|
||||
export default function KanbanHiddenColumnItem(props: Props): JSX.Element {
|
||||
const {boardTree, intl, group} = props
|
||||
const {activeView} = boardTree
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: 'card',
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
drop: (item: Card) => {
|
||||
props.onDrop(item)
|
||||
},
|
||||
}))
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [dragClass, setDragClass] = useState('')
|
||||
let className = 'octo-board-hidden-item'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
ref={drop}
|
||||
key={group.option.id || 'empty'}
|
||||
className={`octo-board-hidden-item ${dragClass}`}
|
||||
onDragOver={(e) => {
|
||||
if (props.hasDraggedCards) {
|
||||
setDragClass('dragover')
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
if (props.hasDraggedCards) {
|
||||
setDragClass('dragover')
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
if (props.hasDraggedCards) {
|
||||
setDragClass('')
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
setDragClass('')
|
||||
e.preventDefault()
|
||||
if (props.hasDraggedCards) {
|
||||
props.onDropToColumn(group.option)
|
||||
}
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<MenuWrapper
|
||||
disabled={props.readonly}
|
||||
|
@ -29,6 +29,24 @@ const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
const displayValue = OctoUtils.propertyDisplayValue(card, propertyValue, propertyTemplate)
|
||||
const finalDisplayValue = displayValue || emptyDisplayValue
|
||||
|
||||
const validateProp = (propType: string, val: string): boolean => {
|
||||
if (val === '') {
|
||||
return true
|
||||
}
|
||||
switch (propType) {
|
||||
case 'number':
|
||||
return !isNaN(parseInt(val, 10))
|
||||
case 'email': {
|
||||
const emailRegexp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
return emailRegexp.test(val.toLowerCase())
|
||||
}
|
||||
case 'text':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (propertyTemplate.type === 'select') {
|
||||
let propertyColorCssClassName = ''
|
||||
const cardPropertyValue = propertyTemplate.options.find((o) => o.id === propertyValue)
|
||||
@ -90,6 +108,7 @@ const PropertyValueElement = (props:Props): JSX.Element => {
|
||||
onChange={setValue}
|
||||
onSave={() => mutator.changePropertyValue(card, propertyTemplate.id, value)}
|
||||
onCancel={() => setValue(propertyValue)}
|
||||
validator={(newValue) => validateProp(propertyTemplate.type, newValue)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -47,7 +47,7 @@
|
||||
.octo-sidebar-list {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
max-width: 250px;
|
||||
width: 250px;
|
||||
|
||||
>div {
|
||||
margin-bottom: 16px;
|
||||
|
@ -1,57 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {useDrag} from 'react-dnd'
|
||||
|
||||
import './horizontalGrip.scss'
|
||||
|
||||
type Props = {
|
||||
onDrag: (offset: number) => void
|
||||
onDragEnd: (offset: number) => void
|
||||
templateId: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
isDragging: boolean
|
||||
startX: number
|
||||
offset: number
|
||||
}
|
||||
const HorizontalGrip = React.memo((props: Props): JSX.Element => {
|
||||
const [, drag] = useDrag(() => ({
|
||||
type: 'horizontalGrip',
|
||||
item: {id: props.templateId},
|
||||
}))
|
||||
|
||||
class HorizontalGrip extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
offset: 0,
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className='HorizontalGrip'
|
||||
onMouseDown={(e) => {
|
||||
this.setState({isDragging: true, startX: e.clientX, offset: 0})
|
||||
window.addEventListener('mousemove', this.globalMouseMove)
|
||||
window.addEventListener('mouseup', this.globalMouseUp)
|
||||
}}
|
||||
/>)
|
||||
}
|
||||
|
||||
private globalMouseMove = (e: MouseEvent) => {
|
||||
if (!this.state.isDragging) {
|
||||
return
|
||||
}
|
||||
const offset = e.clientX - this.state.startX
|
||||
if (offset !== this.state.offset) {
|
||||
this.props.onDrag(offset)
|
||||
this.setState({offset})
|
||||
}
|
||||
}
|
||||
|
||||
private globalMouseUp = (e: MouseEvent) => {
|
||||
window.removeEventListener('mousemove', this.globalMouseMove)
|
||||
window.removeEventListener('mouseup', this.globalMouseUp)
|
||||
this.setState({isDragging: false})
|
||||
const offset = e.clientX - this.state.startX
|
||||
this.props.onDragEnd(offset)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={drag}
|
||||
className='HorizontalGrip'
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export default HorizontalGrip
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import {useDrop, useDragLayer} from 'react-dnd'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {MutableBoardView} from '../../blocks/boardView'
|
||||
@ -10,15 +11,9 @@ import {Constants} from '../../constants'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import SortDownIcon from '../../widgets/icons/sortDown'
|
||||
import SortUpIcon from '../../widgets/icons/sortUp'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import Label from '../../widgets/label'
|
||||
|
||||
import HorizontalGrip from './horizontalGrip'
|
||||
|
||||
import './table.scss'
|
||||
import TableHeaderMenu from './tableHeaderMenu'
|
||||
import TableHeader from './tableHeader'
|
||||
import TableRow from './tableRow'
|
||||
|
||||
type Props = {
|
||||
@ -31,252 +26,174 @@ type Props = {
|
||||
onCardClicked: (e: React.MouseEvent, card: Card) => void
|
||||
}
|
||||
|
||||
type State = {
|
||||
shownCardId?: string
|
||||
}
|
||||
const Table = (props: Props) => {
|
||||
const {boardTree} = props
|
||||
const {board, cards, activeView} = boardTree
|
||||
|
||||
class Table extends React.Component<Props, State> {
|
||||
private draggedHeaderTemplate?: IPropertyTemplate
|
||||
state: State = {}
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const {boardTree} = this.props
|
||||
const {board, cards, activeView} = boardTree
|
||||
const titleRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
let titleSortIcon: React.ReactNode
|
||||
const titleSortOption = activeView.sortOptions.find((o) => o.propertyId === Constants.titleColumnId)
|
||||
if (titleSortOption) {
|
||||
titleSortIcon = titleSortOption.reversed ? <SortUpIcon/> : <SortDownIcon/>
|
||||
const {offset, resizingColumn} = useDragLayer((monitor) => {
|
||||
if (monitor.getItemType() === 'horizontalGrip') {
|
||||
return {
|
||||
offset: monitor.getDifferenceFromInitialOffset()?.x || 0,
|
||||
resizingColumn: monitor.getItem()?.id,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='octo-table-body Table'>
|
||||
|
||||
{/* Headers */}
|
||||
|
||||
<div
|
||||
className='octo-table-header'
|
||||
id='mainBoardHeader'
|
||||
>
|
||||
<div
|
||||
id='mainBoardHeader'
|
||||
ref={titleRef}
|
||||
className='octo-table-cell header-cell'
|
||||
style={{overflow: 'unset', width: this.columnWidth(Constants.titleColumnId)}}
|
||||
>
|
||||
<MenuWrapper disabled={this.props.readonly}>
|
||||
<Label>
|
||||
<FormattedMessage
|
||||
id='TableComponent.name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
{titleSortIcon}
|
||||
</Label>
|
||||
<TableHeaderMenu
|
||||
boardTree={boardTree}
|
||||
templateId={Constants.titleColumnId}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
{!this.props.readonly &&
|
||||
<HorizontalGrip
|
||||
onDrag={(offset) => {
|
||||
const originalWidth = this.columnWidth(Constants.titleColumnId)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
if (titleRef.current) {
|
||||
titleRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
}}
|
||||
onDragEnd={(offset) => {
|
||||
Utils.log(`onDragEnd offset: ${offset}`)
|
||||
const originalWidth = this.columnWidth(Constants.titleColumnId)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
if (titleRef.current) {
|
||||
titleRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
if (newWidth !== columnWidths[Constants.titleColumnId]) {
|
||||
columnWidths[Constants.titleColumnId] = newWidth
|
||||
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.columnWidths = columnWidths
|
||||
mutator.updateBlock(newView, activeView, 'resize column')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Table header row */}
|
||||
|
||||
{board.cardProperties.
|
||||
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
||||
map((template) => {
|
||||
const headerRef = React.createRef<HTMLDivElement>()
|
||||
let sortIcon
|
||||
const sortOption = activeView.sortOptions.find((o) => o.propertyId === template.id)
|
||||
if (sortOption) {
|
||||
sortIcon = sortOption.reversed ? <SortUpIcon/> : <SortDownIcon/>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={template.id}
|
||||
ref={headerRef}
|
||||
style={{overflow: 'unset', width: this.columnWidth(template.id)}}
|
||||
className='octo-table-cell header-cell'
|
||||
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLElement).classList.add('dragover')
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLElement).classList.add('dragover')
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLElement).classList.remove('dragover')
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLElement).classList.remove('dragover')
|
||||
this.onDropToColumn(template)
|
||||
}}
|
||||
>
|
||||
<MenuWrapper
|
||||
disabled={this.props.readonly}
|
||||
>
|
||||
<div
|
||||
draggable={!this.props.readonly}
|
||||
onDragStart={() => {
|
||||
this.draggedHeaderTemplate = template
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
this.draggedHeaderTemplate = undefined
|
||||
}}
|
||||
>
|
||||
<Label>
|
||||
{template.name}
|
||||
{sortIcon}
|
||||
</Label>
|
||||
</div>
|
||||
<TableHeaderMenu
|
||||
boardTree={boardTree}
|
||||
templateId={template.id}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
{!this.props.readonly &&
|
||||
<HorizontalGrip
|
||||
onDrag={(offset) => {
|
||||
const originalWidth = this.columnWidth(template.id)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
if (headerRef.current) {
|
||||
headerRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
}}
|
||||
onDragEnd={(offset) => {
|
||||
Utils.log(`onDragEnd offset: ${offset}`)
|
||||
const originalWidth = this.columnWidth(template.id)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
if (headerRef.current) {
|
||||
headerRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
if (newWidth !== columnWidths[template.id]) {
|
||||
columnWidths[template.id] = newWidth
|
||||
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.columnWidths = columnWidths
|
||||
mutator.updateBlock(newView, activeView, 'resize column')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Rows, one per card */}
|
||||
|
||||
{cards.map((card) => {
|
||||
const tableRow = (
|
||||
<TableRow
|
||||
key={card.id + card.updateAt}
|
||||
boardTree={boardTree}
|
||||
card={card}
|
||||
isSelected={this.props.selectedCardIds.includes(card.id)}
|
||||
focusOnMount={this.props.cardIdToFocusOnRender === card.id}
|
||||
onSaveWithEnter={() => {
|
||||
if (cards.length > 0 && cards[cards.length - 1] === card) {
|
||||
this.props.addCard(false)
|
||||
}
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.props.onCardClicked(e, card)
|
||||
}}
|
||||
showCard={this.props.showCard}
|
||||
readonly={this.props.readonly}
|
||||
/>)
|
||||
|
||||
return tableRow
|
||||
})}
|
||||
|
||||
{/* Add New row */}
|
||||
|
||||
<div className='octo-table-footer'>
|
||||
{!this.props.readonly &&
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
onClick={() => {
|
||||
this.props.addCard(false)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='TableComponent.plus-new'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private columnWidth(templateId: string): number {
|
||||
return Math.max(Constants.minColumnWidth, this.props.boardTree.activeView.columnWidths[templateId] || 0)
|
||||
}
|
||||
|
||||
private async onDropToColumn(template: IPropertyTemplate) {
|
||||
const {draggedHeaderTemplate} = this
|
||||
if (!draggedHeaderTemplate) {
|
||||
return
|
||||
return {
|
||||
offset: 0,
|
||||
resizingColumn: '',
|
||||
}
|
||||
})
|
||||
|
||||
const {boardTree} = this.props
|
||||
const {board} = boardTree
|
||||
const [, drop] = useDrop(() => ({
|
||||
accept: 'horizontalGrip',
|
||||
drop: (item: {id: string}, monitor) => {
|
||||
const columnWidths = {...activeView.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
|
||||
|
||||
Utils.assertValue(mutator)
|
||||
Utils.assertValue(boardTree)
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.columnWidths = columnWidths
|
||||
mutator.updateBlock(newView, activeView, 'resize column')
|
||||
}
|
||||
},
|
||||
}), [activeView])
|
||||
|
||||
Utils.log(`ondrop. Source column: ${draggedHeaderTemplate.name}, dest column: ${template.name}`)
|
||||
const onDropToCard = (srcCard: Card, dstCard: Card) => {
|
||||
Utils.log(`onDropToCard: ${dstCard.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'
|
||||
|
||||
// Update dstCard order
|
||||
let cardOrder = Array.from(new Set([...activeView.cardOrder, ...boardTree.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 (isDraggingDown) {
|
||||
destIndex += 1
|
||||
}
|
||||
cardOrder.splice(destIndex, 0, ...draggedCardIds)
|
||||
|
||||
mutator.performAsUndoGroup(async () => {
|
||||
await mutator.changeViewCardOrder(activeView, cardOrder, description)
|
||||
})
|
||||
}
|
||||
|
||||
const onDropToColumn = async (template: IPropertyTemplate, container: IPropertyTemplate) => {
|
||||
Utils.log(`ondrop. Source column: ${template.name}, dest column: ${container.name}`)
|
||||
|
||||
// Move template to new index
|
||||
const destIndex = template ? board.cardProperties.indexOf(template) : 0
|
||||
await mutator.changePropertyTemplateOrder(board, draggedHeaderTemplate, destIndex)
|
||||
const destIndex = container ? board.cardProperties.indexOf(container) : 0
|
||||
await mutator.changePropertyTemplateOrder(board, template, destIndex >= 0 ? destIndex : 0)
|
||||
}
|
||||
|
||||
const titleSortOption = activeView.sortOptions.find((o) => o.propertyId === Constants.titleColumnId)
|
||||
let titleSorted: 'up' | 'down' | 'none' = 'none'
|
||||
if (titleSortOption) {
|
||||
titleSorted = titleSortOption.reversed ? 'up' : 'down'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='octo-table-body Table'
|
||||
ref={drop}
|
||||
>
|
||||
{/* Headers */}
|
||||
|
||||
<div
|
||||
className='octo-table-header'
|
||||
id='mainBoardHeader'
|
||||
>
|
||||
<TableHeader
|
||||
name={
|
||||
<FormattedMessage
|
||||
id='TableComponent.name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
}
|
||||
sorted={titleSorted}
|
||||
readonly={props.readonly}
|
||||
boardTree={boardTree}
|
||||
template={{id: Constants.titleColumnId, name: 'title', type: 'text', options: []}}
|
||||
offset={resizingColumn === Constants.titleColumnId ? offset : 0}
|
||||
onDrop={onDropToColumn}
|
||||
/>
|
||||
|
||||
{/* Table header row */}
|
||||
|
||||
{board.cardProperties.
|
||||
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
||||
map((template) => {
|
||||
let sorted: 'up' | 'down' | 'none' = 'none'
|
||||
const sortOption = activeView.sortOptions.find((o) => o.propertyId === template.id)
|
||||
if (sortOption) {
|
||||
sorted = sortOption.reversed ? 'up' : 'down'
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHeader
|
||||
name={template.name}
|
||||
sorted={sorted}
|
||||
readonly={props.readonly}
|
||||
boardTree={boardTree}
|
||||
template={template}
|
||||
key={template.id}
|
||||
offset={resizingColumn === template.id ? offset : 0}
|
||||
onDrop={onDropToColumn}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Rows, one per card */}
|
||||
|
||||
{cards.map((card) => {
|
||||
const tableRow = (
|
||||
<TableRow
|
||||
key={card.id + card.updateAt}
|
||||
boardTree={boardTree}
|
||||
card={card}
|
||||
isSelected={props.selectedCardIds.includes(card.id)}
|
||||
focusOnMount={props.cardIdToFocusOnRender === card.id}
|
||||
onSaveWithEnter={() => {
|
||||
if (cards.length > 0 && cards[cards.length - 1] === card) {
|
||||
props.addCard(false)
|
||||
}
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
props.onCardClicked(e, card)
|
||||
}}
|
||||
showCard={props.showCard}
|
||||
readonly={props.readonly}
|
||||
onDrop={onDropToCard}
|
||||
offset={offset}
|
||||
resizingColumn={resizingColumn}
|
||||
/>)
|
||||
|
||||
return tableRow
|
||||
})}
|
||||
|
||||
{/* Add New row */}
|
||||
|
||||
<div className='octo-table-footer'>
|
||||
{!props.readonly &&
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
onClick={() => {
|
||||
props.addCard(false)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='TableComponent.plus-new'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Table
|
||||
|
69
webapp/src/components/table/tableHeader.tsx
Normal file
69
webapp/src/components/table/tableHeader.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Constants} from '../../constants'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import SortDownIcon from '../../widgets/icons/sortDown'
|
||||
import SortUpIcon from '../../widgets/icons/sortUp'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import Label from '../../widgets/label'
|
||||
import useSortable from '../../hooks/sortable'
|
||||
|
||||
import HorizontalGrip from './horizontalGrip'
|
||||
|
||||
import './table.scss'
|
||||
import TableHeaderMenu from './tableHeaderMenu'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
sorted: 'up'|'down'|'none'
|
||||
name: React.ReactNode
|
||||
boardTree: BoardTree
|
||||
template: IPropertyTemplate
|
||||
offset: number
|
||||
onDrop: (template: IPropertyTemplate, container: IPropertyTemplate) => void
|
||||
}
|
||||
|
||||
const TableHeader = React.memo((props: Props): JSX.Element => {
|
||||
const isManualSort = props.boardTree.activeView.sortOptions.length < 1
|
||||
const [isDragging, isOver, columnRef] = useSortable('column', props.template, isManualSort, props.onDrop)
|
||||
|
||||
const columnWidth = (templateId: string): number => {
|
||||
return Math.max(Constants.minColumnWidth, (props.boardTree.activeView.columnWidths[templateId] || 0) + props.offset)
|
||||
}
|
||||
|
||||
let className = 'octo-table-cell header-cell'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{overflow: 'unset', width: columnWidth(props.template.id), opacity: isDragging ? 0.5 : 1}}
|
||||
ref={props.template.id === Constants.titleColumnId ? () => null : columnRef}
|
||||
>
|
||||
<MenuWrapper disabled={props.readonly}>
|
||||
<Label>
|
||||
{props.name}
|
||||
{props.sorted === 'up' && <SortUpIcon/>}
|
||||
{props.sorted === 'down' && <SortDownIcon/>}
|
||||
</Label>
|
||||
<TableHeaderMenu
|
||||
boardTree={props.boardTree}
|
||||
templateId={props.template.id}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
{!props.readonly &&
|
||||
<HorizontalGrip templateId={props.template.id}/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default TableHeader
|
@ -9,6 +9,7 @@ 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 PropertyValueElement from '../propertyValueElement'
|
||||
import './tableRow.scss'
|
||||
@ -21,12 +22,21 @@ type Props = {
|
||||
onSaveWithEnter: () => void
|
||||
showCard: (cardId: string) => void
|
||||
readonly: boolean
|
||||
offset: number
|
||||
resizingColumn: string
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
onDrop: (srcCard: Card, dstCard: Card) => void
|
||||
}
|
||||
|
||||
const TableRow = React.memo((props: Props) => {
|
||||
const {boardTree, onSaveWithEnter} = props
|
||||
const {board, activeView} = boardTree
|
||||
|
||||
const titleRef = useRef<Editable>(null)
|
||||
const [title, setTitle] = useState(props.card.title)
|
||||
const {card} = props
|
||||
const isManualSort = activeView.sortOptions.length < 1
|
||||
const [isDragging, isOver, cardRef] = useSortable('card', card, isManualSort, props.onDrop)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.focusOnMount) {
|
||||
@ -35,18 +45,23 @@ 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.boardTree.activeView.columnWidths[templateId] || 0)
|
||||
}
|
||||
|
||||
const {boardTree, card, onSaveWithEnter} = props
|
||||
const {board, activeView} = boardTree
|
||||
|
||||
const className = props.isSelected ? 'TableRow octo-table-row selected' : 'TableRow octo-table-row'
|
||||
let className = props.isSelected ? 'TableRow octo-table-row selected' : 'TableRow octo-table-row'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={props.onClick}
|
||||
ref={cardRef}
|
||||
style={{opacity: isDragging ? 0.5 : 1}}
|
||||
>
|
||||
|
||||
{/* Name / title */}
|
||||
|
@ -1,8 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {Constants} from '../../constants'
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
import mutator from '../../mutator'
|
||||
@ -13,6 +14,7 @@ import MenuWrapper from '../../widgets/menuWrapper'
|
||||
type Props = {
|
||||
properties: readonly IPropertyTemplate[]
|
||||
activeView: BoardView
|
||||
intl: IntlShape
|
||||
}
|
||||
const ViewHeaderPropertiesMenu = React.memo((props: Props) => {
|
||||
const {properties, activeView} = props
|
||||
@ -25,6 +27,22 @@ const ViewHeaderPropertiesMenu = React.memo((props: Props) => {
|
||||
/>
|
||||
</Button>
|
||||
<Menu>
|
||||
{activeView.viewType === 'gallery' &&
|
||||
<Menu.Switch
|
||||
key={Constants.titleColumnId}
|
||||
id={Constants.titleColumnId}
|
||||
name={props.intl.formatMessage({id: 'default-properties.title', defaultMessage: 'Title'})}
|
||||
isOn={activeView.visiblePropertyIds.includes(Constants.titleColumnId)}
|
||||
onClick={(propertyId: string) => {
|
||||
let newVisiblePropertyIds = []
|
||||
if (activeView.visiblePropertyIds.includes(propertyId)) {
|
||||
newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o: string) => o !== propertyId)
|
||||
} else {
|
||||
newVisiblePropertyIds = [...activeView.visiblePropertyIds, propertyId]
|
||||
}
|
||||
mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds)
|
||||
}}
|
||||
/>}
|
||||
{properties.map((option: IPropertyTemplate) => (
|
||||
<Menu.Switch
|
||||
key={option.id}
|
||||
@ -47,4 +65,4 @@ const ViewHeaderPropertiesMenu = React.memo((props: Props) => {
|
||||
)
|
||||
})
|
||||
|
||||
export default ViewHeaderPropertiesMenu
|
||||
export default injectIntl(ViewHeaderPropertiesMenu)
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -132,6 +132,7 @@ export class ViewMenu extends React.PureComponent<Props> {
|
||||
view.viewType = 'gallery'
|
||||
view.parentId = board.id
|
||||
view.rootId = board.rootId
|
||||
view.visiblePropertyIds = [Constants.titleColumnId]
|
||||
|
||||
const oldViewId = boardTree.activeView.id
|
||||
|
||||
|
29
webapp/src/hooks/sortable.tsx
Normal file
29
webapp/src/hooks/sortable.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useRef} from 'react'
|
||||
import {useDrag, useDrop} from 'react-dnd'
|
||||
|
||||
export default function useSortable(itemType: string, item: any, enabled: boolean, handler: (src: any, st: any) => void): [boolean, boolean, React.RefObject<HTMLDivElement>] {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [{isDragging}, drag] = useDrag(() => ({
|
||||
type: itemType,
|
||||
item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => enabled,
|
||||
}), [itemType, item, enabled])
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: itemType,
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
drop: (dragItem: any) => {
|
||||
handler(dragItem, item)
|
||||
},
|
||||
canDrop: () => enabled,
|
||||
}), [item, handler, enabled])
|
||||
|
||||
drop(drag(ref))
|
||||
return [isDragging, isOver, ref]
|
||||
}
|
@ -96,7 +96,7 @@ class BoardPage extends React.Component<Props, State> {
|
||||
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<Props, State> {
|
||||
} 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<Props, State> {
|
||||
return (
|
||||
<div className='BoardPage'>
|
||||
<HotKeys
|
||||
keyName='shift+ctrl+z,ctrl+z'
|
||||
keyName='shift+ctrl+z,shift+cmd+z,ctrl+z,cmd+z'
|
||||
onKeyDown={this.undoRedoHandler}
|
||||
/>
|
||||
<Workspace
|
||||
|
@ -8,6 +8,7 @@
|
||||
--button-bg: 22, 109, 204;
|
||||
--link-color: 35, 137, 215;
|
||||
--link-visited-color: #551a8b;
|
||||
--error-color: #ffa9a9;
|
||||
|
||||
// Label Colors
|
||||
--prop-default: #fff;
|
||||
@ -32,4 +33,4 @@
|
||||
// Radius
|
||||
--default-rad: 4px;
|
||||
--modal-rad: 8px;
|
||||
}
|
||||
}
|
||||
|
@ -231,6 +231,22 @@ class Utils {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static isMobile() {
|
||||
const toMatch = [
|
||||
/Android/i,
|
||||
/webOS/i,
|
||||
/iPhone/i,
|
||||
/iPad/i,
|
||||
/iPod/i,
|
||||
/BlackBerry/i,
|
||||
/Windows Phone/i,
|
||||
]
|
||||
|
||||
return toMatch.some((toMatchItem) => {
|
||||
return navigator.userAgent.match(toMatchItem)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export {Utils}
|
||||
|
@ -198,7 +198,32 @@ 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)
|
||||
if (searchTextInCardTitle) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
private setGroupByProperty(propertyId: string) {
|
||||
|
@ -3,6 +3,7 @@
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border: 1px solid transparent;
|
||||
&.active {
|
||||
min-width: 100px;
|
||||
}
|
||||
@ -10,4 +11,8 @@
|
||||
color: rgba(var(--body-color), 0.4);
|
||||
opacity: 1;
|
||||
}
|
||||
&.error {
|
||||
border: 1px solid var(--error-color);
|
||||
border-radius: var(--default-rad);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ type Props = {
|
||||
saveOnEsc?: boolean
|
||||
readonly?: boolean
|
||||
|
||||
validator?: (value: string) => boolean
|
||||
onCancel?: () => void
|
||||
onSave?: (saveType: 'onEnter'|'onEsc'|'onBlur') => void
|
||||
}
|
||||
@ -24,6 +25,22 @@ export default class Editable extends React.Component<Props> {
|
||||
return true
|
||||
}
|
||||
|
||||
save = (saveType: 'onEnter'|'onEsc'|'onBlur'): void => {
|
||||
if (this.props.validator && !this.props.validator(this.props.value || '')) {
|
||||
return
|
||||
}
|
||||
if (!this.props.onSave) {
|
||||
return
|
||||
}
|
||||
if (saveType === 'onBlur' && !this.saveOnBlur) {
|
||||
return
|
||||
}
|
||||
if (saveType === 'onEsc' && !this.props.saveOnEsc) {
|
||||
return
|
||||
}
|
||||
this.props.onSave(saveType)
|
||||
}
|
||||
|
||||
public focus(selectAll = false): void {
|
||||
if (this.elementRef.current) {
|
||||
const valueLength = this.elementRef.current.value.length
|
||||
@ -44,30 +61,34 @@ export default class Editable extends React.Component<Props> {
|
||||
|
||||
public render(): JSX.Element {
|
||||
const {value, onChange, className, placeholderText} = this.props
|
||||
let error = false
|
||||
if (this.props.validator) {
|
||||
error = !this.props.validator(value || '')
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={this.elementRef}
|
||||
className={'Editable ' + className}
|
||||
className={'Editable ' + (error ? 'error ' : '') + className}
|
||||
placeholder={placeholderText}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
value={value}
|
||||
title={value}
|
||||
onBlur={() => this.saveOnBlur && this.props.onSave && this.props.onSave('onBlur')}
|
||||
onBlur={() => this.save('onBlur')}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.keyCode === 27 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // ESC
|
||||
e.stopPropagation()
|
||||
if (this.props.saveOnEsc) {
|
||||
this.props.onSave?.('onEsc')
|
||||
this.save('onEsc')
|
||||
} else {
|
||||
this.props.onCancel?.()
|
||||
}
|
||||
this.blur()
|
||||
} else if (e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // Return
|
||||
e.stopPropagation()
|
||||
this.props.onSave?.('onEnter')
|
||||
this.save('onEnter')
|
||||
this.blur()
|
||||
}
|
||||
}}
|
||||
|
@ -2,18 +2,24 @@
|
||||
width: 100%;
|
||||
border-radius: var(--default-rad);
|
||||
color: rgb(var(--body-color));
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--body-color), 0.1),
|
||||
}
|
||||
display: flex;
|
||||
|
||||
> .Label {
|
||||
margin: 0 10px;
|
||||
max-width: calc(100% - 10px);
|
||||
&.empty {
|
||||
color: rgba(var(--body-color), 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.Label {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
border-radius: var(--default-rad);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.value-menu-option {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
@ -107,14 +107,19 @@ function ValueSelector(props: Props): JSX.Element {
|
||||
}),
|
||||
control: (): CSSObject => ({
|
||||
border: 0,
|
||||
width: '100%',
|
||||
margin: '4px 0 0 0',
|
||||
}),
|
||||
valueContainer: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
padding: '0 8px',
|
||||
overflow: 'unset',
|
||||
}),
|
||||
singleValue: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
color: 'rgb(var(--main-fg))',
|
||||
overflow: 'unset',
|
||||
maxWidth: 'calc(100% - 20px)',
|
||||
}),
|
||||
input: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
|
Loading…
x
Reference in New Issue
Block a user