From 78645fdf9d3f2d42d56af35a09d790fed767f5f2 Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Fri, 8 May 2026 14:53:29 +0000 Subject: [PATCH 1/2] Initial plan From 86f72d459fc03b804d913cdc2f3b17a530972152 Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Fri, 8 May 2026 15:05:13 +0000 Subject: [PATCH 2/2] feat: add work item process inspector panel Co-authored-by: CompN3rd <1405794+CompN3rd@users.noreply.github.com> --- media/webviews/workItemSchemaInspector.js | 866 +++++++++++++++++++ package.json | 6 + scripts/build-webviews.js | 3 +- src/api/adoClient.ts | 124 +++ src/commands/schemaInspectorCommands.ts | 41 + src/extension.ts | 8 + src/views/webview/workItemSchemaInspector.ts | 179 ++++ src/views/webviewTypes.ts | 45 + src/views/workItemSchemaInspectorPanel.ts | 253 ++++++ 9 files changed, 1524 insertions(+), 1 deletion(-) create mode 100644 media/webviews/workItemSchemaInspector.js create mode 100644 src/commands/schemaInspectorCommands.ts create mode 100644 src/views/webview/workItemSchemaInspector.ts create mode 100644 src/views/workItemSchemaInspectorPanel.ts diff --git a/media/webviews/workItemSchemaInspector.js b/media/webviews/workItemSchemaInspector.js new file mode 100644 index 0000000..23fb3b1 --- /dev/null +++ b/media/webviews/workItemSchemaInspector.js @@ -0,0 +1,866 @@ +"use strict"; +(() => { + // node_modules/@lit/reactive-element/css-tag.js + var t = globalThis; + var e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype; + var s = /* @__PURE__ */ Symbol(); + var o = /* @__PURE__ */ new WeakMap(); + var n = class { + constructor(t3, e4, o5) { + if (this._$cssResult$ = true, o5 !== s) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); + this.cssText = t3, this.t = e4; + } + get styleSheet() { + let t3 = this.o; + const s4 = this.t; + if (e && void 0 === t3) { + const e4 = void 0 !== s4 && 1 === s4.length; + e4 && (t3 = o.get(s4)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e4 && o.set(s4, t3)); + } + return t3; + } + toString() { + return this.cssText; + } + }; + var r = (t3) => new n("string" == typeof t3 ? t3 : t3 + "", void 0, s); + var i = (t3, ...e4) => { + const o5 = 1 === t3.length ? t3[0] : e4.reduce((e5, s4, o6) => e5 + ((t4) => { + if (true === t4._$cssResult$) return t4.cssText; + if ("number" == typeof t4) return t4; + throw Error("Value passed to 'css' function must be a 'css' function result: " + t4 + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); + })(s4) + t3[o6 + 1], t3[0]); + return new n(o5, t3, s); + }; + var S = (s4, o5) => { + if (e) s4.adoptedStyleSheets = o5.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet); + else for (const e4 of o5) { + const o6 = document.createElement("style"), n4 = t.litNonce; + void 0 !== n4 && o6.setAttribute("nonce", n4), o6.textContent = e4.cssText, s4.appendChild(o6); + } + }; + var c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => { + let e4 = ""; + for (const s4 of t4.cssRules) e4 += s4.cssText; + return r(e4); + })(t3) : t3; + + // node_modules/@lit/reactive-element/reactive-element.js + var { is: i2, defineProperty: e2, getOwnPropertyDescriptor: h, getOwnPropertyNames: r2, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object; + var a = globalThis; + var c2 = a.trustedTypes; + var l = c2 ? c2.emptyScript : ""; + var p = a.reactiveElementPolyfillSupport; + var d = (t3, s4) => t3; + var u = { toAttribute(t3, s4) { + switch (s4) { + case Boolean: + t3 = t3 ? l : null; + break; + case Object: + case Array: + t3 = null == t3 ? t3 : JSON.stringify(t3); + } + return t3; + }, fromAttribute(t3, s4) { + let i5 = t3; + switch (s4) { + case Boolean: + i5 = null !== t3; + break; + case Number: + i5 = null === t3 ? null : Number(t3); + break; + case Object: + case Array: + try { + i5 = JSON.parse(t3); + } catch (t4) { + i5 = null; + } + } + return i5; + } }; + var f = (t3, s4) => !i2(t3, s4); + var b = { attribute: true, type: String, converter: u, reflect: false, useDefault: false, hasChanged: f }; + Symbol.metadata ?? (Symbol.metadata = /* @__PURE__ */ Symbol("metadata")), a.litPropertyMetadata ?? (a.litPropertyMetadata = /* @__PURE__ */ new WeakMap()); + var y = class extends HTMLElement { + static addInitializer(t3) { + this._$Ei(), (this.l ?? (this.l = [])).push(t3); + } + static get observedAttributes() { + return this.finalize(), this._$Eh && [...this._$Eh.keys()]; + } + static createProperty(t3, s4 = b) { + if (s4.state && (s4.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t3) && ((s4 = Object.create(s4)).wrapped = true), this.elementProperties.set(t3, s4), !s4.noAccessor) { + const i5 = /* @__PURE__ */ Symbol(), h3 = this.getPropertyDescriptor(t3, i5, s4); + void 0 !== h3 && e2(this.prototype, t3, h3); + } + } + static getPropertyDescriptor(t3, s4, i5) { + const { get: e4, set: r4 } = h(this.prototype, t3) ?? { get() { + return this[s4]; + }, set(t4) { + this[s4] = t4; + } }; + return { get: e4, set(s5) { + const h3 = e4?.call(this); + r4?.call(this, s5), this.requestUpdate(t3, h3, i5); + }, configurable: true, enumerable: true }; + } + static getPropertyOptions(t3) { + return this.elementProperties.get(t3) ?? b; + } + static _$Ei() { + if (this.hasOwnProperty(d("elementProperties"))) return; + const t3 = n2(this); + t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties); + } + static finalize() { + if (this.hasOwnProperty(d("finalized"))) return; + if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d("properties"))) { + const t4 = this.properties, s4 = [...r2(t4), ...o2(t4)]; + for (const i5 of s4) this.createProperty(i5, t4[i5]); + } + const t3 = this[Symbol.metadata]; + if (null !== t3) { + const s4 = litPropertyMetadata.get(t3); + if (void 0 !== s4) for (const [t4, i5] of s4) this.elementProperties.set(t4, i5); + } + this._$Eh = /* @__PURE__ */ new Map(); + for (const [t4, s4] of this.elementProperties) { + const i5 = this._$Eu(t4, s4); + void 0 !== i5 && this._$Eh.set(i5, t4); + } + this.elementStyles = this.finalizeStyles(this.styles); + } + static finalizeStyles(s4) { + const i5 = []; + if (Array.isArray(s4)) { + const e4 = new Set(s4.flat(1 / 0).reverse()); + for (const s5 of e4) i5.unshift(c(s5)); + } else void 0 !== s4 && i5.push(c(s4)); + return i5; + } + static _$Eu(t3, s4) { + const i5 = s4.attribute; + return false === i5 ? void 0 : "string" == typeof i5 ? i5 : "string" == typeof t3 ? t3.toLowerCase() : void 0; + } + constructor() { + super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev(); + } + _$Ev() { + this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this)); + } + addController(t3) { + (this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.(); + } + removeController(t3) { + this._$EO?.delete(t3); + } + _$E_() { + const t3 = /* @__PURE__ */ new Map(), s4 = this.constructor.elementProperties; + for (const i5 of s4.keys()) this.hasOwnProperty(i5) && (t3.set(i5, this[i5]), delete this[i5]); + t3.size > 0 && (this._$Ep = t3); + } + createRenderRoot() { + const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); + return S(t3, this.constructor.elementStyles), t3; + } + connectedCallback() { + this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.()); + } + enableUpdating(t3) { + } + disconnectedCallback() { + this._$EO?.forEach((t3) => t3.hostDisconnected?.()); + } + attributeChangedCallback(t3, s4, i5) { + this._$AK(t3, i5); + } + _$ET(t3, s4) { + const i5 = this.constructor.elementProperties.get(t3), e4 = this.constructor._$Eu(t3, i5); + if (void 0 !== e4 && true === i5.reflect) { + const h3 = (void 0 !== i5.converter?.toAttribute ? i5.converter : u).toAttribute(s4, i5.type); + this._$Em = t3, null == h3 ? this.removeAttribute(e4) : this.setAttribute(e4, h3), this._$Em = null; + } + } + _$AK(t3, s4) { + const i5 = this.constructor, e4 = i5._$Eh.get(t3); + if (void 0 !== e4 && this._$Em !== e4) { + const t4 = i5.getPropertyOptions(e4), h3 = "function" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u; + this._$Em = e4; + const r4 = h3.fromAttribute(s4, t4.type); + this[e4] = r4 ?? this._$Ej?.get(e4) ?? r4, this._$Em = null; + } + } + requestUpdate(t3, s4, i5, e4 = false, h3) { + if (void 0 !== t3) { + const r4 = this.constructor; + if (false === e4 && (h3 = this[t3]), i5 ?? (i5 = r4.getPropertyOptions(t3)), !((i5.hasChanged ?? f)(h3, s4) || i5.useDefault && i5.reflect && h3 === this._$Ej?.get(t3) && !this.hasAttribute(r4._$Eu(t3, i5)))) return; + this.C(t3, s4, i5); + } + false === this.isUpdatePending && (this._$ES = this._$EP()); + } + C(t3, s4, { useDefault: i5, reflect: e4, wrapped: h3 }, r4) { + i5 && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t3) && (this._$Ej.set(t3, r4 ?? s4 ?? this[t3]), true !== h3 || void 0 !== r4) || (this._$AL.has(t3) || (this.hasUpdated || i5 || (s4 = void 0), this._$AL.set(t3, s4)), true === e4 && this._$Em !== t3 && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t3)); + } + async _$EP() { + this.isUpdatePending = true; + try { + await this._$ES; + } catch (t4) { + Promise.reject(t4); + } + const t3 = this.scheduleUpdate(); + return null != t3 && await t3, !this.isUpdatePending; + } + scheduleUpdate() { + return this.performUpdate(); + } + performUpdate() { + if (!this.isUpdatePending) return; + if (!this.hasUpdated) { + if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) { + for (const [t5, s5] of this._$Ep) this[t5] = s5; + this._$Ep = void 0; + } + const t4 = this.constructor.elementProperties; + if (t4.size > 0) for (const [s5, i5] of t4) { + const { wrapped: t5 } = i5, e4 = this[s5]; + true !== t5 || this._$AL.has(s5) || void 0 === e4 || this.C(s5, void 0, i5, e4); + } + } + let t3 = false; + const s4 = this._$AL; + try { + t3 = this.shouldUpdate(s4), t3 ? (this.willUpdate(s4), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s4)) : this._$EM(); + } catch (s5) { + throw t3 = false, this._$EM(), s5; + } + t3 && this._$AE(s4); + } + willUpdate(t3) { + } + _$AE(t3) { + this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3); + } + _$EM() { + this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false; + } + get updateComplete() { + return this.getUpdateComplete(); + } + getUpdateComplete() { + return this._$ES; + } + shouldUpdate(t3) { + return true; + } + update(t3) { + this._$Eq && (this._$Eq = this._$Eq.forEach((t4) => this._$ET(t4, this[t4]))), this._$EM(); + } + updated(t3) { + } + firstUpdated(t3) { + } + }; + y.elementStyles = [], y.shadowRootOptions = { mode: "open" }, y[d("elementProperties")] = /* @__PURE__ */ new Map(), y[d("finalized")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: y }), (a.reactiveElementVersions ?? (a.reactiveElementVersions = [])).push("2.1.2"); + + // node_modules/lit-html/lit-html.js + var t2 = globalThis; + var i3 = (t3) => t3; + var s2 = t2.trustedTypes; + var e3 = s2 ? s2.createPolicy("lit-html", { createHTML: (t3) => t3 }) : void 0; + var h2 = "$lit$"; + var o3 = `lit$${Math.random().toFixed(9).slice(2)}$`; + var n3 = "?" + o3; + var r3 = `<${n3}>`; + var l2 = document; + var c3 = () => l2.createComment(""); + var a2 = (t3) => null === t3 || "object" != typeof t3 && "function" != typeof t3; + var u2 = Array.isArray; + var d2 = (t3) => u2(t3) || "function" == typeof t3?.[Symbol.iterator]; + var f2 = "[ \n\f\r]"; + var v = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g; + var _ = /-->/g; + var m = />/g; + var p2 = RegExp(`>|${f2}(?:([^\\s"'>=/]+)(${f2}*=${f2}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`, "g"); + var g = /'/g; + var $ = /"/g; + var y2 = /^(?:script|style|textarea|title)$/i; + var x = (t3) => (i5, ...s4) => ({ _$litType$: t3, strings: i5, values: s4 }); + var b2 = x(1); + var w = x(2); + var T = x(3); + var E = /* @__PURE__ */ Symbol.for("lit-noChange"); + var A = /* @__PURE__ */ Symbol.for("lit-nothing"); + var C = /* @__PURE__ */ new WeakMap(); + var P = l2.createTreeWalker(l2, 129); + function V(t3, i5) { + if (!u2(t3) || !t3.hasOwnProperty("raw")) throw Error("invalid template strings array"); + return void 0 !== e3 ? e3.createHTML(i5) : i5; + } + var N = (t3, i5) => { + const s4 = t3.length - 1, e4 = []; + let n4, l3 = 2 === i5 ? "" : 3 === i5 ? "" : "", c4 = v; + for (let i6 = 0; i6 < s4; i6++) { + const s5 = t3[i6]; + let a3, u3, d3 = -1, f3 = 0; + for (; f3 < s5.length && (c4.lastIndex = f3, u3 = c4.exec(s5), null !== u3); ) f3 = c4.lastIndex, c4 === v ? "!--" === u3[1] ? c4 = _ : void 0 !== u3[1] ? c4 = m : void 0 !== u3[2] ? (y2.test(u3[2]) && (n4 = RegExp("" === u3[0] ? (c4 = n4 ?? v, d3 = -1) : void 0 === u3[1] ? d3 = -2 : (d3 = c4.lastIndex - u3[2].length, a3 = u3[1], c4 = void 0 === u3[3] ? p2 : '"' === u3[3] ? $ : g) : c4 === $ || c4 === g ? c4 = p2 : c4 === _ || c4 === m ? c4 = v : (c4 = p2, n4 = void 0); + const x2 = c4 === p2 && t3[i6 + 1].startsWith("/>") ? " " : ""; + l3 += c4 === v ? s5 + r3 : d3 >= 0 ? (e4.push(a3), s5.slice(0, d3) + h2 + s5.slice(d3) + o3 + x2) : s5 + o3 + (-2 === d3 ? i6 : x2); + } + return [V(t3, l3 + (t3[s4] || "") + (2 === i5 ? "" : 3 === i5 ? "" : "")), e4]; + }; + var S2 = class _S { + constructor({ strings: t3, _$litType$: i5 }, e4) { + let r4; + this.parts = []; + let l3 = 0, a3 = 0; + const u3 = t3.length - 1, d3 = this.parts, [f3, v2] = N(t3, i5); + if (this.el = _S.createElement(f3, e4), P.currentNode = this.el.content, 2 === i5 || 3 === i5) { + const t4 = this.el.content.firstChild; + t4.replaceWith(...t4.childNodes); + } + for (; null !== (r4 = P.nextNode()) && d3.length < u3; ) { + if (1 === r4.nodeType) { + if (r4.hasAttributes()) for (const t4 of r4.getAttributeNames()) if (t4.endsWith(h2)) { + const i6 = v2[a3++], s4 = r4.getAttribute(t4).split(o3), e5 = /([.?@])?(.*)/.exec(i6); + d3.push({ type: 1, index: l3, name: e5[2], strings: s4, ctor: "." === e5[1] ? I : "?" === e5[1] ? L : "@" === e5[1] ? z : H }), r4.removeAttribute(t4); + } else t4.startsWith(o3) && (d3.push({ type: 6, index: l3 }), r4.removeAttribute(t4)); + if (y2.test(r4.tagName)) { + const t4 = r4.textContent.split(o3), i6 = t4.length - 1; + if (i6 > 0) { + r4.textContent = s2 ? s2.emptyScript : ""; + for (let s4 = 0; s4 < i6; s4++) r4.append(t4[s4], c3()), P.nextNode(), d3.push({ type: 2, index: ++l3 }); + r4.append(t4[i6], c3()); + } + } + } else if (8 === r4.nodeType) if (r4.data === n3) d3.push({ type: 2, index: l3 }); + else { + let t4 = -1; + for (; -1 !== (t4 = r4.data.indexOf(o3, t4 + 1)); ) d3.push({ type: 7, index: l3 }), t4 += o3.length - 1; + } + l3++; + } + } + static createElement(t3, i5) { + const s4 = l2.createElement("template"); + return s4.innerHTML = t3, s4; + } + }; + function M(t3, i5, s4 = t3, e4) { + if (i5 === E) return i5; + let h3 = void 0 !== e4 ? s4._$Co?.[e4] : s4._$Cl; + const o5 = a2(i5) ? void 0 : i5._$litDirective$; + return h3?.constructor !== o5 && (h3?._$AO?.(false), void 0 === o5 ? h3 = void 0 : (h3 = new o5(t3), h3._$AT(t3, s4, e4)), void 0 !== e4 ? (s4._$Co ?? (s4._$Co = []))[e4] = h3 : s4._$Cl = h3), void 0 !== h3 && (i5 = M(t3, h3._$AS(t3, i5.values), h3, e4)), i5; + } + var R = class { + constructor(t3, i5) { + this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i5; + } + get parentNode() { + return this._$AM.parentNode; + } + get _$AU() { + return this._$AM._$AU; + } + u(t3) { + const { el: { content: i5 }, parts: s4 } = this._$AD, e4 = (t3?.creationScope ?? l2).importNode(i5, true); + P.currentNode = e4; + let h3 = P.nextNode(), o5 = 0, n4 = 0, r4 = s4[0]; + for (; void 0 !== r4; ) { + if (o5 === r4.index) { + let i6; + 2 === r4.type ? i6 = new k(h3, h3.nextSibling, this, t3) : 1 === r4.type ? i6 = new r4.ctor(h3, r4.name, r4.strings, this, t3) : 6 === r4.type && (i6 = new Z(h3, this, t3)), this._$AV.push(i6), r4 = s4[++n4]; + } + o5 !== r4?.index && (h3 = P.nextNode(), o5++); + } + return P.currentNode = l2, e4; + } + p(t3) { + let i5 = 0; + for (const s4 of this._$AV) void 0 !== s4 && (void 0 !== s4.strings ? (s4._$AI(t3, s4, i5), i5 += s4.strings.length - 2) : s4._$AI(t3[i5])), i5++; + } + }; + var k = class _k { + get _$AU() { + return this._$AM?._$AU ?? this._$Cv; + } + constructor(t3, i5, s4, e4) { + this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t3, this._$AB = i5, this._$AM = s4, this.options = e4, this._$Cv = e4?.isConnected ?? true; + } + get parentNode() { + let t3 = this._$AA.parentNode; + const i5 = this._$AM; + return void 0 !== i5 && 11 === t3?.nodeType && (t3 = i5.parentNode), t3; + } + get startNode() { + return this._$AA; + } + get endNode() { + return this._$AB; + } + _$AI(t3, i5 = this) { + t3 = M(this, t3, i5), a2(t3) ? t3 === A || null == t3 || "" === t3 ? (this._$AH !== A && this._$AR(), this._$AH = A) : t3 !== this._$AH && t3 !== E && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : d2(t3) ? this.k(t3) : this._(t3); + } + O(t3) { + return this._$AA.parentNode.insertBefore(t3, this._$AB); + } + T(t3) { + this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3)); + } + _(t3) { + this._$AH !== A && a2(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(l2.createTextNode(t3)), this._$AH = t3; + } + $(t3) { + const { values: i5, _$litType$: s4 } = t3, e4 = "number" == typeof s4 ? this._$AC(t3) : (void 0 === s4.el && (s4.el = S2.createElement(V(s4.h, s4.h[0]), this.options)), s4); + if (this._$AH?._$AD === e4) this._$AH.p(i5); + else { + const t4 = new R(e4, this), s5 = t4.u(this.options); + t4.p(i5), this.T(s5), this._$AH = t4; + } + } + _$AC(t3) { + let i5 = C.get(t3.strings); + return void 0 === i5 && C.set(t3.strings, i5 = new S2(t3)), i5; + } + k(t3) { + u2(this._$AH) || (this._$AH = [], this._$AR()); + const i5 = this._$AH; + let s4, e4 = 0; + for (const h3 of t3) e4 === i5.length ? i5.push(s4 = new _k(this.O(c3()), this.O(c3()), this, this.options)) : s4 = i5[e4], s4._$AI(h3), e4++; + e4 < i5.length && (this._$AR(s4 && s4._$AB.nextSibling, e4), i5.length = e4); + } + _$AR(t3 = this._$AA.nextSibling, s4) { + for (this._$AP?.(false, true, s4); t3 !== this._$AB; ) { + const s5 = i3(t3).nextSibling; + i3(t3).remove(), t3 = s5; + } + } + setConnected(t3) { + void 0 === this._$AM && (this._$Cv = t3, this._$AP?.(t3)); + } + }; + var H = class { + get tagName() { + return this.element.tagName; + } + get _$AU() { + return this._$AM._$AU; + } + constructor(t3, i5, s4, e4, h3) { + this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t3, this.name = i5, this._$AM = e4, this.options = h3, s4.length > 2 || "" !== s4[0] || "" !== s4[1] ? (this._$AH = Array(s4.length - 1).fill(new String()), this.strings = s4) : this._$AH = A; + } + _$AI(t3, i5 = this, s4, e4) { + const h3 = this.strings; + let o5 = false; + if (void 0 === h3) t3 = M(this, t3, i5, 0), o5 = !a2(t3) || t3 !== this._$AH && t3 !== E, o5 && (this._$AH = t3); + else { + const e5 = t3; + let n4, r4; + for (t3 = h3[0], n4 = 0; n4 < h3.length - 1; n4++) r4 = M(this, e5[s4 + n4], i5, n4), r4 === E && (r4 = this._$AH[n4]), o5 || (o5 = !a2(r4) || r4 !== this._$AH[n4]), r4 === A ? t3 = A : t3 !== A && (t3 += (r4 ?? "") + h3[n4 + 1]), this._$AH[n4] = r4; + } + o5 && !e4 && this.j(t3); + } + j(t3) { + t3 === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? ""); + } + }; + var I = class extends H { + constructor() { + super(...arguments), this.type = 3; + } + j(t3) { + this.element[this.name] = t3 === A ? void 0 : t3; + } + }; + var L = class extends H { + constructor() { + super(...arguments), this.type = 4; + } + j(t3) { + this.element.toggleAttribute(this.name, !!t3 && t3 !== A); + } + }; + var z = class extends H { + constructor(t3, i5, s4, e4, h3) { + super(t3, i5, s4, e4, h3), this.type = 5; + } + _$AI(t3, i5 = this) { + if ((t3 = M(this, t3, i5, 0) ?? A) === E) return; + const s4 = this._$AH, e4 = t3 === A && s4 !== A || t3.capture !== s4.capture || t3.once !== s4.once || t3.passive !== s4.passive, h3 = t3 !== A && (s4 === A || e4); + e4 && this.element.removeEventListener(this.name, this, s4), h3 && this.element.addEventListener(this.name, this, t3), this._$AH = t3; + } + handleEvent(t3) { + "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3); + } + }; + var Z = class { + constructor(t3, i5, s4) { + this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i5, this.options = s4; + } + get _$AU() { + return this._$AM._$AU; + } + _$AI(t3) { + M(this, t3); + } + }; + var B = t2.litHtmlPolyfillSupport; + B?.(S2, k), (t2.litHtmlVersions ?? (t2.litHtmlVersions = [])).push("3.3.2"); + var D = (t3, i5, s4) => { + const e4 = s4?.renderBefore ?? i5; + let h3 = e4._$litPart$; + if (void 0 === h3) { + const t4 = s4?.renderBefore ?? null; + e4._$litPart$ = h3 = new k(i5.insertBefore(c3(), t4), t4, void 0, s4 ?? {}); + } + return h3._$AI(t3), h3; + }; + + // node_modules/lit-element/lit-element.js + var s3 = globalThis; + var i4 = class extends y { + constructor() { + super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; + } + createRenderRoot() { + var _a; + const t3 = super.createRenderRoot(); + return (_a = this.renderOptions).renderBefore ?? (_a.renderBefore = t3.firstChild), t3; + } + update(t3) { + const r4 = this.render(); + this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this._$Do = D(r4, this.renderRoot, this.renderOptions); + } + connectedCallback() { + super.connectedCallback(), this._$Do?.setConnected(true); + } + disconnectedCallback() { + super.disconnectedCallback(), this._$Do?.setConnected(false); + } + render() { + return E; + } + }; + i4._$litElement$ = true, i4["finalized"] = true, s3.litElementHydrateSupport?.({ LitElement: i4 }); + var o4 = s3.litElementPolyfillSupport; + o4?.({ LitElement: i4 }); + (s3.litElementVersions ?? (s3.litElementVersions = [])).push("4.2.2"); + + // src/views/webview/commonStyles.ts + var baseStyles = i` + :host { + display: block; + min-height: 100vh; + color: var(--vscode-foreground); + background: var(--vscode-editor-background); + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + } + + .shell { + padding: 16px; + } + + h1 { + margin: 0 0 4px; + font-size: 1.3em; + font-weight: 600; + } + + h2 { + font-size: 1em; + border-bottom: 1px solid var(--vscode-panel-border); + padding-bottom: 4px; + margin: 0 0 8px; + } + + .section { + margin-bottom: 20px; + } + + .toolbar { + display: flex; + gap: 8px; + margin-bottom: 12px; + flex-wrap: wrap; + align-items: center; + } + + button, + select, + textarea, + input { + font: inherit; + } + + .btn { + padding: 4px 10px; + border-radius: 3px; + border: 1px solid var(--vscode-button-border, transparent); + cursor: pointer; + font-size: 0.85em; + } + + .btn-primary { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + } + + .btn-primary:hover { + background: var(--vscode-button-hoverBackground); + } + + .btn-secondary { + background: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + } + + .btn-secondary:hover { + background: var(--vscode-button-secondaryHoverBackground); + } + + .btn-link { + background: transparent; + border: none; + color: var(--vscode-textLink-foreground); + padding: 0; + cursor: pointer; + text-align: left; + } + + .btn-link:hover { + color: var(--vscode-textLink-activeForeground); + text-decoration: underline; + } + + .empty { + color: var(--vscode-descriptionForeground); + font-style: italic; + } + + .meta { + color: var(--vscode-descriptionForeground); + font-size: 0.9em; + } + + .badge { + display: inline-block; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.8em; + } + + .reply-input { + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 3px; + padding: 6px 8px; + resize: vertical; + box-sizing: border-box; + } + + select { + background: var(--vscode-dropdown-background); + color: var(--vscode-dropdown-foreground); + border: 1px solid var(--vscode-dropdown-border); + border-radius: 3px; + padding: 3px 22px 3px 6px; + } + + .check-state { + font-size: 0.8em; + min-width: 80px; + padding: 2px 6px; + border-radius: 3px; + text-align: center; + border: 1px solid; + } + + .check-success { color: var(--vscode-charts-green); border-color: var(--vscode-charts-green); } + .check-failure { color: var(--vscode-charts-red); border-color: var(--vscode-charts-red); } + .check-pending { color: var(--vscode-charts-yellow); border-color: var(--vscode-charts-yellow); } + .check-neutral { color: var(--vscode-descriptionForeground); border-color: var(--vscode-panel-border); } + + @media (max-width: 720px) { + .shell { + padding: 12px; + } + } +`; + + // src/views/webview/vscodeApi.ts + var api; + function vscode() { + if (!api) { + api = acquireVsCodeApi(); + } + return api; + } + function readInitialData() { + const element = document.getElementById("adoext-data"); + if (!element?.textContent) { + throw new Error("Missing ADOExt webview data."); + } + return JSON.parse(element.textContent); + } + function postMessage(message) { + vscode().postMessage(message); + } + + // src/views/webview/workItemSchemaInspector.ts + var AdoWorkItemSchemaInspectorApp = class extends i4 { + constructor() { + super(...arguments); + this.model = readInitialData(); + this.filter = ""; + this.onFilter = (event) => { + this.filter = event.target.value ?? ""; + }; + } + render() { + const filtered = this.filteredTypes(); + const processLabel = this.processLabel(); + return b2`
+
+
+

Work Item Process Inspector

+
${this.model.organization}/${this.model.project}${processLabel ? b2` · ${processLabel}` : A}
+
Fetched: ${this.model.fetchedAt}
+
+
+ + + +
+
+ + ${this.model.warnings.length ? b2`

Warnings

` : A} + + + + ${filtered.length === 0 ? b2`

No matching work item types.

` : b2`${filtered.map((type) => this.renderType(type))}`} + +

Tip: Use “Copy Diagnostic Summary” when filing bugs about custom fields, states, or icons.

+
`; + } + renderType(type) { + const style = type.color ? `--type-color:${type.color}` : ""; + return b2`
+ + ${type.iconUrl ? b2`` : A} + + ${type.name} + ${type.referenceName ? b2`${type.referenceName} · ` : A}${type.stateCount} states ${type.fieldCount} fields + +
+
+
+

States

+ ${type.states.length === 0 ? b2`

No state metadata available.

` : b2`${type.states.map((state) => this.renderStateRow(state))}
NameCategory
`} +
+
+

Fields

+ ${type.fields.length === 0 ? b2`

No field metadata available.

` : b2`${type.fields.map((field) => this.renderFieldRow(field))}
ReferenceName
`} +
+
+
+
`; + } + renderStateRow(state) { + const style = state.color ? `--state-color:${state.color}` : ""; + return b2` + ${state.name} + ${state.category ?? ""} + `; + } + renderFieldRow(field) { + return b2` + ${field.referenceName}${field.alwaysRequired ? b2` required` : A} + ${field.name}${field.helpText ? b2`
${field.helpText}
` : A} + + `; + } + copyField(field) { + this.send({ type: "copyFieldReferenceName", referenceName: field.referenceName }); + } + filteredTypes() { + const filter = this.filter.trim().toLowerCase(); + if (!filter) { + return this.model.types; + } + return this.model.types.filter((type) => { + if (this.match(filter, type.name) || this.match(filter, type.referenceName ?? "")) { + return true; + } + if (type.states.some((state) => this.match(filter, state.name) || this.match(filter, state.category ?? ""))) { + return true; + } + if (type.fields.some((field) => this.match(filter, field.name) || this.match(filter, field.referenceName))) { + return true; + } + return false; + }); + } + match(filter, value) { + return value.toLowerCase().includes(filter); + } + processLabel() { + const template = this.model.processTemplate?.templateName?.trim(); + const version = this.model.processTemplate?.templateVersion?.trim(); + if (!template && !version) { + return void 0; + } + if (template && version) { + return `${template} (${version})`; + } + return template || version; + } + send(message) { + postMessage(message); + } + }; + AdoWorkItemSchemaInspectorApp.properties = { + model: { state: true }, + filter: { state: true } + }; + AdoWorkItemSchemaInspectorApp.styles = [baseStyles, i` + .header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; } + .title { display: flex; flex-direction: column; gap: 4px; min-width: 280px; } + .subtitle { color: var(--vscode-descriptionForeground); } + .toolbar { margin-top: 8px; } + .search { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } + .search input { min-width: 260px; padding: 4px 8px; border: 1px solid var(--vscode-input-border); background: var(--vscode-input-background); color: var(--vscode-input-foreground); border-radius: 3px; } + details { border: 1px solid var(--vscode-panel-border); border-radius: 4px; margin: 10px 0; background: var(--vscode-sideBar-background); } + summary { list-style: none; cursor: pointer; padding: 10px 12px; display: flex; align-items: center; gap: 10px; } + summary::-webkit-details-marker { display: none; } + .type-icon { width: 18px; height: 18px; object-fit: contain; } + .type-name { font-weight: 600; } + .type-meta { color: var(--vscode-descriptionForeground); font-size: 0.9em; } + .type-color { width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--vscode-panel-border); background: var(--type-color, transparent); } + .pill { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 0.8em; background: var(--vscode-badge-background); color: var(--vscode-badge-foreground); } + .body { padding: 0 12px 12px; } + .grid { display: grid; grid-template-columns: 1fr; gap: 14px; } + @media (min-width: 920px) { .grid { grid-template-columns: 1fr 1fr; } } + table { width: 100%; border-collapse: collapse; } + th, td { text-align: left; border-bottom: 1px solid var(--vscode-panel-border); padding: 6px 6px; vertical-align: top; } + th { color: var(--vscode-descriptionForeground); font-weight: 600; font-size: 0.9em; } + code { font-family: var(--vscode-editor-font-family); } + .state-color { width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--vscode-panel-border); background: var(--state-color, transparent); display: inline-block; margin-right: 6px; vertical-align: middle; } + .warnings { border: 1px solid color-mix(in srgb, var(--vscode-charts-yellow) 50%, transparent); background: color-mix(in srgb, var(--vscode-charts-yellow) 10%, transparent); padding: 10px 12px; border-radius: 4px; margin: 10px 0; } + .warnings h2 { margin-bottom: 6px; } + .warnings ul { margin: 0; padding-left: 18px; } + .copy-btn { white-space: nowrap; } + .help { color: var(--vscode-descriptionForeground); font-size: 0.9em; } + `]; + customElements.define("ado-work-item-schema-inspector-app", AdoWorkItemSchemaInspectorApp); +})(); diff --git a/package.json b/package.json index 0e0acc9..1ee4cd4 100644 --- a/package.json +++ b/package.json @@ -202,6 +202,12 @@ "category": "ADOExt", "icon": "$(search)" }, + { + "command": "adoext.openWorkItemSchemaInspector", + "title": "Open Work Item Process Inspector", + "category": "ADOExt", + "icon": "$(beaker)" + }, { "command": "adoext.createWorkItem", "title": "Create Work Item", diff --git a/scripts/build-webviews.js b/scripts/build-webviews.js index 37246d5..0fd966a 100644 --- a/scripts/build-webviews.js +++ b/scripts/build-webviews.js @@ -12,6 +12,7 @@ const buildOptions = { path.join(root, 'src', 'views', 'webview', 'pipelineRunDetails.ts'), path.join(root, 'src', 'views', 'webview', 'prDetails.ts'), path.join(root, 'src', 'views', 'webview', 'workItemDetails.ts'), + path.join(root, 'src', 'views', 'webview', 'workItemSchemaInspector.ts'), path.join(root, 'src', 'views', 'webview', 'planning.ts') ], outdir, @@ -45,4 +46,4 @@ async function main() { main().catch(error => { console.error(error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/src/api/adoClient.ts b/src/api/adoClient.ts index 814e347..3a5d888 100644 --- a/src/api/adoClient.ts +++ b/src/api/adoClient.ts @@ -9,6 +9,7 @@ import { GitVersionType, VersionControlChangeType, GitStatusState, PullRequestAs import { BuildReason, BuildResult, BuildStatus } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { Operation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; import { normalizeWorkItemTypeName, workItemTypeScopeKey } from '../utils/workItemTypeIcons'; +import { mapWithConcurrencyLimit } from '../utils/async'; import type { WorkItem, WorkItemType, @@ -103,6 +104,40 @@ export const PullRequestReviewVotes = { approved: 10 } as const satisfies Record; +export interface WorkItemProcessTemplateInfo { + templateName?: string; + templateTypeId?: string; + templateVersion?: string; +} + +export interface WorkItemSchemaStateInfo { + name: string; + category?: string; + color?: string; +} + +export interface WorkItemSchemaFieldInfo { + name: string; + referenceName: string; + alwaysRequired: boolean; + helpText?: string; +} + +export interface WorkItemTypeSchemaInfo { + name: string; + referenceName?: string; + color?: string; + iconUrl?: string; + states: WorkItemSchemaStateInfo[]; + fields: WorkItemSchemaFieldInfo[]; +} + +export interface WorkItemProcessSchemaInfo { + processTemplate?: WorkItemProcessTemplateInfo; + types: WorkItemTypeSchemaInfo[]; + warnings: string[]; +} + const WORK_ITEM_QUERY_LIMIT = 200; const PLANNING_WORK_ITEM_QUERY_LIMIT = 500; const BUILDS_PER_QUERY = 10; @@ -591,6 +626,95 @@ export class AdoClient { return icons; } + /** + * Fetch a read-only schema snapshot for work item types/states/fields in the project. + * + * This is intended for diagnostics and should not require admin permissions beyond + * reading work item metadata. + */ + async getWorkItemProcessSchema( + project: string, + organization?: string + ): Promise { + const warnings: string[] = []; + const coreApi: ICoreApi = await this.getConnectionFor(organization).getCoreApi(); + const witApi: IWorkItemTrackingApi = await this.getConnectionFor(organization).getWorkItemTrackingApi(); + + let processTemplate: WorkItemProcessTemplateInfo | undefined; + try { + const projectInfo = await coreApi.getProject(project, true, false); + const template = projectInfo?.capabilities?.processTemplate; + if (template) { + processTemplate = { + templateName: template.templateName, + templateTypeId: template.templateTypeId, + templateVersion: template.templateVersion + }; + } + } catch (err) { + warnings.push(`Failed to fetch project process template: ${this.formatError(err)}`); + } + + const typeRefs = await this.getWorkItemTypes(project, organization); + const types = await mapWithConcurrencyLimit(typeRefs, 4, async (typeRef) => { + const typeName = typeRef.name?.trim(); + if (!typeName) { + return undefined; + } + + try { + const full = await witApi.getWorkItemType(project, typeName); + const states = (full.states ?? []) + .map(state => ({ + name: state.name?.trim() ?? '', + category: state.category?.trim() || undefined, + color: state.color?.trim() || undefined + })) + .filter(state => state.name.length > 0); + + const fields = (full.fields ?? full.fieldInstances ?? []) + .map(field => ({ + name: field.name?.trim() ?? '', + referenceName: field.referenceName?.trim() ?? '', + alwaysRequired: Boolean(field.alwaysRequired), + helpText: field.helpText?.trim() || undefined + })) + .filter(field => field.referenceName.length > 0 && field.name.length > 0); + + return { + name: full.name?.trim() ?? typeName, + referenceName: full.referenceName?.trim() || undefined, + color: full.color?.trim() || undefined, + iconUrl: full.icon?.url?.trim() || undefined, + states, + fields + } satisfies WorkItemTypeSchemaInfo; + } catch (err) { + warnings.push(`Failed to fetch work item type "${typeName}": ${this.formatError(err)}`); + return { + name: typeName, + referenceName: typeRef.referenceName?.trim() || undefined, + color: typeRef.color?.trim() || undefined, + iconUrl: typeRef.icon?.url?.trim() || undefined, + states: [], + fields: [] + } satisfies WorkItemTypeSchemaInfo; + } + }); + + return { + processTemplate, + types: types + .filter((type): type is WorkItemTypeSchemaInfo => Boolean(type)) + .sort((a, b) => a.name.localeCompare(b.name)), + warnings + }; + } + + private formatError(error: unknown): string { + return error instanceof Error ? error.message : String(error); + } + // ------------------------------------------------------------------------- // Pull Requests // ------------------------------------------------------------------------- diff --git a/src/commands/schemaInspectorCommands.ts b/src/commands/schemaInspectorCommands.ts new file mode 100644 index 0000000..9af4cb6 --- /dev/null +++ b/src/commands/schemaInspectorCommands.ts @@ -0,0 +1,41 @@ +import * as vscode from 'vscode'; +import type { AdoClient } from '../api/adoClient'; +import type { ConfigManager } from '../config/configManager'; +import { resolveProjectScopes, scopeLabel, type ProjectScope } from '../providers/projectScopes'; +import { showInformationMessage } from '../utils/notifications'; +import { WorkItemSchemaInspectorPanel } from '../views/workItemSchemaInspectorPanel'; + +export async function openWorkItemSchemaInspector( + context: vscode.ExtensionContext, + client: AdoClient, + config: ConfigManager +): Promise { + const scopes = await resolveProjectScopes(client, config); + if (scopes.length === 0) { + showInformationMessage('Select an organization and project first (ADOExt: Select Organization / Select Project).'); + return; + } + + const scope = await pickScope(scopes); + if (!scope) { + return; + } + + await WorkItemSchemaInspectorPanel.show(context, client, config, scope); +} + +async function pickScope(scopes: ProjectScope[]): Promise { + if (scopes.length === 1) { + return scopes[0]; + } + + const choice = await vscode.window.showQuickPick( + scopes.map(scope => ({ + label: scopeLabel(scope), + scope + })), + { placeHolder: 'Select a project scope to inspect' } + ); + return choice?.scope; +} + diff --git a/src/extension.ts b/src/extension.ts index 6f9128a..cf4bd96 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -59,6 +59,7 @@ import { saveWorkItemQuery, savePullRequestQuery } from './commands/queryCommands'; +import { openWorkItemSchemaInspector } from './commands/schemaInspectorCommands'; import { cancelPipelineRun, openPipelineRunInBrowser, @@ -539,6 +540,13 @@ export async function activate(context: vscode.ExtensionContext): Promise }) ); + context.subscriptions.push( + vscode.commands.registerCommand('adoext.openWorkItemSchemaInspector', async () => { + if (!(await ensureSignedIn())) { return; } + await openWorkItemSchemaInspector(context, client, config); + }) + ); + context.subscriptions.push( vscode.commands.registerCommand('adoext.createWorkItem', async () => { if (!(await ensureSignedIn())) { return; } diff --git a/src/views/webview/workItemSchemaInspector.ts b/src/views/webview/workItemSchemaInspector.ts new file mode 100644 index 0000000..ce6be2b --- /dev/null +++ b/src/views/webview/workItemSchemaInspector.ts @@ -0,0 +1,179 @@ +import { LitElement, css, html, nothing, type PropertyDeclarations } from 'lit'; +import { baseStyles } from './commonStyles'; +import { postMessage, readInitialData } from './vscodeApi'; +import type { + WorkItemSchemaInspectorFieldViewModel, + WorkItemSchemaInspectorMessage, + WorkItemSchemaInspectorTypeViewModel, + WorkItemSchemaInspectorViewModel +} from '../webviewTypes'; + +class AdoWorkItemSchemaInspectorApp extends LitElement { + static properties: PropertyDeclarations = { + model: { state: true }, + filter: { state: true } + }; + + static styles = [baseStyles, css` + .header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; } + .title { display: flex; flex-direction: column; gap: 4px; min-width: 280px; } + .subtitle { color: var(--vscode-descriptionForeground); } + .toolbar { margin-top: 8px; } + .search { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } + .search input { min-width: 260px; padding: 4px 8px; border: 1px solid var(--vscode-input-border); background: var(--vscode-input-background); color: var(--vscode-input-foreground); border-radius: 3px; } + details { border: 1px solid var(--vscode-panel-border); border-radius: 4px; margin: 10px 0; background: var(--vscode-sideBar-background); } + summary { list-style: none; cursor: pointer; padding: 10px 12px; display: flex; align-items: center; gap: 10px; } + summary::-webkit-details-marker { display: none; } + .type-icon { width: 18px; height: 18px; object-fit: contain; } + .type-name { font-weight: 600; } + .type-meta { color: var(--vscode-descriptionForeground); font-size: 0.9em; } + .type-color { width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--vscode-panel-border); background: var(--type-color, transparent); } + .pill { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 0.8em; background: var(--vscode-badge-background); color: var(--vscode-badge-foreground); } + .body { padding: 0 12px 12px; } + .grid { display: grid; grid-template-columns: 1fr; gap: 14px; } + @media (min-width: 920px) { .grid { grid-template-columns: 1fr 1fr; } } + table { width: 100%; border-collapse: collapse; } + th, td { text-align: left; border-bottom: 1px solid var(--vscode-panel-border); padding: 6px 6px; vertical-align: top; } + th { color: var(--vscode-descriptionForeground); font-weight: 600; font-size: 0.9em; } + code { font-family: var(--vscode-editor-font-family); } + .state-color { width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--vscode-panel-border); background: var(--state-color, transparent); display: inline-block; margin-right: 6px; vertical-align: middle; } + .warnings { border: 1px solid color-mix(in srgb, var(--vscode-charts-yellow) 50%, transparent); background: color-mix(in srgb, var(--vscode-charts-yellow) 10%, transparent); padding: 10px 12px; border-radius: 4px; margin: 10px 0; } + .warnings h2 { margin-bottom: 6px; } + .warnings ul { margin: 0; padding-left: 18px; } + .copy-btn { white-space: nowrap; } + .help { color: var(--vscode-descriptionForeground); font-size: 0.9em; } + `]; + + model: WorkItemSchemaInspectorViewModel = readInitialData(); + filter = ''; + + render() { + const filtered = this.filteredTypes(); + const processLabel = this.processLabel(); + return html`
+
+
+

