From 70b90d10b2b2053b268a151ff9edf3b8a4af2e88 Mon Sep 17 00:00:00 2001 From: Piumal Rathnayake Date: Sun, 2 Aug 2026 23:49:44 +0530 Subject: [PATCH 01/28] Improve API creation UI --- .../settings/partials/cfg-apis-panel.hbs | 13 ++- .../api-portal/src/scripts/settings-apis.js | 100 +++++++++++++++++- .../api-portal/src/styles/settings-layout.css | 24 ++++- 3 files changed, 127 insertions(+), 10 deletions(-) diff --git a/portals/api-portal/src/pages/settings/partials/cfg-apis-panel.hbs b/portals/api-portal/src/pages/settings/partials/cfg-apis-panel.hbs index 5d0b40e9aa..45202b7266 100644 --- a/portals/api-portal/src/pages/settings/partials/cfg-apis-panel.hbs +++ b/portals/api-portal/src/pages/settings/partials/cfg-apis-panel.hbs @@ -236,9 +236,16 @@ {{/if}}
- - -

Free-text keywords for search and discovery.

+ + {{!-- Chip input, same markup/classes as the subscription-plan picker + above. Tags are free text, so there is no dropdown — a chip is + committed from what was typed. --}} +
+
+ +
+

Free-text keywords for search and discovery. Press Enter or comma to add; Backspace removes the last tag.

