feat: add browser inbox domain binding
This commit is contained in:
parent
607535358c
commit
fdc5bd0f46
|
|
@ -87,6 +87,33 @@
|
||||||
return text(value).trim().replace(/^\/+|\/+$/g, '');
|
return text(value).trim().replace(/^\/+|\/+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanDomain(value) {
|
||||||
|
return text(value).trim().toLowerCase().replace(/^\.+/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function domainFromUrl(value) {
|
||||||
|
try {
|
||||||
|
return cleanDomain(new URL(text(value).trim()).hostname);
|
||||||
|
} catch (_) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function domainFromCapture(capture) {
|
||||||
|
return cleanDomain(capture && capture.domain) || domainFromUrl(capture && capture.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDomainBindings(value) {
|
||||||
|
var result = {};
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return result;
|
||||||
|
Object.keys(value).forEach(function (domain) {
|
||||||
|
var normalizedDomain = cleanDomain(domain);
|
||||||
|
var workspaceRoot = cleanWorkspace(value[domain]);
|
||||||
|
if (normalizedDomain && workspaceRoot) result[normalizedDomain] = workspaceRoot;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function workspaceFromProps(props) {
|
function workspaceFromProps(props) {
|
||||||
var node = props && props.workspaceNode;
|
var node = props && props.workspaceNode;
|
||||||
return cleanWorkspace((props && (props.workspaceRootPath || props.workspaceName || props.workspaceNodeId))
|
return cleanWorkspace((props && (props.workspaceRootPath || props.workspaceName || props.workspaceNodeId))
|
||||||
|
|
@ -162,6 +189,7 @@
|
||||||
source: text(item.source),
|
source: text(item.source),
|
||||||
browserName: text(item.browserName),
|
browserName: text(item.browserName),
|
||||||
workspaceRootPath: cleanWorkspace(item.workspaceRootPath),
|
workspaceRootPath: cleanWorkspace(item.workspaceRootPath),
|
||||||
|
workspaceName: cleanWorkspace(item.workspaceName || item.workspaceRootPath),
|
||||||
_storageKey: storageKey || ''
|
_storageKey: storageKey || ''
|
||||||
};
|
};
|
||||||
}).slice(0, MAX_CAPTURES);
|
}).slice(0, MAX_CAPTURES);
|
||||||
|
|
@ -180,7 +208,8 @@
|
||||||
text: item.text,
|
text: item.text,
|
||||||
source: item.source,
|
source: item.source,
|
||||||
browserName: item.browserName,
|
browserName: item.browserName,
|
||||||
workspaceRootPath: item.workspaceRootPath
|
workspaceRootPath: item.workspaceRootPath,
|
||||||
|
workspaceName: item.workspaceName || item.workspaceRootPath || ''
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -228,6 +257,7 @@
|
||||||
var statusClass = '';
|
var statusClass = '';
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
var unsubscribers = [];
|
var unsubscribers = [];
|
||||||
|
var domainBindings = {};
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'browser-inbox-toolbar' });
|
var toolbar = el('div', { className: 'browser-inbox-toolbar' });
|
||||||
var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? 'Browser Inbox' : 'Browser Inbox · ' + scope.label });
|
var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? 'Browser Inbox' : 'Browser Inbox · ' + scope.label });
|
||||||
|
|
@ -263,9 +293,21 @@
|
||||||
|
|
||||||
function persist() {
|
function persist() {
|
||||||
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
||||||
var toStore = scope.mode === 'global'
|
if (scope.mode === 'global') {
|
||||||
? captures.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; })
|
var grouped = {};
|
||||||
: captures;
|
captures.forEach(function (item) {
|
||||||
|
var key = item._storageKey || GLOBAL_KEY;
|
||||||
|
grouped[key] = grouped[key] || [];
|
||||||
|
grouped[key].push(item);
|
||||||
|
});
|
||||||
|
return Promise.all(Object.keys(grouped).map(function (key) {
|
||||||
|
return api.settings.write(key, storageCaptures(sortCaptures(grouped[key])));
|
||||||
|
})).catch(function (err) {
|
||||||
|
statusText = 'Could not save inbox: ' + (err && err.message ? err.message : String(err));
|
||||||
|
statusClass = 'error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var toStore = captures;
|
||||||
return api.settings.write(scope.key, storageCaptures(toStore)).catch(function (err) {
|
return api.settings.write(scope.key, storageCaptures(toStore)).catch(function (err) {
|
||||||
statusText = 'Could not save inbox: ' + (err && err.message ? err.message : String(err));
|
statusText = 'Could not save inbox: ' + (err && err.message ? err.message : String(err));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
|
|
@ -302,11 +344,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCapture(capture) {
|
function addCapture(capture) {
|
||||||
|
capture = applyDomainBinding(capture);
|
||||||
|
if (scope.mode === 'workspace' && capture.workspaceRootPath && capture.workspaceRootPath !== scope.workspaceRoot) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
var existing = captures.some(function (item) {
|
var existing = captures.some(function (item) {
|
||||||
return item.captureId === capture.captureId;
|
return item.captureId === capture.captureId;
|
||||||
});
|
});
|
||||||
if (existing) return Promise.resolve();
|
if (existing) return Promise.resolve();
|
||||||
capture._storageKey = scope.key;
|
capture._storageKey = storageKeyForCapture(capture);
|
||||||
captures = sortCaptures([capture].concat(captures));
|
captures = sortCaptures([capture].concat(captures));
|
||||||
selectedId = capture.captureId;
|
selectedId = capture.captureId;
|
||||||
statusText = 'Capture received';
|
statusText = 'Capture received';
|
||||||
|
|
@ -314,6 +360,21 @@
|
||||||
return persist().then(render);
|
return persist().then(render);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyDomainBinding(capture) {
|
||||||
|
if (!capture || capture.workspaceRootPath) return capture;
|
||||||
|
var workspaceRoot = domainBindings[domainFromCapture(capture)];
|
||||||
|
if (!workspaceRoot) return capture;
|
||||||
|
capture.workspaceRootPath = workspaceRoot;
|
||||||
|
capture.workspaceName = workspaceRoot;
|
||||||
|
return capture;
|
||||||
|
}
|
||||||
|
|
||||||
|
function storageKeyForCapture(capture) {
|
||||||
|
var workspaceRoot = cleanWorkspace(capture && capture.workspaceRootPath);
|
||||||
|
if (workspaceRoot) return WORKSPACE_PREFIX + encodeKey(workspaceRoot);
|
||||||
|
return scope.key;
|
||||||
|
}
|
||||||
|
|
||||||
function removeCapture(captureId) {
|
function removeCapture(captureId) {
|
||||||
captures = captures.filter(function (item) {
|
captures = captures.filter(function (item) {
|
||||||
return item.captureId !== captureId;
|
return item.captureId !== captureId;
|
||||||
|
|
@ -408,6 +469,7 @@
|
||||||
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
|
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
|
||||||
if (scope.mode === 'global') {
|
if (scope.mode === 'global') {
|
||||||
return api.settings.read().then(function (settings) {
|
return api.settings.read().then(function (settings) {
|
||||||
|
domainBindings = normalizeDomainBindings((settings || {}).domainBindings);
|
||||||
var all = [];
|
var all = [];
|
||||||
globalCaptureKeys(settings || {}).forEach(function (key) {
|
globalCaptureKeys(settings || {}).forEach(function (key) {
|
||||||
all = all.concat(normalizeStoredCaptures((settings || {})[key], key));
|
all = all.concat(normalizeStoredCaptures((settings || {})[key], key));
|
||||||
|
|
@ -420,6 +482,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return api.settings.read().then(function (settings) {
|
return api.settings.read().then(function (settings) {
|
||||||
|
domainBindings = normalizeDomainBindings((settings || {}).domainBindings);
|
||||||
var scopedCaptures = normalizeStoredCaptures((settings || {})[scope.key], scope.key);
|
var scopedCaptures = normalizeStoredCaptures((settings || {})[scope.key], scope.key);
|
||||||
var globalCaptures = normalizeStoredCaptures((settings || {})[GLOBAL_KEY], GLOBAL_KEY).filter(function (item) {
|
var globalCaptures = normalizeStoredCaptures((settings || {})[GLOBAL_KEY], GLOBAL_KEY).filter(function (item) {
|
||||||
return item.workspaceRootPath === scope.workspaceRoot;
|
return item.workspaceRootPath === scope.workspaceRoot;
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,65 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
throw new Error('workspace-tagged global capture leaked into another workspace');
|
throw new Error('workspace-tagged global capture leaked into another workspace');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bindingApi = makeApi({
|
||||||
|
domainBindings: {
|
||||||
|
'client.example.com': 'ClientA',
|
||||||
|
'project.example.com': 'Project',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const bindingGlobal = await mountWithApi(bindingApi, {});
|
||||||
|
await bindingApi.handlers['browser.capture.page']({
|
||||||
|
name: 'browser.capture.page',
|
||||||
|
timestamp: '2026-06-29T00:00:00Z',
|
||||||
|
payload: {
|
||||||
|
captureId: 'bound-client-capture',
|
||||||
|
capturedAt: '2026-06-29T00:00:00.000Z',
|
||||||
|
kind: 'page',
|
||||||
|
url: 'https://client.example.com/page',
|
||||||
|
title: 'Bound Client Page',
|
||||||
|
domain: 'client.example.com',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
if (bindingApi.getStoredCaptures('captures:workspace:ClientA').length !== 1) {
|
||||||
|
throw new Error('domain-bound capture was not stored under ClientA workspace key');
|
||||||
|
}
|
||||||
|
if (bindingApi.getStoredCaptures('captures:global').length !== 0) {
|
||||||
|
throw new Error('domain-bound capture was stored in global queue');
|
||||||
|
}
|
||||||
|
const bindingClient = await mountWithApi(bindingApi, { workspaceNode: { name: 'ClientA' }, workspaceRootPath: 'ClientA' });
|
||||||
|
if (!walk(bindingClient.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'bound-client-capture')) {
|
||||||
|
throw new Error('domain-bound capture was not rendered in bound workspace');
|
||||||
|
}
|
||||||
|
const bindingAggregate = await mountWithApi(bindingApi, {});
|
||||||
|
if (!walk(bindingAggregate.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'bound-client-capture')) {
|
||||||
|
throw new Error('global browser inbox did not aggregate domain-bound capture');
|
||||||
|
}
|
||||||
|
|
||||||
|
await bindingApi.handlers['browser.capture.page']({
|
||||||
|
name: 'browser.capture.page',
|
||||||
|
timestamp: '2026-06-29T00:10:00Z',
|
||||||
|
payload: {
|
||||||
|
captureId: 'explicit-project-capture',
|
||||||
|
capturedAt: '2026-06-29T00:10:00.000Z',
|
||||||
|
kind: 'page',
|
||||||
|
url: 'https://client.example.com/explicit',
|
||||||
|
title: 'Explicit Project Page',
|
||||||
|
domain: 'client.example.com',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
if (!bindingApi.getStoredCaptures('captures:workspace:Project').some((capture) => capture.captureId === 'explicit-project-capture')) {
|
||||||
|
throw new Error('explicit workspace capture was not stored under its payload workspace');
|
||||||
|
}
|
||||||
|
if (bindingApi.getStoredCaptures('captures:workspace:ClientA').some((capture) => capture.captureId === 'explicit-project-capture')) {
|
||||||
|
throw new Error('domain binding overrode explicit workspaceRootPath');
|
||||||
|
}
|
||||||
|
component.unmount && component.unmount(bindingGlobal.container);
|
||||||
|
component.unmount && component.unmount(bindingClient.container);
|
||||||
|
component.unmount && component.unmount(bindingAggregate.container);
|
||||||
|
|
||||||
console.log('browser inbox plugin smoke passed');
|
console.log('browser inbox plugin smoke passed');
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue