kanbanqube 1.0.9 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -11
- package/package.json +1 -1
- package/public/app.js +58 -15
- package/public/index.html +4 -1
- package/public/styles.css +9 -0
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ The app provides lanes, cards, labels, checklists, comments, card covers, file a
|
|
|
23
23
|
- [Vaults](#vaults)
|
|
24
24
|
- [Board Workflow](#board-workflow)
|
|
25
25
|
- [Git Sync](#git-sync)
|
|
26
|
-
- [Import](#import)
|
|
26
|
+
- [Trello Import](#trello-import)
|
|
27
27
|
- [Demo Board](#demo-board)
|
|
28
28
|
- [Identity](#identity)
|
|
29
29
|
- [Attachments And Covers](#attachments-and-covers)
|
|
@@ -125,7 +125,7 @@ vault/
|
|
|
125
125
|
uploads/
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
If `board/` does not exist, KanbanQube creates it automatically. If
|
|
128
|
+
If `board/` does not exist, KanbanQube creates it automatically. If `uploads/` does not exist, it is created when the first file is uploaded.
|
|
129
129
|
|
|
130
130
|
Uploaded filenames are made unique while preserving the original name in the UI. For example:
|
|
131
131
|
|
|
@@ -180,9 +180,9 @@ git commit -m "Initialize KanbanQube board"
|
|
|
180
180
|
git push -u origin main
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
-
## Import
|
|
183
|
+
## Trello Import
|
|
184
184
|
|
|
185
|
-
KanbanQube can import a board export JSON from Settings. Import is only enabled when the current board has no cards. This keeps accidental replacement of an active board from happening inside the app.
|
|
185
|
+
KanbanQube can import a Trello board export JSON from Settings. Import is only enabled when the current board has no cards. This keeps accidental replacement of an active board from happening inside the app.
|
|
186
186
|
|
|
187
187
|
Imported data is normalized and written into the split `board/` vault layout.
|
|
188
188
|
|
|
@@ -278,10 +278,4 @@ brew install mathiasconradt/kanbanqube/kanbanqube
|
|
|
278
278
|
|
|
279
279
|
## Star History
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
<picture>
|
|
283
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=mathiasconradt/kanbanqube&type=timeline&theme=dark&logscale&legend=top-left" />
|
|
284
|
-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=mathiasconradt/kanbanqube&type=timeline&logscale&legend=top-left" />
|
|
285
|
-
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=mathiasconradt/kanbanqube&type=timeline&logscale&legend=top-left" />
|
|
286
|
-
</picture>
|
|
287
|
-
</a>
|
|
281
|
+
[](https://www.star-history.com/?repos=mathiasconradt%2Fkanbanqube&type=timeline&logscale=&legend=top-left)
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -75,6 +75,7 @@ const syncLogCloseButton = document.getElementById("syncLogCloseButton");
|
|
|
75
75
|
const archiveDialog = document.getElementById("archiveDialog");
|
|
76
76
|
const archiveList = document.getElementById("archiveList");
|
|
77
77
|
const closeArchiveButton = document.getElementById("closeArchiveButton");
|
|
78
|
+
const deleteAllArchivedButton = document.getElementById("deleteAllArchivedButton");
|
|
78
79
|
|
|
79
80
|
const cardDialog = document.getElementById("cardDialog");
|
|
80
81
|
const cardTitleInput = document.getElementById("cardTitleInput");
|
|
@@ -247,6 +248,7 @@ function wireEvents() {
|
|
|
247
248
|
boardTitle.addEventListener("click", startBoardTitleEdit);
|
|
248
249
|
closeSettingsButton.addEventListener("click", () => settingsDialog.close());
|
|
249
250
|
closeArchiveButton.addEventListener("click", () => archiveDialog.close());
|
|
251
|
+
deleteAllArchivedButton.addEventListener("click", deleteAllArchivedCards);
|
|
250
252
|
saveSettingsButton.addEventListener("click", saveSettings);
|
|
251
253
|
importBoardButton.addEventListener("click", () => importBoardInput.click());
|
|
252
254
|
importBoardInput.addEventListener("change", () => {
|
|
@@ -620,6 +622,8 @@ function renderCardDialog() {
|
|
|
620
622
|
function renderArchiveDialog() {
|
|
621
623
|
archiveList.textContent = "";
|
|
622
624
|
const cards = archivedCards();
|
|
625
|
+
deleteAllArchivedButton.disabled = cards.length === 0;
|
|
626
|
+
deleteAllArchivedButton.hidden = cards.length === 0;
|
|
623
627
|
|
|
624
628
|
if (cards.length === 0) {
|
|
625
629
|
const empty = document.createElement("div");
|
|
@@ -689,11 +693,13 @@ function renderLabelsEditor(card) {
|
|
|
689
693
|
|
|
690
694
|
const assignedLabels = labelsForCard(card);
|
|
691
695
|
if (assignedLabels.length === 0) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
696
|
+
if (!state.labelEditorOpen) {
|
|
697
|
+
const empty = document.createElement("div");
|
|
698
|
+
empty.className = "empty-state label-summary-trigger";
|
|
699
|
+
empty.textContent = "No labels assigned.";
|
|
700
|
+
empty.addEventListener("click", openLabelEditor);
|
|
701
|
+
cardLabels.append(empty);
|
|
702
|
+
}
|
|
697
703
|
} else {
|
|
698
704
|
for (const label of assignedLabels) {
|
|
699
705
|
const pill = document.createElement("button");
|
|
@@ -724,6 +730,7 @@ function renderLabelsEditor(card) {
|
|
|
724
730
|
searchInput.type = "search";
|
|
725
731
|
searchInput.placeholder = "Search labels…";
|
|
726
732
|
searchInput.value = state.labelSearchTerm;
|
|
733
|
+
searchInput.addEventListener("keydown", stopLabelEditorShortcut);
|
|
727
734
|
searchInput.addEventListener("input", () => {
|
|
728
735
|
state.labelSearchTerm = searchInput.value;
|
|
729
736
|
renderCardDialog();
|
|
@@ -773,11 +780,21 @@ function renderLabelsEditor(card) {
|
|
|
773
780
|
nameInput.value = label.name || "";
|
|
774
781
|
nameInput.placeholder = "Label name";
|
|
775
782
|
nameInput.style.backgroundColor = colorForLabel(label.color);
|
|
783
|
+
let committedName = label.name || "";
|
|
784
|
+
nameInput.addEventListener("keydown", stopLabelEditorShortcut);
|
|
776
785
|
nameInput.addEventListener("input", () => {
|
|
777
|
-
label.name = nameInput.value
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
786
|
+
label.name = nameInput.value;
|
|
787
|
+
});
|
|
788
|
+
nameInput.addEventListener("blur", () => {
|
|
789
|
+
const trimmedName = nameInput.value.trim();
|
|
790
|
+
if (label.name !== trimmedName) {
|
|
791
|
+
label.name = trimmedName;
|
|
792
|
+
}
|
|
793
|
+
if (committedName !== label.name) {
|
|
794
|
+
committedName = label.name;
|
|
795
|
+
queueSave("Label updated");
|
|
796
|
+
renderBoard();
|
|
797
|
+
}
|
|
781
798
|
});
|
|
782
799
|
|
|
783
800
|
const colorSelect = document.createElement("select");
|
|
@@ -786,9 +803,10 @@ function renderLabelsEditor(card) {
|
|
|
786
803
|
const option = document.createElement("option");
|
|
787
804
|
option.value = color;
|
|
788
805
|
option.textContent = color.replaceAll("_", " ");
|
|
789
|
-
|
|
806
|
+
option.selected = color === label.color;
|
|
790
807
|
colorSelect.append(option);
|
|
791
808
|
}
|
|
809
|
+
colorSelect.addEventListener("keydown", stopLabelEditorShortcut);
|
|
792
810
|
colorSelect.addEventListener("change", () => {
|
|
793
811
|
label.color = colorSelect.value;
|
|
794
812
|
queueSave("Label updated");
|
|
@@ -829,6 +847,14 @@ function renderLabelsEditor(card) {
|
|
|
829
847
|
appendLabelEditorPanel(panel);
|
|
830
848
|
}
|
|
831
849
|
|
|
850
|
+
function stopLabelEditorShortcut(event) {
|
|
851
|
+
event.stopPropagation();
|
|
852
|
+
if (event.key === "Enter") {
|
|
853
|
+
event.preventDefault();
|
|
854
|
+
event.currentTarget.blur();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
832
858
|
function appendLabelEditorPanel(panel) {
|
|
833
859
|
const anchor = labelEditorContainer.closest(".sidebar-panel") || labelEditorContainer;
|
|
834
860
|
const rect = anchor.getBoundingClientRect();
|
|
@@ -1461,6 +1487,27 @@ async function deleteArchivedCard(cardId) {
|
|
|
1461
1487
|
render();
|
|
1462
1488
|
}
|
|
1463
1489
|
|
|
1490
|
+
async function deleteAllArchivedCards() {
|
|
1491
|
+
const cards = archivedCards();
|
|
1492
|
+
if (cards.length === 0) return;
|
|
1493
|
+
|
|
1494
|
+
const confirmed = await openConfirmDialog({
|
|
1495
|
+
label: "Delete archive",
|
|
1496
|
+
title: "Delete all archived cards?",
|
|
1497
|
+
message: `Delete ${cards.length} archived card${cards.length === 1 ? "" : "s"} permanently? This cannot be undone.`,
|
|
1498
|
+
confirmLabel: "Delete all",
|
|
1499
|
+
danger: true
|
|
1500
|
+
});
|
|
1501
|
+
if (!confirmed) return;
|
|
1502
|
+
|
|
1503
|
+
for (const card of cards) {
|
|
1504
|
+
removeCardCompletely(card.id);
|
|
1505
|
+
}
|
|
1506
|
+
state.selectedCardId = null;
|
|
1507
|
+
queueSave("Archived cards deleted");
|
|
1508
|
+
render();
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1464
1511
|
async function deleteLane(listId) {
|
|
1465
1512
|
const list = listById(listId);
|
|
1466
1513
|
if (!list) return;
|
|
@@ -1623,11 +1670,7 @@ function toggleKeyboardLabel(card, labelIndex) {
|
|
|
1623
1670
|
}
|
|
1624
1671
|
|
|
1625
1672
|
function sortedBoardLabels() {
|
|
1626
|
-
return [...(state.board?.labels || [])]
|
|
1627
|
-
const leftName = (left.name || left.color || "").toLowerCase();
|
|
1628
|
-
const rightName = (right.name || right.color || "").toLowerCase();
|
|
1629
|
-
return leftName.localeCompare(rightName);
|
|
1630
|
-
});
|
|
1673
|
+
return [...(state.board?.labels || [])];
|
|
1631
1674
|
}
|
|
1632
1675
|
|
|
1633
1676
|
function setKeyboardCard(cardId, shouldRender = true) {
|
package/public/index.html
CHANGED
|
@@ -215,7 +215,10 @@
|
|
|
215
215
|
<p class="modal-label">Archive</p>
|
|
216
216
|
<h2>Archived cards</h2>
|
|
217
217
|
</div>
|
|
218
|
-
<
|
|
218
|
+
<div class="archive-header-actions">
|
|
219
|
+
<button id="deleteAllArchivedButton" type="button" class="danger-button">Delete all</button>
|
|
220
|
+
<button id="closeArchiveButton" type="button" class="icon-button" aria-label="Close archive">✕</button>
|
|
221
|
+
</div>
|
|
219
222
|
</header>
|
|
220
223
|
<div id="archiveList" class="archive-list"></div>
|
|
221
224
|
</form>
|
package/public/styles.css
CHANGED
|
@@ -214,6 +214,7 @@ button {
|
|
|
214
214
|
.topbar-actions,
|
|
215
215
|
.board-meta,
|
|
216
216
|
.lane-actions,
|
|
217
|
+
.archive-header-actions,
|
|
217
218
|
.modal-footer,
|
|
218
219
|
.panel-header,
|
|
219
220
|
.settings-meta {
|
|
@@ -222,6 +223,14 @@ button {
|
|
|
222
223
|
align-items: center;
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
.archive-header-actions {
|
|
227
|
+
flex: 0 0 auto;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#promptDialog .modal-footer {
|
|
231
|
+
margin-top: 1rem;
|
|
232
|
+
}
|
|
233
|
+
|
|
225
234
|
.primary-button,
|
|
226
235
|
.ghost-button,
|
|
227
236
|
.danger-button,
|