@cosmicdrift/kumiko-renderer 0.350.0 → 0.352.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.
@@ -3,8 +3,8 @@ import { evalFieldCondition, isWriteFormEditSection, metricField, parseRefTarget
3
3
  import { applyFormatSpec, buildOptionLabels, computeRelatedListSectionViewModel, fieldLabelKey, fieldOptionLabelKey, isSafeHref, } from "@cosmicdrift/kumiko-headless";
4
4
  import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
5
5
  import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
6
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
7
  import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
7
- import { Temporal } from "temporal-polyfill";
8
8
  import { QueryOptionLabel, resolveOptionsQueryPayload, } from "../components/query-options-select.js";
9
9
  import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
10
10
  import { RenderEdit, resolveRecordTitle, } from "../components/render-edit.js";
@@ -1,7 +1,7 @@
1
1
  // Split from kumiko-screen.tsx: related-list-section.tsx renders through
2
2
  // kumiko-screen.tsx, so importing this back from there would be a require cycle.
3
3
  import { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
4
- import { Temporal } from "temporal-polyfill";
4
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
5
5
  // Fills in a "reference" spec's initially-empty `options` from the
6
6
  // field-keyed options ReferenceFacetBridges resolved, shared by every caller.
7
7
  export function mergeReferenceFacetOptions(specs, optionsByField) {
@@ -0,0 +1,22 @@
1
+ import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
2
+ import type { OptionsQueryPayload } from "@cosmicdrift/kumiko-types/fields";
3
+ import type { ReactNode } from "react";
4
+ export type SelectOptionEntry = {
5
+ readonly value: string;
6
+ readonly label: string;
7
+ readonly description?: string;
8
+ readonly disabled?: boolean;
9
+ };
10
+ export declare function staticSelectOptions(field: EditFieldViewModel): readonly SelectOptionEntry[];
11
+ export declare function mergeOptionAvailability(options: readonly SelectOptionEntry[], rows: readonly unknown[], current: string): readonly SelectOptionEntry[];
12
+ type AvailabilityOptionsSelectProps = {
13
+ readonly field: EditFieldViewModel;
14
+ readonly query: string;
15
+ readonly payload: OptionsQueryPayload;
16
+ readonly row: Readonly<Record<string, unknown>>;
17
+ readonly id: string;
18
+ readonly hasError: boolean;
19
+ readonly onChange: (value: unknown) => void;
20
+ };
21
+ export declare function AvailabilityOptionsSelect({ field, query, payload, row, id, hasError, onChange, }: AvailabilityOptionsSelectProps): ReactNode;
22
+ export {};
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useQuery } from "../hooks/use-query.js";
3
+ import { usePrimitives } from "../primitives.js";
4
+ import { resolveOptionsQueryPayload } from "./query-options-select.js";
5
+ // Rows come from a query handler, so they are checked once here instead of trusted.
6
+ function isAvailabilityRow(row) {
7
+ return (typeof row === "object" &&
8
+ row !== null &&
9
+ "value" in row &&
10
+ typeof row.value === "string" &&
11
+ (!("disabled" in row) || row.disabled === undefined || typeof row.disabled === "boolean") &&
12
+ (!("hint" in row) || row.hint === undefined || typeof row.hint === "string"));
13
+ }
14
+ // Static options with the translated labels of the view-model.
15
+ export function staticSelectOptions(field) {
16
+ const labels = field.optionLabels;
17
+ return (field.options ?? []).map((value) => ({ value, label: labels?.[value] ?? value }));
18
+ }
19
+ // Static options stay authoritative: rows for unknown values are ignored, and the
20
+ // stored value is never disabled so a downgraded tenant keeps seeing its choice.
21
+ export function mergeOptionAvailability(options, rows, current) {
22
+ const byValue = new Map();
23
+ for (const row of rows) {
24
+ if (isAvailabilityRow(row))
25
+ byValue.set(row.value, row);
26
+ }
27
+ return options.map((option) => {
28
+ const row = byValue.get(option.value);
29
+ if (row === undefined)
30
+ return option;
31
+ const disabled = row.disabled === true && option.value !== current;
32
+ return {
33
+ ...option,
34
+ ...(row.hint !== undefined && { description: row.hint }),
35
+ ...(disabled && { disabled: true }),
36
+ };
37
+ });
38
+ }
39
+ // Select with static options whose availability (SelectFieldDef.optionsAvailabilityQuery)
40
+ // comes from a query, so it has to be a mounted component for useQuery. Until the
41
+ // result arrives, and when the query fails, every option stays choosable; the write
42
+ // path enforces the rule either way.
43
+ export function AvailabilityOptionsSelect({ field, query, payload, row, id, hasError, onChange, }) {
44
+ const { Input } = usePrimitives();
45
+ const { data } = useQuery(query, resolveOptionsQueryPayload(payload, row));
46
+ const current = typeof field.value === "string" ? field.value : "";
47
+ const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
48
+ return (_jsx(Input, { kind: "select", id: id, name: field.field, disabled: field.readOnly, required: field.required, hasError: hasError, value: current, onChange: (v) => onChange(v), options: mergeOptionAvailability(staticSelectOptions(field), data?.rows ?? [], current), ...(display !== undefined && { display }) }));
49
+ }
@@ -13,6 +13,7 @@ import { useQuery } from "../hooks/use-query.js";
13
13
  import { referenceOptionSource } from "../hooks/use-reference-lookup.js";
14
14
  import { useLocale, useTranslation } from "../i18n.js";
15
15
  import { usePrimitives } from "../primitives.js";
16
+ import { AvailabilityOptionsSelect, staticSelectOptions } from "./availability-options-select.js";
16
17
  import { EmbeddedListField } from "./embedded-list-field.js";
17
18
  import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
18
19
  import { QueryOptionsSelect } from "./query-options-select.js";
@@ -42,7 +43,9 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
42
43
  // Reference-Field rendert eine eigene Component — sie nutzt
43
44
  // useQuery() für den Live-Lookup, also muss sie als React-
44
45
  // Komponente gemountet werden (nicht als pure render-Call).
45
- const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : field.type === "select" && field.selectOptionsQuery !== undefined && !readOnlyText ? (_jsx(QueryOptionsSelect, { field: field, query: field.selectOptionsQuery.query, payload: field.selectOptionsQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale, t) })) : field.type === "text" && field.writeOnly === true && !readOnlyText ? (_jsx(WriteOnlyTextInput, { field: field, id: id, hasError: hasError, onChange: onChange, changed: changed === true })) : (renderInput({
46
+ const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : field.type === "select" && field.selectOptionsQuery !== undefined && !readOnlyText ? (_jsx(QueryOptionsSelect, { field: field, query: field.selectOptionsQuery.query, payload: field.selectOptionsQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : field.type === "select" &&
47
+ field.selectOptionsAvailabilityQuery !== undefined &&
48
+ !readOnlyText ? (_jsx(AvailabilityOptionsSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, payload: field.selectOptionsAvailabilityQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale, t) })) : field.type === "text" && field.writeOnly === true && !readOnlyText ? (_jsx(WriteOnlyTextInput, { field: field, id: id, hasError: hasError, onChange: onChange, changed: changed === true })) : (renderInput({
46
49
  field,
47
50
  id,
48
51
  hasError,
@@ -374,11 +377,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
374
377
  // `<feature>:entity:<entity>:field:<field>:option:<value>`).
375
378
  // Wenn keine Translations registriert sind, fallback auf raw
376
379
  // value als Label — der ComboboxInput zeigt dann unverändert.
377
- const rawOptions = field.options ?? [];
378
- const labels = field.optionLabels;
379
- const selectOptions = labels !== undefined
380
- ? rawOptions.map((value) => ({ value, label: labels[value] ?? value }))
381
- : rawOptions;
380
+ const selectOptions = field.optionLabels !== undefined ? staticSelectOptions(field) : (field.options ?? []);
382
381
  // `display` is shared with multiSelect's "checkboxes", which is not a
383
382
  // select presentation — narrow to the two a select field can request.
384
383
  const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
@@ -347,6 +347,9 @@ export type InputProps = {
347
347
  * without a group stay ungrouped and come before all groups. Options
348
348
  * with description or group never render as a segmented control. */
349
349
  readonly group?: string;
350
+ /** Not choosable and visibly muted. A disabled option that is the
351
+ * current `value` stays displayed as the selection. */
352
+ readonly disabled?: boolean;
350
353
  }[];
351
354
  readonly disabled?: boolean;
352
355
  readonly required?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.350.0",
3
+ "version": "0.352.0",
4
4
  "description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -15,9 +15,9 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@cosmicdrift/kumiko-framework": "0.350.0",
19
- "@cosmicdrift/kumiko-headless": "0.350.0",
20
- "@cosmicdrift/kumiko-types": "0.350.0",
18
+ "@cosmicdrift/kumiko-framework": "0.352.0",
19
+ "@cosmicdrift/kumiko-headless": "0.352.0",
20
+ "@cosmicdrift/kumiko-types": "0.352.0",
21
21
  "react": "^19.2.6",
22
22
  "temporal-polyfill": "^0.3.2",
23
23
  "zod": "^4.4.3"
@@ -28,7 +28,7 @@
28
28
  "@types/react-dom": "^19.2.3",
29
29
  "jsdom": "^29.1.1",
30
30
  "react-dom": "^19.2.6",
31
- "@cosmicdrift/kumiko-locale-de": "0.350.0"
31
+ "@cosmicdrift/kumiko-locale-de": "0.352.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
package/src/changes.json CHANGED
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "version": "0.352.0",
4
+ "type": "improvement",
5
+ "title": "Select inputs accept disabled options and load their availability from optionsAvailabilityQuery"
6
+ },
2
7
  {
3
8
  "version": "0.348.0",
4
9
  "type": "improvement",