From c71fe8b4430a4c192845e219d38c842c1813990b Mon Sep 17 00:00:00 2001 From: Thiago Krieck Date: Wed, 22 Jan 2025 14:05:26 -0300 Subject: [PATCH 01/10] CRISTAL-428: List elements are flat on Vuetify * Added padding values to list elements --- ds/vuetify/src/vue/css/style.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ds/vuetify/src/vue/css/style.css b/ds/vuetify/src/vue/css/style.css index 6d2e8d386..017066b23 100644 --- a/ds/vuetify/src/vue/css/style.css +++ b/ds/vuetify/src/vue/css/style.css @@ -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*/ @@ -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, From 3383a0637e68586ef47801c654ae0035a1e86745 Mon Sep 17 00:00:00 2001 From: Simpel Date: Thu, 23 Jan 2025 07:25:09 +0000 Subject: [PATCH 02/10] Translated using Weblate (German) Currently translated at 100.0% (16 of 16 strings) Translation: XWiki Contrib/Cristal - Skin Translate-URL: https://l10n.xwiki.org/projects/xwiki-contrib/cristal/cristal-skin/de/ --- skin/langs/translation-de.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skin/langs/translation-de.json b/skin/langs/translation-de.json index 79fb46daf..d909240d4 100644 --- a/skin/langs/translation-de.json +++ b/skin/langs/translation-de.json @@ -11,5 +11,8 @@ "page.creation.menu.submit": "Erstellen", "article.loading": "Lädt...", "page.edited.details.user": "{user} bearbeitete am {date}", - "page.edited.details": "Bearbeitet am {date}" + "page.edited.details": "Bearbeitet am {date}", + "page.creation.menu.alert.content": "Die Seite {pageName} existiert bereits.", + "page.creation.menu.alert.content.edit": "Die Seite {pageName} existiert bereits, verwende entweder einen anderen Namen oder {link}.", + "page.creation.menu.alert.content.edit.link": "die bestehende Seite bearbeiten" } From 792367c4fb1432db6e46714f10242dc79472173f Mon Sep 17 00:00:00 2001 From: tkrieck <149672322+tkrieck@users.noreply.github.com> Date: Thu, 23 Jan 2025 05:18:35 -0300 Subject: [PATCH 03/10] CRISTAL-413: Nav Tree alignment issue on Vuetify (#600) * Temporary fix until the Vuetify project releases the fix officially --- ds/vuetify/src/vue/x-navigation-tree.vue | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ds/vuetify/src/vue/x-navigation-tree.vue b/ds/vuetify/src/vue/x-navigation-tree.vue index af4c233d8..287bf1131 100644 --- a/ds/vuetify/src/vue/x-navigation-tree.vue +++ b/ds/vuetify/src/vue/x-navigation-tree.vue @@ -304,4 +304,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*/ From 268470c7992eb83c8e214262755dba86f96e29ee Mon Sep 17 00:00:00 2001 From: Manuel Leduc Date: Thu, 23 Jan 2025 12:02:57 +0100 Subject: [PATCH 04/10] CRISTAL-380: Allow for users to create their own configurations * Introduce the demo instance in the default config for electron * Introduce the demo instance in the web config * Remove unsupported configs from the demo instance --- .../src/defaultConfig.json | 22 +++++++ web/public/config.json | 64 +++---------------- 2 files changed, 32 insertions(+), 54 deletions(-) diff --git a/electron/configuration/configuration-electron/configuration-electron-main/src/defaultConfig.json b/electron/configuration/configuration-electron/configuration-electron-main/src/defaultConfig.json index d00383848..18e3a34ff 100644 --- a/electron/configuration/configuration-electron/configuration-electron-main/src/defaultConfig.json +++ b/electron/configuration/configuration-electron/configuration-electron-main/src/defaultConfig.json @@ -12,5 +12,27 @@ "serverRendering": false, "offline": false, "designSystem": "shoelace" + }, + "XWiki": { + "name": "XWiki", + "configType": "XWiki", + "serverRendering": false, + "offline": false, + "baseURL": "https://cristal-backend.demo.xwiki.com/xwiki", + "baseRestURL": "/rest/cristal/page?media=json", + "homePage": "Main.WebHome", + "designSystem": "vuetify", + "realtimeURL": "https://cristal.demo.xwiki.com:8093/collaboration" + }, + "XWikiSL": { + "name": "XWikiSL", + "configType": "XWiki", + "serverRendering": false, + "offline": false, + "baseURL": "https://cristal-backend.demo.xwiki.com/xwiki", + "baseRestURL": "/rest/cristal/page?media=json", + "homePage": "Main.WebHome", + "designSystem": "shoelace", + "realtimeURL": "https://cristal.demo.xwiki.com:8093/collaboration" } } diff --git a/web/public/config.json b/web/public/config.json index 927147b49..0acf3de34 100644 --- a/web/public/config.json +++ b/web/public/config.json @@ -4,55 +4,55 @@ "configType": "XWiki", "serverRendering": false, "offline": false, - "baseURL": "https://cristal.demo.xwiki.com/xwiki", + "baseURL": "https://cristal-backend.demo.xwiki.com/xwiki", "baseRestURL": "/rest/cristal/page?media=json", "homePage": "Main.WebHome", "designSystem": "vuetify", - "realtimeURL": "http://localhost:15681/collaboration" + "realtimeURL": "https://cristal.demo.xwiki.com:8093/collaboration" }, "XWikiOffline": { "name": "XWikiOffline", "configType": "XWiki", "serverRendering": true, "offline": true, - "baseURL": "https://cristal.demo.xwiki.com/xwiki", + "baseURL": "https://cristal-backend.demo.xwiki.com/xwiki", "baseRestURL": "/rest/cristal/page?media=json", "homePage": "Main.WebHome", "designSystem": "vuetify", - "realtimeURL": "http://localhost:15681/collaboration" + "realtimeURL": "https://cristal.demo.xwiki.com:8093/collaboration" }, "XWikiSSR": { "name": "XWikiSSR", "configType": "XWiki", "serverRendering": true, "offline": false, - "baseURL": "https://cristal.demo.xwiki.com/xwiki", + "baseURL": "https://cristal-backend.demo.xwiki.com/xwiki", "baseRestURL": "/rest/cristal/page?media=json", "homePage": "Main.WebHome", "designSystem": "vuetify", - "realtimeURL": "http://localhost:15681/collaboration" + "realtimeURL": "https://cristal.demo.xwiki.com:8093/collaboration" }, "XWikiDSFR": { "name": "XWikiDSFR", "configType": "XWiki", "serverRendering": false, "offline": false, - "baseURL": "https://cristal.demo.xwiki.com/xwiki", + "baseURL": "https://cristal-backend.demo.xwiki.com/xwiki", "baseRestURL": "/rest/cristal/page?media=json", "homePage": "Main.WebHome", "designSystem": "dsfr", - "realtimeURL": "http://localhost:15681/collaboration" + "realtimeURL": "https://cristal.demo.xwiki.com:8093/collaboration" }, "XWikiSL": { "name": "XWikiSL", "configType": "XWiki", "serverRendering": false, "offline": false, - "baseURL": "https://cristal.demo.xwiki.com/xwiki", + "baseURL": "https://cristal-backend.demo.xwiki.com/xwiki", "baseRestURL": "/rest/cristal/page?media=json", "homePage": "Main.WebHome", "designSystem": "shoelace", - "realtimeURL": "http://localhost:15681/collaboration" + "realtimeURL": "https://cristal.demo.xwiki.com:8093/collaboration" }, "Localhost": { "name": "Localhost", @@ -76,17 +76,6 @@ "designSystem": "shoelace", "realtimeURL": "http://localhost:15681/collaboration" }, - "LocalhostDSFR": { - "name": "LocalhostDSFR", - "configType": "XWiki", - "serverRendering": false, - "offline": false, - "baseURL": "http://localhost:15680/xwiki", - "baseRestURL": "/rest/cristal/page?media=json", - "homePage": "Main.WebHome", - "designSystem": "dsfr", - "realtimeURL": "http://localhost:15681/collaboration" - }, "LocalhostOffline": { "name": "LocalhostOffline", "configType": "XWiki", @@ -98,39 +87,6 @@ "designSystem": "vuetify", "realtimeURL": "http://localhost:15681/collaboration" }, - "GitHub": { - "name": "GitHub", - "configType": "GitHub", - "serverRendering": false, - "offline": false, - "baseURL": "https://github.com/ldubost/test/tree/master/", - "baseRestURL": "https://raw.githubusercontent.com/ldubost/test/master/", - "homePage": "README.md", - "designSystem": "vuetify", - "realtimeURL": "http://localhost:15681/collaboration" - }, - "GitHubOffline": { - "name": "GitHubOffline", - "configType": "GitHub", - "serverRendering": false, - "offline": true, - "baseURL": "https://github.com/ldubost/test/tree/master/", - "baseRestURL": "https://raw.githubusercontent.com/ldubost/test/master/", - "homePage": "README.md", - "designSystem": "vuetify", - "realtimeURL": "http://localhost:15681/collaboration" - }, - "GitHubSL": { - "name": "GitHubSL", - "configType": "GitHub", - "serverRendering": false, - "offline": false, - "baseURL": "https://github.com/ldubost/test/tree/master/", - "baseRestURL": "https://raw.githubusercontent.com/ldubost/test/master/", - "homePage": "README.md", - "designSystem": "shoelace", - "realtimeURL": "http://localhost:15681/collaboration" - }, "Nextcloud": { "name": "Nextcloud", "configType": "Nextcloud", From 3d284bad8798f6e2d2b27ea386e20359dacef5e7 Mon Sep 17 00:00:00 2001 From: Manuel Leduc Date: Thu, 23 Jan 2025 12:16:57 +0100 Subject: [PATCH 05/10] [Misc] Remove dsfr and github from the default distributions --- electron/renderer/src/index.ts | 3 +- lib/package.json | 5 - lib/src/components/cristalAppLoader.ts | 2 +- lib/src/defaultComponentsList.ts | 119 +++++++++++++++++++++ lib/src/index.ts | 3 +- lib/src/staticBuild.ts | 104 +----------------- pnpm-lock.yaml | 21 ---- sharedworker/impl/package.json | 1 - sharedworker/impl/src/components/worker.ts | 2 - web/package.json | 1 - web/src/index.ts | 2 + 11 files changed, 128 insertions(+), 135 deletions(-) create mode 100644 lib/src/defaultComponentsList.ts diff --git a/electron/renderer/src/index.ts b/electron/renderer/src/index.ts index b19b176b4..20c081948 100644 --- a/electron/renderer/src/index.ts +++ b/electron/renderer/src/index.ts @@ -24,7 +24,7 @@ import { loadConfig } from "@xwiki/cristal-configuration-electron-renderer"; import { ComponentInit as XWikiAuthenticationComponentInit } from "@xwiki/cristal-electron-authentication-xwiki-renderer"; import { ComponentInit as ElectronStorageComponentInit } from "@xwiki/cristal-electron-storage"; import { ComponentInit as FileSystemPageHierarchyComponentInit } from "@xwiki/cristal-hierarchy-filesystem"; -import { CristalAppLoader } from "@xwiki/cristal-lib"; +import { CristalAppLoader, defaultComponentsList } from "@xwiki/cristal-lib"; import { ComponentInit as FileSystemLinkSuggestComponentInit } from "@xwiki/cristal-link-suggest-filesystem"; import { ComponentInit as ModelReferenceFilesystemComponentInit } from "@xwiki/cristal-model-reference-filesystem"; import { ComponentInit as ModelRemoteURLFilesystemComponentInit } from "@xwiki/cristal-model-remote-url-filesystem-default"; @@ -47,6 +47,7 @@ CristalAppLoader.init( true, "FileSystemSL", (container: Container) => { + defaultComponentsList(container); new ElectronStorageComponentInit(container); new BrowserComponentInit(container); new FileSystemPageHierarchyComponentInit(container); diff --git a/lib/package.json b/lib/package.json index 760c17613..24a7bbad4 100644 --- a/lib/package.json +++ b/lib/package.json @@ -36,7 +36,6 @@ "@xwiki/cristal-authentication-ui": "workspace:*", "@xwiki/cristal-backend-api": "workspace:*", "@xwiki/cristal-backend-dexie": "workspace:*", - "@xwiki/cristal-backend-github": "workspace:*", "@xwiki/cristal-backend-nextcloud": "workspace:*", "@xwiki/cristal-backend-xwiki": "workspace:*", "@xwiki/cristal-browser-api": "workspace:*", @@ -44,7 +43,6 @@ "@xwiki/cristal-date-api": "workspace:*", "@xwiki/cristal-document-api": "workspace:*", "@xwiki/cristal-document-default": "workspace:*", - "@xwiki/cristal-dsfr": "workspace:*", "@xwiki/cristal-dsshoelace": "workspace:*", "@xwiki/cristal-dsvuetify": "workspace:*", "@xwiki/cristal-editors-tiptap": "workspace:*", @@ -52,11 +50,9 @@ "@xwiki/cristal-extension-menubuttons": "workspace:*", "@xwiki/cristal-extra-tabs-default": "workspace:*", "@xwiki/cristal-hierarchy-default": "workspace:*", - "@xwiki/cristal-hierarchy-github": "workspace:*", "@xwiki/cristal-hierarchy-nextcloud": "workspace:*", "@xwiki/cristal-hierarchy-xwiki": "workspace:*", "@xwiki/cristal-history-default": "workspace:*", - "@xwiki/cristal-history-github": "workspace:*", "@xwiki/cristal-history-ui": "workspace:*", "@xwiki/cristal-history-xwiki": "workspace:*", "@xwiki/cristal-info-actions-default": "workspace:*", @@ -73,7 +69,6 @@ "@xwiki/cristal-model-remote-url-nextcloud": "workspace:*", "@xwiki/cristal-model-remote-url-xwiki": "workspace:*", "@xwiki/cristal-navigation-tree-default": "workspace:*", - "@xwiki/cristal-navigation-tree-github": "workspace:*", "@xwiki/cristal-navigation-tree-nextcloud": "workspace:*", "@xwiki/cristal-navigation-tree-xwiki": "workspace:*", "@xwiki/cristal-page-actions-default": "workspace:*", diff --git a/lib/src/components/cristalAppLoader.ts b/lib/src/components/cristalAppLoader.ts index 595968e2d..e82760e00 100644 --- a/lib/src/components/cristalAppLoader.ts +++ b/lib/src/components/cristalAppLoader.ts @@ -114,7 +114,7 @@ class CristalAppLoader extends CristalLoader { loadConfig: ConfigurationLoader, isElectron: boolean, configName: string, - additionalComponents?: (container: Container) => void, + additionalComponents: (container: Container) => void, ): Promise { let staticMode = forceStaticMode; if (import.meta.env.MODE == "development" || staticMode) { diff --git a/lib/src/defaultComponentsList.ts b/lib/src/defaultComponentsList.ts new file mode 100644 index 000000000..119d3b809 --- /dev/null +++ b/lib/src/defaultComponentsList.ts @@ -0,0 +1,119 @@ +/* + * 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 { ComponentInit as AlertsDefaultComponentInit } from "@xwiki/cristal-alerts-default"; +import { ComponentInit as AttachmentsDefaultComponentInit } from "@xwiki/cristal-attachments-default"; +import { ComponentInit as AttachmentsUIComponentInit } from "@xwiki/cristal-attachments-ui"; +import { ComponentInit as AuthenticationDefaultComponentInit } from "@xwiki/cristal-authentication-default"; +import { ComponentInit as AuthenticationUIComponentInit } from "@xwiki/cristal-authentication-ui"; +import { ComponentInit as BackendAPIComponentInit } from "@xwiki/cristal-backend-api"; +import { ComponentInit as DexieBackendComponentInit } from "@xwiki/cristal-backend-dexie"; +import { ComponentInit as NextcloudBackendComponentInit } from "@xwiki/cristal-backend-nextcloud"; +import { ComponentInit as XWikiBackendComponentInit } from "@xwiki/cristal-backend-xwiki"; +import { ComponentInit as DateAPIComponentInit } from "@xwiki/cristal-date-api"; +import { ComponentInit as DocumentComponentInit } from "@xwiki/cristal-document-default"; +import { ComponentInit as ShoelaceComponentInit } from "@xwiki/cristal-dsshoelace"; +import { ComponentInit as VueDSComponentInit } from "@xwiki/cristal-dsvuetify"; +import { ComponentInit as EditorTiptapComponentInit } from "@xwiki/cristal-editors-tiptap"; +import { ComponentInit as MenuButtonsComponentInit } from "@xwiki/cristal-extension-menubuttons"; +import { ComponentInit as ExtraTabsComponentInit } from "@xwiki/cristal-extra-tabs-default"; +import { ComponentInit as DefaultPageHierarchyComponentInit } from "@xwiki/cristal-hierarchy-default"; +import { ComponentInit as NextcloudPageHierarchyComponentInit } from "@xwiki/cristal-hierarchy-nextcloud"; +import { ComponentInit as XWikiPageHierarchyComponentInit } from "@xwiki/cristal-hierarchy-xwiki"; +import { ComponentInit as DefaultPageHistoryComponentInit } from "@xwiki/cristal-history-default"; +import { ComponentInit as HistoryUIComponentInit } from "@xwiki/cristal-history-ui"; +import { ComponentInit as XWikiPageHistoryComponentInit } from "@xwiki/cristal-history-xwiki"; +import { ComponentInit as InfoActionsComponentInit } from "@xwiki/cristal-info-actions-default"; +import { ComponentInit as LinkSuggestComponentInit } from "@xwiki/cristal-link-suggest-api"; +import { ComponentInit as NextcloudLinkSuggestComponentInit } from "@xwiki/cristal-link-suggest-nextcloud"; +import { ComponentInit as XWikiLinkSuggestComponentInit } from "@xwiki/cristal-link-suggest-xwiki"; +import { ComponentInit as MacrosComponentInit } from "@xwiki/cristal-macros"; +import { ComponentInit as MarkdownDefaultComponentInit } from "@xwiki/cristal-markdown-default"; +import { ComponentInit as ClickListenerComponentInit } from "@xwiki/cristal-model-click-listener"; +import { ComponentInit as ModelReferenceAPIComponentInit } from "@xwiki/cristal-model-reference-api"; +import { ComponentInit as ModelReferenceNextcloudComponentInit } from "@xwiki/cristal-model-reference-nextcloud"; +import { ComponentInit as ModelReferenceXWikiComponentInit } from "@xwiki/cristal-model-reference-xwiki"; +import { ComponentInit as ModelRemoteURLAPIComponentInit } from "@xwiki/cristal-model-remote-url-api"; +import { ComponentInit as ModelRemoteURLNextcloudComponentInit } from "@xwiki/cristal-model-remote-url-nextcloud"; +import { ComponentInit as ModelRemoteURLXWikiComponentInit } from "@xwiki/cristal-model-remote-url-xwiki"; +import { ComponentInit as DefaultNavigationTreeComponentInit } from "@xwiki/cristal-navigation-tree-default"; +import { ComponentInit as NextcloudNavigationTreeComponentInit } from "@xwiki/cristal-navigation-tree-nextcloud"; +import { ComponentInit as XWikiNavigationTreeComponentInit } from "@xwiki/cristal-navigation-tree-xwiki"; +import { ComponentInit as ActionsPagesComponentInit } from "@xwiki/cristal-page-actions-default"; +import { ComponentInit as ActionsPagesUIComponentInit } from "@xwiki/cristal-page-actions-ui"; +import { ComponentInit as RenderingComponentInit } from "@xwiki/cristal-rendering"; +import { ComponentInit as QueueWorkerComponentInit } from "@xwiki/cristal-sharedworker-impl"; +import { ComponentInit as SkinComponentInit } from "@xwiki/cristal-skin"; +import { ComponentInit as UIExtensionDefaultComponentInit } from "@xwiki/cristal-uiextension-default"; +import type { Container } from "inversify"; + +/** + * Loads all the components of the default distribution. + * + * @param container - the container the load the components in + * @since 0.14 + */ +// eslint-disable-next-line max-statements +export function defaultComponentsList(container: Container): void { + new SkinComponentInit(container); + new MacrosComponentInit(container); + new VueDSComponentInit(container); + new ShoelaceComponentInit(container); + new DexieBackendComponentInit(container); + new NextcloudBackendComponentInit(container); + new XWikiBackendComponentInit(container); + new MenuButtonsComponentInit(container); + new QueueWorkerComponentInit(container); + new RenderingComponentInit(container); + new EditorTiptapComponentInit(container); + new ExtraTabsComponentInit(container); + new InfoActionsComponentInit(container); + new AttachmentsUIComponentInit(container); + new AttachmentsDefaultComponentInit(container); + new UIExtensionDefaultComponentInit(container); + new AuthenticationUIComponentInit(container); + new BackendAPIComponentInit(container); + new AuthenticationDefaultComponentInit(container); + new LinkSuggestComponentInit(container); + new XWikiLinkSuggestComponentInit(container); + new NextcloudLinkSuggestComponentInit(container); + new DocumentComponentInit(container); + new AlertsDefaultComponentInit(container); + new ActionsPagesComponentInit(container); + new ActionsPagesUIComponentInit(container); + new DefaultPageHierarchyComponentInit(container); + new NextcloudPageHierarchyComponentInit(container); + new XWikiPageHierarchyComponentInit(container); + new DefaultNavigationTreeComponentInit(container); + new NextcloudNavigationTreeComponentInit(container); + new XWikiNavigationTreeComponentInit(container); + new DefaultPageHistoryComponentInit(container); + new XWikiPageHistoryComponentInit(container); + new HistoryUIComponentInit(container); + new ClickListenerComponentInit(container); + new ModelRemoteURLAPIComponentInit(container); + new ModelRemoteURLNextcloudComponentInit(container); + new ModelRemoteURLXWikiComponentInit(container); + new ModelReferenceAPIComponentInit(container); + new ModelReferenceNextcloudComponentInit(container); + new ModelReferenceXWikiComponentInit(container); + new DateAPIComponentInit(container); + new MarkdownDefaultComponentInit(container); +} diff --git a/lib/src/index.ts b/lib/src/index.ts index a64769d38..f0ac651c3 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -20,5 +20,6 @@ import { DefaultCristalApp } from "./components/DefaultCristalApp"; import { CristalAppLoader } from "./components/cristalAppLoader"; +import { defaultComponentsList } from "./defaultComponentsList"; -export { CristalAppLoader, DefaultCristalApp }; +export { CristalAppLoader, DefaultCristalApp, defaultComponentsList }; diff --git a/lib/src/staticBuild.ts b/lib/src/staticBuild.ts index fd1cd3814..575cf5bad 100644 --- a/lib/src/staticBuild.ts +++ b/lib/src/staticBuild.ts @@ -18,120 +18,20 @@ * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ -import { ComponentInit as AlertsDefaultComponentInit } from "@xwiki/cristal-alerts-default"; -import { ComponentInit as AttachmentsDefaultComponentInit } from "@xwiki/cristal-attachments-default"; -import { ComponentInit as AttachmentsUIComponentInit } from "@xwiki/cristal-attachments-ui"; -import { ComponentInit as AuthenticationDefaultComponentInit } from "@xwiki/cristal-authentication-default"; -import { ComponentInit as AuthenticationUIComponentInit } from "@xwiki/cristal-authentication-ui"; -import { ComponentInit as BackendAPIComponentInit } from "@xwiki/cristal-backend-api"; -import { ComponentInit as DexieBackendComponentInit } from "@xwiki/cristal-backend-dexie"; -import { ComponentInit as GithubBackendComponentInit } from "@xwiki/cristal-backend-github"; -import { ComponentInit as NextcloudBackendComponentInit } from "@xwiki/cristal-backend-nextcloud"; -import { ComponentInit as XWikiBackendComponentInit } from "@xwiki/cristal-backend-xwiki"; -import { ComponentInit as DateAPIComponentInit } from "@xwiki/cristal-date-api"; -import { ComponentInit as DocumentComponentInit } from "@xwiki/cristal-document-default"; -import { ComponentInit as DSFRComponentInit } from "@xwiki/cristal-dsfr"; -import { ComponentInit as ShoelaceComponentInit } from "@xwiki/cristal-dsshoelace"; -import { ComponentInit as VueDSComponentInit } from "@xwiki/cristal-dsvuetify"; -import { ComponentInit as EditorTiptapComponentInit } from "@xwiki/cristal-editors-tiptap"; -import { ComponentInit as MenuButtonsComponentInit } from "@xwiki/cristal-extension-menubuttons"; -import { ComponentInit as ExtraTabsComponentInit } from "@xwiki/cristal-extra-tabs-default"; -import { ComponentInit as DefaultPageHierarchyComponentInit } from "@xwiki/cristal-hierarchy-default"; -import { ComponentInit as GitHubPageHierarchyComponentInit } from "@xwiki/cristal-hierarchy-github"; -import { ComponentInit as NextcloudPageHierarchyComponentInit } from "@xwiki/cristal-hierarchy-nextcloud"; -import { ComponentInit as XWikiPageHierarchyComponentInit } from "@xwiki/cristal-hierarchy-xwiki"; -import { ComponentInit as DefaultPageHistoryComponentInit } from "@xwiki/cristal-history-default"; -import { ComponentInit as GitHubPageHistoryComponentInit } from "@xwiki/cristal-history-github"; -import { ComponentInit as HistoryUIComponentInit } from "@xwiki/cristal-history-ui"; -import { ComponentInit as XWikiPageHistoryComponentInit } from "@xwiki/cristal-history-xwiki"; -import { ComponentInit as InfoActionsComponentInit } from "@xwiki/cristal-info-actions-default"; -import { ComponentInit as LinkSuggestComponentInit } from "@xwiki/cristal-link-suggest-api"; -import { ComponentInit as NextcloudLinkSuggestComponentInit } from "@xwiki/cristal-link-suggest-nextcloud"; -import { ComponentInit as XWikiLinkSuggestComponentInit } from "@xwiki/cristal-link-suggest-xwiki"; -import { ComponentInit as MacrosComponentInit } from "@xwiki/cristal-macros"; -import { ComponentInit as MarkdownDefaultComponentInit } from "@xwiki/cristal-markdown-default"; -import { ComponentInit as ClickListenerComponentInit } from "@xwiki/cristal-model-click-listener"; -import { ComponentInit as ModelReferenceAPIComponentInit } from "@xwiki/cristal-model-reference-api"; -import { ComponentInit as ModelReferenceNextcloudComponentInit } from "@xwiki/cristal-model-reference-nextcloud"; -import { ComponentInit as ModelReferenceXWikiComponentInit } from "@xwiki/cristal-model-reference-xwiki"; -import { ComponentInit as ModelRemoteURLAPIComponentInit } from "@xwiki/cristal-model-remote-url-api"; -import { ComponentInit as ModelRemoteURLNextcloudComponentInit } from "@xwiki/cristal-model-remote-url-nextcloud"; -import { ComponentInit as ModelRemoteURLXWikiComponentInit } from "@xwiki/cristal-model-remote-url-xwiki"; -import { ComponentInit as DefaultNavigationTreeComponentInit } from "@xwiki/cristal-navigation-tree-default"; -import { ComponentInit as GitHubNavigationTreeComponentInit } from "@xwiki/cristal-navigation-tree-github"; -import { ComponentInit as NextcloudNavigationTreeComponentInit } from "@xwiki/cristal-navigation-tree-nextcloud"; -import { ComponentInit as XWikiNavigationTreeComponentInit } from "@xwiki/cristal-navigation-tree-xwiki"; -import { ComponentInit as ActionsPagesComponentInit } from "@xwiki/cristal-page-actions-default"; -import { ComponentInit as ActionsPagesUIComponentInit } from "@xwiki/cristal-page-actions-ui"; -import { ComponentInit as RenderingComponentInit } from "@xwiki/cristal-rendering"; -import { ComponentInit as QueueWorkerComponentInit } from "@xwiki/cristal-sharedworker-impl"; -import { ComponentInit as SkinComponentInit } from "@xwiki/cristal-skin"; -import { ComponentInit as UIExtensionDefaultComponentInit } from "@xwiki/cristal-uiextension-default"; import type { Container } from "inversify"; export class StaticBuild { // TODO: reduce the number of statements in the following method and reactivate the disabled eslint rule. - // eslint-disable-next-line max-statements + public static init( container: Container, forceStaticBuild: boolean, - additionalComponents?: (container: Container) => void, + additionalComponents: (container: Container) => void, ): void { if ( (import.meta.env && import.meta.env.MODE == "development") || forceStaticBuild ) { - new SkinComponentInit(container); - new MacrosComponentInit(container); - new VueDSComponentInit(container); - new DSFRComponentInit(container); - new ShoelaceComponentInit(container); - new DexieBackendComponentInit(container); - new GithubBackendComponentInit(container); - new NextcloudBackendComponentInit(container); - new XWikiBackendComponentInit(container); - new MenuButtonsComponentInit(container); - new QueueWorkerComponentInit(container); - new RenderingComponentInit(container); - new EditorTiptapComponentInit(container); - new ExtraTabsComponentInit(container); - new InfoActionsComponentInit(container); - new AttachmentsUIComponentInit(container); - new AttachmentsDefaultComponentInit(container); - new UIExtensionDefaultComponentInit(container); - new AuthenticationUIComponentInit(container); - new BackendAPIComponentInit(container); - new AuthenticationDefaultComponentInit(container); - new LinkSuggestComponentInit(container); - new XWikiLinkSuggestComponentInit(container); - new NextcloudLinkSuggestComponentInit(container); - new DocumentComponentInit(container); - new AlertsDefaultComponentInit(container); - new ActionsPagesComponentInit(container); - new ActionsPagesUIComponentInit(container); - new DefaultPageHierarchyComponentInit(container); - new GitHubPageHierarchyComponentInit(container); - new NextcloudPageHierarchyComponentInit(container); - new XWikiPageHierarchyComponentInit(container); - new DefaultNavigationTreeComponentInit(container); - new GitHubNavigationTreeComponentInit(container); - new NextcloudNavigationTreeComponentInit(container); - new XWikiNavigationTreeComponentInit(container); - new DefaultPageHistoryComponentInit(container); - new GitHubPageHistoryComponentInit(container); - new XWikiPageHistoryComponentInit(container); - new HistoryUIComponentInit(container); - new ClickListenerComponentInit(container); - new ModelRemoteURLAPIComponentInit(container); - new ModelRemoteURLNextcloudComponentInit(container); - new ModelRemoteURLXWikiComponentInit(container); - new ModelReferenceAPIComponentInit(container); - new ModelReferenceNextcloudComponentInit(container); - new ModelReferenceXWikiComponentInit(container); - new DateAPIComponentInit(container); - new MarkdownDefaultComponentInit(container); - } - if (additionalComponents) { additionalComponents(container); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5a705f14..03d85037c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3101,9 +3101,6 @@ importers: '@xwiki/cristal-backend-dexie': specifier: workspace:* version: link:../core/backends/backend-dexie - '@xwiki/cristal-backend-github': - specifier: workspace:* - version: link:../core/backends/backend-github '@xwiki/cristal-backend-nextcloud': specifier: workspace:* version: link:../core/backends/backend-nextcloud @@ -3125,9 +3122,6 @@ importers: '@xwiki/cristal-document-default': specifier: workspace:* version: link:../core/document/document-default - '@xwiki/cristal-dsfr': - specifier: workspace:* - version: link:../ds/dsfr '@xwiki/cristal-dsshoelace': specifier: workspace:* version: link:../ds/shoelace @@ -3149,9 +3143,6 @@ importers: '@xwiki/cristal-hierarchy-default': specifier: workspace:* version: link:../core/hierarchy/hierarchy-default - '@xwiki/cristal-hierarchy-github': - specifier: workspace:* - version: link:../core/hierarchy/hierarchy-github '@xwiki/cristal-hierarchy-nextcloud': specifier: workspace:* version: link:../core/hierarchy/hierarchy-nextcloud @@ -3161,9 +3152,6 @@ importers: '@xwiki/cristal-history-default': specifier: workspace:* version: link:../core/history/history-default - '@xwiki/cristal-history-github': - specifier: workspace:* - version: link:../core/history/history-github '@xwiki/cristal-history-ui': specifier: workspace:* version: link:../core/history/history-ui @@ -3212,9 +3200,6 @@ importers: '@xwiki/cristal-navigation-tree-default': specifier: workspace:* version: link:../core/navigation-tree/navigation-tree-default - '@xwiki/cristal-navigation-tree-github': - specifier: workspace:* - version: link:../core/navigation-tree/navigation-tree-github '@xwiki/cristal-navigation-tree-nextcloud': specifier: workspace:* version: link:../core/navigation-tree/navigation-tree-nextcloud @@ -3436,9 +3421,6 @@ importers: '@xwiki/cristal-backend-dexie': specifier: workspace:* version: link:../../core/backends/backend-dexie - '@xwiki/cristal-backend-github': - specifier: workspace:* - version: link:../../core/backends/backend-github '@xwiki/cristal-backend-nextcloud': specifier: workspace:* version: link:../../core/backends/backend-nextcloud @@ -3649,9 +3631,6 @@ importers: '@xwiki/cristal-navigation-tree-default': specifier: workspace:* version: link:../core/navigation-tree/navigation-tree-default - '@xwiki/cristal-navigation-tree-github': - specifier: workspace:* - version: link:../core/navigation-tree/navigation-tree-github '@xwiki/cristal-navigation-tree-nextcloud': specifier: workspace:* version: link:../core/navigation-tree/navigation-tree-nextcloud diff --git a/sharedworker/impl/package.json b/sharedworker/impl/package.json index 70791cdd6..7f76bc1bf 100644 --- a/sharedworker/impl/package.json +++ b/sharedworker/impl/package.json @@ -31,7 +31,6 @@ "@xwiki/cristal-api": "workspace:*", "@xwiki/cristal-authentication-default": "workspace:*", "@xwiki/cristal-backend-dexie": "workspace:*", - "@xwiki/cristal-backend-github": "workspace:*", "@xwiki/cristal-backend-nextcloud": "workspace:*", "@xwiki/cristal-backend-xwiki": "workspace:*", "@xwiki/cristal-configuration-api": "workspace:*", diff --git a/sharedworker/impl/src/components/worker.ts b/sharedworker/impl/src/components/worker.ts index 3445eb8e9..beb4a47d1 100644 --- a/sharedworker/impl/src/components/worker.ts +++ b/sharedworker/impl/src/components/worker.ts @@ -25,7 +25,6 @@ import { ComponentInit as AlertsDefaultComponentInit } from "@xwiki/cristal-aler import { CristalApp, WrappingStorage } from "@xwiki/cristal-api"; import { ComponentInit as AuthenticationDefaultComponentInit } from "@xwiki/cristal-authentication-default"; import { ComponentInit as DexieBackendComponentInit } from "@xwiki/cristal-backend-dexie"; -import { ComponentInit as GithubBackendComponentInit } from "@xwiki/cristal-backend-github"; import { ComponentInit as NextcloudBackendComponentInit } from "@xwiki/cristal-backend-nextcloud"; import { ComponentInit as XWikiBackendComponentInit } from "@xwiki/cristal-backend-xwiki"; import { @@ -257,7 +256,6 @@ export class Worker implements MyWorker { // TODO: find a way to do this loading differently. Here we need to // explicitly depend on all required storage and this is not good. new DexieBackendComponentInit(cristalLoader.container); - new GithubBackendComponentInit(cristalLoader.container); new NextcloudBackendComponentInit(cristalLoader.container); new XWikiBackendComponentInit(cristalLoader.container); new AuthenticationDefaultComponentInit(cristalLoader.container); diff --git a/web/package.json b/web/package.json index c1cdb29fc..1cda00d27 100644 --- a/web/package.json +++ b/web/package.json @@ -35,7 +35,6 @@ "@xwiki/cristal-configuration-web": "workspace:*", "@xwiki/cristal-lib": "workspace:*", "@xwiki/cristal-navigation-tree-default": "workspace:*", - "@xwiki/cristal-navigation-tree-github": "workspace:*", "@xwiki/cristal-navigation-tree-nextcloud": "workspace:*", "@xwiki/cristal-navigation-tree-xwiki": "workspace:*", "express": "4.21.2", diff --git a/web/src/index.ts b/web/src/index.ts index 34aa4df9c..3ce27421e 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -23,6 +23,7 @@ import { ComponentInit as AuthenticationXWikiComponentInit } from "@xwiki/crista import { ComponentInit as BrowserComponentInit } from "@xwiki/cristal-browser-default"; import { Container } from "inversify"; import { loadConfig } from "@xwiki/cristal-configuration-web"; +import { defaultComponentsList } from "@xwiki/cristal-lib"; CristalAppLoader.init( [ @@ -40,6 +41,7 @@ CristalAppLoader.init( false, "XWiki", (container: Container) => { + defaultComponentsList(container); new BrowserComponentInit(container); new AuthenticationXWikiComponentInit(container); }, From 3b637f4191540e39e40b263b4f03b5a73a12e2e7 Mon Sep 17 00:00:00 2001 From: Manuel Leduc Date: Thu, 23 Jan 2025 18:08:27 +0100 Subject: [PATCH 06/10] CRISTAL-218: Warn the user when leaving the editor with unsaved edits --- core/browser/browser-api/src/index.ts | 8 ++ core/browser/browser-default/package.json | 3 +- .../src/components/browser-api-default.ts | 14 ++++ editors/tiptap/langs/translation-en.json | 4 + editors/tiptap/package.json | 4 + editors/tiptap/src/translations.ts | 26 ++++++ editors/tiptap/src/vue/c-edit-tiptap.vue | 79 ++++++++++++++----- .../src/components/browser-api-electron.ts | 5 ++ pnpm-lock.yaml | 24 ++++-- 9 files changed, 139 insertions(+), 28 deletions(-) create mode 100644 editors/tiptap/langs/translation-en.json create mode 100644 editors/tiptap/src/translations.ts diff --git a/core/browser/browser-api/src/index.ts b/core/browser/browser-api/src/index.ts index 8318461a4..0063a9385 100644 --- a/core/browser/browser-api/src/index.ts +++ b/core/browser/browser-api/src/index.ts @@ -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"; diff --git a/core/browser/browser-default/package.json b/core/browser/browser-default/package.json index aaa197977..1abe5cf4a 100644 --- a/core/browser/browser-default/package.json +++ b/core/browser/browser-default/package.json @@ -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" diff --git a/core/browser/browser-default/src/components/browser-api-default.ts b/core/browser/browser-default/src/components/browser-api-default.ts index f9fa2371b..44dd7880e 100644 --- a/core/browser/browser-default/src/components/browser-api-default.ts +++ b/core/browser/browser-default/src/components/browser-api-default.ts @@ -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 @@ -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); + }); + } } diff --git a/editors/tiptap/langs/translation-en.json b/editors/tiptap/langs/translation-en.json new file mode 100644 index 000000000..d8471c76a --- /dev/null +++ b/editors/tiptap/langs/translation-en.json @@ -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." +} diff --git a/editors/tiptap/package.json b/editors/tiptap/package.json index 79cc223b6..9773da3e7 100644 --- a/editors/tiptap/package.json +++ b/editors/tiptap/package.json @@ -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:*", @@ -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": { diff --git a/editors/tiptap/src/translations.ts b/editors/tiptap/src/translations.ts new file mode 100644 index 000000000..ba75b08f2 --- /dev/null +++ b/editors/tiptap/src/translations.ts @@ -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> = { + en, +}; +export default translations; diff --git a/editors/tiptap/src/vue/c-edit-tiptap.vue b/editors/tiptap/src/vue/c-edit-tiptap.vue index bbddbf137..bc6b7ed4a 100644 --- a/editors/tiptap/src/vue/c-edit-tiptap.vue +++ b/editors/tiptap/src/vue/c-edit-tiptap.vue @@ -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"; @@ -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, @@ -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 { @@ -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("cristal")!; const container = cristal.getContainer(); @@ -76,6 +85,11 @@ const authenticationManager = container const modelReferenceHandler = container .get("ModelReferenceHandlerProvider") .get(); +const storage = container.get("StorageProvider").get(); +const browserApi = container.get("BrowserApi"); +const alertsService = cristal + .getContainer() + .get("AlertsService")!; const loading = documentService.isLoading(); const error: Ref = documentService.getError(); const currentPage: Ref = @@ -98,26 +112,47 @@ const viewRouterParams = { name: "view", params: { page: currentPageName.value ?? "" }, }; +let editor: Ref = 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").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) { @@ -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 = ref(undefined); - const debounced = debounce(() => { - save([currentUser!]); + save(); }, 500); async function editorInit( @@ -259,6 +298,8 @@ async function loadEditor(page: PageData | undefined) { modelReferenceSerializer, remoteURLParser, ); + + update(); } } diff --git a/electron/browser/src/components/browser-api-electron.ts b/electron/browser/src/components/browser-api-electron.ts index 935b87f6a..8beeab788 100644 --- a/electron/browser/src/components/browser-api-electron.ts +++ b/electron/browser/src/components/browser-api-electron.ts @@ -39,4 +39,9 @@ export class BrowserApiElectron implements BrowserApi { reload(): void { browserElectron.reloadBrowser(); } + + onClose(): void { + // TODO: CRISTAL-429 we did not find a viable implementation to intercept the close action and display a confirm + // dialog to ask for the user if the confirm closing the page. + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03d85037c..1b575d38a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -675,6 +675,9 @@ importers: inversify: specifier: 6.2.1 version: 6.2.1(reflect-metadata@0.2.2) + vue: + specifier: 3.5.13 + version: 3.5.13(typescript@5.7.3) devDependencies: '@xwiki/cristal-dev-config': specifier: workspace:* @@ -2554,6 +2557,9 @@ importers: '@tiptap/vue-3': specifier: 2.11.2 version: 2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))(@tiptap/pm@2.11.2)(vue@3.5.13(typescript@5.7.3)) + '@xwiki/cristal-alerts-api': + specifier: workspace:* + version: link:../../core/alerts/alerts-api '@xwiki/cristal-api': specifier: workspace:* version: link:../../api @@ -2563,6 +2569,9 @@ importers: '@xwiki/cristal-backend-api': specifier: workspace:* version: link:../../core/backends/backend-api + '@xwiki/cristal-browser-api': + specifier: workspace:* + version: link:../../core/browser/browser-api '@xwiki/cristal-document-api': specifier: workspace:* version: link:../../core/document/document-api @@ -2626,6 +2635,12 @@ importers: vue: specifier: 3.5.13 version: 3.5.13(typescript@5.7.3) + vue-i18n: + specifier: 11.0.1 + version: 11.0.1(vue@3.5.13(typescript@5.7.3)) + vue-router: + specifier: 4.5.0 + version: 4.5.0(vue@3.5.13(typescript@5.7.3)) yjs: specifier: 13.6.23 version: 13.6.23 @@ -6941,9 +6956,6 @@ packages: magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} - magic-string@0.30.15: - resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -10019,7 +10031,7 @@ snapshots: '@vue/compiler-ssr': 3.5.13 '@vue/shared': 3.5.13 estree-walker: 2.0.2 - magic-string: 0.30.15 + magic-string: 0.30.17 postcss: 8.4.49 source-map-js: 1.2.1 @@ -12286,10 +12298,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.15: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 From c7c5974602411dda0aa090f99d5f8827c5d0543e Mon Sep 17 00:00:00 2001 From: Manuel Leduc Date: Fri, 24 Jan 2025 08:44:48 +0100 Subject: [PATCH 07/10] CRISTAL-218: Warn the user when leaving the editor with unsaved edits Add missing file. --- editors/tiptap/src/vue/on-quit-helper.ts | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 editors/tiptap/src/vue/on-quit-helper.ts diff --git a/editors/tiptap/src/vue/on-quit-helper.ts b/editors/tiptap/src/vue/on-quit-helper.ts new file mode 100644 index 000000000..3934d2988 --- /dev/null +++ b/editors/tiptap/src/vue/on-quit-helper.ts @@ -0,0 +1,68 @@ +/* + * 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 messages from "../translations"; +import { BrowserApi } from "@xwiki/cristal-browser-api"; +import { useI18n } from "vue-i18n"; +import { Router } from "vue-router"; + +function hasUnsavedContent(currentContent: string, lastSavedContent: string) { + return currentContent !== lastSavedContent; +} + +function initTranslations() { + const { t, mergeLocaleMessage } = useI18n(); + for (const messagesKey in messages) { + mergeLocaleMessage(messagesKey, messages[messagesKey]); + } + return t; +} + +export function initOnQuitHelper( + getContent: () => string, + router: Router, + browserApi: BrowserApi, +): { update(): void } { + let lastSavedContent: string = getContent(); + const t = initTranslations(); + + // Specific for navigation handled by vue through the router (e.g., click on a link or a button on the UI, making + // the user leave the current view). + router.beforeEach(() => { + if (hasUnsavedContent(getContent(), lastSavedContent)) { + return confirm(t("tiptap.editor.onquit.message")); + } + return true; + }); + + // Specific for the case when the editor is left because the current window is left by click on an element outside + // the webview (e.g., a tab or an electron window is closed). + browserApi.onClose(() => { + if (hasUnsavedContent(getContent(), lastSavedContent)) { + return confirm(t("tiptap.editor.onquit.message")); + } + return false; + }); + + return { + update() { + lastSavedContent = getContent(); + }, + }; +} From dfea6d7477bf4e7de61467e5771f8e883bb601e8 Mon Sep 17 00:00:00 2001 From: Manuel Leduc Date: Fri, 24 Jan 2025 14:54:07 +0100 Subject: [PATCH 08/10] CRISTAL-432: Derive allowed origins from configuration --- .../configuration-electron-main/src/index.ts | 30 +++++++++++++------ electron/main/src/security-restrictions.ts | 25 ++++++++++++---- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/electron/configuration/configuration-electron/configuration-electron-main/src/index.ts b/electron/configuration/configuration-electron/configuration-electron-main/src/index.ts index fc382ec6a..a9d43b1c8 100644 --- a/electron/configuration/configuration-electron/configuration-electron-main/src/index.ts +++ b/electron/configuration/configuration-electron/configuration-electron-main/src/index.ts @@ -18,6 +18,7 @@ * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ import config from "./defaultConfig.json"; +import { Configurations } from "@xwiki/cristal-configuration-api"; import { ipcMain } from "electron"; import Store from "electron-store"; @@ -32,15 +33,26 @@ const storeInstance: Store = new Store({ schema, }); -export function load(): void { - ipcMain.handle("configuration:load", async () => { - // Create the configuration with default values the first time Cristal is loaded. - // @ts-expect-error type resolution failing because of electron-store library bug - if (!storeInstance.has("configuration")) { - // @ts-expect-error type resolution failing because of electron-store library bug - storeInstance.set("configuration", config); - } +/** + * Get access to the configuration from the store instance. + * + * @since 0.14 + */ +function readConfiguration(): Configurations { + // Create the configuration with default values the first time Cristal is loaded. + // @ts-expect-error type resolution failing because of electron-store library bug + if (!storeInstance.has("configuration")) { // @ts-expect-error type resolution failing because of electron-store library bug - return storeInstance.get("configuration"); + storeInstance.set("configuration", config); + } + // @ts-expect-error type resolution failing because of electron-store library bug + return storeInstance.get("configuration"); +} + +function load(): void { + ipcMain.handle("configuration:load", () => { + return readConfiguration(); }); } + +export { load, readConfiguration }; diff --git a/electron/main/src/security-restrictions.ts b/electron/main/src/security-restrictions.ts index b3a44d017..018e139e4 100644 --- a/electron/main/src/security-restrictions.ts +++ b/electron/main/src/security-restrictions.ts @@ -18,6 +18,7 @@ * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ +import { readConfiguration } from "@xwiki/cristal-configuration-electron-main"; import { app, shell } from "electron"; import { URL } from "node:url"; import type { Session } from "electron"; @@ -32,7 +33,8 @@ type Permission = Parameters< /** * A list of origins that you allow open INSIDE the application and permissions for them. * - * In development mode you need allow open `VITE_DEV_SERVER_URL`. + * In development mode, you need allow open `VITE_DEV_SERVER_URL`. + * @deprecated the allowed origins and permissions should be derived from the configuration. */ const ALLOWED_ORIGINS_AND_PERMISSIONS: Map> = new Map< string, @@ -69,9 +71,20 @@ app.on("web-contents-created", (_, contents) => { * * @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation */ + const configuration = readConfiguration(); contents.on("will-navigate", (event, url) => { const { origin } = new URL(url); - if (ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) { + const isAllowedOrigin = Object.values(configuration).some( + (configuration) => { + const baseURL = configuration.baseURL; + if (!baseURL) { + return false; + } + return new URL(baseURL).origin == origin; + }, + ); + + if (isAllowedOrigin) { return; } @@ -93,6 +106,8 @@ app.on("web-contents-created", (_, contents) => { (webContents, permission, callback) => { const { origin } = new URL(webContents.getURL()); + // FIXME: this control is currently based on a fixed set of URLs, but seems also unused. We need to check more + // closely if this check is needed. const permissionGranted = !!ALLOWED_ORIGINS_AND_PERMISSIONS.get(origin)?.has(permission); callback(permissionGranted); @@ -108,9 +123,9 @@ app.on("web-contents-created", (_, contents) => { /** * Hyperlinks leading to allowed sites are opened in the default browser. * - * The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows, - * frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before. - * You should deny any unexpected window creation. + * The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new + * windows, frames, or other renderer processes with more privileges than they had before; or with pages opened that + * they couldn't open before. You should deny any unexpected window creation. * * @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows * @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content From 6606be78517fffd947fd4514525a8c50c52c2bb8 Mon Sep 17 00:00:00 2001 From: Manuel Leduc Date: Fri, 24 Jan 2025 14:56:58 +0100 Subject: [PATCH 09/10] CRISTAL-433: The username is empty in the history when not first or last name --- .../src/components/componentsInit.ts | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/core/history/history-xwiki/src/components/componentsInit.ts b/core/history/history-xwiki/src/components/componentsInit.ts index 39f93a767..74716bbe2 100644 --- a/core/history/history-xwiki/src/components/componentsInit.ts +++ b/core/history/history-xwiki/src/components/componentsInit.ts @@ -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({ From 681797651acc78a93976d301144fccf18dac8aa9 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjean Date: Fri, 24 Jan 2025 17:11:53 +0100 Subject: [PATCH 10/10] CRISTAL-431: file system document reference with no space have a leading / --- .../fileSystemModelReferenceSerializer.test.ts | 14 ++++++++++++++ .../src/filesystemModelReferenceSerializer.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/model/model-reference/model-reference-filesystem/src/__tests__/fileSystemModelReferenceSerializer.test.ts b/core/model/model-reference/model-reference-filesystem/src/__tests__/fileSystemModelReferenceSerializer.test.ts index 13b12dc61..e5dd1aa14 100644 --- a/core/model/model-reference/model-reference-filesystem/src/__tests__/fileSystemModelReferenceSerializer.test.ts +++ b/core/model/model-reference/model-reference-filesystem/src/__tests__/fileSystemModelReferenceSerializer.test.ts @@ -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"); + }); }); diff --git a/core/model/model-reference/model-reference-filesystem/src/filesystemModelReferenceSerializer.ts b/core/model/model-reference/model-reference-filesystem/src/filesystemModelReferenceSerializer.ts index 992a386aa..9d17f8ece 100644 --- a/core/model/model-reference/model-reference-filesystem/src/filesystemModelReferenceSerializer.ts +++ b/core/model/model-reference/model-reference-filesystem/src/filesystemModelReferenceSerializer.ts @@ -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)}`;