mindoodb-app-sdk 0.0.51 → 0.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/TESTING.md +57 -0
- package/dist/{chunk-R76XXRJ6.js → chunk-6NTVTL6G.js} +1 -1
- package/dist/index.d.ts +74 -3
- package/dist/index.js +389 -17
- package/dist/testing/index.d.ts +97 -2
- package/dist/testing/index.js +412 -141
- package/dist/{types-BLyV6Ejy.d.ts → types-CsKw_niD.d.ts} +46 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
releaseMindooDBAppBridgeSessions,
|
|
24
24
|
sanitizeHostShortcutBindings,
|
|
25
25
|
snapshotElementToPng
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-6NTVTL6G.js";
|
|
27
27
|
import {
|
|
28
28
|
MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME,
|
|
29
29
|
MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME,
|
|
@@ -61,6 +61,9 @@ var MINDOODB_APP_DEFINITION_PERMISSIONS = [
|
|
|
61
61
|
"proposeapps"
|
|
62
62
|
];
|
|
63
63
|
var MINDOODB_APP_DEFINITION_REGISTRATION_PERMISSIONS = ["proposeapps"];
|
|
64
|
+
var MINDOODB_APP_LISTING_SUMMARY_MAX = 300;
|
|
65
|
+
var MINDOODB_APP_LISTING_DESCRIPTION_MAX = 4e3;
|
|
66
|
+
var MINDOODB_APP_LISTING_MAX_SCREENSHOTS = 8;
|
|
64
67
|
var LOGICAL_DATABASE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
65
68
|
function isPlainObject(value) {
|
|
66
69
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -200,6 +203,156 @@ function readDatabases(value, errors) {
|
|
|
200
203
|
});
|
|
201
204
|
return result.length ? result : void 0;
|
|
202
205
|
}
|
|
206
|
+
function readLocalizedText(value, label, maxLength, errors) {
|
|
207
|
+
if (value === void 0 || value === null) {
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === "string") {
|
|
211
|
+
const trimmed = value.trim();
|
|
212
|
+
if (trimmed.length > maxLength) {
|
|
213
|
+
errors.push(`${label} must not be longer than ${maxLength} characters.`);
|
|
214
|
+
return void 0;
|
|
215
|
+
}
|
|
216
|
+
return trimmed || void 0;
|
|
217
|
+
}
|
|
218
|
+
if (!isPlainObject(value)) {
|
|
219
|
+
errors.push(`${label} must be a string or an object of per-locale strings.`);
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
const result = {};
|
|
223
|
+
for (const [locale, text] of Object.entries(value)) {
|
|
224
|
+
if (typeof text !== "string") {
|
|
225
|
+
errors.push(`${label}.${locale} must be a string.`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const trimmed = text.trim();
|
|
229
|
+
if (trimmed.length > maxLength) {
|
|
230
|
+
errors.push(`${label}.${locale} must not be longer than ${maxLength} characters.`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (trimmed) {
|
|
234
|
+
result[locale] = trimmed;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!Object.keys(result).length) {
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
if (!result.en) {
|
|
241
|
+
errors.push(`${label} must include an "en" entry as the fallback for other locales.`);
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
function readListingAssetPath(value, label, errors) {
|
|
247
|
+
if (value === void 0 || value === null) {
|
|
248
|
+
return void 0;
|
|
249
|
+
}
|
|
250
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
251
|
+
errors.push(`${label} must be a non-empty string.`);
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
const trimmed = value.trim();
|
|
255
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed)?.[1]?.toLowerCase();
|
|
256
|
+
if (scheme && scheme !== "https") {
|
|
257
|
+
errors.push(`${label} must be a path on the app origin or an https: URL.`);
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
if (trimmed.startsWith("//")) {
|
|
261
|
+
errors.push(`${label} must be a path on the app origin or an https: URL.`);
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
return trimmed;
|
|
265
|
+
}
|
|
266
|
+
function readListing(value, errors) {
|
|
267
|
+
if (value === void 0 || value === null) {
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
if (!isPlainObject(value)) {
|
|
271
|
+
errors.push("App definition listing must be an object.");
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
const listing = {};
|
|
275
|
+
const summary = readLocalizedText(
|
|
276
|
+
value.summary,
|
|
277
|
+
"App definition listing.summary",
|
|
278
|
+
MINDOODB_APP_LISTING_SUMMARY_MAX,
|
|
279
|
+
errors
|
|
280
|
+
);
|
|
281
|
+
if (summary) listing.summary = summary;
|
|
282
|
+
const description = readLocalizedText(
|
|
283
|
+
value.description,
|
|
284
|
+
"App definition listing.description",
|
|
285
|
+
MINDOODB_APP_LISTING_DESCRIPTION_MAX,
|
|
286
|
+
errors
|
|
287
|
+
);
|
|
288
|
+
if (description) listing.description = description;
|
|
289
|
+
const icon = readListingAssetPath(value.icon, "App definition listing.icon", errors);
|
|
290
|
+
if (icon) listing.icon = icon;
|
|
291
|
+
if (value.screenshots !== void 0 && value.screenshots !== null) {
|
|
292
|
+
if (!Array.isArray(value.screenshots)) {
|
|
293
|
+
errors.push("App definition listing.screenshots must be an array.");
|
|
294
|
+
} else if (value.screenshots.length > MINDOODB_APP_LISTING_MAX_SCREENSHOTS) {
|
|
295
|
+
errors.push(
|
|
296
|
+
`App definition listing.screenshots must not list more than ${MINDOODB_APP_LISTING_MAX_SCREENSHOTS} images.`
|
|
297
|
+
);
|
|
298
|
+
} else {
|
|
299
|
+
const screenshots = [];
|
|
300
|
+
value.screenshots.forEach((entry, index) => {
|
|
301
|
+
const label = `App definition listing.screenshots[${index}]`;
|
|
302
|
+
const raw = typeof entry === "string" ? { file: entry } : entry;
|
|
303
|
+
if (!isPlainObject(raw)) {
|
|
304
|
+
errors.push(`${label} must be a path or an object with a file.`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const file = readListingAssetPath(raw.file, `${label}.file`, errors);
|
|
308
|
+
if (!file) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const caption = readLocalizedText(raw.caption, `${label}.caption`, MINDOODB_APP_LISTING_SUMMARY_MAX, errors);
|
|
312
|
+
screenshots.push(caption ? { file, caption } : { file });
|
|
313
|
+
});
|
|
314
|
+
if (screenshots.length) listing.screenshots = screenshots;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (value.publisher !== void 0 && value.publisher !== null) {
|
|
318
|
+
const publisher = isPlainObject(value.publisher) ? value.publisher : {};
|
|
319
|
+
const name = typeof publisher.name === "string" ? publisher.name.trim() : "";
|
|
320
|
+
if (!name) {
|
|
321
|
+
errors.push("App definition listing.publisher must be an object with a non-empty name.");
|
|
322
|
+
} else {
|
|
323
|
+
const url = readListingAssetPath(publisher.url, "App definition listing.publisher.url", errors);
|
|
324
|
+
if (url && !/^https:\/\//i.test(url)) {
|
|
325
|
+
errors.push("App definition listing.publisher.url must be an absolute https: URL.");
|
|
326
|
+
} else {
|
|
327
|
+
listing.publisher = url ? { name, url } : { name };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return Object.keys(listing).length ? listing : void 0;
|
|
332
|
+
}
|
|
333
|
+
function resolveMindooDBAppLocalizedText(text, locale) {
|
|
334
|
+
if (text === void 0) {
|
|
335
|
+
return "";
|
|
336
|
+
}
|
|
337
|
+
if (typeof text === "string") {
|
|
338
|
+
return text;
|
|
339
|
+
}
|
|
340
|
+
const normalized = locale.trim();
|
|
341
|
+
const language = normalized.split(/[-_]/)[0]?.toLowerCase() ?? "";
|
|
342
|
+
return text[normalized] ?? text[normalized.toLowerCase()] ?? (language ? text[language] : void 0) ?? text.en;
|
|
343
|
+
}
|
|
344
|
+
function resolveMindooDBAppListingAssetUrl(path, originUrl) {
|
|
345
|
+
try {
|
|
346
|
+
const base = new URL(originUrl.endsWith("/") ? originUrl : `${originUrl}/`);
|
|
347
|
+
const resolved = new URL(path, base);
|
|
348
|
+
if (resolved.protocol !== "https:" && resolved.protocol !== base.protocol) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
return resolved.toString();
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
203
356
|
function validateMindooDBAppDefinition(raw) {
|
|
204
357
|
const errors = [];
|
|
205
358
|
if (!isPlainObject(raw)) {
|
|
@@ -251,6 +404,7 @@ function validateMindooDBAppDefinition(raw) {
|
|
|
251
404
|
const allowGeolocation = readOptionalBoolean(raw, "allowGeolocation", errors);
|
|
252
405
|
const allowWebRtc = readOptionalBoolean(raw, "allowWebRtc", errors);
|
|
253
406
|
const allowWorkers = readOptionalBoolean(raw, "allowWorkers", errors);
|
|
407
|
+
const listing = readListing(raw.listing, errors);
|
|
254
408
|
if (defaultLaunchDatabaseId && databases && !databases.some((database) => database.logicalDatabaseId === defaultLaunchDatabaseId)) {
|
|
255
409
|
errors.push(
|
|
256
410
|
`App definition defaultLaunchDatabaseId "${defaultLaunchDatabaseId}" is not one of the declared databases.`
|
|
@@ -279,7 +433,8 @@ function validateMindooDBAppDefinition(raw) {
|
|
|
279
433
|
permissions,
|
|
280
434
|
launchParameters,
|
|
281
435
|
defaultLaunchDatabaseId,
|
|
282
|
-
databases
|
|
436
|
+
databases,
|
|
437
|
+
listing
|
|
283
438
|
},
|
|
284
439
|
errors: []
|
|
285
440
|
};
|
|
@@ -389,6 +544,22 @@ function expandAbbreviatedName(value) {
|
|
|
389
544
|
|
|
390
545
|
// src/client/hosting.ts
|
|
391
546
|
var MINDOODB_APP_HOSTING_QUERY_PARAM = "mindoodbAppHosting";
|
|
547
|
+
var MINDOODB_APP_LAUNCH_ID_QUERY_PARAM = "mindoodbAppLaunchId";
|
|
548
|
+
function readMindooDBAppLaunchId(search) {
|
|
549
|
+
const raw = search ?? (typeof window === "undefined" ? "" : window.location.search);
|
|
550
|
+
if (!raw) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
const launchId = new URLSearchParams(raw).get(MINDOODB_APP_LAUNCH_ID_QUERY_PARAM)?.trim();
|
|
555
|
+
return launchId || null;
|
|
556
|
+
} catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function isLaunchedByHaven(search) {
|
|
561
|
+
return readMindooDBAppLaunchId(search) !== null;
|
|
562
|
+
}
|
|
392
563
|
function readMindooDBAppHostingMode(search) {
|
|
393
564
|
const raw = search ?? (typeof window === "undefined" ? "" : window.location.search);
|
|
394
565
|
if (!raw) {
|
|
@@ -404,6 +575,195 @@ function isHostedBundleRuntime(search) {
|
|
|
404
575
|
return readMindooDBAppHostingMode(search) === "hosted";
|
|
405
576
|
}
|
|
406
577
|
|
|
578
|
+
// src/landing.ts
|
|
579
|
+
var DEFAULT_HAVEN_URL = "https://haven.mindoodb.com";
|
|
580
|
+
var APP_ICON_FALLBACKS = ["appicon.png", "appicon.webp", "appicon.svg"];
|
|
581
|
+
var STRINGS = {
|
|
582
|
+
en: {
|
|
583
|
+
eyebrow: "App for MindooDB Haven",
|
|
584
|
+
cta: "Open in MindooDB Haven",
|
|
585
|
+
ctaHint: "Haven sets up your encrypted workspace and installs {label}. Already using Haven? Then the app is simply added.",
|
|
586
|
+
from: "from {origin}",
|
|
587
|
+
by: "by {publisher}",
|
|
588
|
+
screenshots: "Screenshots",
|
|
589
|
+
footer: "{label} runs inside MindooDB Haven, which keeps its data end-to-end encrypted and local-first. Opening this address on its own shows only this page.",
|
|
590
|
+
learnMore: "What is MindooDB?",
|
|
591
|
+
fallbackSummary: "This app runs inside MindooDB Haven."
|
|
592
|
+
},
|
|
593
|
+
de: {
|
|
594
|
+
eyebrow: "App f\xFCr MindooDB Haven",
|
|
595
|
+
cta: "In MindooDB Haven \xF6ffnen",
|
|
596
|
+
ctaHint: "Haven richtet deine verschl\xFCsselte Umgebung ein und installiert {label}. Du nutzt Haven schon? Dann wird die App einfach hinzugef\xFCgt.",
|
|
597
|
+
from: "von {origin}",
|
|
598
|
+
by: "von {publisher}",
|
|
599
|
+
screenshots: "Screenshots",
|
|
600
|
+
footer: "{label} l\xE4uft in MindooDB Haven, das ihre Daten Ende-zu-Ende-verschl\xFCsselt und lokal h\xE4lt. Direkt aufgerufen zeigt diese Adresse nur diese Seite.",
|
|
601
|
+
learnMore: "Was ist MindooDB?",
|
|
602
|
+
fallbackSummary: "Diese App l\xE4uft in MindooDB Haven."
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
function stringsFor(locale) {
|
|
606
|
+
return locale.toLowerCase().startsWith("de") ? STRINGS.de : STRINGS.en;
|
|
607
|
+
}
|
|
608
|
+
function format(template, values) {
|
|
609
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
|
|
610
|
+
}
|
|
611
|
+
function resolveCurrentHavenAppUrl(href = window.location.href) {
|
|
612
|
+
const url = new URL(href);
|
|
613
|
+
url.search = "";
|
|
614
|
+
url.hash = "";
|
|
615
|
+
url.pathname = url.pathname.replace(/\/index\.html?$/i, "/");
|
|
616
|
+
return url.toString();
|
|
617
|
+
}
|
|
618
|
+
function buildHavenAppInstallUrl(appUrl, havenUrl = DEFAULT_HAVEN_URL) {
|
|
619
|
+
const url = new URL(havenUrl);
|
|
620
|
+
url.searchParams.set("app", appUrl);
|
|
621
|
+
return url.toString();
|
|
622
|
+
}
|
|
623
|
+
async function loadDefinition(appUrl) {
|
|
624
|
+
try {
|
|
625
|
+
const definitionUrl = new URL(MINDOODB_APP_DEFINITION_FILE_NAME, appUrl.endsWith("/") ? appUrl : `${appUrl}/`);
|
|
626
|
+
const response = await fetch(definitionUrl, { headers: { Accept: "application/json" } });
|
|
627
|
+
if (!response.ok) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
return validateMindooDBAppDefinition(await response.json()).definition;
|
|
631
|
+
} catch {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function el(tag, className, text) {
|
|
636
|
+
const node = document.createElement(tag);
|
|
637
|
+
if (className) node.className = className;
|
|
638
|
+
if (text !== void 0) node.textContent = text;
|
|
639
|
+
return node;
|
|
640
|
+
}
|
|
641
|
+
var STYLE_ID = "mdb-landing-style";
|
|
642
|
+
var STYLE = `
|
|
643
|
+
.mdb-landing{--bg:#f4f6fb;--card:#fff;--text:#172033;--muted:#5b6478;--line:#e2e6ef;--accent:#1f3a8a;--accent-text:#fff;
|
|
644
|
+
min-height:100vh;box-sizing:border-box;margin:0;padding:48px 16px;background:var(--bg);color:var(--text);
|
|
645
|
+
font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
646
|
+
@media (prefers-color-scheme:dark){.mdb-landing{--bg:#0e1320;--card:#161d2e;--text:#e8ecf5;--muted:#9aa3b8;--line:#263049;--accent:#8fb0ff;--accent-text:#0e1320}}
|
|
647
|
+
.mdb-landing *{box-sizing:border-box}
|
|
648
|
+
.mdb-landing__card{max-width:880px;margin:0 auto;background:var(--card);border:1px solid var(--line);border-radius:24px;padding:40px;box-shadow:0 20px 60px rgba(15,23,42,.08)}
|
|
649
|
+
.mdb-landing__head{display:flex;gap:24px;align-items:center}
|
|
650
|
+
.mdb-landing__icon{width:96px;height:96px;border-radius:22px;object-fit:cover;flex:none;box-shadow:0 10px 30px rgba(15,23,42,.18)}
|
|
651
|
+
.mdb-landing__eyebrow{margin:0 0 4px;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
|
|
652
|
+
.mdb-landing__title{margin:0;font-size:clamp(26px,4vw,36px);line-height:1.15}
|
|
653
|
+
.mdb-landing__meta{margin:6px 0 0;color:var(--muted);font-size:14px}
|
|
654
|
+
.mdb-landing__summary{margin:28px 0 0;font-size:19px}
|
|
655
|
+
.mdb-landing__cta{display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin:28px 0 0}
|
|
656
|
+
.mdb-landing__button{display:inline-block;padding:14px 22px;border-radius:12px;background:var(--accent);color:var(--accent-text);font-weight:600;text-decoration:none}
|
|
657
|
+
.mdb-landing__button:focus-visible{outline:3px solid var(--accent);outline-offset:3px}
|
|
658
|
+
.mdb-landing__hint{margin:0;max-width:420px;color:var(--muted);font-size:14px}
|
|
659
|
+
.mdb-landing__description{margin:32px 0 0}
|
|
660
|
+
.mdb-landing__description p{margin:0 0 12px}
|
|
661
|
+
.mdb-landing__shots-title{margin:32px 0 12px;font-size:15px;color:var(--muted);font-weight:600}
|
|
662
|
+
.mdb-landing__shots{display:grid;grid-auto-flow:column;grid-auto-columns:min(78%,420px);gap:16px;overflow-x:auto;padding-bottom:8px;scroll-snap-type:x mandatory}
|
|
663
|
+
.mdb-landing__shot{margin:0;scroll-snap-align:start}
|
|
664
|
+
.mdb-landing__shot img{width:100%;border-radius:14px;border:1px solid var(--line);display:block}
|
|
665
|
+
.mdb-landing__shot figcaption{margin-top:8px;font-size:13px;color:var(--muted)}
|
|
666
|
+
.mdb-landing__footer{margin:36px 0 0;padding-top:20px;border-top:1px solid var(--line);color:var(--muted);font-size:13px}
|
|
667
|
+
.mdb-landing__footer a{color:inherit}
|
|
668
|
+
@media (max-width:560px){.mdb-landing{padding:16px}.mdb-landing__card{padding:24px;border-radius:18px}.mdb-landing__head{flex-direction:column;align-items:flex-start}.mdb-landing__icon{width:72px;height:72px}}
|
|
669
|
+
`;
|
|
670
|
+
async function renderHavenAppLandingPage(options = {}) {
|
|
671
|
+
const appUrl = options.appUrl ?? resolveCurrentHavenAppUrl();
|
|
672
|
+
const locale = options.locale ?? (typeof navigator === "undefined" ? "en" : navigator.language || "en");
|
|
673
|
+
const strings = stringsFor(locale);
|
|
674
|
+
const definition = options.definition ?? await loadDefinition(appUrl);
|
|
675
|
+
const installUrl = buildHavenAppInstallUrl(appUrl, options.havenUrl);
|
|
676
|
+
const listing = definition?.listing;
|
|
677
|
+
const label = definition?.label ?? (document.title || new URL(appUrl).host);
|
|
678
|
+
const origin = new URL(appUrl).host;
|
|
679
|
+
if (!document.getElementById(STYLE_ID)) {
|
|
680
|
+
const style = el("style");
|
|
681
|
+
style.id = STYLE_ID;
|
|
682
|
+
style.textContent = STYLE;
|
|
683
|
+
document.head.append(style);
|
|
684
|
+
}
|
|
685
|
+
document.title = label;
|
|
686
|
+
const root = el("main", "mdb-landing");
|
|
687
|
+
root.dataset.testid = "haven-app-landing";
|
|
688
|
+
const card = el("article", "mdb-landing__card");
|
|
689
|
+
root.append(card);
|
|
690
|
+
const head = el("header", "mdb-landing__head");
|
|
691
|
+
const iconCandidates = (listing?.icon ? [listing.icon] : [...APP_ICON_FALLBACKS]).map((path) => resolveMindooDBAppListingAssetUrl(path, appUrl)).filter((url) => Boolean(url));
|
|
692
|
+
if (iconCandidates.length) {
|
|
693
|
+
const icon = el("img", "mdb-landing__icon");
|
|
694
|
+
let next = 0;
|
|
695
|
+
icon.alt = "";
|
|
696
|
+
icon.addEventListener("error", () => {
|
|
697
|
+
next += 1;
|
|
698
|
+
if (next < iconCandidates.length) {
|
|
699
|
+
icon.src = iconCandidates[next];
|
|
700
|
+
} else {
|
|
701
|
+
icon.remove();
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
icon.src = iconCandidates[0];
|
|
705
|
+
head.append(icon);
|
|
706
|
+
}
|
|
707
|
+
const titles = el("div");
|
|
708
|
+
titles.append(el("p", "mdb-landing__eyebrow", strings.eyebrow), el("h1", "mdb-landing__title", label));
|
|
709
|
+
const meta = [format(strings.from, { origin })];
|
|
710
|
+
if (listing?.publisher) {
|
|
711
|
+
meta.unshift(format(strings.by, { publisher: listing.publisher.name }));
|
|
712
|
+
}
|
|
713
|
+
titles.append(el("p", "mdb-landing__meta", meta.join(" \xB7 ")));
|
|
714
|
+
head.append(titles);
|
|
715
|
+
card.append(head);
|
|
716
|
+
const summary = resolveMindooDBAppLocalizedText(listing?.summary, locale) || definition?.description || strings.fallbackSummary;
|
|
717
|
+
card.append(el("p", "mdb-landing__summary", summary));
|
|
718
|
+
const cta = el("div", "mdb-landing__cta");
|
|
719
|
+
const button = el("a", "mdb-landing__button", strings.cta);
|
|
720
|
+
button.href = installUrl;
|
|
721
|
+
button.rel = "noopener";
|
|
722
|
+
button.dataset.testid = "haven-app-landing-install";
|
|
723
|
+
cta.append(button, el("p", "mdb-landing__hint", format(strings.ctaHint, { label })));
|
|
724
|
+
card.append(cta);
|
|
725
|
+
const description = resolveMindooDBAppLocalizedText(listing?.description, locale);
|
|
726
|
+
if (description) {
|
|
727
|
+
const block = el("section", "mdb-landing__description");
|
|
728
|
+
for (const paragraph of description.split(/\n\s*\n/)) {
|
|
729
|
+
if (paragraph.trim()) block.append(el("p", void 0, paragraph.trim()));
|
|
730
|
+
}
|
|
731
|
+
card.append(block);
|
|
732
|
+
}
|
|
733
|
+
const screenshots = (listing?.screenshots ?? []).map((shot) => ({
|
|
734
|
+
url: resolveMindooDBAppListingAssetUrl(shot.file, appUrl),
|
|
735
|
+
caption: resolveMindooDBAppLocalizedText(shot.caption, locale)
|
|
736
|
+
})).filter((shot) => Boolean(shot.url));
|
|
737
|
+
if (screenshots.length) {
|
|
738
|
+
card.append(el("h2", "mdb-landing__shots-title", strings.screenshots));
|
|
739
|
+
const strip = el("div", "mdb-landing__shots");
|
|
740
|
+
for (const shot of screenshots) {
|
|
741
|
+
const figure = el("figure", "mdb-landing__shot");
|
|
742
|
+
const image = el("img");
|
|
743
|
+
image.src = shot.url;
|
|
744
|
+
image.alt = shot.caption;
|
|
745
|
+
image.loading = "lazy";
|
|
746
|
+
figure.append(image);
|
|
747
|
+
if (shot.caption) figure.append(el("figcaption", void 0, shot.caption));
|
|
748
|
+
strip.append(figure);
|
|
749
|
+
}
|
|
750
|
+
card.append(strip);
|
|
751
|
+
}
|
|
752
|
+
const footer = el("footer", "mdb-landing__footer");
|
|
753
|
+
footer.append(format(strings.footer, { label }), " ");
|
|
754
|
+
const learnMore = el("a", void 0, strings.learnMore);
|
|
755
|
+
learnMore.href = "https://mindoodb.com";
|
|
756
|
+
learnMore.rel = "noopener";
|
|
757
|
+
footer.append(learnMore);
|
|
758
|
+
card.append(footer);
|
|
759
|
+
const container = options.container ?? document.body;
|
|
760
|
+
container.replaceChildren(root);
|
|
761
|
+
if (container === document.body) {
|
|
762
|
+
document.body.style.margin = "0";
|
|
763
|
+
}
|
|
764
|
+
return { definition, installUrl };
|
|
765
|
+
}
|
|
766
|
+
|
|
407
767
|
// src/client/havenStorageShim.ts
|
|
408
768
|
var DEFAULT_QUOTA_BYTES = 256 * 1024;
|
|
409
769
|
var DEFAULT_MAX_VALUE_BYTES = 64 * 1024;
|
|
@@ -731,11 +1091,11 @@ var MindooDBRichTextHandle = class {
|
|
|
731
1091
|
this.pollTimer = null;
|
|
732
1092
|
}
|
|
733
1093
|
}
|
|
734
|
-
reconcile(
|
|
1094
|
+
reconcile(document2, snapshot) {
|
|
735
1095
|
const previous = JSON.stringify(this.spansInternal);
|
|
736
1096
|
this.spansInternal = cloneSpans(snapshot.spans);
|
|
737
1097
|
this.baseMaterializedText = spansToPlainText(snapshot.spans);
|
|
738
|
-
this.heads = snapshot.heads ??
|
|
1098
|
+
this.heads = snapshot.heads ?? document2.heads ?? [];
|
|
739
1099
|
this.dirtyInternal = false;
|
|
740
1100
|
const changed = JSON.stringify(this.spansInternal) !== previous;
|
|
741
1101
|
if (changed) {
|
|
@@ -754,14 +1114,14 @@ var MindooDBRichTextHandle = class {
|
|
|
754
1114
|
if (this.dirtyInternal) {
|
|
755
1115
|
return false;
|
|
756
1116
|
}
|
|
757
|
-
const
|
|
1117
|
+
const document2 = await this.options.database.documents.get(
|
|
758
1118
|
this.options.document.id
|
|
759
1119
|
);
|
|
760
|
-
if (!
|
|
1120
|
+
if (!document2) {
|
|
761
1121
|
return false;
|
|
762
1122
|
}
|
|
763
1123
|
const snapshot = await this.refresh();
|
|
764
|
-
return this.reconcile(
|
|
1124
|
+
return this.reconcile(document2, snapshot);
|
|
765
1125
|
}
|
|
766
1126
|
async flush(updateSpansConfig) {
|
|
767
1127
|
if (!this.dirtyInternal) {
|
|
@@ -778,7 +1138,7 @@ var MindooDBRichTextHandle = class {
|
|
|
778
1138
|
const localSpansAtFlush = cloneSpans(this.spansInternal);
|
|
779
1139
|
const localTextAtFlush = spansToPlainText(localSpansAtFlush);
|
|
780
1140
|
const textChanged = localTextAtFlush !== this.baseMaterializedText;
|
|
781
|
-
const
|
|
1141
|
+
const document2 = textChanged ? await this.flushTextSteps(localTextAtFlush) : await this.options.database.documents.update(
|
|
782
1142
|
this.options.document.id,
|
|
783
1143
|
{
|
|
784
1144
|
richText: [{
|
|
@@ -796,13 +1156,13 @@ var MindooDBRichTextHandle = class {
|
|
|
796
1156
|
const reconciled = JSON.stringify(snapshot.spans) !== JSON.stringify(localSpansAtFlush);
|
|
797
1157
|
this.spansInternal = cloneSpans(snapshot.spans);
|
|
798
1158
|
this.baseMaterializedText = spansToPlainText(snapshot.spans);
|
|
799
|
-
this.heads = snapshot.heads ??
|
|
1159
|
+
this.heads = snapshot.heads ?? document2.heads ?? [];
|
|
800
1160
|
this.dirtyInternal = false;
|
|
801
1161
|
if (reconciled) {
|
|
802
1162
|
this.emitChange();
|
|
803
1163
|
}
|
|
804
1164
|
return {
|
|
805
|
-
document,
|
|
1165
|
+
document: document2,
|
|
806
1166
|
snapshot,
|
|
807
1167
|
reconciled
|
|
808
1168
|
};
|
|
@@ -940,11 +1300,11 @@ var MindooDBTextBuffer = class {
|
|
|
940
1300
|
* Returns whether the text value changed. Use this after an external refresh
|
|
941
1301
|
* or after receiving a document update through another app-level mechanism.
|
|
942
1302
|
*/
|
|
943
|
-
reconcile(
|
|
944
|
-
const nextValue = readTextAtPath(
|
|
1303
|
+
reconcile(document2) {
|
|
1304
|
+
const nextValue = readTextAtPath(document2.data, this.options.path);
|
|
945
1305
|
const changed = nextValue !== this.valueInternal;
|
|
946
1306
|
this.valueInternal = nextValue;
|
|
947
|
-
this.heads =
|
|
1307
|
+
this.heads = document2.heads ? [...document2.heads] : [];
|
|
948
1308
|
this.pendingEdits = [];
|
|
949
1309
|
return changed;
|
|
950
1310
|
}
|
|
@@ -966,20 +1326,20 @@ var MindooDBTextBuffer = class {
|
|
|
966
1326
|
};
|
|
967
1327
|
}
|
|
968
1328
|
const localValueAtFlush = this.valueInternal;
|
|
969
|
-
const
|
|
1329
|
+
const document2 = await this.options.database.documents.update(this.options.document.id, {
|
|
970
1330
|
text: [{
|
|
971
1331
|
path: this.options.path,
|
|
972
1332
|
baseHeads: this.heads,
|
|
973
1333
|
edits: this.pendingEdits.map((edit) => ({ ...edit }))
|
|
974
1334
|
}]
|
|
975
1335
|
});
|
|
976
|
-
const nextValue = readTextAtPath(
|
|
1336
|
+
const nextValue = readTextAtPath(document2.data, this.options.path);
|
|
977
1337
|
const reconciled = nextValue !== localValueAtFlush;
|
|
978
1338
|
this.valueInternal = nextValue;
|
|
979
|
-
this.heads =
|
|
1339
|
+
this.heads = document2.heads ? [...document2.heads] : [];
|
|
980
1340
|
this.pendingEdits = [];
|
|
981
1341
|
return {
|
|
982
|
-
document,
|
|
1342
|
+
document: document2,
|
|
983
1343
|
value: this.valueInternal,
|
|
984
1344
|
reconciled
|
|
985
1345
|
};
|
|
@@ -1055,6 +1415,7 @@ export {
|
|
|
1055
1415
|
DEFAULT_DRAG_LONG_PRESS_MS,
|
|
1056
1416
|
DEFAULT_DRAG_POINTER_THRESHOLD_PX,
|
|
1057
1417
|
DEFAULT_HAVEN_HOST_SHORTCUTS,
|
|
1418
|
+
DEFAULT_HAVEN_URL,
|
|
1058
1419
|
MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME,
|
|
1059
1420
|
MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME,
|
|
1060
1421
|
MINDOODB_APP_BUNDLE_MANIFEST_FORMAT,
|
|
@@ -1065,6 +1426,10 @@ export {
|
|
|
1065
1426
|
MINDOODB_APP_DEFINITION_REGISTRATION_PERMISSIONS,
|
|
1066
1427
|
MINDOODB_APP_DEFINITION_VERSION,
|
|
1067
1428
|
MINDOODB_APP_HOSTING_QUERY_PARAM,
|
|
1429
|
+
MINDOODB_APP_LAUNCH_ID_QUERY_PARAM,
|
|
1430
|
+
MINDOODB_APP_LISTING_DESCRIPTION_MAX,
|
|
1431
|
+
MINDOODB_APP_LISTING_MAX_SCREENSHOTS,
|
|
1432
|
+
MINDOODB_APP_LISTING_SUMMARY_MAX,
|
|
1068
1433
|
MINDOODB_APP_MAX_DRAG_PAYLOAD_BYTES,
|
|
1069
1434
|
MINDOODB_APP_MAX_DRAG_PREVIEW_BYTES,
|
|
1070
1435
|
MINDOODB_APP_MAX_DRAG_PREVIEW_EDGE,
|
|
@@ -1080,6 +1445,7 @@ export {
|
|
|
1080
1445
|
abbreviateCanonicalName,
|
|
1081
1446
|
applyHostedDocumentOverscrollContain,
|
|
1082
1447
|
bindDragSource,
|
|
1448
|
+
buildHavenAppInstallUrl,
|
|
1083
1449
|
buildMindooDBAppBundleContentHashInput,
|
|
1084
1450
|
canPreviewAttachment,
|
|
1085
1451
|
createMindooDBAppBridge,
|
|
@@ -1092,6 +1458,7 @@ export {
|
|
|
1092
1458
|
installHostShortcutCapture,
|
|
1093
1459
|
isHostShortcutAction,
|
|
1094
1460
|
isHostedBundleRuntime,
|
|
1461
|
+
isLaunchedByHaven,
|
|
1095
1462
|
isMindooDBAppBundleHash,
|
|
1096
1463
|
isMindooDBAppDragType,
|
|
1097
1464
|
matchHostShortcut,
|
|
@@ -1101,8 +1468,13 @@ export {
|
|
|
1101
1468
|
normalizeMindooDBAppBundlePath,
|
|
1102
1469
|
queryDocuments,
|
|
1103
1470
|
readMindooDBAppHostingMode,
|
|
1471
|
+
readMindooDBAppLaunchId,
|
|
1104
1472
|
releaseMindooDBAppBridgeSessions,
|
|
1473
|
+
renderHavenAppLandingPage,
|
|
1474
|
+
resolveCurrentHavenAppUrl,
|
|
1105
1475
|
resolveMindooDBAppDefinitionUrl,
|
|
1476
|
+
resolveMindooDBAppListingAssetUrl,
|
|
1477
|
+
resolveMindooDBAppLocalizedText,
|
|
1106
1478
|
sanitizeHostShortcutBindings,
|
|
1107
1479
|
snapshotElementToPng,
|
|
1108
1480
|
validateMindooDBAppBundleManifest,
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ay as MindooDBAppCreateViewNavigatorInput, cC as MindooDBAppViewNavigator, $ as MindooDBAppBridgeRpcRequest, b7 as MindooDBAppHostTheme, ag as MindooDBAppCapability, i as MindooDBAppDefinition, e as MindooDBAppBridge, cc as MindooDBAppSession, bE as MindooDBAppProposeAppInput, cK as MindooDBAppViewport, cw as MindooDBAppUiPreferences, bK as MindooDBAppQueryResult, cJ as MindooDBAppViewUpdateStats, X as MindooDBAppBridgePortMessage, bl as MindooDBAppLaunchContext, az as MindooDBAppDatabaseInfo, aI as MindooDBAppDocumentApi, cE as MindooDBAppViewNavigatorOpenOptions, A as MindooDBAppAttachmentApi, b8 as MindooDBAppIdentityApi, cs as MindooDBAppTimestampApi, aG as MindooDBAppDirectoryApi, k as MindooDBAppDocument, b4 as MindooDBAppFulltextSetup, b3 as MindooDBAppExtractionSetup, ck as MindooDBAppSummarySetup, bF as MindooDBAppProposeAppResult, c as MindooDBAppDragStartResult, aZ as MindooDBAppDragProfile } from '../types-CsKw_niD.js';
|
|
2
2
|
import * as mindoodb from 'mindoodb';
|
|
3
3
|
import 'mindoodb-view-language';
|
|
4
4
|
|
|
@@ -21,6 +21,92 @@ declare function loadMindoodbForTesting(): Promise<MindoodbVirtualViews | null>;
|
|
|
21
21
|
*/
|
|
22
22
|
declare function createEvaluatingViewNavigator(input: MindooDBAppCreateViewNavigatorInput, documents: EvaluatingViewDocument[]): Promise<MindooDBAppViewNavigator | null>;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A stand-in Haven for a real browser: the app runs in an iframe, exactly as Haven frames
|
|
26
|
+
* it, and talks to the same in-memory mock host the Vitest helpers use.
|
|
27
|
+
*
|
|
28
|
+
* This is what an app's test URL (`/__haven-test/` in the starter) mounts, so Playwright
|
|
29
|
+
* and a developer's own eyes see the real app — real bridge handshake, real DOM — with
|
|
30
|
+
* seeded data and scriptable host services, and without setting up an identity, a
|
|
31
|
+
* tenant and an install in a real Haven. It is never meant to be deployed where end
|
|
32
|
+
* users can reach it.
|
|
33
|
+
*/
|
|
34
|
+
/** A scan the test host hands out on the app's next `attachments.scan()` call. */
|
|
35
|
+
interface BrowserTestHostScan {
|
|
36
|
+
fileName: string;
|
|
37
|
+
mimeType: "image/jpeg" | "image/png" | "application/pdf";
|
|
38
|
+
size: number;
|
|
39
|
+
}
|
|
40
|
+
/** What the test host recorded, in the order the app asked. */
|
|
41
|
+
interface BrowserTestHostLog {
|
|
42
|
+
notifications: Array<{
|
|
43
|
+
id?: string;
|
|
44
|
+
severity?: string;
|
|
45
|
+
text?: string;
|
|
46
|
+
durationMs?: number;
|
|
47
|
+
}>;
|
|
48
|
+
previews: Array<{
|
|
49
|
+
databaseId: string;
|
|
50
|
+
docId: string;
|
|
51
|
+
attachmentName: string;
|
|
52
|
+
}>;
|
|
53
|
+
scans: Array<{
|
|
54
|
+
databaseId: string;
|
|
55
|
+
docId: string;
|
|
56
|
+
result: "scanned" | "cancelled";
|
|
57
|
+
}>;
|
|
58
|
+
focusRequests: number;
|
|
59
|
+
requests: MindooDBAppBridgeRpcRequest[];
|
|
60
|
+
}
|
|
61
|
+
interface CreateBrowserTestHostOptions extends CreateFakeBridgeHostOptions {
|
|
62
|
+
/** The iframe the app runs in. Its `src` is set by the host. */
|
|
63
|
+
frame: HTMLIFrameElement;
|
|
64
|
+
/** URL of the app page, relative to the test page or absolute. */
|
|
65
|
+
appUrl: string;
|
|
66
|
+
/** Called after every request, e.g. to re-render a control panel. */
|
|
67
|
+
onChange?: () => void;
|
|
68
|
+
}
|
|
69
|
+
interface BrowserTestHost {
|
|
70
|
+
host: FakeBridgeHostController;
|
|
71
|
+
log: BrowserTestHostLog;
|
|
72
|
+
/**
|
|
73
|
+
* Decides the outcome of the app's next `attachments.scan()`: a file description to
|
|
74
|
+
* "scan", or `null` to act as if the user cancelled. Without a call, scans cancel.
|
|
75
|
+
*/
|
|
76
|
+
setNextScan(scan: BrowserTestHostScan | null): void;
|
|
77
|
+
setTheme(theme: MindooDBAppHostTheme): void;
|
|
78
|
+
setHostFocus(focused: boolean): void;
|
|
79
|
+
/** Reloads the app frame. The mock data survives; the app's in-memory state does not. */
|
|
80
|
+
reloadApp(): void;
|
|
81
|
+
dispose(): void;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The capabilities Haven grants for the permissions a definition requests. `read` is
|
|
85
|
+
* implied by the mapping, `write` is create plus update.
|
|
86
|
+
*/
|
|
87
|
+
declare function capabilitiesForDefinitionPermissions(permissions: readonly string[] | undefined): MindooDBAppCapability[];
|
|
88
|
+
/** Mock databases for every database a `haven-app.json` declares, granted as requested. */
|
|
89
|
+
declare function mockDatabasesFromDefinition(definition: Pick<MindooDBAppDefinition, "databases">, seed?: Record<string, MockMindooDBAppDatabaseDefinition["documents"]>): MockMindooDBAppDatabaseDefinition[];
|
|
90
|
+
declare function createBrowserTestHost(options: CreateBrowserTestHostOptions): BrowserTestHost;
|
|
91
|
+
interface MountHavenTestHostOptions extends Omit<CreateBrowserTestHostOptions, "frame" | "onChange"> {
|
|
92
|
+
/** Element to render into. Defaults to `document.body`, which is cleared. */
|
|
93
|
+
container?: HTMLElement;
|
|
94
|
+
/** Heading of the control panel, e.g. the app label. */
|
|
95
|
+
title?: string;
|
|
96
|
+
}
|
|
97
|
+
declare global {
|
|
98
|
+
interface Window {
|
|
99
|
+
/** Set by {@link mountHavenTestHost} so Playwright can script the host. */
|
|
100
|
+
__havenTestHost?: BrowserTestHost;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Renders the full test page: the app frame plus a control panel for theme, host focus
|
|
105
|
+
* and the next scan, and a live log of notifications, previews, scans and requests.
|
|
106
|
+
* The returned host is also available as `window.__havenTestHost`.
|
|
107
|
+
*/
|
|
108
|
+
declare function mountHavenTestHost(options: MountHavenTestHostOptions): BrowserTestHost;
|
|
109
|
+
|
|
24
110
|
type MaybePromise<T> = T | Promise<T>;
|
|
25
111
|
type MockViewApi = {
|
|
26
112
|
create(input: MindooDBAppCreateViewNavigatorInput): MaybePromise<MindooDBAppViewNavigator>;
|
|
@@ -149,6 +235,8 @@ type FakeBridgePortMessageHandler = (message: Exclude<MindooDBAppBridgePortMessa
|
|
|
149
235
|
interface CreateFakeBridgeHostOptions extends CreateMockMindooDBAppSessionOptions {
|
|
150
236
|
windowMode?: "parent" | "opener";
|
|
151
237
|
requestHandlers?: Record<string, FakeBridgeRequestHandler>;
|
|
238
|
+
/** Called for every RPC request the app sends, before it is answered. */
|
|
239
|
+
onRequest?: (request: MindooDBAppBridgeRpcRequest) => void;
|
|
152
240
|
onPortMessage?: FakeBridgePortMessageHandler;
|
|
153
241
|
}
|
|
154
242
|
interface FakeBridgeHostController {
|
|
@@ -159,6 +247,13 @@ interface FakeBridgeHostController {
|
|
|
159
247
|
/** Every `apps.propose` call the app under test made, in order. */
|
|
160
248
|
readonly proposedApps: ReadonlyArray<MindooDBAppProposeAppInput>;
|
|
161
249
|
install(): void;
|
|
250
|
+
/**
|
|
251
|
+
* Answers a `mindoodb-app:connect` handshake received some other way than through
|
|
252
|
+
* `install()` — in a real browser, from the app frame's `message` event
|
|
253
|
+
* (`host.acceptConnection(event.data, event.ports)`). Returns `false` for messages
|
|
254
|
+
* that are not a handshake for this launch.
|
|
255
|
+
*/
|
|
256
|
+
acceptConnection(message: unknown, transfer?: readonly Transferable[]): boolean;
|
|
162
257
|
dispose(): void;
|
|
163
258
|
emitThemeChange(theme: MindooDBAppHostTheme): void;
|
|
164
259
|
emitViewportChange(viewport: MindooDBAppViewport): void;
|
|
@@ -180,4 +275,4 @@ interface FakeBridgeHostController {
|
|
|
180
275
|
}
|
|
181
276
|
declare function createFakeBridgeHost(options?: CreateFakeBridgeHostOptions): FakeBridgeHostController;
|
|
182
277
|
|
|
183
|
-
export { type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockSealedChannelOptions, type MockSeedDocument, createEvaluatingViewNavigator, createFakeBridgeHost, createMockMindooDBAppBridge, createMockMindooDBAppSession, loadMindoodbForTesting };
|
|
278
|
+
export { type BrowserTestHost, type BrowserTestHostLog, type BrowserTestHostScan, type CreateBrowserTestHostOptions, type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockSealedChannelOptions, type MockSeedDocument, type MountHavenTestHostOptions, capabilitiesForDefinitionPermissions, createBrowserTestHost, createEvaluatingViewNavigator, createFakeBridgeHost, createMockMindooDBAppBridge, createMockMindooDBAppSession, loadMindoodbForTesting, mockDatabasesFromDefinition, mountHavenTestHost };
|