Skip to content

Commit 1077761

Browse files
authored
Add Move Column Buttons to Kanban (#164)
* add move right/left buttons for columns * remove / add some imports * spelling/spacing * 1.11.1
1 parent bc12bc7 commit 1077761

File tree

3 files changed

+111
-38
lines changed

3 files changed

+111
-38
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "query-builder",
3-
"version": "1.11.0",
3+
"version": "1.11.1",
44
"description": "Introduces new user interfaces for building queries in Roam",
55
"main": "./build/main.js",
66
"author": {

src/components/Kanban.tsx

Lines changed: 108 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
// Design inspiration from Trello
2-
import React from "react";
2+
import React, {
3+
useCallback,
4+
useEffect,
5+
useMemo,
6+
useRef,
7+
useState,
8+
} from "react";
39
import { Column, Result } from "../utils/types";
4-
import { Button, Icon, InputGroup, Tooltip } from "@blueprintjs/core";
10+
import { Button, Icon, Popover, Tooltip } from "@blueprintjs/core";
511
import Draggable from "react-draggable";
612
import setInputSettings from "roamjs-components/util/setInputSettings";
713
import openBlockInSidebar from "roamjs-components/writes/openBlockInSidebar";
@@ -11,8 +17,8 @@ import AutocompleteInput from "roamjs-components/components/AutocompleteInput";
1117
import predefinedSelections from "../utils/predefinedSelections";
1218
import toCellValue from "../utils/toCellValue";
1319
import extractTag from "roamjs-components/util/extractTag";
14-
import createBlock from "roamjs-components/writes/createBlock";
15-
import updateBlock from "roamjs-components/writes/updateBlock";
20+
import deleteBlock from "roamjs-components/writes/deleteBlock";
21+
import getSubTree from "roamjs-components/util/getSubTree";
1622

1723
const zPriority = z.record(z.number().min(0).max(1));
1824

@@ -25,7 +31,7 @@ const KanbanCard = (card: {
2531
$getColumnElement: (x: number) => HTMLDivElement | undefined;
2632
result: Result;
2733
}) => {
28-
const [isDragging, setIsDragging] = React.useState(false);
34+
const [isDragging, setIsDragging] = useState(false);
2935

3036
return (
3137
<Draggable
@@ -99,11 +105,11 @@ const Kanban = ({
99105
onQuery: () => void;
100106
parentUid: string;
101107
}) => {
102-
const byUid = React.useMemo(
108+
const byUid = useMemo(
103109
() => Object.fromEntries(data.map((d) => [d.uid, d] as const)),
104110
[data]
105111
);
106-
const columnKey = React.useMemo(() => {
112+
const columnKey = useMemo(() => {
107113
const configuredKey = Array.isArray(layout.key)
108114
? layout.key[0]
109115
: typeof layout.key === "string"
@@ -130,7 +136,7 @@ const Kanban = ({
130136
return defaultColumnKey;
131137
}, [layout.key]);
132138
const DEFAULT_FORMAT = `No ${columnKey}`;
133-
const displayKey = React.useMemo(() => {
139+
const displayKey = useMemo(() => {
134140
const configuredDisplay = Array.isArray(layout.display)
135141
? layout.display[0]
136142
: typeof layout.display === "string"
@@ -145,7 +151,7 @@ const Kanban = ({
145151
});
146152
return defaultDisplayKey;
147153
}, [layout.key]);
148-
const [columns, setColumns] = React.useState(() => {
154+
const [columns, setColumns] = useState(() => {
149155
const configuredCols = Array.isArray(layout.columns)
150156
? layout.columns
151157
: typeof layout.columns === "string"
@@ -172,7 +178,7 @@ const Kanban = ({
172178
.slice(0, 3);
173179
});
174180

175-
const [prioritization, setPrioritization] = React.useState(() => {
181+
const [prioritization, setPrioritization] = useState(() => {
176182
const base64 = Array.isArray(layout.prioritization)
177183
? layout.prioritization[0]
178184
: typeof layout.prioritization === "string"
@@ -189,16 +195,16 @@ const Kanban = ({
189195
});
190196
return stored;
191197
});
192-
const layoutUid = React.useMemo(() => {
198+
const layoutUid = useMemo(() => {
193199
return Array.isArray(layout.uid)
194200
? layout.uid[0]
195201
: typeof layout.uid === "string"
196202
? layout.uid
197203
: ""; // should we throw an error here? Should never happen in practice...
198204
}, [layout.uid]);
199-
const [isAdding, setIsAdding] = React.useState(false);
200-
const [newColumn, setNewColumn] = React.useState("");
201-
const cards = React.useMemo(() => {
205+
const [isAdding, setIsAdding] = useState(false);
206+
const [newColumn, setNewColumn] = useState("");
207+
const cards = useMemo(() => {
202208
const cards: Record<string, Result[]> = {};
203209
data.forEach((d) => {
204210
const column =
@@ -219,20 +225,20 @@ const Kanban = ({
219225
});
220226
return cards;
221227
}, [data, prioritization, columnKey]);
222-
const potentialColumns = React.useMemo(() => {
228+
const potentialColumns = useMemo(() => {
223229
const columnSet = new Set(columns);
224230
return Object.keys(cards).filter((c) => !columnSet.has(c));
225231
}, [cards, columns]);
226-
React.useEffect(() => {
232+
useEffect(() => {
227233
const base64 = window.btoa(JSON.stringify(prioritization));
228234
setInputSetting({
229235
blockUid: layoutUid,
230236
key: "prioritization",
231237
value: base64,
232238
});
233239
}, [prioritization]);
234-
const containerRef = React.useRef<HTMLDivElement>(null);
235-
const getColumnElement = React.useCallback(
240+
const containerRef = useRef<HTMLDivElement>(null);
241+
const getColumnElement = useCallback(
236242
(x: number) => {
237243
if (!containerRef.current) return;
238244
const columnEls = Array.from<HTMLDivElement>(
@@ -246,7 +252,7 @@ const Kanban = ({
246252
},
247253
[containerRef]
248254
);
249-
const reprioritizeAndUpdateBlock = React.useCallback<Reprioritize>(
255+
const reprioritizeAndUpdateBlock = useCallback<Reprioritize>(
250256
({ uid, x, y }) => {
251257
if (!containerRef.current) return;
252258
const newColumn = getColumnElement(x);
@@ -306,10 +312,41 @@ const Kanban = ({
306312
},
307313
[setPrioritization, cards, containerRef, byUid, columnKey, parentUid]
308314
);
309-
const showLegend = React.useMemo(
315+
const showLegend = useMemo(
310316
() => (Array.isArray(layout.legend) ? layout.legend[0] : layout.legend),
311317
[layout.legend]
312318
);
319+
const [openedPopoverIndex, setOpenedPopoverIndex] = useState<number | null>(
320+
null
321+
);
322+
323+
const moveColumn = async (
324+
direction: "left" | "right",
325+
columnIndex: number
326+
) => {
327+
const offset = direction === "left" ? -1 : 1;
328+
const newColumns = [...columns];
329+
// Swap elements
330+
[newColumns[columnIndex], newColumns[columnIndex + offset]] = [
331+
newColumns[columnIndex + offset],
332+
newColumns[columnIndex],
333+
];
334+
335+
const columnUid = getSubTree({
336+
key: "columns",
337+
parentUid: layoutUid,
338+
}).uid;
339+
await deleteBlock(columnUid);
340+
341+
setInputSettings({
342+
blockUid: layoutUid,
343+
key: "columns",
344+
values: newColumns,
345+
});
346+
setColumns(newColumns);
347+
setOpenedPopoverIndex(null);
348+
};
349+
313350
return (
314351
<>
315352
{showLegend === "Yes" && (
@@ -334,7 +371,7 @@ const Kanban = ({
334371
className="gap-2 items-start relative roamjs-kanban-container overflow-x-scroll grid w-full"
335372
ref={containerRef}
336373
>
337-
{columns.map((col) => {
374+
{columns.map((col, columnIndex) => {
338375
return (
339376
<div
340377
key={col}
@@ -347,19 +384,55 @@ const Kanban = ({
347384
style={{ display: "flex" }}
348385
>
349386
<span className="font-bold">{col}</span>
350-
<Button
351-
icon={"trash"}
352-
minimal
353-
onClick={() => {
354-
const values = columns.filter((c) => c !== col);
355-
setInputSettings({
356-
blockUid: layout.uid as string,
357-
key: "columns",
358-
values,
359-
});
360-
setColumns(values);
361-
}}
362-
/>
387+
<Popover
388+
autoFocus={false}
389+
interactionKind="hover"
390+
placement="bottom"
391+
isOpen={openedPopoverIndex === columnIndex}
392+
onInteraction={(next) =>
393+
next
394+
? setOpenedPopoverIndex(columnIndex)
395+
: setOpenedPopoverIndex(null)
396+
}
397+
captureDismiss={true}
398+
content={
399+
<>
400+
<Button
401+
className="p-4"
402+
minimal
403+
icon="arrow-left"
404+
disabled={columnIndex === 0}
405+
onClick={() => moveColumn("left", columnIndex)}
406+
/>
407+
<Button
408+
className="p-4"
409+
minimal
410+
icon="arrow-right"
411+
disabled={columnIndex === columns.length - 1}
412+
onClick={() => moveColumn("right", columnIndex)}
413+
/>
414+
<Button
415+
className="p-4"
416+
intent="danger"
417+
minimal
418+
icon="trash"
419+
onClick={() => {
420+
const values = columns.filter((c) => c !== col);
421+
setInputSettings({
422+
blockUid: layout.uid as string,
423+
key: "columns",
424+
values,
425+
});
426+
setColumns(values);
427+
setOpenedPopoverIndex(null);
428+
}}
429+
/>
430+
</>
431+
}
432+
position="bottom-left"
433+
>
434+
<Button icon="more" minimal />
435+
</Popover>
363436
</div>
364437
{(cards[col] || [])?.map((d) => (
365438
<KanbanCard
@@ -397,7 +470,7 @@ const Kanban = ({
397470
onClick={() => {
398471
const values = [...columns, newColumn];
399472
setInputSettings({
400-
blockUid: layout.uid as string,
473+
blockUid: layoutUid,
401474
key: "columns",
402475
values,
403476
});

0 commit comments

Comments
 (0)