Release govoplan-views v0.1.22: unify interface contracts and documentation
This commit is contained in:
+4
-3
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "@govoplan/views-webui",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.22",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
|
||||
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs",
|
||||
"test:navigation-layout": "node scripts/test-navigation-layout.mjs"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -17,7 +18,7 @@
|
||||
"./styles/views.css": "./src/styles/views.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@govoplan/core-webui": "^0.1.45",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import vm from "node:vm";
|
||||
|
||||
const require = createRequire(new URL("../../../govoplan-core/webui/package.json", import.meta.url));
|
||||
const ts = require("typescript");
|
||||
const read = (path) => readFileSync(new URL(path, import.meta.url), "utf8");
|
||||
const plain = (value) => JSON.parse(JSON.stringify(value));
|
||||
|
||||
function evaluate(source, fileName) {
|
||||
const exports = {};
|
||||
const compiled = ts.transpileModule(source, {
|
||||
fileName,
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX }
|
||||
}).outputText;
|
||||
vm.runInNewContext(compiled, { exports, require: () => ({}) }, { filename: fileName });
|
||||
return exports;
|
||||
}
|
||||
|
||||
const api = evaluate(read("../src/api/views.ts"), "views.ts");
|
||||
const admin = read("../src/features/views/ViewsAdminPanel.tsx");
|
||||
// Exercise the real private revision/dirty-state functions without mounting or making API calls.
|
||||
const helpers = evaluate(`${admin}\nexport { revisionPresentation, presentationKey };`, "ViewsAdminPanel.tsx");
|
||||
const layout = {
|
||||
contract_version: "1",
|
||||
order: ["separator:work", "files.nav.files", "tasks.nav.tasks"],
|
||||
hidden: ["mail.nav.mail"],
|
||||
locked: [],
|
||||
separators: [{ id: "separator:work", label: "Meine Arbeit" }]
|
||||
};
|
||||
for (const navigation of [null, layout, { ...layout, separators: [] }, { contract_version: "1", order: [], hidden: [] }]) {
|
||||
const presentation = { navigation, navigationMode: "grouped" };
|
||||
const wire = api.presentationToApi(presentation);
|
||||
assert.deepEqual(plain(api.presentationFromApi(wire).navigation), navigation, "Effective API projection preserves null, inherited groups, explicit flat, labels and ordered entries");
|
||||
const revision = helpers.revisionPresentation(wire, []);
|
||||
assert.deepEqual(plain(revision.navigation), navigation, "Opening a saved revision preserves its navigation preferences");
|
||||
assert.equal(helpers.presentationKey(revision), helpers.presentationKey(presentation), "Round-trip does not invent dirty state");
|
||||
}
|
||||
assert.equal(helpers.revisionPresentation(undefined, []).navigation, null, "Legacy revisions inherit navigation");
|
||||
const saved = helpers.presentationKey({ navigation: layout });
|
||||
for (const changed of [
|
||||
{ ...layout, order: [...layout.order].reverse() },
|
||||
{ ...layout, hidden: [] },
|
||||
{ ...layout, separators: [] },
|
||||
{ ...layout, separators: [{ ...layout.separators[0], label: "Changed" }] },
|
||||
null
|
||||
]) {
|
||||
assert.notEqual(helpers.presentationKey({ navigation: changed }), saved, "Every navigation edit or inheritance reset participates in Save/dirty guards");
|
||||
}
|
||||
assert.match(admin, /<NavigationPreferenceEditor\s+scope="view"/);
|
||||
assert.ok(admin.includes("configurableNavigationItemsForModules(modules)"));
|
||||
assert.ok(admin.includes("productAreas={navigationProductAreas}"));
|
||||
assert.ok(admin.includes("disabled={disabled || customNavigation}"), "Conflicting legacy layout controls disable while custom navigation is configured");
|
||||
assert.ok(admin.includes("disabled={disabled || required}"), "Surface visibility remains separate from navigation ordering");
|
||||
for (const key of ["navigation_editor_help", "product_areas_custom_navigation_help"]) {
|
||||
assert.equal(read("../src/i18n/generatedTranslations.ts").split(`"i18n:govoplan-views.${key}":`).length - 1, 2, "Shared layout guidance has English and German text");
|
||||
}
|
||||
console.log("Views shared navigation revision, inheritance, API and dirty-state contract passed.");
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
apiPath,
|
||||
type ApiSettings,
|
||||
type EffectiveViewProjection,
|
||||
type NavigationPreferences,
|
||||
type ViewPresentation
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
@@ -17,6 +18,7 @@ export type ViewRevision = {
|
||||
surface_contract_version: string;
|
||||
visible_surface_ids: string[];
|
||||
presentation: {
|
||||
navigation?: NavigationPreferences | null;
|
||||
navigation_mode?: "grouped" | "flat";
|
||||
product_area_order?: string[];
|
||||
product_area_labels?: Record<string, string>;
|
||||
@@ -260,10 +262,11 @@ export function createViewRevision(
|
||||
);
|
||||
}
|
||||
|
||||
function presentationFromApi(
|
||||
export function presentationFromApi(
|
||||
value: ViewRevision["presentation"] | undefined
|
||||
): ViewPresentation {
|
||||
return {
|
||||
navigation: value?.navigation ?? null,
|
||||
navigationMode: value?.navigation_mode,
|
||||
productAreaOrder: value?.product_area_order ?? [],
|
||||
productAreaLabels: value?.product_area_labels ?? {},
|
||||
@@ -276,6 +279,7 @@ export function presentationToApi(
|
||||
value: ViewPresentation
|
||||
): ViewRevision["presentation"] {
|
||||
return {
|
||||
navigation: value.navigation ?? null,
|
||||
navigation_mode: value.navigationMode ?? "grouped",
|
||||
product_area_order: value.productAreaOrder ?? [],
|
||||
product_area_labels: value.productAreaLabels ?? {},
|
||||
|
||||
@@ -26,6 +26,7 @@ import { FormGrid,
|
||||
ExplorerTree,
|
||||
FormField,
|
||||
IconButton,
|
||||
NavigationPreferenceEditor,
|
||||
SearchableSelect,
|
||||
SegmentedControl,
|
||||
SelectionList,
|
||||
@@ -33,6 +34,7 @@ import { FormGrid,
|
||||
StatusBadge,
|
||||
ToggleSwitch,
|
||||
adminErrorMessage,
|
||||
configurableNavigationItemsForModules,
|
||||
dispatchPlatformViewChanged,
|
||||
i18nMessage,
|
||||
usePlatformLanguage,
|
||||
@@ -134,6 +136,8 @@ export default function ViewsAdminPanel({
|
||||
}) {
|
||||
const surfaces = useViewSurfaces();
|
||||
const modules = usePlatformModules();
|
||||
const navigationItems = useMemo(() => configurableNavigationItemsForModules(modules), [modules]);
|
||||
const navigationProductAreas = useMemo(() => modules.flatMap((module) => module.productAreas ?? []), [modules]);
|
||||
const productAreas = useMemo(() => aggregateProductAreas(modules), [modules]);
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
const { translateText } = usePlatformLanguage();
|
||||
@@ -763,6 +767,26 @@ export default function ViewsAdminPanel({
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<section className="views-product-area-section">
|
||||
<div className="views-section-heading">
|
||||
<div>
|
||||
<h4>i18n:govoplan-views.navigation_layout</h4>
|
||||
<p className="muted small-note">i18n:govoplan-views.navigation_editor_help</p>
|
||||
</div>
|
||||
</div>
|
||||
<NavigationPreferenceEditor
|
||||
scope="view"
|
||||
items={navigationItems}
|
||||
productAreas={navigationProductAreas}
|
||||
value={draft.presentation.navigation ?? null}
|
||||
disabled={!definitionEditable || busy}
|
||||
onChange={(navigation) => setDraft({
|
||||
...draft,
|
||||
presentation: { ...draft.presentation, navigation }
|
||||
})}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{productAreas.length > 0 && (
|
||||
<ProductAreaEditor
|
||||
areas={productAreas}
|
||||
@@ -1480,6 +1504,7 @@ function ProductAreaEditor({
|
||||
}) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const ordered = orderedProductAreas(areas, draft.presentation.productAreaOrder);
|
||||
const customNavigation = draft.presentation.navigation != null;
|
||||
const requiredSurfaceIds = new Set(
|
||||
surfaces.filter((surface) => surface.required).map((surface) => surface.id)
|
||||
);
|
||||
@@ -1518,14 +1543,14 @@ function ProductAreaEditor({
|
||||
<div>
|
||||
<h4>i18n:govoplan-views.product_areas</h4>
|
||||
<p className="muted small-note">
|
||||
i18n:govoplan-views.product_areas_help
|
||||
{customNavigation ? "i18n:govoplan-views.product_areas_custom_navigation_help" : "i18n:govoplan-views.product_areas_help"}
|
||||
</p>
|
||||
</div>
|
||||
<SegmentedControl<"grouped" | "flat">
|
||||
ariaLabel={translateText("i18n:govoplan-views.navigation_layout")}
|
||||
role="group"
|
||||
value={draft.presentation.navigationMode ?? "grouped"}
|
||||
disabled={disabled}
|
||||
disabled={disabled || customNavigation}
|
||||
onChange={(navigationMode) =>
|
||||
updatePresentation({ ...draft.presentation, navigationMode })
|
||||
}
|
||||
@@ -1558,7 +1583,7 @@ function ProductAreaEditor({
|
||||
{ value0: translateText(area.label) }
|
||||
)}
|
||||
maxLength={200}
|
||||
disabled={disabled}
|
||||
disabled={disabled || customNavigation}
|
||||
onChange={(event) => setLabel(area.id, event.target.value)}
|
||||
/>
|
||||
<ToggleSwitch
|
||||
@@ -1575,14 +1600,14 @@ function ProductAreaEditor({
|
||||
label="i18n:govoplan-views.move_up"
|
||||
icon={<ArrowUp size={16} />}
|
||||
variant="ghost"
|
||||
disabled={disabled || index === 0}
|
||||
disabled={disabled || customNavigation || index === 0}
|
||||
onClick={() => move(area.id, -1)}
|
||||
/>
|
||||
<IconButton
|
||||
label="i18n:govoplan-views.move_down"
|
||||
icon={<ArrowDown size={16} />}
|
||||
variant="ghost"
|
||||
disabled={disabled || index === ordered.length - 1}
|
||||
disabled={disabled || customNavigation || index === ordered.length - 1}
|
||||
onClick={() => move(area.id, 1)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1879,6 +1904,7 @@ function aggregateProductAreas(modules: PlatformWebModule[]): ViewProductArea[]
|
||||
|
||||
function defaultPresentation(areas: ViewProductArea[]): ViewPresentation {
|
||||
return {
|
||||
navigation: null,
|
||||
navigationMode: "grouped",
|
||||
productAreaOrder: areas.map((area) => area.id),
|
||||
productAreaLabels: {},
|
||||
@@ -1917,6 +1943,7 @@ function revisionPresentation(
|
||||
): ViewPresentation {
|
||||
const defaults = defaultPresentation(areas);
|
||||
return {
|
||||
navigation: value?.navigation ?? null,
|
||||
navigationMode: value?.navigation_mode ?? defaults.navigationMode,
|
||||
productAreaOrder:
|
||||
value?.product_area_order?.length
|
||||
@@ -1945,6 +1972,7 @@ function orderedProductAreas(
|
||||
|
||||
function presentationKey(value: ViewPresentation): string {
|
||||
return JSON.stringify({
|
||||
navigation: value.navigation ?? null,
|
||||
navigationMode: value.navigationMode ?? "grouped",
|
||||
productAreaOrder: value.productAreaOrder ?? [],
|
||||
productAreaLabels: Object.fromEntries(
|
||||
|
||||
@@ -133,6 +133,8 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-views.product_areas": "Product areas",
|
||||
"i18n:govoplan-views.product_areas_help": "Choose the outcome-based navigation groups, their order, and optional labels for this View.",
|
||||
"i18n:govoplan-views.navigation_layout": "Navigation layout",
|
||||
"i18n:govoplan-views.navigation_editor_help": "Arrange modules and optional named separators with the shared navigation editor. Changes belong to this immutable View revision. Inherit restores tenant defaults; an explicit personal layout takes precedence. Layout changes never grant access or remove administrator locks.",
|
||||
"i18n:govoplan-views.product_areas_custom_navigation_help": "The custom navigation layout above overrides legacy grouped/flat mode, area order, and labels. Area visibility still determines which surfaces this View allows. Inherit the navigation layout to enable the legacy controls again.",
|
||||
"i18n:govoplan-views.grouped": "Grouped",
|
||||
"i18n:govoplan-views.flat": "Flat",
|
||||
"i18n:govoplan-views.hidden": "Hidden",
|
||||
@@ -274,6 +276,8 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-views.product_areas": "Produktbereiche",
|
||||
"i18n:govoplan-views.product_areas_help": "Ergebnisorientierte Navigationsgruppen, ihre Reihenfolge und optionale Bezeichnungen für diese Ansicht festlegen.",
|
||||
"i18n:govoplan-views.navigation_layout": "Navigationsdarstellung",
|
||||
"i18n:govoplan-views.navigation_editor_help": "Module und optional benannte Trennlinien mit dem gemeinsamen Navigationseditor anordnen. Änderungen gehören zu dieser unveränderlichen Ansichtsrevision. Vererben stellt die Mandantenvorgaben wieder her; eine ausdrücklich festgelegte persönliche Anordnung hat Vorrang. Die Darstellung erteilt weder Berechtigungen noch entfernt sie Administrationssperren.",
|
||||
"i18n:govoplan-views.product_areas_custom_navigation_help": "Die eigene Navigationsanordnung oben ersetzt den bisherigen gruppierten/flachen Modus sowie Bereichsreihenfolge und -beschriftungen. Die Bereichssichtbarkeit bestimmt weiterhin, welche Oberflächen die Ansicht zulässt. Die Navigationsanordnung vererben, um die bisherigen Steuerelemente wieder zu aktivieren.",
|
||||
"i18n:govoplan-views.grouped": "Gruppiert",
|
||||
"i18n:govoplan-views.flat": "Flach",
|
||||
"i18n:govoplan-views.hidden": "Ausgeblendet",
|
||||
|
||||
Reference in New Issue
Block a user