mirror of
https://github.com/zadam/trilium.git
synced 2026-03-22 08:13:46 +01:00
Univer Sheets v0 (#8902)
This commit is contained in:
commit
2d00ac4dfb
@ -35,6 +35,8 @@
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"@univerjs/preset-sheets-core": "0.16.1",
|
||||
"@univerjs/presets": "0.16.1",
|
||||
"@zumer/snapdom": "2.0.2",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
|
||||
@ -18,7 +18,7 @@ const RELATION = "relation";
|
||||
* end user. Those types should be used only for checking against, they are
|
||||
* not for direct use.
|
||||
*/
|
||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap";
|
||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet";
|
||||
|
||||
export interface NotePathRecord {
|
||||
isArchived: boolean;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
|
||||
import FNote from "../entities/fnote";
|
||||
import { ViewTypeOptions } from "../widgets/collections/interface";
|
||||
|
||||
@ -17,7 +18,8 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
render: null,
|
||||
search: null,
|
||||
text: null,
|
||||
webView: null
|
||||
webView: null,
|
||||
spreadsheet: null
|
||||
};
|
||||
|
||||
export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
@ -38,6 +40,6 @@ export function getHelpUrlForNote(note: FNote | null | undefined) {
|
||||
} else if (note?.hasLabel("textSnippet")) {
|
||||
return "pwc194wlRzcH";
|
||||
} else if (note && note.type === "book") {
|
||||
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
|
||||
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { t } from "./i18n.js";
|
||||
import froca from "./froca.js";
|
||||
import server from "./server.js";
|
||||
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
|
||||
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
|
||||
import froca from "./froca.js";
|
||||
import { t } from "./i18n.js";
|
||||
import server from "./server.js";
|
||||
|
||||
export interface NoteTypeMapping {
|
||||
type: NoteType;
|
||||
@ -26,6 +26,7 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
|
||||
|
||||
// The default note type (always the first item)
|
||||
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
|
||||
{ type: "spreadsheet", mime: "application/json", title: t("note_types.spreadsheet"), icon: "bx-table", isBeta: true },
|
||||
|
||||
// Text notes group
|
||||
{ type: "book", mime: "", title: t("note_types.book"), icon: "bx-book" },
|
||||
@ -96,9 +97,9 @@ function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandName
|
||||
title: nt.title,
|
||||
command,
|
||||
type: nt.type,
|
||||
uiIcon: "bx " + nt.icon,
|
||||
uiIcon: `bx ${nt.icon}`,
|
||||
badges: []
|
||||
}
|
||||
};
|
||||
|
||||
if (nt.isNew) {
|
||||
menuItem.badges?.push(NEW_BADGE);
|
||||
@ -130,7 +131,7 @@ async function getUserTemplates(command?: TreeCommandNames) {
|
||||
const item: MenuItem<TreeCommandNames> = {
|
||||
title: templateNote.title,
|
||||
uiIcon: templateNote.getIcon(),
|
||||
command: command,
|
||||
command,
|
||||
type: templateNote.type,
|
||||
templateNoteId: templateNote.noteId
|
||||
};
|
||||
@ -159,7 +160,7 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
|
||||
const items: MenuItem<TreeCommandNames>[] = [];
|
||||
if (title) {
|
||||
items.push({
|
||||
title: title,
|
||||
title,
|
||||
kind: "header"
|
||||
});
|
||||
} else {
|
||||
@ -175,7 +176,7 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
|
||||
const item: MenuItem<TreeCommandNames> = {
|
||||
title: templateNote.title,
|
||||
uiIcon: templateNote.getIcon(),
|
||||
command: command,
|
||||
command,
|
||||
type: templateNote.type,
|
||||
templateNoteId: templateNote.noteId
|
||||
};
|
||||
@ -193,7 +194,7 @@ async function isNewTemplate(templateNoteId) {
|
||||
if (rootCreationDate === undefined) {
|
||||
// Retrieve the root note creation date
|
||||
try {
|
||||
let rootNoteInfo: any = await server.get("notes/root");
|
||||
const rootNoteInfo: any = await server.get("notes/root");
|
||||
if ("dateCreated" in rootNoteInfo) {
|
||||
rootCreationDate = new Date(rootNoteInfo.dateCreated);
|
||||
}
|
||||
@ -208,7 +209,7 @@ async function isNewTemplate(templateNoteId) {
|
||||
if (creationDate === undefined) {
|
||||
// The creation date isn't available in the cache, try to retrieve it from the server
|
||||
try {
|
||||
const noteInfo: any = await server.get("notes/" + templateNoteId);
|
||||
const noteInfo: any = await server.get(`notes/${templateNoteId}`);
|
||||
if ("dateCreated" in noteInfo) {
|
||||
creationDate = new Date(noteInfo.dateCreated);
|
||||
creationDateCache.set(templateNoteId, creationDate);
|
||||
@ -230,9 +231,8 @@ async function isNewTemplate(templateNoteId) {
|
||||
const age = (new Date().getTime() - creationDate.getTime()) / DAY_LENGTH;
|
||||
// Return true if the template is at most NEW_TEMPLATE_MAX_AGE days old
|
||||
return (age <= NEW_TEMPLATE_MAX_AGE);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@ -1582,7 +1582,8 @@
|
||||
"ai-chat": "AI Chat",
|
||||
"task-list": "Task List",
|
||||
"new-feature": "New",
|
||||
"collections": "Collections"
|
||||
"collections": "Collections",
|
||||
"spreadsheet": "Spreadsheet"
|
||||
},
|
||||
"protect_note": {
|
||||
"toggle-on": "Protect the note",
|
||||
|
||||
@ -141,5 +141,11 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
|
||||
view: () => import("./type_widgets/SqlConsole"),
|
||||
className: "sql-console-widget-container",
|
||||
isFullHeight: true
|
||||
},
|
||||
spreadsheet: {
|
||||
view: () => import("./type_widgets/Spreadsheet"),
|
||||
className: "note-detail-spreadsheet",
|
||||
printable: true,
|
||||
isFullHeight: true
|
||||
}
|
||||
};
|
||||
|
||||
@ -79,7 +79,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
}
|
||||
|
||||
#isFullWidthNote(note: FNote) {
|
||||
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
|
||||
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap", "spreadsheet"].includes(note.type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
const COLLECTIONS_WITH_BACKGROUND_EFFECTS = [
|
||||
"grid",
|
||||
"list"
|
||||
]
|
||||
];
|
||||
|
||||
if (note.isOptions()) {
|
||||
return true;
|
||||
|
||||
@ -85,7 +85,7 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote
|
||||
);
|
||||
const isElectron = getIsElectron();
|
||||
const isMac = getIsMac();
|
||||
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(noteType);
|
||||
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "spreadsheet"].includes(noteType);
|
||||
const isSearchOrBook = ["search", "book"].includes(noteType);
|
||||
const isHelpPage = note.noteId.startsWith("_help");
|
||||
const [syncServerHost] = useTriliumOption("syncServerHost");
|
||||
|
||||
3
apps/client/src/widgets/type_widgets/Spreadsheet.css
Normal file
3
apps/client/src/widgets/type_widgets/Spreadsheet.css
Normal file
@ -0,0 +1,3 @@
|
||||
.note-detail-spreadsheet > .spreadsheet {
|
||||
height: 100%;
|
||||
}
|
||||
125
apps/client/src/widgets/type_widgets/Spreadsheet.tsx
Normal file
125
apps/client/src/widgets/type_widgets/Spreadsheet.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import "@univerjs/preset-sheets-core/lib/index.css";
|
||||
import "./Spreadsheet.css";
|
||||
|
||||
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
|
||||
import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US';
|
||||
import { CommandType, createUniver, FUniver, IDisposable, IWorkbookData, LocaleType, mergeLocales } from '@univerjs/presets';
|
||||
import { MutableRef, useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { useColorScheme, useEditorSpacedUpdate } from "../react/hooks";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
interface PersistedData {
|
||||
version: number;
|
||||
workbook: Parameters<FUniver["createWorkbook"]>[0];
|
||||
}
|
||||
|
||||
export default function Spreadsheet({ note, noteContext }: TypeWidgetProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const apiRef = useRef<FUniver>();
|
||||
|
||||
useInitializeSpreadsheet(containerRef, apiRef);
|
||||
useDarkMode(apiRef);
|
||||
usePersistence(note, noteContext, apiRef);
|
||||
|
||||
return <div ref={containerRef} className="spreadsheet" />;
|
||||
}
|
||||
|
||||
function useInitializeSpreadsheet(containerRef: MutableRef<HTMLDivElement | null>, apiRef: MutableRef<FUniver | undefined>) {
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const { univerAPI } = createUniver({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: mergeLocales(
|
||||
UniverPresetSheetsCoreEnUS
|
||||
),
|
||||
},
|
||||
presets: [
|
||||
UniverSheetsCorePreset({
|
||||
container: containerRef.current,
|
||||
})
|
||||
]
|
||||
});
|
||||
apiRef.current = univerAPI;
|
||||
return () => univerAPI.dispose();
|
||||
}, [ apiRef, containerRef ]);
|
||||
}
|
||||
|
||||
function useDarkMode(apiRef: MutableRef<FUniver | undefined>) {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
// React to dark mode.
|
||||
useEffect(() => {
|
||||
const univerAPI = apiRef.current;
|
||||
if (!univerAPI) return;
|
||||
univerAPI.toggleDarkMode(colorScheme === 'dark');
|
||||
}, [ colorScheme, apiRef ]);
|
||||
}
|
||||
|
||||
function usePersistence(note: FNote, noteContext: NoteContext | null | undefined, apiRef: MutableRef<FUniver | undefined>) {
|
||||
const changeListener = useRef<IDisposable>(null);
|
||||
|
||||
const spacedUpdate = useEditorSpacedUpdate({
|
||||
noteType: "spreadsheet",
|
||||
note,
|
||||
noteContext,
|
||||
getData() {
|
||||
const univerAPI = apiRef.current;
|
||||
if (!univerAPI) return undefined;
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
if (!workbook) return undefined;
|
||||
const content = {
|
||||
version: 1,
|
||||
workbook: workbook.save()
|
||||
};
|
||||
return {
|
||||
content: JSON.stringify(content)
|
||||
};
|
||||
},
|
||||
onContentChange(newContent) {
|
||||
const univerAPI = apiRef.current;
|
||||
if (!univerAPI) return undefined;
|
||||
|
||||
// Dispose the existing workbook.
|
||||
const existingWorkbook = univerAPI.getActiveWorkbook();
|
||||
if (existingWorkbook) {
|
||||
univerAPI.disposeUnit(existingWorkbook.getId());
|
||||
}
|
||||
|
||||
let workbookData: Partial<IWorkbookData> = {};
|
||||
if (newContent) {
|
||||
try {
|
||||
const parsedContent = JSON.parse(newContent) as unknown;
|
||||
if (parsedContent && typeof parsedContent === "object" && "workbook" in parsedContent) {
|
||||
const persistedData = parsedContent as PersistedData;
|
||||
workbookData = persistedData.workbook;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse spreadsheet content", e);
|
||||
}
|
||||
}
|
||||
|
||||
const workbook = univerAPI.createWorkbook(workbookData);
|
||||
if (changeListener.current) {
|
||||
changeListener.current.dispose();
|
||||
}
|
||||
changeListener.current = workbook.onCommandExecuted(command => {
|
||||
if (command.type !== CommandType.MUTATION) return;
|
||||
spacedUpdate.scheduleUpdate();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (changeListener.current) {
|
||||
changeListener.current.dispose();
|
||||
changeListener.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
import path from "path";
|
||||
import build from "./build.js";
|
||||
import packageJson from "../../package.json" with { type: "json" };
|
||||
import dataDir from "./data_dir.js";
|
||||
import { AppInfo } from "@triliumnext/commons";
|
||||
import path from "path";
|
||||
|
||||
import packageJson from "../../package.json" with { type: "json" };
|
||||
import build from "./build.js";
|
||||
import dataDir from "./data_dir.js";
|
||||
|
||||
const APP_DB_VERSION = 234;
|
||||
const SYNC_VERSION = 36;
|
||||
const SYNC_VERSION = 37;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
export default {
|
||||
|
||||
@ -14,7 +14,8 @@ const noteTypes = [
|
||||
{ type: "launcher", defaultMime: "" },
|
||||
{ type: "doc", defaultMime: "" },
|
||||
{ type: "contentWidget", defaultMime: "" },
|
||||
{ type: "mindMap", defaultMime: "application/json" }
|
||||
{ type: "mindMap", defaultMime: "application/json" },
|
||||
{ type: "spreadsheet", defaultMime: "application/json" }
|
||||
];
|
||||
|
||||
function getDefaultMimeForNoteType(typeName: string) {
|
||||
|
||||
@ -20,7 +20,8 @@ export const NOTE_TYPE_ICONS = {
|
||||
launcher: "bx bx-link",
|
||||
doc: "bx bxs-file-doc",
|
||||
contentWidget: "bx bxs-widget",
|
||||
mindMap: "bx bx-sitemap"
|
||||
mindMap: "bx bx-sitemap",
|
||||
spreadsheet: "bx bx-table"
|
||||
};
|
||||
|
||||
const FILE_MIME_MAPPINGS = {
|
||||
|
||||
@ -119,7 +119,8 @@ export const ALLOWED_NOTE_TYPES = [
|
||||
"book",
|
||||
"webView",
|
||||
"code",
|
||||
"mindMap"
|
||||
"mindMap",
|
||||
"spreadsheet"
|
||||
] as const;
|
||||
export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number];
|
||||
|
||||
|
||||
3220
pnpm-lock.yaml
generated
3220
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user