Work Item Process Inspector

+
${this.model.organization}/${this.model.project}${processLabel ? html` · ${processLabel}` : nothing}
+
Fetched: ${this.model.fetchedAt}
+
+
+ + + +
+
+ + ${this.model.warnings.length + ? html`

Warnings

    ${this.model.warnings.map(w => html`
  • ${w}
  • `)}
` + : nothing} + + + + ${filtered.length === 0 + ? html`

No matching work item types.

` + : html`${filtered.map(type => this.renderType(type))}`} + +

Tip: Use “Copy Diagnostic Summary” when filing bugs about custom fields, states, or icons.

+
`; + } + + private renderType(type: WorkItemSchemaInspectorTypeViewModel) { + const style = type.color ? `--type-color:${type.color}` : ''; + return html`
+ + ${type.iconUrl ? html`` : nothing} + + ${type.name} + ${type.referenceName ? html`${type.referenceName} · ` : nothing}${type.stateCount} states ${type.fieldCount} fields + +
+
+
+

States

+ ${type.states.length === 0 + ? html`

No state metadata available.

` + : html`${type.states.map(state => this.renderStateRow(state))}
NameCategory
`} +
+
+

Fields

+ ${type.fields.length === 0 + ? html`

No field metadata available.

` + : html`${type.fields.map(field => this.renderFieldRow(field))}
ReferenceName
`} +
+
+
+
`; + } + + private renderStateRow(state: WorkItemSchemaInspectorTypeViewModel['states'][number]) { + const style = state.color ? `--state-color:${state.color}` : ''; + return html` + ${state.name} + ${state.category ?? ''} + `; + } + + private renderFieldRow(field: WorkItemSchemaInspectorFieldViewModel) { + return html` + ${field.referenceName}${field.alwaysRequired ? html` required` : nothing} + ${field.name}${field.helpText ? html`
${field.helpText}
` : nothing} + + `; + } + + private copyField(field: WorkItemSchemaInspectorFieldViewModel): void { + this.send({ type: 'copyFieldReferenceName', referenceName: field.referenceName }); + } + + private onFilter = (event: Event): void => { + this.filter = (event.target as HTMLInputElement).value ?? ''; + }; + + private filteredTypes(): WorkItemSchemaInspectorTypeViewModel[] { + const filter = this.filter.trim().toLowerCase(); + if (!filter) { + return this.model.types; + } + + return this.model.types.filter(type => { + if (this.match(filter, type.name) || this.match(filter, type.referenceName ?? '')) { + return true; + } + if (type.states.some(state => this.match(filter, state.name) || this.match(filter, state.category ?? ''))) { + return true; + } + if (type.fields.some(field => this.match(filter, field.name) || this.match(filter, field.referenceName))) { + return true; + } + return false; + }); + } + + private match(filter: string, value: string): boolean { + return value.toLowerCase().includes(filter); + } + + private processLabel(): string | undefined { + const template = this.model.processTemplate?.templateName?.trim(); + const version = this.model.processTemplate?.templateVersion?.trim(); + if (!template && !version) { + return undefined; + } + if (template && version) { + return `${template} (${version})`; + } + return template || version; + } + + private send(message: WorkItemSchemaInspectorMessage): void { + postMessage(message); + } +} + +customElements.define('ado-work-item-schema-inspector-app', AdoWorkItemSchemaInspectorApp); + diff --git a/src/views/webviewTypes.ts b/src/views/webviewTypes.ts index c9c46cd..e67cd9a 100644 --- a/src/views/webviewTypes.ts +++ b/src/views/webviewTypes.ts @@ -224,3 +224,48 @@ export type PipelineRunDetailsMessage = | { type: 'rerun' } | { type: 'cancel' } | { type: 'openArtifact'; url: string }; + +export interface WorkItemSchemaInspectorProcessTemplateViewModel { + templateName?: string; + templateTypeId?: string; + templateVersion?: string; +} + +export interface WorkItemSchemaInspectorStateViewModel { + name: string; + category?: string; + color?: string; +} + +export interface WorkItemSchemaInspectorFieldViewModel { + name: string; + referenceName: string; + alwaysRequired: boolean; + helpText?: string; +} + +export interface WorkItemSchemaInspectorTypeViewModel { + name: string; + referenceName?: string; + color?: string; + iconUrl?: string; + stateCount: number; + fieldCount: number; + states: WorkItemSchemaInspectorStateViewModel[]; + fields: WorkItemSchemaInspectorFieldViewModel[]; +} + +export interface WorkItemSchemaInspectorViewModel { + organization: string; + project: string; + fetchedAt: string; + processTemplate?: WorkItemSchemaInspectorProcessTemplateViewModel; + warnings: string[]; + types: WorkItemSchemaInspectorTypeViewModel[]; +} + +export type WorkItemSchemaInspectorMessage = + | { type: 'refresh' } + | { type: 'openProcessSettings' } + | { type: 'copyDiagnosticSummary' } + | { type: 'copyFieldReferenceName'; referenceName: string }; diff --git a/src/views/workItemSchemaInspectorPanel.ts b/src/views/workItemSchemaInspectorPanel.ts new file mode 100644 index 0000000..6628af9 --- /dev/null +++ b/src/views/workItemSchemaInspectorPanel.ts @@ -0,0 +1,253 @@ +import * as vscode from 'vscode'; +import type { AdoClient, WorkItemProcessSchemaInfo } from '../api/adoClient'; +import type { ConfigManager } from '../config/configManager'; +import { showErrorMessage, showInformationMessage, showWarningMessage } from '../utils/notifications'; +import { buildWebviewDocument, buildMessageDocument, webviewAssetRoots } from './webviewHtml'; +import type { WorkItemSchemaInspectorMessage, WorkItemSchemaInspectorViewModel } from './webviewTypes'; + +export interface WorkItemSchemaInspectorScope { + organization: string; + project: string; +} + +export class WorkItemSchemaInspectorPanel { + private static _panels = new Map(); + + private readonly _panel: vscode.WebviewPanel; + private readonly _panelKey: string; + private _disposables: vscode.Disposable[] = []; + + static async show( + context: vscode.ExtensionContext, + client: AdoClient, + config: ConfigManager, + scope: WorkItemSchemaInspectorScope + ): Promise { + const key = WorkItemSchemaInspectorPanel.panelKey(scope); + const existing = WorkItemSchemaInspectorPanel._panels.get(key); + if (existing) { + existing._panel.reveal(vscode.ViewColumn.One); + await existing._refresh(client, config, scope); + return; + } + new WorkItemSchemaInspectorPanel(context, client, config, scope, key); + } + + private static panelKey(scope: WorkItemSchemaInspectorScope): string { + return `${scope.organization}\u0000${scope.project}`; + } + + private constructor( + private readonly _context: vscode.ExtensionContext, + private readonly _client: AdoClient, + private readonly _config: ConfigManager, + private _scope: WorkItemSchemaInspectorScope, + panelKey: string + ) { + this._panelKey = panelKey; + this._panel = vscode.window.createWebviewPanel( + 'adoext.workItemSchemaInspector', + `Work Item Process Inspector: ${_scope.organization}/${_scope.project}`, + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: webviewAssetRoots(_context) + } + ); + + this._panel.onDidDispose(() => this._dispose(), null, this._disposables); + this._panel.webview.onDidReceiveMessage( + async (msg) => this._handleMessage(msg), + null, + this._disposables + ); + + WorkItemSchemaInspectorPanel._panels.set(panelKey, this); + void this._refresh(this._client, this._config, this._scope); + } + + private async _handleMessage(msg: WorkItemSchemaInspectorMessage): Promise { + if (msg.type === 'refresh') { + await this._refresh(this._client, this._config, this._scope); + return; + } + + if (msg.type === 'openProcessSettings') { + const url = `https://dev.azure.com/${this._scope.organization}/_settings/process`; + void vscode.env.openExternal(vscode.Uri.parse(url)); + return; + } + + if (msg.type === 'copyFieldReferenceName') { + const ref = (msg.referenceName ?? '').trim(); + if (!ref) { return; } + await vscode.env.clipboard.writeText(ref); + showInformationMessage(`Copied field reference name: ${ref}`); + return; + } + + if (msg.type === 'copyDiagnosticSummary') { + const summary = await this._buildDiagnosticSummary(); + await vscode.env.clipboard.writeText(summary); + showInformationMessage('Copied process/schema diagnostic summary to clipboard.'); + return; + } + } + + private async _buildDiagnosticSummary(): Promise { + const organization = this._scope.organization; + const project = this._scope.project; + let schema: WorkItemProcessSchemaInfo | undefined; + try { + schema = await this._client.getWorkItemProcessSchema(project, organization); + } catch (err) { + return JSON.stringify({ + organization, + project, + error: this._formatError(err) + }, null, 2); + } + + return JSON.stringify({ + organization, + project, + processTemplate: schema.processTemplate ?? null, + fetchedAt: new Date().toISOString(), + typeCount: schema.types.length, + warnings: schema.warnings, + types: schema.types.map(type => ({ + name: type.name, + referenceName: type.referenceName ?? null, + color: type.color ?? null, + iconUrl: type.iconUrl ?? null, + stateCount: type.states.length, + fieldCount: type.fields.length, + states: type.states.map(state => ({ + name: state.name, + category: state.category ?? null, + color: state.color ?? null + })), + fields: type.fields.map(field => ({ + referenceName: field.referenceName, + name: field.name, + alwaysRequired: field.alwaysRequired, + helpText: field.helpText ?? null + })) + })) + }, null, 2); + } + + private async _refresh( + client: AdoClient, + _config: ConfigManager, + scope: WorkItemSchemaInspectorScope + ): Promise { + this._scope = scope; + this._panel.title = `Work Item Process Inspector: ${scope.organization}/${scope.project}`; + const { organization, project } = scope; + + if (!organization || !project) { + this._panel.webview.html = buildMessageDocument(this._panel.webview, 'Select an organization and project first.'); + return; + } + + let schema: WorkItemProcessSchemaInfo; + try { + schema = await client.getWorkItemProcessSchema(project, organization); + } catch (err) { + showErrorMessage(`Failed to load work item process schema: ${this._formatError(err)}`); + this._panel.webview.html = buildMessageDocument(this._panel.webview, 'Failed to load work item schema. See the ADOExt output for details.'); + return; + } + + if (schema.warnings.length) { + showWarningMessage(`Work item schema loaded with warnings (${schema.warnings.length}).`); + } + + const model: WorkItemSchemaInspectorViewModel = { + organization, + project, + fetchedAt: new Date().toLocaleString(), + processTemplate: schema.processTemplate + ? { + templateName: schema.processTemplate.templateName, + templateTypeId: schema.processTemplate.templateTypeId, + templateVersion: schema.processTemplate.templateVersion + } + : undefined, + warnings: schema.warnings, + types: schema.types.map(type => ({ + name: type.name, + referenceName: type.referenceName, + color: this._sanitizeHexColor(type.color), + iconUrl: this._sanitizeIconUrl(type.iconUrl), + stateCount: type.states.length, + fieldCount: type.fields.length, + states: type.states + .map(state => ({ + name: state.name, + category: state.category, + color: this._sanitizeHexColor(state.color) + })) + .sort((a, b) => a.name.localeCompare(b.name)), + fields: type.fields + .map(field => ({ + name: field.name, + referenceName: field.referenceName, + alwaysRequired: field.alwaysRequired, + helpText: field.helpText + })) + .sort((a, b) => a.referenceName.localeCompare(b.referenceName)) + })) + }; + + this._panel.webview.html = this._buildHtml(model); + } + + private _buildHtml(model: WorkItemSchemaInspectorViewModel): string { + return buildWebviewDocument(this._context, this._panel.webview, { + title: `Work Item Process Inspector`, + entry: 'workItemSchemaInspector.js', + appTag: 'ado-work-item-schema-inspector-app', + data: model + }); + } + + private _sanitizeIconUrl(value: string | undefined): string | undefined { + if (!value) { + return undefined; + } + try { + const uri = vscode.Uri.parse(value); + return uri.scheme === 'https' ? uri.toString(true) : undefined; + } catch { + return undefined; + } + } + + private _sanitizeHexColor(value: string | undefined): string | undefined { + if (!value) { + return undefined; + } + const raw = value.trim(); + const withHash = raw.startsWith('#') ? raw : `#${raw}`; + if (!/^#[0-9a-fA-F]{6}$/.test(withHash)) { + return undefined; + } + return withHash.toLowerCase(); + } + + private _formatError(err: unknown): string { + return err instanceof Error ? err.message : String(err); + } + + private _dispose(): void { + WorkItemSchemaInspectorPanel._panels.delete(this._panelKey); + for (const disposable of this._disposables) { + disposable.dispose(); + } + this._disposables = []; + } +} +