Merge pull request #9903 from colinux/attestation-ux

ETQ admin, je peux tester l'attestation v2
This commit is contained in:
Colin Darie 2024-02-06 08:09:53 +00:00 committed by GitHub
commit 70e92f7c6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 1776 additions and 609 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -36,24 +36,39 @@
#attestation {
@media screen {
max-width: 21cm;
padding: 17mm;
margin: 0 auto;
.a4-container {
display: flex;
flex-direction: column;
justify-content: space-between; // This will push the footer down
max-width: 21cm;
height: 29.7cm;
padding: 17mm;
margin: 0 auto;
background: #FFFFFF;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); // Optional: for better visualization
}
}
font-family: Marianne;
.header {
header {
display: flex;
justify-content: space-between;
p {
margin: 0;
}
}
.right {
text-align: right;
.official-layout & {
.direction {
margin-top: 5.25mm;
}
}
.bloc-marque {
margin-bottom: 14mm;
margin-right: 17mm; // 4x 4.25mm
}
.marianne {
@ -65,6 +80,7 @@
font-size: 12pt;
font-weight: bold;
margin: 0 0 1mm;
line-height: 12pt;
}
.devise {
@ -72,36 +88,83 @@
margin: 0;
}
.issuer {
font-size: 10pt;
margin: 0 0 14mm; // pas sur, pour mettre une marge si issuer plus bas que date
// weasyprint flexbox with img is broken
// so we're using old inline tricks
.logo-co-emetteur,
.direction {
display: inline-block;
vertical-align: top;
}
.logo-co-emetteur {
img {
max-height: 28mm;
margin-right: 5mm;
}
}
.direction {
font-size: 12pt;
margin: 5.25mm 0 23.3mm;
line-height: 14pt;
font-weight: bold;
margin: 0 0 23.3mm;
}
.date {
font-size: 8pt;
margin: 0 0 14mm;
}
.title {
font-size: 12pt;
font-weight: bold;
text-align: center;
margin: 0 0 12.6mm;
.body-start {
margin-top: 12.6mm; // from masque traitement de texte
}
.main {
font-size: 10pt;
.header {
&:first-of-type {
font-size: 10pt;
}
&:last-of-type {
font-size: 8pt;
}
}
}
.notice {
font-size: 10pt;
font-style: italic;
h1,
h2 {
// both titles have the same size
font-size: 12pt;
font-weight: bold;
}
h1 {
margin: 14mm 0 8mm;
}
h2 {
margin: 0;
line-height: 8pt;
}
h3 {
font-size: 10pt; // same as text
font-weight: bold;
line-height: 4pt;
}
li p {
margin: 0.25rem 0;
}
.signature {
text-align: right;
margin-top: 14mm;
margin-right: 25mm;
}
.signature,
.logo-free-layout {
img {
max-height: 50mm;
max-width: 50mm;
}
}
.footer {

View file

@ -1,7 +1,72 @@
@import "constants";
#attestation-edit {
.attestation-schema {
width: 100%;
margin-top: 3em;
top: 3em;
position: sticky;
}
.tiptap {
padding: 8px;
padding: $default-spacer;
overflow-y: scroll;
min-height: 400px;
}
.editor {
// Visual zones
.header .flex-1,
h1 {
border: 1px solid var(--background-contrast-grey-hover);
padding: $default-spacer / 2;
}
.header,
h1,
h2,
h3 {
margin-bottom: $default-spacer;
}
// Styles
.header {
align-content: center;
p {
margin-bottom: 0rem;
font-size: 0.8rem;
}
}
h1,
h2 {
font-size: 1.25rem;
}
h2 {
line-height: 2rem;
}
h3 {
font-size: 1rem; // same as text
font-weight: bold;
line-height: 1rem;
}
li p {
margin-bottom: 0;
}
// Tags
.fr-menu__list {
max-height: 500px;
}
.fr-tag:not(.fr-menu .fr-tag) {
// style span rendered by tiptap like a button/link tag
color: var(--text-action-high-blue-france);
background-color: var(--background-action-low-blue-france);
}
}
}

View file

@ -127,7 +127,6 @@
}
}
// How to add a new remix icon and work with DSFR markup:
// 1. Find it on https://remixicon.com/
// 2. Take its DataURL (copy the url background-image value).
@ -136,6 +135,22 @@
// 4. Keep this list alphabetic :)
.fr-icon {
// scss-lint:disable VendorPrefix
&-align-center {
&:before,
&:after {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 4H21V6H3V4ZM5 19H19V21H5V19ZM3 14H21V16H3V14ZM5 9H19V11H5V9Z' fill='currentColor'%3E%3C/path%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 4H21V6H3V4ZM5 19H19V21H5V19ZM3 14H21V16H3V14ZM5 9H19V11H5V9Z' fill='currentColor'%3E%3C/path%3E%3C/svg%3E");
}
}
&-align-right {
&:before,
&:after {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 4H21V6H3V4ZM7 19H21V21H7V19ZM3 14H21V16H3V14ZM7 9H21V11H7V9Z' fill='currentColor'%3E%3C/path%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 4H21V6H3V4ZM7 19H21V21H7V19ZM3 14H21V16H3V14ZM7 9H21V11H7V9Z' fill='currentColor'%3E%3C/path%3E%3C/svg%3E");
}
}
&-calendar-close-fill {
&:before,
&:after {
@ -167,5 +182,13 @@
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12.917 13C12.441 15.8377 9.973 18 7 18C3.68629 18 1 15.3137 1 12C1 8.68629 3.68629 6 7 6C9.973 6 12.441 8.16229 12.917 11H23V13H21V17H19V13H17V17H15V13H12.917ZM7 16C9.20914 16 11 14.2091 11 12C11 9.79086 9.20914 8 7 8C4.79086 8 3 9.79086 3 12C3 14.2091 4.79086 16 7 16Z' fill='currentColor'%3E%3C/path%3E%3C/svg%3E");
}
}
&-underline {
&:before,
&:after {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 3V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V3H18V12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12V3H8ZM4 20H20V22H4V20Z' fill='currentColor'%3E%3C/path%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 3V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V3H18V12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12V3H8ZM4 20H20V22H4V20Z' fill='currentColor'%3E%3C/path%3E%3C/svg%3E");
}
}
// scss-lint:enable VendorPrefix
}

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class TagsButtonListComponent < ApplicationComponent
attr_reader :tags
def initialize(tags:)
@tags = tags
end
def button_label(tag)
tag[:libelle].truncate_words(12)
end
def button_title(tag)
tag[:description].presence || tag[:libelle]
end
end

View file

@ -0,0 +1,8 @@
---
en:
categories:
individual: Identity
etablissement: Establishment
dossier: File
champ_public: Form data
champ_private: Private annotations

View file

@ -0,0 +1,8 @@
---
fr:
categories:
individual: Identité
etablissement: Établissement
dossier: Dossier
champ_public: Formulaire
champ_private: Annotations privées

View file

@ -0,0 +1,9 @@
- tags.each_pair do |category, tags|
%p.fr-label.fr-text--sm.fr-text--bold.fr-mb-1w= t(category, scope: ".categories")
%ul.fr-tags-group
- tags.each do |tag|
%li
- label = button_label(tag)
%button.fr-tag.fr-tag--sm{ type: "button", title: button_title(tag), data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: label } }
= label

View file

@ -1,10 +1,13 @@
module Administrateurs
class AttestationTemplateV2sController < AdministrateurController
include UninterlacePngConcern
before_action :retrieve_procedure, :retrieve_attestation_template, :ensure_feature_active
def show
json_body = @attestation_template.json_body&.deep_symbolize_keys
@body = TiptapService.to_html(json_body, {})
preview_dossier = @procedure.dossier_for_preview(current_user)
@body = @attestation_template.render_attributes_for(dossier: preview_dossier).fetch(:body)
respond_to do |format|
format.html do
@ -31,9 +34,9 @@ module Administrateurs
['Souligner', 'underline', 'underline']
],
[
['Titre', 'title', 'h-1'],
['Sous titre', 'heading2', 'h-2'],
['Titre de section', 'heading3', 'h-3']
['Titre', 'title', :hidden], # only for "title" section, without any action possible
['Sous titre', 'heading2', 'h-1'],
['Titre de section', 'heading3', 'h-2']
],
[
['Liste à puces', 'bulletList', 'list-unordered'],
@ -49,12 +52,32 @@ module Administrateurs
['Redo', 'redo', 'arrow-go-forward-line']
]
]
@attestation_template.validate
end
def update
@attestation_template.update!(editor_params)
attestation_params = editor_params
logo_file = attestation_params.delete(:logo)
signature_file = attestation_params.delete(:signature)
if logo_file
attestation_params[:logo] = uninterlace_png(logo_file)
end
if signature_file
attestation_params[:signature] = uninterlace_png(signature_file)
end
if !@attestation_template.update(attestation_params)
flash.alert = "Le modèle de lattestation contient des erreurs et n'a pas pu être enregistré. Corriger les erreurs."
end
render :update
end
def create = update
private
def ensure_feature_active
@ -62,11 +85,11 @@ module Administrateurs
end
def retrieve_attestation_template
@attestation_template = @procedure.attestation_template || @procedure.build_attestation_template
@attestation_template = @procedure.attestation_template_v2 || @procedure.build_attestation_template_v2(json_body: AttestationTemplate::TIPTAP_BODY_DEFAULT)
end
def editor_params
params.required(:attestation_template).permit(:tiptap_body)
params.required(:attestation_template).permit(:official_layout, :label_logo, :label_direction, :tiptap_body, :footer, :logo, :signature, :activated)
end
end
end

View file

@ -14,7 +14,7 @@ module Administrateurs
end
def update
@attestation_template = @procedure.attestation_template
@attestation_template = @procedure.attestation_template_v1
if @attestation_template.update(activated_attestation_params)
flash.notice = "Le modèle de lattestation a bien été modifié"
@ -50,7 +50,7 @@ module Administrateurs
private
def build_attestation_template(attributes = {})
attestation_template = @procedure.attestation_template || @procedure.build_attestation_template
attestation_template = @procedure.attestation_template_v1 || @procedure.build_attestation_template_v1
attestation_template.attributes = attributes
attestation_template
end

View file

@ -111,7 +111,8 @@ module Administrateurs
types_de_champ: [],
revision_types_de_champ: { type_de_champ: { piece_justificative_template_attachment: :blob } }
},
attestation_template: [],
attestation_template_v1: [],
attestation_template_v2: [],
initiated_mail: [],
received_mail: [],
closed_mail: [],

View file

@ -0,0 +1,44 @@
import { toggle } from '@utils';
import { ApplicationController } from './application_controller';
export class AttestationController extends ApplicationController {
static targets = [
'layoutToggle',
'logoMarianneLabelFieldset',
'logoAttachmentFieldset'
];
static values = {
logoAttachmentOfficialLabel: String,
logoAttachmentFreeLabel: String
};
declare readonly layoutToggleTarget: HTMLInputElement;
declare readonly logoMarianneLabelFieldsetTarget: HTMLElement;
declare readonly logoAttachmentFieldsetTarget: HTMLElement;
declare readonly logoAttachmentOfficialLabelValue: string;
declare readonly logoAttachmentFreeLabelValue: string;
connect() {
this.layoutToggleTarget.addEventListener('change', () => {
this.update();
});
}
private get isStateLayout() {
return this.layoutToggleTarget.checked;
}
private update() {
toggle(this.logoMarianneLabelFieldsetTarget, this.isStateLayout);
const logoAttachmentLabel =
this.logoAttachmentFieldsetTarget.querySelector('label');
if (logoAttachmentLabel) {
logoAttachmentLabel.innerText = this.isStateLayout
? this.logoAttachmentOfficialLabelValue
: this.logoAttachmentFreeLabelValue;
}
}
}

View file

@ -2,17 +2,20 @@ import { isFormInputElement, matchInputElement } from '@coldwired/utils';
import { ApplicationController } from './application_controller';
const AUTOSUBMIT_DEBOUNCE_DELAY = 500;
const AUTOSUBMIT_DATE_DEBOUNCE_DELAY = 5000;
const AUTOSUBMIT_EVENTS = ['input', 'change', 'blur'];
export class AutosubmitController extends ApplicationController {
static targets = ['submitter', 'input'];
static values = {
debounceDelay: { type: Number, default: 500 }
};
declare readonly submitterTarget: HTMLButtonElement | HTMLInputElement;
declare readonly hasSubmitterTarget: boolean;
declare readonly inputTarget: HTMLInputElement;
declare readonly hasInputTarget: boolean;
declare readonly debounceDelayValue: number;
#dateTimeChangedInputs = new WeakSet<HTMLElement>();
@ -46,8 +49,8 @@ export class AutosubmitController extends ApplicationController {
matchInputElement(target, {
date: () => {},
inputable: () => this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY),
hidden: () => this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY)
inputable: () => this.debounce(this.submit, this.debounceDelayValue),
hidden: () => this.debounce(this.submit, this.debounceDelayValue)
});
}

View file

