|
|
|
|
@@ -164,162 +164,269 @@ export class SipproxyViewProviders extends DeesElement {
|
|
|
|
|
iconName: 'lucide:plus',
|
|
|
|
|
type: ['header'] as any,
|
|
|
|
|
actionFunc: async () => {
|
|
|
|
|
await this.openAddModal();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Add Sipgate',
|
|
|
|
|
iconName: 'lucide:phone',
|
|
|
|
|
type: ['header'] as any,
|
|
|
|
|
actionFunc: async () => {
|
|
|
|
|
await this.openAddModal(PROVIDER_TEMPLATES.sipgate, 'Sipgate');
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Add O2/Alice',
|
|
|
|
|
iconName: 'lucide:phone',
|
|
|
|
|
type: ['header'] as any,
|
|
|
|
|
actionFunc: async () => {
|
|
|
|
|
await this.openAddModal(PROVIDER_TEMPLATES.o2, 'O2/Alice');
|
|
|
|
|
await this.openAddStepper();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- add provider modal --------------------------------------------------
|
|
|
|
|
// ---- add provider stepper ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
private async openAddModal(
|
|
|
|
|
template?: typeof PROVIDER_TEMPLATES.sipgate,
|
|
|
|
|
templateName?: string,
|
|
|
|
|
) {
|
|
|
|
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
|
|
|
private async openAddStepper() {
|
|
|
|
|
const { DeesStepper } = await import('@design.estate/dees-catalog');
|
|
|
|
|
type TDeesStepper = InstanceType<typeof DeesStepper>;
|
|
|
|
|
// IStep / menuOptions types: we keep content typing loose (`any[]`) to
|
|
|
|
|
// avoid having to import tsclass IMenuItem just for one parameter annotation.
|
|
|
|
|
|
|
|
|
|
const formData = {
|
|
|
|
|
displayName: templateName || '',
|
|
|
|
|
domain: template?.domain || '',
|
|
|
|
|
outboundProxyAddress: template?.outboundProxy?.address || '',
|
|
|
|
|
outboundProxyPort: String(template?.outboundProxy?.port ?? 5060),
|
|
|
|
|
type TProviderType = 'Custom' | 'Sipgate' | 'O2/Alice';
|
|
|
|
|
interface IAccumulator {
|
|
|
|
|
providerType: TProviderType;
|
|
|
|
|
displayName: string;
|
|
|
|
|
domain: string;
|
|
|
|
|
outboundProxyAddress: string;
|
|
|
|
|
outboundProxyPort: string;
|
|
|
|
|
username: string;
|
|
|
|
|
password: string;
|
|
|
|
|
// Advanced — exposed in step 4
|
|
|
|
|
registerIntervalSec: string;
|
|
|
|
|
codecs: string;
|
|
|
|
|
earlyMediaSilence: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const accumulator: IAccumulator = {
|
|
|
|
|
providerType: 'Custom',
|
|
|
|
|
displayName: '',
|
|
|
|
|
domain: '',
|
|
|
|
|
outboundProxyAddress: '',
|
|
|
|
|
outboundProxyPort: '5060',
|
|
|
|
|
username: '',
|
|
|
|
|
password: '',
|
|
|
|
|
registerIntervalSec: String(template?.registerIntervalSec ?? 300),
|
|
|
|
|
codecs: template?.codecs ? template.codecs.join(', ') : '9, 0, 8, 101',
|
|
|
|
|
earlyMediaSilence: template?.quirks?.earlyMediaSilence ?? false,
|
|
|
|
|
registerIntervalSec: '300',
|
|
|
|
|
codecs: '9, 0, 8, 101',
|
|
|
|
|
earlyMediaSilence: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const heading = template
|
|
|
|
|
? `Add ${templateName} Provider`
|
|
|
|
|
: 'Add Provider';
|
|
|
|
|
// Snapshot the currently-selected step's form (if any) into accumulator.
|
|
|
|
|
const snapshotActiveForm = async (stepper: TDeesStepper) => {
|
|
|
|
|
const form = stepper.activeForm;
|
|
|
|
|
if (!form) return;
|
|
|
|
|
const data: Record<string, any> = await form.collectFormData();
|
|
|
|
|
Object.assign(accumulator, data);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await DeesModal.createAndShow({
|
|
|
|
|
heading,
|
|
|
|
|
width: 'small',
|
|
|
|
|
showCloseButton: true,
|
|
|
|
|
// Overwrite template-owned fields. Keep user-owned fields (username,
|
|
|
|
|
// password) untouched. displayName is replaced only when empty or still
|
|
|
|
|
// holds a branded auto-fill.
|
|
|
|
|
const applyTemplate = (type: TProviderType) => {
|
|
|
|
|
const tpl =
|
|
|
|
|
type === 'Sipgate' ? PROVIDER_TEMPLATES.sipgate
|
|
|
|
|
: type === 'O2/Alice' ? PROVIDER_TEMPLATES.o2
|
|
|
|
|
: null;
|
|
|
|
|
if (!tpl) return;
|
|
|
|
|
accumulator.domain = tpl.domain;
|
|
|
|
|
accumulator.outboundProxyAddress = tpl.outboundProxy.address;
|
|
|
|
|
accumulator.outboundProxyPort = String(tpl.outboundProxy.port);
|
|
|
|
|
accumulator.registerIntervalSec = String(tpl.registerIntervalSec);
|
|
|
|
|
accumulator.codecs = tpl.codecs.join(', ');
|
|
|
|
|
accumulator.earlyMediaSilence = tpl.quirks.earlyMediaSilence;
|
|
|
|
|
if (
|
|
|
|
|
!accumulator.displayName ||
|
|
|
|
|
accumulator.displayName === 'Sipgate' ||
|
|
|
|
|
accumulator.displayName === 'O2/Alice'
|
|
|
|
|
) {
|
|
|
|
|
accumulator.displayName = type;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Step builders (called after step 1 so accumulator is populated) ---
|
|
|
|
|
|
|
|
|
|
const buildConnectionStep = (): any => ({
|
|
|
|
|
title: 'Connection',
|
|
|
|
|
content: html`
|
|
|
|
|
<div style="display:flex;flex-direction:column;gap:12px;padding:4px 0;">
|
|
|
|
|
<dees-form>
|
|
|
|
|
<dees-input-text
|
|
|
|
|
.key=${'displayName'}
|
|
|
|
|
.label=${'Display Name'}
|
|
|
|
|
.value=${formData.displayName}
|
|
|
|
|
@input=${(e: Event) => { formData.displayName = (e.target as any).value; }}
|
|
|
|
|
.value=${accumulator.displayName}
|
|
|
|
|
.required=${true}
|
|
|
|
|
></dees-input-text>
|
|
|
|
|
<dees-input-text
|
|
|
|
|
.key=${'domain'}
|
|
|
|
|
.label=${'Domain'}
|
|
|
|
|
.value=${formData.domain}
|
|
|
|
|
@input=${(e: Event) => { formData.domain = (e.target as any).value; }}
|
|
|
|
|
.value=${accumulator.domain}
|
|
|
|
|
.required=${true}
|
|
|
|
|
></dees-input-text>
|
|
|
|
|
<dees-input-text
|
|
|
|
|
.key=${'outboundProxyAddress'}
|
|
|
|
|
.label=${'Outbound Proxy Address'}
|
|
|
|
|
.value=${formData.outboundProxyAddress}
|
|
|
|
|
@input=${(e: Event) => { formData.outboundProxyAddress = (e.target as any).value; }}
|
|
|
|
|
.value=${accumulator.outboundProxyAddress}
|
|
|
|
|
></dees-input-text>
|
|
|
|
|
<dees-input-text
|
|
|
|
|
.key=${'outboundProxyPort'}
|
|
|
|
|
.label=${'Outbound Proxy Port'}
|
|
|
|
|
.value=${formData.outboundProxyPort}
|
|
|
|
|
@input=${(e: Event) => { formData.outboundProxyPort = (e.target as any).value; }}
|
|
|
|
|
.value=${accumulator.outboundProxyPort}
|
|
|
|
|
></dees-input-text>
|
|
|
|
|
</dees-form>
|
|
|
|
|
`,
|
|
|
|
|
menuOptions: [
|
|
|
|
|
{
|
|
|
|
|
name: 'Continue',
|
|
|
|
|
iconName: 'lucide:arrow-right',
|
|
|
|
|
action: async (stepper: TDeesStepper) => {
|
|
|
|
|
await snapshotActiveForm(stepper);
|
|
|
|
|
stepper.goNext();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const buildCredentialsStep = (): any => ({
|
|
|
|
|
title: 'Credentials',
|
|
|
|
|
content: html`
|
|
|
|
|
<dees-form>
|
|
|
|
|
<dees-input-text
|
|
|
|
|
.key=${'username'}
|
|
|
|
|
.label=${'Username / Auth ID'}
|
|
|
|
|
.value=${formData.username}
|
|
|
|
|
@input=${(e: Event) => { formData.username = (e.target as any).value; }}
|
|
|
|
|
.value=${accumulator.username}
|
|
|
|
|
.required=${true}
|
|
|
|
|
></dees-input-text>
|
|
|
|
|
<dees-input-text
|
|
|
|
|
.key=${'password'}
|
|
|
|
|
.label=${'Password'}
|
|
|
|
|
.isPasswordBool=${true}
|
|
|
|
|
.value=${formData.password}
|
|
|
|
|
@input=${(e: Event) => { formData.password = (e.target as any).value; }}
|
|
|
|
|
.value=${accumulator.password}
|
|
|
|
|
.required=${true}
|
|
|
|
|
></dees-input-text>
|
|
|
|
|
</dees-form>
|
|
|
|
|
`,
|
|
|
|
|
menuOptions: [
|
|
|
|
|
{
|
|
|
|
|
name: 'Continue',
|
|
|
|
|
iconName: 'lucide:arrow-right',
|
|
|
|
|
action: async (stepper: TDeesStepper) => {
|
|
|
|
|
await snapshotActiveForm(stepper);
|
|
|
|
|
stepper.goNext();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const buildAdvancedStep = (): any => ({
|
|
|
|
|
title: 'Advanced',
|
|
|
|
|
content: html`
|
|
|
|
|
<dees-form>
|
|
|
|
|
<dees-input-text
|
|
|
|
|
.key=${'registerIntervalSec'}
|
|
|
|
|
.label=${'Register Interval (sec)'}
|
|
|
|
|
.value=${formData.registerIntervalSec}
|
|
|
|
|
@input=${(e: Event) => { formData.registerIntervalSec = (e.target as any).value; }}
|
|
|
|
|
.value=${accumulator.registerIntervalSec}
|
|
|
|
|
></dees-input-text>
|
|
|
|
|
<dees-input-text
|
|
|
|
|
.key=${'codecs'}
|
|
|
|
|
.label=${'Codecs (comma-separated payload types)'}
|
|
|
|
|
.value=${formData.codecs}
|
|
|
|
|
@input=${(e: Event) => { formData.codecs = (e.target as any).value; }}
|
|
|
|
|
.value=${accumulator.codecs}
|
|
|
|
|
></dees-input-text>
|
|
|
|
|
<dees-input-checkbox
|
|
|
|
|
.key=${'earlyMediaSilence'}
|
|
|
|
|
.label=${'Early Media Silence (quirk)'}
|
|
|
|
|
.value=${formData.earlyMediaSilence}
|
|
|
|
|
@newValue=${(e: CustomEvent) => { formData.earlyMediaSilence = e.detail; }}
|
|
|
|
|
.value=${accumulator.earlyMediaSilence}
|
|
|
|
|
></dees-input-checkbox>
|
|
|
|
|
</div>
|
|
|
|
|
</dees-form>
|
|
|
|
|
`,
|
|
|
|
|
menuOptions: [
|
|
|
|
|
{
|
|
|
|
|
name: 'Cancel',
|
|
|
|
|
iconName: 'lucide:x',
|
|
|
|
|
action: async (modalRef: any) => {
|
|
|
|
|
modalRef.destroy();
|
|
|
|
|
name: 'Continue',
|
|
|
|
|
iconName: 'lucide:arrow-right',
|
|
|
|
|
action: async (stepper: TDeesStepper) => {
|
|
|
|
|
await snapshotActiveForm(stepper);
|
|
|
|
|
// Rebuild the review step so its rendering reflects the latest
|
|
|
|
|
// accumulator values (the review step captures values at build time).
|
|
|
|
|
stepper.steps = [...stepper.steps.slice(0, 4), buildReviewStep()];
|
|
|
|
|
await (stepper as any).updateComplete;
|
|
|
|
|
stepper.goNext();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const buildReviewStep = (): any => {
|
|
|
|
|
const resolvedId = slugify(accumulator.displayName);
|
|
|
|
|
return {
|
|
|
|
|
title: 'Review & Create',
|
|
|
|
|
content: html`
|
|
|
|
|
<dees-panel>
|
|
|
|
|
<div
|
|
|
|
|
style="display:grid;grid-template-columns:auto 1fr;gap:6px 16px;font-size:.85rem;padding:8px 4px;"
|
|
|
|
|
>
|
|
|
|
|
<div style="color:#94a3b8;">Type</div>
|
|
|
|
|
<div>${accumulator.providerType}</div>
|
|
|
|
|
<div style="color:#94a3b8;">Display Name</div>
|
|
|
|
|
<div>${accumulator.displayName}</div>
|
|
|
|
|
<div style="color:#94a3b8;">ID</div>
|
|
|
|
|
<div style="font-family:'JetBrains Mono',monospace;">${resolvedId}</div>
|
|
|
|
|
<div style="color:#94a3b8;">Domain</div>
|
|
|
|
|
<div>${accumulator.domain}</div>
|
|
|
|
|
<div style="color:#94a3b8;">Outbound Proxy</div>
|
|
|
|
|
<div>
|
|
|
|
|
${accumulator.outboundProxyAddress || accumulator.domain}:${accumulator.outboundProxyPort}
|
|
|
|
|
</div>
|
|
|
|
|
<div style="color:#94a3b8;">Username</div>
|
|
|
|
|
<div>${accumulator.username}</div>
|
|
|
|
|
<div style="color:#94a3b8;">Password</div>
|
|
|
|
|
<div>${'*'.repeat(Math.min(accumulator.password.length, 12))}</div>
|
|
|
|
|
<div style="color:#94a3b8;">Register Interval</div>
|
|
|
|
|
<div>${accumulator.registerIntervalSec}s</div>
|
|
|
|
|
<div style="color:#94a3b8;">Codecs</div>
|
|
|
|
|
<div>${accumulator.codecs}</div>
|
|
|
|
|
<div style="color:#94a3b8;">Early-Media Silence</div>
|
|
|
|
|
<div>${accumulator.earlyMediaSilence ? 'yes' : 'no'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</dees-panel>
|
|
|
|
|
`,
|
|
|
|
|
menuOptions: [
|
|
|
|
|
{
|
|
|
|
|
name: 'Create',
|
|
|
|
|
name: 'Create Provider',
|
|
|
|
|
iconName: 'lucide:check',
|
|
|
|
|
action: async (modalRef: any) => {
|
|
|
|
|
if (!formData.displayName.trim() || !formData.domain.trim()) {
|
|
|
|
|
deesCatalog.DeesToast.error('Display name and domain are required');
|
|
|
|
|
return;
|
|
|
|
|
action: async (stepper: TDeesStepper) => {
|
|
|
|
|
// Collision-resolve id against current state.
|
|
|
|
|
const existing = (this.appData.providers || []).map((p) => p.id);
|
|
|
|
|
let uniqueId = resolvedId;
|
|
|
|
|
let suffix = 2;
|
|
|
|
|
while (existing.includes(uniqueId)) {
|
|
|
|
|
uniqueId = `${resolvedId}-${suffix++}`;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const providerId = slugify(formData.displayName);
|
|
|
|
|
const codecs = formData.codecs
|
|
|
|
|
|
|
|
|
|
const parsedCodecs = accumulator.codecs
|
|
|
|
|
.split(',')
|
|
|
|
|
.map((s: string) => parseInt(s.trim(), 10))
|
|
|
|
|
.filter((n: number) => !isNaN(n));
|
|
|
|
|
|
|
|
|
|
const newProvider: any = {
|
|
|
|
|
id: providerId,
|
|
|
|
|
displayName: formData.displayName.trim(),
|
|
|
|
|
domain: formData.domain.trim(),
|
|
|
|
|
id: uniqueId,
|
|
|
|
|
displayName: accumulator.displayName.trim(),
|
|
|
|
|
domain: accumulator.domain.trim(),
|
|
|
|
|
outboundProxy: {
|
|
|
|
|
address: formData.outboundProxyAddress.trim() || formData.domain.trim(),
|
|
|
|
|
port: parseInt(formData.outboundProxyPort, 10) || 5060,
|
|
|
|
|
address:
|
|
|
|
|
accumulator.outboundProxyAddress.trim() || accumulator.domain.trim(),
|
|
|
|
|
port: parseInt(accumulator.outboundProxyPort, 10) || 5060,
|
|
|
|
|
},
|
|
|
|
|
username: formData.username.trim(),
|
|
|
|
|
password: formData.password,
|
|
|
|
|
registerIntervalSec: parseInt(formData.registerIntervalSec, 10) || 300,
|
|
|
|
|
codecs,
|
|
|
|
|
username: accumulator.username.trim(),
|
|
|
|
|
password: accumulator.password,
|
|
|
|
|
registerIntervalSec: parseInt(accumulator.registerIntervalSec, 10) || 300,
|
|
|
|
|
codecs: parsedCodecs.length ? parsedCodecs : [9, 0, 8, 101],
|
|
|
|
|
quirks: {
|
|
|
|
|
earlyMediaSilence: formData.earlyMediaSilence,
|
|
|
|
|
earlyMediaSilence: accumulator.earlyMediaSilence,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await appState.apiSaveConfig({
|
|
|
|
|
addProvider: newProvider,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.ok) {
|
|
|
|
|
modalRef.destroy();
|
|
|
|
|
deesCatalog.DeesToast.success(`Provider "${formData.displayName}" created`);
|
|
|
|
|
await stepper.destroy();
|
|
|
|
|
deesCatalog.DeesToast.success(
|
|
|
|
|
`Provider "${newProvider.displayName}" created`,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
deesCatalog.DeesToast.error('Failed to save provider');
|
|
|
|
|
}
|
|
|
|
|
@@ -330,7 +437,73 @@ export class SipproxyViewProviders extends DeesElement {
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Step 1: Provider Type ------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Note: `DeesStepper.createAndShow()` dismisses on backdrop click; a user
|
|
|
|
|
// mid-form could lose work. Acceptable for v1 — revisit if users complain.
|
|
|
|
|
|
|
|
|
|
const typeOptions: { option: string; key: TProviderType }[] = [
|
|
|
|
|
{ option: 'Custom', key: 'Custom' },
|
|
|
|
|
{ option: 'Sipgate', key: 'Sipgate' },
|
|
|
|
|
{ option: 'O2 / Alice', key: 'O2/Alice' },
|
|
|
|
|
];
|
|
|
|
|
const currentTypeOption =
|
|
|
|
|
typeOptions.find((o) => o.key === accumulator.providerType) || null;
|
|
|
|
|
|
|
|
|
|
const typeStep: any = {
|
|
|
|
|
title: 'Choose provider type',
|
|
|
|
|
content: html`
|
|
|
|
|
<dees-form>
|
|
|
|
|
<dees-input-dropdown
|
|
|
|
|
.key=${'providerType'}
|
|
|
|
|
.label=${'Provider Type'}
|
|
|
|
|
.options=${typeOptions}
|
|
|
|
|
.selectedOption=${currentTypeOption}
|
|
|
|
|
.enableSearch=${false}
|
|
|
|
|
.required=${true}
|
|
|
|
|
></dees-input-dropdown>
|
|
|
|
|
</dees-form>
|
|
|
|
|
`,
|
|
|
|
|
menuOptions: [
|
|
|
|
|
{
|
|
|
|
|
name: 'Continue',
|
|
|
|
|
iconName: 'lucide:arrow-right',
|
|
|
|
|
action: async (stepper: TDeesStepper) => {
|
|
|
|
|
// `dees-input-dropdown.value` is an object `{option, key, payload?}`,
|
|
|
|
|
// not a plain string — extract the `key` directly instead of using
|
|
|
|
|
// the generic `snapshotActiveForm` helper (which would clobber
|
|
|
|
|
// `accumulator.providerType`'s string type via Object.assign).
|
|
|
|
|
const form = stepper.activeForm;
|
|
|
|
|
if (form) {
|
|
|
|
|
const data: Record<string, any> = await form.collectFormData();
|
|
|
|
|
const selected = data.providerType;
|
|
|
|
|
if (selected && typeof selected === 'object' && selected.key) {
|
|
|
|
|
accumulator.providerType = selected.key as TProviderType;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!accumulator.providerType) {
|
|
|
|
|
accumulator.providerType = 'Custom';
|
|
|
|
|
}
|
|
|
|
|
applyTemplate(accumulator.providerType);
|
|
|
|
|
// (Re)build steps 2-5 with current accumulator values.
|
|
|
|
|
stepper.steps = [
|
|
|
|
|
typeStep,
|
|
|
|
|
buildConnectionStep(),
|
|
|
|
|
buildCredentialsStep(),
|
|
|
|
|
buildAdvancedStep(),
|
|
|
|
|
buildReviewStep(),
|
|
|
|
|
];
|
|
|
|
|
await (stepper as any).updateComplete;
|
|
|
|
|
stepper.goNext();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await DeesStepper.createAndShow({ steps: [typeStep] });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- edit provider modal -------------------------------------------------
|
|
|
|
|
|