feat(mcp): WhatsApp connector, archivable catalog, MCP connector config endpoint

This commit is contained in:
2026-07-19 10:55:44 +01:00
parent ffea3f41f9
commit f85876350e
23 changed files with 1324 additions and 3614 deletions
+51 -8
View File
@@ -2,6 +2,23 @@ import { html, nothing } from 'lit';
import { LightElement } from '../lib/base.js';
import { t } from '../lib/i18n.js';
function _maybeT(key, fallback) {
const v = t(key);
return v !== key ? v : fallback;
}
function _configSetSlug(name) {
const slugs = {
'Interface': 'interface',
'TIC Agent': 'tic_agent',
};
return slugs[name] ?? null;
}
function _propKeyId(propKey) {
return propKey.replace(/\./g, '__');
}
export class ConfigPage extends LightElement {
static properties = {
_open: { state: true },
@@ -130,7 +147,7 @@ export class ConfigPage extends LightElement {
.checked=${checked}
@change=${e => { this._setValue(prop.key, e.target.checked ? 'true' : 'false'); this._save(prop); }} />
<label class="form-check-label" for="cfg-${prop.key}">
${checked ? 'Enabled' : 'Disabled'}
${checked ? t('config.enabled') : t('config.disabled')}
</label>
</div>`;
}
@@ -144,7 +161,13 @@ export class ConfigPage extends LightElement {
@input=${e => this._setValue(prop.key, e.target.value)} />`;
}
// Dropdown-style property types. The backend ships the allowed values in
// `prop.options` (a list of {id, name}); we only decide how to frame them.
// Adding a new custom type from a config section? Give it a `property_type`
// on the backend, attach its `options`, and add a branch like these — a
// free-text box becomes a proper picker for the price of a few lines.
if (prop.property_type === 'security_group') {
// Nullable: the empty choice means "fall back to the instance default".
const groups = prop.options ?? [];
return html`
<select class="form-select form-select-sm config-input"
@@ -156,6 +179,20 @@ export class ConfigPage extends LightElement {
</select>`;
}
if (prop.property_type === 'locale') {
// Interface languages the instance supports; labels are native endonyms.
// Always a concrete pick (no empty option) — falls back to default_value.
const locales = prop.options ?? [];
const current = val || prop.default_value || 'en';
return html`
<select class="form-select form-select-sm config-input"
.value=${current}
@change=${e => { this._setValue(prop.key, e.target.value); this._save(prop); }}>
${locales.map(l => html`
<option value=${l.id} ?selected=${current === l.id}>${l.name}</option>`)}
</select>`;
}
return html`
<input type="text"
class="form-control form-control-sm config-input"
@@ -165,11 +202,14 @@ export class ConfigPage extends LightElement {
}
_renderSet(set) {
const slug = _configSetSlug(set.name);
const sName = slug ? _maybeT(`config.set.${slug}.name`, set.name) : set.name;
const sDesc = slug ? _maybeT(`config.set.${slug}.desc`, set.description) : set.description;
return html`
<div class="config-set">
<div class="config-set-header">
<div class="config-set-name">${set.name}</div>
<div class="config-set-desc">${set.description}</div>
<div class="config-set-name">${sName}</div>
<div class="config-set-desc">${sDesc}</div>
</div>
<div class="config-rows">
${set.properties.map(p => this._renderRow(p))}
@@ -180,22 +220,25 @@ export class ConfigPage extends LightElement {
_renderRow(prop) {
const saving = this._saving.has(prop.key);
const saved = this._saved.has(prop.key);
const pk = _propKeyId(prop.key);
const pName = _maybeT(`config.prop.${pk}.name`, prop.name);
const pDesc = _maybeT(`config.prop.${pk}.desc`, prop.description);
return html`
<div class="config-row">
<div class="config-row-meta">
<div class="config-row-name">${prop.name}</div>
<div class="config-row-desc">${prop.description}</div>
<div class="config-row-name">${pName}</div>
<div class="config-row-desc">${pDesc}</div>
</div>
<div class="config-row-control">
${this._renderInput(prop)}
${prop.property_type !== 'bool' ? html`
${!['bool', 'locale'].includes(prop.property_type) ? html`
<button class="btn btn-sm ${saved ? 'btn-success' : 'btn-primary'} config-save-btn"
?disabled=${saving}
@click=${() => this._save(prop)}>
${saving
? html`<span class="spinner-border spinner-border-sm"></span>`
: saved ? 'Saved' : 'Save'}
: saved ? t('common.saved') : t('common.save')}
</button>` : nothing}
</div>
</div>`;
@@ -237,7 +280,7 @@ export class ConfigPage extends LightElement {
?disabled=${this._debugLoading}
@change=${() => this._toggleDebugMode()} />
<label class="form-check-label" for="cfg-debug-mode">
${this._debugMode ? 'Enabled' : 'Disabled'}
${this._debugMode ? t('config.enabled') : t('config.disabled')}
</label>
</div>
</div>