@ -0,0 +1,44 @@
import { ApplicationController } from './application_controller';
export class TextareaController extends ApplicationController {
static values = {
maxRows: Number
};
declare readonly maxRowsValue: number;
connect() {
if (this.maxRowsValue) {
this.attachEvents();
}
}
private attachEvents() {
this.on('keyup', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
this.processTextareaContent(event);
}
});
this.on('paste', (event: ClipboardEvent) => {
// Wait for the paste event to complete
setTimeout(() => this.processTextareaContent(event), 0);
});
}
private processTextareaContent(event: Event) {
const target = event.target as HTMLTextAreaElement;
let lines = target.value.split('\n');
if (lines.length > this.maxRowsValue) {
// Truncate lines to the maximum allowed
lines = lines.slice(0, this.maxRowsValue);
target.value = lines.join('\n');
if (event instanceof KeyboardEvent) {
// Prevent the default action only for KeyboardEvent (enter key)
event.preventDefault();
}
}
}
}

View file

@ -9,11 +9,15 @@ import { createEditor } from '../shared/tiptap/editor';
export class TiptapController extends ApplicationController {
static targets = ['editor', 'input', 'button', 'tag'];
static values = {
insertAfterTag: { type: String, default: '' }
};
declare editorTarget: Element;
declare inputTarget: HTMLInputElement;
declare buttonTargets: HTMLButtonElement[];
declare tagTargets: HTMLElement[];
declare insertAfterTagValue: string;
#initializing = true;
#editor?: Editor;
@ -58,11 +62,15 @@ export class TiptapController extends ApplicationController {
insertTag(event: MouseEvent) {
if (this.#editor && isHTMLElement(event.target)) {
const tag = tagSchema.parse(event.target.dataset);
this.#editor
const editor = this.#editor
.chain()
.focus()
.insertContent({ type: 'mention', attrs: tag })
.run();
.insertContent({ type: 'mention', attrs: tag });
if (this.insertAfterTagValue != '') {
editor.insertContent({ type: 'text', text: this.insertAfterTagValue });
}
editor.run();
}
}

View file

@ -21,7 +21,7 @@ const EDITOR_ACTIONS: Record<string, (editor: Editor) => EditorAction> = {
isDisabled: () => !editor.isActive('title')
}),
heading2: (editor) => ({
run: () => editor.chain().focus().setHeading({ level: 2 }).run(),
run: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => editor.isActive('heading', { level: 2 }),
isDisabled: () =>
editor.isActive('title') ||
@ -29,7 +29,7 @@ const EDITOR_ACTIONS: Record<string, (editor: Editor) => EditorAction> = {
editor.isActive('footer')
}),
heading3: (editor) => ({
run: () => editor.chain().focus().setHeading({ level: 3 }).run(),
run: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: () => editor.isActive('heading', { level: 3 }),
isDisabled: () =>
editor.isActive('title') ||

View file

@ -25,13 +25,7 @@ import {
type Extensions
} from '@tiptap/core';
import {
DocumentWithHeader,
Title,
Header,
Footer,
HeaderColumn
} from './nodes';
import { DocumentWithHeader, Title, Header, HeaderColumn } from './nodes';
import { createSuggestionMenu, type TagSchema } from './tags';
export function createEditor({
@ -83,24 +77,8 @@ function getEditorOptions(
case 'orderedList':
extensions.push(OrderedList);
break;
case 'left':
case 'center':
case 'right':
case 'justify':
extensions.push(
TextAlign.configure({
types: actions.includes('title')
? ['headerColumn', 'title', 'footer', 'heading', 'paragraph']
: ['heading', 'paragraph']
})
);
break;
case 'title':
extensions.push(Header, HeaderColumn, Title, Footer);
break;
case 'heading2':
case 'heading3':
extensions.push(Heading.configure({ levels: [2, 3] }));
extensions.push(Header, HeaderColumn, Title);
break;
}
}
@ -108,14 +86,34 @@ function getEditorOptions(
if (actions.includes('bulletList') || actions.includes('orderedList')) {
extensions.push(ListItem);
}
if (actions.includes('heading2') || actions.includes('heading3')) {
extensions.push(Heading.configure({ levels: [2, 3] }));
}
if (
actions.includes('left') ||
actions.includes('center') ||
actions.includes('right') ||
actions.includes('justify')
) {
extensions.push(
TextAlign.configure({
types: actions.includes('title')
? ['headerColumn', 'title', 'heading', 'paragraph']
: ['heading', 'paragraph']
})
);
}
if (tags.length > 0) {
extensions.push(
Mention.configure({
renderLabel({ node }) {
return `--${node.attrs.label}--`;
return node.attrs.label;
},
HTMLAttributes: {
class: 'fr-badge fr-badge--sm fr-badge--info fr-badge--no-icon'
class: 'fr-tag fr-tag--sm'
},
suggestion: createSuggestionMenu(tags, element)
})

View file

@ -3,7 +3,7 @@ import { Node, mergeAttributes } from '@tiptap/core';
export const DocumentWithHeader = Node.create({
name: 'doc',
topNode: true,
content: 'header title block+ footer'
content: 'header title block+'
});
export const Title = Node.create({
@ -31,28 +31,15 @@ export const Header = Node.create({
renderHTML({ HTMLAttributes }) {
return [
'header',
mergeAttributes(HTMLAttributes, { class: 'header flex' }),
mergeAttributes(HTMLAttributes, { class: 'header flex flex-gap-1' }),
0
];
}
});
export const Footer = Node.create({
name: 'footer',
content: 'paragraph+',
defining: true,
parseHTML() {
return [{ tag: `footer` }];
},
renderHTML({ HTMLAttributes }) {
return ['footer', mergeAttributes(HTMLAttributes, { class: 'footer' }), 0];
}
});
export const HeaderColumn = Node.create({
name: 'headerColumn',
content: 'paragraph',
content: 'paragraph{1,2}',
defining: true,
parseHTML() {

View file

@ -84,35 +84,56 @@ class SuggestionMenu {
destroy() {
this.#popup?.destroy();
this.#element?.remove();
this.#element?.removeEventListener('click', this.handleItemClick);
}
private render() {
if (this.#props.items.length == 0) {
this.#element?.remove();
return;
}
this.#element ??= this.createMenu();
const list = this.#element.firstChild as HTMLUListElement;
const html = this.#props.items
.map((item, i) => {
return `<li class="fr-badge fr-badge--sm fr-badge--no-icon${
i == this.#selectedIndex ? ' fr-badge--info' : ''
}">${item.label}</li>`;
return `<li><button class="fr-tag fr-tag--sm" aria-pressed="${
i == this.#selectedIndex ? 'true' : 'false'
}" data-tag-index="${i}">${item.label}</button></li>`;
})
.join('');
this.#element.classList.add('fr-menu__list');
list.innerHTML = html;
const hint =
'<li><span class="fr-hint-text">Tapez le nom dune balise, naviguez avec les flèches, validez avec Entrée ou en cliquant sur la balise.</span></li>';
list.innerHTML = hint + html;
list.querySelector<HTMLElement>('.selected')?.focus();
}
private createMenu() {
const menu = document.createElement('div');
const list = document.createElement('ul');
menu.classList.add('fr-menu');
list.classList.add('fr-menu__list');
const list = document.createElement('ul');
list.classList.add('fr-menu__list', 'fr-tag-list', 'list-style-type-none');
menu.appendChild(list);
menu.addEventListener('click', this.handleItemClick);
return menu;
}
private handleItemClick = (event: Event) => {
const target = event.target as HTMLElement;
if (!target || target.dataset.tagIndex === undefined) {
return;
}
this.#props.command(this.#props.items[Number(target.dataset.tagIndex)]);
this.#popup?.hide();
};
private up() {
this.#selectedIndex =
(this.#selectedIndex + this.#props.items.length - 1) %
@ -145,6 +166,8 @@ export function createSuggestionMenu(
): Omit<SuggestionOptions<TagSchema>, 'editor'> {
return {
char: '@',
allowedPrefixes: null,
allowSpaces: true,
items: ({ query }) => {
return matchSorter(tags, query, { keys: ['label'] }).slice(0, 6);
},

View file

@ -2,13 +2,14 @@ class AttestationTemplate < ApplicationRecord
include ActionView::Helpers::NumberHelper
include TagsSubstitutionConcern
belongs_to :procedure, inverse_of: :attestation_template
belongs_to :procedure, inverse_of: :attestation_template_v2
has_one_attached :logo
has_one_attached :signature
validates :title, tags: true, if: -> { procedure.present? }
validates :body, tags: true, if: -> { procedure.present? }
validates :title, tags: true, if: -> { procedure.present? && version == 1 }
validates :body, tags: true, if: -> { procedure.present? && version == 1 }
validates :json_body, tags: true, if: -> { procedure.present? && version == 2 }
validates :footer, length: { maximum: 190 }
FILE_MAX_SIZE = 1.megabytes
@ -17,6 +18,54 @@ class AttestationTemplate < ApplicationRecord
DOSSIER_STATE = Dossier.states.fetch(:accepte)
scope :v1, -> { where(version: 1) }
scope :v2, -> { where(version: 2) }
TIPTAP_BODY_DEFAULT = {
"type" => "doc",
"content" => [
{
"type" => "header",
"content" => [
{
"type" => "headerColumn",
"content" => [
{
"type" => "paragraph",
"attrs" => { "textAlign" => "left" },
"content" => [{ "type" => "mention", "attrs" => { "id" => "dossier_service_name", "label" => "nom du service" } }]
}
]
},
{
"type" => "headerColumn",
"content" => [
{
"type" => "paragraph",
"attrs" => { "textAlign" => "left" },
"content" => [
{ "text" => "Fait le ", "type" => "text" },
{ "type" => "mention", "attrs" => { "id" => "dossier_processed_at", "label" => "date de décision" } }
]
}
]
}
]
},
{ "type" => "title", "attrs" => { "textAlign" => "center" }, "content" => [{ "text" => "Titre de lattestation", "type" => "text" }] },
{
"type" => "paragraph",
"attrs" => { "textAlign" => "left" },
"content" => [
{
"text" => "Vous pouvez éditer ce texte pour personnaliser votre attestation. Pour ajouter du contenu issu du dossier, utilisez les balises situées sous cette zone de saisie.",
"type" => "text"
}
]
}
]
}.freeze
def attestation_for(dossier)
attestation = Attestation.new(title: replace_tags(title, dossier, escape: false))
attestation.pdf.attach(
@ -60,26 +109,19 @@ class AttestationTemplate < ApplicationRecord
end
def render_attributes_for(params = {})
attributes = {
created_at: Time.zone.now,
groupe_instructeur = params[:groupe_instructeur]
groupe_instructeur ||= params[:dossier]&.groupe_instructeur
base_attributes = {
created_at: Time.current,
footer: params.fetch(:footer, footer),
logo: params.fetch(:logo, logo.attached? ? logo : nil)
signature: signature_to_render(groupe_instructeur)
}
dossier = params[:dossier]
if dossier.present?
attributes.merge({
title: replace_tags(title, dossier, escape: false),
body: replace_tags(body, dossier, escape: false),
signature: signature_to_render(dossier.groupe_instructeur)
})
if version == 2
render_attributes_for_v2(params, base_attributes)
else
attributes.merge({
title: params.fetch(:title, title),
body: params.fetch(:body, body),
signature: signature_to_render(params[:groupe_instructeur])
})
render_attributes_for_v1(params, base_attributes)
end
end
@ -109,6 +151,48 @@ class AttestationTemplate < ApplicationRecord
private
def render_attributes_for_v1(params, base_attributes)
attributes = base_attributes.merge(
logo: params.fetch(:logo, logo.attached? ? logo : nil)
)
dossier = params[:dossier]
if dossier.present?
attributes.merge(
title: replace_tags(title, dossier, escape: false),
body: replace_tags(body, dossier, escape: false)
)
else
attributes.merge(
title: params.fetch(:title, title),
body: params.fetch(:body, body)
)
end
end
def render_attributes_for_v2(params, base_attributes)
dossier = params[:dossier]
json = json_body&.deep_symbolize_keys
tiptap = TiptapService.new
if dossier.present?
# 2x faster this way than with `replace_tags` which would reparse text
used_tags = tiptap.used_tags_and_libelle_for(json.deep_symbolize_keys)
substitutions = tags_substitutions(used_tags, dossier, escape: false)
body = tiptap.to_html(json, substitutions)
attributes.merge(
body:
)
else
attributes.merge(
body: params.fetch(:body) { tiptap.to_html(json) }
)
end
end
def signature_to_render(groupe_instructeur)
if groupe_instructeur&.signature&.attached?
groupe_instructeur.signature

View file

@ -155,14 +155,14 @@ module TagsSubstitutionConcern
available_for_states: Dossier::SOUMIS
},
{
id: 'individual_first_name',
id: 'individual_last_name',
libelle: 'nom',
description: "nom de l'usager",
target: :nom,
available_for_states: Dossier::SOUMIS
},
{
id: 'individual_last_name',
id: 'individual_first_name',
libelle: 'prénom',
description: "prénom de l'usager",
target: :prenom,
@ -240,8 +240,26 @@ module TagsSubstitutionConcern
tags_for_dossier_state(identity_tags + dossier_tags + champ_public_tags + champ_private_tags + routage_tags)
end
def used_type_de_champ_tags(text)
used_tags_and_libelle_for(text).filter_map do |(tag, libelle)|
def tags_categorized
identity_key = procedure.for_individual? ? :individual : :etablissement
{
identity_key => tags_for_dossier_state(identity_tags),
dossier: tags_for_dossier_state(dossier_tags + routage_tags),
champ_public: tags_for_dossier_state(champ_public_tags),
champ_private: tags_for_dossier_state(champ_private_tags)
}.reject { |_, ary| ary.empty? }
end
def used_type_de_champ_tags(text_or_tiptap)
used_tags =
if text_or_tiptap.respond_to?(:deconstruct_keys) # hash pattern matching
TiptapService.new.used_tags_and_libelle_for(text_or_tiptap.deep_symbolize_keys)
else
used_tags_and_libelle_for(text_or_tiptap.to_s)
end
used_tags.filter_map do |(tag, libelle)|
if tag.nil?
[libelle]
elsif !tag.in?(SHARED_TAG_IDS) && tag.start_with?('tdc')
@ -254,6 +272,34 @@ module TagsSubstitutionConcern
used_tags_and_libelle_for(text).map { _1.first.nil? ? _1.second : _1.first }
end
def tags_substitutions(tags_and_libelles, dossier, escape: true)
# NOTE:
# - tags_and_libelles est un simple Set de couples (tag_id, libelle) (pas la même structure que dans replace_tags)
# - dans `replace_tags`, on fait référence à des tags avec ou sans id, mais pas ici,
# (inutile car tiptap ne référence que des ids)
@escape_unsafe_tags = escape
flat_tags = tags_and_datas_list(dossier).each_with_object({}) do |(tags, data), result|
next if data.nil?
valid_tags = tags_for_dossier_state(tags)
valid_tags.each do |tag|
result[tag[:id]] = [tag, data]
end
end
tags_and_libelles.each_with_object({}) do |(tag_id, libelle), substitutions|
substitutions[tag_id] = case flat_tags[tag_id]
in tag, data
replace_tag(tag, data)
else # champ not in dossier, for example during preview on draft revision
libelle
end
end
end
private
def format_date(date)
@ -323,14 +369,7 @@ module TagsSubstitutionConcern
tokens = parse_tags(text)
tags_and_datas = [
[champ_public_tags(dossier: dossier), dossier.champs_public],
[champ_private_tags(dossier: dossier), dossier.champs_private],
[dossier_tags, dossier],
[ROUTAGE_TAGS, dossier],
[INDIVIDUAL_TAGS, dossier.individual],
[ENTREPRISE_TAGS, dossier.etablissement&.entreprise]
].filter_map do |(tags, data)|
tags_and_datas = tags_and_datas_list(dossier).filter_map do |(tags, data)|
data && [tags_for_dossier_state(tags).index_by { _1[:id] }, data]
end
@ -408,4 +447,15 @@ module TagsSubstitutionConcern
end
end
end
def tags_and_datas_list(dossier)
[
[champ_public_tags(dossier:), dossier.champs_public],
[champ_private_tags(dossier:), dossier.champs_private],
[dossier_tags, dossier],
[ROUTAGE_TAGS, dossier],
[INDIVIDUAL_TAGS, dossier.individual],
[ENTREPRISE_TAGS, dossier.etablissement&.entreprise]
]
end
end

View file

@ -47,7 +47,11 @@ class Procedure < ApplicationRecord
foreign_key: "replaced_by_procedure_id", dependent: :nullify
has_one :module_api_carto, dependent: :destroy
has_one :attestation_template, dependent: :destroy
has_many :attestation_templates, dependent: :destroy
has_one :attestation_template_v1, -> { AttestationTemplate.v1 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
has_one :attestation_template_v2, -> { AttestationTemplate.v2 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
has_one :attestation_template, -> { AttestationTemplate.v1.or(AttestationTemplate.v2) }, dependent: :destroy, inverse_of: :procedure
belongs_to :parent_procedure, class_name: 'Procedure', optional: true
belongs_to :canonical_procedure, class_name: 'Procedure', optional: true
@ -988,6 +992,14 @@ class Procedure < ApplicationRecord
draft_revision.revision_types_de_champ_public.filter { _1.type_de_champ.header_section? }
end
def dossier_for_preview(user)
# Try to use a preview or a dossier filled by current user
dossiers.where(for_procedure_preview: true).or(dossiers.not_brouillon)
.order(Arel.sql("CASE WHEN for_procedure_preview = True THEN 1 ELSE 0 END DESC,
CASE WHEN user_id = #{user.id} THEN 1 ELSE 0 END DESC")) \
.first
end
private
def pieces_jointes_list

View file

@ -1,74 +1,99 @@
class TiptapService
class << self
def to_html(node, tags)
return '' if node.nil?
def to_html(node, substitutions = {})
return '' if node.nil?
children(node[:content], tags)
children(node[:content], substitutions, 0)
end
# NOTE: node must be deep symbolized keys
def used_tags_and_libelle_for(node, tags = Set.new)
case node
in type: 'mention', attrs: { id:, label: }, **rest
tags << [id, label]
in { content:, **rest } if content.is_a?(Array)
content.each { used_tags_and_libelle_for(_1, tags) }
in type:, **rest
# noop
end
private
tags
end
def children(content, tags)
content.map { node_to_html(_1, tags) }.join
private
def initialize
@body_started = false
end
def children(content, substitutions, level)
content.map { node_to_html(_1, substitutions, level) }.join
end
def node_to_html(node, substitutions, level)
if level == 0 && !@body_started && node[:type].in?(['paragraph', 'heading']) && node.key?(:content)
@body_started = true
body_start_mark = " class=\"body-start\""
end
def node_to_html(node, tags)
case node
in type: 'header', content:
"<header>#{children(content, tags)}</header>"
in type: 'footer', content:, **rest
"<footer#{text_align(rest[:attrs])}>#{children(content, tags)}</footer>"
in type: 'headerColumn', content:, **rest
"<div#{text_align(rest[:attrs])} class=\"column\">#{children(content, tags)}</div>"
in type: 'paragraph', content:, **rest
"<p#{text_align(rest[:attrs])}>#{children(content, tags)}</p>"
in type: 'title', content:, **rest
"<h1#{text_align(rest[:attrs])}>#{children(content, tags)}</h1>"
in type: 'heading', attrs: { level:, **attrs }, content:
"<h#{level}#{text_align(attrs)}>#{children(content, tags)}</h#{level}>"
in type: 'bulletList', content:
"<ul>#{children(content, tags)}</ul>"
in type: 'orderedList', content:
"<ol>#{children(content, tags)}</ol>"
in type: 'listItem', content:
"<li>#{children(content, tags)}</li>"
in type: 'text', text:, **rest
if rest[:marks].present?
apply_marks(text, rest[:marks])
else
text
end
in type: 'mention', attrs: { id: }, **rest
if rest[:marks].present?
apply_marks(tags[id], rest[:marks])
else
tags[id]
end
end
end
def text_align(attrs)
if attrs.present? && attrs[:textAlign].present?
" style=\"text-align: #{attrs[:textAlign]}\""
case node
in type: 'header', content:
"<header>#{children(content, substitutions, level + 1)}</header>"
in type: 'footer', content:, **rest
"<footer#{text_align(rest[:attrs])}>#{children(content, substitutions, level + 1)}</footer>"
in type: 'headerColumn', content:, **rest
"<div#{text_align(rest[:attrs])}>#{children(content, substitutions, level + 1)}</div>"
in type: 'paragraph', content:, **rest
"<p#{body_start_mark}#{text_align(rest[:attrs])}>#{children(content, substitutions, level + 1)}</p>"
in type: 'title', content:, **rest
"<h1#{text_align(rest[:attrs])}>#{children(content, substitutions, level + 1)}</h1>"
in type: 'heading', attrs: { level: hlevel, **attrs }, content:
"<h#{hlevel}#{body_start_mark}#{text_align(attrs)}>#{children(content, substitutions, level + 1)}</h#{hlevel}>"
in type: 'bulletList', content:
"<ul>#{children(content, substitutions, level + 1)}</ul>"
in type: 'orderedList', content:
"<ol>#{children(content, substitutions, level + 1)}</ol>"
in type: 'listItem', content:
"<li>#{children(content, substitutions, level + 1)}</li>"
in type: 'text', text:, **rest
if rest[:marks].present?
apply_marks(text, rest[:marks])
else
""
text
end
end
in type: 'mention', attrs: { id: }, **rest
text = substitutions.fetch(id) { "--#{id}--" }
def apply_marks(text, marks)
marks.reduce(text) do |text, mark|
case mark
in type: 'bold'
"<strong>#{text}</strong>"
in type: 'italic'
"<em>#{text}</em>"
in type: 'underline'
"<u>#{text}</u>"
in type: 'strike'
"<s>#{text}</s>"
in type: 'highlight'
"<mark>#{text}</mark>"
end
if rest[:marks].present?
apply_marks(text, rest[:marks])
else
text
end
in { type: type } if ["paragraph", "title", "heading"].include?(type) && !node.key?(:content)
# noop
end
end
def text_align(attrs)
if attrs.present? && attrs[:textAlign].present?
" style=\"text-align: #{attrs[:textAlign]}\""
else
""
end
end
def apply_marks(text, marks)
marks.reduce(text) do |text, mark|
case mark
in type: 'bold'
"<strong>#{text}</strong>"
in type: 'italic'
"<em>#{text}</em>"
in type: 'underline'
"<u>#{text}</u>"
in type: 'strike'
"<s>#{text}</s>"
in type: 'highlight'
"<mark>#{text}</mark>"
end
end
end

View file

@ -1,16 +1,16 @@
class TagsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
procedure = record.procedure
tags = record.used_type_de_champ_tags(value || '')
tags = record.used_type_de_champ_tags(value)
invalid_tags = tags.filter_map do |(tag, stable_id)|
tag if stable_id.nil?
end
invalid_for_draft_revision = invalid_tags_for_revision(record, attribute, tags, procedure.draft_revision)
invalid_for_draft_revision = invalid_tags_for_revision(record, tags, procedure.draft_revision)
invalid_for_published_revision = if procedure.published_revision_id.present?
invalid_tags_for_revision(record, attribute, tags, procedure.published_revision)
invalid_tags_for_revision(record, tags, procedure.published_revision)
else
[]
end
@ -18,7 +18,7 @@ class TagsValidator < ActiveModel::EachValidator
invalid_for_previous_revision = procedure
.revisions_with_pending_dossiers
.flat_map do |revision|
invalid_tags_for_revision(record, attribute, tags, revision)
invalid_tags_for_revision(record, tags, revision)
end.uniq
# champ is added in draft revision but not yet published
@ -48,7 +48,7 @@ class TagsValidator < ActiveModel::EachValidator
end
end
def invalid_tags_for_revision(record, attribute, tags, revision)
def invalid_tags_for_revision(record, tags, revision)
revision_stable_ids = revision
.revision_types_de_champ
.filter { !_1.child? }

View file

@ -1 +1,2 @@
#autosave-notice.fr-badge.fr-badge--sm.fr-badge--success= t(".form_saved")
- success = local_assigns.fetch(:success, true)
#autosave-notice.fr-badge.fr-badge--sm{ class: class_names("fr-badge--success" => success, "fr-badge--error" => !success) }= success ? t(".form_saved") : t(".form_error")

View file

@ -1,16 +1,134 @@
#attestation-edit.fr-container.mt-2{ data: { controller: 'tiptap' } }
= form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure), data: { turbo: 'true', controller: 'autosubmit' } do |form|
.flex.flex-gap-2
- @buttons.each do |buttons|
.flex.flex-gap-1
- buttons.each do |(label, action, icon)|
%button.fr-btn.fr-btn--secondary.fr-btn--sm{ type: 'button', title: label, class: "fr-icon-#{icon}", data: { action: 'click->tiptap#menuButton', tiptap_target: 'button', tiptap_action: action } }
= label
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Attestation']] }
.editor.mt-2{ data: { tiptap_target: 'editor' } }
= form.hidden_field :tiptap_body, data: { tiptap_target: 'input' }
= render NestedForms::FormOwnerComponent.new
= form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure), html: { multipart: true },
data: { turbo: 'true',
controller: 'autosubmit attestation',
autosubmit_debounce_delay_value: 1000,
attestation_logo_attachment_official_label_value: AttestationTemplate.human_attribute_name(:logo_additional),
attestation_logo_attachment_free_label_value: AttestationTemplate.human_attribute_name(:logo) } do |f|
%ul.mt-2.flex.wrap.flex-gap-1
- @attestation_template.tags.each do |tag|
%li.fr-badge.fr-badge--sm{ role: 'button', title: tag[:description], data: { action: 'click->tiptap#insertTag', tiptap_target: 'tag', tag_id: tag[:id], tag_label: tag[:libelle] } }
= tag[:libelle]
#attestation-edit.fr-container.fr-mt-4w{ data: { controller: 'tiptap', tiptap_insert_after_tag_value: ' ' } }
.fr-mb-6w
= render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur dattestation", heading_level: 'h3') do |c|
- c.with_body do
Cette page permet la mise en forme de lattestation avec un nouvel éditeur plus flexible
tout en respectant la charte de létat. Essayez-la et donnez-nous votre avis
en nous envoyant un email à #{mail_to(CONTACT_EMAIL, subject: "Feedback attestation v2")}.
%br
%strong Les attestations délivrées suivent encore lancien format :
lactivation des attestations basées sur ce format sera bientôt disponible.
%br
= link_to("Suivez ce lien pour revenir aux attestations actuellement délivrées", edit_admin_procedure_attestation_template_path(@procedure))
.fr-grid-row.fr-grid-row--gutters
.fr-col-12.fr-col-md-8
%fieldset.fr-fieldset{ aria: { labelledby: 'edit-attestation' } }
%legend.fr-fieldset__legend#edit-attestation
%h1.fr-h2 Attestation
%p.fr-text--regular
Lattestation est émise au moment où un dossier est accepté, elle est jointe à lemail daccusé dacceptation.
Elle est également disponible au téléchargement depuis lespace personnel de lusager.
.fr-fieldset__element
%h2.fr-h4 En-tête
.fr-fieldset__element
.fr-toggle
= f.check_box :official_layout, class: "fr-toggle-input", id: dom_id(@attestation_template, :official_layout), data: { "attestation-target": "layoutToggle"}
%label.fr-toggle__label{ for: dom_id(@attestation_template, :official_layout), data: { fr_checked_label: "Activé", fr_unchecked_label: "Désactivé" } }
Je souhaite générer une attestation à la charte de létat (logo avec Marianne)
.fr-fieldset__element{ class: class_names("hidden" => !@attestation_template.official_layout?), data: { "attestation-target": 'logoMarianneLabelFieldset'} }
= render Dsfr::InputComponent.new(form: f, attribute: :label_logo, input_type: :text_area, required: @attestation_template.official_layout?, opts: { rows: 3, data: { controller: :textarea, textarea_max_rows_value: 3 } }) do |c|
- c.with_hint { "Exemple: Ministère de la Mer. 3 lignes maximum" }
.fr-fieldset__element{ data: { attestation_target: 'logoAttachmentFieldset' } }
%label.fr-label{ for: field_id(@attestation_template, :logo) }
- if @attestation_template.official_layout?
= AttestationTemplate.human_attribute_name(:logo_additional)
- else
= AttestationTemplate.human_attribute_name(:logo)
%span.fr-hint-text
Dimensions conseillées : au minimum 500px de largeur ou de hauteur.
%div{ id: dom_id(@attestation_template, :logo_attachment) }
= render Attachment::EditComponent.new(attached_file: @attestation_template.logo, direct_upload: false)
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :label_direction, input_type: :text_area, required: false, opts: { rows: 2, data: { controller: :textarea, textarea_max_rows_value: 2 } }) do |c|
- c.with_hint { "Exemple: Direction interministérielle du numérique. 2 lignes maximum" }
.fr-fieldset__element.fr-mt-2w
.fr-input-group{ class: class_names("fr-input-group--error" => f.object.errors.include?(:json_body)) }
%label.fr-label.fr-h4
= AttestationTemplate.human_attribute_name :body
= render EditableChamp::AsteriskMandatoryComponent.new
#editor.editor{ data: { tiptap_target: 'editor' }, aria: { describedby: dom_id(f.object, "json-body-messages")} }
= f.hidden_field :tiptap_body, data: { tiptap_target: 'input' }
.fr-error-text{ id: dom_id(f.object, "json-body-messages"), class: class_names("hidden" => !f.object.errors.include?(:json_body)) }
- if f.object.errors.include?(:json_body)
= render partial: "shared/errors_list", locals: { object: f.object, attribute: :json_body }
.fr-fieldset__element
.flex.flex-gap-2
- @buttons.each do |buttons|
.flex.flex-gap-1
- buttons.each do |(label, action, icon)|
%button.fr-btn.fr-btn--secondary.fr-btn--sm{ type: 'button', title: label, class: icon == :hidden ? "hidden" : "fr-icon-#{icon}", data: { action: 'click->tiptap#menuButton', tiptap_target: 'button', tiptap_action: action } }
= label
.fr-fieldset__element
%p.fr-hint-text
Tapez le caractère
%strong.fr-text-title--grey @
suivi du nom de la balise, ou cliquez sur les boutons ci-dessous. Les champs conditionnés ne sont pas disponibles.
= render TagsButtonListComponent.new(tags: @attestation_template.tags_categorized)
.fr-fieldset__element.fr-mt-2w
%h2.fr-h4 Pied de page
.fr-fieldset__element
%label.fr-label{ for: field_id(@attestation_template, :signature) } Tampon ou signature
%span.fr-hint-text
Dimensions conseillées : au minimum 500px de largeur ou de hauteur.
%div{ id: dom_id(@attestation_template, :signature_attachment) }
= render Attachment::EditComponent.new(attached_file: @attestation_template.signature, direct_upload: false)
.fr-fieldset__element
= render Dsfr::InputComponent.new(form: f, attribute: :footer, input_type: :text_area, required: false, opts: { rows: 3, data: { controller: :textarea, textarea_max_rows_value: 3 } }) do |c|
- c.with_hint { "Exemple: 20 avenue de Ségur, 75007 Paris" }
.fr-col-12.fr-col-md-4.fr-background-alt--blue-france
= image_tag("attestation-template-schema-official.jpg", class: "attestation-schema", alt: "Schéma dune attestation au modèle de létat")
.padded-fixed-footer
.fixed-footer
.fr-container
.fr-grid-row
.fr-col-12.fr-col-md-7
%ul.fr-btns-group.fr-btns-group--inline-md
%li
= link_to 'Prévisualiser lattestation PDF', admin_procedure_attestation_template_v2_path(@procedure, format: :pdf), class: 'fr-btn fr-btn', target: '_blank', rel: 'noopener'
%li
= link_to admin_procedure_path(id: @procedure), class: 'fr-btn fr-btn--secondary' do
%span.fr-icon-arrow-go-back-line.fr-icon--sm.fr-mr-1v
Revenir à la démarche
.fr-col-12.fr-col-md-5
-# .fr-toggle
-# = f.check_box :activated, class: "fr-toggle-input", disabled: true, id: dom_id(@attestation_template, :activated)
-# %label.fr-toggle__label{ for: dom_id(@attestation_template, :activated), data: { fr_checked_label: "Attestation activée", fr_unchecked_label: "Attestation désactivée" } }
.text-right
%span#autosave-notice
%p.fr-hint-text Lactivation de cette attestation sera bientôt disponible.

View file

@ -1,30 +1,30 @@
= image_tag('centered_marianne.svg', alt: '', class: 'marianne')
.header
.left
.bloc-marque
%p.intitule
PRÉFET<br />
DU VAL-<br />
DE-MARNE
= image_tag('liberte2.svg', alt: '', class: 'devise')
.a4-container{ class: class_names("official-layout": @attestation_template.official_layout?) }
.content
%header.first-header
.left
- if @attestation_template.official_layout?
= image_tag('centered_marianne.svg', alt: '', class: 'marianne')
.bloc-marque
= simple_format @attestation_template.label_logo.presence || "INTITULE de\nVOTRE INSTITUTION", class: "intitule"
= image_tag('liberte2.svg', alt: '', class: 'devise')
- elsif @attestation_template.logo.present?
.bloc-marque.logo-free-layout
= image_tag(@attestation_template.logo_url)
%p.issuer
Service Hébergement et Accès au Logement<br />
Bureau de l'Accès au Logement
.right
- if @attestation_template.official_layout? && @attestation_template.logo.present?
.logo-co-emetteur
= image_tag(@attestation_template.logo_url)
.right
%p.direction
Direction Régionale et Interdépartementale<br />
de l'Hébergement et du Logement<br />
DRIHL Val-de-Marne
- if @attestation_template.label_direction.present?
= simple_format @attestation_template.label_direction, class: "direction"
%p.date Créteil, le 20 mars 2023
.main
= sanitize(@body, attributes: %w[class style], tags: Rails.configuration.action_view.sanitized_allowed_tags + %w[header])
%h1.title ATTESTATION
- if @attestation_template.signature.present?
.signature
= image_tag(@attestation_template.signature_url)
.main
= sanitize(@body)
%p.footer
12/14 rue des Archives 94000 Créteil<br />
www.drihl.ile-de-france.developpement-durable.gouv.fr
- if @attestation_template.footer.present?
= simple_format @attestation_template.footer, class: "footer"

View file

@ -0,0 +1,20 @@
= turbo_stream.show 'autosave-notice'
= turbo_stream.replace 'autosave-notice', render(partial: 'administrateurs/autosave_notice', locals: { success: !@attestation_template.changed? })
= turbo_stream.hide 'autosave-notice', delay: 15000
- if @attestation_template.logo_blob&.previously_new_record?
= turbo_stream.update dom_id(@attestation_template, :logo_attachment) do
= render(Attachment::EditComponent.new(attached_file: @attestation_template.logo, direct_upload: false))
- if @attestation_template.signature_blob&.previously_new_record?
= turbo_stream.update dom_id(@attestation_template, :signature_attachment) do
= render(Attachment::EditComponent.new(attached_file: @attestation_template.signature, direct_upload: false))
- body_id = dom_id(@attestation_template, "json-body-messages")
- if @attestation_template.errors.include?(:json_body)
= turbo_stream.update body_id do
= render partial: "shared/errors_list", locals: { object: @attestation_template, attribute: :json_body }
= turbo_stream.show body_id
- else
= turbo_stream.hide body_id
= turbo_stream.update body_id, nil

View file

@ -5,46 +5,54 @@
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Attestation']] }
.procedure-form#attestation-template-edit
.procedure-form__columns.container
= render NestedForms::FormOwnerComponent.new
= form_for @attestation_template,
url: admin_procedure_attestation_template_path(@procedure),
html: { multipart: true, class: 'form procedure-form__column--form fr-background-alt--blue-france' } do |f|
.fr-container
- if @procedure.feature_enabled?(:attestation_v2)
.fr-mb-6w
= render Dsfr::AlertComponent.new(state: :info, title: "Nouvel éditeur dattestation", heading_level: 'h3') do |c|
- c.with_body do
Cette page concerne lattestation actuellement délivrée aux usagers.
= link_to("Suivez ce lien pour tester le nouvel éditeur dattestation", edit_admin_procedure_attestation_template_v2_path(@procedure))
%h1.page-title
Délivrance dattestation
- if @attestation_template.activated?
%span.text-active activée
- else
%span.text-inactive désactivée
.procedure-form#attestation-template-edit
.procedure-form__columns
= render NestedForms::FormOwnerComponent.new
= form_for @attestation_template,
url: admin_procedure_attestation_template_path(@procedure),
html: { multipart: true, class: 'form procedure-form__column--form fr-background-alt--blue-france' } do |f|
%p.notice
Lattestation, si elle est activée, est émise au moment où un dossier est accepté.
%br
Lemail daccusé dacceptation envoyé à lusager comporte alors un lien vers lattestation ;
celle-ci est également disponible au téléchargement depuis lespace personnel de lusager.
%h1.page-title
Délivrance dattestation
- if @attestation_template.activated?
%span.text-active activée
- else
%span.text-inactive désactivée
= render partial: 'administrateurs/attestation_templates/informations', locals: { f: f }
%p.notice
Lattestation, si elle est activée, est émise au moment où un dossier est accepté.
%br
Lemail daccusé dacceptation envoyé à lusager comporte alors un lien vers lattestation ;
celle-ci est également disponible au téléchargement depuis lespace personnel de lusager.
.procedure-form__actions.sticky--bottom
.actions-left
%label.toggle-switch
= f.check_box :activated, class: 'toggle-switch-checkbox'
%span.toggle-switch-control.round
%span.toggle-switch-label.on Attestation activée
%span.toggle-switch-label.off Attestation désactivée
= render partial: 'administrateurs/attestation_templates/informations', locals: { f: f }
.actions-right
= link_to 'Annuler', edit_admin_procedure_attestation_template_path(id: @procedure), class: 'fr-btn fr-btn--secondary fr-mr-2w', data: { confirm: 'Êtes-vous sûr de vouloir annuler les modifications effectuées ?'}
= f.button 'Enregistrer', class: 'fr-btn'
.procedure-form__actions.sticky--bottom
.actions-left
%label.toggle-switch
= f.check_box :activated, class: 'toggle-switch-checkbox'
%span.toggle-switch-control.round
%span.toggle-switch-label.on Attestation activée
%span.toggle-switch-label.off Attestation désactivée
.procedure-form__column--preview
.procedure-form__preview.sticky--top
%h3
.procedure-form__preview-title
Aperçu
.notice
Cet aperçu est mis à jour après chaque sauvegarde.
.procedure-preview
= render partial: 'administrateurs/attestation_templates/apercu', locals: { procedure: @procedure }
.actions-right
= link_to 'Annuler', edit_admin_procedure_attestation_template_path(id: @procedure), class: 'fr-btn fr-btn--secondary fr-mr-2w', data: { confirm: 'Êtes-vous sûr de vouloir annuler les modifications effectuées ?'}
= f.button 'Enregistrer', class: 'fr-btn'
.procedure-form__column--preview
.procedure-form__preview.sticky--top
%h3
.procedure-form__preview-title
Aperçu
.notice
Cet aperçu est mis à jour après chaque sauvegarde.
.procedure-preview
= render partial: 'administrateurs/attestation_templates/apercu', locals: { procedure: @procedure }

View file

@ -0,0 +1,3 @@
%ul.list-style-type-none.fr-pl-0
- object.errors.full_messages_for(attribute).map do |error_message|
%li= error_message

View file

@ -4,9 +4,13 @@ fr:
attestation_template: 'Attestation'
attributes:
attestation_template:
label_logo: Intitulé de votre institution
label_direction: Intitulé de la direction
logo: Logo
logo_additional: Logo additionnel
title: Titre de lattestation
body: Contenu de lattestation
footer: Pied de page
footer: Contenu du pied de page
errors:
models:
@ -16,8 +20,8 @@ fr:
one: contient la balise "%{tags}" qui nexiste pas. Supprimer la balise
other: contient %{count} balises (%{tags}) qui nexistent pas. Supprimer les balises
champ_missing_in_draft_revision:
one: contient la balise "%{tags}" qui a été supprimée mais la suppression nest pas encore publiée. Publier la nouvelle version de la démarche et recommencer
other: contient %{count} balises (%{tags}) qui ont été supprimées mais la suppression nest pas encore publiée. Publier la nouvelle version de la démarche et recommencer
one: contient la balise "%{tags}" qui a été supprimée dans les modifications en cours du formulaire. Supprimer cette balise ou réinitialiser les modifications du formulaire puis recommencer
other: contient %{count} balises (%{tags}) qui ont été supprimées dans les modifications en coure du formumlaire. Supprimer cette balise ou réinitialiser les modifications du formulaire puis recommencer
champ_missing_in_published_revision:
one: contient la balise "%{tags}" qui nest pas encore publiée. Publier la nouvelle version de la démarche et recommencer
other: contient %{count} balises (%{tags}) qui ne sont pas encore publiées. Publier la nouvelle version de la démarche et recommencer
@ -34,3 +38,6 @@ fr:
body:
format: Le champ « Contenu de lattestation » %{message}
<<: *tags_errors
json_body:
format: Le champ « Contenu de lattestation » %{message}
<<: *tags_errors

View file

@ -53,3 +53,4 @@ en:
submit: Publish
autosave_notice:
form_saved: "Form saved"
form_error: "Form in error"

View file

@ -53,3 +53,4 @@ fr:
submit: Publier
autosave_notice:
form_saved: "Formulaire enregistré"
form_error: "Formulaire en erreur"

View file

@ -650,7 +650,7 @@ Rails.application.routes.draw do
get 'add_champ_engagement_juridique'
end
resource :attestation_template_v2, only: [:show, :edit, :update]
resource :attestation_template_v2, only: [:show, :edit, :update, :create]
resource :dossier_submitted_message, only: [:edit, :update, :create]
# ADDED TO ACCESS IT FROM THE IFRAME

View file

@ -0,0 +1,6 @@
class AddLabelsToAttestationTemplates < ActiveRecord::Migration[7.0]
def change
add_column :attestation_templates, :label_logo, :string, default: nil
add_column :attestation_templates, :label_direction, :string, default: nil
end
end

View file

@ -0,0 +1,5 @@
class AddLayoutToAttestationTemplates < ActiveRecord::Migration[7.0]
def change
add_column :attestation_templates, :official_layout, :boolean, default: true, null: false
end
end

View file

@ -0,0 +1,9 @@
class AddVersionToAttestationTemplates < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
add_column :attestation_templates, :version, :integer, default: 1, null: false
add_index :attestation_templates, [:procedure_id, :version], unique: true, algorithm: :concurrently
remove_index :attestation_templates, :procedure_id, unique: true, algorithm: :concurrently
end
end

View file

@ -151,10 +151,14 @@ ActiveRecord::Schema[7.0].define(version: 2024_01_26_071130) do
t.datetime "created_at", precision: nil, null: false
t.text "footer"
t.jsonb "json_body"
t.string "label_direction"
t.string "label_logo"
t.boolean "official_layout", default: true, null: false
t.integer "procedure_id"
t.text "title"
t.datetime "updated_at", precision: nil, null: false
t.index ["procedure_id"], name: "index_attestation_templates_on_procedure_id", unique: true
t.integer "version", default: 1, null: false
t.index ["procedure_id", "version"], name: "index_attestation_templates_on_procedure_id_and_version", unique: true
end
create_table "attestations", id: :serial, force: :cascade do |t|

View file

@ -102,6 +102,7 @@ namespace :benchmarks do
controller.request = ActionDispatch::TestRequest.create
controller.response = ActionDispatch::TestResponse.new
controller.request.env['warden'] = warden
# controller.request.path_parameters[:format] = 'pdf'
params = ENV.fetch("PARAMS") { "" }.split(",")
params.each do |param|

View file

@ -19,27 +19,27 @@
"@reach/slider": "^0.17.0",
"@sentry/browser": "7.66.0",
"@stimulus/polyfills": "^2.0.0",
"@tiptap/core": "^2.1.12",
"@tiptap/extension-bold": "^2.1.12",
"@tiptap/extension-bullet-list": "^2.1.12",
"@tiptap/extension-document": "^2.1.12",
"@tiptap/extension-gapcursor": "^2.1.12",
"@tiptap/extension-heading": "^2.1.12",
"@tiptap/extension-highlight": "^2.1.12",
"@tiptap/extension-history": "^2.1.12",
"@tiptap/extension-italic": "^2.1.12",
"@tiptap/extension-link": "^2.1.12",
"@tiptap/extension-list-item": "^2.1.12",
"@tiptap/extension-mention": "^2.1.12",
"@tiptap/extension-ordered-list": "^2.1.12",
"@tiptap/extension-paragraph": "^2.1.12",
"@tiptap/extension-strike": "^2.1.12",
"@tiptap/extension-text": "^2.1.12",
"@tiptap/extension-text-align": "^2.1.12",
"@tiptap/extension-typography": "^2.1.12",
"@tiptap/extension-underline": "^2.1.12",
"@tiptap/pm": "^2.1.12",
"@tiptap/suggestion": "^2.1.12",
"@tiptap/core": "^2.2.0",
"@tiptap/extension-bold": "^2.2.0",
"@tiptap/extension-bullet-list": "^2.2.0",
"@tiptap/extension-document": "^2.2.0",
"@tiptap/extension-gapcursor": "^2.2.0",
"@tiptap/extension-heading": "^2.2.0",
"@tiptap/extension-highlight": "^2.2.0",
"@tiptap/extension-history": "^2.2.0",
"@tiptap/extension-italic": "^2.2.0",
"@tiptap/extension-link": "^2.2.0",
"@tiptap/extension-list-item": "^2.2.0",
"@tiptap/extension-mention": "^2.2.0",
"@tiptap/extension-ordered-list": "^2.2.0",
"@tiptap/extension-paragraph": "^2.2.0",
"@tiptap/extension-strike": "^2.2.0",
"@tiptap/extension-text": "^2.2.0",
"@tiptap/extension-text-align": "^2.2.0",
"@tiptap/extension-typography": "^2.2.0",
"@tiptap/extension-underline": "^2.2.0",
"@tiptap/pm": "^2.2.0",
"@tiptap/suggestion": "^2.2.0",
"@tmcw/togeojson": "^5.6.0",
"chartkick": "^5.0.1",
"core-js": "^3.31.0",

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
class TagsButtonListComponentPreview < ViewComponent::Preview
include TagsSubstitutionConcern
def default
render(TagsButtonListComponent.new(tags:
{
individual: TagsSubstitutionConcern::INDIVIDUAL_TAGS,
etablissement: TagsSubstitutionConcern::ENTREPRISE_TAGS,
dossier: TagsSubstitutionConcern::DOSSIER_TAGS,
champ_public: [
{
id: 'tdc12',
libelle: 'Votre avis',
description: 'Détaillez votre avis'
},
{
id: 'tdc13',
libelle: 'Votre avis très ' + 'long ' * 12,
description: 'Ce libellé a été tronqué'
}
],
champ_private: [
{
id: 'tdc22',
libelle: 'Montant accordé'
}
]
}))
end
end

View file

@ -0,0 +1,216 @@
describe Administrateurs::AttestationTemplateV2sController, type: :controller do
let(:admin) { create(:administrateur) }
let(:attestation_template) { build(:attestation_template, :v2) }
let!(:procedure) { create(:procedure, administrateur: admin, attestation_template: attestation_template, libelle: "Ma démarche") }
let(:logo) { fixture_file_upload('spec/fixtures/files/white.png', 'image/png') }
let(:signature) { fixture_file_upload('spec/fixtures/files/black.png', 'image/png') }
let(:update_params) do
{
official_layout: true,
label_logo: "Ministère des specs",
label_direction: "RSPEC",
footer: "en bas",
activated: false,
tiptap_body: {
type: :doc,
content: [
{
type: :paragraph,
content: [{ text: "Yo from spec", type: :text }]
}
]
}.to_json
}
end
before do
sign_in(admin.user)
Flipper.enable(:attestation_v2)
end
describe 'GET #show' do
subject do
get :show, params: { procedure_id: procedure.id }
response.body
end
context 'if an attestation template exists on the procedure' do
render_views
context 'with preview dossier' do
let!(:dossier) { create(:dossier, :en_construction, procedure:, for_procedure_preview: true) }
it do
is_expected.to include("Mon titre pour Ma démarche")
is_expected.to include("#{dossier.id}")
end
end
context 'without preview dossier' do
it do
is_expected.to include("Mon titre pour --dossier_procedure_libelle--")
end
end
context 'with logo label' do
it do
is_expected.to include("Ministère des devs")
is_expected.to match(/centered_marianne-\w+\.svg/)
end
end
context 'with label direction' do
let(:attestation_template) { build(:attestation_template, :v2, label_direction: "calé à droite") }
it do
is_expected.to include("calé à droite")
end
end
context 'with footer' do
let(:attestation_template) { build(:attestation_template, :v2, footer: "c'est le pied") }
it do
is_expected.to include("c'est le pied")
end
end
context 'with additional logo' do
let(:attestation_template) { build(:attestation_template, :v2, logo:) }
it do
is_expected.to include("Ministère des devs")
is_expected.to include("white.png")
end
end
context 'with signature' do
let(:attestation_template) { build(:attestation_template, :v2, signature:) }
it do
is_expected.to include("black.png")
end
end
end
end
describe 'GET edit' do
subject do
get :edit, params: { procedure_id: procedure.id }
response.body
end
context 'if an attestation template does not exists yet on the procedure' do
let(:attestation_template) { nil }
it 'creates new v2 attestation template' do
subject
expect(assigns(:attestation_template).version).to eq(2)
end
end
context 'if an attestation template already exist on v1' do
let(:attestation_template) { build(:attestation_template, version: 1) }
it 'build new v2 attestation template' do
subject
expect(assigns(:attestation_template).version).to eq(2)
end
end
context 'if attestation template already exist on v2' do
it 'assigns v2 attestation template' do
subject
expect(assigns(:attestation_template)).to eq(attestation_template)
end
end
end
describe 'POST create' do
let(:attestation_template) { nil }
subject do
post :create, params: { procedure_id: procedure.id, attestation_template: update_params }, format: :turbo_stream
response.body
end
context 'when attestation template is valid' do
render_views
it "create template" do
subject
attestation_template = procedure.reload.attestation_template
expect(attestation_template.official_layout).to eq(true)
expect(attestation_template.label_logo).to eq("Ministère des specs")
expect(attestation_template.label_direction).to eq("RSPEC")
expect(attestation_template.footer).to eq("en bas")
expect(attestation_template.activated).to eq(false)
expect(attestation_template.tiptap_body).to eq(update_params[:tiptap_body])
expect(response.body).to include("Formulaire enregistré")
end
context "with files" do
let(:update_params) { super().merge(logo:, signature:) }
it "upload files" do
subject
attestation_template = procedure.reload.attestation_template
expect(attestation_template.logo.download).to eq(logo.read)
expect(attestation_template.signature.download).to eq(signature.read)
end
end
end
end
describe 'PATCH update' do
render_views
subject do
patch :update, params: { procedure_id: procedure.id, attestation_template: update_params }, format: :turbo_stream
response.body
end
context 'when attestation template is valid' do
it "update template" do
subject
attestation_template.reload
expect(attestation_template.official_layout).to eq(true)
expect(attestation_template.label_logo).to eq("Ministère des specs")
expect(attestation_template.label_direction).to eq("RSPEC")
expect(attestation_template.footer).to eq("en bas")
expect(attestation_template.activated).to eq(false)
expect(attestation_template.tiptap_body).to eq(update_params[:tiptap_body])
expect(response.body).to include("Formulaire enregistré")
end
context "with files" do
let(:update_params) { super().merge(logo:, signature:) }
it "upload files" do
subject
attestation_template.reload
expect(attestation_template.logo.download).to eq(logo.read)
expect(attestation_template.signature.download).to eq(signature.read)
end
end
context 'with error' do
let(:update_params) do
super().merge(tiptap_body: { type: :doc, content: [{ type: :mention, attrs: { id: "tdc12", label: "oops" } }] }.to_json)
end
it "render error" do
subject
expect(response.body).to include("Formulaire en erreur")
expect(response.body).to include('Supprimer cette balise')
end
end
end
end
end

View file

@ -241,7 +241,7 @@ describe Administrateurs::AttestationTemplatesController, type: :controller do
render_views
let(:body) { "body --#{removed_type_de_champ.libelle}--" }
it { expect(response.body).to have_content("Le champ « Contenu de lattestation » contient la balise \"#{removed_type_de_champ.libelle}\" qui a été supprimée mais la suppression nest pas encore publiée. Publier la nouvelle version de la démarche et recommencer") }
it { expect(response.body).to have_content("Le champ « Contenu de lattestation » contient la balise \"#{removed_type_de_champ.libelle}\" qui a été supprimée dans les modifications en cours du formulaire. Supprimer cette balise ou réinitialiser les modifications du formulaire puis recommencer") }
end
context 'with removed and published' do

View file

@ -2,11 +2,46 @@ FactoryBot.define do
factory :attestation_template do
title { 'title' }
body { 'body' }
json_body { nil }
footer { 'footer' }
activated { true }
version { 1 }
official_layout { true }
label_direction { nil }
label_logo { nil }
association :procedure
end
trait :v2 do
version { 2 }
body { nil }
title { nil }
label_logo { "Ministère des devs" }
json_body do
{
"type" => "doc",
"content" => [
{
"type" => "header", "content" => [
{ "type" => "headerColumn", "attrs" => { "textAlign" => "left" }, "content" => [{ "type" => "paragraph", "attrs" => { "textAlign" => "left" } }] },
{ "type" => "headerColumn", "attrs" => { "textAlign" => "left" }, "content" => [{ "type" => "paragraph", "attrs" => { "textAlign" => "left" } }] }
]
},
{ "type" => "title", "attrs" => { "textAlign" => "center" }, "content" => [{ "text" => "Mon titre pour ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_procedure_libelle", "label" => "libellé démarche" } }] },
{ "type" => "paragraph", "attrs" => { "textAlign" => "left" }, "content" => [{ "text" => "Dossier: n° ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "dossier_number", "label" => "numéro du dossier" } }] },
{
"type" => "paragraph",
"content" => [
{ "text" => "Nom: ", "type" => "text" }, { "type" => "mention", "attrs" => { "id" => "individual_last_name", "label" => "prénom" } }, { "text" => " ", "type" => "text" },
{ "type" => "mention", "attrs" => { "id" => "individual_first_name", "label" => "nom" } }, { "text" => " ", "type" => "text" }
]
}
]
}
end
end
trait :with_files do
logo { Rack::Test::UploadedFile.new('spec/fixtures/files/logo_test_procedure.png', 'image/png') }
signature { Rack::Test::UploadedFile.new('spec/fixtures/files/logo_test_procedure.png', 'image/png') }

View file

@ -173,5 +173,16 @@ describe AttestationTemplate, type: :model do
end
end
end
context 'body v2' do
let(:attestation) { create(:attestation_template, :v2) }
let(:dossier) { create(:dossier, procedure: attestation.procedure, individual: build(:individual, nom: 'Doe', prenom: 'John')) }
it do
body = attestation.render_attributes_for(dossier: dossier)[:body]
expect(body).to include("Mon titre pour #{dossier.procedure.libelle}")
expect(body).to include("Doe John")
end
end
end
end

View file

@ -32,6 +32,48 @@ describe TagsSubstitutionConcern, type: :model do
end).new(procedure, state)
end
describe 'tags_substitutions' do
let(:individual) { nil }
let(:etablissement) { create(:etablissement) }
let(:dossier) { create(:dossier, :en_construction, procedure:, individual:, etablissement:) }
let(:instructeur) { create(:instructeur) }
let(:tags) { Set.new([["dossier_number", "numéro de dossier"]]) }
subject { template_concern.tags_substitutions(tags, dossier) }
context 'dossiers metadata' do
before { travel_to(Time.zone.local(2024, 1, 15, 12)) }
let(:tags) do
Set.new([
["dossier_number", "n° de dossier"],
["dossier_depose_at", "date de dépôt"],
["dossier_processed_at", "date dinstruction"],
["dossier_procedure_libelle", "Nom de la démarche"],
["tdc_123", "Un champ"]
])
end
it do
is_expected.to eq(
"dossier_number" => dossier.id.to_s,
"dossier_depose_at" => "15/01/2024",
"dossier_processed_at" => "",
"dossier_procedure_libelle" => procedure.libelle,
"tdc_123" => "Un champ"
)
end
end
context 'when the dossier and the procedure has an individual' do
let(:for_individual) { true }
let(:individual) { Individual.create(nom: 'Adama', prenom: 'William', gender: 'M') }
let(:tags) { Set.new(['individual_gender', 'individual_last_name']) }
it { is_expected.to eq({ "individual_gender" => 'M', "individual_last_name" => "Adama" }) }
end
end
describe 'replace_tags' do
let(:individual) { nil }
let(:etablissement) { create(:etablissement) }
@ -523,6 +565,23 @@ describe TagsSubstitutionConcern, type: :model do
it { is_expected.to eq([["public", procedure.draft_revision.types_de_champ.first.stable_id], ['yolo']]) }
end
describe 'tags_categorized' do
let(:types_de_champ_public) do
[
{ libelle: 'public' },
{ type: :email, libelle: 'email' }
]
end
it do
categories = template_concern.tags_categorized
expect(categories.keys).to match([:etablissement, :dossier, :champ_public])
expect(categories[:etablissement].map { _1[:id] }).to include("entreprise_siren")
expect(categories[:dossier].map { _1[:id] }).to include("dossier_number")
expect(categories[:champ_public].map { _1[:libelle] }).to match_array(["public", "email"])
end
end
describe 'parser' do
it do
tokens = TagsSubstitutionConcern::TagsParser.parse("hello world --public--, --numéro du dossier--, un test--yolo-- encore du text\n---\n encore du text --- et encore du text\n--tag--")

View file

@ -1,158 +1,159 @@
RSpec.describe TiptapService do
let(:json) do
{
type: 'doc',
content: [
{
type: 'header',
content: [
{
type: 'headerColumn',
content: [{ type: 'text', text: 'Left' }]
},
{
type: 'headerColumn',
content: [{ type: 'text', text: 'Right' }]
}
]
},
{
type: 'title',
content: [{ type: 'text', text: 'Title' }]
},
{
type: 'title' # remained empty in editor
},
{
type: 'heading',
attrs: { level: 2, textAlign: 'center' },
content: [{ type: 'text', text: 'Heading 2' }]
},
{
type: 'heading',
attrs: { level: 3, textAlign: 'center' },
content: [{ type: 'text', text: 'Heading 3' }]
},
{
type: 'heading',
attrs: { level: 3 } # remained empty in editor
},
{
type: 'paragraph',
attrs: { textAlign: 'right' },
content: [{ type: 'text', text: 'First paragraph' }]
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Bonjour ',
marks: [{ type: 'italic' }, { type: 'strike' }]
},
{
type: 'mention',
attrs: { id: 'name', label: 'Nom' },
marks: [{ type: 'bold' }, { type: 'underline' }]
},
{
type: 'text',
text: ' '
},
{
type: 'text',
text: '!',
marks: [{ type: 'highlight' }]
}
]
},
{
type: 'paragraph'
# no content, empty line
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Item 1'
}
]
}
]
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Item 2'
}
]
}
]
}
]
},
{
type: 'orderedList',
content: [
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Item 1'
}
]
}
]
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Item 2'
}
]
}
]
}
]
},
{
type: 'footer',
content: [{ type: 'text', text: 'Footer' }]
}
]
}
end
describe '.to_html' do
let(:json) do
{
type: 'doc',
content: [
{
type: 'header',
content: [
{
type: 'headerColumn',
content: [{ type: 'text', text: 'Left' }]
},
{
type: 'headerColumn',
content: [{ type: 'text', text: 'Right' }]
}
]
},
{
type: 'title',
content: [{ type: 'text', text: 'Title' }]
},
{
type: 'paragraph',
attrs: { textAlign: 'right' },
content: [
{
type: 'text',
text: 'Hello world!'
}
]
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Bonjour ',
marks: [{ type: 'italic' }, { type: 'strike' }]
},
{
type: 'mention',
attrs: { id: 'name' },
marks: [{ type: 'bold' }, { type: 'underline' }]
},
{
type: 'text',
text: ' '
},
{
type: 'text',
text: '!',
marks: [{ type: 'highlight' }]
}
]
},
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Heading 1' }]
},
{
type: 'heading',
attrs: { level: 2, textAlign: 'center' },
content: [{ type: 'text', text: 'Heading 2' }]
},
{
type: 'heading',
attrs: { level: 3 },
content: [{ type: 'text', text: 'Heading 3' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Item 1'
}
]
}
]
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Item 2'
}
]
}
]
}
]
},
{
type: 'orderedList',
content: [
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Item 1'
}
]
}
]
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Item 2'
}
]
}
]
}
]
},
{
type: 'footer',
content: [{ type: 'text', text: 'Footer' }]
}
]
}
end
let(:tags) { { 'name' => 'Paul' } }
let(:substitutions) { { 'name' => 'Paul' } }
let(:html) do
[
'<header><div class="column">Left</div><div class="column">Right</div></header>',
'<header><div>Left</div><div>Right</div></header>',
'<h1>Title</h1>',
'<p style="text-align: right">Hello world!</p>',
'<h2 class="body-start" style="text-align: center">Heading 2</h2>',
'<h3 style="text-align: center">Heading 3</h3>',
'<p style="text-align: right">First paragraph</p>',
'<p><s><em>Bonjour </em></s><u><strong>Paul</strong></u> <mark>!</mark></p>',
'<h1>Heading 1</h1>',
'<h2 style="text-align: center">Heading 2</h2>',
'<h3>Heading 3</h3>',
'<ul><li><p>Item 1</p></li><li><p>Item 2</p></li></ul>',
'<ol><li><p>Item 1</p></li><li><p>Item 2</p></li></ol>',
'<footer>Footer</footer>'
@ -160,7 +161,35 @@ RSpec.describe TiptapService do
end
it 'returns html' do
expect(described_class.to_html(json, tags)).to eq(html)
expect(described_class.new.to_html(json, substitutions)).to eq(html)
end
context 'body start on paragraph' do
let(:json) do
{
type: 'doc',
content: [
{
type: 'title',
content: [{ type: 'text', text: 'The Title' }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'First paragraph' }]
}
]
}
end
it 'defines stat body on first paragraph' do
expect(described_class.new.to_html(json, substitutions)).to eq("<h1>The Title</h1><p class=\"body-start\">First paragraph</p>")
end
end
end
describe '#used_tags' do
it 'returns used tags' do
expect(described_class.new.used_tags_and_libelle_for(json)).to eq(Set.new([['name', 'Nom']]))
end
end
end

