@ossy/platform 3.13.0 → 3.15.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/platform",
3
- "version": "3.13.0",
3
+ "version": "3.15.0",
4
4
  "description": "Ossy application server runtime",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,14 +48,15 @@
48
48
  "@ossy/config": "^3.0.9",
49
49
  "@ossy/event-store": "^3.12.0",
50
50
  "@ossy/locale": "^3.4.0",
51
- "@ossy/manifest": "^3.9.0",
51
+ "@ossy/manifest": "^3.15.0",
52
52
  "@ossy/observability": "^3.0.9",
53
53
  "@ossy/policies": "^3.0.9",
54
- "@ossy/schema": "^3.8.0",
54
+ "@ossy/router": "^3.0.9",
55
+ "@ossy/schema": "^3.15.0",
55
56
  "@ossy/sdk": "^3.12.0",
56
57
  "@ossy/tokens": "^3.11.1",
57
- "@ossy/users": "^3.12.0",
58
- "@ossy/workspaces": "^3.12.0",
58
+ "@ossy/users": "^3.15.0",
59
+ "@ossy/workspaces": "^3.15.0",
59
60
  "cookie-parser": "^1.4.7",
60
61
  "dotenv": ">=16.0.0 <18.0.0",
61
62
  "express": ">=5.0.0 <6.0.0",
@@ -76,5 +77,5 @@
76
77
  "Dockerfile",
77
78
  "docker-healthcheck.js"
78
79
  ],
79
- "gitHead": "7a02d1e056b731aa36f72e3f8d6df941101dbd50"
80
+ "gitHead": "23517232ddc4f2c64ca0062faa3cad84ceaa34a7"
80
81
  }
package/src/runtime.js CHANGED
@@ -180,14 +180,23 @@ export async function startRuntime ({ port } = {}) {
180
180
  return
181
181
  }
182
182
 
183
+ const config = manifest.config
184
+ const { language, messages, fallbackMessages } = resolveRequestLocale(pageRouter, requestUrl, buildDir, config)
185
+ if (typeof pageEntry.redirect === 'string' && pageEntry.redirect.trim()) {
186
+ const targetId = pageEntry.redirect.trim().replace(/^@/, '')
187
+ const targetPath = pageRouter.getPathname({ id: targetId, language })
188
+ if (targetPath) {
189
+ const search = new URL(requestUrl, 'http://localhost').search || ''
190
+ res.redirect(302, `${targetPath}${search}`)
191
+ return
192
+ }
193
+ }
194
+
183
195
  const mod = await loadEntry(pageEntry.entry)
184
196
  if (typeof mod.render !== 'function') {
185
197
  res.status(503).type('text').send('SSR runtime unavailable')
186
198
  return
187
199
  }
188
-
189
- const config = manifest.config
190
- const { language, messages, fallbackMessages } = resolveRequestLocale(pageRouter, requestUrl, buildDir, config)
191
200
  const {
192
201
  Layout,
193
202
  layoutEntry,
@@ -212,6 +221,9 @@ export async function startRuntime ({ port } = {}) {
212
221
  id: page.id,
213
222
  path: page.path,
214
223
  ...(page.package ? { package: page.package } : {}),
224
+ ...(typeof page.redirect === 'string' && page.redirect.trim()
225
+ ? { redirect: page.redirect.trim() }
226
+ : {}),
215
227
  })),
216
228
  pageId: pageRoute.id,
217
229
  componentEntries: manifest.components,
package/src/server.js CHANGED
@@ -505,6 +505,16 @@ export async function startServer (options = {}) {
505
505
  res.status(404).send('Not found')
506
506
  return
507
507
  }
508
+ if (typeof pageEntry.redirect === 'string' && pageEntry.redirect.trim()) {
509
+ const { language } = resolveRequestLocale(pageRouter, requestUrl, buildDir, config)
510
+ const targetId = pageEntry.redirect.trim().replace(/^@/, '')
511
+ const targetPath = pageRouter.getPathname({ id: targetId, language })
512
+ if (targetPath) {
513
+ const search = new URL(requestUrl, 'http://localhost').search || ''
514
+ res.redirect(302, `${targetPath}${search}`)
515
+ return
516
+ }
517
+ }
508
518
  const loadEntryTimer = createTimedOperation(`SSR loadEntry ${pageRoute.id}`, { log })
