{"version":3,"sources":["libs/campaign/src/lib/dependencies/src/tokens.ts","libs/campaign/src/lib/dependencies/src/config.ts","libs/campaign/src/lib/dependencies/src/legacy.ts","libs/campaign/src/lib/dependencies/src/index.ts","libs/campaign/src/lib/assets/i18n/en_devel.json","libs/campaign/src/lib/assets/i18n/campaign-i18n.module.ts","libs/campaign/src/lib/campaign-generate-dialog/controlErrorStateMatcher.ts","libs/campaign/src/lib/campaign-generate-dialog/campaign-generate-dialog.component.ts","libs/campaign/src/lib/campaign-generate-dialog/campaign-generate-dialog.component.html","libs/campaign/src/lib/campaign-list-page/my-campaigns-table.service.ts","libs/campaign/src/lib/dependencies/index.ts","node_modules/fuse.js/dist/fuse.esm.js","libs/campaign/src/lib/tags/sender-tags.service.ts","libs/campaign/src/lib/tags/add-new-tag-dialog.component.ts","libs/campaign/src/lib/tags/add-new-tag-dialog.component.html","libs/campaign/src/lib/manage-tags/tag-toggle/tag-toggle.component.ts","libs/campaign/src/lib/manage-tags/tag-toggle/tag-toggle.component.html","libs/campaign/src/lib/manage-tags/manage-tags.component.ts","libs/campaign/src/lib/manage-tags/manage-tags.component.html","libs/campaign/src/lib/campaign-list-page/my-campaigns-table-filter.service.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-duplicate-dialog/campaign-duplicate-dialog.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-duplicate-dialog/campaign-duplicate-dialog.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-duplicate-success-dialog/campaign-duplicate-success-dialog.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-duplicate-success-dialog/campaign-duplicate-success-dialog.component.html","libs/galaxy/provider-default-overrides/src/provider-default-overrides.ts","libs/galaxy/provider-default-overrides/public_api.ts","libs/galaxy/provider-default-overrides/index.ts","libs/campaign/src/lib/campaign-preview-dialog/campaign-preview-data-selector.directive.ts","libs/campaign/src/lib/campaign-preview-dialog/event-day.pipe.ts","libs/campaign/src/lib/campaign-preview-dialog/non-email-steps.ts","libs/campaign/src/lib/campaign-preview-dialog/campaign-preview-dialog.component.ts","libs/campaign/src/lib/campaign-preview-dialog/campaign-preview-dialog.component.html","libs/campaign/src/lib/campaign-list-page/column-components/my-campaigns-column.directive.ts","libs/campaign/src/lib/campaign-list-page/column-components/my-campaigns-actions-column.component.ts","libs/campaign/src/lib/campaign-list-page/column-components/my-campaigns-actions-column.component.html","libs/campaign/src/lib/campaign-list-page/column-components/my-campaigns-active-recipients-column.component.ts","libs/campaign/src/lib/campaign-list-page/column-components/my-campaigns-emails-delivered-column.component.ts","libs/campaign/src/lib/campaign-list-page/column-components/my-campaigns-name-column.component.ts","libs/campaign/src/lib/campaign-list-page/column-components/my-campaigns-open-rate-column.component.ts","libs/campaign/src/lib/campaign-list-page/column-components/my-campaigns-status-column.component.ts","libs/campaign/src/lib/campaign-list-page/column-components/my-campaigns-total-recipients-column.component.ts","libs/campaign/src/lib/campaign-list-page/my-campaigns-table-model.ts","libs/campaign/src/lib/tags/tags.module.ts","libs/campaign/src/lib/campaign-list-page/column-components/tags-column/tags-column.module.ts","libs/campaign/src/lib/manage-tags/manage-tags.module.ts","libs/campaign/src/lib/sms-configuration-alert/sms-configuration-alert.component.ts","libs/campaign/src/lib/sms-configuration-alert/sms-configuration-alert.component.html","libs/campaign/src/lib/campaign-list-page/campaign-list-page.component.ts","libs/campaign/src/lib/campaign-list-page/campaign-list-page.component.html","libs/campaign/src/lib/quota-banner/quota-banner.component.ts","libs/campaign/src/lib/quota-banner/quota-banner.component.html","libs/campaign/src/lib/campaign-dashboard/campaign-dashboard.component.ts","libs/campaign/src/lib/campaign-dashboard/campaign-dashboard.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/interface.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/marketing-automation.service.ts","libs/galaxy/side-drawer/src/dynamic-open-close-template-ref.service.ts","libs/galaxy/side-drawer/src/closing-side-drawer/closing-side-drawer.component.ts","libs/galaxy/side-drawer/src/closing-side-drawer/closing-side-drawer.component.html","libs/galaxy/side-drawer/src/side-drawer-container/side-drawer-container.component.ts","libs/galaxy/side-drawer/src/side-drawer-container/side-drawer-container.component.html","libs/galaxy/side-drawer/src/side-drawer.module.ts","libs/galaxy/side-drawer/public_api.ts","libs/galaxy/side-drawer/index.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/upsell-sms-dialog/upsell-sms-dialog.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/upsell-sms-dialog/upsell-sms-dialog.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/add-actions/add-actions.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/add-actions/add-actions.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-options/campaign-options.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-options/campaign-options.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-state/campaign-state.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-state/campaign-state.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/utils.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-email-stats.service.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-stats/recipient_campaign_stats.service.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-stats/campaign-stats.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-stats/campaign-stats.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-sms-stats.service.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-stats.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-step-details-preview/campaign-step-details-preview.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-step-details-preview/campaign-step-details-preview.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-step-duplicate-dialog/campaign-step-duplicate-dialog.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-step-duplicate-dialog/campaign-step-duplicate-dialog.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-step-options/campaign-step-options.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-step-options/campaign-step-options.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-step/time-convertor.service.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-step/campaign-step.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-step/campaign-step.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/scheduling/scheduling.service.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/scheduling/scheduling.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/scheduling/scheduling.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-details.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-details.component.html","libs/campaign/src/lib/campaign-details-page/campaign-details-page.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-contacts-dialog/campaign-contacts-data-service.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-contacts-dialog/list-item.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-contacts-dialog/campaign-contacts-dialog.component.ts","libs/campaign/src/lib/campaign-details-page/campaign-details/campaign-contacts-dialog/campaign-contacts-dialog.component.html","libs/campaign/src/lib/campaign-history/history-table/history-table.service.ts","libs/campaign/src/lib/pipes/event-name.pipe.ts","libs/campaign/src/lib/pipes/pipes.module.ts","libs/campaign/src/lib/campaign-history/history-table/history-table-filters.ts","libs/campaign/src/lib/campaign-history/history-table/history-table.component.ts","libs/campaign/src/lib/campaign-history/history-table/history-table.component.html","libs/campaign/src/lib/campaign-history/campaign-history.component.ts","libs/campaign/src/lib/campaign-history/campaign-history.component.html","libs/campaign/src/lib/core/vbc-campaign-apis.ts","libs/campaign/src/lib/campaigns-card/recipient-campaigns-ui.model.ts","libs/campaign/src/lib/core/campaigns-legacy-apis.ts","libs/campaign/src/lib/campaigns-card/campaigns-card.ui-service.ts","libs/campaign/src/lib/core/campaigns-apis.ts","libs/campaign/shared/src/campaign-preview-send/campaign-selector/campaign-selector.service.ts","libs/campaign/shared/src/campaign-preview-send/campaign-selector/campaign-selector.component.ts","libs/campaign/shared/src/campaign-preview-send/campaign-selector/campaign-selector.component.html","libs/campaign/shared/src/campaign-previewer.service.ts","libs/campaign/shared/src/campaign-preview-send/campaign-sender-settings/campaign-sender-settings.component.ts","libs/campaign/shared/src/campaign-preview-send/campaign-sender-settings/campaign-sender-settings.component.html","libs/campaign/shared/src/campaign-step-previewer/contact-selector-modal/contact-selector.component.ts","libs/campaign/shared/src/campaign-step-previewer/contact-selector-modal/contact-selector.component.html","libs/campaign/shared/src/campaign-step-previewer/campaign-step-selector/campaign-step-selector.component.ts","libs/campaign/shared/src/campaign-step-previewer/campaign-step-selector/campaign-step-selector.component.html","libs/campaign/shared/src/campaign-step-previewer/campaign-step-previewer.component.ts","libs/campaign/shared/src/campaign-step-previewer/campaign-step-previewer.component.html","libs/campaign/shared/src/campaign-preview-send/campaign-preview-send.component.ts","libs/campaign/shared/src/campaign-preview-send/campaign-preview-send.component.html","libs/campaign/src/lib/campaign.module.ts","libs/campaign/src/index.ts"],"sourcesContent":["import { Component, InjectionToken, Type } from '@angular/core';\nimport { CampaignConfig } from './config';\nimport { WorkStates } from '@vendasta/rx-utils/work-state';\nimport { SaveResult } from '../../campaign-email-builder/page/shared/email-template-saver';\nimport { EmailContentData, EmailPreviewHydrationData } from '@galaxy/email-ui/email-builder';\nimport { combineLatest, Observable } from 'rxjs';\nimport { AccountGroup } from '@vendasta/account-group/lib/_internal/objects/api';\nimport { map } from 'rxjs/operators';\nimport { SenderInterface } from '@vendasta/campaigns';\n\nexport const PREVIEW_DATA_TOKEN = new InjectionToken('com.vendasta.campaigns.preview.data');\nexport type PREVIEW_DATA_TYPE = Observable<{ [key: string]: string }>;\n\nexport const PREVIEW_DATA_SELECTOR_COMPONENT_TOKEN = new InjectionToken(\n 'com.vendasta.campaigns.preview.data.selector_component',\n);\nexport type PREVIEW_DATA_SELECTOR_COMPONENT_TYPE = Type;\n\nexport interface PreviewDataProvider {\n data$: PREVIEW_DATA_TYPE;\n}\n\nexport interface TemplateExporter {\n workState: WorkStates;\n\n updateContentData(update: EmailContentData): void;\n\n updateHTML(html: string): void;\n}\n\nexport function buildHydrationData(\n accountGroup$: Observable,\n campaignConfig: CampaignConfig,\n): Observable {\n return combineLatest([accountGroup$, campaignConfig.userId$]).pipe(\n map(([accountGroup, userId]: [AccountGroup, string]) => ({\n business: {\n accountGroupId: accountGroup.accountGroupId,\n name: accountGroup.napData?.companyName,\n hasSnapshot: accountGroup.snapshotReports?.snapshots?.length > 0,\n hasSalesperson: accountGroup.accountGroupExternalIdentifiers?.salesPersonId !== '',\n },\n partnerID: '',\n marketID: '',\n userID: userId,\n locale: 'en',\n useFakeData: false,\n })),\n );\n}\n\nexport const SUPPORTED_LOCALES_TOKEN = 'com.vendasta.campaigns.supported_locales';\nexport type LOCALE_DATA_TYPE = Observable<{ value: string; name: string }[]>;\n\nexport interface QuotaConfig {\n sender$: Observable;\n actionURL?: string;\n}\n\nexport type QUOTA_CONFIG = QuotaConfig;\nexport const QUOTA_BANNER_CONFIG = 'campaign.quota.banner.config';\n\nexport enum AddActionType {\n New = 'add-new',\n Existing = 'add-existing',\n Snapshot = 'add-snapshot',\n SMS = 'add-sms',\n}\n\nexport type Sender = SenderInterface;\nexport const CAMPAIGN_SENDER_TOKEN = 'campaign.sender';\n","export {};\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IE9ic2VydmFibGUgfSBmcm9tICdyeGpzJztcbmltcG9ydCB7IFNlbmRlckludGVyZmFjZSB9IGZyb20gJ0B2ZW5kYXN0YS9jYW1wYWlnbnMnO1xuXG5leHBvcnQgaW50ZXJmYWNlIENhbXBhaWduQ29uZmlnIHtcbiAgLy8gVGhlIChyZWxhdGl2ZSkgcHJlZml4IHRvIGluY2x1ZGUgaW4gVVJMcyBmb3Igcm91dGluZy4gRS5nLiBpZiB0aGVcbiAgLy8gY29tcGxldGUgVVJMIGlzICdodHRwczovL2V4YW1wbGUuY29tL3NpY2svY2FtcGFpZ256L0NBTVBBSUdOLTEyMy9kZXRhaWxzJ1xuICAvLyB0aGVuIHRoZSBiYXNlUGF0aCBpcyAnc2ljay9jYW1wYWlnbnonIGJlY2F1c2UgdGhlIHJlbWFpbmRlciBvZiB0aGUgcGF0aFxuICAvLyB3YXMgY29uc3RydWN0ZWQgYnkgdGhpcyBsaWIuICBTZWUgUEFHRV9ST1VURVMuXG4gIGJhc2VQYXRoJDogT2JzZXJ2YWJsZTxzdHJpbmc+O1xuICAvLyBUaGUgb3duZXIgb2YgdGhlIGNhbXBhaWducy5cbiAgc2VuZGVyJDogT2JzZXJ2YWJsZTxTZW5kZXJJbnRlcmZhY2U+O1xuICAvLyBUaGUgSUQgb2YgdGhlIHVzZXIgd2hvIGlzIGN1cnJlbnRseSB1c2luZyB0aGUgYXBwXG4gIHVzZXJJZCQ6IE9ic2VydmFibGU8c3RyaW5nPjtcbiAgLy8gVGhlIGxvY2F0aW9uIG9mIHRoZSBhY2NvdW50IGdyb3VwXG4gIGxvY2F0aW9uJDogT2JzZXJ2YWJsZTxzdHJpbmc+O1xuICAvLyBUaGUgVVJMIHRvIGdldCB0byBDYW1wYWlnbnMgUHJvIC8gU01TIGFkZG9ucyBpbiB0aGUgTWFya2V0cGxhY2VcbiAgcHJvZHVjdEJhc2VQYXRoJDogT2JzZXJ2YWJsZTxzdHJpbmc+O1xuICAvLyBUaGUgVVJMIHRvIGZpbmQgU01TIHNldHRpbmdzIGF0XG4gIHNtc1NldHRpbmdzUGF0aCQ/OiBPYnNlcnZhYmxlPHN0cmluZz47XG59XG4iXSwibWFwcGluZ3MiOiIiLCJpZ25vcmVMaXN0IjpbXX0=","import { Observable, map } from 'rxjs';\nimport { SenderInterface, SenderType } from '@vendasta/campaigns';\n\nexport function SenderTypeFromBusinessId(businessId$: Observable): Observable {\n return businessId$.pipe(\n map((v) => ({\n id: v,\n type: SenderType.SENDER_TYPE_BUSINESS,\n })),\n );\n}\n\nexport function SenderTypeFromPartnerId(partnerId: Observable): Observable {\n return partnerId.pipe(\n map((v) => ({\n id: v,\n type: SenderType.SENDER_TYPE_PARTNER,\n })),\n );\n}\n\nexport type RecipientType = string;\n\nexport class RecipientTypes {\n public static readonly IAMUser: RecipientType = 'RecipientTypeIAMUser';\n public static readonly CampaignRecipient: RecipientType = 'RecipientTypeCampaignRecipient';\n}\n\nexport function stepDataForPartnerEmail(\n recipientAccountGroupId: string,\n recipientType: RecipientType,\n recipientId: string,\n): { [key: string]: string } {\n return {\n account_group_id: recipientAccountGroupId,\n recipient_external_type: recipientType,\n recipient_external_id: recipientId,\n };\n}\n\nexport function stepDataForBusinessEmail(senderAccountGroupId: string): { [key: string]: string } {\n return {\n account_group_id: senderAccountGroupId,\n };\n}\n","export * from './config';\nexport * from './legacy';\nexport * from './tokens';\n","{\n \"CONTACT_TABLE\": {\n \"TABLE_HEADERS\": {\n \"NAME\": \"Name\",\n \"RECIPIENT_EMAIL\": \" Recipient Email\",\n \"CLICKS\": \"Clicks\",\n \"DATE_CLICKED\": \"Date Clicked\"\n },\n \"EMPTY_STATE\": {\n \"DESCRIPTION\": \"There is no link activity to display\"\n },\n \"DOWNLOAD_DIALOG\": {\n \"TITLE\": \"Download link activity\",\n \"DESCRIPTION\": \"This action will download the data that's been filtered on your screen.\",\n \"NOTIFICATION\": \"You'll receive a notification when the file is ready to download. You can access the file by clicking that notification.\",\n \"DOWNLOAD\": \"Download\",\n \"SUCCESS\": \"Generating CSV file. You will be notified when it is ready. \\uD83D\\uDD14\",\n \"ERROR\": \"There was a problem downloading data. Please try again.\"\n }\n },\n \"EMAIL_STATS\": {\n \"CLICK_RATIO\": \"Click Ratio\",\n \"LINK_ACTIVITY\": \"Link Activity\",\n \"TOTAL_CLICKS\": \"Total Clicks\",\n \"UNIQUE_CLICKS\": \"Unique Clicks\",\n \"SEE_WHO_CLICKED\": \"See who clicked\",\n \"LINK_CLICK_RATE_TOOLTIP\": \"Percent of email recipients that clicked on this link.\"\n },\n \"CAMPAIGNS\": {\n \"TITLE\": \"Campaigns\",\n \"BETA\": \"BETA\",\n \"NO_CAMPAIGNS\": \"No email campaigns have been started for this account\",\n \"CAMPAIGN_ACTIVE_TOOLTIP\": \"Campaign is active\",\n \"CAMPAIGN_PAUSED_TOOLTIP\": \"Campaign is paused\",\n \"CAMPAIGN_ENDED_TOOLTIP\": \"Campaign has ended\",\n \"CAMPAIGN_CREATING_TOOLTIP\": \"Campaign creation takes a moment. Please check back again later.\",\n \"FAILED_STATUS_UPDATE\": \"Failed to update status. Click to try again.\",\n \"CREATE_CAMPAIGN\": \"Create Campaign\",\n \"CAMPAIGN_NAME\": \"Campaign Name\",\n \"NAME\": {\n \"CREATE\": \"Enter a name to create a campaign\",\n \"UPDATE_SUCCESS\": \"Campaign name updated\",\n \"UPDATE_FAILURE\": \"The campaign name failed to update. Please try again.\",\n \"TOO_LONG\": \"The campaign name can't exceed 100 characters\",\n \"TOO_SHORT\": \"Your campaign needs to be named\"\n },\n \"CONTACTS\": {\n \"SELECT_CAMPAIGN_RECIPIENTS\": \"Select campaign recipients\",\n \"MODAL\": {\n \"SELECT_ALL_SUCCESS\": \"{{ totalObjects }} contacts added to campaign: {{ campaignTitle }}\",\n \"ID\": \"ID\",\n \"NAME\": \"Name\",\n \"EMAIL\": \"Email\",\n \"NO_CONTACTS\": \"No contacts found\",\n \"NEXT\": \"Next\",\n \"BACK\": \"Back\",\n \"SCHEDULE_OR_START\": \"Schedule or start campaign\",\n \"SEND_NOW\": \"Now\",\n \"SEND_TIME\": \"Campaign will start for the selected recipients on\",\n \"SCHEDULE_LATER\": \"Later\",\n \"SCHEDULE_LATER_LABEL\": \"Campaign will start for the selected recipients at a scheduled time\",\n \"PICK_DATE\": \"Pick a date\",\n \"PICK_TIME\": \"Pick a time\",\n \"START_CAMPAIGN\": \"Start campaign\",\n \"CANCEL\": \"Cancel\",\n \"NUM_RECIPIENTS\": \"Recipients selected ( Max 20 )\",\n \"ERRORS\": {\n \"GENERIC_ERROR\": \"There was a problem starting the campaign. Please try again.\",\n \"SELECT_DATE\": \"Please select a date.\",\n \"SELECT_TIME\": \"Please select a time.\",\n \"SELECT_CONTACTS\": \"Please select at least one contact.\",\n \"MAX_CONTACTS\": \"You can only select up to 20 contacts.\",\n \"FAILED_ADDRESS_ERROR\": \"Failed to add the following addresses: \",\n \"FAILED_CONTACT_ERROR\": \"Failed to add 1 contact. Please ensure that the selected recipient has the required information for each step in the campaign.\",\n \"FAILED_CONTACTS_COUNT_ERROR\": \"Failed to add {{ count }} contacts. Please ensure that the selected recipients have the required information for each step in the campaign.\",\n \"FAILED_CAMPAIGN_ALERT_TITLE\": \"Some contacts cannot be added to this campaign\",\n \"FAILED_CAMPAIGN_ALERT_MESSAGE\": \"Some contacts cannot be added to this campaign. Please try again.\",\n \"FAILED_ADDRESS_ERROR_EMAIL\": \"Contacts must have an email address to receive campaigns with email steps.\",\n \"FAILED_ADDRESS_ERROR_SMS\": \"Contacts must have a phone number to receive campaigns with SMS steps.\"\n },\n \"SUCCESS\": \"Success\"\n }\n }\n },\n \"CAMPAIGN_GENERIC\": {\n \"TEST_EMAIL\": {\n \"TITLE\": \"Preview of {{ name }}\",\n \"SUBJECT\": \"Subject:\",\n \"COUNT\": \"Event {{ curr }} of {{ total }}\",\n \"NO_EVENTS\": \"No events to preview.\",\n \"SEND_EMAIL\": \"Send test email\",\n \"SEND_TEST_EMAIL_TO_MYSELF\": \"Send test email to myself\",\n \"SEND_TEST_LABEL\": \"Send emails to (space or comma separated, up to 5 entries)\",\n \"SEND_TEST_PLACEHOLDER\": \"user@example.com admin@example.com\",\n \"SEND_TEST_HINT\": \"To preview this email as a specific account, return to the main campaign page.\",\n \"SUCCESS\": \"Test email sent.\",\n \"FAILURE\": \"There was a problem sending the test email: {{ message }}\",\n \"PREVIEW_AS\": \"Preview email as\",\n \"GENERIC_ACCOUNT\": \"Generic account\"\n }\n },\n \"TABLE\": {\n \"COLUMNS\": {\n \"TAGS\": {\n \"ADD_TAG_BUTTON_TEXT\": \"Add tag\"\n }\n }\n },\n \"MANAGE_TAGS\": {\n \"MANAGE_TAGS\": \"Manage Tags\",\n \"NEW_TAG\": \"New Tag\",\n \"CLOSE\": \"Close\",\n \"ADD_NEW\": \"Add New Tag\",\n \"NAME_OF_NEW_TAG\": \"Name of New Tag\",\n \"CANCEL\": \"Cancel\",\n \"TAG_ADD_SUCCESS\": \"Tag added\",\n \"TAG_ADD_FAILURE\": \"Failed to add tag\",\n \"TAG_CREATE_SUCCESS\": \"Tag created\",\n \"TAG_CREATE_FAILURE\": \"Failed to create new tag\",\n \"SAVE\": \"Save\",\n \"TAG_NAME\": \"Tag name\",\n \"SEARCH_FOR_TAG\": \"Search for a tag\",\n \"BACK_COMPAT_POPOVER_HINT\": \"Read only\",\n \"TAG_DELETE_SUCCESS\": \"Tag deleted\",\n \"TAG_DELETE_FAILURE\": \"Failed to delete tag\"\n },\n \"COMMON\": {\n \"ACTION_LABELS\": {\n \"VIEW_MORE\": \"View more\",\n \"VIEW_LESS\": \"View less\",\n \"PAUSE\": \"Pause\",\n \"RESUME\": \"Resume\",\n \"NA\": \"N/A\",\n \"ACTIONS\": \"Actions\",\n \"PUBLISH\": \"Publish\",\n \"UNPUBLISH\": \"Unpublish\",\n \"ADD_EMAIL\": \"Add email\",\n \"COPY\": \"Copy\",\n \"ARCHIVE\": \"Archive\",\n \"DELETE\": \"Delete\",\n \"ADD_ACCOUNT_LIST_TO_CAMPAIGN\": \"Add account list to campaign\",\n \"CREATE\": \"Create\",\n \"CANCEL\": \"Cancel\",\n \"ADD_RECIPIENTS\": \"Add recipients\",\n \"VIEW_HISTORY\": \"View history\",\n \"LEAVE\": \"Leave\"\n }\n },\n \"PREVIEW\": {\n \"ACTION\": \"Preview\",\n \"PREVIOUS\": \"Previous\",\n \"COUNT\": \"Event {{ curr }} of {{ total }}\",\n \"NEXT\": \"Next\"\n },\n \"CAMPAIGN_STATUS\": {\n \"PAUSED\": \"Paused\",\n \"DRAFT\": \"Draft\",\n \"PUBLISHED\": \"Published\",\n \"ARCHIVED\": \"Archived\",\n \"ONGOING\": \"Ongoing\",\n \"COMPLETED\": \"Completed\"\n },\n \"CAMPAIGN_FOCUS\": {\n \"ACQUIRE\": \"Acquire campaign\",\n \"ADOPT\": \"Adoption campaign\",\n \"UPSELL\": \"Upsell campaign\",\n \"UPDATE_SUCCESS\": \"Campaign focus updated\",\n \"UPDATE_FAILURE\": \"The campaign focus wasn't updated. Please try again\"\n },\n \"EMAIL_LIBRARY\": {\n \"TITLE\": \"Emails\",\n \"ADD_EMAILS\": \"Add emails\",\n \"FEATURED_TEMPLATES\": \"Featured templates\",\n \"MY_TEMPLATES\": \"My templates\",\n \"CARD\": {\n \"CREATED\": \"Created\",\n \"UPDATED\": \"Updated\"\n },\n \"NO_RESULTS\": \"No results found\",\n \"SEARCH_PLACEHOLDER\": \"Search for an email\",\n \"TOTAL_RESULTS_PLURAL\": \"{{ totalResults }} emails\",\n \"TOTAL_RESULTS_SINGULAR\": \"{{ totalResults }} email\"\n },\n \"CAMPAIGN_DETAILS\": {\n \"TITLE\": \"Campaign Details\",\n \"DRAFT_MODE\": \"Draft mode\",\n \"DRAFT_WARNING\": \"You're editing a draft of this campaign. When you're ready to start using it, click 'Publish'.\",\n \"ADD_EMAIL\": \"Add Email\",\n \"TEMPLATE\": {\n \"CREATE_NEW\": \"Add new email\",\n \"ADD_EXISTING\": \"Add existing email\",\n \"ADD_SMS\": \"Add SMS\",\n \"ADD_SNAPSHOT_REPORT\": \"Add snapshot report\",\n \"EDIT_SMS\": \"Edit SMS\"\n },\n \"ACTIONS\": {\n \"COPY\": \"Copy\",\n \"ARCHIVE_SUCCESS\": \"Campaign archived\",\n \"ARCHIVE_FAILURE\": \"Failed to archive campaign\",\n \"COPY_DIALOG\": {\n \"PLEASE_WAIT\": \"Please wait while a copy of this campaign is created...\",\n \"LONG_WAIT\": \"This is taking longer than usual, but the duplication is still running. Please wait...\",\n \"ERROR\": \"There was a problem making a copy of the campaign.\",\n \"TRY_AGAIN\": \"Try Again\"\n },\n \"COPY_DIALOG_SUCCESS\": {\n \"SUCCESS_TITLE\": \"Campaign copied\",\n \"OPEN_COPIED_CAMPAIGN\": \"Would you like to open the copied campaign?\",\n \"OPEN_BUTTON\": \"Go to copied campaign\",\n \"CLOSE\": \"Close\"\n }\n },\n \"UPDATE\": {\n \"SUCCESS\": \"Updated campaign\",\n \"FAILURE\": \"We couldn't update the campaign. Please try again.\",\n \"ERROR\": \"Add one or more steps to the campaign\"\n },\n \"CONFIRMATION_DIALOG\": {\n \"ARCHIVE\": {\n \"TITLE\": \"Archive {{ title }}?\",\n \"MESSAGE\": \"You won't be able to use or edit this campaign, and you can't undo this action. The recipients who have been added to this campaign will still receive the remaining emails.\",\n \"CONFIRM\": \"Archive campaign\",\n \"DECLINE\": \"Cancel\"\n },\n \"DELETE_STEP\": {\n \"EVENT\": {\n \"TITLE\": \"Delete {{ name }} event?\",\n \"MESSAGE\": \"Are you sure you want to delete the \\\"{{ name }}\\\" event? This action can't be undone.\"\n },\n \"CONFIRM\": \"Delete event\",\n \"DECLINE\": \"Keep event\"\n },\n \"EDIT\": {\n \"TITLE\": \"Unsaved Changes\",\n \"MESSAGE\": \"You have unsaved changes. Are you sure you want to leave this page?\"\n }\n },\n \"STEP\": {\n \"DAY\": \"Day\",\n \"DELAY\": \"Delay\",\n \"WAIT\": \"Wait\",\n \"PREVIOUS_EVENT\": \"after previous event\",\n \"FIRST_EVENT\": \"before starting\",\n \"SUBJECT\": \"Subject:\",\n \"CONTENT\": \"Content:\",\n \"EDIT\": \"Edit email\",\n \"EDIT_SMS\": \"Edit SMS\",\n \"PREVIEW\": \"Preview\",\n \"SMS_PREVIEW\": \"SMS Preview\",\n \"EMAIL_PREVIEW\": \"Email Preview\",\n \"EMAIL_PREVIEW_ERRORS\": \"We couldn't update the preview due to the following errors:\",\n \"EMAIL_PERFORMANCE\": \"Email Performance\",\n \"SMS_PERFORMANCE\": \"SMS Performance\",\n \"MORE_ACTIONS\": \"More actions\",\n \"DETAILS\": \"Details\",\n \"DELETE\": \"Delete\",\n \"NAME\": \"Name:\",\n \"MAKE_A_COPY\": \"Make a copy\",\n \"VIEW_EMAIL_ACTIVITY\": \"View email activity\",\n \"RENDERING_FAKE_DATA\": \"This preview contains some placeholder content\"\n },\n \"PAUSE\": {\n \"ACTION\": \"Pause\",\n \"TITLE\": \"Pause {{ title }}?\",\n \"MESSAGE\": \"Scheduled campaign events will be paused for all recipients currently on this campaign. This action may take a few minutes to complete.\",\n \"CONFIRM\": \"Pause campaign\",\n \"DECLINE\": \"Cancel\",\n \"SUCCESS\": \"Campaign paused\",\n \"FAILURE\": \"There was a problem pausing the campaign. Refresh the page and try again.\"\n },\n \"RESUME\": {\n \"ACTION\": \"Resume\",\n \"TITLE\": \"Resume {{ title }} for paused recipients?\",\n \"MESSAGE\": \"The campaign will resume as scheduled, and the recipients in the 'Stopped' state will move to 'In progress'. Recipients won’t receive emails they’ve already received, and delays before scheduled events will restart. This action may take a few minutes to complete.\",\n \"CONFIRM\": \"Resume campaign\",\n \"DECLINE\": \"Cancel\",\n \"SUCCESS\": \"Campaign resumed\",\n \"FAILURE\": \"There was a problem resuming the campaign. Refresh the page and try again.\"\n },\n \"DELETE\": {\n \"TITLE\": \"Delete draft?\",\n \"MESSAGE\": \"This campaign will be permanently deleted.\",\n \"ACTION\": \"Delete\"\n },\n \"EMPTY_STATE\": {\n \"TITLE\": \"Add steps to your campaign\",\n \"DESCRIPTION_1\": \"You can add a single step, or multiple steps with delays in between sending.\",\n \"DESCRIPTION_2\": \"Once you’re done, publish your campaign to start adding recipients.\"\n }\n },\n \"CAMPAIGN_OPTIONS\": {\n \"DAYS\": {\n \"MONDAY\": \"Mon\",\n \"TUESDAY\": \"Tue\",\n \"WEDNESDAY\": \"Wed\",\n \"THURSDAY\": \"Thu\",\n \"FRIDAY\": \"Fri\",\n \"SATURDAY\": \"Sat\",\n \"SUNDAY\": \"Sun\"\n },\n \"CONFIG\": {\n \"TITLE\": \"Campaign configuration\",\n \"SUBTITLE\": \"Configure sending days and timezone\",\n \"SUCCESS\": \"Campaign scheduling saved\",\n \"FAILURE\": \"There was an error saving the campaign scheduling. Please try again.\",\n \"TIMEZONE_FAILURE\": \"Select a valid timezone from the scheduling dropdown.\"\n },\n \"SCHEDULING\": {\n \"TITLE\": \"Scheduling\",\n \"TIME_ZONE\": {\n \"DESCRIPTION\": \"Select 'campaign sent' time zone\",\n \"HINT\": \"This setting affects the time that your content will be sent\"\n },\n \"INCLUDED_DAYS\": {\n \"TITLE\": \"Included days\",\n \"WARNING\": \"No days selected, campaign will not be sent out\",\n \"HINT\": \"Emails will only be sent on the selected days above.\\nNote: If a timed campaign event lands on a day that isn’t included, it will be sent on the next included day.\"\n }\n }\n },\n \"EMAIL_EDITOR_PREVIEW\": {\n \"DESKTOP\": \"Desktop\",\n \"TABLET\": \"Tablet\",\n \"MOBILE\": \"Mobile\",\n \"CREATE_NEW\": \"Create new email\",\n \"ADD_EXISTING\": \"Add existing email\"\n },\n \"CAMPAIGN_PREVIEW\": {\n \"SEND_SUCCESS\": \"Preview email sent successfully\",\n \"SEND_FAILED\": \"Failed to send preview email. Try selecting another contact.\"\n },\n \"STATS\": {\n \"QUEUED\": \"Queued\",\n \"FAILED\": \"Failed\",\n \"SENT\": \"Sent\",\n \"UNDELIVERED\": \"Undelivered\",\n \"DELIVERED\": \"Delivered\",\n \"PENDING\": \"Pending\",\n \"OPEN_RATE\": \"Open rate\",\n \"CLICK_RATE\": \"Click rate\",\n \"OPENED\": \"Opened\",\n \"CLICKS\": \"Clicks\",\n \"DELIVERY_STATUS\": \"Delivery status\",\n \"BOUNCED\": \"Bounced\",\n \"DROPPED\": \"Dropped\",\n \"UNSUBSCRIBED\": \"Unsubscribed\",\n \"SPAM\": \"Spam\",\n \"SMS\": {\n \"QUEUED_TOOLTIP_SINGLE\": \"1 sms is queued for delivery within the specified date range\",\n \"QUEUED_TOOLTIP\": \"{{ queued }} SMSs are queued for delivery within the specified date range\",\n \"SENT_TOOLTIP_SINGLE\": \"1 sms was sent within the specified date range\",\n \"SENT_TOOLTIP\": \"{{ sent }} SMSs sent within the specified date range\",\n \"FAILED_TOOLTIP_SINGLE\": \"1 sms failed within the specified date range\",\n \"FAILED_TOOLTIP\": \"{{ failed }} SMSs failed within the specified date range\",\n \"UNDELIVERED_TOOLTIP_SINGLE\": \"1 sms was undelivered within the specified date range\",\n \"UNDELIVERED_TOOLTIP\": \"{{ undelivered }} SMSs undelivered within the specified date range\"\n },\n \"EMAIL\": {\n \"DELIVERED_TOOLTIP_SINGLE\": \"1 email has been delivered within the specified date range\",\n \"DELIVERED_TOOLTIP\": \"{{ delivered }} emails have been delivered within the specified date range\",\n \"PENDING_TOOLTIP_SINGLE\": \"1 email is pending delivery within the specified date range\",\n \"PENDING_TOOLTIP\": \"{{ pending }} emails are pending delivery within the specified date range\",\n \"OPEN_RATE_TOOLTIP_SINGLE\": \"1 email has been opened within the specified date range\",\n \"OPEN_RATE_TOOLTIP\": \"{{ openRate }}% of emails have been opened within the specified date range\",\n \"CLICK_RATE_TOOLTIP_SINGLE\": \"1 email has been clicked within the specified date range\",\n \"CLICK_RATE_TOOLTIP\": \"{{ clickRate }}% of emails have been clicked within the specified date range\",\n \"OPEN_TOOLTIP_SINGLE\": \"1 email has been opened within the specified date range\",\n \"OPEN_TOOLTIP\": \"{{ opens }} emails have been opened within the specified date range\",\n \"CLICK_TOOLTIP_SINGLE\": \"1 email has been clicked within the specified date range\",\n \"CLICK_TOOLTIP\": \"{{ clicks }} emails have been clicked within the specified date range\",\n \"BOUNCED_TOOLTIP_SINGLE\": \"1 email has bounced within the specified date range\",\n \"BOUNCED_TOOLTIP\": \"{{ bounces }} emails have bounced within the specified date range\",\n \"DROPPED_TOOLTIP_SINGLE\": \"1 email has been dropped within the specified date range\",\n \"DROPPED_TOOLTIP\": \"{{ drops }} emails have been dropped within the specified date range\",\n \"UNSUBSCRIBED_TOOLTIP_SINGLE\": \"1 email has been unsubscribed within the specified date range\",\n \"UNSUBSCRIBED_TOOLTIP\": \"{{ unsubscribes }} emails have been unsubscribed within the specified date range\",\n \"SPAM_TOOLTIP_SINGLE\": \"1 email has been marked as spam within the specified date range\",\n \"SPAM_TOOLTIP\": \"{{ spamReport }} emails have been marked as spam within the specified date range\"\n },\n \"RECIPIENT_STATS\": {\n \"TITLE\": \"Campaign status by recipient\",\n \"RECIPIENTS\": \"Recipients\",\n \"RECIPIENTS_TOOLTIP_SINGLE\": \"1 email recipient has received an email in this campaign within the specified date range\",\n \"RECIPIENTS_TOOLTIP\": \"{{ recipients }} email recipients have received an email in this campaign within the specified date range\",\n \"BUFFERING\": \"Buffering\",\n \"BUFFERING_TOOLTIP_SINGLE\": \"1 recipient hasn’t received the first email in this campaign yet, due to the specified daily limit. This number is not applicable when selecting a specific date range.\",\n \"BUFFERING_TOOLTIP\": \"{{ buffering }} recipients haven’t received the first email in this campaign yet, due to the specified daily limit. This number is not applicable when selecting a specific date range.\",\n \"IN_PROGRESS\": \"In progress\",\n \"IN_PROGRESS_TOOLTIP_SINGLE\": \"1 recipient has started but not completed this campaign. This number is not applicable when selecting a specific date range.\",\n \"IN_PROGRESS_TOOLTIP\": \"{{ inProgress }} recipients have started but not completed this campaign. This number is not applicable when selecting a specific date range.\",\n \"STOPPED\": \"Stopped\",\n \"STOPPED_TOOLTIP_SINGLE\": \"1 recipient has been paused or stopped before completing this campaign. This number is not applicable when selecting a specific date range.\",\n \"STOPPED_TOOLTIP\": \"{{ stopped }} recipients have been paused or stopped before completing this campaign. This number is not applicable when selecting a specific date range.\",\n \"COMPLETED\": \"Completed\",\n \"COMPLETED_TOOLTIP_SINGLE\": \"1 recipient has received all the emails in this campaign. This number is not applicable when selecting a specific date range.\",\n \"COMPLETED_TOOLTIP\": \"{{ completed }} recipients have received all the emails in this campaign. This number is not applicable when selecting a specific date range.\"\n },\n \"PERFORMANCE_STATS\": {\n \"TITLE\": \"Campaign Performance\",\n \"TOTAL_LEADS\": \"Engaged Recipients\",\n \"TOTAL_LEADS_TOOLTIP_SINGLE\": \"1 account opened an email in this campaign.\",\n \"TOTAL_LEADS_TOOLTIP\": \"{{ totalLeads }} accounts opened an email this campaign.\",\n \"DELIVERY_RATE\": \"Delivery rate\",\n \"DELIVERY_RATE_TOOLTIP_SINGLE\": \"1 of the sent emails was delivered to accounts.\",\n \"DELIVERY_RATE_TOOLTIP\": \"{{ delivered }} of the sent emails were delivered to accounts.\",\n \"OPEN_RATE\": \"Open rate\",\n \"OPEN_RATE_TOOLTIP_SINGLE\": \"1 of the delivered emails was opened.\",\n \"OPEN_RATE_TOOLTIP\": \"{{ opened }} of the delivered emails were opened.\",\n \"CTOR\": \"CTOR\",\n \"CLICKED_TOOLTIP_SINGLE\": \"1 email was clicked through by accounts on this campaign.\",\n \"CLICKED_TOOLTIP\": \"{{ clickedThrough }} emails were clicked through by accounts on this campaign.\",\n \"CLICKED_THROUGH_TOOLTIP_SINGLE\": \"Click-to-open rate. 1 of the opened emails was clicked through.\",\n \"CLICKED_THROUGH_TOOLTIP\": \"Click-to-open rate. {{ clickedThrough }} of the opened emails were clicked through.\",\n \"UNDELIVERABLE\": \"Undeliverable\",\n \"UNDELIVERABLE_TOOLTIP_SINGLE\": \"1 of the sent emails could not be delivered to accounts.\",\n \"UNDELIVERABLE_TOOLTIP\": \"{{ undeliverable }} of the sent emails could not be delivered to accounts.\"\n }\n },\n \"STEP\": {\n \"DAY\": \"Day\",\n \"DELAY\": \"Delay\",\n \"WAIT\": \"Wait\",\n \"PREVIOUS_EVENT\": \"after previous event\",\n \"FIRST_EVENT\": \"before starting\",\n \"SUBJECT\": \"Subject:\",\n \"CONTENT\": \"Content:\",\n \"EDIT\": \"Edit email\",\n \"PREVIEW\": \"Preview\",\n \"EMAIL_PREVIEW\": \"Email Preview\",\n \"EMAIL_PREVIEW_ERRORS\": \"We couldn't update the preview due to the following errors:\",\n \"EMAIL_PERFORMANCE\": \"Email Performance\",\n \"MORE_ACTIONS\": \"More actions\",\n \"DETAILS\": \"Details\",\n \"DELETE\": \"Delete\",\n \"MAKE_A_COPY\": \"Make a copy\",\n \"VIEW_EMAIL_ACTIVITY\": \"View email activity\",\n \"RENDERING_FAKE_DATA\": \"This preview contains some placeholder content\"\n },\n \"EMAIL_BUILDER\": {\n \"PAGE\": {\n \"ACTIONS\": {\n \"SEND_PREVIEW_EMAIL\": \"Send preview email\",\n \"SAVE\": \"Save\",\n \"SAVE_SUCCESS\": \"Saved successfully\",\n \"ERROR_NOTICE\": \"There was a problem saving the campaign step\"\n },\n \"TITLE\": \"Edit email\",\n \"PREVIOUS_PAGE_TOOLTIP\": \"Back to Campaign Details\",\n \"HANDLE_RENDER_ERROR\": \"There is an error with one or more of the dynamic components. Please check for incorrect spelling or added/missing characters.\"\n }\n },\n \"SMS_BUILDER\": {\n \"PAGE\": {\n \"TITLE\": \"Edit SMS\"\n }\n },\n \"CAMPAIGN_ACTIVITY\": {\n \"TITLE\": \"Campaign Activity\",\n \"TABLE\": {\n \"HEADERS\": {\n \"RECIPIENT\": \"Recipient\",\n \"STEP_NAME\": \"Event\",\n \"EVENT_TYPE\": \"Activity\",\n \"DATE\": \"Date\"\n },\n \"EMPTY\": \"No activity yet\",\n \"FILTERS\": {\n \"TITLE\": \"Filters\",\n \"EVENT\": \"Event\",\n \"EVENT_PLACEHOLDER\": \"Select Event\",\n \"EMAIL_ACTIVITY\": \"Email Activity\",\n \"PROCESSED\": \"Processed\",\n \"DELIVERED\": \"Delivered\",\n \"OPENED\": \"Opened\",\n \"CLICKED_THROUGH\": \"Clicked Through\",\n \"BOUNCED\": \"Bounced\",\n \"SPAM_REPORT\": \"Spam Report\",\n \"UNSUBSCRIBE\": \"Unsubscribe\",\n \"DROPPED\": \"Dropped\"\n }\n }\n },\n \"QUOTA_BANNER\": {\n \"USAGE\": {\n \"BANNER_TITLE\": \"Marketing emails sent this billing period\",\n \"SMS_BANNER_TITLE\": \"Marketing SMS sent this billing period\",\n \"BANNER_CTA\": \"Upgrade\",\n \"BANNER_OF\": \"{{usage}} of {{quota}}\"\n }\n },\n \"SMS\": {\n \"TITLE\": \"SMS messages\",\n \"SUBTITLE\": \"Available as add ons\",\n \"DESCRIPTION\": \"Increase customer engagement with your marketing.\",\n \"DESCRIPTION_2\": \"Send personalized SMS messages through campaigns to keep customers engaged and further drive sales\",\n \"PURCHASE\": \"Purchase add-on\"\n },\n \"CAMPAIGN_PREVIEW_MODAL\": {\n \"TITLE\": \"Send campaign\",\n \"PREVIEWING_AS\": \"Previewing as:\",\n \"SELECT_ACTION\": \"Change\",\n \"SEND\": \"Send\",\n \"SEND_NOW\": \"Send now\",\n \"SEND_NOW_TOOLTIP\": \"Using send now will begin the campaign(s) immediately and will disregard the campaign configuration. \",\n \"SCHEDULE_SEND\": \"Schedule\",\n \"CANCEL\": \"Cancel\",\n \"CAMPAIGN_LABEL\": \"Campaign\",\n \"SEND_TO\": \"Send to\",\n \"SEND_FROM\": \"Send from\",\n \"SEARCH_CAMPAIGNS\": \"Search campaigns...\",\n \"SELECTED_RECIPIENTS\": \"{{selected}} recipients selected\",\n \"SELECT_RECIPIENTS\": \"Select recipients\",\n \"SEARCH_CONTACTS\": \"Search contacts...\",\n \"SHOW_LESS\": \"Show less\",\n \"SHOW_MORE\": \"+ {{ recipient_count }} more\",\n \"SELECTED_RECIPIENT_SINGLE\": \"1 recipient selected\",\n \"ESTIMATED_RECIPIENTS\": \"Estimated recipients: {{estimated}}/{{total}}\",\n \"SELECT_ALL_ESTIMATED_RECIPIENTS\": \"Estimated recipients: {{estimated}}\",\n \"ESTIMATED_RECIPIENTS_INFO\": \"This is the total number of contacts that have the required information for each step in a campaign: email addresses for email steps and phone numbers for SMS steps.\",\n \"DEFAULT_EMAIL_SETTINGS\": \"Default email\",\n \"REQUIRED_FIELD\": \"This field is required\",\n \"SENDER_OPTIONS\": {\n \"DEFAULT\": \"Default email\",\n \"CUSTOM\": \"Custom email\",\n \"SALESPERSON\": \"Assigned salesperson\"\n },\n \"EMAIL_SETTINGS\": {\n \"EMAIL_SETTINGS\": \"Email settings\",\n \"SENDER_NAME\": \"Sender name\",\n \"SENDER_NAME_HINT\": \"This is the from name recipients will see in their inbox\",\n \"SENDER_ADDRESS\": \"Sender address\",\n \"SENDER_ADDRESS_HINT\": \"This is the address recipients will see the email coming from \",\n \"REPLY_ADDRESS\": \"Reply address\",\n \"REPLY_ADDRESS_HINT\": \"This is where emails will be sent to if a recipient replies\"\n },\n \"ASSIGNED_SALESPERSON\": {\n \"TITLE\": \"Emails using unauthenticated domains are subject to change\",\n \"DESCRIPTION\": \"Salespeople email addresses that do not use an authenticated domain will be changed to use the verified domain (eg. yourname@mail.com will become \\u2028yourname-mail.com@verifieddomain.com).\",\n \"DESCRIPTION_2\": \"Replies will go to the unverified email.\"\n },\n \"YOUR_ASSIGNED_SALESPERSON\": \"Your assigned salesperson\",\n \"SUCCESS\": \"{{ count }} recipients added to campaign: {{ campaignTitle }}\",\n\n \"CONTACT_MODAl\": {\n \"HINT\": \"Contact\",\n \"TITLE\": \"Select contact\",\n \"CANCEL\": \"Cancel\"\n },\n \"TABS\": {\n \"SENDING_OPTIONS\": \"Sending options\",\n \"PREVIEW\": \"Preview\"\n }\n }\n}\n","import { NgModule } from '@angular/core';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { LexiconModule } from '@galaxy/lexicon';\nimport En from './en_devel.json';\n\n@NgModule({\n imports: [\n TranslateModule,\n LexiconModule.forChild({\n componentName: 'common/campaign',\n baseTranslation: En,\n }),\n ],\n})\nexport class CampaignI18nModule {}\n","import { UntypedFormControl } from '@angular/forms';\nimport { ErrorStateMatcher } from '@angular/material/core';\n\nexport class ControlErrorStateMatcher implements ErrorStateMatcher {\n isErrorState(control: UntypedFormControl | null): boolean {\n return !!(control && control.invalid && (control.dirty || control.touched));\n }\n}\n","import { Component } from '@angular/core';\nimport { ReactiveFormsModule, UntypedFormBuilder, UntypedFormControl, Validators } from '@angular/forms';\nimport { MatDialogModule, MatDialogRef } from '@angular/material/dialog';\nimport { ControlErrorStateMatcher } from './controlErrorStateMatcher';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatButtonModule } from '@angular/material/button';\nimport { CommonModule } from '@angular/common';\nimport { ProductAnalyticsService } from '@vendasta/product-analytics';\n\n@Component({\n selector: 'campaign-generate-dialog',\n templateUrl: './campaign-generate-dialog.component.html',\n styleUrls: ['./campaign-generate-dialog.component.scss'],\n imports: [\n CommonModule,\n MatDialogModule,\n MatFormFieldModule,\n MatInputModule,\n MatButtonModule,\n TranslateModule,\n ReactiveFormsModule,\n ],\n})\nexport class CampaignGenerateDialogComponent {\n campaignNameControl = new UntypedFormControl('', [Validators.required, Validators.maxLength(100)]);\n errorMatcher = new ControlErrorStateMatcher();\n formGroup = this.fb.group({\n name: [this.campaignNameControl],\n });\n\n constructor(\n public dialogRef: MatDialogRef,\n private posthogService: ProductAnalyticsService,\n private fb: UntypedFormBuilder,\n ) {}\n\n create(): void {\n this.posthogService.trackEvent('user-clicked-create-campaign-in-modal', 'create-campaign-workflow', 'click');\n this.dialogRef.close(this.campaignNameControl.value);\n }\n}\n","

\n {{ 'CAMPAIGNS.CREATE_CAMPAIGN' | translate }}\n

\n
\n \n \n \n {{ 'CAMPAIGNS.CAMPAIGN_NAME' | translate }}\n \n \n \n \n {{ 'CAMPAIGNS.NAME.CREATE' | translate }}\n \n 100\">\n {{ 'CAMPAIGNS.NAME.TOO_LONG' | translate }}\n \n \n
\n
\n
\n \n \n \n \n
\n","import { HttpClient } from '@angular/common/http';\nimport { Inject, Injectable } from '@angular/core';\nimport { TranslateService } from '@ngx-translate/core';\nimport {\n AllDates,\n CampaignService,\n CampaignState,\n CampaignStatsService,\n DateFilter,\n DeletedTagsPolicy,\n Focuses,\n GetAggregatedCampaignStatsRequestInterface,\n GetAggregatedCampaignStatsResponse,\n ListCampaignStatsForSenderRequestInterface,\n ListCampaignStatsForSenderResponse,\n ListCampaignStatsForSenderResponseInterface,\n ListCampaignStatsStructInterface,\n ListCampaignTagDetails,\n PagedResponseMetadata,\n Sender,\n SenderInterface,\n Statuses,\n} from '@vendasta/campaigns';\nimport { LoadRequest, TableDataService } from '@vendasta/va-filter2-table';\nimport { BehaviorSubject, Observable, Subject, combineLatest, merge } from 'rxjs';\nimport { distinctUntilChanged, map, scan, shareReplay, switchMap, take, tap, withLatestFrom } from 'rxjs/operators';\nimport { TagChangesListener } from '../manage-tags/manage-tags.component';\nimport { TagData } from '../manage-tags/tag-toggle/tag-toggle.component';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../shared/src/tokens';\n\nexport interface CampaignStatsSource {\n listStatsForSender(data: ListCampaignStatsForSenderRequestInterface): Observable;\n\n get(statsRequest: GetAggregatedCampaignStatsRequestInterface): Observable;\n}\n\ninterface Translator {\n instant(key: string): string;\n}\n\ninterface TagUpdate {\n campaignId: string;\n allTags: TagData[];\n}\n\nexport interface InMemoryTags {\n [campaignId: string]: TagData[];\n}\n\n@Injectable()\nexport class MyCampaignsTableService implements TableDataService, TagChangesListener {\n private totalResults$$: BehaviorSubject = new BehaviorSubject(0);\n public readonly totalResults$: Observable = this.totalResults$$.asObservable();\n private readonly campaignFocus$$: BehaviorSubject = new BehaviorSubject('');\n private readonly campaignDateFilter$$: BehaviorSubject = new BehaviorSubject(AllDates);\n public readonly campaignDateFilter$: Observable = this.campaignDateFilter$$.asObservable();\n private readonly inMemoryTagUpdates$$ = new Subject();\n private readonly inMemoryTags$: Observable = this.inMemoryTagUpdates$$.pipe(\n scan((acc, value) => {\n acc[value.campaignId] = value.allTags;\n return acc;\n }, {}),\n );\n\n private cursors: { [id: number]: string | undefined } = { 0: '' };\n\n /**\n * @deprecated Use isLoading$\n */\n public isLoading$$: BehaviorSubject = new BehaviorSubject(true);\n public readonly isLoading$ = this.isLoading$$.pipe(distinctUntilChanged());\n public reload$$: BehaviorSubject = new BehaviorSubject(false);\n\n constructor(\n @Inject(CONFIG_TOKEN) private readonly config: CONFIG_TYPE,\n @Inject(CampaignStatsService) private campaignStatsService: CampaignStatsSource,\n @Inject(TranslateService) private translateService: Translator,\n @Inject(CampaignService) private campaignService: CampaignService,\n private readonly http: HttpClient,\n ) {}\n\n createNewCampaign(name: string): Observable {\n return this.config.sender$.pipe(\n take(1),\n map((sender) => {\n return { type: sender.type, id: sender.id };\n }),\n switchMap((senderData) => this.campaignService.create(senderData, name)),\n );\n }\n\n getSender(): Observable {\n return this.config.sender$;\n }\n\n load(request: LoadRequest): Observable {\n const loadedStats$ = combineLatest([\n this.config.sender$,\n this.reload$$.asObservable(),\n this.campaignDateFilter$,\n ]).pipe(\n switchMap(([sender, ,]) => this.fetchCampaignStats(sender, request)),\n tap((resp) => this.updateTotalResults(request, resp)),\n map((resp) => resp?.stats || []),\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n return preferInMemoryTags$(loadedStats$, this.inMemoryTags$);\n }\n\n private fetchCampaignStats(\n sender: SenderInterface,\n request: LoadRequest,\n ): Observable {\n const lcsRequest: ListCampaignStatsForSenderRequestInterface = {\n sender: sender,\n tagIds: request.filters?.tag ? [request.filters?.tag?.tagId] : [],\n marketId: request.filters?.marketId ?? '',\n statuses: this.getStatuses(request),\n states: this.getStates(request),\n searchTerm: request.filters?.search,\n pagingOptions: {\n cursor: this.cursors[request.pageIndex || 0],\n pageSize: request.pageSize,\n },\n deletedTagsPolicy: DeletedTagsPolicy.DELETED_TAGS_POLICY_EXCLUDE_FROM_RESPONSE,\n };\n\n return this.campaignStatsService.listStatsForSender(lcsRequest);\n }\n\n setFocus(focus: string): void {\n this.campaignFocus$$.next(focus);\n }\n\n getStates(request: LoadRequest): CampaignState[] {\n const stateList: CampaignState[] = [];\n if (request.filters?.Published) {\n stateList.push(convertCampaignStateToProto('idle'));\n }\n if (request.filters?.Ongoing) {\n stateList.push(convertCampaignStateToProto('active'));\n }\n return stateList;\n }\n\n getStatuses(request: LoadRequest): Statuses[] {\n const statusList: Statuses[] = [];\n if (request.filters?.Published) {\n statusList.push(convertStatusToProto('published'));\n statusList.push(convertStatusToProto('active'));\n }\n if (request.filters?.Ongoing) {\n statusList.push(convertStatusToProto('active'));\n statusList.push(convertStatusToProto('published'));\n }\n if (request.filters?.Draft) {\n statusList.push(convertStatusToProto('draft'));\n }\n if (request.filters?.Archived) {\n statusList.push(convertStatusToProto('archived'));\n }\n\n return statusList;\n }\n\n reloadData(): void {\n // When we force the table to reload after an action from my-campaigns-actions-column has been clicked/requested\n // there is a significant delay to fetching the updated data; The timeout is to account for this as with no delay,\n // the results for our table will be the same as the previous request\n window.setTimeout(() => this.reload$$.next(true), 4500);\n }\n\n private updateTotalResults(request: LoadRequest, resp: ListCampaignStatsForSenderResponseInterface): void {\n if (!resp.pagingMetadata) {\n resp.pagingMetadata = new PagedResponseMetadata({});\n }\n resp.pagingMetadata.totalResults = request.pageSize * (request.pageIndex + 1);\n if (resp.pagingMetadata.hasMore) {\n resp.pagingMetadata.totalResults += request.pageSize;\n this.cursors[request.pageIndex + 1] = resp.pagingMetadata.nextCursor;\n }\n this.totalResults$$.next(resp.pagingMetadata.totalResults);\n }\n\n handleTagsChanged(campaignId: string, allTags: TagData[]): void {\n const newVar: TagUpdate = {\n campaignId: campaignId,\n allTags: allTags,\n } as TagUpdate;\n this.inMemoryTagUpdates$$.next(newVar);\n }\n}\n\nexport function convertFocusToProto(focus: string): Focuses {\n switch (focus) {\n case 'acquire':\n return Focuses.FOCUSES_ACQUIRE;\n case 'adopt':\n return Focuses.FOCUSES_ADOPT;\n case 'upsell':\n return Focuses.FOCUSES_UPSELL;\n case 'none':\n return Focuses.FOCUSES_NONE;\n default:\n return Focuses.FOCUSES_ACQUIRE;\n }\n}\n\nfunction convertStatusToProto(status: string): Statuses {\n switch (status) {\n case 'draft':\n return Statuses.STATUSES_DRAFT;\n case 'published':\n return Statuses.STATUSES_PUBLISHED;\n case 'active':\n return Statuses.STATUSES_ACTIVE;\n case 'archived':\n return Statuses.STATUSES_ARCHIVED;\n default:\n return Statuses.STATUSES_UNSPECIFIED;\n }\n}\n\nfunction convertCampaignStateToProto(state: string): CampaignState {\n switch (state) {\n case 'active':\n return CampaignState.CAMPAIGN_STATE_ACTIVE;\n case 'idle':\n return CampaignState.CAMPAIGN_STATE_IDLE;\n default:\n return CampaignState.CAMPAIGN_STATE_UNSPECIFIED;\n }\n}\n\nexport function preferInMemoryTags$(\n loadedStats$: Observable,\n inMemoryTags$: Observable,\n): Observable {\n const inMemoryWithLoaded$ = inMemoryTags$.pipe(\n withLatestFrom(loadedStats$),\n map(([imt, ls]) => preferInMemoryTags(imt, ls)),\n );\n return merge(inMemoryWithLoaded$, loadedStats$);\n}\n\nexport function preferInMemoryTags(\n inMemoryTags: InMemoryTags,\n loadedStats: ListCampaignStatsStructInterface[],\n): ListCampaignStatsStructInterface[] {\n return loadedStats.map((s) => {\n if (!Object.keys(inMemoryTags).includes(s.campaignId!)) {\n return s;\n }\n const newStats = { ...s };\n const imt = inMemoryTags[s.campaignId!]?.filter((v) => v.selected);\n newStats.tags = imt?.map(\n (t) =>\n new ListCampaignTagDetails({\n tagId: t.tagId,\n text: t.tagName,\n }),\n );\n return newStats;\n });\n}\n","export * from './src';\n","/**\n * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io)\n *\n * Copyright (c) 2022 Kiro Risk (http://kiro.me)\n * All Rights Reserved. Apache Software License 2.0\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n */\n\nfunction isArray(value) {\n return !Array.isArray ? getTag(value) === '[object Array]' : Array.isArray(value);\n}\n\n// Adapted from: https://github.com/lodash/lodash/blob/master/.internal/baseToString.js\nconst INFINITY = 1 / 0;\nfunction baseToString(value) {\n // Exit early for strings to avoid a performance hit in some environments.\n if (typeof value == 'string') {\n return value;\n }\n let result = value + '';\n return result == '0' && 1 / value == -INFINITY ? '-0' : result;\n}\nfunction toString(value) {\n return value == null ? '' : baseToString(value);\n}\nfunction isString(value) {\n return typeof value === 'string';\n}\nfunction isNumber(value) {\n return typeof value === 'number';\n}\n\n// Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js\nfunction isBoolean(value) {\n return value === true || value === false || isObjectLike(value) && getTag(value) == '[object Boolean]';\n}\nfunction isObject(value) {\n return typeof value === 'object';\n}\n\n// Checks if `value` is object-like.\nfunction isObjectLike(value) {\n return isObject(value) && value !== null;\n}\nfunction isDefined(value) {\n return value !== undefined && value !== null;\n}\nfunction isBlank(value) {\n return !value.trim().length;\n}\n\n// Gets the `toStringTag` of `value`.\n// Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js\nfunction getTag(value) {\n return value == null ? value === undefined ? '[object Undefined]' : '[object Null]' : Object.prototype.toString.call(value);\n}\nconst EXTENDED_SEARCH_UNAVAILABLE = 'Extended search is not available';\nconst INCORRECT_INDEX_TYPE = \"Incorrect 'index' type\";\nconst LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = key => `Invalid value for key ${key}`;\nconst PATTERN_LENGTH_TOO_LARGE = max => `Pattern length exceeds max of ${max}.`;\nconst MISSING_KEY_PROPERTY = name => `Missing ${name} property in key`;\nconst INVALID_KEY_WEIGHT_VALUE = key => `Property 'weight' in key '${key}' must be a positive integer`;\nconst hasOwn = Object.prototype.hasOwnProperty;\nclass KeyStore {\n constructor(keys) {\n this._keys = [];\n this._keyMap = {};\n let totalWeight = 0;\n keys.forEach(key => {\n let obj = createKey(key);\n totalWeight += obj.weight;\n this._keys.push(obj);\n this._keyMap[obj.id] = obj;\n totalWeight += obj.weight;\n });\n\n // Normalize weights so that their sum is equal to 1\n this._keys.forEach(key => {\n key.weight /= totalWeight;\n });\n }\n get(keyId) {\n return this._keyMap[keyId];\n }\n keys() {\n return this._keys;\n }\n toJSON() {\n return JSON.stringify(this._keys);\n }\n}\nfunction createKey(key) {\n let path = null;\n let id = null;\n let src = null;\n let weight = 1;\n let getFn = null;\n if (isString(key) || isArray(key)) {\n src = key;\n path = createKeyPath(key);\n id = createKeyId(key);\n } else {\n if (!hasOwn.call(key, 'name')) {\n throw new Error(MISSING_KEY_PROPERTY('name'));\n }\n const name = key.name;\n src = name;\n if (hasOwn.call(key, 'weight')) {\n weight = key.weight;\n if (weight <= 0) {\n throw new Error(INVALID_KEY_WEIGHT_VALUE(name));\n }\n }\n path = createKeyPath(name);\n id = createKeyId(name);\n getFn = key.getFn;\n }\n return {\n path,\n id,\n weight,\n src,\n getFn\n };\n}\nfunction createKeyPath(key) {\n return isArray(key) ? key : key.split('.');\n}\nfunction createKeyId(key) {\n return isArray(key) ? key.join('.') : key;\n}\nfunction get(obj, path) {\n let list = [];\n let arr = false;\n const deepGet = (obj, path, index) => {\n if (!isDefined(obj)) {\n return;\n }\n if (!path[index]) {\n // If there's no path left, we've arrived at the object we care about.\n list.push(obj);\n } else {\n let key = path[index];\n const value = obj[key];\n if (!isDefined(value)) {\n return;\n }\n\n // If we're at the last value in the path, and if it's a string/number/bool,\n // add it to the list\n if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value))) {\n list.push(toString(value));\n } else if (isArray(value)) {\n arr = true;\n // Search each item in the array.\n for (let i = 0, len = value.length; i < len; i += 1) {\n deepGet(value[i], path, index + 1);\n }\n } else if (path.length) {\n // An object. Recurse further.\n deepGet(value, path, index + 1);\n }\n }\n };\n\n // Backwards compatibility (since path used to be a string)\n deepGet(obj, isString(path) ? path.split('.') : path, 0);\n return arr ? list : list[0];\n}\nconst MatchOptions = {\n // Whether the matches should be included in the result set. When `true`, each record in the result\n // set will include the indices of the matched characters.\n // These can consequently be used for highlighting purposes.\n includeMatches: false,\n // When `true`, the matching function will continue to the end of a search pattern even if\n // a perfect match has already been located in the string.\n findAllMatches: false,\n // Minimum number of characters that must be matched before a result is considered a match\n minMatchCharLength: 1\n};\nconst BasicOptions = {\n // When `true`, the algorithm continues searching to the end of the input even if a perfect\n // match is found before the end of the same input.\n isCaseSensitive: false,\n // When true, the matching function will continue to the end of a search pattern even if\n includeScore: false,\n // List of properties that will be searched. This also supports nested properties.\n keys: [],\n // Whether to sort the result list, by score\n shouldSort: true,\n // Default sort function: sort by ascending score, ascending index\n sortFn: (a, b) => a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1\n};\nconst FuzzyOptions = {\n // Approximately where in the text is the pattern expected to be found?\n location: 0,\n // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match\n // (of both letters and location), a threshold of '1.0' would match anything.\n threshold: 0.6,\n // Determines how close the match must be to the fuzzy location (specified above).\n // An exact letter match which is 'distance' characters away from the fuzzy location\n // would score as a complete mismatch. A distance of '0' requires the match be at\n // the exact location specified, a threshold of '1000' would require a perfect match\n // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.\n distance: 100\n};\nconst AdvancedOptions = {\n // When `true`, it enables the use of unix-like search commands\n useExtendedSearch: false,\n // The get function to use when fetching an object's properties.\n // The default will search nested paths *ie foo.bar.baz*\n getFn: get,\n // When `true`, search will ignore `location` and `distance`, so it won't matter\n // where in the string the pattern appears.\n // More info: https://fusejs.io/concepts/scoring-theory.html#fuzziness-score\n ignoreLocation: false,\n // When `true`, the calculation for the relevance score (used for sorting) will\n // ignore the field-length norm.\n // More info: https://fusejs.io/concepts/scoring-theory.html#field-length-norm\n ignoreFieldNorm: false,\n // The weight to determine how much field length norm effects scoring.\n fieldNormWeight: 1\n};\nvar Config = {\n ...BasicOptions,\n ...MatchOptions,\n ...FuzzyOptions,\n ...AdvancedOptions\n};\nconst SPACE = /[^ ]+/g;\n\n// Field-length norm: the shorter the field, the higher the weight.\n// Set to 3 decimals to reduce index size.\nfunction norm(weight = 1, mantissa = 3) {\n const cache = new Map();\n const m = Math.pow(10, mantissa);\n return {\n get(value) {\n const numTokens = value.match(SPACE).length;\n if (cache.has(numTokens)) {\n return cache.get(numTokens);\n }\n\n // Default function is 1/sqrt(x), weight makes that variable\n const norm = 1 / Math.pow(numTokens, 0.5 * weight);\n\n // In place of `toFixed(mantissa)`, for faster computation\n const n = parseFloat(Math.round(norm * m) / m);\n cache.set(numTokens, n);\n return n;\n },\n clear() {\n cache.clear();\n }\n };\n}\nclass FuseIndex {\n constructor({\n getFn = Config.getFn,\n fieldNormWeight = Config.fieldNormWeight\n } = {}) {\n this.norm = norm(fieldNormWeight, 3);\n this.getFn = getFn;\n this.isCreated = false;\n this.setIndexRecords();\n }\n setSources(docs = []) {\n this.docs = docs;\n }\n setIndexRecords(records = []) {\n this.records = records;\n }\n setKeys(keys = []) {\n this.keys = keys;\n this._keysMap = {};\n keys.forEach((key, idx) => {\n this._keysMap[key.id] = idx;\n });\n }\n create() {\n if (this.isCreated || !this.docs.length) {\n return;\n }\n this.isCreated = true;\n\n // List is Array\n if (isString(this.docs[0])) {\n this.docs.forEach((doc, docIndex) => {\n this._addString(doc, docIndex);\n });\n } else {\n // List is Array\n this.docs.forEach((doc, docIndex) => {\n this._addObject(doc, docIndex);\n });\n }\n this.norm.clear();\n }\n // Adds a doc to the end of the index\n add(doc) {\n const idx = this.size();\n if (isString(doc)) {\n this._addString(doc, idx);\n } else {\n this._addObject(doc, idx);\n }\n }\n // Removes the doc at the specified index of the index\n removeAt(idx) {\n this.records.splice(idx, 1);\n\n // Change ref index of every subsquent doc\n for (let i = idx, len = this.size(); i < len; i += 1) {\n this.records[i].i -= 1;\n }\n }\n getValueForItemAtKeyId(item, keyId) {\n return item[this._keysMap[keyId]];\n }\n size() {\n return this.records.length;\n }\n _addString(doc, docIndex) {\n if (!isDefined(doc) || isBlank(doc)) {\n return;\n }\n let record = {\n v: doc,\n i: docIndex,\n n: this.norm.get(doc)\n };\n this.records.push(record);\n }\n _addObject(doc, docIndex) {\n let record = {\n i: docIndex,\n $: {}\n };\n\n // Iterate over every key (i.e, path), and fetch the value at that key\n this.keys.forEach((key, keyIndex) => {\n let value = key.getFn ? key.getFn(doc) : this.getFn(doc, key.path);\n if (!isDefined(value)) {\n return;\n }\n if (isArray(value)) {\n let subRecords = [];\n const stack = [{\n nestedArrIndex: -1,\n value\n }];\n while (stack.length) {\n const {\n nestedArrIndex,\n value\n } = stack.pop();\n if (!isDefined(value)) {\n continue;\n }\n if (isString(value) && !isBlank(value)) {\n let subRecord = {\n v: value,\n i: nestedArrIndex,\n n: this.norm.get(value)\n };\n subRecords.push(subRecord);\n } else if (isArray(value)) {\n value.forEach((item, k) => {\n stack.push({\n nestedArrIndex: k,\n value: item\n });\n });\n } else ;\n }\n record.$[keyIndex] = subRecords;\n } else if (isString(value) && !isBlank(value)) {\n let subRecord = {\n v: value,\n n: this.norm.get(value)\n };\n record.$[keyIndex] = subRecord;\n }\n });\n this.records.push(record);\n }\n toJSON() {\n return {\n keys: this.keys,\n records: this.records\n };\n }\n}\nfunction createIndex(keys, docs, {\n getFn = Config.getFn,\n fieldNormWeight = Config.fieldNormWeight\n} = {}) {\n const myIndex = new FuseIndex({\n getFn,\n fieldNormWeight\n });\n myIndex.setKeys(keys.map(createKey));\n myIndex.setSources(docs);\n myIndex.create();\n return myIndex;\n}\nfunction parseIndex(data, {\n getFn = Config.getFn,\n fieldNormWeight = Config.fieldNormWeight\n} = {}) {\n const {\n keys,\n records\n } = data;\n const myIndex = new FuseIndex({\n getFn,\n fieldNormWeight\n });\n myIndex.setKeys(keys);\n myIndex.setIndexRecords(records);\n return myIndex;\n}\nfunction computeScore$1(pattern, {\n errors = 0,\n currentLocation = 0,\n expectedLocation = 0,\n distance = Config.distance,\n ignoreLocation = Config.ignoreLocation\n} = {}) {\n const accuracy = errors / pattern.length;\n if (ignoreLocation) {\n return accuracy;\n }\n const proximity = Math.abs(expectedLocation - currentLocation);\n if (!distance) {\n // Dodge divide by zero error.\n return proximity ? 1.0 : accuracy;\n }\n return accuracy + proximity / distance;\n}\nfunction convertMaskToIndices(matchmask = [], minMatchCharLength = Config.minMatchCharLength) {\n let indices = [];\n let start = -1;\n let end = -1;\n let i = 0;\n for (let len = matchmask.length; i < len; i += 1) {\n let match = matchmask[i];\n if (match && start === -1) {\n start = i;\n } else if (!match && start !== -1) {\n end = i - 1;\n if (end - start + 1 >= minMatchCharLength) {\n indices.push([start, end]);\n }\n start = -1;\n }\n }\n\n // (i-1 - start) + 1 => i - start\n if (matchmask[i - 1] && i - start >= minMatchCharLength) {\n indices.push([start, i - 1]);\n }\n return indices;\n}\n\n// Machine word size\nconst MAX_BITS = 32;\nfunction search(text, pattern, patternAlphabet, {\n location = Config.location,\n distance = Config.distance,\n threshold = Config.threshold,\n findAllMatches = Config.findAllMatches,\n minMatchCharLength = Config.minMatchCharLength,\n includeMatches = Config.includeMatches,\n ignoreLocation = Config.ignoreLocation\n} = {}) {\n if (pattern.length > MAX_BITS) {\n throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS));\n }\n const patternLen = pattern.length;\n // Set starting location at beginning text and initialize the alphabet.\n const textLen = text.length;\n // Handle the case when location > text.length\n const expectedLocation = Math.max(0, Math.min(location, textLen));\n // Highest score beyond which we give up.\n let currentThreshold = threshold;\n // Is there a nearby exact match? (speedup)\n let bestLocation = expectedLocation;\n\n // Performance: only computer matches when the minMatchCharLength > 1\n // OR if `includeMatches` is true.\n const computeMatches = minMatchCharLength > 1 || includeMatches;\n // A mask of the matches, used for building the indices\n const matchMask = computeMatches ? Array(textLen) : [];\n let index;\n\n // Get all exact matches, here for speed up\n while ((index = text.indexOf(pattern, bestLocation)) > -1) {\n let score = computeScore$1(pattern, {\n currentLocation: index,\n expectedLocation,\n distance,\n ignoreLocation\n });\n currentThreshold = Math.min(score, currentThreshold);\n bestLocation = index + patternLen;\n if (computeMatches) {\n let i = 0;\n while (i < patternLen) {\n matchMask[index + i] = 1;\n i += 1;\n }\n }\n }\n\n // Reset the best location\n bestLocation = -1;\n let lastBitArr = [];\n let finalScore = 1;\n let binMax = patternLen + textLen;\n const mask = 1 << patternLen - 1;\n for (let i = 0; i < patternLen; i += 1) {\n // Scan for the best match; each iteration allows for one more error.\n // Run a binary search to determine how far from the match location we can stray\n // at this error level.\n let binMin = 0;\n let binMid = binMax;\n while (binMin < binMid) {\n const score = computeScore$1(pattern, {\n errors: i,\n currentLocation: expectedLocation + binMid,\n expectedLocation,\n distance,\n ignoreLocation\n });\n if (score <= currentThreshold) {\n binMin = binMid;\n } else {\n binMax = binMid;\n }\n binMid = Math.floor((binMax - binMin) / 2 + binMin);\n }\n\n // Use the result from this iteration as the maximum for the next.\n binMax = binMid;\n let start = Math.max(1, expectedLocation - binMid + 1);\n let finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen;\n\n // Initialize the bit array\n let bitArr = Array(finish + 2);\n bitArr[finish + 1] = (1 << i) - 1;\n for (let j = finish; j >= start; j -= 1) {\n let currentLocation = j - 1;\n let charMatch = patternAlphabet[text.charAt(currentLocation)];\n if (computeMatches) {\n // Speed up: quick bool to int conversion (i.e, `charMatch ? 1 : 0`)\n matchMask[currentLocation] = +!!charMatch;\n }\n\n // First pass: exact match\n bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch;\n\n // Subsequent passes: fuzzy match\n if (i) {\n bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1];\n }\n if (bitArr[j] & mask) {\n finalScore = computeScore$1(pattern, {\n errors: i,\n currentLocation,\n expectedLocation,\n distance,\n ignoreLocation\n });\n\n // This match will almost certainly be better than any existing match.\n // But check anyway.\n if (finalScore <= currentThreshold) {\n // Indeed it is\n currentThreshold = finalScore;\n bestLocation = currentLocation;\n\n // Already passed `loc`, downhill from here on in.\n if (bestLocation <= expectedLocation) {\n break;\n }\n\n // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`.\n start = Math.max(1, 2 * expectedLocation - bestLocation);\n }\n }\n }\n\n // No hope for a (better) match at greater error levels.\n const score = computeScore$1(pattern, {\n errors: i + 1,\n currentLocation: expectedLocation,\n expectedLocation,\n distance,\n ignoreLocation\n });\n if (score > currentThreshold) {\n break;\n }\n lastBitArr = bitArr;\n }\n const result = {\n isMatch: bestLocation >= 0,\n // Count exact matches (those with a score of 0) to be \"almost\" exact\n score: Math.max(0.001, finalScore)\n };\n if (computeMatches) {\n const indices = convertMaskToIndices(matchMask, minMatchCharLength);\n if (!indices.length) {\n result.isMatch = false;\n } else if (includeMatches) {\n result.indices = indices;\n }\n }\n return result;\n}\nfunction createPatternAlphabet(pattern) {\n let mask = {};\n for (let i = 0, len = pattern.length; i < len; i += 1) {\n const char = pattern.charAt(i);\n mask[char] = (mask[char] || 0) | 1 << len - i - 1;\n }\n return mask;\n}\nclass BitapSearch {\n constructor(pattern, {\n location = Config.location,\n threshold = Config.threshold,\n distance = Config.distance,\n includeMatches = Config.includeMatches,\n findAllMatches = Config.findAllMatches,\n minMatchCharLength = Config.minMatchCharLength,\n isCaseSensitive = Config.isCaseSensitive,\n ignoreLocation = Config.ignoreLocation\n } = {}) {\n this.options = {\n location,\n threshold,\n distance,\n includeMatches,\n findAllMatches,\n minMatchCharLength,\n isCaseSensitive,\n ignoreLocation\n };\n this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase();\n this.chunks = [];\n if (!this.pattern.length) {\n return;\n }\n const addChunk = (pattern, startIndex) => {\n this.chunks.push({\n pattern,\n alphabet: createPatternAlphabet(pattern),\n startIndex\n });\n };\n const len = this.pattern.length;\n if (len > MAX_BITS) {\n let i = 0;\n const remainder = len % MAX_BITS;\n const end = len - remainder;\n while (i < end) {\n addChunk(this.pattern.substr(i, MAX_BITS), i);\n i += MAX_BITS;\n }\n if (remainder) {\n const startIndex = len - MAX_BITS;\n addChunk(this.pattern.substr(startIndex), startIndex);\n }\n } else {\n addChunk(this.pattern, 0);\n }\n }\n searchIn(text) {\n const {\n isCaseSensitive,\n includeMatches\n } = this.options;\n if (!isCaseSensitive) {\n text = text.toLowerCase();\n }\n\n // Exact match\n if (this.pattern === text) {\n let result = {\n isMatch: true,\n score: 0\n };\n if (includeMatches) {\n result.indices = [[0, text.length - 1]];\n }\n return result;\n }\n\n // Otherwise, use Bitap algorithm\n const {\n location,\n distance,\n threshold,\n findAllMatches,\n minMatchCharLength,\n ignoreLocation\n } = this.options;\n let allIndices = [];\n let totalScore = 0;\n let hasMatches = false;\n this.chunks.forEach(({\n pattern,\n alphabet,\n startIndex\n }) => {\n const {\n isMatch,\n score,\n indices\n } = search(text, pattern, alphabet, {\n location: location + startIndex,\n distance,\n threshold,\n findAllMatches,\n minMatchCharLength,\n includeMatches,\n ignoreLocation\n });\n if (isMatch) {\n hasMatches = true;\n }\n totalScore += score;\n if (isMatch && indices) {\n allIndices = [...allIndices, ...indices];\n }\n });\n let result = {\n isMatch: hasMatches,\n score: hasMatches ? totalScore / this.chunks.length : 1\n };\n if (hasMatches && includeMatches) {\n result.indices = allIndices;\n }\n return result;\n }\n}\nclass BaseMatch {\n constructor(pattern) {\n this.pattern = pattern;\n }\n static isMultiMatch(pattern) {\n return getMatch(pattern, this.multiRegex);\n }\n static isSingleMatch(pattern) {\n return getMatch(pattern, this.singleRegex);\n }\n search(/*text*/) {}\n}\nfunction getMatch(pattern, exp) {\n const matches = pattern.match(exp);\n return matches ? matches[1] : null;\n}\n\n// Token: 'file\n\nclass ExactMatch extends BaseMatch {\n constructor(pattern) {\n super(pattern);\n }\n static get type() {\n return 'exact';\n }\n static get multiRegex() {\n return /^=\"(.*)\"$/;\n }\n static get singleRegex() {\n return /^=(.*)$/;\n }\n search(text) {\n const isMatch = text === this.pattern;\n return {\n isMatch,\n score: isMatch ? 0 : 1,\n indices: [0, this.pattern.length - 1]\n };\n }\n}\n\n// Token: !fire\n\nclass InverseExactMatch extends BaseMatch {\n constructor(pattern) {\n super(pattern);\n }\n static get type() {\n return 'inverse-exact';\n }\n static get multiRegex() {\n return /^!\"(.*)\"$/;\n }\n static get singleRegex() {\n return /^!(.*)$/;\n }\n search(text) {\n const index = text.indexOf(this.pattern);\n const isMatch = index === -1;\n return {\n isMatch,\n score: isMatch ? 0 : 1,\n indices: [0, text.length - 1]\n };\n }\n}\n\n// Token: ^file\n\nclass PrefixExactMatch extends BaseMatch {\n constructor(pattern) {\n super(pattern);\n }\n static get type() {\n return 'prefix-exact';\n }\n static get multiRegex() {\n return /^\\^\"(.*)\"$/;\n }\n static get singleRegex() {\n return /^\\^(.*)$/;\n }\n search(text) {\n const isMatch = text.startsWith(this.pattern);\n return {\n isMatch,\n score: isMatch ? 0 : 1,\n indices: [0, this.pattern.length - 1]\n };\n }\n}\n\n// Token: !^fire\n\nclass InversePrefixExactMatch extends BaseMatch {\n constructor(pattern) {\n super(pattern);\n }\n static get type() {\n return 'inverse-prefix-exact';\n }\n static get multiRegex() {\n return /^!\\^\"(.*)\"$/;\n }\n static get singleRegex() {\n return /^!\\^(.*)$/;\n }\n search(text) {\n const isMatch = !text.startsWith(this.pattern);\n return {\n isMatch,\n score: isMatch ? 0 : 1,\n indices: [0, text.length - 1]\n };\n }\n}\n\n// Token: .file$\n\nclass SuffixExactMatch extends BaseMatch {\n constructor(pattern) {\n super(pattern);\n }\n static get type() {\n return 'suffix-exact';\n }\n static get multiRegex() {\n return /^\"(.*)\"\\$$/;\n }\n static get singleRegex() {\n return /^(.*)\\$$/;\n }\n search(text) {\n const isMatch = text.endsWith(this.pattern);\n return {\n isMatch,\n score: isMatch ? 0 : 1,\n indices: [text.length - this.pattern.length, text.length - 1]\n };\n }\n}\n\n// Token: !.file$\n\nclass InverseSuffixExactMatch extends BaseMatch {\n constructor(pattern) {\n super(pattern);\n }\n static get type() {\n return 'inverse-suffix-exact';\n }\n static get multiRegex() {\n return /^!\"(.*)\"\\$$/;\n }\n static get singleRegex() {\n return /^!(.*)\\$$/;\n }\n search(text) {\n const isMatch = !text.endsWith(this.pattern);\n return {\n isMatch,\n score: isMatch ? 0 : 1,\n indices: [0, text.length - 1]\n };\n }\n}\nclass FuzzyMatch extends BaseMatch {\n constructor(pattern, {\n location = Config.location,\n threshold = Config.threshold,\n distance = Config.distance,\n includeMatches = Config.includeMatches,\n findAllMatches = Config.findAllMatches,\n minMatchCharLength = Config.minMatchCharLength,\n isCaseSensitive = Config.isCaseSensitive,\n ignoreLocation = Config.ignoreLocation\n } = {}) {\n super(pattern);\n this._bitapSearch = new BitapSearch(pattern, {\n location,\n threshold,\n distance,\n includeMatches,\n findAllMatches,\n minMatchCharLength,\n isCaseSensitive,\n ignoreLocation\n });\n }\n static get type() {\n return 'fuzzy';\n }\n static get multiRegex() {\n return /^\"(.*)\"$/;\n }\n static get singleRegex() {\n return /^(.*)$/;\n }\n search(text) {\n return this._bitapSearch.searchIn(text);\n }\n}\n\n// Token: 'file\n\nclass IncludeMatch extends BaseMatch {\n constructor(pattern) {\n super(pattern);\n }\n static get type() {\n return 'include';\n }\n static get multiRegex() {\n return /^'\"(.*)\"$/;\n }\n static get singleRegex() {\n return /^'(.*)$/;\n }\n search(text) {\n let location = 0;\n let index;\n const indices = [];\n const patternLen = this.pattern.length;\n\n // Get all exact matches\n while ((index = text.indexOf(this.pattern, location)) > -1) {\n location = index + patternLen;\n indices.push([index, location - 1]);\n }\n const isMatch = !!indices.length;\n return {\n isMatch,\n score: isMatch ? 0 : 1,\n indices\n };\n }\n}\n\n// ❗Order is important. DO NOT CHANGE.\nconst searchers = [ExactMatch, IncludeMatch, PrefixExactMatch, InversePrefixExactMatch, InverseSuffixExactMatch, SuffixExactMatch, InverseExactMatch, FuzzyMatch];\nconst searchersLen = searchers.length;\n\n// Regex to split by spaces, but keep anything in quotes together\nconst SPACE_RE = / +(?=(?:[^\\\"]*\\\"[^\\\"]*\\\")*[^\\\"]*$)/;\nconst OR_TOKEN = '|';\n\n// Return a 2D array representation of the query, for simpler parsing.\n// Example:\n// \"^core go$ | rb$ | py$ xy$\" => [[\"^core\", \"go$\"], [\"rb$\"], [\"py$\", \"xy$\"]]\nfunction parseQuery(pattern, options = {}) {\n return pattern.split(OR_TOKEN).map(item => {\n let query = item.trim().split(SPACE_RE).filter(item => item && !!item.trim());\n let results = [];\n for (let i = 0, len = query.length; i < len; i += 1) {\n const queryItem = query[i];\n\n // 1. Handle multiple query match (i.e, once that are quoted, like `\"hello world\"`)\n let found = false;\n let idx = -1;\n while (!found && ++idx < searchersLen) {\n const searcher = searchers[idx];\n let token = searcher.isMultiMatch(queryItem);\n if (token) {\n results.push(new searcher(token, options));\n found = true;\n }\n }\n if (found) {\n continue;\n }\n\n // 2. Handle single query matches (i.e, once that are *not* quoted)\n idx = -1;\n while (++idx < searchersLen) {\n const searcher = searchers[idx];\n let token = searcher.isSingleMatch(queryItem);\n if (token) {\n results.push(new searcher(token, options));\n break;\n }\n }\n }\n return results;\n });\n}\n\n// These extended matchers can return an array of matches, as opposed\n// to a singl match\nconst MultiMatchSet = new Set([FuzzyMatch.type, IncludeMatch.type]);\n\n/**\n * Command-like searching\n * ======================\n *\n * Given multiple search terms delimited by spaces.e.g. `^jscript .python$ ruby !java`,\n * search in a given text.\n *\n * Search syntax:\n *\n * | Token | Match type | Description |\n * | ----------- | -------------------------- | -------------------------------------- |\n * | `jscript` | fuzzy-match | Items that fuzzy match `jscript` |\n * | `=scheme` | exact-match | Items that are `scheme` |\n * | `'python` | include-match | Items that include `python` |\n * | `!ruby` | inverse-exact-match | Items that do not include `ruby` |\n * | `^java` | prefix-exact-match | Items that start with `java` |\n * | `!^earlang` | inverse-prefix-exact-match | Items that do not start with `earlang` |\n * | `.js$` | suffix-exact-match | Items that end with `.js` |\n * | `!.go$` | inverse-suffix-exact-match | Items that do not end with `.go` |\n *\n * A single pipe character acts as an OR operator. For example, the following\n * query matches entries that start with `core` and end with either`go`, `rb`,\n * or`py`.\n *\n * ```\n * ^core go$ | rb$ | py$\n * ```\n */\nclass ExtendedSearch {\n constructor(pattern, {\n isCaseSensitive = Config.isCaseSensitive,\n includeMatches = Config.includeMatches,\n minMatchCharLength = Config.minMatchCharLength,\n ignoreLocation = Config.ignoreLocation,\n findAllMatches = Config.findAllMatches,\n location = Config.location,\n threshold = Config.threshold,\n distance = Config.distance\n } = {}) {\n this.query = null;\n this.options = {\n isCaseSensitive,\n includeMatches,\n minMatchCharLength,\n findAllMatches,\n ignoreLocation,\n location,\n threshold,\n distance\n };\n this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase();\n this.query = parseQuery(this.pattern, this.options);\n }\n static condition(_, options) {\n return options.useExtendedSearch;\n }\n searchIn(text) {\n const query = this.query;\n if (!query) {\n return {\n isMatch: false,\n score: 1\n };\n }\n const {\n includeMatches,\n isCaseSensitive\n } = this.options;\n text = isCaseSensitive ? text : text.toLowerCase();\n let numMatches = 0;\n let allIndices = [];\n let totalScore = 0;\n\n // ORs\n for (let i = 0, qLen = query.length; i < qLen; i += 1) {\n const searchers = query[i];\n\n // Reset indices\n allIndices.length = 0;\n numMatches = 0;\n\n // ANDs\n for (let j = 0, pLen = searchers.length; j < pLen; j += 1) {\n const searcher = searchers[j];\n const {\n isMatch,\n indices,\n score\n } = searcher.search(text);\n if (isMatch) {\n numMatches += 1;\n totalScore += score;\n if (includeMatches) {\n const type = searcher.constructor.type;\n if (MultiMatchSet.has(type)) {\n allIndices = [...allIndices, ...indices];\n } else {\n allIndices.push(indices);\n }\n }\n } else {\n totalScore = 0;\n numMatches = 0;\n allIndices.length = 0;\n break;\n }\n }\n\n // OR condition, so if TRUE, return\n if (numMatches) {\n let result = {\n isMatch: true,\n score: totalScore / numMatches\n };\n if (includeMatches) {\n result.indices = allIndices;\n }\n return result;\n }\n }\n\n // Nothing was matched\n return {\n isMatch: false,\n score: 1\n };\n }\n}\nconst registeredSearchers = [];\nfunction register(...args) {\n registeredSearchers.push(...args);\n}\nfunction createSearcher(pattern, options) {\n for (let i = 0, len = registeredSearchers.length; i < len; i += 1) {\n let searcherClass = registeredSearchers[i];\n if (searcherClass.condition(pattern, options)) {\n return new searcherClass(pattern, options);\n }\n }\n return new BitapSearch(pattern, options);\n}\nconst LogicalOperator = {\n AND: '$and',\n OR: '$or'\n};\nconst KeyType = {\n PATH: '$path',\n PATTERN: '$val'\n};\nconst isExpression = query => !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]);\nconst isPath = query => !!query[KeyType.PATH];\nconst isLeaf = query => !isArray(query) && isObject(query) && !isExpression(query);\nconst convertToExplicit = query => ({\n [LogicalOperator.AND]: Object.keys(query).map(key => ({\n [key]: query[key]\n }))\n});\n\n// When `auto` is `true`, the parse function will infer and initialize and add\n// the appropriate `Searcher` instance\nfunction parse(query, options, {\n auto = true\n} = {}) {\n const next = query => {\n let keys = Object.keys(query);\n const isQueryPath = isPath(query);\n if (!isQueryPath && keys.length > 1 && !isExpression(query)) {\n return next(convertToExplicit(query));\n }\n if (isLeaf(query)) {\n const key = isQueryPath ? query[KeyType.PATH] : keys[0];\n const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key];\n if (!isString(pattern)) {\n throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key));\n }\n const obj = {\n keyId: createKeyId(key),\n pattern\n };\n if (auto) {\n obj.searcher = createSearcher(pattern, options);\n }\n return obj;\n }\n let node = {\n children: [],\n operator: keys[0]\n };\n keys.forEach(key => {\n const value = query[key];\n if (isArray(value)) {\n value.forEach(item => {\n node.children.push(next(item));\n });\n }\n });\n return node;\n };\n if (!isExpression(query)) {\n query = convertToExplicit(query);\n }\n return next(query);\n}\n\n// Practical scoring function\nfunction computeScore(results, {\n ignoreFieldNorm = Config.ignoreFieldNorm\n}) {\n results.forEach(result => {\n let totalScore = 1;\n result.matches.forEach(({\n key,\n norm,\n score\n }) => {\n const weight = key ? key.weight : null;\n totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm));\n });\n result.score = totalScore;\n });\n}\nfunction transformMatches(result, data) {\n const matches = result.matches;\n data.matches = [];\n if (!isDefined(matches)) {\n return;\n }\n matches.forEach(match => {\n if (!isDefined(match.indices) || !match.indices.length) {\n return;\n }\n const {\n indices,\n value\n } = match;\n let obj = {\n indices,\n value\n };\n if (match.key) {\n obj.key = match.key.src;\n }\n if (match.idx > -1) {\n obj.refIndex = match.idx;\n }\n data.matches.push(obj);\n });\n}\nfunction transformScore(result, data) {\n data.score = result.score;\n}\nfunction format(results, docs, {\n includeMatches = Config.includeMatches,\n includeScore = Config.includeScore\n} = {}) {\n const transformers = [];\n if (includeMatches) transformers.push(transformMatches);\n if (includeScore) transformers.push(transformScore);\n return results.map(result => {\n const {\n idx\n } = result;\n const data = {\n item: docs[idx],\n refIndex: idx\n };\n if (transformers.length) {\n transformers.forEach(transformer => {\n transformer(result, data);\n });\n }\n return data;\n });\n}\nlet Fuse = /*#__PURE__*/(() => {\n class Fuse {\n constructor(docs, options = {}, index) {\n this.options = {\n ...Config,\n ...options\n };\n if (this.options.useExtendedSearch && !true) {\n throw new Error(EXTENDED_SEARCH_UNAVAILABLE);\n }\n this._keyStore = new KeyStore(this.options.keys);\n this.setCollection(docs, index);\n }\n setCollection(docs, index) {\n this._docs = docs;\n if (index && !(index instanceof FuseIndex)) {\n throw new Error(INCORRECT_INDEX_TYPE);\n }\n this._myIndex = index || createIndex(this.options.keys, this._docs, {\n getFn: this.options.getFn,\n fieldNormWeight: this.options.fieldNormWeight\n });\n }\n add(doc) {\n if (!isDefined(doc)) {\n return;\n }\n this._docs.push(doc);\n this._myIndex.add(doc);\n }\n remove(predicate = (/* doc, idx */) => false) {\n const results = [];\n for (let i = 0, len = this._docs.length; i < len; i += 1) {\n const doc = this._docs[i];\n if (predicate(doc, i)) {\n this.removeAt(i);\n i -= 1;\n len -= 1;\n results.push(doc);\n }\n }\n return results;\n }\n removeAt(idx) {\n this._docs.splice(idx, 1);\n this._myIndex.removeAt(idx);\n }\n getIndex() {\n return this._myIndex;\n }\n search(query, {\n limit = -1\n } = {}) {\n const {\n includeMatches,\n includeScore,\n shouldSort,\n sortFn,\n ignoreFieldNorm\n } = this.options;\n let results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query);\n computeScore(results, {\n ignoreFieldNorm\n });\n if (shouldSort) {\n results.sort(sortFn);\n }\n if (isNumber(limit) && limit > -1) {\n results = results.slice(0, limit);\n }\n return format(results, this._docs, {\n includeMatches,\n includeScore\n });\n }\n _searchStringList(query) {\n const searcher = createSearcher(query, this.options);\n const {\n records\n } = this._myIndex;\n const results = [];\n\n // Iterate over every string in the index\n records.forEach(({\n v: text,\n i: idx,\n n: norm\n }) => {\n if (!isDefined(text)) {\n return;\n }\n const {\n isMatch,\n score,\n indices\n } = searcher.searchIn(text);\n if (isMatch) {\n results.push({\n item: text,\n idx,\n matches: [{\n score,\n value: text,\n norm,\n indices\n }]\n });\n }\n });\n return results;\n }\n _searchLogical(query) {\n const expression = parse(query, this.options);\n const evaluate = (node, item, idx) => {\n if (!node.children) {\n const {\n keyId,\n searcher\n } = node;\n const matches = this._findMatches({\n key: this._keyStore.get(keyId),\n value: this._myIndex.getValueForItemAtKeyId(item, keyId),\n searcher\n });\n if (matches && matches.length) {\n return [{\n idx,\n item,\n matches\n }];\n }\n return [];\n }\n const res = [];\n for (let i = 0, len = node.children.length; i < len; i += 1) {\n const child = node.children[i];\n const result = evaluate(child, item, idx);\n if (result.length) {\n res.push(...result);\n } else if (node.operator === LogicalOperator.AND) {\n return [];\n }\n }\n return res;\n };\n const records = this._myIndex.records;\n const resultMap = {};\n const results = [];\n records.forEach(({\n $: item,\n i: idx\n }) => {\n if (isDefined(item)) {\n let expResults = evaluate(expression, item, idx);\n if (expResults.length) {\n // Dedupe when adding\n if (!resultMap[idx]) {\n resultMap[idx] = {\n idx,\n item,\n matches: []\n };\n results.push(resultMap[idx]);\n }\n expResults.forEach(({\n matches\n }) => {\n resultMap[idx].matches.push(...matches);\n });\n }\n }\n });\n return results;\n }\n _searchObjectList(query) {\n const searcher = createSearcher(query, this.options);\n const {\n keys,\n records\n } = this._myIndex;\n const results = [];\n\n // List is Array\n records.forEach(({\n $: item,\n i: idx\n }) => {\n if (!isDefined(item)) {\n return;\n }\n let matches = [];\n\n // Iterate over every key (i.e, path), and fetch the value at that key\n keys.forEach((key, keyIndex) => {\n matches.push(...this._findMatches({\n key,\n value: item[keyIndex],\n searcher\n }));\n });\n if (matches.length) {\n results.push({\n idx,\n item,\n matches\n });\n }\n });\n return results;\n }\n _findMatches({\n key,\n value,\n searcher\n }) {\n if (!isDefined(value)) {\n return [];\n }\n let matches = [];\n if (isArray(value)) {\n value.forEach(({\n v: text,\n i: idx,\n n: norm\n }) => {\n if (!isDefined(text)) {\n return;\n }\n const {\n isMatch,\n score,\n indices\n } = searcher.searchIn(text);\n if (isMatch) {\n matches.push({\n score,\n key,\n value: text,\n idx,\n norm,\n indices\n });\n }\n });\n } else {\n const {\n v: text,\n n: norm\n } = value;\n const {\n isMatch,\n score,\n indices\n } = searcher.searchIn(text);\n if (isMatch) {\n matches.push({\n score,\n key,\n value: text,\n norm,\n indices\n });\n }\n }\n return matches;\n }\n }\n Fuse.version = '6.6.2';\n Fuse.createIndex = createIndex;\n Fuse.parseIndex = parseIndex;\n Fuse.config = Config;\n return Fuse;\n})();\n{\n Fuse.parseQuery = parse;\n}\n{\n register(ExtendedSearch);\n}\nexport { Fuse as default };","import { Inject, Injectable } from '@angular/core';\nimport { CampaignTaggingService, SenderInterface, SenderType } from '@vendasta/campaigns';\nimport { ListTagsForSenderResponse } from '@vendasta/campaigns/lib/_internal/objects/api';\nimport { Observable, Subject, combineLatest } from 'rxjs';\nimport { map, scan, shareReplay, startWith, switchMap } from 'rxjs/operators';\nimport { SenderTag } from '../manage-tags/tag-toggle/tag-toggle.component';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../shared/src/tokens';\n\n// Senders will eventually have more than 100. We'll have to handle that.\nconst MAX_SENDER_TAGS = 100;\n\ninterface SenderTagsSource {\n listTagsForSender(\n senderType: SenderType,\n senderId: string,\n cursor: string,\n pageSize: number, //\n ): Observable;\n}\n\n@Injectable()\nexport class SenderTagsService {\n private readonly senderTags$: Observable;\n private readonly userAddedTagEvents$$ = new Subject();\n private readonly userAddedTags$ = this.userAddedTagEvents$$.pipe(\n scan((allTags, newTag) => {\n return allTags.concat(newTag);\n }, []),\n startWith([]),\n );\n private readonly deleteTagEvents$$ = new Subject();\n private readonly deletedTags$ = this.deleteTagEvents$$.pipe(\n scan((allTags, removeTag) => {\n return allTags.concat(removeTag);\n }, []),\n startWith([]),\n );\n\n public static getObservable$(svc: SenderTagsService): Observable {\n return svc.senderTags$;\n }\n\n constructor(\n @Inject(CampaignTaggingService) private readonly campaignTaggingService: SenderTagsSource,\n @Inject(CONFIG_TOKEN) config: CONFIG_TYPE,\n ) {\n const loadedTags$ = config.sender$.pipe(switchMap((s) => this.loadSenderTags(s)));\n this.senderTags$ = combineLatest([loadedTags$, this.userAddedTags$, this.deletedTags$]).pipe(\n map(([loaded, userAdded, deleted]) => {\n const allTags = loaded.concat(userAdded);\n return allTags.filter(function (el) {\n return deleted.findIndex((d) => d.tagId === el.tagId) < 0;\n });\n }),\n shareReplay({ bufferSize: 1, refCount: false }),\n );\n }\n\n addTag(newTag: SenderTag): void {\n this.userAddedTagEvents$$.next(newTag);\n }\n\n deleteTag(removeTag: SenderTag): void {\n this.deleteTagEvents$$.next(removeTag);\n }\n\n private loadSenderTags(sender: SenderInterface): Observable {\n return this.campaignTaggingService\n .listTagsForSender(\n sender.type!,\n sender.id!,\n '',\n MAX_SENDER_TAGS, //\n )\n .pipe(\n map((st) =>\n st.campaignTags.map((t) => ({\n tagId: t.tagId,\n tagName: t.text,\n isBackCompatTag: t.tagId.startsWith('FOCUS-'),\n })),\n ),\n );\n }\n}\n","import { Component, Inject } from '@angular/core';\nimport { UntypedFormControl } from '@angular/forms';\nimport { MatDialogRef } from '@angular/material/dialog';\nimport { CampaignTag, CampaignTaggingService } from '@vendasta/campaigns';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { firstValueFrom } from 'rxjs';\nimport { SenderTagsService } from './sender-tags.service';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../shared/src/tokens';\n\nconst defaultColour = '#000000';\n\n@Component({\n templateUrl: 'add-new-tag-dialog.component.html',\n styleUrls: ['./add-new-tag-dialog.component.scss'],\n standalone: false,\n})\nexport class AddNewTagDialogComponent {\n formControl: UntypedFormControl = new UntypedFormControl();\n\n constructor(\n public readonly dialogRef: MatDialogRef,\n private readonly campaignTaggingService: CampaignTaggingService,\n private readonly senderTagsService: SenderTagsService,\n private readonly snackbarService: SnackbarService,\n @Inject(CONFIG_TOKEN) private readonly config: CONFIG_TYPE,\n ) {}\n\n async save(newTagName: string): Promise {\n const sender = await firstValueFrom(this.config.sender$);\n return firstValueFrom(this.campaignTaggingService.create(sender.type, sender.id, newTagName, defaultColour))\n .then((newTag: CampaignTag) => {\n this.snackbarService.openSuccessSnack('CAMPAIGN_DETAILS.ACTIONS.MANAGE_TAGS.TAG_CREATE_SUCCESS');\n this.senderTagsService.addTag({ tagId: newTag.tagId, tagName: newTag.text });\n this.dialogRef.close(newTag);\n })\n .catch((err) => {\n console.log(err);\n this.snackbarService.openErrorSnack('CAMPAIGN_DETAILS.ACTIONS.MANAGE_TAGS.TAG_CREATE_FAILURE');\n });\n }\n\n cancel(): void {\n this.dialogRef.close(null);\n }\n}\n","

{{ 'MANAGE_TAGS.ADD_NEW' | translate }}

\n\n \n\n\n\n \n \n\n","import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { MatCheckboxChange } from '@angular/material/checkbox';\nimport { CampaignTaggingService, Sender } from '@vendasta/campaigns';\nimport { PopoverPositions } from '@vendasta/galaxy/popover';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { Observable, of, throwError } from 'rxjs';\nimport { catchError, shareReplay, tap } from 'rxjs/operators';\n\nexport interface SenderTag {\n tagId: string;\n tagName: string;\n isBackCompatTag?: boolean;\n}\n\nexport interface TagData extends SenderTag {\n selected: boolean;\n}\n\n@Component({\n selector: 'campaign-tag-toggle',\n templateUrl: './tag-toggle.component.html',\n styleUrls: ['./tag-toggle.component.scss'],\n standalone: false,\n})\nexport class TagToggleComponent {\n public PopoverPositions = PopoverPositions;\n\n @Input() sender: Sender | undefined;\n @Input() campaignId: string | undefined;\n @Input() senderMode = false;\n @Input() tag: TagData | undefined;\n\n @Output() tagSelectionChanged = new EventEmitter();\n\n public popoverOpen = false;\n\n updateWork$: Observable = of(null);\n\n constructor(\n private readonly campaignTaggingService: CampaignTaggingService,\n private snackbarService: SnackbarService,\n ) {}\n\n openPopover(): void {\n if (this.tag.isBackCompatTag) {\n this.popoverOpen = true;\n }\n }\n\n closePopover(): void {\n this.popoverOpen = false;\n }\n\n async changeTag(tagId: string, event: MatCheckboxChange): Promise {\n let updateWork$: Observable;\n if (event.checked === true) {\n updateWork$ = this.campaignTaggingService.addTagToCampaign(\n this.sender.type,\n this.sender.id,\n this.campaignId,\n tagId,\n );\n } else {\n updateWork$ = this.campaignTaggingService.removeTagFromCampaign(this.campaignId, tagId);\n }\n updateWork$ = updateWork$.pipe(\n shareReplay({ bufferSize: 1, refCount: true }),\n tap(() => this.updateSelected(event.checked)),\n tap(() => {\n this.snackbarService.openSuccessSnack('CAMPAIGN_DETAILS.ACTIONS.MANAGE_TAGS.TAG_ADD_SUCCESS');\n }),\n catchError((err) => {\n this.snackbarService.openErrorSnack('CAMPAIGN_DETAILS.ACTIONS.MANAGE_TAGS.TAG_ADD_FAILURE');\n return throwError(err);\n }),\n );\n\n this.updateWork$ = updateWork$;\n }\n\n private updateSelected(selected: boolean): void {\n this.tagSelectionChanged.emit({\n selected: selected,\n tagId: this.tag.tagId,\n tagName: this.tag.tagName,\n });\n }\n}\n","\n
\n \n \n {{ tag.tagName }}\n \n \n\n \n \n \n \n \n
\n
\n\n\n {{ 'CAMPAIGN_DETAILS.ACTIONS.MANAGE_TAGS.BACK_COMPAT_POPOVER_HINT' | translate }}\n\n","import { Component, Inject, OnDestroy, OnInit, Optional } from '@angular/core';\nimport { FormControl } from '@angular/forms';\nimport { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { CampaignTag, CampaignTagInterface, CampaignTaggingService, SenderInterface } from '@vendasta/campaigns';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport Fuse from 'fuse.js';\nimport { Observable, Subject, Subscription, combineLatest, firstValueFrom, of } from 'rxjs';\nimport { debounceTime, map, scan, startWith, take, tap } from 'rxjs/operators';\nimport { AddNewTagDialogComponent } from '../tags/add-new-tag-dialog.component';\nimport { SenderTagsService } from '../tags/sender-tags.service';\nimport { SenderTag, TagData } from './tag-toggle/tag-toggle.component';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../shared/src/tokens';\n\nexport interface DialogData {\n campaignId: string;\n senderMode: boolean;\n}\n\nexport const TAG_CHANGES_LISTENER_TOKEN = 'com.vendasta.campaigns.actions.change_tags';\n\nexport interface TagChangesListener {\n handleTagsChanged(campaignId: string, allTags: TagData[]): void;\n}\n\nexport const SENDER_TAGS_TOKEN = 'com.vendasta.campaigns.manage_tags.sender_tags';\n\nexport interface UserToggledTagsMap {\n [tagId: string]: boolean; // boolean value of \"selected\"\n}\n\n@Component({\n templateUrl: 'manage-tags.component.html',\n styleUrls: ['manage-tags.component.scss'],\n standalone: false,\n})\nexport class ManageTagsComponent implements OnInit, OnDestroy {\n userModifiedTagEvents$$: Subject = new Subject();\n tags$: Observable = new Observable();\n searchBarControl: FormControl = new FormControl();\n subscriptions: Subscription[] = [];\n readonly sender$: Observable;\n\n public static openAndAddToURL(\n dialog: MatDialog,\n router: Router,\n activatedRoute: ActivatedRoute,\n campaignId: string,\n ): MatDialogRef {\n const dialogRef = dialog.open(ManageTagsComponent, {\n data: {\n campaignId: campaignId,\n },\n width: '800px',\n });\n this.addToURL(router, activatedRoute, campaignId);\n dialogRef\n .afterClosed()\n .pipe(take(1))\n .subscribe(() => this.removeFromURL(router, activatedRoute));\n return dialogRef;\n }\n\n private static addToURL(router: Router, activatedRoute: ActivatedRoute, campaignId: string): void {\n router.navigate([], {\n relativeTo: activatedRoute,\n queryParams: {\n manageTagsForCampaignId: campaignId,\n },\n queryParamsHandling: 'merge',\n skipLocationChange: true,\n });\n }\n\n private static removeFromURL(router: Router, activatedRoute: ActivatedRoute): void {\n router.navigate([], {\n relativeTo: activatedRoute,\n queryParams: {\n manageTagsForCampaignId: null,\n },\n queryParamsHandling: 'merge',\n skipLocationChange: true,\n });\n }\n\n constructor(\n @Inject(MAT_DIALOG_DATA) public readonly data: DialogData,\n public dialogRef: MatDialogRef,\n private readonly campaignTaggingService: CampaignTaggingService,\n private readonly dialog: MatDialog,\n private senderTagService: SenderTagsService,\n private snackbarService: SnackbarService,\n @Inject(SENDER_TAGS_TOKEN) private readonly senderTags$: Observable,\n @Inject(CONFIG_TOKEN) private readonly config: CONFIG_TYPE,\n @Optional() @Inject(TAG_CHANGES_LISTENER_TOKEN) private readonly tagChangesListener?: TagChangesListener,\n ) {\n this.sender$ = config.sender$;\n }\n\n ngOnInit(): void {\n const allTagsFromServer$ = this.loadAllTagsFromServer();\n\n const userToggledTags$ = this.userModifiedTagEvents$$.pipe(\n // builds up a map of selected/deselected tags from each toggle event\n scan((acc: UserToggledTagsMap, value: TagData) => {\n acc[value.tagId] = value.selected;\n return acc;\n }, {}),\n startWith({}),\n );\n\n const allTagsToggledByUser$ = combineLatest([allTagsFromServer$, userToggledTags$]).pipe(\n map(([senderTags, toggled]) => applyUserSelectionToTags(senderTags, toggled)),\n tap((allTags) => this.tagChangesListener?.handleTagsChanged(this.data.campaignId, allTags)),\n );\n\n this.tags$ = combineLatest([\n this.searchBarControl.valueChanges.pipe(startWith(''), debounceTime(200)),\n allTagsToggledByUser$,\n ]).pipe(map(([searchTerm, allTags]) => this.filterTagsBySearchTerm(searchTerm, allTags)));\n }\n\n async deleteTag(tag: SenderTag): Promise {\n const sender = await firstValueFrom(this.config.sender$);\n firstValueFrom(this.campaignTaggingService.deleteTag(sender.type, sender.id, tag.tagId))\n .catch(() => {\n this.snackbarService.openErrorSnack('MANAGE_TAGS.TAG_DELETE_FAILURE');\n })\n .then(() => {\n this.senderTagService.deleteTag(tag);\n this.snackbarService.openSuccessSnack('MANAGE_TAGS.TAG_DELETE_SUCCESS');\n });\n }\n\n loadAllTagsFromServer(): Observable {\n return combineLatest([\n this.senderTags$,\n this.data.campaignId\n ? this.campaignTaggingService.listTagsForCampaign(this.data.campaignId, '', 10)\n : of({ campaignTags: [] }),\n ]).pipe(\n map(([senderTags, campaign]) => {\n if (campaign) {\n return this.applyCampaignSelectionToTags(senderTags, campaign?.campaignTags);\n }\n return this.applyCampaignSelectionToTags(senderTags, []);\n }),\n );\n }\n\n /**\n * Returns the set of tags available to the sender with a \"selected\" boolean\n * set to \"true\" for each tag that is also present on the campaign.\n */\n private applyCampaignSelectionToTags(tagsAvailableToSender: SenderTag[], tagsOnCampaign: CampaignTag[]): TagData[] {\n return tagsAvailableToSender.map((tag) => {\n const selected = this.checkIfCampaignUsesTag(tag, tagsOnCampaign);\n return {\n tagName: tag.tagName,\n selected: selected,\n tagId: tag.tagId,\n isBackCompatTag: tag.isBackCompatTag,\n };\n });\n }\n\n checkIfCampaignUsesTag(senderTag: CampaignTagInterface, campaignTags: CampaignTagInterface[]): boolean {\n return campaignTags.some((campaignTag) => campaignTag.tagId === senderTag.tagId);\n }\n\n filterTagsBySearchTerm(searchTerm: string, tags: TagData[]): TagData[] {\n if (!searchTerm) {\n return tags;\n }\n const fuse = new Fuse(tags, {\n keys: ['tagName'],\n threshold: 0.3,\n ignoreLocation: true,\n });\n return fuse.search(searchTerm).map((result) => result.item);\n }\n\n async openNewTagDialog(): Promise {\n this.dialog.open(AddNewTagDialogComponent, {\n width: '300px',\n });\n }\n\n handleTagUpdated(tag: TagData): void {\n this.userModifiedTagEvents$$.next(tag);\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((sub) => sub.unsubscribe());\n }\n}\n\n/**\n * If a tag ID is present in \"userToggledTags\", then its \"selected\" value\n * will be used. Otherwise, the original \"selected\" value will be used.\n */\nexport function applyUserSelectionToTags(tags: TagData[], userToggledTags: UserToggledTagsMap): TagData[] {\n return tags.map((tag) => ({\n tagId: tag.tagId,\n selected: useIfPresentElse(userToggledTags[tag.tagId], tag.selected),\n tagName: tag.tagName,\n isBackCompatTag: tag.isBackCompatTag,\n }));\n}\n\nfunction useIfPresentElse(useIfPresent: boolean | undefined, elseUse: boolean): boolean {\n if (useIfPresent === undefined) {\n return elseUse;\n }\n return useIfPresent;\n}\n","

{{ 'MANAGE_TAGS.MANAGE_TAGS' | translate }}

\n\n\n \n \n \n
\n \n \n delete\n \n
\n
\n
\n
\n\n \n \n\n","import { Inject, Injectable } from '@angular/core';\nimport {\n CheckboxFilterControl,\n FilterGroup,\n SearchFilterControl,\n SearchSelectFilterControl,\n} from '@vendasta/va-filter2';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { SenderTag } from '../manage-tags/tag-toggle/tag-toggle.component';\n\nexport const SENDER_TAGS_TOKEN = 'com.vendasta.partner_center.campaigns.table_filter.sender_tags';\n\n@Injectable()\nexport class MyCampaignsTableFilterService {\n filters: FilterGroup = new FilterGroup('my-campaigns-table');\n\n constructor(@Inject(SENDER_TAGS_TOKEN) senderTags$: Observable) {\n this.filters\n .addToolbarSection(new SearchFilterControl('search', 'Search Campaigns'))\n .addSection('Tags', [\n new SearchSelectFilterControl('tag', 'Tag', this.tagFilterOptions(senderTags$), null, {\n searchDisplay: (v: SenderTag) => v?.tagName,\n appliedValueMapper: (name: string, value: SenderTag) => {\n const uppercaseName = name.charAt(0).toUpperCase() + name.slice(1);\n return {\n name: name,\n value: value,\n label: value ? `${uppercaseName}: ${value.tagName}` : null,\n };\n },\n }),\n ])\n .addSection('Status', [\n new CheckboxFilterControl('Published', 'Published', true),\n new CheckboxFilterControl('Draft', 'Draft', true),\n new CheckboxFilterControl('Ongoing', 'Ongoing', true),\n new CheckboxFilterControl('Archived', 'Archived', false),\n ]);\n }\n\n tagFilterOptions(senderTags$: Observable): Observable> {\n return senderTags$.pipe(\n map((st) => {\n const tagsMap = new Map();\n tagsMap.set('All tags', null);\n st.forEach((t) => tagsMap.set(t.tagName, t));\n return tagsMap;\n }),\n );\n }\n}\n","import { CommonModule } from '@angular/common';\nimport { Component, Inject, OnDestroy, OnInit } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';\nimport { TranslateModule, TranslateService } from '@ngx-translate/core';\nimport { AsyncWorkStatus, CampaignService, SenderInterface } from '@vendasta/campaigns';\nimport { CheckDuplicateCampaignStatusResponse } from '@vendasta/campaigns/lib/_internal/objects/api';\nimport { GalaxyLoadingSpinnerModule } from '@vendasta/galaxy/loading-spinner';\nimport { SubscriptionList } from '@vendasta/rx-utils';\nimport { EMPTY, firstValueFrom, interval } from 'rxjs';\nimport { catchError, filter, shareReplay, switchMap, take, takeUntil } from 'rxjs/operators';\n\n// If copying this component a third time, consider implementing an abstraction.\nexport interface Data {\n campaignId: string;\n sender: SenderInterface;\n}\n\nexport interface DialogResult {\n newCampaignId: string;\n}\n\n@Component({\n imports: [CommonModule, MatDialogModule, MatButtonModule, TranslateModule, GalaxyLoadingSpinnerModule],\n templateUrl: './campaign-duplicate-dialog.component.html',\n styleUrls: ['./campaign-duplicate-dialog.component.scss'],\n})\nexport class CampaignDuplicateDialogComponent implements OnInit, OnDestroy {\n errored = false;\n completed = false;\n waitingMessage = '';\n\n private readonly subs = SubscriptionList.new();\n\n constructor(\n @Inject(MAT_DIALOG_DATA) private readonly data: Data,\n readonly ref: MatDialogRef,\n private readonly campaigns: CampaignService,\n private readonly translate: TranslateService,\n ) {}\n\n async ngOnInit(): Promise {\n this.waitingMessage = this.translate.instant('CAMPAIGN_DETAILS.ACTIONS.COPY_DIALOG.PLEASE_WAIT');\n window.setTimeout(() => {\n this.waitingMessage = this.translate.instant('CAMPAIGN_DETAILS.ACTIONS.COPY_DIALOG.LONG_WAIT');\n }, 10000);\n await this.requestAndPoll();\n }\n\n async requestAndPoll(): Promise {\n this.reset();\n\n const workflowID = firstValueFrom(this.campaigns.duplicateCampaignAsyncV2(this.data.campaignId, this.data.sender));\n workflowID\n .then(\n (id: string) => this.pollForStatus(id), //\n )\n .catch(\n () => (this.errored = true), //\n );\n }\n\n private reset(): void {\n this.errored = false;\n this.completed = false;\n this.subs.destroy();\n }\n\n private pollForStatus(id: string): void {\n const poll = interval(3000).pipe(\n switchMap(() => {\n return this.campaigns.checkCampaignDuplicateStatus(id);\n }),\n catchError((err) => {\n console.error(err);\n return EMPTY; // Keep polling\n }),\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n\n const error = poll.pipe(\n filter((v) => v.status === AsyncWorkStatus.ASYNC_WORK_STATUS_ERROR),\n take(1),\n shareReplay({ bufferSize: 1, refCount: true }),\n );\n\n const pollUntilErr = poll.pipe(takeUntil(error));\n\n this.subs.add(pollUntilErr, (v: CheckDuplicateCampaignStatusResponse) => {\n if (v.status === AsyncWorkStatus.ASYNC_WORK_STATUS_COMPLETE) {\n const res: DialogResult = { newCampaignId: v.campaignId };\n this.ref.close(res);\n }\n });\n this.subs.add(error, () => (this.errored = true));\n }\n\n ngOnDestroy(): void {\n this.subs.destroy();\n }\n}\n","\n

\n {{ waitingMessage }}\n \n

\n

\n {{ 'CAMPAIGN_DETAILS.ACTIONS.COPY_DIALOG.ERROR' | translate }}\n

\n
\n\n\n \n\n","import { CommonModule } from '@angular/common';\nimport { Component, Inject } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';\nimport { Router } from '@angular/router';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { firstValueFrom } from 'rxjs';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../../../shared/src/tokens';\n\nexport interface Data {\n newCampaignId: string;\n}\n\n@Component({\n templateUrl: './campaign-duplicate-success-dialog.component.html',\n styleUrls: ['./campaign-duplicate-success-dialog.component.scss'],\n imports: [CommonModule, MatDialogModule, MatButtonModule, TranslateModule],\n})\nexport class CampaignDuplicateSuccessDialogComponent {\n private readonly campaignId: string;\n\n constructor(\n @Inject(MAT_DIALOG_DATA) private readonly data: Data,\n @Inject(CONFIG_TOKEN) private readonly config: CONFIG_TYPE,\n private readonly ref: MatDialogRef,\n private readonly router: Router,\n ) {\n this.campaignId = data.newCampaignId;\n }\n\n async goToNewCampaign(): Promise {\n const basePath = await firstValueFrom(this.config.basePath$);\n await this.router\n .navigate([basePath, this.campaignId, 'details'])\n .then(() => this.ref.close())\n .catch((err) => console.error(err));\n }\n\n closeDialog(): void {\n this.ref.close();\n }\n}\n","

{{ 'CAMPAIGN_DETAILS.ACTIONS.COPY_DIALOG_SUCCESS.SUCCESS_TITLE' | translate }}

\n\n

{{ 'CAMPAIGN_DETAILS.ACTIONS.COPY_DIALOG_SUCCESS.OPEN_COPIED_CAMPAIGN' | translate }}

\n
\n\n\n \n \n\n","import { EnvironmentProviders, Provider } from '@angular/core';\nimport { MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS } from '@angular/material/button-toggle';\nimport { MAT_CARD_CONFIG } from '@angular/material/card';\nimport { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';\nimport { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';\nimport { MAT_RADIO_DEFAULT_OPTIONS } from '@angular/material/radio';\nimport { MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS } from '@angular/material/slide-toggle';\nimport { MAT_TABS_CONFIG } from '@angular/material/tabs';\nimport { MAT_TOOLTIP_DEFAULT_OPTIONS } from '@angular/material/tooltip';\n\nexport const GalaxyDefaultProviderOverrides: (Provider | EnvironmentProviders)[] = [\n {\n // Vendasta design settled on \"outlined\" as the default appearance for cards,\n // but the angular default is \"raised\".\n provide: MAT_CARD_CONFIG,\n useValue: { appearance: 'outlined' },\n },\n {\n // We want our checkboxes to be primary colored by default.\n provide: MAT_CHECKBOX_DEFAULT_OPTIONS,\n useValue: { color: 'primary' },\n },\n {\n // We want our radio buttons to be primary colored by default.\n provide: MAT_RADIO_DEFAULT_OPTIONS,\n useValue: { color: 'primary' },\n },\n {\n // We want our slide toggles to be primary colored by default.\n provide: MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS,\n useValue: { color: 'primary' },\n },\n {\n // The Mat tabs animation can look very chunky/stuttery when switching tabs if the tabs\n // have a lot of content. Disabling the animation makes the UI feel faster. The default\n // animationDuration is '500ms'\n //\n // In the Material 15 update, tabs stretch by default. This is a problem\n // as it can look weird when there are only a few tabs which take up the whole\n // width of the tab group. In Material 16, we will be able to override this behavior.\n provide: MAT_TABS_CONFIG,\n useValue: {\n animationDuration: '0ms',\n stretchTabs: false,\n },\n },\n {\n // Mat tooltip recently changed its default behavior and now allows you to\n // interact with the tooltip by default. This causes the tooltip to get in the\n // way of elements under it (like butons) and can be a hassle.\n // Disabling this new behavior makes the tooltip behave like it did before.\n provide: MAT_TOOLTIP_DEFAULT_OPTIONS,\n useValue: { disableTooltipInteractivity: true },\n },\n {\n // Vendasta design settled on \"outline\" as the default appearance for form fields,\n // but the angular default is \"fill\". This rule overrides the default to be \"outline\".\n provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,\n useValue: { appearance: 'outline' },\n },\n {\n // Don't show indicator ✓ on button toggles by default\n provide: MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS,\n useValue: { hideMultipleSelectionIndicator: true, hideSingleSelectionIndicator: true },\n },\n];\n","export { GalaxyDefaultProviderOverrides } from './src/provider-default-overrides';\n","// Export things like public API, and interfaces from here.\nexport * from './public_api';\n","import { Directive, ViewContainerRef } from '@angular/core';\n\n@Directive({\n selector: '[campaignPreviewDataSelector]',\n standalone: true,\n})\nexport class CampaignPreviewDataSelectorDirective {\n constructor(public viewContainerRef: ViewContainerRef) {}\n}\n","import { Pipe, PipeTransform } from '@angular/core';\nimport { TranslateService } from '@ngx-translate/core';\nimport { CampaignStepInterface } from '@vendasta/campaigns/lib/_internal/objects/api';\n\nconst ONE_DAY_IN_SECONDS = 86400;\n\n@Pipe({\n name: 'eventDay',\n standalone: true,\n})\nexport class EventDayPipe implements PipeTransform {\n constructor(private translate: TranslateService) {}\n\n transform(schedule: CampaignStepInterface[], index: number): string {\n const secondsDelay = schedule\n .slice(0, index)\n .map((x) => x.secondsAfterLastEmail || 0)\n .reduce((a, b) => a + b, 0);\n console.log(`schedule`, schedule);\n console.log(`secondsDelay[${index}]`, secondsDelay);\n const daysDifferent = 1 + secondsDelay / ONE_DAY_IN_SECONDS;\n const day = this.translate.instant('CAMPAIGN_DETAILS.STEP.DAY');\n return `${day} ${daysDifferent}`;\n }\n}\n","export interface NonEmailCampaignStep {\n // No title found here since it is passed back via the details endpoint\n title?: string;\n icon: {\n name: string;\n color: string;\n };\n description: string;\n}\n\nexport const NonEmailCampaignSteps: { [key: string]: NonEmailCampaignStep } = {\n 'snapshot-creation': {\n icon: {\n name: 'note_add',\n color: '#512C8C',\n },\n description:\n 'Create or refresh the Snapshot Report if it is older than 30 days.
There is no email to preview for this event.',\n },\n};\n","import { CommonModule } from '@angular/common';\nimport { Component, Inject, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatProgressSpinnerModule } from '@angular/material/progress-spinner';\nimport { MatSelectModule } from '@angular/material/select';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { AccountGroup } from '@galaxy/account-group';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { CampaignService, CampaignStepType, GetterCampaignData } from '@vendasta/campaigns';\nimport { GalaxyAlertModule } from '@vendasta/galaxy/alert';\nimport { GalaxyButtonLoadingIndicatorModule } from '@vendasta/galaxy/button-loading-indicator';\nimport { GalaxyEmailViewerModule } from '@vendasta/galaxy/email-viewer';\nimport { GalaxyInputModule } from '@vendasta/galaxy/input';\nimport { GalaxyDefaultProviderOverrides } from '@vendasta/galaxy/provider-default-overrides';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { BehaviorSubject, EMPTY, Observable, Subscription, combineLatest, firstValueFrom, of } from 'rxjs';\nimport { filter, map, shareReplay, switchMap, take, tap } from 'rxjs/operators';\nimport {\n PREVIEW_DATA_SELECTOR_COMPONENT_TOKEN,\n PREVIEW_DATA_SELECTOR_COMPONENT_TYPE,\n PREVIEW_DATA_TOKEN,\n PREVIEW_DATA_TYPE,\n} from '../dependencies';\nimport { CampaignPreviewDataSelectorDirective } from './campaign-preview-data-selector.directive';\nimport { EventDayPipe } from './event-day.pipe';\nimport { NonEmailCampaignStep, NonEmailCampaignSteps } from './non-email-steps';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../shared/src/tokens';\n\nexport interface PreviewCampaignDialogData {\n campaignId: string;\n templateId?: string;\n}\n\nexport interface PreviewData {\n html: string;\n name: string;\n subject: string;\n}\n\n@Component({\n templateUrl: './campaign-preview-dialog.component.html',\n styleUrls: ['./campaign-preview-dialog.component.scss'],\n providers: [GalaxyDefaultProviderOverrides],\n imports: [\n CommonModule,\n TranslateModule,\n MatProgressSpinnerModule,\n MatButtonModule,\n MatFormFieldModule,\n MatIconModule,\n MatDialogModule,\n MatSelectModule,\n GalaxyEmailViewerModule,\n GalaxyInputModule,\n GalaxyAlertModule,\n EventDayPipe,\n CampaignPreviewDataSelectorDirective,\n GalaxyButtonLoadingIndicatorModule,\n ],\n})\nexport class CampaignPreviewDialogComponent implements OnInit, OnDestroy {\n public static openAndAddToURL(\n dialog: MatDialog,\n router: Router,\n activatedRoute: ActivatedRoute,\n campaignId: string,\n templateId?: string,\n ): MatDialogRef {\n const data: PreviewCampaignDialogData = {\n campaignId: campaignId,\n templateId: templateId,\n };\n const dialogRef = dialog.open(CampaignPreviewDialogComponent, {\n data: data,\n });\n this.addToURL(router, activatedRoute, campaignId);\n dialogRef\n .afterClosed()\n .pipe(take(1))\n .subscribe(() => this.removeFromURL(router, activatedRoute));\n return dialogRef;\n }\n\n private static addToURL(router: Router, activatedRoute: ActivatedRoute, campaignId: string): void {\n router.navigate([], {\n relativeTo: activatedRoute,\n queryParams: {\n previewForCampaignId: campaignId,\n },\n queryParamsHandling: 'merge',\n skipLocationChange: true,\n });\n }\n\n private static removeFromURL(router: Router, activatedRoute: ActivatedRoute): void {\n router.navigate([], {\n relativeTo: activatedRoute,\n queryParams: {\n previewForCampaignId: null,\n },\n queryParamsHandling: 'merge',\n skipLocationChange: true,\n });\n }\n\n @ViewChild(CampaignPreviewDataSelectorDirective, { static: true }) stepDataHost:\n | CampaignPreviewDataSelectorDirective\n | undefined;\n\n campaignDetails$: Observable = EMPTY;\n\n private selectedStepIndex$$: BehaviorSubject = new BehaviorSubject(0);\n private selectedStepIndex$: Observable = this.selectedStepIndex$$.asObservable();\n\n previewData$: Observable = EMPTY;\n\n sendingEmails = false;\n\n campaignStepType = CampaignStepType;\n currentStepType = CampaignStepType.CAMPAIGN_STEP_TYPE_EMAIL;\n nonEmailStepDetails: NonEmailCampaignStep | undefined;\n loadingEmail = false;\n\n subscriptions: Subscription[] = [];\n\n constructor(\n public dialogRef: MatDialogRef,\n @Inject(MAT_DIALOG_DATA) public data: PreviewCampaignDialogData,\n private readonly snackbarService: SnackbarService,\n private readonly campaignsSDK: CampaignService,\n @Inject(CONFIG_TOKEN) private readonly config: CONFIG_TYPE,\n @Optional()\n @Inject(PREVIEW_DATA_SELECTOR_COMPONENT_TOKEN)\n private readonly dataComponent?: PREVIEW_DATA_SELECTOR_COMPONENT_TYPE,\n @Optional()\n @Inject(PREVIEW_DATA_TOKEN)\n private data$: PREVIEW_DATA_TYPE = EMPTY,\n ) {\n if (!this.dataComponent && !this.data$) {\n throw new Error(\n `No provider for: ` +\n `[${PREVIEW_DATA_TOKEN}, ${PREVIEW_DATA_SELECTOR_COMPONENT_TOKEN}]. ` +\n `Exactly one must be provided.`,\n );\n }\n if (this.dataComponent && this.data$) {\n throw new Error(\n `A provider is present for both: ` +\n `[${PREVIEW_DATA_TOKEN}, ${PREVIEW_DATA_SELECTOR_COMPONENT_TOKEN}]. ` +\n `Exactly one must be provided.`,\n );\n }\n\n this.campaignDetails$ = this.campaignsSDK\n .get(this.data.campaignId)\n .pipe(shareReplay({ bufferSize: 1, refCount: true }));\n }\n\n ngOnInit(): void {\n let stepData$: PREVIEW_DATA_TYPE;\n if (this.dataComponent) {\n const component = this.stepDataHost!.viewContainerRef.createComponent(this.dataComponent);\n stepData$ = component.instance.data$;\n } else {\n stepData$ = this.data$!;\n }\n this.data$ = stepData$.pipe(shareReplay({ bufferSize: 1, refCount: true }));\n\n this.previewData$ = combineLatest([\n this.campaignDetails$ || EMPTY, //\n this.selectedStepIndex$,\n this.data$,\n ]).pipe(\n filter(([details]) => !!details),\n tap(() => (this.loadingEmail = true)),\n switchMap(([details, index, stepData]) => {\n // No steps found, output that\n if (!details.campaignSchedule[index]) {\n this.loadingEmail = false;\n return of({} as PreviewData);\n }\n // Non email displaying step\n if (details.campaignSchedule[index].stepType !== CampaignStepType.CAMPAIGN_STEP_TYPE_EMAIL) {\n this.loadingEmail = false;\n this.currentStepType = CampaignStepType.CAMPAIGN_STEP_TYPE_UNSPECIFIED;\n this.nonEmailStepDetails = NonEmailCampaignSteps[details.campaignSchedule[index].stepType];\n this.nonEmailStepDetails.title = details.campaignSchedule[index].name;\n return of({} as PreviewData);\n }\n this.currentStepType = CampaignStepType.CAMPAIGN_STEP_TYPE_EMAIL;\n return this.campaignsSDK\n .getEmailStepContent(details.campaignId, details.campaignSchedule[index].campaignStepId, stepData)\n .pipe(\n map(\n (resp) =>\n ({\n name: details.campaignSchedule[index].name,\n subject: resp.subject,\n html: resp.html,\n }) as PreviewData,\n ),\n );\n }),\n tap(() => {\n this.loadingEmail = false;\n }),\n shareReplay(1),\n );\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((s) => s.unsubscribe());\n }\n\n stepChanged(index: number): void {\n this.selectedStepIndex$$.next(index);\n }\n\n displayFn(accountGroup?: AccountGroup): string | undefined {\n return accountGroup ? accountGroup.napData.companyName : 'Generic account';\n }\n\n async sendTestEmail(): Promise {\n const details = await firstValueFrom(this.campaignDetails$);\n this.dialogRef.disableClose = true;\n\n // const selectedTemplateId = await firstValueFrom(this.selectedStepIndex$);\n // const currentTemplate = this.data.templateId || details.campaignSchedule[selectedTemplateId].templateId;\n this.sendingEmails = true;\n const sender = await firstValueFrom(this.config.sender$);\n const currentStepIndex = await firstValueFrom(this.selectedStepIndex$);\n const currentStepId = details.campaignSchedule[currentStepIndex].campaignStepId;\n const currentUserId = await firstValueFrom(this.config.userId$);\n const stepData = await firstValueFrom(this.data$);\n await firstValueFrom(\n this.campaignsSDK.sendStepTestEmail(sender, details.campaignId, currentStepId, currentUserId, stepData),\n )\n .catch((err) => {\n this.snackbarService.openErrorSnack('CAMPAIGN_GENERIC.TEST_EMAIL.FAILURE', {\n duration: 5000,\n interpolateTranslateParams: {\n message: err.error.message,\n },\n });\n throw err;\n })\n .then(() => {\n this.snackbarService.openSuccessSnack('CAMPAIGN_GENERIC.TEST_EMAIL.SUCCESS');\n })\n .finally(() => {\n this.sendingEmails = false;\n });\n }\n}\n","
\n close\n

\n \n {{ 'CAMPAIGN_GENERIC.TEST_EMAIL.TITLE' | translate : { name: details.name } }}\n \n

\n\n
\n
\n 0\">\n \n \n {{\n 'CAMPAIGN_GENERIC.TEST_EMAIL.COUNT' | translate : { curr: i + 1, total: details.campaignSchedule.length }\n }}\n ({{ details.campaignSchedule | eventDay : i + 1 }})\n \n \n \n
\n \n \n {{ 'CAMPAIGN_GENERIC.TEST_EMAIL.SEND_EMAIL' | translate }}\n \n \n
\n\n
\n \n
\n\n \n

\n \n {{ 'CAMPAIGN_GENERIC.TEST_EMAIL.SUBJECT' | translate }}\n \n \n {{ data.subject }}\n \n

\n
\n
\n\n \n \n \n\n
\n {{ 'CAMPAIGN_GENERIC.TEST_EMAIL.NO_EVENTS' | translate }}\n
\n
\n \n {{ nonEmailStepDetails?.icon?.name }}\n \n
\n {{ nonEmailStepDetails?.title! }}\n
\n
\n
\n
\n
\n \n \n \n
\n
\n\n\n
\n
\n","import { Input, Directive } from '@angular/core';\nimport { ColumnDefinition, CustomCellComponent } from '@vendasta/va-filter2-table';\nimport { TranslateService } from '@ngx-translate/core';\nimport { ListCampaignStatsStructInterface } from '@vendasta/campaigns/lib/_internal/interfaces/api.interface';\n\n@Directive()\nexport class MyCampaignsColumnDirective implements CustomCellComponent {\n @Input() columnDefinition: ColumnDefinition;\n @Input() translateService: TranslateService;\n @Input() element: ListCampaignStatsStructInterface;\n}\n","import { CommonModule } from '@angular/common';\nimport { Component, EventEmitter, Inject, OnDestroy, Output } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatDialog } from '@angular/material/dialog';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatMenuModule } from '@angular/material/menu';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { Statuses } from '@vendasta/campaigns';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { Subscription, firstValueFrom } from 'rxjs';\nimport { take } from 'rxjs/operators';\nimport {\n CampaignDuplicateDialogComponent,\n DialogResult,\n} from '../../campaign-details-page/campaign-details/campaign-duplicate-dialog/campaign-duplicate-dialog.component';\nimport { CampaignDuplicateSuccessDialogComponent } from '../../campaign-details-page/campaign-details/campaign-duplicate-success-dialog/campaign-duplicate-success-dialog.component';\nimport { CampaignsService } from '../../campaign-details-page/campaign-details/campaigns.service';\nimport {\n DuplicateCampaignData,\n DuplicateCampaignSuccessData,\n} from '../../campaign-details-page/campaign-details/interface';\nimport { CampaignPreviewDialogComponent } from '../../campaign-preview-dialog/campaign-preview-dialog.component';\nimport { ManageTagsComponent } from '../../manage-tags/manage-tags.component';\nimport { ConfirmationDialogComponent } from './dialog/confirmation-dialog.component';\nimport { MyCampaignsColumnDirective } from './my-campaigns-column.directive';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../../shared/src/tokens';\n\n@Component({\n templateUrl: './my-campaigns-actions-column.component.html',\n styleUrls: ['../campaign-list-page.component.scss'],\n imports: [CommonModule, MatIconModule, MatMenuModule, TranslateModule, MatButtonModule],\n})\nexport class MyCampaignsActionsColumnComponent extends MyCampaignsColumnDirective implements OnDestroy {\n @Output() eventEmitter = new EventEmitter();\n subscriptions: Subscription[] = [];\n Statuses = Statuses;\n isHidden = false;\n\n constructor(\n private readonly matDialog: MatDialog,\n private readonly router: Router,\n private readonly route: ActivatedRoute,\n private readonly snackbarService: SnackbarService,\n @Inject(CONFIG_TOKEN) private readonly config: CONFIG_TYPE,\n private readonly campaignsService: CampaignsService,\n ) {\n super();\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((s) => s.unsubscribe());\n }\n\n previewCampaign(campaignId: string): void {\n CampaignPreviewDialogComponent.openAndAddToURL(this.matDialog, this.router, this.route, campaignId);\n }\n\n async duplicateCampaign(campaignId: string): Promise {\n const data: DuplicateCampaignData = {\n campaignId: campaignId,\n sender: await firstValueFrom(this.config.sender$),\n };\n const ref = this.matDialog.open(CampaignDuplicateDialogComponent, { data: data });\n const result: DialogResult = await firstValueFrom(ref.afterClosed());\n if (result?.newCampaignId) {\n const successData: DuplicateCampaignSuccessData = {\n newCampaignId: result.newCampaignId,\n };\n this.matDialog.open(CampaignDuplicateSuccessDialogComponent, { data: successData });\n }\n }\n\n async pauseCampaign(campaignId: string, campaignName: string): Promise {\n const dialogRef = this.matDialog.open(ConfirmationDialogComponent, {\n data: {\n title: this.translateService.instant('CAMPAIGN_DETAILS.PAUSE.TITLE', { title: campaignName }),\n message: this.translateService.instant('CAMPAIGN_DETAILS.PAUSE.MESSAGE'),\n buttonText: {\n confirm: this.translateService.instant('CAMPAIGN_DETAILS.PAUSE.CONFIRM'),\n decline: this.translateService.instant('CAMPAIGN_DETAILS.PAUSE.DECLINE'),\n },\n },\n maxWidth: '600px',\n });\n const dialogResult = await dialogRef.afterClosed().toPromise();\n if (dialogResult) {\n this.campaignsService.pauseCampaign(campaignId).subscribe({\n next: () => {\n this.snackbarService.openSuccessSnack('CAMPAIGN_DETAILS.PAUSE.SUCCESS');\n },\n error: () => {\n this.snackbarService.openErrorSnack('CAMPAIGN_DETAILS.PAUSE.FAILURE');\n },\n });\n }\n }\n\n async resumeCampaign(campaignId: string, campaignName: string): Promise {\n const dialogRef = this.matDialog.open(ConfirmationDialogComponent, {\n data: {\n title: this.translateService.instant('CAMPAIGN_DETAILS.RESUME.TITLE', { title: campaignName }),\n message: this.translateService.instant('CAMPAIGN_DETAILS.RESUME.MESSAGE'),\n buttonText: {\n confirm: this.translateService.instant('CAMPAIGN_DETAILS.RESUME.CONFIRM'),\n decline: this.translateService.instant('CAMPAIGN_DETAILS.RESUME.DECLINE'),\n },\n },\n maxWidth: '600px',\n });\n const dialogResult = await firstValueFrom(dialogRef.afterClosed());\n if (dialogResult) {\n this.campaignsService.unpauseCampaign(campaignId).subscribe({\n next: () => {\n this.snackbarService.openSuccessSnack('CAMPAIGN_DETAILS.RESUME.SUCCESS');\n },\n error: () => {\n this.snackbarService.openErrorSnack('CAMPAIGN_DETAILS.RESUME.FAILURE');\n },\n });\n }\n }\n\n async archiveCampaign(campaignId: string, campaignName: string): Promise {\n const dialogRef = this.matDialog.open(ConfirmationDialogComponent, {\n data: {\n title: this.translateService.instant('CAMPAIGN_DETAILS.CONFIRMATION_DIALOG.ARCHIVE.TITLE', {\n title: campaignName,\n }),\n message: this.translateService.instant('CAMPAIGN_DETAILS.CONFIRMATION_DIALOG.ARCHIVE.MESSAGE'),\n warn: true,\n buttonText: {\n confirm: this.translateService.instant('CAMPAIGN_DETAILS.CONFIRMATION_DIALOG.ARCHIVE.CONFIRM'),\n decline: this.translateService.instant('CAMPAIGN_DETAILS.CONFIRMATION_DIALOG.ARCHIVE.DECLINE'),\n },\n },\n maxWidth: '600px',\n });\n const dialogResult = await firstValueFrom(dialogRef.afterClosed());\n if (dialogResult) {\n // TODO: Update campaign status via campaigns microservice [MAEL-405] [MAEL-406]\n this.snackbarService.openErrorSnack('Not implemented');\n // const pauseResult = await firstValueFrom(\n // return this.marketingAutomationService.archiveCampaign(campaignId, this.partnerService.partnerId);\n // ).catch(err => {\n // console.error(err);\n // this.snackbarService.openErrorSnack('CAMPAIGN_DETAILS.ACTIONS.ARCHIVE_FAILURE');\n // }).then(() => {\n // this.translateService.instant('CAMPAIGN_DETAILS.ACTIONS.ARCHIVE_SUCCESS'),\n // });\n }\n }\n\n async deleteCampaign(campaignId: string, campaignName: string): Promise {\n const dialogRef = this.matDialog.open(ConfirmationDialogComponent, {\n data: {\n title: this.translateService.instant('CAMPAIGN_DETAILS.CONFIRMATION_DIALOG.DELETE_CAMPAIGN.TITLE', {\n title: campaignName,\n }),\n message: this.translateService.instant('CAMPAIGN_DETAILS.CONFIRMATION_DIALOG.DELETE_CAMPAIGN.MESSAGE'),\n warn: true,\n buttonText: {\n confirm: this.translateService.instant('CAMPAIGN_DETAILS.CONFIRMATION_DIALOG.DELETE_CAMPAIGN.CONFIRM'),\n decline: this.translateService.instant('CAMPAIGN_DETAILS.CONFIRMATION_DIALOG.DELETE_CAMPAIGN.DECLINE'),\n },\n },\n maxWidth: '600px',\n });\n const dialogResult = await firstValueFrom(dialogRef.afterClosed());\n if (dialogResult) {\n // TODO: Update campaign status via campaigns microservice [MAEL-405] [MAEL-406]\n this.snackbarService.openErrorSnack('Not implemented');\n // const pauseResult = await firstValueFrom(\n // return this.marketingAutomationService.deleteCampaign(campaignId);\n // ).catch(err => {\n // console.error(err);\n // this.snackbarService.openErrorSnack('CAMPAIGN_DETAILS.ACTIONS.DELETE_FAILURE');\n // }).then(() => {\n // this.snackbarService.openSuccessSnack('CAMPAIGN_DETAILS.ACTIONS.DELETE_SUCCESS');\n // this.eventEmitter.emit(true);\n // });\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n openAddListToCampaignModal(campaignId: string): void {\n // TODO: Implement \"add recipients to campaign\"\n this.snackbarService.openErrorSnack('Not implemented');\n }\n\n viewCampaignHistory(campaignId: string): void {\n this.router.navigate(['marketing', 'campaign', campaignId, 'history']);\n }\n\n manageTags(campaignId: string): void {\n const diaglogRef = ManageTagsComponent.openAndAddToURL(this.matDialog, this.router, this.route, campaignId);\n diaglogRef\n .afterClosed()\n .pipe(take(1))\n .subscribe({\n next: () => {\n this.eventEmitter.emit(true);\n },\n error: (err) => {\n console.error(err);\n },\n });\n }\n}\n","
\n \n \n \n \n \n \n {{ 'CAMPAIGN_DETAILS.PAUSE.ACTION' | translate }}\n \n\n \n {{ 'CAMPAIGN_DETAILS.RESUME.ACTION' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n
\n","import { Component, OnInit } from '@angular/core';\nimport { MyCampaignsColumnDirective } from './my-campaigns-column.directive';\n\n@Component({\n template: `\n
\n {{ activeRecipients }}\n
\n `,\n styleUrls: ['../campaign-list-page.component.scss'],\n standalone: false,\n})\nexport class MyCampaignsActiveRecipientsColumnComponent extends MyCampaignsColumnDirective implements OnInit {\n activeRecipients = 0;\n\n ngOnInit(): void {\n const recipients = this.element[this.columnDefinition.field];\n this.activeRecipients = recipients ? recipients : 0;\n }\n}\n","import { Component, OnInit } from '@angular/core';\nimport { MyCampaignsColumnDirective } from './my-campaigns-column.directive';\n\n@Component({\n template: `\n
\n {{ emailsDelivered }}\n
\n `,\n styleUrls: ['../campaign-list-page.component.scss'],\n standalone: false,\n})\nexport class MyCampaignsEmailsDeliveredColumnComponent extends MyCampaignsColumnDirective implements OnInit {\n emailsDelivered: number;\n\n constructor() {\n super();\n }\n\n ngOnInit(): void {\n const emails = this.element[this.columnDefinition.field];\n this.emailsDelivered = emails ? emails : 0;\n }\n}\n","import { CommonModule } from '@angular/common';\nimport { Component, Inject, OnInit } from '@angular/core';\nimport { RouterModule } from '@angular/router';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { PAGE_ROUTES } from '../../routing-constants';\nimport { MyCampaignsColumnDirective } from './my-campaigns-column.directive';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../../shared/src/tokens';\n\n@Component({\n template: `\n \n `,\n styleUrls: ['../campaign-list-page.component.scss'],\n imports: [CommonModule, RouterModule],\n})\nexport class MyCampaignsNameColumnComponent extends MyCampaignsColumnDirective implements OnInit {\n detailsUrl$: Observable | undefined;\n\n constructor(@Inject(CONFIG_TOKEN) private readonly config: CONFIG_TYPE) {\n super();\n }\n\n ngOnInit(): void {\n this.detailsUrl$ = this.config.basePath$.pipe(\n map((v) => `/${v}/${PAGE_ROUTES.ROOT.CAMPAIGN_SUBROUTES.DETAILS(this.element.campaignId!)}`),\n );\n }\n}\n","import { Component, OnInit } from '@angular/core';\nimport { MyCampaignsColumnDirective } from './my-campaigns-column.directive';\n@Component({\n template: `\n
\n {{ rateDisplay }}\n
\n `,\n styleUrls: ['../campaign-list-page.component.scss'],\n standalone: false,\n})\nexport class MyCampaignsOpenRateColumnComponent extends MyCampaignsColumnDirective implements OnInit {\n rateDisplay: string;\n\n constructor() {\n super();\n }\n\n ngOnInit(): void {\n const rate = this.element[this.columnDefinition.field] as number;\n if (rate) {\n this.rateDisplay = rate > 0 ? formatPercentage(rate) : this.translateService.instant('COMMON.ACTION_LABELS.NA');\n } else {\n this.rateDisplay = this.translateService.instant('COMMON.ACTION_LABELS.NA');\n }\n }\n}\n\nfunction formatPercentage(percent: number): string {\n return percent % 1 === 0 ? percent.toFixed(0) + '%' : percent.toFixed(1) + '%';\n}\n","import { Component, OnInit } from '@angular/core';\nimport { MyCampaignsColumnDirective } from './my-campaigns-column.directive';\nimport { CampaignState, Statuses } from '@vendasta/campaigns';\nimport { VaBadgeModule } from '@vendasta/uikit';\nimport { CommonModule } from '@angular/common';\n\n@Component({\n template: `\n
\n \n {{ status }}\n \n \n {{ status }}\n \n \n {{ status }}\n \n \n {{ status }}\n \n
\n `,\n styleUrls: ['../campaign-list-page.component.scss'],\n imports: [CommonModule, VaBadgeModule],\n})\nexport class MyCampaignsStatusColumnComponent extends MyCampaignsColumnDirective implements OnInit {\n status: string;\n\n constructor() {\n super();\n }\n\n ngOnInit(): void {\n this.status = this.convertStatus(this.element.status, this.element.state);\n }\n\n convertStatus(status: Statuses, state: CampaignState): string {\n switch (status) {\n case Statuses.STATUSES_ACTIVE:\n case Statuses.STATUSES_PUBLISHED:\n if (state === CampaignState.CAMPAIGN_STATE_ACTIVE) {\n return 'Ongoing';\n }\n return 'Published';\n case Statuses.STATUSES_ARCHIVED:\n return 'Archived';\n case Statuses.STATUSES_DRAFT:\n return 'Draft';\n default:\n return '';\n }\n }\n}\n","import { Component, OnInit } from '@angular/core';\nimport { MyCampaignsColumnDirective } from './my-campaigns-column.directive';\nimport { CommonModule } from '@angular/common';\n\n@Component({\n template: `\n
\n {{ totalRecipients }}\n
\n `,\n styleUrls: ['../campaign-list-page.component.scss'],\n imports: [CommonModule],\n})\nexport class MyCampaignsTotalRecipientsColumnComponent extends MyCampaignsColumnDirective implements OnInit {\n totalRecipients: number | undefined;\n\n ngOnInit(): void {\n const total = this.element[this.columnDefinition.field];\n this.totalRecipients = total ? total : 0;\n }\n}\n","import { ColumnType, ColumnWidth, Pinned, TableDefinition } from '@vendasta/va-filter2-table';\nimport { MyCampaignsActionsColumnComponent } from './column-components/my-campaigns-actions-column.component';\nimport { MyCampaignsActiveRecipientsColumnComponent } from './column-components/my-campaigns-active-recipients-column.component';\nimport { MyCampaignsEmailsDeliveredColumnComponent } from './column-components/my-campaigns-emails-delivered-column.component';\nimport { MyCampaignsNameColumnComponent } from './column-components/my-campaigns-name-column.component';\nimport { MyCampaignsOpenRateColumnComponent } from './column-components/my-campaigns-open-rate-column.component';\nimport { MyCampaignsStatusColumnComponent } from './column-components/my-campaigns-status-column.component';\nimport { MyCampaignsTotalRecipientsColumnComponent } from './column-components/my-campaigns-total-recipients-column.component';\nimport { MyCampaignsTableFilterService } from './my-campaigns-table-filter.service';\n\nconst MY_CAMPAIGNS_TABLE_DEF: TableDefinition = {\n id: 'my-campaigns-table',\n labels: {\n noResults: 'No results',\n cancel: 'Cancel',\n none: 'None',\n update: 'Update',\n sortBy: 'Sort By',\n thenSortBy: 'Then Sort By',\n addAnotherSortColumn: 'Add another column to sort on',\n chooseColumnToSortBy: 'Choose a column to sort on',\n columnSorting: {\n date: {\n ascending: 'Earliest to Latest',\n descending: 'Latest to Earliest',\n },\n number: {\n ascending: '1 to 9',\n descending: '9 to 1',\n },\n string: {\n ascending: 'A to Z',\n descending: 'Z to A',\n },\n default: {\n ascending: 'ascending',\n descending: 'descending',\n },\n },\n export: {\n buttonText: 'Export',\n buttonTooltipAfterCount: '',\n buttonTooltipBeforeCount: '',\n },\n },\n options: {\n selectableRows: false,\n },\n columns: [\n {\n cellComponent: MyCampaignsNameColumnComponent,\n id: 'campaignName',\n displayName: 'Name',\n field: 'name',\n translateCell: false,\n type: ColumnType.COLUMN_TYPE_STRING,\n pinned: Pinned.PINNED_LEFT,\n },\n // TODO: MAEL-861 - Add the tag column back in once tags are supported for BCC users\n // {\n // cellComponent: TagsColumnComponent,\n // id: 'tags',\n // displayName: 'Tags',\n // field: 'tags',\n // translateCell: false,\n // },\n {\n cellComponent: MyCampaignsTotalRecipientsColumnComponent,\n id: 'totalRecipients',\n displayName: 'Total Recipients',\n field: 'totalAccounts',\n translateCell: false,\n type: ColumnType.COLUMN_TYPE_NUMBER,\n width: ColumnWidth.WIDTH_NARROW,\n },\n {\n cellComponent: MyCampaignsActiveRecipientsColumnComponent,\n id: 'activeRecipients',\n displayName: 'Active Recipients',\n field: 'activeAccounts',\n translateCell: false,\n type: ColumnType.COLUMN_TYPE_NUMBER,\n width: ColumnWidth.WIDTH_NARROW,\n },\n {\n cellComponent: MyCampaignsEmailsDeliveredColumnComponent,\n id: 'emailsDelivered',\n displayName: 'Emails Delivered',\n field: 'emailsDelivered',\n translateCell: false,\n type: ColumnType.COLUMN_TYPE_NUMBER,\n width: ColumnWidth.WIDTH_NARROW,\n },\n {\n cellComponent: MyCampaignsOpenRateColumnComponent,\n id: 'openRate',\n displayName: 'Open Rate',\n field: 'openRate',\n translateCell: false,\n type: ColumnType.COLUMN_TYPE_NUMBER,\n width: ColumnWidth.WIDTH_NARROW,\n },\n {\n cellComponent: MyCampaignsOpenRateColumnComponent,\n id: 'clickThroughRate',\n displayName: 'CTOR',\n field: 'clickThroughRate',\n translateCell: false,\n type: ColumnType.COLUMN_TYPE_NUMBER,\n width: ColumnWidth.WIDTH_NARROW,\n },\n {\n cellComponent: MyCampaignsStatusColumnComponent,\n id: 'status',\n displayName: 'Status',\n field: 'status',\n translateCell: false,\n type: ColumnType.COLUMN_TYPE_STRING,\n width: ColumnWidth.WIDTH_NARROW,\n },\n {\n cellComponent: MyCampaignsActionsColumnComponent,\n id: 'actions',\n displayName: '',\n translateCell: false,\n width: ColumnWidth.WIDTH_ACTION,\n },\n ],\n};\n\nexport const tableDefinitionFactory = (filterService: MyCampaignsTableFilterService) => {\n const tableDefinition = MY_CAMPAIGNS_TABLE_DEF;\n tableDefinition.filters = filterService.filters;\n return tableDefinition;\n};\n","import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { AddNewTagDialogComponent } from './add-new-tag-dialog.component';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatDialogModule } from '@angular/material/dialog';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { GalaxyInputModule } from '@vendasta/galaxy/input';\n\n@NgModule({\n declarations: [AddNewTagDialogComponent],\n imports: [\n CommonModule,\n TranslateModule,\n MatButtonModule,\n MatDialogModule,\n FormsModule,\n ReactiveFormsModule,\n GalaxyInputModule,\n ],\n})\nexport class TagsModule {}\n","import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { TagsColumnComponent } from './tags-column.component';\nimport { GalaxyBadgeModule } from '@vendasta/galaxy/badge';\nimport { GalaxyTooltipModule } from '@vendasta/galaxy/tooltip';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { MatIconModule } from '@angular/material/icon';\n\n@NgModule({\n declarations: [\n TagsColumnComponent, //\n ],\n imports: [\n CommonModule, //\n GalaxyBadgeModule,\n GalaxyTooltipModule,\n TranslateModule,\n MatIconModule,\n ],\n})\nexport class TagsColumnModule {}\n","import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatCheckboxModule } from '@angular/material/checkbox';\nimport { MatDialogModule } from '@angular/material/dialog';\nimport { MatIconModule } from '@angular/material/icon';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { GalaxyCheckboxModule } from '@vendasta/galaxy/checkbox';\nimport { GalaxyWrapModule } from '@vendasta/galaxy/galaxy-wrap';\nimport { GalaxyInputModule } from '@vendasta/galaxy/input';\nimport { GalaxyLoadingSpinnerModule } from '@vendasta/galaxy/loading-spinner';\nimport { GalaxyPipesModule } from '@vendasta/galaxy/pipes';\nimport { GalaxyPopoverModule } from '@vendasta/galaxy/popover';\nimport { MyCampaignsTableService } from '../campaign-list-page/my-campaigns-table.service';\nimport { SenderTagsService } from '../tags/sender-tags.service';\nimport { TagsModule } from '../tags/tags.module';\nimport { ManageTagsComponent, SENDER_TAGS_TOKEN, TAG_CHANGES_LISTENER_TOKEN } from './manage-tags.component';\nimport { TagToggleComponent } from './tag-toggle/tag-toggle.component';\n\n@NgModule({\n imports: [\n GalaxyCheckboxModule,\n GalaxyWrapModule,\n GalaxyPipesModule,\n GalaxyLoadingSpinnerModule,\n MatDialogModule,\n GalaxyInputModule,\n ReactiveFormsModule,\n FormsModule,\n CommonModule,\n MatButtonModule,\n MatCheckboxModule,\n // TODO: Hook up lexicon\n // LexiconModule.forChild({\n // componentName: WEBLATE_COMPONENT_NAME,\n // baseTranslation: baseTranslation,\n // }),\n TagsModule,\n GalaxyPopoverModule,\n MatIconModule,\n TranslateModule,\n ],\n declarations: [\n ManageTagsComponent, //\n TagToggleComponent,\n ],\n providers: [\n {\n provide: TAG_CHANGES_LISTENER_TOKEN,\n useExisting: MyCampaignsTableService,\n },\n {\n provide: SENDER_TAGS_TOKEN,\n useFactory: SenderTagsService.getObservable$,\n deps: [SenderTagsService],\n },\n ],\n})\nexport class ManageTagsModule {}\n","import { Component, Inject, OnInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { combineLatest, Observable } from 'rxjs';\nimport { map, switchMap, take } from 'rxjs/operators';\nimport { SmsModule } from '@galaxy/sms';\nimport { Router } from '@angular/router';\nimport { CampaignConfig } from '@galaxy/campaign/dependencies';\nimport { FeatureFlagService } from '@galaxy/partner';\nimport { CONFIG_TOKEN } from '../../../shared/src/tokens';\n\n@Component({\n selector: 'campaign-sms-configuration-alert',\n imports: [CommonModule, SmsModule],\n templateUrl: './sms-configuration-alert.component.html',\n styleUrls: [],\n})\nexport class SmsConfigurationAlertComponent implements OnInit {\n showAlert$: Observable = new Observable();\n\n constructor(\n private router: Router,\n @Inject(CONFIG_TOKEN) private readonly campaignConfig: CampaignConfig,\n private readonly featureFlagService: FeatureFlagService,\n @Inject('PARTNER_ID') readonly partnerId$: Observable,\n ) {}\n\n ngOnInit(): void {\n const featureFlagEnabled$ = this.partnerId$.pipe(\n switchMap((pid) => this.featureFlagService.batchGetStatus(pid, '', ['campaigns_sms'])),\n map((resp) => resp['campaigns_sms']),\n );\n\n this.showAlert$ = combineLatest([\n this.campaignConfig.location$,\n this.campaignConfig.smsSettingsPath$,\n featureFlagEnabled$,\n ]).pipe(\n map(([location, smsSettingsPath, featureFlagEnabled]) => {\n if (!featureFlagEnabled) {\n return false;\n }\n if (!smsSettingsPath) {\n return false;\n }\n return location === 'US';\n }),\n );\n }\n\n navigateToSmsConfiguration() {\n this.campaignConfig.smsSettingsPath$.pipe(take(1)).subscribe((smsSettingsPath) => {\n this.router.navigateByUrl(smsSettingsPath);\n });\n }\n}\n","\n \n\n","import { Component, Input, OnInit } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport {\n Filter2TableModule,\n TABLE_DEFINITION,\n VaFilteredMatTableService,\n VaTableSortService,\n} from '@vendasta/va-filter2-table';\nimport { ManageTagsComponent } from '../manage-tags/manage-tags.component';\nimport { MyCampaignsTableFilterService, SENDER_TAGS_TOKEN } from './my-campaigns-table-filter.service';\nimport { tableDefinitionFactory } from './my-campaigns-table-model';\nimport { MyCampaignsTableService } from './my-campaigns-table.service';\nimport { GalaxyPageModule } from '@vendasta/galaxy/page';\nimport { TagsModule } from '../tags/tags.module';\nimport { SenderTagsService } from '../tags/sender-tags.service';\nimport { TagsColumnModule } from './column-components/tags-column/tags-column.module';\nimport { ManageTagsModule } from '../manage-tags/manage-tags.module';\nimport { CommonModule } from '@angular/common';\nimport { CampaignPreviewDialogComponent } from '../campaign-preview-dialog/campaign-preview-dialog.component';\nimport { SmsConfigurationAlertComponent } from '../sms-configuration-alert/sms-configuration-alert.component';\n\n@Component({\n selector: 'campaign-list-page',\n templateUrl: './campaign-list-page.component.html',\n styleUrls: ['./campaign-list-page.component.scss'],\n imports: [\n CommonModule,\n GalaxyPageModule,\n Filter2TableModule,\n TagsModule,\n TagsColumnModule,\n ManageTagsModule,\n SmsConfigurationAlertComponent,\n ],\n providers: [\n VaFilteredMatTableService,\n VaTableSortService,\n {\n provide: SENDER_TAGS_TOKEN,\n useFactory: SenderTagsService.getObservable$,\n deps: [SenderTagsService],\n },\n MyCampaignsTableFilterService,\n {\n provide: TABLE_DEFINITION,\n useFactory: tableDefinitionFactory,\n deps: [MyCampaignsTableFilterService],\n },\n ],\n})\nexport class CampaignListComponent implements OnInit {\n @Input() key: string | undefined;\n private manageTagsForCampaignId: string | undefined;\n private previewForCampaignId: string | undefined;\n\n constructor(\n readonly myCampaignsTableService: MyCampaignsTableService,\n private readonly dialog: MatDialog,\n private readonly router: Router,\n private readonly activatedRoute: ActivatedRoute,\n ) {}\n\n async ngOnInit(): Promise {\n this.myCampaignsTableService.setFocus(this.key || '');\n\n return this.restoreLastActivityViaURLQuery();\n }\n\n private async restoreLastActivityViaURLQuery(): Promise {\n if (await this.openManageTagsIfPresentInURL()) {\n return Promise.resolve();\n }\n if (await this.openPreviewIfPresentInURL()) {\n return Promise.resolve();\n }\n }\n async openManageTagsIfPresentInURL(): Promise {\n this.manageTagsForCampaignId = this.activatedRoute.snapshot.queryParams['manageTagsForCampaignId'];\n if (!this.manageTagsForCampaignId) {\n return Promise.resolve(false);\n }\n ManageTagsComponent.openAndAddToURL(this.dialog, this.router, this.activatedRoute, this.manageTagsForCampaignId);\n return Promise.resolve(true);\n }\n\n async openPreviewIfPresentInURL(): Promise {\n this.previewForCampaignId = this.activatedRoute.snapshot.queryParams['previewForCampaignId'];\n if (!this.previewForCampaignId) {\n return Promise.resolve(false);\n }\n\n CampaignPreviewDialogComponent.openAndAddToURL(\n this.dialog,\n this.router,\n this.activatedRoute,\n this.previewForCampaignId,\n );\n return Promise.resolve(true);\n }\n\n reloadTable(): void {\n this.myCampaignsTableService.reloadData();\n }\n}\n","\n\n","import { Component, Inject } from '@angular/core';\nimport { QUOTA_BANNER_CONFIG, QUOTA_CONFIG } from '../dependencies';\nimport { switchMap, map } from 'rxjs/operators';\nimport { BehaviorSubject, Observable } from 'rxjs';\nimport { CampaignQuotaCategory, Period, QuotaService, SenderType } from '@vendasta/campaigns';\n\ninterface CampaignQuota {\n count: number;\n limit: number;\n}\n\n@Component({\n selector: 'campaign-quota-banner',\n templateUrl: './quota-banner.component.html',\n styleUrls: ['./quota-banner.component.scss'],\n standalone: false,\n})\nexport class QuotaBannerComponent {\n emailQuota$: Observable;\n smsQuota$: Observable;\n isBusinessSender$: BehaviorSubject = new BehaviorSubject(false);\n constructor(\n private quotaService: QuotaService,\n @Inject(QUOTA_BANNER_CONFIG) public readonly config: QUOTA_CONFIG,\n ) {\n this.config.sender$.subscribe((sender) => {\n if (sender.type === SenderType.SENDER_TYPE_BUSINESS) {\n this.isBusinessSender$.next(true);\n }\n });\n\n this.emailQuota$ = config.sender$.pipe(\n switchMap((sndr) => {\n return this.quotaService.getCampaignQuota(\n sndr,\n CampaignQuotaCategory.CAMPAIGN_QUOTA_TYPE_EMAIL,\n Period.PERIOD_MONTH,\n );\n }),\n map((resp) => {\n return { count: resp.count, limit: resp.limit };\n }),\n );\n this.smsQuota$ = config.sender$.pipe(\n switchMap((sndr) => {\n return this.quotaService.getCampaignQuota(\n sndr,\n CampaignQuotaCategory.CAMPAIGN_QUOTA_TYPE_SMS,\n Period.PERIOD_MONTH,\n );\n }),\n map((resp) => {\n return { count: resp.count, limit: resp.limit };\n }),\n );\n }\n}\n","\n = 0\">\n \n
\n
\n {{ 'QUOTA_BANNER.USAGE.BANNER_TITLE' | translate }}\n = usage.limit }\"\n >\n \n {{\n 'QUOTA_BANNER.USAGE.BANNER_OF'\n | translate: { usage: (usage.count ? usage.count : 0) | number, quota: usage.limit | number }\n }}\n \n
\n
\n
\n \n
\n
\n
\n
\n\n\n = 0\">\n \n
\n
\n {{ 'QUOTA_BANNER.USAGE.SMS_BANNER_TITLE' | translate }}\n = smsUsage.limit }\"\n >\n \n {{\n 'QUOTA_BANNER.USAGE.BANNER_OF'\n | translate: { usage: (smsUsage.count ? smsUsage.count : 0) | number, quota: smsUsage.limit | number }\n }}\n \n
\n
\n
\n
\n
\n","import { Component, inject, Inject, OnDestroy, OnInit } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { CampaignQuotaCategory, Period, QuotaService, SenderInterface } from '@vendasta/campaigns';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { ProductAnalyticsService } from '@vendasta/product-analytics';\nimport { EMPTY, Observable, of, Subscription } from 'rxjs';\nimport { map, switchMap, take } from 'rxjs/operators';\nimport { CampaignGenerateDialogComponent } from '../campaign-generate-dialog/campaign-generate-dialog.component';\nimport { MyCampaignsTableService } from '../campaign-list-page/my-campaigns-table.service';\nimport { CampaignListPageData } from '../core/interfaces';\nimport { QUOTA_BANNER_CONFIG, QUOTA_CONFIG } from '../dependencies';\nimport { PAGE_ROUTES } from '../routing-constants';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../shared/src/tokens';\n\ninterface EmailQuota {\n count: number;\n limit: number;\n}\n@Component({\n selector: 'campaign-dashboard',\n templateUrl: './campaign-dashboard.component.html',\n styleUrls: ['./campaign-dashboard.component.scss'],\n standalone: false,\n})\nexport class CampaignDashboardComponent implements OnInit, OnDestroy {\n public pageData$: Observable = of({\n key: 'none',\n title: 'Campaigns',\n } as CampaignListPageData);\n\n private readonly activatedRoute = inject(ActivatedRoute);\n protected readonly templatesUrl = this.router.createUrlTree(['../../emails'], {\n relativeTo: this.activatedRoute,\n });\n\n private senderId = '';\n private subscriptions: Subscription[] = [];\n quota$: Observable;\n\n constructor(\n private readonly dialog: MatDialog,\n private readonly myCampaignsTableService: MyCampaignsTableService,\n private readonly router: Router,\n private readonly snackbarService: SnackbarService,\n @Inject(CONFIG_TOKEN) private readonly config: CONFIG_TYPE,\n @Inject(QUOTA_BANNER_CONFIG) public readonly quotaConfig: QUOTA_CONFIG,\n private posthogService: ProductAnalyticsService,\n private quotaService: QuotaService,\n ) {\n this.quota$ = quotaConfig.sender$.pipe(\n switchMap((sndr) => {\n return this.quotaService.getCampaignQuota(\n sndr,\n CampaignQuotaCategory.CAMPAIGN_QUOTA_TYPE_EMAIL,\n Period.PERIOD_MONTH,\n );\n }),\n map((resp) => {\n return { count: resp.count, limit: resp.limit };\n }),\n );\n }\n\n ngOnInit(): void {\n this.subscriptions.push(\n this.myCampaignsTableService\n .getSender()\n .pipe(\n take(1),\n map((sender: SenderInterface) => {\n this.senderId = sender.id || '';\n }),\n )\n .subscribe(),\n );\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((subscription) => subscription.unsubscribe());\n }\n\n navigateToCampaignDetails(campaignId: string): void {\n this.subscriptions.push(\n this.config.basePath$\n .pipe(map((v) => this.router.navigate([`/${v}/${PAGE_ROUTES.ROOT.CAMPAIGN_SUBROUTES.DETAILS(campaignId)}`])))\n .subscribe(),\n );\n }\n\n openCreatecampaignDialog(): void {\n this.posthogService.trackEvent('user-clicked-create-campaign', 'create-campaign-workflow', 'click');\n const dialogRef = this.dialog.open(CampaignGenerateDialogComponent);\n dialogRef\n .afterClosed()\n .pipe(\n switchMap((name) => {\n if (!name || name.trim() === '') {\n return EMPTY;\n }\n return this.myCampaignsTableService.createNewCampaign(name.trim());\n }),\n )\n .subscribe({\n next: (campaignId) => {\n if (campaignId) {\n this.navigateToCampaignDetails(campaignId);\n }\n },\n error: (err) => {\n this.snackbarService.openErrorSnack(err.error.message);\n },\n });\n }\n}\n","\n \n \n {{ 'CAMPAIGNS.TITLE' | translate }}\n \n \n \n \n \n \n\n \n \n \n \n \n \n\n","import { SenderInterface } from '@vendasta/campaigns';\nimport { Context } from '../../campaign-email-builder/page/shared/context';\n\nexport interface CampaignData {\n is_enabled: boolean;\n status: string;\n updated: string;\n open_rate: number;\n hidden_for_partners: any[];\n events_count: number;\n emails_delivered: number;\n created: string;\n campaign_id: string;\n campaign_focus: string;\n is_editable: boolean;\n created_user: string;\n accounts_count: number;\n click_through_rate: number;\n has_automations: boolean;\n parent_campaign_id: string;\n active_accounts_count: number;\n is_hidden: boolean;\n is_draft: boolean;\n updated_user: string;\n name: string;\n campaign_schedule: CampaignStepInterface[];\n partner_id: string;\n market_id: string;\n campaign_schedule_version: number;\n locale: string;\n email_category_id: string;\n is_premade: boolean;\n}\n\nexport interface CampaignPreviewInterface {\n html: string;\n name: string;\n subject: string;\n content?: string;\n}\n\nexport interface CampaignStepInterface {\n campaign_step_id: string;\n step_type: string;\n template_id: string;\n seconds_after_last_email: number;\n name: string;\n}\n\nexport interface TemplateData {\n fullHtml: string;\n htmlBody: string;\n name: string;\n subject: string;\n templateId: string;\n useFullHtml: boolean;\n}\n\nexport interface SMSContentData {\n campaignStepId?: string;\n templateId?: string;\n name?: string;\n message?: string;\n ownerId?: string;\n appNamespace?: string;\n context?: Context;\n stepType?: string;\n}\n\nexport interface CampaignDelay {\n delay: number;\n validDays: string[];\n timezone: string;\n rateLimitEnabled: boolean;\n rateLimit: number;\n}\n\nexport interface CampaignConfig {\n validDays: string[];\n timezone: string;\n rateLimitEnabled?: boolean;\n rateLimit?: number;\n}\n\nexport interface CampaignStepPreviewData {\n html: string;\n name: string;\n subject: string;\n}\n\nexport type CampaignStepStats = EmailStepStats | SmsStepStats;\n\nexport class Ratio {\n numerator: number;\n denominator: number;\n\n private formatter: (n: number, d: number) => string = (n, d) => {\n return (n / d).toString();\n };\n\n constructor(numerator: number, denominator: number) {\n this.numerator = numerator;\n this.denominator = denominator;\n }\n\n withFormatter(f: (n: number, d: number) => string): Ratio {\n this.formatter = f;\n return this;\n }\n\n toString(): string {\n return this.formatter(this.numerator, this.denominator);\n }\n\n toNumber(): number {\n return this.numerator / this.denominator;\n }\n}\n\nexport interface EmailStepStats {\n bounced: number;\n campaign_step_id: string;\n clickedThrough: number;\n created: number;\n delivered: number;\n dropped: number;\n notRequired: number;\n onDeck: number;\n opened: number;\n paused: number;\n pending: number;\n refreshed: number;\n sent: number;\n spamReport: number;\n unsubscribed: number;\n clickToOpenRate: number;\n clickToOpenRatio?: Ratio;\n openRate: number;\n openRatio?: Ratio;\n}\n\nexport function isSmsStepStats(s: SmsStepStats | EmailStepStats): s is SmsStepStats {\n return (s as SmsStepStats).queued !== undefined;\n}\n\nexport interface SmsStepStats {\n queued: number;\n sent: number;\n delivered: number;\n undelivered: number;\n failed: number;\n}\n\nexport interface DuplicateCampaignData {\n campaignId: string;\n sender: SenderInterface;\n}\n\nexport interface DuplicateCampaignSuccessData {\n newCampaignId: string;\n}\n\nexport interface EmailStats {\n active: number;\n clicked_through: number;\n completed: number;\n delivered: number;\n opened: number;\n sent: number;\n stopped: number;\n total_accounts: number;\n total_leads: number;\n total_recipients: number;\n undeliverable: number;\n waiting_on_rate_limit: number;\n}\n\nexport interface RecipientCampaignStats {\n active: number;\n completed: number;\n stopped: number;\n total_accounts: number;\n total_recipients: number;\n waiting_on_rate_limit: number;\n total_leads: number;\n}\n\nexport interface CampaignStats extends EmailStats, RecipientCampaignStats {}\n\nexport interface EmailStat {\n count: number;\n unique: number;\n unique_by_email: number;\n}\n\nexport interface EmailStatsData {\n processed: EmailStat;\n // processed: number;\n // unique_processed: number;\n // unique_processed_by_email: number;\n\n delivered: EmailStat;\n\n // delivered: number;\n // unique_delivered: number;\n // unique_delivered_by_email: number;\n\n opened: EmailStat;\n // opened: number;\n // unique_opened: number;\n // unique_opened_by_email: number;\n\n clicked: EmailStat;\n // clicked: number;\n // unique_clicked: number;\n // unique_clicked_by_email: number;\n\n bounced: EmailStat;\n // bounced: number;\n // unique_bounced: number;\n // unique_bounced_by_email: number;\n\n deferred: EmailStat;\n // deferred: number;\n // unique_deferred: number;\n // unique_deferred_by_email: number;\n\n dropped: EmailStat;\n // dropped: number;\n // unique_dropped: number;\n // unique_dropped_by_email: number;\n\n spamreport: EmailStat;\n // spamreport: number;\n // unique_spamreport: number;\n // unique_spamreport_by_email: number;\n\n unsubscribed: EmailStat;\n // unsubscribed: number;\n // unique_unsubscribed:\n resubscribed: EmailStat;\n // resubscribed: number;\n}\n\nexport enum CampaignStatus {\n ACTIVE = 'active',\n DRAFT = 'draft',\n PUBLISHED = 'published',\n ARCHIVED = 'archived',\n UNSPECIFIED = '',\n}\n\nexport interface StepNumberInterface {\n currentStep: number;\n totalSteps: number;\n}\n\nexport interface MarketingAutomationPageData {\n key: string;\n title: string;\n imageHeader: {\n title: string;\n subtitle: string;\n };\n showNewPage?: boolean;\n manageTagsCampaignId?: string;\n}\n\nexport interface Contact {\n id: string;\n name: string;\n email: string;\n disabled?: boolean;\n phoneNumber?: string;\n}\n\nexport interface ContactsState {\n contacts: Contact[];\n}\n\nexport interface PagedResponseMetadataInterface {\n nextCursor: string;\n hasMore: boolean;\n totalResults: number;\n}\n\nexport enum ModalState {\n CONTACTS = 'contacts',\n SCHEDULE = 'schedule',\n}\n\nexport enum CampaignFocus {\n ACQUIRE = 'acquire',\n ADOPT = 'adopt',\n UPSELL = 'upsell',\n NONE = '',\n UNSPECIFIED = '',\n}\n\nexport enum ScheduleType {\n NOW = 'now',\n LATER = 'later',\n}\n\nexport class Market {\n name = '';\n partner_id = '';\n market_id = '';\n\n constructor(data: any) {\n Object.assign(this, data);\n if (data.pid) {\n this.partner_id = data.pid;\n }\n }\n}\n","import { HttpClient } from '@angular/common/http';\nimport { Injectable } from '@angular/core';\nimport { EmailTemplateService, SenderInterface } from '@vendasta/campaigns';\nimport { BehaviorSubject, Observable, of } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { CampaignPreviewInterface } from './interface';\nimport { TemplatesService } from '@vendasta/templates';\nimport { AppNamespace } from '@vendasta/email-builder';\n\ninterface LoadedTemplates {\n [templateId: string]: PreviewEmailType;\n}\n\nconst seenTemplates: LoadedTemplates = {};\n\ninterface PreviewEmailType {\n html?: string;\n name?: string;\n subject?: string;\n}\n\n@Injectable({\n providedIn: 'root',\n})\nexport class MarketingAutomationService {\n private templateId$$: BehaviorSubject = new BehaviorSubject('');\n templateId$: Observable = this.templateId$$.asObservable();\n\n constructor(\n private readonly http: HttpClient,\n private readonly campaignsService: EmailTemplateService,\n private templatesService: TemplatesService,\n ) {}\n\n getHTMLContentOfEmail(\n sender: SenderInterface,\n templateId: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n currentStep: any = null,\n accountGroupId = '',\n locale?: string,\n useCache = true,\n ): Observable {\n this.templateId$$.next(templateId);\n\n if (templateId === 'snapshot-creation') {\n return of({\n html: '',\n name: '',\n subject: '',\n });\n }\n if (useCache && templateId in seenTemplates && accountGroupId === '') {\n const html = seenTemplates[templateId].html || '';\n const stepSubject = seenTemplates[templateId].subject || '';\n const stepName = seenTemplates[templateId].name || '';\n\n return of({\n html: html,\n name: stepName,\n subject: stepSubject,\n });\n }\n if (templateId.startsWith('SMS-BUILDER')) {\n return this.templatesService.get(AppNamespace.CAMPAIGNS, templateId).pipe(\n map((resp) => {\n return {\n content: resp.content,\n name: resp.name,\n subject: '',\n html: '',\n };\n }),\n );\n }\n return this.campaignsService\n .previewEmailTemplate(templateId, accountGroupId, '', '', locale || 'en', '', sender)\n .pipe(\n map((resp) => {\n return {\n html: resp.htmlBody,\n name: resp.name,\n subject: resp.subject,\n };\n }),\n );\n }\n}\n","import { Injectable, InjectionToken, TemplateRef } from '@angular/core';\nimport { BehaviorSubject, Observable, ReplaySubject } from 'rxjs';\nimport { map, shareReplay } from 'rxjs/operators';\n\nexport interface DynamicOpenCloseTemplateRef {\n id: string;\n templateRef: TemplateRef | undefined;\n}\n\nexport const DYNAMIC_OPEN_CLOSE_TEMPLATE_DATA: InjectionToken> = new InjectionToken>(\n 'com.vendasta.galaxy.side-drawer.dynamic-open-close-template-ref.data',\n);\n\n@Injectable({ providedIn: 'root' })\nexport class DynamicOpenCloseTemplateRefService {\n private templateMap = new Map>();\n private openTemplateId$$ = new BehaviorSubject('');\n openTemplate$: Observable;\n\n private _open$$ = new BehaviorSubject(false);\n private _open$ = this._open$$.asObservable();\n\n private _dynamicOpenCloseTemplateData$$: ReplaySubject = new ReplaySubject(1);\n private _dynamicOpenCloseTemplateData$ = this._dynamicOpenCloseTemplateData$$.asObservable();\n\n public static dynamicOpenCloseTemplateData$(svc: DynamicOpenCloseTemplateRefService): Observable {\n return svc._dynamicOpenCloseTemplateData$;\n }\n\n get open$(): Observable {\n return this._open$;\n }\n\n constructor() {\n this.openTemplate$ = this.openTemplateId$$.pipe(\n map((id) => {\n const templateRef = this.templateMap.get(id);\n return {\n id: templateRef ? id : '',\n templateRef: templateRef,\n };\n }),\n shareReplay({ refCount: true, bufferSize: 1 }),\n );\n }\n\n close(): void {\n this._open$$.next(false);\n }\n\n registerTemplate(id: string, template: TemplateRef): void {\n this.templateMap.set(id, template);\n }\n\n open(id: string, data?: any): void {\n if (data) {\n this._dynamicOpenCloseTemplateData$$.next(data);\n }\n window.setTimeout(() => {\n this.openTemplateId$$.next(id);\n this._open$$.next(true);\n });\n }\n\n toggle(id: string, data?: any): void {\n if (data) {\n this._dynamicOpenCloseTemplateData$$.next(data);\n }\n if (this._open$$.getValue()) {\n this.close();\n } else {\n this.open(id);\n }\n }\n}\n","import { booleanAttribute, Component, Directive, EventEmitter, HostBinding, Input, Output } from '@angular/core';\nimport { DynamicOpenCloseTemplateRefService } from '../dynamic-open-close-template-ref.service';\n\n@Directive({\n selector: 'glxy-side-drawer-content, [glxy-side-drawer-content], [glxySideDrawerContent]',\n standalone: false,\n})\nexport class SideDrawerContentDirective {\n @HostBinding('class') className = 'side-drawer-content';\n}\n\n@Directive({\n selector: 'glxy-side-drawer-footer, [glxy-side-drawer-footer], [glxySideDrawerFooter]',\n standalone: false,\n})\nexport class SideDrawerFooterDirective {\n @HostBinding('class') className = 'side-drawer-footer';\n}\n\n@Directive({\n selector: 'glxy-side-drawer-title-actions, [glxy-side-drawer-title-actions], [glxySideDrawerTitleActions]',\n standalone: false,\n})\nexport class SideDrawerTitleActionsDirective {\n @HostBinding('class') className = 'side-drawer-title-actions';\n}\n\n@Component({\n selector: 'glxy-closing-side-drawer',\n templateUrl: './closing-side-drawer.component.html',\n styleUrls: ['./closing-side-drawer.component.scss'],\n standalone: false,\n})\nexport class ClosingSideDrawerComponent {\n @Input() title?: string;\n\n @Input({ transform: booleanAttribute }) preventClose = false;\n\n @Output()\n closeDrawer: EventEmitter = new EventEmitter();\n\n constructor(private readonly dynamicOpenCloseTemplateRefService: DynamicOpenCloseTemplateRefService) {}\n\n closeSideDrawer(): void {\n this.closeDrawer.emit();\n this.dynamicOpenCloseTemplateRefService.close();\n }\n}\n","
\n \n \n

{{ title }}

\n
\n \n
\n \n \n \n
\n
\n
\n \n
\n
\n \n
\n
\n","import {\n booleanAttribute,\n Component,\n Input,\n OnDestroy,\n OnInit,\n Optional,\n QueryList,\n ViewChildren,\n} from '@angular/core';\nimport { MatDrawer } from '@angular/material/sidenav';\nimport { Observable, Subscription } from 'rxjs';\nimport { tap } from 'rxjs/operators';\nimport {\n DynamicOpenCloseTemplateRef,\n DynamicOpenCloseTemplateRefService,\n} from '../dynamic-open-close-template-ref.service';\n\n@Component({\n selector: 'glxy-side-drawer-container',\n templateUrl: './side-drawer-container.component.html',\n styleUrls: ['./side-drawer-container.component.scss'],\n standalone: false,\n})\nexport class SideDrawerContainerComponent implements OnInit, OnDestroy {\n @ViewChildren(MatDrawer) drawerReference?: QueryList;\n\n @Input({ transform: booleanAttribute }) hasBackdrop = true;\n\n sideDrawerTemplate$?: Observable;\n\n private subscriptions: Subscription[] = [];\n\n constructor(\n @Optional()\n private dynamicOpenCloseTemplateRefService: DynamicOpenCloseTemplateRefService,\n ) {}\n\n ngOnInit(): void {\n if (this.dynamicOpenCloseTemplateRefService) {\n this.sideDrawerTemplate$ = this.dynamicOpenCloseTemplateRefService.openTemplate$;\n\n this.subscriptions.push(\n this.dynamicOpenCloseTemplateRefService.open$\n .pipe(\n tap((open) => {\n if (!this.drawerReference?.first) {\n return;\n }\n\n if (open) {\n this.drawerReference.first.open('program');\n } else {\n this.drawerReference.first.close();\n }\n }),\n )\n .subscribe(),\n );\n }\n }\n\n closeDrawer(): void {\n this.dynamicOpenCloseTemplateRefService.close();\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((subscription) => {\n subscription.unsubscribe();\n });\n }\n\n protected readonly close = close;\n}\n","\n \n \n \n \n \n \n\n\n\n \n\n","import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport {\n ClosingSideDrawerComponent,\n SideDrawerContentDirective,\n SideDrawerFooterDirective,\n SideDrawerTitleActionsDirective,\n} from './closing-side-drawer/closing-side-drawer.component';\nimport { MatToolbarModule } from '@angular/material/toolbar';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\n\nimport { ProductAnalyticsModule } from '@vendasta/product-analytics';\nimport {\n DYNAMIC_OPEN_CLOSE_TEMPLATE_DATA,\n DynamicOpenCloseTemplateRefService,\n} from './dynamic-open-close-template-ref.service';\nimport { MatSidenavModule } from '@angular/material/sidenav';\nimport { SideDrawerContainerComponent } from './side-drawer-container/side-drawer-container.component';\n\n@NgModule({\n declarations: [\n ClosingSideDrawerComponent,\n SideDrawerContentDirective,\n SideDrawerFooterDirective,\n SideDrawerTitleActionsDirective,\n SideDrawerContainerComponent,\n ],\n imports: [CommonModule, MatToolbarModule, MatIconModule, MatButtonModule, ProductAnalyticsModule, MatSidenavModule],\n exports: [\n ClosingSideDrawerComponent,\n SideDrawerContentDirective,\n SideDrawerFooterDirective,\n SideDrawerTitleActionsDirective,\n SideDrawerContainerComponent,\n ],\n providers: [\n {\n provide: DYNAMIC_OPEN_CLOSE_TEMPLATE_DATA,\n useFactory: DynamicOpenCloseTemplateRefService.dynamicOpenCloseTemplateData$,\n deps: [DynamicOpenCloseTemplateRefService],\n },\n ],\n})\nexport class GalaxySideDrawerModule {}\n","export { GalaxySideDrawerModule } from './src/side-drawer.module';\nexport { DynamicOpenCloseTemplateRefService } from './src/dynamic-open-close-template-ref.service';\nexport {\n ClosingSideDrawerComponent,\n SideDrawerContentDirective,\n SideDrawerFooterDirective,\n SideDrawerTitleActionsDirective,\n} from './src/closing-side-drawer/closing-side-drawer.component';\nexport { SideDrawerContainerComponent } from './src/side-drawer-container/side-drawer-container.component';\n","// Export things like public API, and interfaces from here.\nexport * from './public_api';\n","import { Component, Inject } from '@angular/core';\nimport { firstValueFrom } from 'rxjs';\nimport { CampaignConfig } from '@galaxy/campaign/dependencies';\nimport { Router } from '@angular/router';\nimport { CONFIG_TOKEN } from '../../../../../shared/src/tokens';\n\n@Component({\n selector: 'campaign-upsell-sms-dialog',\n templateUrl: './upsell-sms-dialog.component.html',\n styleUrls: ['./upsell-sms-dialog.component.scss'],\n standalone: false,\n})\nexport class UpsellSmsDialogComponent {\n constructor(\n @Inject(CONFIG_TOKEN) private readonly campaignConfig: CampaignConfig,\n private router: Router,\n ) {}\n\n protected smsAddOnPath$ = this.campaignConfig.productBasePath$;\n\n async gotoAddOns(): Promise {\n const smsAddOnPath = await firstValueFrom(this.smsAddOnPath$);\n return this.router.navigateByUrl(`${smsAddOnPath}`);\n }\n}\n","chat\n

{{ 'SMS.TITLE' | translate }}

\n\n
\n

\n {{ 'SMS.SUBTITLE' | translate }}\n

\n
\n\n

\n {{ 'SMS.DESCRIPTION' | translate }}\n

\n

{{ 'SMS.DESCRIPTION_2' | translate }}

\n
\n\n\n @if ((smsAddOnPath$ | async) !== null) {\n \n }\n\n","import { Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatMenuModule } from '@angular/material/menu';\nimport { Observable, take, switchMap, map, Subscription } from 'rxjs';\nimport { SENDER_INJECTION_TOKEN } from '@galaxy/email-ui/email-builder/src/shared';\nimport { Sender } from '@vendasta/email';\nimport { Feature, ActivationService, SenderInterface, CampaignService } from '@vendasta/campaigns';\nimport { AddActionType, CampaignConfig } from '../../../dependencies';\nimport { SnackbarService } from '@vendasta/galaxy/snackbar-service';\nimport { FeatureFlagService } from '@galaxy/partner';\nimport { CampaignStateService } from '../campaign-state.service';\nimport { MatDialog, MatDialogRef, MatDialogModule } from '@angular/material/dialog';\nimport { UpsellSmsDialogComponent } from '../upsell-sms-dialog/upsell-sms-dialog.component';\nimport { CONFIG_TOKEN } from '../../../../../shared/src/tokens';\n\n@Component({\n selector: 'campaign-details-add-actions',\n templateUrl: './add-actions.component.html',\n styleUrls: ['./add-actions.component.scss'],\n imports: [CommonModule, TranslateModule, MatIconModule, MatButtonModule, MatMenuModule, MatDialogModule],\n providers: [FeatureFlagService],\n})\nexport class AddActionsComponent implements OnInit, OnDestroy {\n @Input() showActions = false;\n @Input() loading = false;\n @Input() noSteps = false;\n @Output() actionClicked: EventEmitter = new EventEmitter();\n actionTypes = AddActionType;\n isLocationValid = false;\n isSMSAvailable$: Observable;\n availableSteps$: Observable;\n features = Feature;\n sender: SenderInterface;\n subscriptions: Subscription[] = [];\n sender$: Observable = this.campaignConfig.sender$;\n\n smsAvailable$: Observable = new Observable();\n\n constructor(\n @Inject(CONFIG_TOKEN) private readonly campaignConfig: CampaignConfig,\n @Inject(SENDER_INJECTION_TOKEN) public readonly sndr: Observable,\n private activation: ActivationService,\n private snackbarService: SnackbarService,\n private readonly campaignStateService: CampaignStateService,\n public dialogRef: MatDialogRef,\n public dialog: MatDialog,\n private campaigns: CampaignService,\n private readonly featureFlagService: FeatureFlagService,\n @Inject('PARTNER_ID') readonly partnerId$: Observable,\n ) {\n this.subscriptions.push(\n this.campaignConfig.sender$.subscribe((sender) => {\n this.campaignStateService.setSender(sender);\n }),\n );\n\n this.subscriptions.push(\n this.sender$.subscribe((sender) => {\n this.sender = sender;\n }),\n );\n\n this.availableSteps$ = sndr.pipe(\n take(1),\n switchMap((sndr) => {\n return campaigns.getAvailableStepTypesForSender(sndr);\n }),\n map((resp) => resp.availableStepTypes),\n );\n this.isSMSAvailable$ = this.availableSteps$.pipe(map((steps) => steps.includes(Feature.SEND_SMS)));\n }\n ngOnInit() {\n this.campaignConfig.location$.subscribe((location) => {\n if (location === 'US' || location === 'CA') {\n this.isLocationValid = true;\n }\n });\n\n this.smsAvailable$ = this.partnerId$.pipe(\n switchMap((pid) => this.featureFlagService.batchGetStatus(pid, '', ['campaigns_sms'])),\n map((status) => status['campaigns_sms']),\n );\n }\n\n openDialogOrEmitAction() {\n this.isSMSAvailable$.pipe(take(1)).subscribe((smsAvailable) => {\n if (smsAvailable) {\n this.actionClicked.emit(AddActionType.SMS);\n } else {\n this.openSMSUpsellDialog();\n }\n });\n }\n\n openSMSUpsellDialog() {\n this.dialog.open(UpsellSmsDialogComponent, {\n width: '450px',\n data: { sender: this.sender },\n });\n }\n\n ngOnDestroy(): void {\n this.subscriptions.forEach((subscription) => subscription.unsubscribe());\n }\n}\n","
\n \n

{{ 'CAMPAIGN_DETAILS.EMPTY_STATE.TITLE' | translate }}

\n

\n {{ 'CAMPAIGN_DETAILS.EMPTY_STATE.DESCRIPTION_1' | translate }}
\n {{ 'CAMPAIGN_DETAILS.EMPTY_STATE.DESCRIPTION_2' | translate }}\n

\n
\n
\n
\n \n \n {{ 'CAMPAIGN_DETAILS.TEMPLATE.ADD_SMS' | translate }}\n \n \n
\n \n \n \n \n {{ 'CAMPAIGN_DETAILS.TEMPLATE.ADD_EXISTING' | translate }}\n \n \n {{ 'CAMPAIGN_DETAILS.TEMPLATE.ADD_SNAPSHOT_REPORT' | translate }}\n \n \n \n
\n","import { CommonModule } from '@angular/common';\nimport { Component, EventEmitter, Inject, Input, Output } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatDialog } from '@angular/material/dialog';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatMenuModule } from '@angular/material/menu';\nimport { TranslateModule } from '@ngx-translate/core';\nimport { Statuses } from '@vendasta/campaigns';\nimport { firstValueFrom } from 'rxjs';\nimport {\n CampaignDuplicateDialogComponent,\n Data,\n DialogResult,\n} from '../campaign-duplicate-dialog/campaign-duplicate-dialog.component';\nimport { CampaignStateService } from '../campaign-state.service';\nimport { CampaignStatus } from '../interface';\nimport { CONFIG_TOKEN, CONFIG_TYPE } from '../../../../../shared/src/tokens';\n\nexport type CampaignID = string;\n\n@Component({\n selector: 'campaign-options',\n imports: [CommonModule, TranslateModule, MatMenuModule, MatIconModule, MatButtonModule],\n templateUrl: './campaign-options.component.html',\n styleUrls: ['./campaign-options.component.scss'],\n})\nexport class CampaignOptionsComponent {\n @Input() campaignId: string;\n @Input() partnerId: string;\n @Input() draftMode = false;\n @Input() isCampaignEditable = false;\n @Input() isCampaignHidden = false;\n @Input() campaignHasChildSteps = false;\n @Input() campaignStatus: string | null;\n @Input() updatingStatus = false;\n\n @Output() campaignDuplicated = new EventEmitter();\n\n // TODO: Move the code for these actions into this component to reduce duplication\n @Output() pauseRequested = new EventEmitter();\n @Output() resumeRequested = new EventEmitter();\n @Output() addListRequested = new EventEmitter();\n @Output() archiveRequested = new EventEmitter();\n @Output() deleteRequested = new EventEmitter();\n\n cs: typeof CampaignStatus = CampaignStatus;\n isPremade = this.campaignStateService.state.campaignDetails.isPremade;\n\n constructor(\n private readonly dialog: MatDialog,\n private readonly campaignStateService: CampaignStateService,\n @Inject(CONFIG_TOKEN) private readonly config: CONFIG_TYPE,\n ) {}\n\n async duplicateCampaign(): Promise {\n const sender = await firstValueFrom(this.config.sender$);\n const data: Data = {\n campaignId: this.campaignId,\n sender: sender,\n };\n\n const ref = this.dialog.open(CampaignDuplicateDialogComponent, { data: data });\n const result: DialogResult = await firstValueFrom(ref.afterClosed());\n this.campaignDuplicated.emit(result.newCampaignId);\n }\n\n unpublishCampaign(): void {\n this.campaignStateService.updateStatus(Statuses.STATUSES_DRAFT);\n }\n}\n","\n\n \n {{ 'COMMON.ACTION_LABELS.UNPUBLISH' | translate }}\n \n \n \n \n {{ 'COMMON.ACTION_LABELS.RESUME' | translate }}\n \n \n {{ 'COMMON.ACTION_LABELS.ARCHIVE' | translate }}\n \n \n\n","import { Component, Input, OnChanges } from '@angular/core';\nimport { TranslateService } from '@ngx-translate/core';\nimport { CommonModule } from '@angular/common';\nimport { CampaignStatus } from '../interface';\nimport { GalaxyBadgeModule } from '@vendasta/galaxy/badge';\n\n@Component({\n selector: 'campaign-state',\n imports: [CommonModule, GalaxyBadgeModule],\n templateUrl: './campaign-state.component.html',\n})\nexport class CampaignStateComponent implements OnChanges {\n state: string;\n colour: string;\n @Input() campaignState: string;\n @Input() status: CampaignStatus;\n\n constructor(private translate: TranslateService) {}\n\n ngOnChanges(): void {\n switch (this.status) {\n case CampaignStatus.PUBLISHED:\n case CampaignStatus.ACTIVE: {\n if (this.campaignState === 'ongoing') {\n this.state = this.translate.instant('CAMPAIGN_STATUS.ONGOING');\n this.colour = 'primary'; //blue\n break;\n }\n this.state = this.translate.instant('CAMPAIGN_STATUS.PUBLISHED');\n this.colour = 'green';\n break;\n }\n case CampaignStatus.ARCHIVED: {\n this.state = this.translate.instant('CAMPAIGN_STATUS.ARCHIVED');\n this.colour = 'gray';\n break;\n }\n case CampaignStatus.DRAFT:\n default: {\n this.state = this.translate.instant('CAMPAIGN_STATUS.DRAFT');\n this.colour = 'yellow';\n break;\n }\n }\n }\n}\n","{{ state }}\n","import { CampaignStepType } from '@vendasta/campaigns';\n\n/**\n * This will take a numerator and denominator and return a string percentage formatted to\n * at most one decimal point\n * @param numerator\n * @param denominator\n * @return formatted percentage\n */\nexport function formatStatsPercentage(numerator: number, denominator: number): string {\n let formattedValue = 'N/A';\n if (denominator) {\n // convert to a percentage then round to 1 decimal point\n const percentage = Math.round((numerator / denominator) * 100 * 10) / 10;\n formattedValue = percentage % 1 === 0 ? percentage.toFixed(0) + '%' : percentage.toFixed(1) + '%';\n }\n return formattedValue;\n}\n\n/**\n * Takes in a singular translation and a non-singular translation and returns the proper translation depending\n * on the comparison value\n * @param singularTranslation\n * @param pluralTranslation\n * @param comparisonValue\n * @return the singular or plural translation\n */\nexport function formatTranslation(\n singularTranslation: string,\n pluralTranslation: string,\n comparisonValue: number,\n): string {\n return comparisonValue === 1 ? singularTranslation : pluralTranslation;\n}\n\n/**\n * This will take a value and format it according to en-US locale. If truncate is set to true, values 1000 and up will\n * be abbreviated\n * @param value\n * @param truncate\n * @return formatted number\n */\nexport function formatStatsNum(value: number, truncate = false): string {\n if (truncate && value >= 1000) {\n let denominator = 1000;\n let suffix = 'k';\n\n if (value >= 1000000) {\n denominator = 1000000;\n suffix = 'm';\n }\n const truncated = Math.round((value / denominator) * 10) / 10;\n return truncated % 1 === 0 ? truncated.toFixed(0) + suffix : truncated.toFixed(1) + suffix;\n }\n return value.toLocaleString('en-US');\n}\n\n/**\n * Takes a number value and formats it so whole values have no decimal and one decimal place otherwise\n * @param percent\n * @return formatted percentage\n */\nexport function formatPercentage(percent: number): string {\n return percent % 1 === 0 ? percent.toFixed(0) + '%' : percent.toFixed(1) + '%';\n}\n\nexport function ConvertSteptypeStringToProto(step: string): CampaignStepType {\n switch (step) {\n case 'CAMPAIGN_STEP_TYPE_UNSPECIFIED':\n return CampaignStepType.CAMPAIGN_STEP_TYPE_UNSPECIFIED;\n case 'CAMPAIGN_STEP_TYPE_EMAIL':\n return CampaignStepType.CAMPAIGN_STEP_TYPE_EMAIL;\n case 'CAMPAIGN_STEP_TYPE_SNAPSHOT_CREATION':\n return CampaignStepType.CAMPAIGN_STEP_TYPE_SNAPSHOT_CREATION;\n case 'CAMPAIGN_STEP_TYPE_SMS':\n return CampaignStepType.CAMPAIGN_STEP_TYPE_SMS;\n }\n}\n","import { Injectable } from '@angular/core';\n\nimport { EmailEventService, EventType } from '@vendasta/email';\nimport { BehaviorSubject, combineLatest, Observable } from 'rxjs';\nimport { CampaignStats, EmailStat, EmailStatsData } from './interface';\nimport { SenderInterface, SenderType } from '@vendasta/campaigns';\nimport { GetAttributeEventStatsResponseEventStats } from '@vendasta/email/lib/_internal/objects/api';\nimport { map, startWith, switchMap } from 'rxjs/operators';\n\n@Injectable()\nexport class CampaignEmailStatsService {\n constructor(private emailEventService: EmailEventService) {}\n\n set sender(sender: SenderInterface) {\n this.sender$$.next(sender);\n }\n\n set campaignID(campaignID: string) {\n this.campaignID$$.next(campaignID);\n }\n //\n private sender$$: BehaviorSubject = new BehaviorSubject({\n type: SenderType.SENDER_TYPE_INVALID,\n id: '',\n });\n private campaignID$$: BehaviorSubject = new BehaviorSubject('');\n //\n public emailStats$: Observable = combineLatest([this.sender$$, this.campaignID$$]).pipe(\n switchMap(([sender, campaignID]: [SenderInterface, string]) => {\n return this.getEmailStats(sender, campaignID);\n }),\n );\n\n getEmailStats(sender: SenderInterface, campaignID: string): Observable {\n return this.emailEventService\n .getAttributeEventStats(\n {\n id: sender.id,\n type: sender.type,\n },\n {\n key: 'campaign_id',\n value: campaignID,\n },\n )\n .pipe(\n map((r) => this.convertEventStatsToEmailStatsData(r?.data)),\n startWith(this.createEmailStatsData({})),\n );\n }\n\n getEmailStepStats(sender: SenderInterface, campaignStepID: string): Observable {\n return this.emailEventService\n .getAttributeEventStats(\n {\n id: sender.id,\n type: sender.type,\n },\n {\n key: 'campaign_step_id',\n value: campaignStepID,\n },\n )\n .pipe(\n map((r) => this.convertEventStatsToEmailStatsData(r?.data)),\n startWith(this.createEmailStatsData({})),\n );\n }\n\n convertEventStatsToEmailStatsData(data: GetAttributeEventStatsResponseEventStats[]): EmailStatsData {\n const statsData = this.emptyEmailStatsData();\n if (data?.length > 0) {\n for (const d of data) {\n switch (d.eventType) {\n case EventType.DELIVERED:\n statsData.delivered = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n case EventType.OPENED:\n statsData.opened = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n case EventType.PROCESSED:\n statsData.processed = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n case EventType.CLICKED:\n statsData.clicked = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n case EventType.BOUNCED:\n statsData.bounced = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n case EventType.DEFERRED:\n statsData.deferred = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n case EventType.DROPPED:\n statsData.dropped = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n case EventType.SPAMREPORT:\n statsData.spamreport = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n case EventType.UNSUBSCRIBED:\n statsData.unsubscribed = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n case EventType.RESUBSCRIBED:\n statsData.resubscribed = {\n count: d.count,\n unique: d.uniqueCount,\n unique_by_email: d.uniqueRecipients,\n };\n break;\n }\n }\n }\n return statsData;\n }\n\n createCampaignStatsData = >(initialValues: T): CampaignStats & T => {\n return Object.assign(this.emptyCampaignStatsData(), initialValues);\n };\n\n createEmailStatsData = >(initialValues: T): EmailStatsData & T => {\n return Object.assign(this.emptyEmailStatsData(), initialValues);\n };\n\n emptyEmailStat = (): EmailStat => ({\n count: 0,\n unique: 0,\n unique_by_email: 0,\n });\n\n emptyEmailStatsData = (): EmailStatsData => ({\n //TODO, change this to meet the new interface\n processed: this.emptyEmailStat(),\n delivered: this.emptyEmailStat(),\n opened: this.emptyEmailStat(),\n clicked: this.emptyEmailStat(),\n bounced: this.emptyEmailStat(),\n deferred: this.emptyEmailStat(),\n dropped: this.emptyEmailStat(),\n spamreport: this.emptyEmailStat(),\n unsubscribed: this.emptyEmailStat(),\n resubscribed: this.emptyEmailStat(),\n });\n\n emptyCampaignStatsData = (): CampaignStats => ({\n active: 0,\n clicked_through: 0,\n completed: 0,\n delivered: 0,\n opened: 0,\n sent: 0,\n stopped: 0,\n total_accounts: 0,\n total_leads: 0,\n total_recipients: 0,\n undeliverable: 0,\n waiting_on_rate_limit: 0,\n });\n}\n","import { Injectable } from '@angular/core';\nimport {\n CampaignStatsService,\n GetCampaignDetailsStatsResponseInterface,\n SenderInterface,\n SenderType,\n} from '@vendasta/campaigns';\nimport { BehaviorSubject, combineLatest, distinct, Observable } from 'rxjs';\nimport { filter, map, switchMap } from 'rxjs/operators';\nimport { CampaignEmailStatsService } from '../campaign-email-stats.service';\nimport { EmailStatsData } from '../interface';\n\n@Injectable()\nexport class RecipientCampaignStatsService {\n set campaignID(campaignID: string) {\n this.campaignID$$.next(campaignID);\n }\n\n set sender(sender: SenderInterface) {\n this.sender$$.next(sender);\n }\n\n public recipientCampaignStats$: Observable;\n public emailStats$: Observable;\n\n private campaignID$$: BehaviorSubject = new BehaviorSubject('');\n private sender$$: BehaviorSubject = new BehaviorSubject({\n type: SenderType.SENDER_TYPE_INVALID,\n id: '',\n });\n private readonly senderID$ = this.sender$$.pipe(\n filter((sender: SenderInterface) => sender.type !== SenderType.SENDER_TYPE_INVALID),\n filter((sender: SenderInterface) => sender.id !== ''),\n map((sender: SenderInterface): string => {\n if (sender.type === SenderType.SENDER_TYPE_BUSINESS) {\n return 'business/' + sender.id;\n }\n return sender.id || '';\n }),\n );\n\n constructor(\n private readonly campaignStatsService: CampaignStatsService,\n private readonly emailStatsService: CampaignEmailStatsService,\n ) {\n this.recipientCampaignStats$ = combineLatest([this.campaignID$$, this.senderID$]).pipe(\n distinct(),\n switchMap(([campaignID, senderId]: [string, string]) => {\n return this.campaignStatsService.getCampaignDetailsStats({ partnerId: senderId, campaignId: campaignID });\n }),\n );\n\n this.emailStats$ = combineLatest([this.campaignID$$, this.sender$$]).pipe(\n distinct(),\n switchMap(([campaignID, sender]: [string, SenderInterface]) => {\n return this.emailStatsService.getEmailStats(sender, campaignID);\n }),\n );\n }\n}\n","import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';\nimport { Router, RouterModule } from '@angular/router';\nimport { TranslateModule, TranslateService } from '@ngx-translate/core';\nimport { CampaignFocus, CampaignStepInterface, EmailStatsData, RecipientCampaignStats } from '../interface';\nimport { ConvertSteptypeStringToProto, formatStatsPercentage, formatTranslation } from '../utils';\nimport { CommonModule } from '@angular/common';\nimport { MatTooltipModule } from '@angular/material/tooltip';\nimport { UIKitModule } from '@vendasta/uikit';\nimport { MatIconModule } from '@angular/material/icon';\nimport { FormatStatPipe } from '@vendasta/galaxy/pipes';\nimport { RecipientCampaignStatsService } from './recipient_campaign_stats.service';\nimport {\n CampaignStatsService,\n CampaignStepType,\n GetCampaignDetailsStatsResponseInterface,\n SenderInterface,\n} from '@vendasta/campaigns';\nimport { map, shareReplay } from 'rxjs/operators';\nimport { Observable } from 'rxjs';\nimport { CampaignEmailStatsService } from '../campaign-email-stats.service';\nimport { EmailEventService } from '@vendasta/email';\nimport { PAGE_ROUTES } from '../../../routing-constants';\n\n@Component({\n selector: 'campaign-stats',\n imports: [CommonModule, TranslateModule, MatTooltipModule, UIKitModule, MatIconModule, FormatStatPipe, RouterModule],\n providers: [RecipientCampaignStatsService, CampaignEmailStatsService, EmailEventService, CampaignStatsService],\n templateUrl: './campaign-stats.component.html',\n styleUrls: ['./campaign-stats.component.scss'],\n})\nexport class CampaignStatsComponent implements OnChanges, OnInit {\n public historyPageRoute = '';\n\n @Input()\n showRecipientStats = true;\n @Input()\n focus: string = CampaignFocus.UNSPECIFIED;\n @Input()\n sender: SenderInterface = {};\n @Input()\n campaignID = '';\n @Input()\n campaignSchedule: CampaignStepInterface[];\n queryParams = {};\n\n recipientCampaignStats$: Observable =\n this.recipientCampaignStatsService.recipientCampaignStats$.pipe(\n map((stats: GetCampaignDetailsStatsResponseInterface): RecipientCampaignStats => {\n return {\n active: stats.stats?.active || 0,\n completed: stats.stats?.completed || 0,\n stopped: stats.stats?.stopped || 0,\n total_accounts: stats.stats?.totalAccounts || 0,\n total_recipients: stats.stats?.totalRecipients || 0,\n waiting_on_rate_limit: stats.stats?.waitingOnRateLimit || 0,\n total_leads: stats.stats?.totalLeads || 0,\n };\n }),\n shareReplay(1),\n );\n\n emailstats$: Observable = this.campaignEmailStatsService.emailStats$.pipe(shareReplay(1));\n hasEmailStep = false;\n\n recipientsTooltip$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatTranslation(\n this.translate.instant('STATS.RECIPIENT_STATS.RECIPIENTS_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.RECIPIENT_STATS.RECIPIENTS_TOOLTIP', {\n recipients: stats.total_recipients,\n }),\n stats.total_recipients,\n );\n }),\n );\n hotLeadsTooltip$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatTranslation(\n this.translate.instant('STATS.PERFORMANCE_STATS.TOTAL_LEADS_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.PERFORMANCE_STATS.TOTAL_LEADS_TOOLTIP', {\n totalLeads: stats.total_leads,\n }),\n stats.total_leads,\n );\n }),\n );\n\n bufferingTooltip$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatTranslation(\n this.translate.instant('STATS.RECIPIENT_STATS.BUFFERING_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.RECIPIENT_STATS.BUFFERING_TOOLTIP', {\n buffering: stats.waiting_on_rate_limit,\n }),\n stats.waiting_on_rate_limit,\n );\n }),\n );\n inProgressTooltip$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatTranslation(\n this.translate.instant('STATS.RECIPIENT_STATS.IN_PROGRESS_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.RECIPIENT_STATS.IN_PROGRESS_TOOLTIP', {\n inProgress: stats.active,\n }),\n stats.active,\n );\n }),\n );\n stoppedTooltip$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatTranslation(\n this.translate.instant('STATS.RECIPIENT_STATS.STOPPED_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.RECIPIENT_STATS.STOPPED_TOOLTIP', {\n stopped: stats.stopped,\n }),\n stats.stopped,\n );\n }),\n );\n completedTooltip$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatTranslation(\n this.translate.instant('STATS.RECIPIENT_STATS.COMPLETED_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.RECIPIENT_STATS.COMPLETED_TOOLTIP', {\n completed: stats.completed,\n }),\n stats.completed,\n );\n }),\n );\n deliveryRateTooltip$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatTranslation(\n this.translate.instant('STATS.PERFORMANCE_STATS.DELIVERY_RATE_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.PERFORMANCE_STATS.DELIVERY_RATE_TOOLTIP', {\n delivered: stats.delivered.unique_by_email,\n }),\n stats.delivered.unique_by_email,\n );\n }),\n );\n openRateTooltip$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatTranslation(\n this.translate.instant('STATS.PERFORMANCE_STATS.OPEN_RATE_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.PERFORMANCE_STATS.OPEN_RATE_TOOLTIP', {\n opened: stats.opened.unique_by_email,\n }),\n stats.opened.unique_by_email,\n );\n }),\n );\n ctorTooltip$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatTranslation(\n this.translate.instant('STATS.PERFORMANCE_STATS.CLICKED_THROUGH_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.PERFORMANCE_STATS.CLICKED_THROUGH_TOOLTIP', {\n clickedThrough: stats.clicked.unique_by_email,\n }),\n stats.clicked.unique_by_email,\n );\n }),\n );\n undeliverableTooltip$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatTranslation(\n this.translate.instant('STATS.PERFORMANCE_STATS.UNDELIVERABLE_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.PERFORMANCE_STATS.UNDELIVERABLE_TOOLTIP', {\n undeliverable: stats.dropped.unique_by_email + stats.bounced.unique_by_email,\n }),\n stats.dropped.unique_by_email + stats.bounced.unique_by_email,\n );\n }),\n );\n clickedTooltip$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatTranslation(\n this.translate.instant('STATS.PERFORMANCE_STATS.CLICKED_TOOLTIP_SINGLE'),\n this.translate.instant('STATS.PERFORMANCE_STATS.CLICKED_TOOLTIP', {\n clicked: stats.clicked.unique_by_email,\n }),\n stats.clicked.unique_by_email,\n );\n }),\n );\n\n percentBuffering$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatStatsPercentage(stats.waiting_on_rate_limit, stats.total_recipients);\n }),\n );\n percentInProgress$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatStatsPercentage(stats.active, stats.total_recipients);\n }),\n );\n percentStopped$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatStatsPercentage(stats.stopped, stats.total_recipients);\n }),\n );\n percentCompleted$: Observable = this.recipientCampaignStats$.pipe(\n map((stats: RecipientCampaignStats): string => {\n return formatStatsPercentage(stats.completed, stats.total_recipients);\n }),\n );\n\n percentDeliveryRate$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatStatsPercentage(stats.delivered.unique_by_email, stats.processed.unique_by_email);\n }),\n );\n percentOpenRate$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatStatsPercentage(stats.opened.unique_by_email, stats.delivered.unique_by_email);\n }),\n );\n percentCTOR$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatStatsPercentage(stats.clicked.unique_by_email, stats.opened.unique_by_email);\n }),\n );\n percentUndeliverable$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatStatsPercentage(\n stats.bounced.unique_by_email + stats.dropped.unique_by_email,\n stats.processed.unique_by_email,\n );\n }),\n );\n percentClickedThroughRate$: Observable = this.emailstats$.pipe(\n map((stats: EmailStatsData): string => {\n return formatStatsPercentage(stats.clicked.unique_by_email, stats.delivered.unique_by_email);\n }),\n );\n\n constructor(\n private translate: TranslateService,\n private router: Router,\n private readonly recipientCampaignStatsService: RecipientCampaignStatsService,\n private readonly campaignEmailStatsService: CampaignEmailStatsService,\n ) {}\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes['campaignID'] && changes['campaignID'].currentValue) {\n const campaignID = changes['campaignID'].currentValue;\n this.recipientCampaignStatsService.campaignID = campaignID;\n this.campaignEmailStatsService.campaignID = campaignID;\n\n this.historyPageRoute = '../../' + PAGE_ROUTES.ROOT.CAMPAIGN_SUBROUTES.HISTORY(campaignID);\n this.queryParams = {\n filters: JSON.stringify({\n opened: true,\n }),\n };\n }\n if (changes['sender'] && changes['sender'].currentValue) {\n this.recipientCampaignStatsService.sender = changes['sender'].currentValue;\n this.campaignEmailStatsService.sender = changes['sender'].currentValue;\n }\n\n if (changes['campaignSchedule']) {\n this.campaignSchedule.forEach((step) => {\n if (ConvertSteptypeStringToProto(step.step_type) == CampaignStepType.CAMPAIGN_STEP_TYPE_EMAIL) {\n this.hasEmailStep = true;\n }\n });\n }\n }\n\n navigateToAccounts(campaignId: string): void {\n this.router.navigate(['campaign', 'accounts', campaignId]);\n }\n\n navigateToRecipients(campaignId: string): void {\n this.router.navigate(['marketing', 'campaign', campaignId, 'history']);\n }\n\n ngOnInit(): void {\n this.campaignSchedule.forEach((step) => {\n if (ConvertSteptypeStringToProto(step.step_type) == CampaignStepType.CAMPAIGN_STEP_TYPE_EMAIL) {\n this.hasEmailStep = true;\n }\n });\n }\n}\n","\n \n
\n \n \n \n \n
\n
\n {{ 'STATS.RECIPIENT_STATS.RECIPIENTS' | translate }}\n
\n @if (hasEmailStep) {\n \n {{ recipientCampaignStats.total_recipients | formatStat }}\n \n } @else {\n {{ recipientCampaignStats.total_recipients | formatStat }}\n }\n
\n
\n
\n \n \n \n
\n \n {{ 'STATS.RECIPIENT_STATS.BUFFERING' | translate }}\n \n \n {{ percentBuffering$ | async }}\n {{ recipientCampaignStats.waiting_on_rate_limit | formatStat: true }}\n \n
\n
\n \n {{ 'STATS.RECIPIENT_STATS.IN_PROGRESS' | translate }}\n \n \n {{ percentInProgress$ | async }}\n {{ recipientCampaignStats.active | formatStat: true }}\n \n
\n
\n \n {{ 'STATS.RECIPIENT_STATS.STOPPED' | translate }}\n \n \n {{ percentStopped$ | async }}\n {{ recipientCampaignStats.stopped | formatStat: true }}\n \n
\n
\n \n {{ 'STATS.RECIPIENT_STATS.COMPLETED' | translate }}\n \n \n {{ percentCompleted$ | async }}\n {{ recipientCampaignStats.completed | formatStat: true }}\n \n
\n
\n
\n
\n \n \n \n \n
\n
\n {{ 'STATS.PERFORMANCE_STATS.TOTAL_LEADS' | translate }}\n
\n @if (hasEmailStep) {\n \n {{ emailStats.opened.unique_by_email }}\n \n } @else {\n {{ emailStats.opened.unique_by_email }}\n }\n\n \n
\n \n
\n
\n {{ 'STATS.EMAIL.CLICKED' | translate }}\n
\n {{ emailStats.clicked.unique_by_email }}\n
\n
\n
\n {{ 'STATS.PERFORMANCE_STATS.CTOR' | translate }}\n
\n {{ percentClickedThroughRate$ | async }}\n
\n
\n
\n
\n \n \n \n
\n \n {{ 'STATS.PERFORMANCE_STATS.DELIVERY_RATE' | translate }}\n \n \n {{ percentDeliveryRate$ | async }}\n {{ emailStats.delivered.unique_by_email | formatStat: true }}\n \n
\n
\n \n {{ 'STATS.PERFORMANCE_STATS.OPEN_RATE' | translate }}\n \n \n {{ percentOpenRate$ | async }}\n {{ emailStats.opened.unique_by_email | formatStat: true }}\n \n
\n
\n \n {{ 'STATS.PERFORMANCE_STATS.CTOR' | translate }}\n \n \n {{ percentCTOR$ | async }}\n {{ emailStats.clicked.unique_by_email | formatStat: true }}\n \n
\n
\n \n {{ 'STATS.PERFORMANCE_STATS.UNDELIVERABLE' | translate }}\n \n \n {{ percentUndeliverable$ | async }}\n {{ emailStats.bounced.unique_by_email + emailStats.dropped.unique_by_email | formatStat: true }}\n \n
\n
\n
\n
\n
\n
\n
\n","import { Injectable } from '@angular/core';\nimport { SenderInterface, SenderType } from '@vendasta/campaigns';\nimport { EventService, OwnerType, StatusStatInterface, StatusType } from '@vendasta/smsv2';\nimport { SmsStepStats } from './interface';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs/operators';\n\n@Injectable()\nexport class CampaignSmsStatsService {\n constructor(private smsEventService: EventService) {}\n\n getSmsStepStats(sender: SenderInterface, campaignStepID: string): Observable {\n return this.smsEventService\n .getAttributeEventStats(\n {\n ownerId: sender.id,\n ownerType: this.senderToOwner(sender.type || 0),\n namespace: 'RM', // campaigns uses the RM namespace\n },\n {\n key: 'campaign_step_id',\n value: campaignStepID,\n },\n )\n .pipe(\n map((r) => {\n if (r.stats?.length === 0) {\n return this.emptyCampaignStatsData();\n }\n return this.responseToSmsStepStats(r.stats ?? []);\n }),\n );\n }\n\n responseToSmsStepStats(stats: StatusStatInterface[]): SmsStepStats {\n const result = this.emptyCampaignStatsData();\n for (const stat of stats) {\n switch (stat.status) {\n case StatusType.STATUS_TYPE_DELIVERED:\n result.delivered = stat.value || 0;\n break;\n case StatusType.STATUS_TYPE_SENT:\n result.sent = stat.value || 0;\n break;\n case StatusType.STATUS_TYPE_FAILED:\n result.failed = stat.value || 0;\n break;\n case StatusType.STATUS_TYPE_QUEUED:\n result.queued = stat.value || 0;\n break;\n case StatusType.STATUS_TYPE_UNDELIVERED:\n result.undelivered = stat.value || 0;\n break;\n }\n }\n return result;\n }\n\n emptyCampaignStatsData = (): SmsStepStats => ({\n queued: 0,\n sent: 0,\n delivered: 0,\n undelivered: 0,\n failed: 0,\n });\n\n senderToOwner(t: SenderType): OwnerType {\n switch (t) {\n case SenderType.SENDER_TYPE_PARTNER:\n return OwnerType.OWNER_TYPE_PARTNER;\n case SenderType.SENDER_TYPE_BUSINESS:\n return OwnerType.OWNER_TYPE_ACCOUNT_GROUP;\n default:\n return OwnerType.OWNER_TYPE_UNSPECIFIED;\n }\n }\n}\n","import { CampaignSmsStatsService } from './campaign-sms-stats.service';\nimport { CampaignStepStats, EmailStepStats, Ratio, SmsStepStats } from './interface';\nimport { CampaignEmailStatsService } from './campaign-email-stats.service';\nimport { formatStatsPercentage } from './utils';\nimport { map, Observable } from 'rxjs';\nimport { SenderInterface } from '@vendasta/campaigns';\n\nexport class SMSStepStatsGetter implements StepStatsGetter {\n constructor(private smsEventStatsService: CampaignSmsStatsService) {}\n\n getStepStats(sender: SenderInterface, campaign_step_id: string): Observable {\n return this.smsEventStatsService.getSmsStepStats(sender, campaign_step_id);\n }\n}\n\nexport class EmailStepStatsGetter implements StepStatsGetter {\n constructor(private emailEventStatsService: CampaignEmailStatsService) {}\n getStepStats(sender: SenderInterface, campaign_step_id: string): Observable {\n return this.emailEventStatsService.getEmailStepStats(sender, campaign_step_id).pipe(\n map((stats) => {\n return {\n bounced: stats.bounced.count,\n campaign_step_id: campaign_step_id,\n clickedThrough: stats.clicked.count,\n created: 0,\n delivered: stats.delivered.count,\n dropped: stats.dropped.count,\n notRequired: 0,\n onDeck: 0,\n opened: stats.opened.count,\n paused: 0,\n pending: 0,\n refreshed: 0,\n sent: 0,\n spamReport: stats.spamreport.count,\n unsubscribed: stats.unsubscribed.count,\n clickToOpenRate: 0,\n clickToOpenRatio: new Ratio(stats.clicked.unique_by_email, stats.opened.unique_by_email).withFormatter(\n formatStatsPercentage,\n ),\n openRate: 0,\n openRatio: new Ratio(stats.opened.unique_by_email, stats.delivered.unique_by_email).withFormatter(\n formatStatsPercentage,\n ),\n };\n }),\n );\n }\n}\n\nexport abstract class StepStatsGetter {\n abstract getStepStats(sender: SenderInterface, campaign_step_id: string): Observable;\n}\n","import { CommonModule } from '@angular/common';\nimport { Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { MatAutocompleteModule } from '@angular/material/autocomplete';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatCardModule } from '@angular/material/card';\nimport { MatOptionModule } from '@angular/material/core';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatGridListModule } from '@angular/material/grid-list';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatProgressSpinnerModule } from '@angular/material/progress-spinner';\nimport { MatTooltipModule } from '@angular/material/tooltip';\nimport { Router, RouterLink } from '@angular/router';\nimport { EmailStatsModule } from '@galaxy/email-ui/email-activity';\nimport { DisplayLinkStats } from '@galaxy/email-ui/email-activity/src/email-stats/email-stats';\nimport { LinkActivityService } from '@galaxy/email-ui/email-activity/src/link-activity/link-activity.service';\nimport { TranslateModule, TranslateService } from '@ngx-translate/core';\nimport { CampaignStepInterfaceInterface, CampaignStepType } from '@vendasta/campaigns';\nimport { AppNamespace } from '@vendasta/email-builder';\nimport { GalaxyAlertModule } from '@vendasta/galaxy/alert';\nimport { GalaxyChatModule } from '@vendasta/galaxy/chat';\nimport { EmailViewerComponent, GalaxyEmailViewerModule } from '@vendasta/galaxy/email-viewer';\nimport { GalaxyInputModule } from '@vendasta/galaxy/input';\nimport { GalaxyLoadingSpinnerModule } from '@vendasta/galaxy/loading-spinner';\nimport { GalaxyPipesModule } from '@vendasta/galaxy/pipes';\nimport { GalaxySideDrawerModule } from '@vendasta/galaxy/side-drawer/src/side-drawer.module';\nimport { GalaxyStatisticModule } from '@vendasta/galaxy/statistic';\nimport { Template, TemplatesService, TemplateType } from '@vendasta/templates';\nimport { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';\nimport { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';\nimport { CampaignConfig } from '../../../dependencies';\nimport { PAGE_ROUTES } from '../../../routing-constants';\nimport { CampaignEmailStatsService } from '../campaign-email-stats.service';\nimport { StepDetailInterface } from '../campaign-step/campaign-step.component';\nimport { CampaignStepInterface, CampaignStepStats, isSmsStepStats, StepNumberInterface } from '../interface';\nimport { MarketingAutomationService } from '../marketing-automation.service';\nimport { formatTranslation } from '../utils';\nimport { CampaignSmsStatsService } from '../campaign-sms-stats.service';\nimport { EmailStepStatsGetter, SMSStepStatsGetter } from '../campaign-stats';\nimport { CONFIG_TOKEN } from '../../../../../shared/src/tokens';\n\nenum ActivityTypeFilter {\n CLICKED = 'clicked',\n OPENED = 'opened',\n DELIVERED = 'delivered',\n BOUNCED = 'bounced',\n SPAMREPORT = 'spamreport',\n UNSUBSCRIBED = 'unsubscribed',\n DROPPED = 'dropped',\n}\n\ninterface state {\n stepId: string;\n stepTemplateID: string;\n stepType: CampaignStepType;\n\n previewStepName: string;\n previewSubject: string;\n previewHTML: string;\n\n loadingPreview?: boolean;\n}\n\nconst initialState: state = {\n stepId: '',\n stepTemplateID: '',\n stepType: CampaignStepType.CAMPAIGN_STEP_TYPE_UNSPECIFIED,\n\n previewStepName: '',\n previewSubject: '',\n previewHTML: '',\n};\n\n@Component({\n imports: [\n CommonModule,\n TranslateModule,\n GalaxyEmailViewerModule,\n MatProgressSpinnerModule,\n GalaxyInputModule,\n MatOptionModule,\n MatAutocompleteModule,\n MatFormFieldModule,\n GalaxyAlertModule,\n MatIconModule,\n GalaxyPipesModule,\n GalaxyStatisticModule,\n MatGridListModule,\n FormsModule,\n ReactiveFormsModule,\n MatCardModule,\n EmailStatsModule,\n MatTooltipModule,\n GalaxySideDrawerModule,\n RouterLink,\n MatButtonModule,\n GalaxyLoadingSpinnerModule,\n GalaxyChatModule,\n ],\n selector: 'campaign-step-details-preview',\n templateUrl: './campaign-step-details-preview.component.html',\n styleUrls: ['./campaign-step-details-preview.component.scss'],\n})\nexport class CampaignStepDetailsPreviewComponent implements OnInit, OnDestroy {\n @ViewChild('preview') emailViewer: EmailViewerComponent;\n @Input() campaignDetails: {\n campaignId: string;\n locale: string;\n isEditable: boolean;\n campaignSchedule: CampaignStepInterfaceInterface[];\n };\n @Input() showStats: boolean;\n @Input() stepData: CampaignStepInterface;\n @Input() initialStepID: string;\n\n @Output() editEmailFromPreviewClick: EventEmitter = new EventEmitter();\n @Output() editSMSFromPreviewClick: EventEmitter = new EventEmitter();\n\n subscriptions: Subscription[] = [];\n\n private state$$ = new BehaviorSubject(initialState);\n\n set state(state: state) {\n this.state$$.next(state);\n }\n\n get state(): state {\n return this.state$$.getValue();\n }\n\n activityTypeFilter = ActivityTypeFilter;\n\n public templatePreview$: Observable<{\n previewSubject: string;\n previewBodyHTML: string;\n previewName: string;\n }>;\n\n public smsPreview$: Observable