View file

@ -5,9 +5,6 @@ require 'selenium/webdriver'
def setup_driver(app, download_path, options)
Capybara::Selenium::Driver.new(app, browser: :chrome, options:).tap do |driver|
# Set download dir for Chrome < 77
driver.browser.download_path = download_path
if ENV['MAKE_IT_SLOW'].present?
driver.browser.network_conditions = {
offline: false,

View file

@ -28,6 +28,8 @@ describe 'As an administrateur, I want to manage the procedures attestation',
expect(page).to have_content('Désactivée')
find_attestation_card(with_nested_selector: ".fr-badge")
expect(page).not_to have_content("Nouvel éditeur dattestation")
# now process to enable attestation
find_attestation_card.click
fill_in "Titre de lattestation", with: 'BOOM'
@ -61,4 +63,99 @@ describe 'As an administrateur, I want to manage the procedures attestation',
find_attestation_card(with_nested_selector: ".fr-badge")
end
end
context 'Update attestation v2' do
before { Flipper.enable(:attestation_v2) }
scenario do
visit admin_procedure_path(procedure)
find_attestation_card(with_nested_selector: ".fr-badge")
find_attestation_card.click
within(".fr-alert", text: /Nouvel éditeur/) do
find("a").click
end
expect(procedure.reload.attestation_template_v2).to be_nil
expect(page).to have_css("label", text: "Logo additionnel")
fill_in "Intitulé de votre institution", with: "System Test"
fill_in "Intitulé de la direction", with: "The boss"
attestation = nil
wait_until {
attestation = procedure.reload.attestation_template_v2
attestation.present?
}
expect(attestation.label_logo).to eq("System Test")
expect(attestation.activated?).to be_falsey
expect(page).to have_content("Formulaire enregistré")
click_on "date de décision"
# TODO find a way to fill in tiptap
attach_file('Tampon ou signature', Rails.root + 'spec/fixtures/files/white.png')
wait_until { attestation.reload.signature.attached? }
fill_in "Contenu du pied de page", with: "Footer"
wait_until {
body = JSON.parse(attestation.reload.tiptap_body)
first_content = body.dig("content").first&.dig("content")&.first&.dig("content")&.first&.dig("content")
first_content == [
{ "type" => "mention", "attrs" => { "id" => "dossier_processed_at", "label" => "date de décision" } }, # added by click above
{ "type" => "text", "text" => " " },
{ "type" => "mention", "attrs" => { "id" => "dossier_service_name", "label" => "nom du service" } } # defaut initial content
]
}
find("label", text: /à la charte de létat/).click
expect(page).not_to have_css("label", text: "Logo additionnel", visible: true)
expect(page).not_to have_css("label", text: "Intitulé du logo", visible: true)
attach_file('Logo', Rails.root + 'spec/fixtures/files/black.png')
wait_until {
attestation.reload.logo.attached? && attestation.signature.attached? && !attestation.official_layout?
}
# footer is rows-limited
fill_in "Contenu du pied de page", with: ["line1", "line2", "line3", "line4"].join("\n")
expect(page).to have_field("Contenu du pied de page", with: "line1\nline2\nline3line4")
end
context "tag in error" do
before do
tdc = procedure.active_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'age')
procedure.publish_revision!
attestation = procedure.build_attestation_template_v2(json_body: AttestationTemplate::TIPTAP_BODY_DEFAULT, label_logo: "test")
attestation.json_body["content"] << { type: :mention, attrs: { id: "tdc#{tdc.stable_id}", label: tdc.libelle } }
attestation.save!
procedure.draft_revision.remove_type_de_champ(tdc)
end
scenario do
visit edit_admin_procedure_attestation_template_v2_path(procedure)
expect(page).to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"")
click_on "date de décision"
expect(page).to have_content("Formulaire en erreur")
expect(page).to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"")
page.execute_script("document.getElementById('attestation_template_tiptap_body').type = 'text'")
fill_in "attestation_template[tiptap_body]", with: AttestationTemplate::TIPTAP_BODY_DEFAULT.to_json
expect(page).to have_content("Formulaire enregistré")
expect(page).not_to have_content("Formulaire en erreur")
expect(page).not_to have_content("Le champ « Contenu de lattestation » contient la balise \"age\"")
end
end
end
end

