mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
Merge branch 'react/collections' of https://github.com/TriliumNext/trilium into react/collections
This commit is contained in:
commit
3ddcaddd79
@ -8,6 +8,7 @@ import GeoView from "./geomap";
|
||||
import ViewModeStorage from "../view_widgets/view_mode_storage";
|
||||
import CalendarView from "./calendar";
|
||||
import TableView from "./table";
|
||||
import BoardView from "./board";
|
||||
|
||||
interface NoteListProps<T extends object> {
|
||||
note?: FNote | null;
|
||||
@ -88,6 +89,8 @@ function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps<
|
||||
return <CalendarView {...props} />
|
||||
case "table":
|
||||
return <TableView {...props} />
|
||||
case "board":
|
||||
return <BoardView {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
|
31
apps/client/src/widgets/collections/board/api.ts
Normal file
31
apps/client/src/widgets/collections/board/api.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import FNote from "../../../entities/fnote";
|
||||
import attributes from "../../../services/attributes";
|
||||
import note_create from "../../../services/note_create";
|
||||
|
||||
export async function createNewItem(parentNote: FNote, column: string) {
|
||||
try {
|
||||
// Get the parent note path
|
||||
const parentNotePath = parentNote.noteId;
|
||||
const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status";
|
||||
|
||||
// Create a new note as a child of the parent note
|
||||
const { note: newNote } = await note_create.createNote(parentNotePath, {
|
||||
activate: false,
|
||||
title: "New item"
|
||||
});
|
||||
|
||||
if (newNote) {
|
||||
// Set the status label to place it in the correct column
|
||||
await changeColumn(newNote.noteId, column, statusAttribute);
|
||||
|
||||
// Start inline editing of the newly created card
|
||||
//this.startInlineEditingCard(newNote.noteId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create new item:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function changeColumn(noteId: string, newColumn: string, statusAttribute: string) {
|
||||
await attributes.setLabel(noteId, statusAttribute, newColumn);
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
import FBranch from "../../../entities/fbranch";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { BoardData } from "./config";
|
||||
import { BoardViewData } from "./index";
|
||||
|
||||
export type ColumnMap = Map<string, {
|
||||
branch: FBranch;
|
||||
note: FNote;
|
||||
}[]>;
|
||||
|
||||
export async function getBoardData(parentNote: FNote, groupByColumn: string, persistedData: BoardData) {
|
||||
export async function getBoardData(parentNote: FNote, groupByColumn: string, persistedData: BoardViewData) {
|
||||
const byColumn: ColumnMap = new Map();
|
||||
|
||||
// First, scan all notes to find what columns actually exist
|
||||
@ -43,7 +43,7 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
|
||||
}
|
||||
|
||||
// Return updated persisted data only if there were changes
|
||||
let newPersistedData: BoardData | undefined;
|
||||
let newPersistedData: BoardViewData | undefined;
|
||||
const hasChanges = newColumnValues.length > 0 ||
|
||||
existingPersistedColumns.length !== deduplicatedColumns.length ||
|
||||
!existingPersistedColumns.every((col, idx) => deduplicatedColumns[idx]?.value === col.value);
|
||||
@ -65,6 +65,7 @@ async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupB
|
||||
for (const branch of branches) {
|
||||
const note = await branch.getNote();
|
||||
if (!note) {
|
||||
console.warn("Not note found");
|
||||
continue;
|
||||
}
|
||||
|
276
apps/client/src/widgets/collections/board/index.css
Normal file
276
apps/client/src/widgets/collections/board/index.css
Normal file
@ -0,0 +1,276 @@
|
||||
.board-view {
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.board-view-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
padding: 1em;
|
||||
padding-bottom: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.board-view-container .board-column {
|
||||
width: 250px;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em;
|
||||
background-color: var(--accented-background-color);
|
||||
transition: border-color 0.2s ease;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.board-view-container .board-column.drag-over {
|
||||
border-color: var(--main-text-color);
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3 {
|
||||
font-size: 1em;
|
||||
margin-bottom: 0.75em;
|
||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
cursor: grab;
|
||||
position: relative;
|
||||
transition: background-color 0.2s ease, border-radius 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3.editing {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3.editing {
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-text-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.board-view-container .board-column.column-dragging {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.98);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3 input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3 .edit-icon {
|
||||
opacity: 0;
|
||||
margin-left: 0.5em;
|
||||
transition: opacity 0.2s ease;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3:hover .edit-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3.editing .edit-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.board-view-container .board-note {
|
||||
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.25);
|
||||
margin: 0.65em 0;
|
||||
padding: 0.5em;
|
||||
border-radius: 5px;
|
||||
cursor: move;
|
||||
position: relative;
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.15s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.board-view-container .board-note.fade-in {
|
||||
animation: fadeIn 0.15s ease-in;
|
||||
}
|
||||
|
||||
.board-view-container .board-note.fade-out {
|
||||
animation: fadeOut 0.15s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.board-view-container .board-note.card-updated {
|
||||
animation: cardUpdate 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes cardUpdate {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.board-view-container .board-note:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.board-view-container .board-note.dragging {
|
||||
opacity: 0.8;
|
||||
transform: rotate(5deg);
|
||||
z-index: 1000;
|
||||
box-shadow: 4px 8px 16px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.board-view-container .board-note.editing {
|
||||
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.35);
|
||||
border-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.board-view-container .board-note.editing input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.board-view-container .board-note .icon {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.board-drop-indicator {
|
||||
height: 3px;
|
||||
background-color: var(--main-text-color);
|
||||
border-radius: 2px;
|
||||
margin: 0.25em 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.board-drop-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.column-drop-indicator {
|
||||
width: 4px;
|
||||
background-color: var(--main-text-color);
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.column-drop-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.board-new-item {
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
border-radius: 5px;
|
||||
color: var(--muted-text-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.board-new-item:hover {
|
||||
border-color: var(--main-text-color);
|
||||
color: var(--main-text-color);
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.board-new-item .icon {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.board-add-column {
|
||||
width: 180px;
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em;
|
||||
background-color: var(--accented-background-color);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--muted-text-color);
|
||||
font-size: 0.9em;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.board-add-column:hover {
|
||||
border-color: var(--main-text-color);
|
||||
color: var(--main-text-color);
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.board-add-column .icon {
|
||||
margin-right: 0.5em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.board-add-column input {
|
||||
background: var(--main-background-color);
|
||||
border: 1px solid var(--main-text-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.5em;
|
||||
color: var(--main-text-color);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.board-drag-preview {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
transform: rotate(5deg);
|
||||
box-shadow: 4px 8px 16px rgba(0, 0, 0, 0.5);
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
font-size: 0.9em;
|
||||
max-width: 200px;
|
||||
word-wrap: break-word;
|
||||
}
|
177
apps/client/src/widgets/collections/board/index.tsx
Normal file
177
apps/client/src/widgets/collections/board/index.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import "./index.css";
|
||||
import { ColumnMap, getBoardData } from "./data";
|
||||
import { useNoteLabel, useTriliumEvent } from "../../react/hooks";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import FBranch from "../../../entities/fbranch";
|
||||
import Icon from "../../react/Icon";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { createNewItem } from "./api";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
|
||||
export interface BoardViewData {
|
||||
columns?: BoardColumnData[];
|
||||
}
|
||||
|
||||
export interface BoardColumnData {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default function BoardView({ note: parentNote, noteIds, viewConfig, saveConfig }: ViewModeProps<BoardViewData>) {
|
||||
const [ statusAttribute ] = useNoteLabel(parentNote, "board:groupBy");
|
||||
const [ byColumn, setByColumn ] = useState<ColumnMap>();
|
||||
const [ columns, setColumns ] = useState<string[]>();
|
||||
|
||||
function refresh() {
|
||||
getBoardData(parentNote, statusAttribute ?? "status", viewConfig ?? {}).then(({ byColumn, newPersistedData }) => {
|
||||
setByColumn(byColumn);
|
||||
|
||||
if (newPersistedData) {
|
||||
viewConfig = { ...newPersistedData };
|
||||
saveConfig(newPersistedData);
|
||||
}
|
||||
|
||||
// Use the order from persistedData.columns, then add any new columns found
|
||||
const orderedColumns = viewConfig?.columns?.map(col => col.value) || [];
|
||||
const allColumns = Array.from(byColumn.keys());
|
||||
const newColumns = allColumns.filter(col => !orderedColumns.includes(col));
|
||||
setColumns([...orderedColumns, ...newColumns]);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(refresh, [ parentNote, noteIds ]);
|
||||
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
// TODO: Re-enable
|
||||
return;
|
||||
|
||||
// Check if any changes affect our board
|
||||
const hasRelevantChanges =
|
||||
// React to changes in status attribute for notes in this board
|
||||
loadResults.getAttributeRows().some(attr => attr.name === statusAttribute && noteIds.includes(attr.noteId!)) ||
|
||||
// React to changes in note title
|
||||
loadResults.getNoteIds().some(noteId => noteIds.includes(noteId)) ||
|
||||
// React to changes in branches for subchildren (e.g., moved, added, or removed notes)
|
||||
loadResults.getBranchRows().some(branch => noteIds.includes(branch.noteId!)) ||
|
||||
// React to changes in note icon or color.
|
||||
loadResults.getAttributeRows().some(attr => [ "iconClass", "color" ].includes(attr.name ?? "") && noteIds.includes(attr.noteId ?? "")) ||
|
||||
// React to attachment change
|
||||
loadResults.getAttachmentRows().some(att => att.ownerId === parentNote.noteId && att.title === "board.json") ||
|
||||
// React to changes in "groupBy"
|
||||
loadResults.getAttributeRows().some(attr => attr.name === "board:groupBy" && attr.noteId === parentNote.noteId);
|
||||
|
||||
if (hasRelevantChanges) {
|
||||
console.log("Trigger refresh");
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="board-view">
|
||||
<div className="board-view-container">
|
||||
{byColumn && columns?.map(column => (
|
||||
<Column
|
||||
column={column}
|
||||
columnItems={byColumn.get(column)}
|
||||
parentNote={parentNote}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AddNewColumn viewConfig={viewConfig} saveConfig={saveConfig} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Column({ parentNote, column, columnItems }: { parentNote: FNote, column: string, columnItems?: { note: FNote, branch: FBranch }[] }) {
|
||||
return (
|
||||
<div className="board-column">
|
||||
<h3>
|
||||
<span>{column}</span>
|
||||
<span
|
||||
className="edit-icon icon bx bx-edit-alt"
|
||||
title="Click to edit column title" />
|
||||
</h3>
|
||||
|
||||
{(columnItems ?? []).map(({ note, branch }) => (
|
||||
<Card note={note} branch={branch} column={column} />
|
||||
))}
|
||||
|
||||
<div className="board-new-item" onClick={() => createNewItem(parentNote, column)}>
|
||||
<Icon icon="bx bx-plus" />{" "}
|
||||
{t("board_view.new-item")}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Card({ note }: { note: FNote, branch: FBranch, column: string }) {
|
||||
const colorClass = note.getColorClass() || '';
|
||||
|
||||
return (
|
||||
<div className={`board-note ${colorClass}`}>
|
||||
<span class={`icon ${note.getIcon()}`} />
|
||||
{note.title}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AddNewColumn({ viewConfig, saveConfig }: { viewConfig?: BoardViewData, saveConfig: (data: BoardViewData) => void }) {
|
||||
const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false);
|
||||
const columnNameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const addColumnCallback = useCallback(() => {
|
||||
setIsCreatingNewColumn(true);
|
||||
}, []);
|
||||
|
||||
const finishEdit = useCallback((save: boolean) => {
|
||||
const columnName = columnNameRef.current?.value;
|
||||
if (!columnName || !save) {
|
||||
setIsCreatingNewColumn(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the new column to persisted data if it doesn't exist
|
||||
if (!viewConfig) {
|
||||
viewConfig = {};
|
||||
}
|
||||
|
||||
if (!viewConfig.columns) {
|
||||
viewConfig.columns = [];
|
||||
}
|
||||
|
||||
const existingColumn = viewConfig.columns.find(col => col.value === columnName);
|
||||
if (!existingColumn) {
|
||||
viewConfig.columns.push({ value: columnName });
|
||||
saveConfig(viewConfig);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`board-add-column ${isCreatingNewColumn ? "editing" : ""}`} onClick={addColumnCallback}>
|
||||
{!isCreatingNewColumn
|
||||
? <>
|
||||
<Icon icon="bx bx-plus" />{" "}
|
||||
{t("board_view.add-column")}
|
||||
</>
|
||||
: <>
|
||||
<FormTextBox
|
||||
inputRef={columnNameRef}
|
||||
type="text"
|
||||
placeholder="Enter column name..."
|
||||
onBlur={() => finishEdit(true)}
|
||||
onKeyDown={(e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
finishEdit(true);
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
finishEdit(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -29,10 +29,6 @@ export default class BoardApi {
|
||||
return this.byColumn.get(column);
|
||||
}
|
||||
|
||||
async changeColumn(noteId: string, newColumn: string) {
|
||||
await attributes.setLabel(noteId, this._statusAttribute, newColumn);
|
||||
}
|
||||
|
||||
openNote(noteId: string) {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: noteId });
|
||||
}
|
||||
@ -95,21 +91,6 @@ export default class BoardApi {
|
||||
this.viewStorage.store(this.persistedData);
|
||||
}
|
||||
|
||||
async createColumn(columnValue: string) {
|
||||
// Add the new column to persisted data if it doesn't exist
|
||||
if (!this.persistedData.columns) {
|
||||
this.persistedData.columns = [];
|
||||
}
|
||||
|
||||
const existingColumn = this.persistedData.columns.find(col => col.value === columnValue);
|
||||
if (!existingColumn) {
|
||||
this.persistedData.columns.push({ value: columnValue });
|
||||
await this.viewStorage.store(this.persistedData);
|
||||
}
|
||||
|
||||
return columnValue;
|
||||
}
|
||||
|
||||
async reorderColumns(newColumnOrder: string[]) {
|
||||
// Update the column order in persisted data
|
||||
if (!this.persistedData.columns) {
|
||||
@ -135,16 +116,13 @@ export default class BoardApi {
|
||||
|
||||
async refresh(parentNote: FNote) {
|
||||
// Refresh the API data by re-fetching from the parent note
|
||||
const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status";
|
||||
this._statusAttribute = statusAttribute;
|
||||
|
||||
// Use the current in-memory persisted data instead of restoring from storage
|
||||
// This ensures we don't lose recent updates like column renames
|
||||
const { byColumn, newPersistedData } = await getBoardData(parentNote, statusAttribute, this.persistedData);
|
||||
|
||||
|
||||
// Update internal state
|
||||
this.byColumn = byColumn;
|
||||
|
||||
|
||||
if (newPersistedData) {
|
||||
this.persistedData = newPersistedData;
|
||||
this.viewStorage.store(this.persistedData);
|
||||
@ -161,18 +139,6 @@ export default class BoardApi {
|
||||
const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status";
|
||||
|
||||
let persistedData = await viewStorage.restore() ?? {};
|
||||
const { byColumn, newPersistedData } = await getBoardData(parentNote, statusAttribute, persistedData);
|
||||
|
||||
// Use the order from persistedData.columns, then add any new columns found
|
||||
const orderedColumns = persistedData.columns?.map(col => col.value) || [];
|
||||
const allColumns = Array.from(byColumn.keys());
|
||||
const newColumns = allColumns.filter(col => !orderedColumns.includes(col));
|
||||
const columns = [...orderedColumns, ...newColumns];
|
||||
|
||||
if (newPersistedData) {
|
||||
persistedData = newPersistedData;
|
||||
viewStorage.store(persistedData);
|
||||
}
|
||||
|
||||
return new BoardApi(columns, parentNote.noteId, viewStorage, byColumn, persistedData, statusAttribute);
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
export interface BoardColumnData {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface BoardData {
|
||||
columns?: BoardColumnData[];
|
||||
}
|
@ -95,8 +95,6 @@ export class DifferentialBoardRenderer {
|
||||
const $columnEl = this.createColumn(column, columnItems);
|
||||
this.$container.append($columnEl);
|
||||
}
|
||||
|
||||
this.addAddColumnButton();
|
||||
}
|
||||
|
||||
private async differentialRender(oldState: BoardState, newState: BoardState): Promise<void> {
|
||||
@ -329,24 +327,6 @@ export class DifferentialBoardRenderer {
|
||||
}
|
||||
|
||||
private createColumn(column: string, columnItems: { note: any; branch: any }[]): JQuery<HTMLElement> {
|
||||
const $columnEl = $("<div>")
|
||||
.addClass("board-column")
|
||||
.attr("data-column", column);
|
||||
|
||||
// Create header
|
||||
const $titleEl = $("<h3>").attr("data-column-value", column);
|
||||
|
||||
// Create title text
|
||||
const $titleText = $("<span>").text(column);
|
||||
|
||||
// Create edit icon
|
||||
const $editIcon = $("<span>")
|
||||
.addClass("edit-icon icon bx bx-edit-alt")
|
||||
.attr("title", "Click to edit column title");
|
||||
|
||||
$titleEl.append($titleText, $editIcon);
|
||||
$columnEl.append($titleEl);
|
||||
|
||||
// Setup column dragging
|
||||
this.dragHandler.setupColumnDrag($columnEl, column);
|
||||
|
||||
@ -363,47 +343,18 @@ export class DifferentialBoardRenderer {
|
||||
this.dragHandler.setupNoteDropZone($columnEl, column);
|
||||
this.dragHandler.setupColumnDropZone($columnEl);
|
||||
|
||||
// Add cards
|
||||
for (const item of columnItems) {
|
||||
if (item.note) {
|
||||
const $noteEl = this.createCard(item.note, item.branch, column);
|
||||
$columnEl.append($noteEl);
|
||||
}
|
||||
}
|
||||
|
||||
// Add "New item" button
|
||||
const $newItemEl = $("<div>")
|
||||
.addClass("board-new-item")
|
||||
.attr("data-column", column)
|
||||
.html(`<span class="icon bx bx-plus"></span> ${t("board_view.new-item")}`);
|
||||
.html(`<span class="icon bx bx-plus"></span> ${}`);
|
||||
|
||||
$newItemEl.on("click", () => this.onCreateNewItem(column));
|
||||
$columnEl.append($newItemEl);
|
||||
|
||||
return $columnEl;
|
||||
}
|
||||
|
||||
private createCard(note: any, branch: any, column: string): JQuery<HTMLElement> {
|
||||
const $iconEl = $("<span>")
|
||||
.addClass("icon")
|
||||
.addClass(note.getIcon());
|
||||
|
||||
const colorClass = note.getColorClass() || '';
|
||||
|
||||
const $noteEl = $("<div>")
|
||||
.addClass("board-note")
|
||||
.attr("data-note-id", note.noteId)
|
||||
.attr("data-branch-id", branch.branchId)
|
||||
.attr("data-current-column", column)
|
||||
.attr("data-icon-class", note.getIcon())
|
||||
.attr("data-color-class", colorClass)
|
||||
.text(note.title);
|
||||
|
||||
// Add color class to the card if it exists
|
||||
if (colorClass) {
|
||||
$noteEl.addClass(colorClass);
|
||||
}
|
||||
|
||||
$noteEl.prepend($iconEl);
|
||||
$noteEl.on("click", () => appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId }));
|
||||
|
||||
@ -413,16 +364,6 @@ export class DifferentialBoardRenderer {
|
||||
return $noteEl;
|
||||
}
|
||||
|
||||
private addAddColumnButton(): void {
|
||||
if (this.$container.find('.board-add-column').length === 0) {
|
||||
const $addColumnEl = $("<div>")
|
||||
.addClass("board-add-column")
|
||||
.html(`<span class="icon bx bx-plus"></span> ${t("board_view.add-column")}`);
|
||||
|
||||
this.$container.append($addColumnEl);
|
||||
}
|
||||
}
|
||||
|
||||
forceFullRender(): void {
|
||||
this.lastState = null;
|
||||
if (this.updateTimeout) {
|
||||
|
@ -9,279 +9,6 @@ import BoardApi from "./api";
|
||||
import { BoardDragHandler, DragContext } from "./drag_handler";
|
||||
import { DifferentialBoardRenderer } from "./differential_renderer";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="board-view">
|
||||
<style>
|
||||
.board-view {
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.board-view-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
padding: 1em;
|
||||
padding-bottom: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.board-view-container .board-column {
|
||||
width: 250px;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em;
|
||||
background-color: var(--accented-background-color);
|
||||
transition: border-color 0.2s ease;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.board-view-container .board-column.drag-over {
|
||||
border-color: var(--main-text-color);
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3 {
|
||||
font-size: 1em;
|
||||
margin-bottom: 0.75em;
|
||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
cursor: grab;
|
||||
position: relative;
|
||||
transition: background-color 0.2s ease, border-radius 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3.editing {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3:hover {
|
||||
background-color: var(--hover-item-background-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3.editing {
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-text-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.board-view-container .board-column.column-dragging {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.98);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3 input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3 .edit-icon {
|
||||
opacity: 0;
|
||||
margin-left: 0.5em;
|
||||
transition: opacity 0.2s ease;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3:hover .edit-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.board-view-container .board-column h3.editing .edit-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.board-view-container .board-note {
|
||||
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.25);
|
||||
margin: 0.65em 0;
|
||||
padding: 0.5em;
|
||||
border-radius: 5px;
|
||||
cursor: move;
|
||||
position: relative;
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.15s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.board-view-container .board-note.fade-in {
|
||||
animation: fadeIn 0.15s ease-in;
|
||||
}
|
||||
|
||||
.board-view-container .board-note.fade-out {
|
||||
animation: fadeOut 0.15s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.board-view-container .board-note.card-updated {
|
||||
animation: cardUpdate 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes cardUpdate {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.board-view-container .board-note:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.board-view-container .board-note.dragging {
|
||||
opacity: 0.8;
|
||||
transform: rotate(5deg);
|
||||
z-index: 1000;
|
||||
box-shadow: 4px 8px 16px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.board-view-container .board-note.editing {
|
||||
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.35);
|
||||
border-color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.board-view-container .board-note.editing input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.board-view-container .board-note .icon {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.board-drop-indicator {
|
||||
height: 3px;
|
||||
background-color: var(--main-text-color);
|
||||
border-radius: 2px;
|
||||
margin: 0.25em 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.board-drop-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.column-drop-indicator {
|
||||
width: 4px;
|
||||
background-color: var(--main-text-color);
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.column-drop-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.board-new-item {
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
border-radius: 5px;
|
||||
color: var(--muted-text-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.board-new-item:hover {
|
||||
border-color: var(--main-text-color);
|
||||
color: var(--main-text-color);
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.board-new-item .icon {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.board-add-column {
|
||||
width: 180px;
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em;
|
||||
background-color: var(--accented-background-color);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--muted-text-color);
|
||||
font-size: 0.9em;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.board-add-column:hover {
|
||||
border-color: var(--main-text-color);
|
||||
color: var(--main-text-color);
|
||||
background-color: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.board-add-column .icon {
|
||||
margin-right: 0.5em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.board-drag-preview {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
transform: rotate(5deg);
|
||||
box-shadow: 4px 8px 16px rgba(0, 0, 0, 0.5);
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
font-size: 0.9em;
|
||||
max-width: 200px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="board-view-container"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class BoardView extends ViewMode<BoardData> {
|
||||
|
||||
private $root: JQuery<HTMLElement>;
|
||||
@ -337,7 +64,6 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
this.$container,
|
||||
this.api,
|
||||
this.dragHandler,
|
||||
(column: string) => this.createNewItem(column),
|
||||
this.parentNote,
|
||||
this.viewStorage,
|
||||
() => this.refreshApi()
|
||||
@ -493,32 +219,6 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
}
|
||||
}
|
||||
|
||||
private async createNewItem(column: string) {
|
||||
try {
|
||||
// Get the parent note path
|
||||
const parentNotePath = this.parentNote.noteId;
|
||||
|
||||
// Create a new note as a child of the parent note
|
||||
const { note: newNote } = await noteCreateService.createNote(parentNotePath, {
|
||||
activate: false,
|
||||
title: "New item"
|
||||
});
|
||||
|
||||
if (newNote) {
|
||||
// Set the status label to place it in the correct column
|
||||
await this.api?.changeColumn(newNote.noteId, column);
|
||||
|
||||
// Refresh the board to show the new item
|
||||
await this.renderList();
|
||||
|
||||
// Start inline editing of the newly created card
|
||||
this.startInlineEditingCard(newNote.noteId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create new item:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async insertItemAtPosition(column: string, relativeToBranchId: string, direction: "before" | "after"): Promise<void> {
|
||||
try {
|
||||
// Create the note without opening it
|
||||
@ -546,27 +246,6 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
}
|
||||
|
||||
private startCreatingNewColumn($addColumnEl: JQuery<HTMLElement>) {
|
||||
if ($addColumnEl.hasClass("editing")) {
|
||||
return; // Already editing
|
||||
}
|
||||
|
||||
$addColumnEl.addClass("editing");
|
||||
|
||||
const $input = $("<input>")
|
||||
.attr("type", "text")
|
||||
.attr("placeholder", "Enter column name...")
|
||||
.css({
|
||||
background: "var(--main-background-color)",
|
||||
border: "1px solid var(--main-text-color)",
|
||||
borderRadius: "4px",
|
||||
padding: "0.5em",
|
||||
color: "var(--main-text-color)",
|
||||
fontFamily: "inherit",
|
||||
fontSize: "inherit",
|
||||
width: "100%",
|
||||
textAlign: "center"
|
||||
});
|
||||
|
||||
$addColumnEl.empty().append($input);
|
||||
$input.focus();
|
||||
|
||||
@ -583,21 +262,7 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
await this.createNewColumn(columnName.trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the add button
|
||||
$addColumnEl.html('<span class="icon bx bx-plus"></span>Add Column');
|
||||
};
|
||||
|
||||
$input.on("blur", () => finishEdit(true));
|
||||
$input.on("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
finishEdit(true);
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
finishEdit(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async createNewColumn(columnName: string) {
|
||||
@ -618,31 +283,6 @@ export default class BoardView extends ViewMode<BoardData> {
|
||||
}
|
||||
}
|
||||
|
||||
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// Check if any changes affect our board
|
||||
const hasRelevantChanges =
|
||||
// React to changes in status attribute for notes in this board
|
||||
loadResults.getAttributeRows().some(attr => attr.name === this.api?.statusAttribute && this.noteIds.includes(attr.noteId!)) ||
|
||||
// React to changes in note title
|
||||
loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId)) ||
|
||||
// React to changes in branches for subchildren (e.g., moved, added, or removed notes)
|
||||
loadResults.getBranchRows().some(branch => this.noteIds.includes(branch.noteId!)) ||
|
||||
// React to changes in note icon or color.
|
||||
loadResults.getAttributeRows().some(attr => [ "iconClass", "color" ].includes(attr.name ?? "") && this.noteIds.includes(attr.noteId ?? "")) ||
|
||||
// React to attachment change
|
||||
loadResults.getAttachmentRows().some(att => att.ownerId === this.parentNote.noteId && att.title === "board.json") ||
|
||||
// React to changes in "groupBy"
|
||||
loadResults.getAttributeRows().some(attr => attr.name === "board:groupBy" && attr.noteId === this.parentNote.noteId);
|
||||
|
||||
if (hasRelevantChanges && this.renderer) {
|
||||
// Use differential rendering with API refresh
|
||||
await this.renderer.renderBoard(true);
|
||||
}
|
||||
|
||||
// Don't trigger full view refresh - let differential renderer handle it
|
||||
return false;
|
||||
}
|
||||
|
||||
private onSave() {
|
||||
this.viewStorage.store(this.persistentData);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user