Compare commits

..

1 Commits

Author SHA1 Message Date
vvzvlad
fcf1fdec89 Merge pull request #1 from vvzvlad/develop
Release 0.94.0
2026-06-26 18:23:28 +03:00
8 changed files with 21 additions and 76 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "client",
"private": true,
"version": "0.94.1",
"version": "0.94.0",
"scripts": {
"dev": "node scripts/copy-vad-assets.mjs && vite",
"build": "node scripts/copy-vad-assets.mjs && tsc && vite build",

View File

@@ -104,19 +104,6 @@
min-width: 0;
}
/* The inner editable paragraph inherits `.ProseMirror p { margin: 0.5em 0 }`,
which pushes the first text line ~0.5em below the "N." marker (aligned to
flex-start), making the number float above the text. Drop the outer margins
so the marker and the first line share the same top edge — same approach
used for callouts in core.css. */
.definitionContent > :first-child {
margin-top: 0;
}
.definitionContent > :last-child {
margin-bottom: 0;
}
.backLink {
flex: 0 0 auto;
cursor: pointer;

View File

@@ -10,15 +10,9 @@ ul[data-type="taskList"] {
display: flex;
> label {
/* Box exactly one text-line tall and center the checkbox in it, so the
checkbox lines up with the first line of the item's text. This tracks
the editor line-height (--mantine-line-height-xl) instead of a magic
padding-top that drifts from the real line box. */
padding-top: 0.2rem;
flex: 0 0 auto;
margin-right: 0.5rem;
height: calc(var(--mantine-line-height-xl, 1.65) * 1em);
display: inline-flex;
align-items: center;
user-select: none;
}

View File

@@ -1,6 +1,6 @@
{
"name": "server",
"version": "0.94.1",
"version": "0.94.0",
"description": "",
"author": "",
"private": true,

View File

@@ -1,34 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('AppController (e2e)', () => {
let app: NestFastifyApplication;
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
// Docmost runs on Fastify (see src/main.ts). The default
// createNestApplication() would load @nestjs/platform-express, which is not
// a dependency of this project, so an explicit FastifyAdapter is required.
app = moduleFixture.createNestApplication<NestFastifyApplication>(
new FastifyAdapter(),
);
app = moduleFixture.createNestApplication();
await app.init();
// Fastify must finish booting before its HTTP server can serve requests.
await app.getHttpAdapter().getInstance().ready();
});
afterEach(async () => {
// Guard with optional chaining: if beforeEach throws before `app` is
// assigned, closing undefined would mask the original failure.
await app?.close();
});
it('/ (GET)', () => {

View File

@@ -1,18 +1,14 @@
{
"moduleFileExtensions": ["js", "json", "ts", "tsx"],
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
"^.+\\.(t|j)s$": "ts-jest"
},
"transformIgnorePatterns": [
"/node_modules/(?!(\\.pnpm/)?(nanoid|uuid|image-dimensions|marked|happy-dom|lib0|@sindresorhus[+/][a-z0-9-]+|escape-string-regexp|p-limit|yocto-queue)(@|/))"
],
"moduleNameMapper": {
"^@docmost/db/(.*)$": "<rootDir>/../src/database/$1",
"^@docmost/transactional/(.*)$": "<rootDir>/../src/integrations/transactional/$1",
"^@docmost/ee/(.*)$": "<rootDir>/../src/ee/$1",
"^src/(.*)$": "<rootDir>/../src/$1"
"^@docmost/ee/(.*)$": "<rootDir>/../src/ee/$1"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "docmost",
"homepage": "https://docmost.com",
"version": "0.94.1",
"version": "0.94.0",
"private": true,
"scripts": {
"build": "nx run-many -t build",

View File

@@ -7,7 +7,6 @@ import { writeFileSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { deflateSync } from "node:zlib";
import { createServer } from "node:http";
const API = process.env.DOCMOST_API_URL;
if (!API || !process.env.DOCMOST_EMAIL || !process.env.DOCMOST_PASSWORD) {
@@ -105,7 +104,7 @@ async function main() {
{ find: "БУКВОЕД", replace: "КНИГОЛЮБ" },
{ find: "[1]", replace: "[42]" },
]);
check("edit_page_text: both edits applied", editRes.applied.every((e) => e.replacements === 1));
check("edit_page_text: both edits applied", editRes.edits.every((e) => e.replacements === 1));
await new Promise((r) => setTimeout(r, 16000)); // wait for server persistence
const pj2 = await client.getPageJson(pageId);
const text2 = JSON.stringify(pj2.content);
@@ -150,24 +149,11 @@ async function main() {
check("update_page_json: paragraph appended", JSON.stringify(pj4.content).includes("добавленный через update_page_json"));
check("update_page_json: custom node id preserved", lastNode.attrs?.id === "testidjsonpush", lastNode.attrs?.id);
// 6b. images: upload / insert / replace (clean src, fresh attachment on replace).
// insert_image / replace_image take an http(s) URL that the SERVER fetches;
// local file paths are intentionally unsupported. The Docmost server runs on
// the same host as this test, so serve the PNG bytes over a throwaway
// localhost HTTP server it can reach.
const bytesA = makePng(255, 0, 0); // red
const bytesB = makePng(0, 0, 255); // blue (a DIFFERENT valid PNG)
const imgServer = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "image/png" });
res.end(req.url === "/b.png" ? bytesB : bytesA);
});
await new Promise((resolve, reject) => {
imgServer.once("error", reject);
imgServer.listen(0, "127.0.0.1", resolve);
});
const imgPort = imgServer.address().port;
const urlA = `http://127.0.0.1:${imgPort}/a.png`;
const urlB = `http://127.0.0.1:${imgPort}/b.png`;
// 6b. images: upload / insert / replace (clean src, fresh attachment on replace)
const pngA = join(tmpdir(), `mcp-e2e-img-a-${Date.now()}.png`);
const pngB = join(tmpdir(), `mcp-e2e-img-b-${Date.now()}.png`);
writeFileSync(pngA, makePng(255, 0, 0)); // red
writeFileSync(pngB, makePng(0, 0, 255)); // blue (a DIFFERENT valid PNG)
try {
// Independent login to fetch file bytes with the same cookie the editor uses.
const login = await axios.post(
@@ -187,7 +173,7 @@ async function main() {
});
// insert_image: append the first PNG, src must be clean (no ?v=) and fetchable.
const ins = await client.insertImage(pageId, urlA);
const ins = await client.insertImage(pageId, pngA);
check("insert_image: src has no ?v= cache-buster", !ins.src.includes("?v="), ins.src);
const fileA = await fetchFile(ins.src);
check("insert_image: file fetch returns 200", fileA.status === 200, `status=${fileA.status}`);
@@ -213,7 +199,7 @@ async function main() {
// replace_image: must create a NEW attachment with a clean, fetchable URL.
// The 200 fetch is the assertion that catches the in-place-overwrite HTTP 500 regression.
const rep = await client.replaceImage(pageId, oldAttachmentId, urlB);
const rep = await client.replaceImage(pageId, oldAttachmentId, pngB);
check("replace_image: new attachment id differs from old", rep.newAttachmentId !== oldAttachmentId, `${oldAttachmentId} -> ${rep.newAttachmentId}`);
check("replace_image: src has no ?v= cache-buster", !rep.src.includes("?v="), rep.src);
const fileB = await fetchFile(rep.src);
@@ -229,7 +215,8 @@ async function main() {
check("replace_image: page has new attachment id", !!findImage(pjImg2.content.content, rep.newAttachmentId), rep.newAttachmentId);
check("replace_image: old attachment id repointed away", !findImage(pjImg2.content.content, oldAttachmentId), oldAttachmentId);
} finally {
imgServer.close();
try { unlinkSync(pngA); } catch {}
try { unlinkSync(pngB); } catch {}
}
// 6c. rich formatting: callout type, task list, inline marks, table alignment,
@@ -454,10 +441,7 @@ async function main() {
// 9. comments: create / list / reply / update / check_new / delete
const beforeComments = new Date(Date.now() - 1000).toISOString();
// A top-level comment requires an inline "selection": exact contiguous text
// that exists in the persisted page to anchor on. "Добавленный абзац." is a
// plain paragraph re-imported in section 5 and still present here.
const c1 = await client.createComment(pageId, "Первый **комментарий** с [ссылкой](https://example.com).", "inline", "Добавленный абзац.");
const c1 = await client.createComment(pageId, "Первый **комментарий** с [ссылкой](https://example.com).");
check("create_comment: created", !!c1.data.id, c1.data.id);
check("create_comment: markdown round-trip", c1.data.content.includes("**комментарий**"), c1.data.content);
const reply = await client.createComment(pageId, "Ответ на комментарий.", "page", undefined, c1.data.id);