Skip to content

Commit

Permalink
Merge branch 'main' into CRISTAL-83
Browse files Browse the repository at this point in the history
  • Loading branch information
pjeanjean committed Jan 27, 2025
2 parents a9ff36e + 6817976 commit 1101b3e
Show file tree
Hide file tree
Showing 30 changed files with 470 additions and 252 deletions.
8 changes: 8 additions & 0 deletions core/browser/browser-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ export interface BrowserApi {
* @since 0.11
*/
reload(): void;

/**
* Calls a callback when the current "window" is close, can either be a browser tab, or an electron windows.
* @param callback - the lamda called before the window is closed
* @since 0.14
*/

onClose(callback: () => boolean): void;
}

export const name = "BrowserApi";
3 changes: 2 additions & 1 deletion core/browser/browser-default/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"dependencies": {
"@xwiki/cristal-api": "workspace:*",
"@xwiki/cristal-browser-api": "workspace:*",
"inversify": "6.2.1"
"inversify": "6.2.1",
"vue": "3.5.13"
},
"peerDependencies": {
"reflect-metadata": "0.2.2"
Expand Down
14 changes: 14 additions & 0 deletions core/browser/browser-default/src/components/browser-api-default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import { WikiConfig } from "@xwiki/cristal-api";
import { BrowserApi } from "@xwiki/cristal-browser-api";
import { injectable } from "inversify";
import { onBeforeMount, onBeforeUnmount } from "vue";

/**
* Default implementation for the browser. Set the window location, and the
Expand All @@ -35,4 +36,17 @@ export class BrowserApiDefault implements BrowserApi {
reload(): void {
window.location.reload();
}

onClose(callback: () => boolean): void {
const listener = (e: Event) => {
e.preventDefault();
return callback();
};
onBeforeMount(() => {
window.addEventListener("beforeunload", listener);
});
onBeforeUnmount(() => {
window.removeEventListener("beforeunload", listener);
});
}
}
40 changes: 23 additions & 17 deletions core/history/history-xwiki/src/components/componentsInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,23 +156,29 @@ class XWikiPageRevisionManager implements PageRevisionManager {
headers.Authorization = authorization;
}
const response = await fetch(restApiUrl, { headers });
const jsonResponse = await response.json();
let user = jsonResponse.pageName;
jsonResponse.properties.forEach(
(property: { name: string; value: string }) => {
// Properties are sorted alphabetically.
switch (property.name) {
case "first_name":
user = property.value;
break;
case "last_name":
if (property.value) {
user += ` ${property.value}`;
}
break;
}
},
);
const jsonResponse: {
properties: { name: string; value: string }[];
pageName: string;
} = await response.json();

const firstName =
jsonResponse.properties
.filter((p) => p.name == "first_name")
.map((p) => p.value) ?? "";
const lastName =
jsonResponse.properties
.filter((p) => p.name == "last_name")
.map((p) => p.value) ?? "";
const fullName = `${firstName} ${lastName}`.trim();
let user: string;
// If at least one of first or last name is not empty, the full name is not empty and the fullname is used.
// Otherwise, we fallback to the user id.
if (fullName != "") {
user = fullName;
} else {
user = jsonResponse.pageName;
}

return {
name: user,
profile: this.cristalApp.getRouter().resolve({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,18 @@ describe("FileSystemModelReferenceSerializer", () => {
);
expect(url).toEqual("a/b/c/attachments/file.png");
});

it("serialize a document reference without a space", () => {
const url = fileSystemModelReferenceSerializer.serialize(
new DocumentReference("page"),
);
expect(url).toEqual("page");
});

it("serialize a document reference with an empty space", () => {
const url = fileSystemModelReferenceSerializer.serialize(
new DocumentReference("page", new SpaceReference(undefined)),
);
expect(url).toEqual("page");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class FileSystemModelReferenceSerializer
const documentReference = reference as DocumentReference;
const spaces = this.serialize(documentReference.space);
const name = documentReference.name;
if (spaces === undefined) {
if (spaces === undefined || spaces == "") {
return this.escapeSegment(name);
} else {
return `${spaces}/${this.escapeSegment(name)}`;
Expand Down
6 changes: 6 additions & 0 deletions ds/vuetify/src/vue/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
height: 64px;
}

main ul {
padding-inline-start: var(--cr-spacing-x-large);
}

:root {
--cr-sizes-max-page-width: 960px; /*This value controls the width of the main content. It should be 100% for spanning text along the whole width or an arbitraty value*/
--cr-sizes-main-sidebar-width: 15%; /*The default width of the main sidebar*/
Expand All @@ -39,6 +43,8 @@
* Typography
*/



/* Fonts */
--cr-font-mono: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
--cr-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
Expand Down
12 changes: 12 additions & 0 deletions ds/vuetify/src/vue/x-navigation-tree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,16 @@ async function onDocumentUpdate(page: PageData) {
:deep(.v-list-item--link) {
cursor: default;
}
/*TODO: This section needs to be removed when the oficial fix is released on Vuetify
https://github.com/vuetifyjs/vuetify/issues/20421#event-16001533054
START FIX
*/
:deep(.v-treeview-item:not(.v-list-group__header)) {
padding-inline-start: calc(39px + 6px) !important;
}

:deep(.v-treeview-item .v-list-item__spacer) {
width: 4px !important;
}
/*END FIX*/
</style>
4 changes: 4 additions & 0 deletions editors/tiptap/langs/translation-en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"tiptap.editor.onquit.message": "You have unsaved changes. Are you sure you want to quit?",
"tiptap.editor.save.error": "Failed to save the page."
}
4 changes: 4 additions & 0 deletions editors/tiptap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@
"@tiptap/starter-kit": "2.11.2",
"@tiptap/suggestion": "2.11.2",
"@tiptap/vue-3": "2.11.2",
"@xwiki/cristal-alerts-api": "workspace:*",
"@xwiki/cristal-api": "workspace:*",
"@xwiki/cristal-authentication-api": "workspace:*",
"@xwiki/cristal-backend-api": "workspace:*",
"@xwiki/cristal-browser-api": "workspace:*",
"@xwiki/cristal-document-api": "workspace:*",
"@xwiki/cristal-dsapi": "workspace:*",
"@xwiki/cristal-icons": "workspace:*",
Expand All @@ -66,6 +68,8 @@
"tiptap-markdown": "0.8.10",
"utility-types": "3.11.0",
"vue": "3.5.13",
"vue-i18n": "11.0.1",
"vue-router": "4.5.0",
"yjs": "13.6.23"
},
"peerDependencies": {
Expand Down
26 changes: 26 additions & 0 deletions editors/tiptap/src/translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* See the LICENSE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/

import en from "../langs/translation-en.json";

const translations: Record<string, Record<string, string>> = {
en,
};
export default translations;
79 changes: 60 additions & 19 deletions editors/tiptap/src/vue/c-edit-tiptap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import CConnectionStatus from "./c-connection-status.vue";
import CSaveStatus from "./c-save-status.vue";
import CTiptapBubbleMenu from "./c-tiptap-bubble-menu.vue";
import { computeCurrentUser } from "./compute-current-user";
import { initOnQuitHelper } from "./on-quit-helper";
import { loadLinkSuggest } from "../components/extensions/link-suggest";
import { Slash } from "../components/extensions/slash";
import { CollaborationKit, User } from "../extensions/collaboration";
import initLinkExtension from "../extensions/link";
import initMarkdown from "../extensions/markdown";
import messages from "../translations";
import Placeholder from "@tiptap/extension-placeholder";
import Table from "@tiptap/extension-table";
import TableCell from "@tiptap/extension-table-cell";
Expand All @@ -36,6 +38,7 @@ import StarterKit from "@tiptap/starter-kit";
import { Editor, EditorContent } from "@tiptap/vue-3";
import { CristalApp, PageData } from "@xwiki/cristal-api";
import { AuthenticationManagerProvider } from "@xwiki/cristal-authentication-api";
import { BrowserApi } from "@xwiki/cristal-browser-api";
import { name as documentServiceName } from "@xwiki/cristal-document-api";
import {
ModelReferenceHandlerProvider,
Expand All @@ -57,6 +60,8 @@ import { Container } from "inversify";
import { debounce } from "lodash";
import GlobalDragHandle from "tiptap-extension-global-drag-handle";
import { inject, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import type { AlertsService } from "@xwiki/cristal-alerts-api";
import type { StorageProvider } from "@xwiki/cristal-backend-api";
import type { DocumentService } from "@xwiki/cristal-document-api";
import type {
Expand All @@ -66,6 +71,10 @@ import type {
import type { Markdown } from "tiptap-markdown";
import type { Ref } from "vue";

const { t } = useI18n({
messages,
});

const cristal: CristalApp = inject<CristalApp>("cristal")!;

const container = cristal.getContainer();
Expand All @@ -76,6 +85,11 @@ const authenticationManager = container
const modelReferenceHandler = container
.get<ModelReferenceHandlerProvider>("ModelReferenceHandlerProvider")
.get();
const storage = container.get<StorageProvider>("StorageProvider").get();
const browserApi = container.get<BrowserApi>("BrowserApi");
const alertsService = cristal
.getContainer()
.get<AlertsService>("AlertsService")!;
const loading = documentService.isLoading();
const error: Ref<Error | undefined> = documentService.getError();
const currentPage: Ref<PageData | undefined> =
Expand All @@ -98,26 +112,47 @@ const viewRouterParams = {
name: "view",
params: { page: currentPageName.value ?? "" },
};
let editor: Ref<Editor | undefined> = ref(undefined);

const view = () => {
// Destroy the editor instance.
editor.value?.destroy();
// Navigate to view mode.
cristal?.getRouter().push(viewRouterParams);
};
const save = async (authors: User[]) => {
console.log(
"Saving changes made by: ",
authors.map((author) => author.name).join(", "),
);

const storage = container.get<StorageProvider>("StorageProvider").get();
// TODO: html does not make any sense here.
await storage.save(
currentPageName.value ?? "",
editor.value?.storage.markdown.getMarkdown(),
title.value,
"html",
);

// init last save content with the initial editor value.
const { update } = initOnQuitHelper(
getEditorMarkdown,
cristal.getRouter(),
browserApi,
);
const updateOnQuitContent: () => void = update;

function getEditorMarkdown() {
return editor?.value?.storage.markdown.getMarkdown();
}

let lastSaveSucceeded = true;

const save = async () => {
try {
// TODO: html does not make any sense here.
await storage.save(
currentPageName.value ?? "",
getEditorMarkdown(),
title.value,
"html",
);
// Update the on quit content with the last successfully saved content.
updateOnQuitContent();
lastSaveSucceeded = true;
} catch (e) {
lastSaveSucceeded = false;
console.error(e);
alertsService.error(t("tiptap.editor.save.error"));
}

// If this save operation just created the document, the current document
// will be undefined. So we update it.
if (!currentPage.value) {
Expand All @@ -127,19 +162,23 @@ const save = async (authors: User[]) => {
};
const submit = async () => {
if (!hasRealtime) {
await save([currentUser!]);
await save();
} else {
await editor.value?.storage.cristalCollaborationKit.autoSaver.save();
}
view();
let goToView = true;
if (!lastSaveSucceeded) {
goToView = confirm(t("tiptap.editor.onquit.message"));
}
if (goToView) {
view();
}
};

let currentUser: undefined | User = undefined;

let editor: Ref<Editor | undefined> = ref(undefined);

const debounced = debounce(() => {
save([currentUser!]);
save();
}, 500);

async function editorInit(
Expand Down Expand Up @@ -259,6 +298,8 @@ async function loadEditor(page: PageData | undefined) {
modelReferenceSerializer,
remoteURLParser,
);

update();
}
}

Expand Down
Loading

0 comments on commit 1101b3e

Please sign in to comment.