Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions WebUI/src/main/ts/api/publishing/designApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export interface LocationSchemeSummary {
* append. The server does not replace the stored set.
*/
addParameter?: boolean;
/**
* Update-only. When true, {@code parameters} is exactly one parameter to
* remove by name. The server does not replace the stored set and does not
* delete the scheme.
*/
removeParameter?: boolean;
}

export interface SiteDesignSummary {
Expand Down Expand Up @@ -654,6 +660,9 @@ export function wrapLocationScheme(body: LocationSchemeSummary): {
if (body.addParameter) {
wire.addParameter = true;
}
if (body.removeParameter) {
wire.removeParameter = true;
}
if (body.parameters && body.parameters.length > 0) {
// JAXB wrapper the publishing-design resource binds. A bare schemeParameter
// array on locationScheme is ignored, so create and add-one would drop it.
Expand Down
177 changes: 176 additions & 1 deletion WebUI/src/main/ts/publishing/design/ContextsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ import {
schemesAfterSuccessfulAdd,
validateLocationSchemeAddParameter,
} from "../locationSchemeAddParameter";
import {
buildLocationSchemeRemoveParameterBody,
schemesAfterSuccessfulRemove,
validateLocationSchemeRemoveParameter,
} from "../locationSchemeRemoveParameter";
import { useDirtyForm } from "../dirtyFormContext";
import { normalizeSchemeType } from "./designLegacyTypes";
import { SiteRootBrowser } from "./SiteRootBrowser";
Expand All @@ -124,6 +129,12 @@ type Mode =
kind: "scheme-add-parameter";
source: LocationSchemeSummary;
contextId: string;
}
| {
kind: "scheme-remove-parameter";
source: LocationSchemeSummary;
contextId: string;
parameter: SchemeParameter;
};

/**
Expand Down Expand Up @@ -848,6 +859,82 @@ export function ContextsPanel(): React.ReactElement {
}
}

async function openSchemeRemoveParameter(
source: LocationSchemeSummary,
parameter: SchemeParameter,
): Promise<void> {
if (!source.schemeId || !selected || saving || !(parameter.name ?? "").trim()) {
return;
}
setError(null);
setDirty(false);
const contextId = selected;
let full = source;
try {
full = await getScheme(source.schemeId);
} catch {
full = source;
}
const name = (parameter.name ?? "").trim();
const loaded = (full.parameters ?? source.parameters ?? []).find(
(row) => (row.name ?? "").trim() === name,
);
setMode({
kind: "scheme-remove-parameter",
source: full,
contextId,
parameter: loaded ?? parameter,
});
}

function closeSchemeRemoveParameter(): void {
if (saving) {
return;
}
if (!confirmIfDirty()) {
return;
}
setDirty(false);
setError(null);
setMode({ kind: "list" });
}

async function saveSchemeRemoveParameter(): Promise<void> {
if (mode.kind !== "scheme-remove-parameter" || !mode.source.schemeId || saving) {
return;
}
const validated = validateLocationSchemeRemoveParameter(
mode.parameter.name ?? "",
mode.source.parameters,
);
if (!validated.ok) {
setError(validated.error);
return;
}
const id = String(mode.source.schemeId);
const contextId = mode.contextId;
const removed = validated.parameter;
setError(null);
setSaving(true);
const previous = schemes;
try {
await updateScheme(id, buildLocationSchemeRemoveParameterBody(removed));
setDirty(false);
setMode({ kind: "list" });
let refreshed: LocationSchemeSummary[] | null = null;
try {
refreshed = await listSchemesForContext(contextId);
} catch {
refreshed = null;
}
setSchemes(schemesAfterSuccessfulRemove(refreshed, id, removed.name, previous));
} catch (e) {
setError(mapLocationSchemeSaveError(e));
} finally {
setSaving(false);
}
}

async function copyScheme(): Promise<void> {
if (mode.kind !== "scheme-copy" || !mode.source.schemeId) {
return;
Expand Down Expand Up @@ -1844,6 +1931,85 @@ export function ContextsPanel(): React.ReactElement {
);
}

if (mode.kind === "scheme-remove-parameter") {
const source = mode.source;
const targetName = (mode.parameter.name ?? "").trim();
const others = (source.parameters ?? []).filter(
(p) => (p.name ?? "").trim() !== targetName,
);
return (
<div data-testid="scheme-remove-parameter">
<h3>Remove location scheme parameter</h3>
<p>
Name: <span data-testid="scheme-remove-parameter-name">{source.name ?? ""}</span>
</p>
<p>
Generator:{" "}
<span data-testid="scheme-remove-parameter-generator">
{source.generator ?? ""}
</span>
</p>
<p>
Description:{" "}
<span data-testid="scheme-remove-parameter-description">
{source.description ?? ""}
</span>
</p>
<p>
Content type:{" "}
<span data-testid="scheme-remove-parameter-content-type">
{source.contentTypeId != null ? String(source.contentTypeId) : ""}
</span>
</p>
<p>
Template:{" "}
<span data-testid="scheme-remove-parameter-template">
{source.templateId != null ? String(source.templateId) : ""}
</span>
</p>
<p data-testid="scheme-remove-parameter-fields-note">
Name, generator, description, content type, and template stay on this scheme.
Other parameters stay. Removing the last parameter does not delete the scheme.
</p>
<p data-testid="scheme-remove-parameter-target">
{targetName} ({mode.parameter.type ?? ""}): {mode.parameter.value ?? ""}
</p>
<ul data-testid="scheme-remove-parameter-others" style={listStyle}>
{others.map((p, i) => (
<li key={`${p.name}-${i}`} data-testid="scheme-remove-parameter-other">
{p.name} ({p.type ?? ""}): {p.value}
</li>
))}
</ul>
{error && (
<p style={errorStyle} role="alert">
{error}
</p>
)}
<div style={toolbarStyle}>
<button
type="button"
style={primaryButtonStyle}
data-testid="location-scheme-remove-parameter-confirm"
disabled={saving}
onClick={() => void saveSchemeRemoveParameter()}
>
Remove parameter
</button>
<button
type="button"
style={buttonStyle}
data-testid="location-scheme-remove-parameter-cancel"
disabled={saving}
onClick={closeSchemeRemoveParameter}
>
Cancel
</button>
</div>
</div>
);
}

if (mode.kind === "scheme-copy") {
return (
<div data-testid="scheme-copy">
Expand Down Expand Up @@ -2039,7 +2205,16 @@ export function ContextsPanel(): React.ReactElement {
<div data-testid={`scheme-list-parameters-${s.schemeId}`}>
{(s.parameters ?? []).map((p, i) => (
<span key={`${p.name}-${i}`} data-testid="scheme-list-parameter">
{p.name} ({p.type ?? ""}): {p.value}
{p.name} ({p.type ?? ""}): {p.value}{" "}
<button
type="button"
style={buttonStyle}
data-testid="location-scheme-remove-parameter"
disabled={saving}
onClick={() => void openSchemeRemoveParameter(s, p)}
>
Remove
</button>
</span>
))}
</div>
Expand Down
116 changes: 116 additions & 0 deletions WebUI/src/main/ts/publishing/locationSchemeRemoveParameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright (c) 2026 Intersoft Data Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type {
LocationSchemeSummary,
SchemeParameter,
} from "../api/publishing/designApi";

export const LOCATION_SCHEME_PARAMETER_NAME_REQUIRED = "Parameter name is required";
export const LOCATION_SCHEME_PARAMETER_NOT_ON_SCHEME =
"Parameter is not on this scheme";

export type LocationSchemeRemoveParameterDraft = {
name: string;
type?: string;
value?: string;
};

export type LocationSchemeRemoveParameterResult =
| { ok: true; parameter: LocationSchemeRemoveParameterDraft }
| { ok: false; error: string };

/**
* Reject a blank name or a name that is not already stored. Nothing is written
* in those cases. The kept type and value are copied from the stored row so the
* request identifies that parameter and does not invent a replacement.
*/
export function validateLocationSchemeRemoveParameter(
nameRaw: string,
existing: SchemeParameter[] | undefined,
): LocationSchemeRemoveParameterResult {
const name = nameRaw.trim();
if (name.length === 0) {
return { ok: false, error: LOCATION_SCHEME_PARAMETER_NAME_REQUIRED };
}
const match = (existing ?? []).find((row) => (row.name ?? "").trim() === name);
if (!match) {
return { ok: false, error: LOCATION_SCHEME_PARAMETER_NOT_ON_SCHEME };
}
const type = (match.type ?? "").trim();
return {
ok: true,
parameter: {
name,
...(type.length > 0 ? { type } : {}),
value: match.value ?? "",
},
};
}

/**
* Remove-one {@code updateScheme} body. Name, generator, description, content
* type, template, and context are omitted so the server leaves them stored.
* {@code removeParameter} drops this one name and does not replace the stored
* set. The scheme id is the path parameter, not a field on this body.
*/
export function buildLocationSchemeRemoveParameterBody(
parameter: LocationSchemeRemoveParameterDraft,
): LocationSchemeSummary {
return {
removeParameter: true,
parameters: [
{
name: parameter.name,
...(parameter.type ? { type: parameter.type } : {}),
value: parameter.value ?? "",
},
],
};
}

function parametersAfterRemove(
row: LocationSchemeSummary,
removedName: string,
known: SchemeParameter[] | undefined,
): SchemeParameter[] {
const base = row.parameters ?? known ?? [];
return base.filter((rowParameter) => (rowParameter.name ?? "").trim() !== removedName);
}

/**
* List to show after a remove succeeds. Prefer the refreshed rows, then drop
* the removed name when a refresh still lists it or omits parameters. Do not
* call this when the update failed. An empty parameter list does not drop the
* scheme row.
*/
export function schemesAfterSuccessfulRemove(
refreshed: LocationSchemeSummary[] | null,
schemeId: string,
removedName: string,
previous: LocationSchemeSummary[],
): LocationSchemeSummary[] {
const rows = refreshed ?? previous;
const id = String(schemeId);
const known = previous.find((row) => String(row.schemeId ?? "") === id)?.parameters;
return rows.map((row) => {
if (String(row.schemeId ?? "") !== id) {
return row;
}
return { ...row, parameters: parametersAfterRemove(row, removedName, known) };
});
}
Loading
Loading