From b43ba4c1b218579709d60fc4e16c58887f7b2f20 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 12 Feb 2026 22:47:24 +0100 Subject: [PATCH] add campaign company resource handling Signed-off-by: Ronni Skansing --- .../components/header/CompanyBanner.svelte | 167 ++++++++++++++---- frontend/src/lib/store/resourceContext.js | 45 +++++ .../src/routes/campaign-template/+page.svelte | 45 ++++- .../src/routes/campaign/[id]/+page.svelte | 27 ++- 4 files changed, 247 insertions(+), 37 deletions(-) create mode 100644 frontend/src/lib/store/resourceContext.js diff --git a/frontend/src/lib/components/header/CompanyBanner.svelte b/frontend/src/lib/components/header/CompanyBanner.svelte index 0673e22..d48fa29 100644 --- a/frontend/src/lib/components/header/CompanyBanner.svelte +++ b/frontend/src/lib/components/header/CompanyBanner.svelte @@ -1,25 +1,44 @@ -{#if isCompanyView} +{#if isCompanyView || hasContextMismatch} -
+
-
- Viewing as - - {context.companyName} - -
+ {#if hasContextMismatch} + +
+ {#if isCompanyView && isResourceGlobal} + Viewing as + {context.companyName} + + + This {resource.resourceType || 'resource'} is + global + + {:else if !isCompanyView && resource.resourceCompanyID} + + This {resource.resourceType || 'resource'} belongs to + {resource.resourceCompanyName || 'a company'} + + {:else if isResourceInDifferentCompany} + Viewing as + {context.companyName} + + + This {resource.resourceType || 'resource'} belongs to + {resource.resourceCompanyName || 'another company'} + + {/if} - - + + +
+ {:else} + +
+ Viewing as + + {context.companyName} + +
+ + + + {/if}
{/if} - -{#if isCompanyView} + +{#if isCompanyView || hasContextMismatch}
{/if} @@ -81,7 +182,7 @@ left: 0; right: 0; bottom: 0; - border: 3px solid #1e3fa8; + border: 3px solid; pointer-events: none; z-index: 9999; } diff --git a/frontend/src/lib/store/resourceContext.js b/frontend/src/lib/store/resourceContext.js new file mode 100644 index 0000000..be7a5bb --- /dev/null +++ b/frontend/src/lib/store/resourceContext.js @@ -0,0 +1,45 @@ +import { writable } from 'svelte/store'; + +/** + * store for tracking the current resource being viewed + * this allows the company banner to show context mismatch information + */ +function createResourceContextStore() { + const { subscribe, set, update } = writable({ + resourceType: null, // e.g., 'campaign', 'template', 'email' + resourceCompanyID: null, + resourceCompanyName: null, + isActive: false + }); + + return { + subscribe, + /** + * set the current resource context + * @param {string} type - resource type (e.g., 'campaign', 'template') + * @param {string|null} companyID - company id the resource belongs to (null for global) + * @param {string|null} companyName - company name (null for global) + */ + setResource: (type, companyID, companyName) => { + set({ + resourceType: type, + resourceCompanyID: companyID, + resourceCompanyName: companyName, + isActive: true + }); + }, + /** + * clear the resource context (call when leaving a resource page) + */ + clear: () => { + set({ + resourceType: null, + resourceCompanyID: null, + resourceCompanyName: null, + isActive: false + }); + } + }; +} + +export const resourceContext = createResourceContextStore(); diff --git a/frontend/src/routes/campaign-template/+page.svelte b/frontend/src/routes/campaign-template/+page.svelte index 371a590..e988d64 100644 --- a/frontend/src/routes/campaign-template/+page.svelte +++ b/frontend/src/routes/campaign-template/+page.svelte @@ -37,6 +37,7 @@ import CopyCell from '$lib/components/table/CopyCell.svelte'; import TextFieldSelectWithType from '$lib/components/form/TextFieldSelectWithType.svelte'; import ConditionalDisplay from '$lib/components/ConditionalDisplay.svelte'; + import { resourceContext } from '$lib/store/resourceContext'; // services const appStateService = AppStateService.instance; @@ -135,8 +136,11 @@ } hideIsLoading(); })(); + + // cleanup resource context when leaving page return () => { tableURLParams.unsubscribe(); + resourceContext.clear(); }; }); @@ -403,14 +407,13 @@ const closeModal = () => { isModalVisible = false; - form?.reset(); formValues = { id: null, templateType: 'Email', name: null, domain: null, landingPage: null, - landingPageType: 'page', + landingPageType: 'page', // 'page' or 'proxy' beforeLandingPage: null, afterLandingPage: null, afterLandingPageRedirectURL: '', @@ -421,6 +424,10 @@ stateIdentifier: 'session', urlPath: '' }; + modalError = ''; + showAdvancedOptions = false; + // clear resource context when closing modal + resourceContext.clear(); }; /** @param {string} id */ @@ -432,10 +439,26 @@ const r = globalButtonDisabledAttributes(template, contextCompanyID); if (r.disabled) { hideIsLoading(); + resourceContext.clear(); return; } assignTemplate(template); + + // if company exists but name is missing, fetch it + if (template.companyID && !template.company?.name) { + try { + const companyRes = await api.company.getByID(template.companyID); + if (companyRes.success && companyRes.data) { + template.company = companyRes.data; + } + } catch (e) { + console.error('failed to load company details', e); + } + } + + // set resource context for banner + resourceContext.setResource('template', template.companyID, template.company?.name); isModalVisible = true; } catch (e) { addToast('Failed to load campaign template', 'Error'); @@ -452,6 +475,21 @@ const template = await getTemplate(id); assignTemplate(template); formValues.id = null; + + // if company exists but name is missing, fetch it + if (template.companyID && !template.company?.name) { + try { + const companyRes = await api.company.getByID(template.companyID); + if (companyRes.success && companyRes.data) { + template.company = companyRes.data; + } + } catch (e) { + console.error('failed to load company details', e); + } + } + + // set resource context for banner + resourceContext.setResource('template', template.companyID, template.company?.name); isModalVisible = true; } catch (e) { addToast('Failed to load campaign template', 'Error'); @@ -513,8 +551,9 @@ +
- Campaign templates + Campaigns templates New template { await refresh(); recipientTableUrlParams.onChange(refreshRecipients); @@ -176,9 +180,12 @@ await refreshRecipientsTimes(); await refreshCampaignEventsSince(); })(); + + // cleanup resource context when leaving page return () => { recipientTableUrlParams.unsubscribe(); eventsTableURLParams.unsubscribe(); + resourceContext.clear(); }; }); @@ -240,6 +247,21 @@ campaign.denyPage = t.denyPage; campaign.evasionPage = t.evasionPage; campaign.webhookID = t.webhookID; + campaign.companyID = t.companyID; + campaign.company = t.company; + + // if company exists but name is missing, fetch it + if (campaign.companyID && !campaign.company?.name) { + try { + const companyRes = await api.company.getByID(campaign.companyID); + if (companyRes.success && companyRes.data) { + campaign.company = companyRes.data; + } + } catch (e) { + console.error('failed to load company details', e); + } + } + // fetch the full template object if (t.templateID) { const templateRes = await api.campaignTemplate.getByID(t.templateID, true); @@ -255,6 +277,9 @@ if (campaign.allowDeny && campaign.allowDeny[0]) { allowedFilter = campaign.allowDeny[0].allowed; } + + // set resource context for banner + resourceContext.setResource('campaign', campaign.companyID, campaign.company?.name); } catch (e) { addToast('Failed to load campaign', 'Error'); console.error('failed to load campaign', e);