diff --git a/portals/api-portal/src/scripts/settings-apis.js b/portals/api-portal/src/scripts/settings-apis.js index 60e91063e3..fb450ba3a2 100644 --- a/portals/api-portal/src/scripts/settings-apis.js +++ b/portals/api-portal/src/scripts/settings-apis.js @@ -225,6 +225,95 @@ }); }()); + /* ── tag chip input ── + Same chip markup and classes as the subscription-plan picker above, but tags are + free text: there is nothing to search, so a chip is committed from whatever was + typed rather than picked from a dropdown. */ + var tagChips = []; + + function renderTagChips() { + var container = document.getElementById('wz-tags-chips'); + if (!container) return; + container.innerHTML = ''; + tagChips.forEach(function(tag) { + var chip = document.createElement('span'); + chip.className = 'cfg-chip'; + chip.innerHTML = esc(tag) + + ''; + chip.querySelector('.cfg-chip-remove').addEventListener('click', function(e) { + e.stopPropagation(); + removeTag(e.currentTarget.dataset.tag); + var input = document.getElementById('wz-tags-input'); + if (input) input.focus(); + }); + container.appendChild(chip); + }); + } + + /* Case-insensitive dedupe, keeping the casing first entered: "Travel" and "travel" + as separate chips is a typo every time, and unlike the old free-text field the + duplicate is now plainly visible. */ + function addTag(raw) { + var tag = String(raw || '').trim(); + if (!tag) return; + var lower = tag.toLowerCase(); + if (tagChips.some(function(t){ return t.toLowerCase() === lower; })) return; + tagChips.push(tag); + renderTagChips(); + } + + function removeTag(tag) { + tagChips = tagChips.filter(function(t){ return t !== tag; }); + renderTagChips(); + } + + /* Splits on commas so a pasted "a, b, c" — and anything typed in the old + comma-separated habit — still lands as separate chips. */ + function commitTagInput() { + var input = document.getElementById('wz-tags-input'); + if (!input) return; + input.value.split(',').forEach(addTag); + input.value = ''; + } + + function getTags() { return tagChips.slice(); } + + function setTags(list) { + tagChips = []; + var items = Array.isArray(list) ? list : (list ? String(list).split(',') : []); + items.forEach(addTag); + renderTagChips(); + var input = document.getElementById('wz-tags-input'); + if (input) input.value = ''; + } + + (function() { + var wrap = document.getElementById('wz-tags-wrap'); + var input = document.getElementById('wz-tags-input'); + if (!input) return; + if (wrap) wrap.addEventListener('click', function() { input.focus(); }); + input.addEventListener('keydown', function(e) { + if (e.key === 'Enter' || e.key === ',') { + // Enter would otherwise submit the wizard with the tag still uncommitted. + e.preventDefault(); + commitTagInput(); + } else if (e.key === 'Backspace' && !input.value && tagChips.length) { + e.preventDefault(); + removeTag(tagChips[tagChips.length - 1]); + } + }); + // Losing focus commits too — otherwise a typed-but-not-entered tag is silently + // dropped when the user clicks Save. + input.addEventListener('blur', commitTagInput); + input.addEventListener('paste', function(e) { + var text = (e.clipboardData || window.clipboardData) && (e.clipboardData || window.clipboardData).getData('text'); + if (text && text.indexOf(',') >= 0) { + e.preventDefault(); + text.split(',').forEach(addTag); + } + }); + }()); + /* ── wizard show/hide ── */ function showWizard(api, kindHint) { /* MCP servers and APIs share this wizard. Derive the "kind" from the record being @@ -271,7 +360,7 @@ sel('wz-type', api.apiType); sel('wz-status', api.apiStatus === 'DEPRECATED' ? 'DEPRECATED' : 'PUBLISHED'); sv('wz-desc', api.apiDescription); - sv('wz-tags', (api.tags && api.tags.length) ? (Array.isArray(api.tags) ? api.tags.join(', ') : api.tags) : ''); + setTags(api.tags); sv('wz-prod', api.productionUrl); sv('wz-sandbox', api.sandboxUrl); sv('wz-tech-owner', (api.owners && api.owners.technicalOwner) || api.technicalOwner || ''); @@ -289,7 +378,8 @@ /* add mode */ editingId = null; document.getElementById('cfg-wizard-title').textContent = isMcp ? 'Add MCP Server' : 'Add API'; - ['wz-name','wz-version','wz-handle','wz-desc','wz-tags','wz-prod','wz-sandbox','wz-tech-owner','wz-tech-email','wz-biz-owner','wz-biz-email'].forEach(function(id){ sv(id,''); }); + ['wz-name','wz-version','wz-handle','wz-desc','wz-prod','wz-sandbox','wz-tech-owner','wz-tech-email','wz-biz-owner','wz-biz-email'].forEach(function(id){ sv(id,''); }); + setTags([]); document.getElementById('wz-handle').readOnly = false; sel('wz-type', isMcp ? 'Mcp' : 'RestApi'); sel('wz-status','PUBLISHED'); agentVis = 'Visible'; @@ -369,6 +459,10 @@ /* ── save API (create / update) ── */ async function saveApi() { + /* Belt and braces: the tag input commits on blur, which normally fires before the + Save click lands. Committing here too means a tag typed but never Entered is + saved rather than quietly discarded, whatever order those events arrive in. */ + commitTagInput(); var name = v('wz-name'); var version = v('wz-version'); var handle = v('wz-handle'); @@ -384,7 +478,7 @@ type: document.getElementById('wz-type').value, status: document.getElementById('wz-status').value, description: v('wz-desc'), - tags: v('wz-tags') ? v('wz-tags').split(',').map(function(t){return t.trim();}).filter(Boolean) : [], + tags: getTags(), labels: getSelectedLabels(), agentVisibility: agentVis, owners: { diff --git a/portals/api-portal/src/styles/settings-layout.css b/portals/api-portal/src/styles/settings-layout.css index 40030b2fc7..a98928d073 100644 --- a/portals/api-portal/src/styles/settings-layout.css +++ b/portals/api-portal/src/styles/settings-layout.css @@ -1442,18 +1442,34 @@ white-space: nowrap; } +/* Sized as a real hit target rather than a decorative glyph: at 0.75rem with only + 1px of padding this was ~14x13px, which is both hard to see against the chip's + tinted background and fiddly to click. Shared with the subscription-plan chips, + so both pickers get the same affordance. */ .cfg-chip-remove { background: none; border: none; cursor: pointer; - color: var(--text-muted); - padding: 0 1px; + color: var(--primary); + opacity: 0.65; + padding: 0; + margin-left: 1px; + width: 18px; + height: 18px; + border-radius: 4px; line-height: 1; - font-size: 0.75rem; + font-size: 0.875rem; display: flex; align-items: center; + justify-content: center; + transition: background-color .12s, color .12s, opacity .12s; +} +.cfg-chip-remove:hover, +.cfg-chip-remove:focus-visible { + color: var(--danger); + opacity: 1; + background: color-mix(in srgb, var(--danger) 14%, transparent); } -.cfg-chip-remove:hover { color: var(--danger); } .cfg-chip-text-input { flex: 1; From 41c1250dd120904acd76576f6000dd7260556e38 Mon Sep 17 00:00:00 2001 From: Piumal Rathnayake Date: Mon, 3 Aug 2026 00:42:12 +0530 Subject: [PATCH 02/28] Improve tags field --- .../api-portal/src/scripts/settings-apis.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/portals/api-portal/src/scripts/settings-apis.js b/portals/api-portal/src/scripts/settings-apis.js index fb450ba3a2..a0280abaa6 100644 --- a/portals/api-portal/src/scripts/settings-apis.js +++ b/portals/api-portal/src/scripts/settings-apis.js @@ -306,11 +306,19 @@ // dropped when the user clicks Save. input.addEventListener('blur', commitTagInput); input.addEventListener('paste', function(e) { - var text = (e.clipboardData || window.clipboardData) && (e.clipboardData || window.clipboardData).getData('text'); - if (text && text.indexOf(',') >= 0) { - e.preventDefault(); - text.split(',').forEach(addTag); - } + var clip = e.clipboardData || window.clipboardData; + var text = clip && clip.getData('text'); + // No comma: nothing to split, so let the browser paste normally. + if (!text || text.indexOf(',') < 0) return; + e.preventDefault(); + /* Splice the pasted text in at the caret (replacing any selection) rather than + committing it on its own, so a half-typed tag joins the paste instead of + being left orphaned in the input: "foo" with "bar, baz" pasted at the end + commits as "foobar" and "baz" — what typing those same characters would do. */ + var start = input.selectionStart != null ? input.selectionStart : input.value.length; + var end = input.selectionEnd != null ? input.selectionEnd : input.value.length; + input.value = input.value.slice(0, start) + text + input.value.slice(end); + commitTagInput(); }); }()); From 3ba0be2fe5e3157251f3642898b6a21f3dea50e5 Mon Sep 17 00:00:00 2001 From: Piumal Rathnayake Date: Mon, 3 Aug 2026 07:29:41 +0530 Subject: [PATCH 03/28] Improve mobile responsiveness --- .../src/defaultContent/partials/header.hbs | 7 ++ .../src/defaultContent/partials/sidebar.hbs | 3 + .../src/defaultContent/styles/header.css | 28 +++++ .../src/defaultContent/styles/side-bar.css | 111 +++++++++++++++++- portals/api-portal/src/scripts/common.js | 72 ++++++++++++ .../api-portal/src/styles/settings-layout.css | 46 +++++++- 6 files changed, 260 insertions(+), 7 deletions(-) diff --git a/portals/api-portal/src/defaultContent/partials/header.hbs b/portals/api-portal/src/defaultContent/partials/header.hbs index 1d202cb441..3f42c71e49 100644 --- a/portals/api-portal/src/defaultContent/partials/header.hbs +++ b/portals/api-portal/src/defaultContent/partials/header.hbs @@ -1,6 +1,13 @@