chore(collections): add collection toolbar to all collections

This commit is contained in:
Elian Doran 2026-01-31 11:39:48 +02:00
parent 88ba4451eb
commit 1c9f8a2540
No known key found for this signature in database
6 changed files with 133 additions and 113 deletions

View File

@ -1,20 +1,23 @@
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { ViewModeProps } from "../interface";
import "./index.css";
import { ColumnMap, getBoardData } from "./data";
import { createContext, TargetedKeyboardEvent } from "preact";
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import { t } from "../../../services/i18n";
import toast from "../../../services/toast";
import CollectionProperties from "../../note_bars/CollectionProperties";
import FormTextArea from "../../react/FormTextArea";
import FormTextBox from "../../react/FormTextBox";
import { useNoteLabelBoolean, useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
import Icon from "../../react/Icon";
import { t } from "../../../services/i18n";
import Api from "./api";
import FormTextBox from "../../react/FormTextBox";
import { createContext, TargetedKeyboardEvent } from "preact";
import { onWheelHorizontalScroll } from "../../widget_utils";
import Column from "./column";
import BoardApi from "./api";
import FormTextArea from "../../react/FormTextArea";
import FNote from "../../../entities/fnote";
import NoteAutocomplete from "../../react/NoteAutocomplete";
import toast from "../../../services/toast";
import { onWheelHorizontalScroll } from "../../widget_utils";
import { ViewModeProps } from "../interface";
import Api from "./api";
import BoardApi from "./api";
import Column from "./column";
import { ColumnMap, getBoardData } from "./data";
export interface BoardViewData {
columns?: BoardColumnData[];
@ -145,7 +148,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
const insertBefore = mouseX < columnMiddle;
// Calculate the target position
let targetIndex = insertBefore ? index : index + 1;
const targetIndex = insertBefore ? index : index + 1;
setColumnDropPosition(targetIndex);
}, [draggedColumn]);
@ -163,6 +166,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
className="board-view"
onWheel={onWheelHorizontalScroll}
>
<CollectionProperties note={parentNote} />
<BoardViewContext.Provider value={boardViewContext}>
{byColumn && columns && <div
className="board-view-container"
@ -194,7 +198,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
</div>}
</BoardViewContext.Provider>
</div>
)
);
}
function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMode: boolean }) {
@ -218,26 +222,26 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo
tabIndex={300}
>
{!isCreatingNewColumn
? <>
<Icon icon="bx bx-plus" />{" "}
{t("board_view.add-column")}
</>
: (
<TitleEditor
placeholder={t("board_view.add-column-placeholder")}
save={async (columnName) => {
const created = await api.addNewColumn(columnName);
if (!created) {
toast.showMessage(t("board_view.column-already-exists"), undefined, "bx bx-duplicate");
}
}}
dismiss={() => setIsCreatingNewColumn(false)}
isNewItem
mode={isInRelationMode ? "relation" : "normal"}
/>
)}
? <>
<Icon icon="bx bx-plus" />{" "}
{t("board_view.add-column")}
</>
: (
<TitleEditor
placeholder={t("board_view.add-column-placeholder")}
save={async (columnName) => {
const created = await api.addNewColumn(columnName);
if (!created) {
toast.showMessage(t("board_view.column-already-exists"), undefined, "bx bx-duplicate");
}
}}
dismiss={() => setIsCreatingNewColumn(false)}
isNewItem
mode={isInRelationMode ? "relation" : "normal"}
/>
)}
</div>
)
);
}
export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
@ -302,26 +306,26 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, is
onBlur={onBlur}
/>
);
} else {
return (
<NoteAutocomplete
inputRef={inputRef}
noteId={currentValue ?? ""}
opts={{
hideAllButtons: true,
allowCreatingNotes: true
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
dismiss();
}
}}
onBlur={() => dismiss()}
noteIdChanged={(newValue) => {
save(newValue);
dismiss();
}}
/>
);
}
return (
<NoteAutocomplete
inputRef={inputRef}
noteId={currentValue ?? ""}
opts={{
hideAllButtons: true,
allowCreatingNotes: true
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
dismiss();
}
}}
onBlur={() => dismiss()}
noteIdChanged={(newValue) => {
save(newValue);
dismiss();
}}
/>
);
}

View File