331
yarn.lock
View file

@ -2190,131 +2190,131 @@
eventlistener-polyfill "^1.0.5"
mutation-observer-inner-html-shim "^1.0.0"
"@tiptap/core@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.12.tgz#904fdf147e91b5e60561c76e7563c1b5a32f54ab"
integrity sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ==
"@tiptap/core@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.2.0.tgz#6cad2026a0535edcea4b985595d29c9929f5548a"
integrity sha512-ped7XlQ9k5VyE2xUwyRegn1yVF/CAsaF+riBUBJ9+71/gSo2mCZ+6gQvee+LVN1+rD1qN/vWgKhKNDVaU+VaFg==
"@tiptap/extension-bold@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.1.12.tgz#5dbf41105fc0fbde8adbff629312187fbebc39b0"
integrity sha512-AZGxIxcGU1/y6V2YEbKsq6BAibL8yQrbRm6EdcBnby41vj1WziewEKswhLGmZx5IKM2r2ldxld03KlfSIlKQZg==
"@tiptap/extension-bold@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.2.0.tgz#7d2da171a8ecb70fff0d9e5f42579ba71690684a"
integrity sha512-GlrI0FzUSzYhXoYbctcXALbyc22uKfZ0nv1k0qTw8qkKbWsz6qT/rm+rcB2YgugW3r6cu5n5HDQbzbgjapNAEA==
"@tiptap/extension-bullet-list@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.12.tgz#7c905a577ce30ef2cb335870a23f9d24fd26f6aa"
integrity sha512-vtD8vWtNlmAZX8LYqt2yU9w3mU9rPCiHmbp4hDXJs2kBnI0Ju/qAyXFx6iJ3C3XyuMnMbJdDI9ee0spAvFz7cQ==
"@tiptap/extension-bullet-list@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.2.0.tgz#8bcfe18234989d3d2409d14e828737f55bdb1a38"
integrity sha512-V/jVw5g1c7EIKo+44Rw1WeQ9NUzXjtecnCclDALXOlhZ3Kz+a6mBOkuMSeoZhlH0sZ7Cq5u4W+64IuDOstA9yg==
"@tiptap/extension-document@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.1.12.tgz#e19e4716dfad60cbeb6abaf2f362fed759963529"
integrity sha512-0QNfAkCcFlB9O8cUNSwTSIQMV9TmoEhfEaLz/GvbjwEq4skXK3bU+OQX7Ih07waCDVXIGAZ7YAZogbvrn/WbOw==
"@tiptap/extension-document@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.2.0.tgz#b9d3133119a5c186180380afc12ee46f840ba6bf"
integrity sha512-P21yqZP8DQQ03Q84jO4l73XPswCqjCPb318/eSdovF7m7xXcY55HkHtWU5Fvt/KJxhwvG6WWGmvoox9/bDW0DQ==
"@tiptap/extension-gapcursor@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.1.12.tgz#63844c3abd1a38af915839cf0c097b6d2e5a86fe"
integrity sha512-zFYdZCqPgpwoB7whyuwpc8EYLYjUE5QYKb8vICvc+FraBUDM51ujYhFSgJC3rhs8EjI+8GcK8ShLbSMIn49YOQ==
"@tiptap/extension-gapcursor@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.2.0.tgz#0b8fe3c56c9ab0661bd6b8ea7bc3d545155217d9"
integrity sha512-FsIoLA2xC1tUCK2cw5jWOKlwYNZgex3puMRwQaZjbph2oz8Jel8SRAzAwsfoi4JkaN9TpNlRP1i00xzGGaezow==
"@tiptap/extension-heading@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.1.12.tgz#05ae4684d6f29ae611495ab114038e14a5d1dff6"
integrity sha512-MoANP3POAP68Ko9YXarfDKLM/kXtscgp6m+xRagPAghRNujVY88nK1qBMZ3JdvTVN6b/ATJhp8UdrZX96TLV2w==
"@tiptap/extension-heading@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.2.0.tgz#112e2917f11fe1f2cdc9f93a14de73c1dfba5d52"
integrity sha512-4VwRtGDhRhUtP/c8BB6pPMS6g8PRv5cs+pYxCp466en5awSBhM4AN2cxDD6ruXP4YvNTDprky9H0IKjhs6Ym4Q==
"@tiptap/extension-highlight@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.1.12.tgz#184efb75238c9cbc6c18d523b735de4329f78ecc"
integrity sha512-buen31cYPyiiHA2i0o2i/UcjRTg/42mNDCizGr1OJwvv3AELG3qOFc4Y58WJWIvWNv+1Dr4ZxHA3GNVn0ANWyg==
"@tiptap/extension-highlight@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.2.0.tgz#d9a6290203d1886b1252c0dd2338bd5f2ba56fe4"
integrity sha512-88W04AW8MCtoepmNn3343+VF1zBNlAY0tSfU+XqlYhhLo1IvbAYHeS7BN9XbqN8CUx1R3X5F2eUbEFsbxOAr3A==
"@tiptap/extension-history@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.1.12.tgz#03bcb9422e8ea2b82dc45207d1a1b0bc0241b055"
integrity sha512-6b7UFVkvPjq3LVoCTrYZAczt5sQrQUaoDWAieVClVZoFLfjga2Fwjcfgcie8IjdPt8YO2hG/sar/c07i9vM0Sg==
"@tiptap/extension-history@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.2.0.tgz#fcd376ec6a1f5d980dd31bfa4aff38cd0913f543"
integrity sha512-djHQxD5KP4EI3U6cri0/wcJxyMspU1B6+UVXL1G7867JiV9mvAw/HUoZsHTOsn+kajTi8szFfl2exa6dCYVrmA==
"@tiptap/extension-italic@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.1.12.tgz#e99480eb77f8b4e5444fc236add8a831d5aa2343"
integrity sha512-/XYrW4ZEWyqDvnXVKbgTXItpJOp2ycswk+fJ3vuexyolO6NSs0UuYC6X4f+FbHYL5VuWqVBv7EavGa+tB6sl3A==
"@tiptap/extension-italic@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.2.0.tgz#82a73c8c485f7fc6141bea66464ac5b8ecc142cb"
integrity sha512-eOesosmbf8mXCJ8E58PnuhO5gtDpviCTpi3ZaGn1yP0gLRV3wlo8wpCbxlGXLStgFT0tejN26MGbEHCJHkRkFQ==
"@tiptap/extension-link@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.1.12.tgz#a18f83a0b54342e6274ff9e5a5907ef7f15aa723"
integrity sha512-Sti5hhlkCqi5vzdQjU/gbmr8kb578p+u0J4kWS+SSz3BknNThEm/7Id67qdjBTOQbwuN07lHjDaabJL0hSkzGQ==
"@tiptap/extension-link@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.2.0.tgz#c277096854443802ea32e31a8568392ee69bc6af"
integrity sha512-1evYv5Fjod47kobd/0RsHYyCFWrkU5IYZSIBC7bAGsnzG4fId2O5SEyn9SPsMYOVtEL2EIhiqb0i+wcwXf6jOQ==
dependencies:
linkifyjs "^4.1.0"
"@tiptap/extension-list-item@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.12.tgz#3eb28dc998490a98f14765783770b3cf6587d39e"
integrity sha512-Gk7hBFofAPmNQ8+uw8w5QSsZOMEGf7KQXJnx5B022YAUJTYYxO3jYVuzp34Drk9p+zNNIcXD4kc7ff5+nFOTrg==
"@tiptap/extension-list-item@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.2.0.tgz#3c3bf819ac4063ff10dac80cb097725fcc21d542"
integrity sha512-st5S4z2+IXsaX9Otm+4y1NZhE/yLtfFELn5VIUX69PwnNThaN2/ioBXkO1o2ZdLex++D0oMaY5ILIW/PhCWP8Q==
"@tiptap/extension-mention@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.1.12.tgz#a395e7757b45630ec3047f14b0ba2dde8e1c9c93"
integrity sha512-Nc8wFlyPp+/48IpOFPk2O3hYsF465wizcM3aihMvZM96Ahic7dvv9yVptyOfoOwgpExl2FIn1QPjRDXF60VAUg==
"@tiptap/extension-mention@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.2.0.tgz#6eabf028d2fefc38811af21fb987f8589dd2c7bd"
integrity sha512-fkePDuTYj+Nv99MZRUsjVeQaSsTA+qpRObftXACbxNr0Pp/UH3YF/arjv9c5bRHZhZrCy30U+QzrmXGTHDe18w==
"@tiptap/extension-ordered-list@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.12.tgz#f41a45bc66b4d19e379d4833f303f2e0cd6b9d60"
integrity sha512-tF6VGl+D2avCgn9U/2YLJ8qVmV6sPE/iEzVAFZuOSe6L0Pj7SQw4K6AO640QBob/d8VrqqJFHCb6l10amJOnXA==
"@tiptap/extension-ordered-list@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.2.0.tgz#b7fa60938ed11f597e91a8e9aaf098542d289efc"
integrity sha512-LR+RQdv8yEqSYkTBGYuh099wr4r0QIosxfGe/ZlshwDJ4qFKVhyWSZ8qyaEiufmXDadTasj7WQvlbpVAhrVTBQ==
"@tiptap/extension-paragraph@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.1.12.tgz#922447b2aa1c7184787d351ceec593a74d24ed03"
integrity sha512-hoH/uWPX+KKnNAZagudlsrr4Xu57nusGekkJWBcrb5MCDE91BS+DN2xifuhwXiTHxnwOMVFjluc0bPzQbkArsw==
"@tiptap/extension-paragraph@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.2.0.tgz#468ed21f990a779ee49eafae808250d7186ec25e"
integrity sha512-niuvuBEkhz9gnQvFHbxs5z044bpDXRH9zz8QW2bA8+IDSxWHfnVmSZ3AZsed7OJ4EK1AcgGxy+gFOpAcZ73XTw==
"@tiptap/extension-strike@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.1.12.tgz#2b049aedf2985e9c9e3c3f1cc0b203a574c85bd8"
integrity sha512-HlhrzIjYUT8oCH9nYzEL2QTTn8d1ECnVhKvzAe6x41xk31PjLMHTUy8aYjeQEkWZOWZ34tiTmslV1ce6R3Dt8g==
"@tiptap/extension-strike@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.2.0.tgz#6fc3e056fb4512dbdf27c645439da48879a44ee8"
integrity sha512-AMVC94mWNCAXLyZdkB5KqLai9FMUaQDkWAZSD1DKCRq2OJeA71nW9G1eDPa2TzQezl2IVTW9mU4m37hcsTmRfg==
"@tiptap/extension-text-align@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text-align/-/extension-text-align-2.1.12.tgz#962dca4d284ce57ed345fd4b94ddb3a97944e0d1"
integrity sha512-siMlwrkgVrAxxgmZn8GOc75J7UZi2CVrP9vDHkUPPyKm/fjssYekXwGCEk4Vswii1BbOh2gt+MDsRkeYRGyDlQ==
"@tiptap/extension-text-align@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text-align/-/extension-text-align-2.2.0.tgz#29d4a873d678bd312933691e983c462ecf02d349"
integrity sha512-Fub3RyQlmwB10aWzmVFqyfi9ww0uQYLag76ae/bjkkqSjHo/1ET9/9vhEqpQQVR7kAifs4CyRQguaASYSPxKYg==
"@tiptap/extension-text@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.1.12.tgz#466e3244bdd9b2db2304c0c9a1d51ce59f5327d0"
integrity sha512-rCNUd505p/PXwU9Jgxo4ZJv4A3cIBAyAqlx/dtcY6cjztCQuXJhuQILPhjGhBTOLEEL4kW2wQtqzCmb7O8i2jg==
"@tiptap/extension-text@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.2.0.tgz#92ccaf3e06cd3deddd3334293fd7f7dc8bccbd03"
integrity sha512-h3lJRUZaUBisqUSQDEO+NU4SgKW7rj/vvbsbML8klWdEhg8U9btdv4eZgoJxbsqOdpUc6Cy6dBhre4myWI8Y2w==
"@tiptap/extension-typography@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-typography/-/extension-typography-2.1.12.tgz#0bf3ba500b49b4f239205e76dda512bda7d3b4f7"
integrity sha512-OFkQHmUbKQwVO0b/NP2MLuuqQIWxw/gHaWQF/atgZf3mG0YDV2x3P/u+RBpKnsIujPZFvoEBRJGnstvEAB7zfQ==
"@tiptap/extension-typography@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-typography/-/extension-typography-2.2.0.tgz#283c602efd738fe3156843ad46db6e3ef90af35e"
integrity sha512-te9JQqqjZbaEBPeNJeEDiy9HU/lvlG0lCq9Zz8EnHIJG69A3rXHpwLAbGY/WiQNk/MCx6HjBeVyjRCeHvo56Cw==
"@tiptap/extension-underline@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-2.1.12.tgz#abd59c4b6c8434dbadb4ff9bff23eefcc6bc095e"
integrity sha512-NwwdhFT8gDD0VUNLQx85yFBhP9a8qg8GPuxlGzAP/lPTV8Ubh3vSeQ5N9k2ZF/vHlEvnugzeVCbmYn7wf8vn1g==
"@tiptap/extension-underline@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-2.2.0.tgz#7ad7734f15a07268627fd4c9e90f5ff43dc110a3"
integrity sha512-y+D9gUWa/sVeCftZBMCEpcD1tD4OgNXfjsS7/qxv6ge9t0HCRhAJfHWm6rX8P7QybjacinZuun/T6CudRdGbMg==
"@tiptap/pm@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.1.12.tgz#88a4b19be0eabb13d42ddd540c19ba1bbe74b322"
integrity sha512-Q3MXXQABG4CZBesSp82yV84uhJh/W0Gag6KPm2HRWPimSFELM09Z9/5WK9RItAYE0aLhe4Krnyiczn9AAa1tQQ==
"@tiptap/pm@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.2.0.tgz#18131f41220a87816f389db71de80d58a8de65a3"
integrity sha512-CL/ys9rvUgYcRHyeQFuIQdw09+0LUgKAfYWzCr6Pu4DDdbRooiex/a9M29imnRMEgS9SwuHN2v17fKHH0w65Hg==
dependencies:
prosemirror-changeset "^2.2.0"
prosemirror-collab "^1.3.0"
prosemirror-commands "^1.3.1"
prosemirror-dropcursor "^1.5.0"
prosemirror-gapcursor "^1.3.1"
prosemirror-history "^1.3.0"
prosemirror-inputrules "^1.2.0"
prosemirror-keymap "^1.2.0"
prosemirror-markdown "^1.10.1"
prosemirror-menu "^1.2.1"
prosemirror-model "^1.18.1"
prosemirror-schema-basic "^1.2.0"
prosemirror-schema-list "^1.2.2"
prosemirror-state "^1.4.1"
prosemirror-tables "^1.3.0"
prosemirror-trailing-node "^2.0.2"
prosemirror-transform "^1.7.0"
prosemirror-view "^1.28.2"
prosemirror-changeset "^2.2.1"
prosemirror-collab "^1.3.1"
prosemirror-commands "^1.5.2"
prosemirror-dropcursor "^1.8.1"
prosemirror-gapcursor "^1.3.2"
prosemirror-history "^1.3.2"
prosemirror-inputrules "^1.3.0"
prosemirror-keymap "^1.2.2"
prosemirror-markdown "^1.12.0"
prosemirror-menu "^1.2.4"
prosemirror-model "^1.19.4"
prosemirror-schema-basic "^1.2.2"
prosemirror-schema-list "^1.3.0"
prosemirror-state "^1.4.3"
prosemirror-tables "^1.3.5"
prosemirror-trailing-node "^2.0.7"
prosemirror-transform "^1.8.0"
prosemirror-view "^1.32.7"
"@tiptap/suggestion@^2.1.12":
version "2.1.12"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.1.12.tgz#a13782d1e625ec03b3f61b6839ecc95b6b685d3f"
integrity sha512-rhlLWwVkOodBGRMK0mAmE34l2a+BqM2Y7q1ViuQRBhs/6sZ8d83O4hARHKVwqT5stY4i1l7d7PoemV3uAGI6+g==
"@tiptap/suggestion@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.2.0.tgz#620bf053d1ee3ac4063ccbef555cc8564beeb195"
integrity sha512-xcjGEVgRB0tx21LdNTr5F/HEKNlFuq9cnJlVGDLlNP43ixXM6aODAeY3VvEZ3XtEtLpHsiCVrinrjRNtpUf+eQ==
"@tmcw/togeojson@^5.6.0":
version "5.6.0"
@ -2465,14 +2465,14 @@
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
"@types/object.omit@^3.0.0":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/object.omit/-/object.omit-3.0.1.tgz#1b9de058cf94344b9284308a41b17e3a356ed18e"
integrity sha512-24XD34UeRWw505TsMNBrQ4bES2s8IxiFC59mmNUFhTz9IX2hAtA7gQ8wVww1i17QmhBYILg5iqYP2y7aqA3pwQ==
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/object.omit/-/object.omit-3.0.3.tgz#cc52b1d9774c1619b5c6fc50229d087f01eabd68"
integrity sha512-xrq4bQTBGYY2cw+gV4PzoG2Lv3L0pjZ1uXStRRDQoATOYW1lCsFQHhQ+OkPhIcQoqLjAq7gYif7D14Qaa6Zbew==
"@types/object.pick@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/object.pick/-/object.pick-1.3.2.tgz#9eb28118240ad8f658b9c9c6caf35359fdb37150"
integrity sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg==
version "1.3.4"
resolved "https://registry.yarnpkg.com/@types/object.pick/-/object.pick-1.3.4.tgz#1a38b6e69a35f36ec2dcc8b9f5ffd555c1c4d7fc"
integrity sha512-5PjwB0uP2XDp3nt5u5NJAG2DORHIRClPzWT/TTZhJ2Ekwe8M5bA9tvPdi9NO/n2uvu2/ictat8kgqvLfcIE1SA==
"@types/prop-types@*":
version "15.7.4"
@ -3877,11 +3877,6 @@ entities@~2.1.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
entities@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@ -5937,12 +5932,12 @@ linkify-it@^3.0.1:
dependencies:
uc.micro "^1.0.1"
linkify-it@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec"
integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==
linkify-it@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
dependencies:
uc.micro "^1.0.1"
uc.micro "^2.0.0"
linkifyjs@^4.1.0:
version "4.1.2"
@ -6118,16 +6113,17 @@ markdown-it@^12.2.0:
mdurl "^1.0.1"
uc.micro "^1.0.5"
markdown-it@^13.0.1:
version "13.0.2"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.2.tgz#1bc22e23379a6952e5d56217fbed881e0c94d536"
integrity sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==
markdown-it@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.0.0.tgz#b4b2ddeb0f925e88d981f84c183b59bac9e3741b"
integrity sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==
dependencies:
argparse "^2.0.1"
entities "~3.0.1"
linkify-it "^4.0.1"
mdurl "^1.0.1"
uc.micro "^1.0.5"
entities "^4.4.0"
linkify-it "^5.0.0"
mdurl "^2.0.0"
punycode.js "^2.3.1"
uc.micro "^2.0.0"
marked@^4.0.12:
version "4.3.0"
@ -6157,6 +6153,11 @@ mdurl@^1.0.1:
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==
mdurl@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
meow@^10.1.3:
version "10.1.3"
resolved "https://registry.yarnpkg.com/meow/-/meow-10.1.3.tgz#21689959a7d00e8901aff30d208acb2122eb8088"
@ -6997,21 +6998,21 @@ prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
prosemirror-changeset@^2.2.0:
prosemirror-changeset@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz#dae94b63aec618fac7bb9061648e6e2a79988383"
integrity sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==
dependencies:
prosemirror-transform "^1.0.0"
prosemirror-collab@^1.3.0:
prosemirror-collab@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-commands@^1.0.0, prosemirror-commands@^1.3.1:
prosemirror-commands@^1.0.0, prosemirror-commands@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz#e94aeea52286f658cd984270de9b4c3fff580852"
integrity sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==
@ -7020,7 +7021,7 @@ prosemirror-commands@^1.0.0, prosemirror-commands@^1.3.1:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-dropcursor@^1.5.0:
prosemirror-dropcursor@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d"
integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==
@ -7029,7 +7030,7 @@ prosemirror-dropcursor@^1.5.0:
prosemirror-transform "^1.1.0"
prosemirror-view "^1.1.0"
prosemirror-gapcursor@^1.3.1:
prosemirror-gapcursor@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4"
integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==
@ -7039,7 +7040,7 @@ prosemirror-gapcursor@^1.3.1:
prosemirror-state "^1.0.0"
prosemirror-view "^1.0.0"
prosemirror-history@^1.0.0, prosemirror-history@^1.3.0:
prosemirror-history@^1.0.0, prosemirror-history@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.2.tgz#ce6ad7ab9db83e761aee716f3040d74738311b15"
integrity sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==
@ -7049,15 +7050,15 @@ prosemirror-history@^1.0.0, prosemirror-history@^1.3.0:
prosemirror-view "^1.31.0"
rope-sequence "^1.3.0"
prosemirror-inputrules@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.2.1.tgz#8faf3d78c16150aedac71d326a3e3947417ce557"
integrity sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ==
prosemirror-inputrules@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz#ef1519bb2cb0d1e0cec74bad1a97f1c1555068bb"
integrity sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.0:
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e"
integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==
@ -7065,15 +7066,15 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.0:
prosemirror-state "^1.0.0"
w3c-keyname "^2.2.0"
prosemirror-markdown@^1.10.1:
version "1.11.2"
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.11.2.tgz#f6e529e669d11fa3eec859e93c0d2c91788d6c80"
integrity sha512-Eu5g4WPiCdqDTGhdSsG9N6ZjACQRYrsAkrF9KYfdMaCmjIApH75aVncsWYOJvEk2i1B3i8jZppv3J/tnuHGiUQ==
prosemirror-markdown@^1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.12.0.tgz#d2de09d37897abf7adb6293d925ff132dac5b0a6"
integrity sha512-6F5HS8Z0HDYiS2VQDZzfZP6A0s/I0gbkJy8NCzzDMtcsz3qrfqyroMMeoSjAmOhDITyon11NbXSzztfKi+frSQ==
dependencies:
markdown-it "^13.0.1"
markdown-it "^14.0.0"
prosemirror-model "^1.0.0"
prosemirror-menu@^1.2.1:
prosemirror-menu@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz#3cfdc7c06d10f9fbd1bce29082c498bd11a0a79a"
integrity sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==
@ -7083,21 +7084,21 @@ prosemirror-menu@^1.2.1:
prosemirror-history "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1:
version "1.19.3"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006"
integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==
prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.19.0, prosemirror-model@^1.19.4, prosemirror-model@^1.8.1:
version "1.19.4"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.4.tgz#e45e84480c97dd3922095dbe579e1c98c86c0704"
integrity sha512-RPmVXxUfOhyFdayHawjuZCxiROsm9L4FCUA6pWI+l7n2yCBsWy9VpdE1hpDHUS8Vad661YLY9AzqfjLhAKQ4iQ==
dependencies:
orderedmap "^2.0.0"
prosemirror-schema-basic@^1.2.0:
prosemirror-schema-basic@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7"
integrity sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==
dependencies:
prosemirror-model "^1.19.0"
prosemirror-schema-list@^1.2.2:
prosemirror-schema-list@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.3.0.tgz#05374702cf35a3ba5e7ec31079e355a488d52519"
integrity sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==
@ -7106,7 +7107,7 @@ prosemirror-schema-list@^1.2.2:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.7.3"
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.1:
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080"
integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==
@ -7115,10 +7116,10 @@ prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, pr
prosemirror-transform "^1.0.0"
prosemirror-view "^1.27.0"
prosemirror-tables@^1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.3.4.tgz#0b7cc16d49f90c5b834c9f29291c545478ce9ab0"
integrity sha512-z6uLSQ1BLC3rgbGwZmpfb+xkdvD7W/UOsURDfognZFYaTtc0gsk7u/t71Yijp2eLflVpffMk6X0u0+u+MMDvIw==
prosemirror-tables@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.3.5.tgz#80f03394f5b9991f9693bcb3a90b6dba6b16254d"
integrity sha512-JSZ2cCNlApu/ObAhdPyotrjBe2cimniniTpz60YXzbL0kZ+47nEYk2LWbfKU2lKpBkUNquta2PjteoNi4YCluQ==
dependencies:
prosemirror-keymap "^1.1.2"
prosemirror-model "^1.8.1"
@ -7126,7 +7127,7 @@ prosemirror-tables@^1.3.0:
prosemirror-transform "^1.2.1"
prosemirror-view "^1.13.3"
prosemirror-trailing-node@^2.0.2:
prosemirror-trailing-node@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.7.tgz#ba782a7929f18bcae650b1c7082a2d10443eab19"
integrity sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==
@ -7135,17 +7136,17 @@ prosemirror-trailing-node@^2.0.2:
"@remirror/core-helpers" "^3.0.0"
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.0, prosemirror-transform@^1.7.3:
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.3, prosemirror-transform@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz#a47c64a3c373c1bd0ff46e95be3210c8dda0cd11"
integrity sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.28.2, prosemirror-view@^1.31.0:
version "1.32.1"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.32.1.tgz#bcd0877f1673ffe5f94c1e966b6fbdadcd2d5bbf"
integrity sha512-9SnB4HBgRczzTyIMZLPE1iszegL04hNfUyS8uPtP1RPxNM2NTCiIs8KwNsJU4nbZO9rxJTwVTv7Jm3zU4CR78A==
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.32.7:
version "1.32.7"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.32.7.tgz#b9c4e8471daeba79489befa59eaeaeb4cd2e2653"
integrity sha512-pvxiOoD4shW41X5bYDjRQk3DSG4fMqxh36yPMt7VYgU3dWRmqFzWJM/R6zeo1KtC8nyk717ZbQND3CC9VNeptw==
dependencies:
prosemirror-model "^1.16.0"
prosemirror-state "^1.0.0"
@ -7166,6 +7167,11 @@ psl@^1.1.33:
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
punycode.js@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
@ -8404,6 +8410,11 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
uc.micro@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.0.0.tgz#84b3c335c12b1497fd9e80fcd3bfa7634c363ff1"
integrity sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==
ufo@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.2.tgz#c7d719d0628a1c80c006d2240e0d169f6e3c0496"