509
519
  const mod = await loadEntry(pageEntry.entry)
510
520
  loadEntryTimer.finish()
@@ -575,6 +585,9 @@ export async function startServer (options = {}) {
575
585
  id: page.id,
576
586
  path: page.path,
577
587
  ...(page.package ? { package: page.package } : {}),
588
+ ...(typeof page.redirect === 'string' && page.redirect.trim()
589
+ ? { redirect: page.redirect.trim() }
590
+ : {}),
578
591
  })),
579
592
  pageId: pageRoute.id,
580
593
  componentEntries: manifest.components,
@@ -328,23 +328,49 @@ export async function runFlow (flow, options = {}) {
328
328
  if (value === undefined || value === null) {
329
329
  throw new Error(`mockFormContent produced no value for field "${field.name}"`)
330
330
  }
331
- const locator = formRoot.locator(`[name="${field.name}"]`).first()
332
- await locator.waitFor({ state: 'visible', timeout: formTimeout })
331
+ const asText = String(value)
332
+ const nameAttr = escapeCssAttrValue(field.name)
333
+ const locator = formRoot.locator(`[name="${nameAttr}"]`).first()
334
+ // Overlay Select uses a hidden input; wait for attach, not visibility.
335
+ await locator.waitFor({ state: 'attached', timeout: formTimeout })
333
336
  const tag = await locator.evaluate(el => el.tagName.toLowerCase()).catch(() => 'input')
334
337
  if (tag === 'select') {
335
- await locator.selectOption(String(value))
338
+ await locator.waitFor({ state: 'visible', timeout: formTimeout })
339
+ await locator.selectOption(asText)
336
340
  return
337
341
  }
338
342
  if (tag === 'input') {
339
343
  const inputType = await locator.getAttribute('type')
340
344
  if (inputType === 'checkbox') {
345
+ await locator.waitFor({ state: 'visible', timeout: formTimeout })
341
346
  if (value) await locator.check()
342
347
  else await locator.uncheck()
343
348
  return
344
349
  }
350
+ // Design-system Select: hidden input + Dropdown / ContextMenu options.
351
+ const inOverlaySelect = await locator.evaluate(
352
+ (el) => !!el.closest?.('[data-select]'),
353
+ ).catch(() => false)
354
+ if (inputType === 'hidden' && (inOverlaySelect || field.type === 'select' || field.type === 'month')) {
355
+ const current = await locator.inputValue().catch(() => '')
356
+ if (current === asText) return
357
+ const trigger = formRoot
358
+ .locator(`[data-select]:has([name="${nameAttr}"]) [data-select-trigger]`)
359
+ .first()
360
+ await trigger.waitFor({ state: 'visible', timeout: formTimeout })
361
+ await trigger.click()
362
+ // Menu is portaled via Overlay — scope to the page, not the form.
363
+ const option = page
364
+ .locator(`[data-select-menu] [role="option"][data-value="${escapeCssAttrValue(asText)}"]`)
365
+ .first()
366
+ await option.waitFor({ state: 'visible', timeout: formTimeout })
367
+ await option.click()
368
+ await expectFn(locator).toHaveValue(asText, { timeout: 2000 })
369
+ return
370
+ }
345
371
  }
372
+ await locator.waitFor({ state: 'visible', timeout: formTimeout })
346
373
  // Retry fills: early hydration / sibling controlled updates can wipe earlier fields.
347
- const asText = String(value)
348
374
  for (let attempt = 0; attempt < 3; attempt++) {
349
375
  await locator.fill(asText)
350
376
  // Blur so React controlled state commits before the next field fill.
@@ -381,6 +407,7 @@ export async function runFlow (flow, options = {}) {
381
407
  if (tag === 'input') {
382
408
  const inputType = await locator.getAttribute('type')
383
409
  if (inputType === 'checkbox' || inputType === 'file') continue
410
+ // Overlay Select keeps value on a hidden input — still verify/repair it.
384
411
  }
385
412
  const asText = String(value)
386
413
  const current = await locator.inputValue().catch(() => '')