@ -1,24 +1,27 @@
import Map from "./map";
import "./index.css";
import { ViewModeProps } from "../interface";
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
import { divIcon, GPXOptions, LatLng, LeafletMouseEvent } from "leaflet";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import Marker, { GpxTrack } from "./marker";
import froca from "../../../services/froca";
import FNote from "../../../entities/fnote";
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../../components/app_context";
import { createNewNote, moveMarker } from "./api";
import openContextMenu, { openMapContextMenu } from "./context_menu";
import toast from "../../../services/toast";
import FNote from "../../../entities/fnote";
import branches from "../../../services/branches";
import froca from "../../../services/froca";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import branches from "../../../services/branches";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSlider } from "../../react/TouchBar";
import toast from "../../../services/toast";
import CollectionProperties from "../../note_bars/CollectionProperties";
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import { ParentComponent } from "../../react/react_utils";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSlider } from "../../react/TouchBar";
import { ViewModeProps } from "../interface";
import { createNewNote, moveMarker } from "./api";
import openContextMenu, { openMapContextMenu } from "./context_menu";
import Map from "./map";
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
import Marker, { GpxTrack } from "./marker";
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
const DEFAULT_ZOOM = 2;
@ -50,7 +53,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
}
}, 5000);
useEffect(() => { froca.getNotes(noteIds).then(setNotes) }, [ noteIds ]);
useEffect(() => { froca.getNotes(noteIds).then(setNotes); }, [ noteIds ]);
useEffect(() => {
if (!note) return;
@ -60,7 +63,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
// Note creation.
useTriliumEvent("geoMapCreateChildNote", () => {
toast.showPersistent({
toast.showPersistent({
icon: "plus",
id: "geo-new-note",
title: "New note",
@ -130,6 +133,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
return (
<div className={`geo-view ${state === State.NewNote ? "placing-note" : ""}`}>
<CollectionProperties note={note} />
{ coordinates !== undefined && zoom !== undefined && <Map
apiRef={apiRef} containerRef={containerRef}
coordinates={coordinates}
@ -204,7 +208,7 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
onDragged={editable ? onDragged : undefined}
onClick={!editable ? onClick : undefined}
onContextMenu={onContextMenu}
/>
/>;
}
function NoteGpxTrack({ note }: { note: FNote }) {
@ -238,7 +242,7 @@ function NoteGpxTrack({ note }: { note: FNote }) {
color: note.getLabelValue("color") ?? "blue"
}
}), [ color, iconClass ]);
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />;
}
function buildIcon(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) {
@ -292,5 +296,5 @@ function GeoMapTouchBar({ state, map }: { state: State, map: L.Map | null | unde
enabled={state === State.Normal}
/>
</TouchBar>
)
);
}

View File

@ -7,6 +7,7 @@ import attribute_renderer from "../../../services/attribute_renderer";
import content_renderer from "../../../services/content_renderer";
import { t } from "../../../services/i18n";
import link from "../../../services/link";
import CollectionProperties from "../../note_bars/CollectionProperties";
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks";
import Icon from "../../react/Icon";
import NoteLink from "../../react/NoteLink";
@ -22,6 +23,8 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
return (
<div class="note-list list-view">
<CollectionProperties note={note} />
{ noteIds.length > 0 && <div class="note-list-wrapper">
<Pager {...pagination} />
@ -48,6 +51,8 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
return (
<div class="note-list grid-view">
<CollectionProperties note={note} />
<div class="note-list-wrapper">
<Pager {...pagination} />

View File

@ -1,18 +1,21 @@
import { ViewModeMedia, ViewModeProps } from "../interface";
import "./index.css";
import { RefObject } from "preact";
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import Reveal from "reveal.js";
import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw";
import slideCustomStylesheet from "./slidejs.css?raw";
import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model";
import ShadowDom from "../../react/ShadowDom";
import ActionButton from "../../react/ActionButton";
import "./index.css";
import { RefObject } from "preact";
import { openInCurrentNoteContext } from "../../../components/note_context";
import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
import { t } from "../../../services/i18n";
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
import FNote from "../../../entities/fnote";
import { t } from "../../../services/i18n";
import CollectionProperties from "../../note_bars/CollectionProperties";
import ActionButton from "../../react/ActionButton";
import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
import ShadowDom from "../../react/ShadowDom";
import { ViewModeMedia, ViewModeProps } from "../interface";
import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model";
import slideCustomStylesheet from "./slidejs.css?raw";
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
export default function PresentationView({ note, noteIds, media, onReady, onProgressChanged }: ViewModeProps<{}>) {
const [ presentation, setPresentation ] = useState<PresentationModel>();
@ -52,13 +55,14 @@ export default function PresentationView({ note, noteIds, media, onReady, onProg
if (media === "screen") {
return (
<>
<CollectionProperties note={note} />
<ShadowDom
className="presentation-container"
containerRef={containerRef}
>{content}</ShadowDom>
<ButtonOverlay containerRef={containerRef} api={api} />
</>
)
);
} else if (media === "print") {
// Printing needs a query parameter that is read by Reveal.js.
const url = new URL(window.location.href);
@ -143,7 +147,7 @@ function ButtonOverlay({ containerRef, api }: { containerRef: RefObject<HTMLDivE
/>
</div>
</div>
)
);
}
function Presentation({ presentation, setApi } : { presentation: PresentationModel, setApi: (api: Reveal.Api | undefined) => void }) {
@ -179,7 +183,7 @@ function Presentation({ presentation, setApi } : { presentation: PresentationMod
api.destroy();
setRevealApi(undefined);
setApi(undefined);
}
};
}, []);
useEffect(() => {
@ -191,19 +195,19 @@ function Presentation({ presentation, setApi } : { presentation: PresentationMod
<div className="slides">
{presentation.slides?.map(slide => {
if (!slide.verticalSlides) {
return <Slide key={slide.noteId} slide={slide} />
} else {
return (
<section>
<Slide key={slide.noteId} slide={slide} />
{slide.verticalSlides.map(slide => <Slide key={slide.noteId} slide={slide} /> )}
</section>
);
return <Slide key={slide.noteId} slide={slide} />;
}
return (
<section>
<Slide key={slide.noteId} slide={slide} />
{slide.verticalSlides.map(slide => <Slide key={slide.noteId} slide={slide} /> )}
</section>
);
})}
</div>
</div>
)
);
}

View File

@ -1,20 +1,23 @@
import "./index.css";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { ViewModeProps } from "../interface";
import { TableData } from "./rows";
import { useLegacyWidget } from "../../react/hooks";
import Tabulator from "./tabulator";
import { Tabulator as VanillaTabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule, Options, RowComponent} from 'tabulator-tables';
import { useContextMenu } from "./context_menu";
import { ParentComponent } from "../../react/react_utils";
import { DataTreeModule, EditModule, FormatModule, FrozenColumnsModule, InteractionModule, MoveColumnsModule, MoveRowsModule, Options, PersistenceModule, ResizeColumnsModule, RowComponent,SortModule, Tabulator as VanillaTabulator} from 'tabulator-tables';
import FNote from "../../../entities/fnote";
import { t } from "../../../services/i18n";
import Button from "../../react/Button";
import "./index.css";
import useRowTableEditing from "./row_editing";
import useColTableEditing from "./col_editing";
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
import SpacedUpdate from "../../../services/spaced_update";
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
import CollectionProperties from "../../note_bars/CollectionProperties";
import Button from "../../react/Button";
import { useLegacyWidget } from "../../react/hooks";
import { ParentComponent } from "../../react/react_utils";
import { ViewModeProps } from "../interface";
import useColTableEditing from "./col_editing";
import { useContextMenu } from "./context_menu";
import useData, { TableConfig } from "./data";
import useRowTableEditing from "./row_editing";
import { TableData } from "./rows";
import Tabulator from "./tabulator";
export default function TableView({ note, noteIds, notePath, viewConfig, saveConfig }: ViewModeProps<TableConfig>) {
const tabulatorRef = useRef<VanillaTabulator>(null);
@ -36,7 +39,7 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
dataTreeChildIndent: 20,
dataTreeExpandElement: `<button class="tree-expand"><span class="bx bx-chevron-right"></span></button>`,
dataTreeCollapseElement: `<button class="tree-collapse"><span class="bx bx-chevron-down"></span></button>`
}
};
}, [ hasChildren ]);
const rowFormatter = useCallback((row: RowComponent) => {
@ -46,6 +49,8 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
return (
<div className="table-view">
<CollectionProperties note={note} />
{rowData !== undefined && persistenceProps && (
<>
<Tabulator
@ -72,7 +77,7 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
)}
{attributeDetailWidgetEl}
</div>
)
);
}
function TableFooter({ note }: { note: FNote }) {
@ -84,7 +89,7 @@ function TableFooter({ note }: { note: FNote }) {
<Button triggerCommand="addNewTableColumn" icon="bx bx-carousel" text={t("table_view.new-column")} />
</div>
</div>
)
);
}
function usePersistence(viewConfig: TableConfig | null | undefined, saveConfig: (newConfig: TableConfig) => void) {

View File

@ -30,12 +30,10 @@
@media (max-width: 991px) {
flex-wrap: wrap;
padding: 0.55em 1em;
margin: 0;
margin-bottom: 0.5em;
>div {
flex-grow: 1;
justify-content: center;
}
}
}