mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-06 15:12:27 +02:00
Compare commits
3 Commits
fix/export
...
fix/patch-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37eaf6abfd | ||
|
|
85128c7b11 | ||
|
|
5f700ed6c4 |
@@ -12,6 +12,8 @@ and this project adheres to
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix loading comments transaction #2273
|
||||
- 💬(frontend) add missing link in onboarding description #2233
|
||||
- 🐛(frontend) sanitize pasted and dropped content in document title #2210
|
||||
- 🐛(frontend) Emoji menu doesn't display above comment box #2229
|
||||
- 🐛(frontend) Block menu doesn't stay open on 1st line #2229
|
||||
|
||||
@@ -161,7 +161,8 @@
|
||||
},
|
||||
"onboarding": {
|
||||
"enabled": true,
|
||||
"learn_more_url": ""
|
||||
"learn_more_url": "",
|
||||
"ready_template_url": ""
|
||||
},
|
||||
"help": {
|
||||
"documentation_url": ""
|
||||
|
||||
@@ -153,7 +153,8 @@ test.describe('Help feature', () => {
|
||||
theme_customization: {
|
||||
onboarding: {
|
||||
enabled: true,
|
||||
learn_more_url: 'https://example.com/learn-more',
|
||||
learn_more_url: 'http://localhost:3000/learn-more',
|
||||
ready_template_url: 'http://localhost:3000/ready-template',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -184,18 +185,19 @@ test.describe('Help feature', () => {
|
||||
'0',
|
||||
);
|
||||
|
||||
await page.getByTestId('onboarding-step-3').click();
|
||||
await expect(page.getByTestId('onboarding-step-3')).toHaveAttribute(
|
||||
'tabindex',
|
||||
'0',
|
||||
);
|
||||
const step3 = page.getByTestId('onboarding-step-3');
|
||||
await step3.click();
|
||||
await expect(step3).toHaveAttribute('tabindex', '0');
|
||||
await expect(
|
||||
step3.getByRole('link', { name: 'ready-made template' }),
|
||||
).toHaveAttribute('href', 'http://localhost:3000/ready-template');
|
||||
|
||||
const learnMoreLink = page.getByRole('link', {
|
||||
name: 'Learn more docs features',
|
||||
});
|
||||
await expect(learnMoreLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://example.com/learn-more',
|
||||
'http://localhost:3000/learn-more',
|
||||
);
|
||||
await learnMoreLink.click();
|
||||
|
||||
@@ -241,6 +243,16 @@ test.describe('Help feature', () => {
|
||||
await expect(
|
||||
modal.getByRole('button', { name: /Suivant/i }),
|
||||
).toBeVisible();
|
||||
await modal
|
||||
.getByText(/Tirez parti de la bibliothèque de contenu/)
|
||||
.first()
|
||||
.click();
|
||||
await expect(
|
||||
modal.getByText(/Commencez à partir de/).first(),
|
||||
).toBeVisible();
|
||||
await expect(modal.getByRole('link')).toHaveText(
|
||||
"modèles prêts à l'emploi",
|
||||
);
|
||||
});
|
||||
|
||||
test('Modal is displayed automatically on first connection', async ({
|
||||
|
||||
@@ -28,6 +28,7 @@ interface ThemeCustomization {
|
||||
onboarding?: {
|
||||
enabled: true;
|
||||
learn_more_url?: string;
|
||||
ready_template_url?: string;
|
||||
};
|
||||
translations?: Resource;
|
||||
waffle?: WaffleType;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommentBody, ThreadStore } from '@blocknote/core/comments';
|
||||
import type { Awareness } from 'y-protocols/awareness';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
@@ -17,6 +18,13 @@ import {
|
||||
|
||||
type ServerThreadListResponse = ServerThread[];
|
||||
|
||||
/**
|
||||
* notifySubscribers generate a transaction, to distinguish
|
||||
* the origin of the update, we use a specific origin "commentMarkUpdate"
|
||||
* for the updates coming from the comment mark changes.
|
||||
*/
|
||||
export const COMMENT_UPDATE_ORIGIN = 'commentMarkUpdate';
|
||||
|
||||
export class DocsThreadStore extends ThreadStore {
|
||||
protected static COMMENTS_PING = 'commentsPing';
|
||||
protected threads: Map<string, ClientThreadData> = new Map();
|
||||
@@ -24,6 +32,7 @@ export class DocsThreadStore extends ThreadStore {
|
||||
(threads: Map<string, ClientThreadData>) => void
|
||||
>();
|
||||
private awareness?: Awareness;
|
||||
private yDoc?: Y.Doc;
|
||||
private lastPingAt = 0;
|
||||
private pingTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
@@ -31,11 +40,13 @@ export class DocsThreadStore extends ThreadStore {
|
||||
protected docId: Doc['id'],
|
||||
awareness: Awareness | undefined,
|
||||
protected docAuth: DocsThreadStoreAuth,
|
||||
yDoc?: Y.Doc,
|
||||
) {
|
||||
super(docAuth);
|
||||
|
||||
if (docAuth.canSee) {
|
||||
this.awareness = awareness;
|
||||
this.yDoc = yDoc;
|
||||
this.awareness?.on('update', this.onAwarenessUpdate);
|
||||
|
||||
this.refreshThreads();
|
||||
@@ -134,18 +145,30 @@ export class DocsThreadStore extends ThreadStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all subscribers about the current thread state
|
||||
* Notifies all subscribers about the current thread state.
|
||||
* We trigger the transaction with a specific origin so we will be able
|
||||
* to flag that the update comes from a comment update.
|
||||
* The inner ydoc.transact calls from y-prosemirror will see there's already
|
||||
* an active transaction and reuse it.
|
||||
*/
|
||||
private notifySubscribers() {
|
||||
// Always emit a new Map reference to help consumers detect changes
|
||||
const threads = new Map(this.threads);
|
||||
this.subscribers.forEach((cb) => {
|
||||
try {
|
||||
cb(threads);
|
||||
} catch (e) {
|
||||
console.warn('DocsThreadStore subscriber threw', e);
|
||||
}
|
||||
});
|
||||
const notify = () => {
|
||||
this.subscribers.forEach((cb) => {
|
||||
try {
|
||||
cb(threads);
|
||||
} catch (e) {
|
||||
console.warn('DocsThreadStore subscriber threw', e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (this.yDoc) {
|
||||
this.yDoc.transact(notify, COMMENT_UPDATE_ORIGIN);
|
||||
} else {
|
||||
notify();
|
||||
}
|
||||
}
|
||||
|
||||
private upsertClientThreadData(thread: ClientThreadData) {
|
||||
|
||||
@@ -27,8 +27,15 @@ export function useComments(
|
||||
encodeURIComponent(user?.full_name || ''),
|
||||
canComment,
|
||||
),
|
||||
provider?.document,
|
||||
);
|
||||
}, [docId, canComment, provider?.awareness, user?.full_name]);
|
||||
}, [
|
||||
docId,
|
||||
canComment,
|
||||
provider?.awareness,
|
||||
provider?.document,
|
||||
user?.full_name,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (canComment) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { COMMENT_UPDATE_ORIGIN } from '@/docs/doc-editor/components/comments/DocsThreadStore';
|
||||
import { useDocContentUpdate } from '@/docs/doc-management/api/useDocContentUpdate';
|
||||
import { useProviderStore } from '@/docs/doc-management/stores/useProviderStore';
|
||||
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning/api/useDocVersions';
|
||||
@@ -65,6 +66,16 @@ export const useSaveDoc = (docId: string, yDoc: Y.Doc) => {
|
||||
const isAIChange =
|
||||
!transaction.local && transactionOrigin !== PROVIDER_ORIGIN_CONSTRUCTOR;
|
||||
|
||||
/**
|
||||
* notifySubscribers generate a transaction that can be
|
||||
* interpreted as a local change.
|
||||
* We intercept the update with this origin to
|
||||
* avoid marking the change as local.
|
||||
*/
|
||||
if (transaction.origin === COMMENT_UPDATE_ORIGIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLocalChange(transaction.local || isAIChange);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import {
|
||||
ModalSize,
|
||||
OnboardingModal,
|
||||
type OnboardingModalProps,
|
||||
OnboardingStep,
|
||||
} from '@gouvfr-lasuite/ui-kit';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
@@ -10,9 +12,29 @@ import { useConfig } from '@/core/config/api/useConfig';
|
||||
|
||||
import { useOnboardingSteps } from '../hooks/useOnboardingSteps';
|
||||
|
||||
/**
|
||||
* typing was not correct on ui-kit side for the description prop of OnboardingStep,
|
||||
* it can be a string or a ReactNode but was typed as string only, so we need to override the
|
||||
* type here to be able to use ReactNode
|
||||
*/
|
||||
type OnboardingStepFixed = Omit<OnboardingStep, 'description'> & {
|
||||
description?: ReactNode;
|
||||
};
|
||||
|
||||
type OnboardingModalPropsFixed = Omit<OnboardingModalProps, 'steps'> & {
|
||||
steps?: OnboardingStepFixed[];
|
||||
};
|
||||
|
||||
const OnboardingModalFixed =
|
||||
OnboardingModal as React.ComponentType<OnboardingModalPropsFixed>;
|
||||
|
||||
const OnBoardingStyle = createGlobalStyle`
|
||||
.c__onboarding-modal__steps{
|
||||
height: auto;
|
||||
|
||||
& a{
|
||||
color:inherit;
|
||||
}
|
||||
}
|
||||
.c__onboarding-modal__content {
|
||||
height: 350px;
|
||||
@@ -32,7 +54,7 @@ const OnBoardingStyle = createGlobalStyle`
|
||||
*:not(.material-icons):not(.material-icons-filled):not(
|
||||
.material-symbols-outlined
|
||||
) {
|
||||
font-family: Marianne, Inter, Roboto Flex Variable, sans-serif;
|
||||
font-family: var(--c--globals--font--families--base);
|
||||
}
|
||||
|
||||
/* Separator between content and footer actions/link */
|
||||
@@ -56,6 +78,10 @@ const OnBoardingStyle = createGlobalStyle`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
a{
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
& .c__onboarding-modal__body{
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -81,7 +107,7 @@ export const OnBoarding = (props: OnBoardingProps) => {
|
||||
return (
|
||||
<>
|
||||
{props.isOpen ? <OnBoardingStyle /> : null}
|
||||
<OnboardingModal
|
||||
<OnboardingModalFixed
|
||||
size={ModalSize.LARGE}
|
||||
appName={t('Discover Docs')}
|
||||
mainTitle={t('Learn the core principles')}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type OnboardingStep } from '@gouvfr-lasuite/ui-kit';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { useConfig } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import DragIndicatorIcon from '../assets/drag_indicator.svg';
|
||||
@@ -16,6 +17,9 @@ export interface OnboardingStepsData {
|
||||
|
||||
export const useOnboardingSteps = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useConfig();
|
||||
const readyTemplateUrl =
|
||||
config?.theme_customization?.onboarding?.ready_template_url;
|
||||
const { contextualTokens, colorsTokens } = useCunninghamTheme();
|
||||
const activeColor =
|
||||
contextualTokens.content.semantic.brand.tertiary ??
|
||||
@@ -122,8 +126,21 @@ export const useOnboardingSteps = () => {
|
||||
</OnboardingStepIcon>
|
||||
),
|
||||
title: t('Draw inspiration from the content library'),
|
||||
description: t(
|
||||
'Start from ready-made templates for common use cases, then customize them to match your workflow in minutes.',
|
||||
description: (
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="Start from <Link>ready-made templates</Link> for common use cases, then customize them to match your workflow in minutes."
|
||||
components={{
|
||||
Link: (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={readyTemplateUrl}
|
||||
aria-label={t('Ready-made templates (opens in a new tab)')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<Image
|
||||
|
||||
@@ -1482,7 +1482,7 @@
|
||||
"Simple document icon": "Icône simple du document",
|
||||
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
|
||||
"Start Writing": "Commencer à écrire",
|
||||
"Start from ready-made templates for common use cases, then customize them to match your workflow in minutes.": "Commencez à partir de modèles prêts à l'emploi pour les cas d'utilisation courants, puis personnalisez-les pour correspondre à votre flux de travail en quelques minutes.",
|
||||
"Start from <Link>ready-made templates</Link> for common use cases, then customize them to match your workflow in minutes.": "Commencez à partir de <Link>modèles prêts à l'emploi</Link> pour les cas d'utilisation courants, puis personnalisez-les pour correspondre à votre flux de travail en quelques minutes.",
|
||||
"Stop": "Arrêter",
|
||||
"Summarize": "Résumer",
|
||||
"Summary": "Sommaire",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@sentry/node": "10.49.0",
|
||||
"@sentry/profiling-node": "10.49.0",
|
||||
"@tiptap/extensions": "*",
|
||||
"axios": "1.15.1",
|
||||
"axios": "1.15.2",
|
||||
"cors": "2.8.6",
|
||||
"express": "5.2.1",
|
||||
"express-ws": "5.0.2",
|
||||
|
||||
@@ -7771,10 +7771,10 @@ axe-core@^4.10.0:
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.0.tgz#16f74d6482e343ff263d4f4503829e9ee91a86b6"
|
||||
integrity sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==
|
||||
|
||||
axios@1.15.1:
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.1.tgz#075420b785da8adbdf545785b69f90c926b28542"
|
||||
integrity sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==
|
||||
axios@1.15.2:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.2.tgz#eb8fb6d30349abace6ade5b4cb4d9e8a0dc23e5b"
|
||||
integrity sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.11"
|
||||
form-data "^4.0.5"
|
||||
|
||||
Reference in New Issue
Block a user