@forgeroute/ui 0.4.27 → 0.4.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/forgeroute-ui.mjs +4274 -47
- package/fesm2022/forgeroute-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/types/forgeroute-ui.d.ts +808 -9
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as i0 from "@angular/core";
|
|
2
|
-
import { ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Directive, ElementRef, EnvironmentInjector, HostBinding, HostListener, Injectable, InjectionToken, Injector, Input, NgZone, PLATFORM_ID, Renderer2, TemplateRef, ViewEncapsulation, afterNextRender, computed, contentChild, contentChildren, createComponent, effect, forwardRef, inject, input, isDevMode, linkedSignal, makeEnvironmentProviders, model, output, provideEnvironmentInitializer, signal, untracked, viewChild } from "@angular/core";
|
|
2
|
+
import { ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Directive, ElementRef, EnvironmentInjector, HostBinding, HostListener, Injectable, InjectionToken, Injector, Input, NgZone, PLATFORM_ID, Renderer2, TemplateRef, ViewEncapsulation, afterEveryRender, afterNextRender, computed, contentChild, contentChildren, createComponent, effect, forwardRef, inject, input, isDevMode, linkedSignal, makeEnvironmentProviders, model, output, provideEnvironmentInitializer, signal, untracked, viewChild, viewChildren } from "@angular/core";
|
|
3
3
|
import { HttpClient, HttpErrorResponse, HttpParams } from "@angular/common/http";
|
|
4
4
|
import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
|
|
5
5
|
import { catchError, debounceTime, distinctUntilChanged, skip, switchMap } from "rxjs/operators";
|
|
6
|
-
import { Subject, catchError as catchError$1, debounceTime as debounceTime$1, filter, of, switchMap as switchMap$1, throwError } from "rxjs";
|
|
6
|
+
import { Subject, catchError as catchError$1, debounceTime as debounceTime$1, filter, firstValueFrom, isObservable, of, switchMap as switchMap$1, throwError } from "rxjs";
|
|
7
7
|
import { DOCUMENT, DatePipe, DecimalPipe, Location, NgTemplateOutlet, isPlatformBrowser } from "@angular/common";
|
|
8
8
|
import * as i1 from "@angular/forms";
|
|
9
9
|
import { FormControl, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule } from "@angular/forms";
|
|
@@ -40896,7 +40896,7 @@ const REAL_ESTATE_AND_SHARED_INTEGRATIONS = [
|
|
|
40896
40896
|
key: "google-ads",
|
|
40897
40897
|
name: "Google Ads",
|
|
40898
40898
|
description: "Sync retargeting audiences and track lead attribution from your Google Ads campaigns.",
|
|
40899
|
-
apps: ["
|
|
40899
|
+
apps: ["marketing"],
|
|
40900
40900
|
status: "live",
|
|
40901
40901
|
group: "Marketing",
|
|
40902
40902
|
iconBg: "#fefce8",
|
|
@@ -40937,27 +40937,46 @@ const REAL_ESTATE_AND_SHARED_INTEGRATIONS = [
|
|
|
40937
40937
|
]
|
|
40938
40938
|
},
|
|
40939
40939
|
{
|
|
40940
|
-
key: "facebook
|
|
40941
|
-
name: "Facebook
|
|
40942
|
-
description: "
|
|
40943
|
-
apps: ["
|
|
40944
|
-
status: "
|
|
40940
|
+
key: "facebook",
|
|
40941
|
+
name: "Facebook & Meta Ads",
|
|
40942
|
+
description: "Publish to your Facebook Page and capture Lead Ad leads into your pipeline.",
|
|
40943
|
+
apps: ["marketing"],
|
|
40944
|
+
status: "live",
|
|
40945
|
+
group: "Social Media",
|
|
40945
40946
|
iconBg: "#eff6ff",
|
|
40946
40947
|
iconColor: "#1877f2",
|
|
40947
|
-
connectLabel: "Connect
|
|
40948
|
-
connectDesc: "
|
|
40948
|
+
connectLabel: "Connect Facebook",
|
|
40949
|
+
connectDesc: "Sign in with Facebook and choose the Page to post to. The same connection powers Meta Ads tracking and lead capture.",
|
|
40950
|
+
setupSteps: [
|
|
40951
|
+
{
|
|
40952
|
+
title: "Open Marketing and click \"Connect Facebook\"",
|
|
40953
|
+
desc: "You'll be redirected to Facebook. Sign in with the account that manages your business Page."
|
|
40954
|
+
},
|
|
40955
|
+
{
|
|
40956
|
+
title: "Approve Page, ads and lead access",
|
|
40957
|
+
desc: "Facebook asks for permission to post to your Page, read ad performance and receive Lead Ad form submissions."
|
|
40958
|
+
},
|
|
40959
|
+
{
|
|
40960
|
+
title: "Choose your Page",
|
|
40961
|
+
desc: "Back in the CRM, confirm the Page name shown on this card. Open Marketing → Ads to see campaigns."
|
|
40962
|
+
}
|
|
40963
|
+
],
|
|
40949
40964
|
howItWorks: [
|
|
40950
40965
|
{
|
|
40951
|
-
title: "
|
|
40952
|
-
desc: "
|
|
40966
|
+
title: "Post and schedule",
|
|
40967
|
+
desc: "Publish listings and updates to your Page from the Marketing calendar."
|
|
40968
|
+
},
|
|
40969
|
+
{
|
|
40970
|
+
title: "Lead Ads become contacts",
|
|
40971
|
+
desc: "Facebook Lead Form submissions automatically create contacts with the source set."
|
|
40953
40972
|
},
|
|
40954
40973
|
{
|
|
40955
|
-
title: "
|
|
40956
|
-
desc: "
|
|
40974
|
+
title: "Ad performance and attribution",
|
|
40975
|
+
desc: "Campaign spend and leads sync every few hours so you can see which campaigns drive deals."
|
|
40957
40976
|
},
|
|
40958
40977
|
{
|
|
40959
|
-
title: "
|
|
40960
|
-
desc: "
|
|
40978
|
+
title: "Conversions and audiences",
|
|
40979
|
+
desc: "Send conversions and custom audiences back to Meta from the CRM."
|
|
40961
40980
|
}
|
|
40962
40981
|
]
|
|
40963
40982
|
},
|
|
@@ -41344,7 +41363,7 @@ const REAL_ESTATE_AND_SHARED_INTEGRATIONS = [
|
|
|
41344
41363
|
key: "mailchimp",
|
|
41345
41364
|
name: "Mailchimp",
|
|
41346
41365
|
description: "Email marketing campaigns, drip sequences, and newsletters.",
|
|
41347
|
-
apps: ["
|
|
41366
|
+
apps: ["marketing"],
|
|
41348
41367
|
status: "coming-soon",
|
|
41349
41368
|
iconBg: "#fefce8",
|
|
41350
41369
|
iconColor: "#ca8a04",
|
|
@@ -41356,7 +41375,7 @@ const REAL_ESTATE_AND_SHARED_INTEGRATIONS = [
|
|
|
41356
41375
|
key: "canva",
|
|
41357
41376
|
name: "Canva",
|
|
41358
41377
|
description: "Design listing flyers, social posts, and marketing materials.",
|
|
41359
|
-
apps: ["
|
|
41378
|
+
apps: ["marketing"],
|
|
41360
41379
|
status: "coming-soon",
|
|
41361
41380
|
iconBg: "#eff6ff",
|
|
41362
41381
|
iconColor: "#2563eb",
|
|
@@ -41368,7 +41387,7 @@ const REAL_ESTATE_AND_SHARED_INTEGRATIONS = [
|
|
|
41368
41387
|
key: "bombbomb",
|
|
41369
41388
|
name: "BombBomb",
|
|
41370
41389
|
description: "Video email marketing to stand out in client inboxes.",
|
|
41371
|
-
apps: ["
|
|
41390
|
+
apps: ["marketing"],
|
|
41372
41391
|
status: "coming-soon",
|
|
41373
41392
|
iconBg: "#fef2f2",
|
|
41374
41393
|
iconColor: "#e53e3e",
|
|
@@ -41379,50 +41398,166 @@ const REAL_ESTATE_AND_SHARED_INTEGRATIONS = [
|
|
|
41379
41398
|
{
|
|
41380
41399
|
key: "instagram",
|
|
41381
41400
|
name: "Instagram",
|
|
41382
|
-
description: "Post listings
|
|
41383
|
-
apps: ["
|
|
41384
|
-
status: "
|
|
41401
|
+
description: "Post listings to your Instagram business account and sync lead-form leads.",
|
|
41402
|
+
apps: ["marketing"],
|
|
41403
|
+
status: "live",
|
|
41404
|
+
group: "Social Media",
|
|
41385
41405
|
iconBg: "#fff1f2",
|
|
41386
41406
|
iconColor: "#e11d48",
|
|
41387
|
-
connectLabel: "
|
|
41388
|
-
connectDesc: "",
|
|
41389
|
-
|
|
41407
|
+
connectLabel: "Connect Instagram",
|
|
41408
|
+
connectDesc: "Needs an Instagram Business or Creator account linked to a Facebook Page.",
|
|
41409
|
+
setupSteps: [
|
|
41410
|
+
{
|
|
41411
|
+
title: "Switch to a Business or Creator account",
|
|
41412
|
+
desc: "In the Instagram app: Settings → Account type and tools → Switch to professional account."
|
|
41413
|
+
},
|
|
41414
|
+
{
|
|
41415
|
+
title: "Click \"Connect Instagram\" on Marketing",
|
|
41416
|
+
desc: "You'll be redirected to Meta to sign in and approve publishing access."
|
|
41417
|
+
},
|
|
41418
|
+
{
|
|
41419
|
+
title: "Confirm the account name",
|
|
41420
|
+
desc: "Back in the CRM, the connected handle shows on this card."
|
|
41421
|
+
}
|
|
41422
|
+
],
|
|
41423
|
+
howItWorks: [
|
|
41424
|
+
{
|
|
41425
|
+
title: "Post and schedule",
|
|
41426
|
+
desc: "Posts and reels publish from the Marketing calendar."
|
|
41427
|
+
},
|
|
41428
|
+
{
|
|
41429
|
+
title: "Engagement analytics",
|
|
41430
|
+
desc: "Likes, comments and reach are pulled into Marketing analytics."
|
|
41431
|
+
},
|
|
41432
|
+
{
|
|
41433
|
+
title: "Lead sync",
|
|
41434
|
+
desc: "Instagram lead-form leads become contacts every few hours."
|
|
41435
|
+
},
|
|
41436
|
+
{
|
|
41437
|
+
title: "Reconnect prompts",
|
|
41438
|
+
desc: "If the access token expires you'll be asked to reconnect."
|
|
41439
|
+
}
|
|
41440
|
+
]
|
|
41390
41441
|
},
|
|
41391
41442
|
{
|
|
41392
41443
|
key: "linkedin",
|
|
41393
41444
|
name: "LinkedIn",
|
|
41394
|
-
description: "
|
|
41395
|
-
apps: ["
|
|
41396
|
-
status: "
|
|
41445
|
+
description: "Share listings and market updates to LinkedIn and import Lead Gen form leads.",
|
|
41446
|
+
apps: ["marketing"],
|
|
41447
|
+
status: "live",
|
|
41448
|
+
group: "Social Media",
|
|
41397
41449
|
iconBg: "#eff6ff",
|
|
41398
41450
|
iconColor: "#0a66c2",
|
|
41399
|
-
connectLabel: "
|
|
41400
|
-
connectDesc: "",
|
|
41401
|
-
|
|
41451
|
+
connectLabel: "Connect LinkedIn",
|
|
41452
|
+
connectDesc: "Posts as your personal profile.",
|
|
41453
|
+
setupSteps: [
|
|
41454
|
+
{
|
|
41455
|
+
title: "Click \"Connect LinkedIn\" on Marketing",
|
|
41456
|
+
desc: "You'll be redirected to LinkedIn to sign in."
|
|
41457
|
+
},
|
|
41458
|
+
{
|
|
41459
|
+
title: "Approve sharing and lead-gen access",
|
|
41460
|
+
desc: "LinkedIn asks permission to post for you and to read Lead Gen form responses."
|
|
41461
|
+
},
|
|
41462
|
+
{
|
|
41463
|
+
title: "Set up Lead Gen forms",
|
|
41464
|
+
desc: "Create Lead Gen forms in LinkedIn Campaign Manager; responses flow into the CRM."
|
|
41465
|
+
}
|
|
41466
|
+
],
|
|
41467
|
+
howItWorks: [
|
|
41468
|
+
{
|
|
41469
|
+
title: "Share to your network",
|
|
41470
|
+
desc: "Posts publish to your profile from the Marketing calendar."
|
|
41471
|
+
},
|
|
41472
|
+
{
|
|
41473
|
+
title: "Lead Gen form leads",
|
|
41474
|
+
desc: "Responses are checked every 15 minutes and become contacts."
|
|
41475
|
+
},
|
|
41476
|
+
{
|
|
41477
|
+
title: "Reconnect when needed",
|
|
41478
|
+
desc: "LinkedIn access lasts about 60 days; you'll be asked to reconnect."
|
|
41479
|
+
}
|
|
41480
|
+
]
|
|
41402
41481
|
},
|
|
41403
41482
|
{
|
|
41404
|
-
key: "tiktok
|
|
41405
|
-
name: "TikTok
|
|
41406
|
-
description: "
|
|
41407
|
-
apps: ["
|
|
41408
|
-
status: "
|
|
41409
|
-
|
|
41483
|
+
key: "tiktok",
|
|
41484
|
+
name: "TikTok",
|
|
41485
|
+
description: "Publish videos to TikTok and capture TikTok Lead Generation leads.",
|
|
41486
|
+
apps: ["marketing"],
|
|
41487
|
+
status: "live",
|
|
41488
|
+
group: "Social Media",
|
|
41489
|
+
iconBg: "#f1f5f9",
|
|
41410
41490
|
iconColor: "#010101",
|
|
41411
|
-
connectLabel: "
|
|
41412
|
-
connectDesc: "",
|
|
41413
|
-
|
|
41491
|
+
connectLabel: "Connect TikTok",
|
|
41492
|
+
connectDesc: "Connect your TikTok account first; lead capture uses the advertiser id from this connection.",
|
|
41493
|
+
setupSteps: [
|
|
41494
|
+
{
|
|
41495
|
+
title: "Click \"Connect TikTok\" on Marketing",
|
|
41496
|
+
desc: "You'll be redirected to TikTok to sign in and approve access."
|
|
41497
|
+
},
|
|
41498
|
+
{
|
|
41499
|
+
title: "Copy the lead webhook URL",
|
|
41500
|
+
desc: "Use the webhook URL for TikTok leads (shown on Marketing → Lead Sync)."
|
|
41501
|
+
},
|
|
41502
|
+
{
|
|
41503
|
+
title: "Add it in TikTok Ads Manager",
|
|
41504
|
+
desc: "Paste the URL into your Lead Generation form settings and submit a test lead."
|
|
41505
|
+
}
|
|
41506
|
+
],
|
|
41507
|
+
howItWorks: [
|
|
41508
|
+
{
|
|
41509
|
+
title: "Publish videos",
|
|
41510
|
+
desc: "Listing videos publish to TikTok from the Marketing calendar."
|
|
41511
|
+
},
|
|
41512
|
+
{
|
|
41513
|
+
title: "Leads become contacts",
|
|
41514
|
+
desc: "Each Lead Generation submission is matched to your company by advertiser id and becomes a contact with source TikTok."
|
|
41515
|
+
},
|
|
41516
|
+
{
|
|
41517
|
+
title: "Secure delivery",
|
|
41518
|
+
desc: "Lead requests are signature-checked before they are accepted."
|
|
41519
|
+
}
|
|
41520
|
+
]
|
|
41414
41521
|
},
|
|
41415
41522
|
{
|
|
41416
41523
|
key: "youtube",
|
|
41417
41524
|
name: "YouTube",
|
|
41418
|
-
description: "Upload property tour videos
|
|
41419
|
-
apps: ["
|
|
41420
|
-
status: "
|
|
41525
|
+
description: "Upload property tour videos to your YouTube channel.",
|
|
41526
|
+
apps: ["marketing"],
|
|
41527
|
+
status: "live",
|
|
41528
|
+
group: "Social Media",
|
|
41421
41529
|
iconBg: "#fef2f2",
|
|
41422
41530
|
iconColor: "#dc2626",
|
|
41423
|
-
connectLabel: "
|
|
41424
|
-
connectDesc: "",
|
|
41425
|
-
|
|
41531
|
+
connectLabel: "Connect YouTube",
|
|
41532
|
+
connectDesc: "Grants upload and read access to your channel.",
|
|
41533
|
+
setupSteps: [
|
|
41534
|
+
{
|
|
41535
|
+
title: "Click \"Connect YouTube\" on Marketing",
|
|
41536
|
+
desc: "You'll be redirected to Google to sign in."
|
|
41537
|
+
},
|
|
41538
|
+
{
|
|
41539
|
+
title: "Choose the channel",
|
|
41540
|
+
desc: "Pick the YouTube channel the videos should go to."
|
|
41541
|
+
},
|
|
41542
|
+
{
|
|
41543
|
+
title: "Approve upload access",
|
|
41544
|
+
desc: "Grant permission to upload videos and read basic channel stats."
|
|
41545
|
+
}
|
|
41546
|
+
],
|
|
41547
|
+
howItWorks: [
|
|
41548
|
+
{
|
|
41549
|
+
title: "Upload from the CRM",
|
|
41550
|
+
desc: "Videos are uploaded from the Marketing calendar with the title and description of the post."
|
|
41551
|
+
},
|
|
41552
|
+
{
|
|
41553
|
+
title: "Stats read back",
|
|
41554
|
+
desc: "View counts flow into Marketing analytics."
|
|
41555
|
+
},
|
|
41556
|
+
{
|
|
41557
|
+
title: "Disconnect any time",
|
|
41558
|
+
desc: "Disconnecting revokes the CRM's access to your channel."
|
|
41559
|
+
}
|
|
41560
|
+
]
|
|
41426
41561
|
},
|
|
41427
41562
|
{
|
|
41428
41563
|
key: "loom",
|
|
@@ -41460,6 +41595,206 @@ const REAL_ESTATE_AND_SHARED_INTEGRATIONS = [
|
|
|
41460
41595
|
connectDesc: "",
|
|
41461
41596
|
howItWorks: []
|
|
41462
41597
|
},
|
|
41598
|
+
{
|
|
41599
|
+
key: "google-business",
|
|
41600
|
+
name: "Google Business Profile",
|
|
41601
|
+
description: "Post updates and offers to your Google Business Profile.",
|
|
41602
|
+
apps: ["marketing"],
|
|
41603
|
+
status: "live",
|
|
41604
|
+
group: "Social Media",
|
|
41605
|
+
iconBg: "#eff6ff",
|
|
41606
|
+
iconColor: "#4285f4",
|
|
41607
|
+
connectLabel: "Connect Google Business",
|
|
41608
|
+
connectDesc: "Use the Google account that manages your business listing.",
|
|
41609
|
+
setupSteps: [
|
|
41610
|
+
{
|
|
41611
|
+
title: "Click \"Connect Google Business\" on Marketing",
|
|
41612
|
+
desc: "You'll be redirected to Google to sign in."
|
|
41613
|
+
},
|
|
41614
|
+
{
|
|
41615
|
+
title: "Choose the account that owns your listing",
|
|
41616
|
+
desc: "Pick the location you want to post to."
|
|
41617
|
+
},
|
|
41618
|
+
{
|
|
41619
|
+
title: "Approve access",
|
|
41620
|
+
desc: "Grant permission to manage posts on your business profile."
|
|
41621
|
+
}
|
|
41622
|
+
],
|
|
41623
|
+
howItWorks: [
|
|
41624
|
+
{
|
|
41625
|
+
title: "Posts on your listing",
|
|
41626
|
+
desc: "Updates appear on your Google profile and Maps listing."
|
|
41627
|
+
},
|
|
41628
|
+
{
|
|
41629
|
+
title: "Scheduled posts",
|
|
41630
|
+
desc: "Scheduled posts publish automatically."
|
|
41631
|
+
},
|
|
41632
|
+
{
|
|
41633
|
+
title: "Token health",
|
|
41634
|
+
desc: "The card shows when a reconnect is needed."
|
|
41635
|
+
}
|
|
41636
|
+
]
|
|
41637
|
+
},
|
|
41638
|
+
{
|
|
41639
|
+
key: "pinterest",
|
|
41640
|
+
name: "Pinterest",
|
|
41641
|
+
description: "Pin listing photos to your Pinterest boards.",
|
|
41642
|
+
apps: ["marketing"],
|
|
41643
|
+
status: "live",
|
|
41644
|
+
group: "Social Media",
|
|
41645
|
+
iconBg: "#fef2f2",
|
|
41646
|
+
iconColor: "#e60023",
|
|
41647
|
+
connectLabel: "Connect Pinterest",
|
|
41648
|
+
connectDesc: "Sign in with Pinterest and pick the board to pin to.",
|
|
41649
|
+
setupSteps: [
|
|
41650
|
+
{
|
|
41651
|
+
title: "Click \"Connect Pinterest\" on Marketing",
|
|
41652
|
+
desc: "You'll be redirected to Pinterest to sign in."
|
|
41653
|
+
},
|
|
41654
|
+
{
|
|
41655
|
+
title: "Approve pin access",
|
|
41656
|
+
desc: "Grant permission to create pins on your boards."
|
|
41657
|
+
},
|
|
41658
|
+
{
|
|
41659
|
+
title: "Choose a board",
|
|
41660
|
+
desc: "Pick the board listing photos should go to."
|
|
41661
|
+
}
|
|
41662
|
+
],
|
|
41663
|
+
howItWorks: [
|
|
41664
|
+
{
|
|
41665
|
+
title: "Pin listings",
|
|
41666
|
+
desc: "Listing photos are pinned from the Marketing calendar."
|
|
41667
|
+
},
|
|
41668
|
+
{
|
|
41669
|
+
title: "Scheduled pins",
|
|
41670
|
+
desc: "Scheduled pins publish automatically."
|
|
41671
|
+
},
|
|
41672
|
+
{
|
|
41673
|
+
title: "Disconnect any time",
|
|
41674
|
+
desc: "Disconnecting revokes access."
|
|
41675
|
+
}
|
|
41676
|
+
]
|
|
41677
|
+
},
|
|
41678
|
+
{
|
|
41679
|
+
key: "x",
|
|
41680
|
+
name: "X (Twitter)",
|
|
41681
|
+
description: "Post listing updates and links to X.",
|
|
41682
|
+
apps: ["marketing"],
|
|
41683
|
+
status: "live",
|
|
41684
|
+
group: "Social Media",
|
|
41685
|
+
iconBg: "#f1f5f9",
|
|
41686
|
+
iconColor: "#000000",
|
|
41687
|
+
connectLabel: "Connect X",
|
|
41688
|
+
connectDesc: "Sign in with X to post on your behalf.",
|
|
41689
|
+
setupSteps: [
|
|
41690
|
+
{
|
|
41691
|
+
title: "Click \"Connect X\" on Marketing",
|
|
41692
|
+
desc: "You'll be redirected to X to sign in."
|
|
41693
|
+
},
|
|
41694
|
+
{
|
|
41695
|
+
title: "Approve posting access",
|
|
41696
|
+
desc: "Grant permission to post on your behalf."
|
|
41697
|
+
},
|
|
41698
|
+
{
|
|
41699
|
+
title: "Write your first post",
|
|
41700
|
+
desc: "Compose a post on Marketing and pick X."
|
|
41701
|
+
}
|
|
41702
|
+
],
|
|
41703
|
+
howItWorks: [
|
|
41704
|
+
{
|
|
41705
|
+
title: "Post listing updates",
|
|
41706
|
+
desc: "Posts and links publish from the Marketing calendar."
|
|
41707
|
+
},
|
|
41708
|
+
{
|
|
41709
|
+
title: "Scheduled posts",
|
|
41710
|
+
desc: "Scheduled posts publish automatically."
|
|
41711
|
+
},
|
|
41712
|
+
{
|
|
41713
|
+
title: "Disconnect any time",
|
|
41714
|
+
desc: "Disconnecting revokes access."
|
|
41715
|
+
}
|
|
41716
|
+
]
|
|
41717
|
+
},
|
|
41718
|
+
{
|
|
41719
|
+
key: "nextdoor",
|
|
41720
|
+
name: "Nextdoor",
|
|
41721
|
+
description: "Share posts to your Nextdoor neighborhoods.",
|
|
41722
|
+
apps: ["marketing"],
|
|
41723
|
+
status: "live",
|
|
41724
|
+
group: "Social Media",
|
|
41725
|
+
iconBg: "#f0fdf4",
|
|
41726
|
+
iconColor: "#8ed500",
|
|
41727
|
+
connectLabel: "Connect Nextdoor",
|
|
41728
|
+
connectDesc: "Sign in with Nextdoor and choose the neighborhoods to post to.",
|
|
41729
|
+
setupSteps: [
|
|
41730
|
+
{
|
|
41731
|
+
title: "Click \"Connect Nextdoor\" on Marketing",
|
|
41732
|
+
desc: "You'll be redirected to Nextdoor to sign in."
|
|
41733
|
+
},
|
|
41734
|
+
{
|
|
41735
|
+
title: "Approve posting access",
|
|
41736
|
+
desc: "Grant permission to post on your behalf."
|
|
41737
|
+
},
|
|
41738
|
+
{
|
|
41739
|
+
title: "Choose neighborhoods",
|
|
41740
|
+
desc: "Pick the neighborhoods your posts should reach."
|
|
41741
|
+
}
|
|
41742
|
+
],
|
|
41743
|
+
howItWorks: [
|
|
41744
|
+
{
|
|
41745
|
+
title: "Neighborhood posts",
|
|
41746
|
+
desc: "Posts publish to the neighborhoods you chose."
|
|
41747
|
+
},
|
|
41748
|
+
{
|
|
41749
|
+
title: "Scheduled posts",
|
|
41750
|
+
desc: "Scheduled posts publish automatically."
|
|
41751
|
+
},
|
|
41752
|
+
{
|
|
41753
|
+
title: "Disconnect any time",
|
|
41754
|
+
desc: "Disconnecting revokes access."
|
|
41755
|
+
}
|
|
41756
|
+
]
|
|
41757
|
+
},
|
|
41758
|
+
{
|
|
41759
|
+
key: "postgrid",
|
|
41760
|
+
name: "Direct Mail (PostGrid)",
|
|
41761
|
+
description: "Mail postcards, letters and EDDM to your sphere and farm areas.",
|
|
41762
|
+
apps: ["real-estate"],
|
|
41763
|
+
status: "live",
|
|
41764
|
+
group: "Direct Mail",
|
|
41765
|
+
iconBg: "#fff7ed",
|
|
41766
|
+
iconColor: "#ea580c",
|
|
41767
|
+
connectLabel: "Open Direct Mail",
|
|
41768
|
+
connectDesc: "Included with your plan: there is nothing to connect. Printing and mailing run through our PostGrid account.",
|
|
41769
|
+
setupSteps: [
|
|
41770
|
+
{
|
|
41771
|
+
title: "Open Direct Mail",
|
|
41772
|
+
desc: "Go to Marketing → Direct Mail."
|
|
41773
|
+
},
|
|
41774
|
+
{
|
|
41775
|
+
title: "Pick a template and an audience",
|
|
41776
|
+
desc: "Choose a postcard or letter template and the contacts or farm area to mail."
|
|
41777
|
+
},
|
|
41778
|
+
{
|
|
41779
|
+
title: "Add your return address and send",
|
|
41780
|
+
desc: "Confirm your return address, then send or schedule the campaign."
|
|
41781
|
+
}
|
|
41782
|
+
],
|
|
41783
|
+
howItWorks: [
|
|
41784
|
+
{
|
|
41785
|
+
title: "Printed and mailed for you",
|
|
41786
|
+
desc: "Pieces are printed and mailed by PostGrid; there is no account of your own to set up."
|
|
41787
|
+
},
|
|
41788
|
+
{
|
|
41789
|
+
title: "Delivery tracking",
|
|
41790
|
+
desc: "Delivery status updates in the CRM as pieces move through the mail."
|
|
41791
|
+
},
|
|
41792
|
+
{
|
|
41793
|
+
title: "Billed per piece",
|
|
41794
|
+
desc: "Costs follow the number of pieces mailed."
|
|
41795
|
+
}
|
|
41796
|
+
]
|
|
41797
|
+
},
|
|
41463
41798
|
{
|
|
41464
41799
|
key: "microsoft-teams",
|
|
41465
41800
|
name: "Microsoft Teams",
|
|
@@ -41646,17 +41981,22 @@ const IDX_BEHAVIOR_SIGNALS = [
|
|
|
41646
41981
|
const INTEGRATION_GROUP_ORDER = [
|
|
41647
41982
|
"IDX Website",
|
|
41648
41983
|
"Email",
|
|
41984
|
+
"Direct Mail",
|
|
41649
41985
|
"Text Messaging",
|
|
41650
41986
|
"Calendar",
|
|
41651
41987
|
"Real Estate Data",
|
|
41652
41988
|
"Financials",
|
|
41653
41989
|
"Marketing",
|
|
41990
|
+
"Social Media",
|
|
41654
41991
|
"Documents & Signing",
|
|
41655
41992
|
"Automation & Tools"
|
|
41656
41993
|
];
|
|
41657
41994
|
function integrationsForApp(app) {
|
|
41658
41995
|
return INTEGRATION_CATALOG.filter((i) => i.apps === "all" || i.apps.includes(app));
|
|
41659
41996
|
}
|
|
41997
|
+
function integrationsOnlyFor(app) {
|
|
41998
|
+
return INTEGRATION_CATALOG.filter((i) => i.apps !== "all" && i.apps.includes(app));
|
|
41999
|
+
}
|
|
41660
42000
|
function integrationPlainText(html) {
|
|
41661
42001
|
const strong = [];
|
|
41662
42002
|
return {
|
|
@@ -41667,6 +42007,3893 @@ function integrationPlainText(html) {
|
|
|
41667
42007
|
strong
|
|
41668
42008
|
};
|
|
41669
42009
|
}
|
|
41670
|
-
|
|
42010
|
+
const str = (v) => typeof v === "string" ? v : "";
|
|
42011
|
+
const SETUP_INTEGRATIONS = {
|
|
42012
|
+
quickbooks: {
|
|
42013
|
+
key: "quickbooks",
|
|
42014
|
+
title: "QuickBooks Online",
|
|
42015
|
+
icon: "bank",
|
|
42016
|
+
description: "Sync commissions and expenses to your QuickBooks books.",
|
|
42017
|
+
statusEndpoint: "/quickbooks/status",
|
|
42018
|
+
connectedDetail: (s) => str(s["companyName"]) ? `Connected — ${str(s["companyName"])}` : "Connected",
|
|
42019
|
+
connect: {
|
|
42020
|
+
type: "oauth",
|
|
42021
|
+
authorizeEndpoint: "/quickbooks/authorize"
|
|
42022
|
+
}
|
|
42023
|
+
},
|
|
42024
|
+
slack: {
|
|
42025
|
+
key: "slack",
|
|
42026
|
+
title: "Slack",
|
|
42027
|
+
icon: "message",
|
|
42028
|
+
description: "Get new-lead and deal alerts in a Slack channel.",
|
|
42029
|
+
statusEndpoint: "/slack/status",
|
|
42030
|
+
connectedDetail: (s) => str(s["teamName"]) ? `Connected — ${str(s["teamName"])}${str(s["channelName"]) ? " #" + str(s["channelName"]) : ""}` : "Connected",
|
|
42031
|
+
connect: {
|
|
42032
|
+
type: "oauth",
|
|
42033
|
+
authorizeEndpoint: "/slack/authorize"
|
|
42034
|
+
}
|
|
42035
|
+
},
|
|
42036
|
+
twilio: {
|
|
42037
|
+
key: "twilio",
|
|
42038
|
+
title: "Twilio (calls & texts)",
|
|
42039
|
+
icon: "call",
|
|
42040
|
+
description: "Get a business phone number for texting and calling clients.",
|
|
42041
|
+
statusEndpoint: "/integrations/twilio/status",
|
|
42042
|
+
connectedDetail: (s) => str(s["phoneNumber"]) ? `Connected — ${str(s["phoneNumber"])}` : "Connected",
|
|
42043
|
+
connect: {
|
|
42044
|
+
type: "area-code",
|
|
42045
|
+
connectEndpoint: "/integrations/twilio/connect"
|
|
42046
|
+
}
|
|
42047
|
+
},
|
|
42048
|
+
gmail: {
|
|
42049
|
+
key: "gmail",
|
|
42050
|
+
title: "Gmail / Google Workspace",
|
|
42051
|
+
icon: "email",
|
|
42052
|
+
description: "Connect your Google account for two-way email sync.",
|
|
42053
|
+
statusEndpoint: "/email-sync/status",
|
|
42054
|
+
statusKey: "gmail",
|
|
42055
|
+
connectedDetail: (s) => `Connected — ${str(s["connectedEmail"])}`,
|
|
42056
|
+
connect: {
|
|
42057
|
+
type: "oauth",
|
|
42058
|
+
authorizeEndpoint: "/email-sync/authorize/gmail"
|
|
42059
|
+
}
|
|
42060
|
+
},
|
|
42061
|
+
outlook: {
|
|
42062
|
+
key: "outlook",
|
|
42063
|
+
title: "Outlook / Microsoft 365",
|
|
42064
|
+
icon: "email",
|
|
42065
|
+
description: "Connect your Microsoft account for two-way email sync.",
|
|
42066
|
+
statusEndpoint: "/email-sync/status",
|
|
42067
|
+
statusKey: "outlook",
|
|
42068
|
+
connectedDetail: (s) => `Connected — ${str(s["connectedEmail"])}`,
|
|
42069
|
+
connect: {
|
|
42070
|
+
type: "oauth",
|
|
42071
|
+
authorizeEndpoint: "/email-sync/authorize/outlook"
|
|
42072
|
+
}
|
|
42073
|
+
},
|
|
42074
|
+
"google-calendar": {
|
|
42075
|
+
key: "google-calendar",
|
|
42076
|
+
title: "Google Calendar",
|
|
42077
|
+
icon: "link",
|
|
42078
|
+
description: "Two-way sync with your Google Calendar.",
|
|
42079
|
+
statusEndpoint: "/calendar-sync/status",
|
|
42080
|
+
statusKey: "google",
|
|
42081
|
+
connectedDetail: (s) => `Connected — ${str(s["calendarEmail"])}`,
|
|
42082
|
+
connect: {
|
|
42083
|
+
type: "oauth",
|
|
42084
|
+
authorizeEndpoint: "/calendar-sync/authorize/google"
|
|
42085
|
+
}
|
|
42086
|
+
},
|
|
42087
|
+
"outlook-calendar": {
|
|
42088
|
+
key: "outlook-calendar",
|
|
42089
|
+
title: "Outlook Calendar",
|
|
42090
|
+
icon: "link",
|
|
42091
|
+
description: "Two-way sync with your Outlook / Microsoft 365 calendar.",
|
|
42092
|
+
statusEndpoint: "/calendar-sync/status",
|
|
42093
|
+
statusKey: "outlook",
|
|
42094
|
+
connectedDetail: (s) => `Connected — ${str(s["calendarEmail"])}`,
|
|
42095
|
+
connect: {
|
|
42096
|
+
type: "oauth",
|
|
42097
|
+
authorizeEndpoint: "/calendar-sync/authorize/outlook"
|
|
42098
|
+
}
|
|
42099
|
+
}
|
|
42100
|
+
};
|
|
42101
|
+
function setupIntegrations(...keys) {
|
|
42102
|
+
return keys.map((k) => {
|
|
42103
|
+
const found = SETUP_INTEGRATIONS[k];
|
|
42104
|
+
if (!found) throw new Error(`Unknown setup integration "${k}".`);
|
|
42105
|
+
return found;
|
|
42106
|
+
});
|
|
42107
|
+
}
|
|
42108
|
+
var SetupWizardStepDirective = class SetupWizardStepDirective {
|
|
42109
|
+
constructor() {
|
|
42110
|
+
this.key = input.required({
|
|
42111
|
+
...ngDevMode ? { debugName: "key" } : /* istanbul ignore next */ {},
|
|
42112
|
+
alias: "uiSetupStep"
|
|
42113
|
+
});
|
|
42114
|
+
this.template = inject(TemplateRef);
|
|
42115
|
+
}
|
|
42116
|
+
static {
|
|
42117
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
42118
|
+
minVersion: "12.0.0",
|
|
42119
|
+
version: "22.2.2",
|
|
42120
|
+
ngImport: i0,
|
|
42121
|
+
type: SetupWizardStepDirective,
|
|
42122
|
+
deps: [],
|
|
42123
|
+
target: i0.ɵɵFactoryTarget.Directive
|
|
42124
|
+
});
|
|
42125
|
+
}
|
|
42126
|
+
static {
|
|
42127
|
+
this.ɵdir = i0.ɵɵngDeclareDirective({
|
|
42128
|
+
minVersion: "17.1.0",
|
|
42129
|
+
version: "22.2.2",
|
|
42130
|
+
type: SetupWizardStepDirective,
|
|
42131
|
+
isStandalone: true,
|
|
42132
|
+
selector: "ng-template[uiSetupStep]",
|
|
42133
|
+
inputs: { key: {
|
|
42134
|
+
classPropertyName: "key",
|
|
42135
|
+
publicName: "uiSetupStep",
|
|
42136
|
+
isSignal: true,
|
|
42137
|
+
isRequired: true,
|
|
42138
|
+
transformFunction: null
|
|
42139
|
+
} },
|
|
42140
|
+
ngImport: i0
|
|
42141
|
+
});
|
|
42142
|
+
}
|
|
42143
|
+
};
|
|
42144
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
42145
|
+
minVersion: "12.0.0",
|
|
42146
|
+
version: "22.2.2",
|
|
42147
|
+
ngImport: i0,
|
|
42148
|
+
type: SetupWizardStepDirective,
|
|
42149
|
+
decorators: [{
|
|
42150
|
+
type: Directive,
|
|
42151
|
+
args: [{
|
|
42152
|
+
selector: "ng-template[uiSetupStep]",
|
|
42153
|
+
standalone: true
|
|
42154
|
+
}]
|
|
42155
|
+
}],
|
|
42156
|
+
propDecorators: { key: [{
|
|
42157
|
+
type: i0.Input,
|
|
42158
|
+
args: [{
|
|
42159
|
+
isSignal: true,
|
|
42160
|
+
alias: "uiSetupStep",
|
|
42161
|
+
required: true
|
|
42162
|
+
}]
|
|
42163
|
+
}] }
|
|
42164
|
+
});
|
|
42165
|
+
const DEFAULT_TEXTS = {
|
|
42166
|
+
brand: "Setup Wizard",
|
|
42167
|
+
stepperLabel: "Setup steps",
|
|
42168
|
+
progress: (done, total) => `${done} of ${total} complete`,
|
|
42169
|
+
back: "Back",
|
|
42170
|
+
skip: "Skip for now",
|
|
42171
|
+
saveAndContinue: "Save & Continue",
|
|
42172
|
+
continue: "Continue",
|
|
42173
|
+
exit: "Exit Setup",
|
|
42174
|
+
connect: "Connect",
|
|
42175
|
+
reconnect: "Reconnect",
|
|
42176
|
+
connected: "Connected",
|
|
42177
|
+
importMore: "Import more",
|
|
42178
|
+
whatGetsImported: "WHAT GETS IMPORTED:",
|
|
42179
|
+
doneTitle: "You're All Set!",
|
|
42180
|
+
doneDescription: "Your account is configured and ready to go. Here's a quick summary:",
|
|
42181
|
+
doneHint: "You can always update these settings later from the Settings menu.",
|
|
42182
|
+
finish: "Go to Dashboard",
|
|
42183
|
+
summaryDone: "Done",
|
|
42184
|
+
summarySkipped: "Skipped",
|
|
42185
|
+
summaryPending: "Pending",
|
|
42186
|
+
areaCode: "Area code",
|
|
42187
|
+
connectIntegrationFailed: (title) => `Failed to connect ${title}. Please try again from the Integrations page after setup.`
|
|
42188
|
+
};
|
|
42189
|
+
const SETUP_DONE_KEY = "done";
|
|
42190
|
+
var SetupWizardComponent = class SetupWizardComponent {
|
|
42191
|
+
constructor() {
|
|
42192
|
+
this.http = inject(HttpClient);
|
|
42193
|
+
this.apiBase = inject(FORGEROUTE_CONFIG, { optional: true })?.baseUrl ?? "/api";
|
|
42194
|
+
this.destroyRef = inject(DestroyRef);
|
|
42195
|
+
this.steps = input.required(...ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []);
|
|
42196
|
+
this.texts = input({}, ...ngDevMode ? [{ debugName: "texts" }] : /* istanbul ignore next */ []);
|
|
42197
|
+
this.finished = output();
|
|
42198
|
+
this.exited = output();
|
|
42199
|
+
this.stepChange = output();
|
|
42200
|
+
this.stepTemplates = contentChildren(SetupWizardStepDirective, ...ngDevMode ? [{ debugName: "stepTemplates" }] : /* istanbul ignore next */ []);
|
|
42201
|
+
this.importers = viewChildren("importer", ...ngDevMode ? [{ debugName: "importers" }] : /* istanbul ignore next */ []);
|
|
42202
|
+
this.t = computed(() => ({
|
|
42203
|
+
...DEFAULT_TEXTS,
|
|
42204
|
+
...this.texts()
|
|
42205
|
+
}), ...ngDevMode ? [{ debugName: "t" }] : /* istanbul ignore next */ []);
|
|
42206
|
+
this.currentKey = signal("", ...ngDevMode ? [{ debugName: "currentKey" }] : /* istanbul ignore next */ []);
|
|
42207
|
+
this.completed = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "completed" }] : /* istanbul ignore next */ []);
|
|
42208
|
+
this.skipped = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "skipped" }] : /* istanbul ignore next */ []);
|
|
42209
|
+
this.imports = signal({}, ...ngDevMode ? [{ debugName: "imports" }] : /* istanbul ignore next */ []);
|
|
42210
|
+
this.statuses = signal({}, ...ngDevMode ? [{ debugName: "statuses" }] : /* istanbul ignore next */ []);
|
|
42211
|
+
this.busy = signal(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
|
|
42212
|
+
this.error = signal("", ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
|
|
42213
|
+
this.areaCodeOpen = signal("", ...ngDevMode ? [{ debugName: "areaCodeOpen" }] : /* istanbul ignore next */ []);
|
|
42214
|
+
this.areaCode = signal("", ...ngDevMode ? [{ debugName: "areaCode" }] : /* istanbul ignore next */ []);
|
|
42215
|
+
this.validAreaCode = computed(() => /^\d{3}$/.test(this.areaCode().trim()), ...ngDevMode ? [{ debugName: "validAreaCode" }] : /* istanbul ignore next */ []);
|
|
42216
|
+
this.allSteps = computed(() => [...this.steps(), {
|
|
42217
|
+
key: SETUP_DONE_KEY,
|
|
42218
|
+
label: this.t().doneTitle,
|
|
42219
|
+
kind: "custom",
|
|
42220
|
+
skippable: false
|
|
42221
|
+
}], ...ngDevMode ? [{ debugName: "allSteps" }] : /* istanbul ignore next */ []);
|
|
42222
|
+
this.index = computed(() => Math.max(0, this.allSteps().findIndex((s) => s.key === this.currentKey())), ...ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []);
|
|
42223
|
+
this.current = computed(() => this.allSteps()[this.index()], ...ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []);
|
|
42224
|
+
this.isDone = computed(() => this.current().key === SETUP_DONE_KEY, ...ngDevMode ? [{ debugName: "isDone" }] : /* istanbul ignore next */ []);
|
|
42225
|
+
this.completedCount = computed(() => this.allSteps().filter((s) => this.completed().has(s.key) || this.skipped().has(s.key)).length, ...ngDevMode ? [{ debugName: "completedCount" }] : /* istanbul ignore next */ []);
|
|
42226
|
+
this.stepperSteps = computed(() => this.allSteps().map((s) => ({
|
|
42227
|
+
key: s.key,
|
|
42228
|
+
label: s.label,
|
|
42229
|
+
complete: this.completed().has(s.key),
|
|
42230
|
+
disabled: this.busy()
|
|
42231
|
+
})), ...ngDevMode ? [{ debugName: "stepperSteps" }] : /* istanbul ignore next */ []);
|
|
42232
|
+
this.summarySections = computed(() => [{ items: this.steps().map((s) => ({
|
|
42233
|
+
label: s.label,
|
|
42234
|
+
badge: this.completed().has(s.key) ? {
|
|
42235
|
+
label: this.t().summaryDone,
|
|
42236
|
+
variant: "green"
|
|
42237
|
+
} : this.skipped().has(s.key) ? {
|
|
42238
|
+
label: this.t().summarySkipped,
|
|
42239
|
+
variant: "gray"
|
|
42240
|
+
} : {
|
|
42241
|
+
label: this.t().summaryPending,
|
|
42242
|
+
variant: "slate"
|
|
42243
|
+
}
|
|
42244
|
+
})) }], ...ngDevMode ? [{ debugName: "summarySections" }] : /* istanbul ignore next */ []);
|
|
42245
|
+
effect(() => {
|
|
42246
|
+
const steps = this.allSteps();
|
|
42247
|
+
untracked(() => {
|
|
42248
|
+
if (!steps.some((s) => s.key === this.currentKey())) this.enter(steps[0].key);
|
|
42249
|
+
});
|
|
42250
|
+
});
|
|
42251
|
+
}
|
|
42252
|
+
templateFor(key) {
|
|
42253
|
+
return this.stepTemplates().find((d) => d.key() === key)?.template ?? null;
|
|
42254
|
+
}
|
|
42255
|
+
goTo(key) {
|
|
42256
|
+
if (!this.busy() && this.allSteps().some((s) => s.key === key)) this.enter(key);
|
|
42257
|
+
}
|
|
42258
|
+
enter(key) {
|
|
42259
|
+
this.error.set("");
|
|
42260
|
+
this.areaCodeOpen.set("");
|
|
42261
|
+
this.areaCode.set("");
|
|
42262
|
+
this.currentKey.set(key);
|
|
42263
|
+
const step = this.allSteps().find((s) => s.key === key);
|
|
42264
|
+
if (step.kind === "integrations") this.loadStatuses(step.integrations ?? []);
|
|
42265
|
+
step.onEnter?.();
|
|
42266
|
+
this.stepChange.emit(key);
|
|
42267
|
+
}
|
|
42268
|
+
advanceFrom(key) {
|
|
42269
|
+
const steps = this.allSteps();
|
|
42270
|
+
const next = steps[steps.findIndex((s) => s.key === key) + 1];
|
|
42271
|
+
if (next) this.enter(next.key);
|
|
42272
|
+
}
|
|
42273
|
+
back() {
|
|
42274
|
+
if (this.busy()) return;
|
|
42275
|
+
const prev = this.allSteps()[this.index() - 1];
|
|
42276
|
+
if (prev) this.enter(prev.key);
|
|
42277
|
+
}
|
|
42278
|
+
skip() {
|
|
42279
|
+
if (this.busy()) return;
|
|
42280
|
+
const key = this.current().key;
|
|
42281
|
+
this.skipped.update((s) => new Set(s).add(key));
|
|
42282
|
+
this.advanceFrom(key);
|
|
42283
|
+
}
|
|
42284
|
+
markCompleted(key) {
|
|
42285
|
+
this.completed.update((s) => new Set(s).add(key));
|
|
42286
|
+
this.skipped.update((s) => {
|
|
42287
|
+
const n = new Set(s);
|
|
42288
|
+
n.delete(key);
|
|
42289
|
+
return n;
|
|
42290
|
+
});
|
|
42291
|
+
}
|
|
42292
|
+
async saveAndContinue() {
|
|
42293
|
+
if (this.busy()) return;
|
|
42294
|
+
const step = this.current();
|
|
42295
|
+
this.busy.set(true);
|
|
42296
|
+
this.error.set("");
|
|
42297
|
+
try {
|
|
42298
|
+
await this.run(step.kind === "custom" ? step.save?.() : step.onContinue?.());
|
|
42299
|
+
this.markCompleted(step.key);
|
|
42300
|
+
this.advanceFrom(step.key);
|
|
42301
|
+
} catch (e) {
|
|
42302
|
+
this.error.set(this.messageOf(e, "Failed to save. Please try again."));
|
|
42303
|
+
} finally {
|
|
42304
|
+
this.busy.set(false);
|
|
42305
|
+
}
|
|
42306
|
+
}
|
|
42307
|
+
async run(result) {
|
|
42308
|
+
if (isObservable(result)) await firstValueFrom(result, { defaultValue: void 0 });
|
|
42309
|
+
else await result;
|
|
42310
|
+
}
|
|
42311
|
+
messageOf(e, fallback) {
|
|
42312
|
+
const text = e instanceof HttpErrorResponse ? e.error?.message : e?.error?.message ?? e?.message;
|
|
42313
|
+
return typeof text === "string" && text ? text : fallback;
|
|
42314
|
+
}
|
|
42315
|
+
openImport(key) {
|
|
42316
|
+
if (this.steps().findIndex((s) => s.key === key) < 0) return;
|
|
42317
|
+
this.importers()[0]?.openImport();
|
|
42318
|
+
}
|
|
42319
|
+
onImported(key) {
|
|
42320
|
+
const imported = this.importers()[0]?.importResult?.imported ?? 0;
|
|
42321
|
+
this.imports.update((m) => ({
|
|
42322
|
+
...m,
|
|
42323
|
+
[key]: imported
|
|
42324
|
+
}));
|
|
42325
|
+
this.markCompleted(key);
|
|
42326
|
+
}
|
|
42327
|
+
importResult(key) {
|
|
42328
|
+
const n = this.imports()[key];
|
|
42329
|
+
return n === void 0 ? null : n;
|
|
42330
|
+
}
|
|
42331
|
+
clearImport(key) {
|
|
42332
|
+
this.imports.update((m) => {
|
|
42333
|
+
const n = { ...m };
|
|
42334
|
+
delete n[key];
|
|
42335
|
+
return n;
|
|
42336
|
+
});
|
|
42337
|
+
}
|
|
42338
|
+
importTitle(key) {
|
|
42339
|
+
const imp = this.steps().find((s) => s.key === key)?.import;
|
|
42340
|
+
const n = this.imports()[key] ?? 0;
|
|
42341
|
+
return imp?.resultTitle ? imp.resultTitle(n) : `${n} ${(imp?.config.entityNamePlural ?? "records").toLowerCase()} imported successfully.`;
|
|
42342
|
+
}
|
|
42343
|
+
continueFromImport() {
|
|
42344
|
+
this.advanceFrom(this.current().key);
|
|
42345
|
+
}
|
|
42346
|
+
loadStatuses(list) {
|
|
42347
|
+
for (const url of new Set(list.map((i) => i.statusEndpoint))) this.http.get(this.apiBase + url).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
42348
|
+
next: (body) => this.setStatuses(list.filter((i) => i.statusEndpoint === url), body ?? {}),
|
|
42349
|
+
error: () => this.setStatuses(list.filter((i) => i.statusEndpoint === url), {})
|
|
42350
|
+
});
|
|
42351
|
+
}
|
|
42352
|
+
setStatuses(list, body) {
|
|
42353
|
+
this.statuses.update((cur) => {
|
|
42354
|
+
const next = { ...cur };
|
|
42355
|
+
for (const i of list) next[i.key] = (i.statusKey ? body[i.statusKey] : body) ?? { connected: false };
|
|
42356
|
+
return next;
|
|
42357
|
+
});
|
|
42358
|
+
}
|
|
42359
|
+
isConnected(i) {
|
|
42360
|
+
return !!this.statuses()[i.key]?.connected;
|
|
42361
|
+
}
|
|
42362
|
+
detailFor(i) {
|
|
42363
|
+
const st = this.statuses()[i.key];
|
|
42364
|
+
return st?.connected ? i.connectedDetail ? i.connectedDetail(st) : this.t().connected : i.description;
|
|
42365
|
+
}
|
|
42366
|
+
connect(i) {
|
|
42367
|
+
if (this.busy()) return;
|
|
42368
|
+
this.error.set("");
|
|
42369
|
+
this.areaCodeOpen.set("");
|
|
42370
|
+
this.areaCode.set("");
|
|
42371
|
+
if (i.connect.type === "area-code") {
|
|
42372
|
+
this.areaCodeOpen.set(i.key);
|
|
42373
|
+
return;
|
|
42374
|
+
}
|
|
42375
|
+
this.http.get(this.apiBase + i.connect.authorizeEndpoint).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
42376
|
+
next: (res) => {
|
|
42377
|
+
if (res?.url) this.redirect(res.url);
|
|
42378
|
+
},
|
|
42379
|
+
error: () => this.error.set(this.t().connectIntegrationFailed(i.title))
|
|
42380
|
+
});
|
|
42381
|
+
}
|
|
42382
|
+
areaCodeLabelFor(i) {
|
|
42383
|
+
return (i.connect.type === "area-code" ? i.connect.areaCodeLabel : void 0) ?? this.t().areaCode;
|
|
42384
|
+
}
|
|
42385
|
+
redirect(url) {
|
|
42386
|
+
window.location.href = url;
|
|
42387
|
+
}
|
|
42388
|
+
submitAreaCode(i) {
|
|
42389
|
+
if (i.connect.type !== "area-code" || this.busy() || !this.validAreaCode()) return;
|
|
42390
|
+
const connectEndpoint = i.connect.connectEndpoint;
|
|
42391
|
+
this.busy.set(true);
|
|
42392
|
+
this.error.set("");
|
|
42393
|
+
this.http.post(this.apiBase + connectEndpoint, { areaCode: this.areaCode().trim() }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
42394
|
+
next: () => {
|
|
42395
|
+
this.busy.set(false);
|
|
42396
|
+
this.areaCodeOpen.set("");
|
|
42397
|
+
this.areaCode.set("");
|
|
42398
|
+
this.loadStatuses([i]);
|
|
42399
|
+
},
|
|
42400
|
+
error: (e) => {
|
|
42401
|
+
this.busy.set(false);
|
|
42402
|
+
this.error.set(this.messageOf(e, this.t().connectIntegrationFailed(i.title)));
|
|
42403
|
+
}
|
|
42404
|
+
});
|
|
42405
|
+
}
|
|
42406
|
+
static {
|
|
42407
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
42408
|
+
minVersion: "12.0.0",
|
|
42409
|
+
version: "22.2.2",
|
|
42410
|
+
ngImport: i0,
|
|
42411
|
+
type: SetupWizardComponent,
|
|
42412
|
+
deps: [],
|
|
42413
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
42414
|
+
});
|
|
42415
|
+
}
|
|
42416
|
+
static {
|
|
42417
|
+
this.ɵcmp = i0.ɵɵngDeclareComponent({
|
|
42418
|
+
minVersion: "17.0.0",
|
|
42419
|
+
version: "22.2.2",
|
|
42420
|
+
type: SetupWizardComponent,
|
|
42421
|
+
isStandalone: true,
|
|
42422
|
+
selector: "ui-setup-wizard",
|
|
42423
|
+
inputs: {
|
|
42424
|
+
steps: {
|
|
42425
|
+
classPropertyName: "steps",
|
|
42426
|
+
publicName: "steps",
|
|
42427
|
+
isSignal: true,
|
|
42428
|
+
isRequired: true,
|
|
42429
|
+
transformFunction: null
|
|
42430
|
+
},
|
|
42431
|
+
texts: {
|
|
42432
|
+
classPropertyName: "texts",
|
|
42433
|
+
publicName: "texts",
|
|
42434
|
+
isSignal: true,
|
|
42435
|
+
isRequired: false,
|
|
42436
|
+
transformFunction: null
|
|
42437
|
+
}
|
|
42438
|
+
},
|
|
42439
|
+
outputs: {
|
|
42440
|
+
finished: "finished",
|
|
42441
|
+
exited: "exited",
|
|
42442
|
+
stepChange: "stepChange"
|
|
42443
|
+
},
|
|
42444
|
+
queries: [{
|
|
42445
|
+
propertyName: "stepTemplates",
|
|
42446
|
+
predicate: SetupWizardStepDirective,
|
|
42447
|
+
isSignal: true
|
|
42448
|
+
}],
|
|
42449
|
+
viewQueries: [{
|
|
42450
|
+
propertyName: "importers",
|
|
42451
|
+
predicate: ["importer"],
|
|
42452
|
+
descendants: true,
|
|
42453
|
+
isSignal: true
|
|
42454
|
+
}],
|
|
42455
|
+
ngImport: i0,
|
|
42456
|
+
template: `
|
|
42457
|
+
<div class="sw-page" data-testid="setup-wizard-page">
|
|
42458
|
+
<aside class="sw-sidebar">
|
|
42459
|
+
<div class="sw-sidebar-top">
|
|
42460
|
+
<div class="sw-brand">
|
|
42461
|
+
<ng-content select="[setup-wizard-logo]" />
|
|
42462
|
+
<span class="sw-brand-text" data-testid="setup-wizard-brand">{{ t().brand }}</span>
|
|
42463
|
+
</div>
|
|
42464
|
+
<ui-stepper class="sw-step-nav" [ariaLabel]="t().stepperLabel" orientation="vertical" size="sm" compact="md"
|
|
42465
|
+
[steps]="stepperSteps()" [current]="currentKey()" [clickable]="true" [linear]="false" (stepClick)="goTo($event)" />
|
|
42466
|
+
</div>
|
|
42467
|
+
<div class="sw-sidebar-bottom">
|
|
42468
|
+
<ui-progress-bar [value]="completedCount()" [max]="allSteps().length" ariaLabel="Setup progress" />
|
|
42469
|
+
<span class="sw-progress-text" data-testid="wizard-progress-text">{{ t().progress(completedCount(), allSteps().length) }}</span>
|
|
42470
|
+
</div>
|
|
42471
|
+
</aside>
|
|
42472
|
+
|
|
42473
|
+
<main class="sw-content">
|
|
42474
|
+
<div class="sw-content-header">
|
|
42475
|
+
<h1 class="sw-step-title" data-testid="wizard-step-title">{{ current().label }}</h1>
|
|
42476
|
+
<ui-button variant="ghost" size="sm" data-testid="wizard-exit" (click)="exited.emit()">{{ t().exit }}</ui-button>
|
|
42477
|
+
</div>
|
|
42478
|
+
|
|
42479
|
+
@if (isDone()) {
|
|
42480
|
+
<div class="sw-step-content sw-done" data-testid="wizard-done">
|
|
42481
|
+
<p class="sw-step-desc">{{ t().doneDescription }}</p>
|
|
42482
|
+
<ui-panel-info-list class="sw-summary" data-testid="wizard-summary" [sections]="summarySections()" />
|
|
42483
|
+
<p class="sw-done-hint">{{ t().doneHint }}</p>
|
|
42484
|
+
<ui-button variant="primary" size="lg" data-testid="wizard-finish" (click)="finished.emit()">{{ t().finish }}</ui-button>
|
|
42485
|
+
</div>
|
|
42486
|
+
} @else {
|
|
42487
|
+
<div class="sw-step-content" [attr.data-step]="current().key">
|
|
42488
|
+
@if (current().description) { <p class="sw-step-desc">{{ current().description }}</p> }
|
|
42489
|
+
|
|
42490
|
+
@switch (current().kind) {
|
|
42491
|
+
@case ('custom') {
|
|
42492
|
+
@if (templateFor(current().key); as tpl) { <ng-container [ngTemplateOutlet]="tpl" /> }
|
|
42493
|
+
}
|
|
42494
|
+
@case ('import') {
|
|
42495
|
+
@if (current().import; as imp) {
|
|
42496
|
+
@if (imp.tags?.length) {
|
|
42497
|
+
<div class="sw-import-tags" role="group" [attr.aria-label]="t().whatGetsImported">
|
|
42498
|
+
<span class="sw-tag">{{ t().whatGetsImported }}</span>
|
|
42499
|
+
@for (tag of imp.tags; track tag) { <ui-badge [label]="tag" variant="slate" /> }
|
|
42500
|
+
</div>
|
|
42501
|
+
}
|
|
42502
|
+
@if (importResult(current().key); as res) {
|
|
42503
|
+
<ui-alert variant="success" [title]="importTitle(current().key)" [attr.data-testid]="'import-result-' + current().key">
|
|
42504
|
+
{{ imp.resultText }}
|
|
42505
|
+
<ui-button variant="link" size="sm" data-testid="import-more" (click)="clearImport(current().key)">{{ t().importMore }}</ui-button>
|
|
42506
|
+
</ui-alert>
|
|
42507
|
+
} @else {
|
|
42508
|
+
@if (imp.hint) { <p class="sw-hint">{{ imp.hint }}</p> }
|
|
42509
|
+
<ui-button variant="primary" size="lg" [attr.data-testid]="'open-import-' + current().key" (click)="openImport(current().key)">{{ imp.buttonLabel }}</ui-button>
|
|
42510
|
+
}
|
|
42511
|
+
<ui-import-export #importer style="display:none" [config]="imp.config" [importSources]="imp.sources" (imported)="onImported(current().key)" />
|
|
42512
|
+
}
|
|
42513
|
+
}
|
|
42514
|
+
@case ('integrations') {
|
|
42515
|
+
<div class="sw-provider-grid" role="group" [attr.aria-label]="current().label">
|
|
42516
|
+
@for (it of current().integrations ?? []; track it.key) {
|
|
42517
|
+
<ui-list-item [attr.data-testid]="'integration-' + it.key" [interactive]="false" [bordered]="true"
|
|
42518
|
+
[icon]="it.icon ?? 'link'" iconColor="blue" [title]="it.title" [details]="[detailFor(it)]"
|
|
42519
|
+
[badge]="t().connected" badgeVariant="green" [alwaysShowBadge]="isConnected(it)">
|
|
42520
|
+
<ui-button list-item-action variant="secondary" size="sm" [ariaLabel]="(isConnected(it) ? t().reconnect : t().connect) + ' ' + it.title" (click)="connect(it)">
|
|
42521
|
+
{{ isConnected(it) ? t().reconnect : t().connect }}
|
|
42522
|
+
</ui-button>
|
|
42523
|
+
</ui-list-item>
|
|
42524
|
+
@if (areaCodeOpen() === it.key) {
|
|
42525
|
+
<div class="sw-area-code" [attr.data-testid]="'area-code-' + it.key">
|
|
42526
|
+
<ui-text-input [label]="areaCodeLabelFor(it)" [maxlength]="3" inputmode="numeric" [ngModel]="areaCode()" (ngModelChange)="areaCode.set($event ?? '')" [inputTestId]="'area-code-input-' + it.key" />
|
|
42527
|
+
<ui-button variant="primary" size="sm" [loading]="busy()" [disabled]="!validAreaCode()" (click)="submitAreaCode(it)">{{ t().connect }}</ui-button>
|
|
42528
|
+
</div>
|
|
42529
|
+
}
|
|
42530
|
+
}
|
|
42531
|
+
</div>
|
|
42532
|
+
}
|
|
42533
|
+
}
|
|
42534
|
+
|
|
42535
|
+
@if (error()) { <ui-alert variant="danger" data-testid="wizard-save-error">{{ error() }}</ui-alert> }
|
|
42536
|
+
|
|
42537
|
+
<div class="sw-step-actions">
|
|
42538
|
+
@if (index() > 0) { <ui-button variant="ghost" size="md" data-testid="wizard-back" [disabled]="busy()" (click)="back()">{{ t().back }}</ui-button> } @else { <span></span> }
|
|
42539
|
+
<div class="sw-actions-right">
|
|
42540
|
+
@if (current().skippable !== false) {
|
|
42541
|
+
<ui-button variant="ghost" size="md" data-testid="wizard-skip" [disabled]="busy()" (click)="skip()">{{ t().skip }}</ui-button>
|
|
42542
|
+
}
|
|
42543
|
+
@if (current().kind === 'custom') {
|
|
42544
|
+
<ui-button variant="primary" size="lg" data-testid="wizard-save" [loading]="busy()" [disabled]="current().canContinue ? !current().canContinue!() : false" (click)="saveAndContinue()">
|
|
42545
|
+
{{ current().saveLabel ?? t().saveAndContinue }}
|
|
42546
|
+
</ui-button>
|
|
42547
|
+
} @else if (current().kind === 'integrations') {
|
|
42548
|
+
<ui-button variant="primary" size="lg" data-testid="wizard-save" [loading]="busy()" (click)="saveAndContinue()">{{ t().saveAndContinue }}</ui-button>
|
|
42549
|
+
} @else if (importResult(current().key)) {
|
|
42550
|
+
<ui-button variant="primary" size="lg" data-testid="wizard-continue" (click)="continueFromImport()">{{ t().continue }}</ui-button>
|
|
42551
|
+
}
|
|
42552
|
+
</div>
|
|
42553
|
+
</div>
|
|
42554
|
+
</div>
|
|
42555
|
+
}
|
|
42556
|
+
</main>
|
|
42557
|
+
</div>
|
|
42558
|
+
`,
|
|
42559
|
+
isInline: true,
|
|
42560
|
+
styles: [".sw-page{display:flex;min-height:100vh;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.sw-sidebar{width:260px;min-width:260px;background:#faf9f7;border-right:1px solid #e5e2dc;display:flex;flex-direction:column;justify-content:space-between;padding:28px 20px}.sw-brand{display:flex;align-items:center;gap:10px;margin-bottom:28px}.sw-brand-text{font-size:1.125rem;font-weight:700;color:#1e293b}.sw-step-nav{display:block}.sw-sidebar-bottom{margin-top:24px}.sw-progress-text{display:block;margin-top:8px;font-size:.75rem;color:#94a3b8}.sw-content{flex:1;min-width:0;background:#fff;display:flex;flex-direction:column;overflow-y:auto}.sw-content-header{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:20px 32px;border-bottom:1px solid #f1f5f9}.sw-step-title{font-size:1.25rem;font-weight:700;color:#111827;margin:0}.sw-step-content{max-width:780px;padding:16px 48px 60px}.sw-step-desc{font-size:.9375rem;color:#6b7280;line-height:1.5;margin:0 0 32px}.sw-hint{font-size:.8125rem;color:#94a3b8;margin:0 0 16px}.sw-import-tags{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:28px}.sw-tag{font-size:.6875rem;font-weight:700;letter-spacing:1px;color:#6b7280}.sw-provider-grid{display:flex;flex-direction:column;gap:8px;margin-bottom:24px}.sw-area-code{display:flex;align-items:flex-end;gap:12px;padding:0 0 8px 4px;max-width:320px}.sw-area-code ui-text-input{flex:1}.sw-content ui-alert{display:block;margin-bottom:16px}.sw-step-actions{display:flex;justify-content:space-between;align-items:center;margin-top:16px;padding-top:20px;border-top:1px solid #f1f5f9}.sw-actions-right{display:flex;align-items:center;gap:16px}.sw-done{text-align:center;display:flex;flex-direction:column;align-items:center;max-width:none}.sw-done .sw-step-desc{max-width:480px}.sw-summary{display:block;width:100%;max-width:480px;margin-bottom:24px;text-align:left}.sw-done-hint{font-size:.8125rem;color:#9ca3af;margin-bottom:24px}@media(max-width:768px){.sw-page{flex-direction:column}.sw-sidebar{width:100%;min-width:unset;padding:16px}.sw-brand{margin-bottom:12px}.sw-sidebar-bottom{display:none}.sw-step-content{padding:12px 20px 40px}}\n"],
|
|
42561
|
+
dependencies: [
|
|
42562
|
+
{
|
|
42563
|
+
kind: "directive",
|
|
42564
|
+
type: NgTemplateOutlet,
|
|
42565
|
+
selector: "[ngTemplateOutlet]",
|
|
42566
|
+
inputs: [
|
|
42567
|
+
"ngTemplateOutletContext",
|
|
42568
|
+
"ngTemplateOutlet",
|
|
42569
|
+
"ngTemplateOutletInjector"
|
|
42570
|
+
]
|
|
42571
|
+
},
|
|
42572
|
+
{
|
|
42573
|
+
kind: "ngmodule",
|
|
42574
|
+
type: FormsModule
|
|
42575
|
+
},
|
|
42576
|
+
{
|
|
42577
|
+
kind: "directive",
|
|
42578
|
+
type: i1.NgControlStatus,
|
|
42579
|
+
selector: "[formControlName],[ngModel],[formControl]"
|
|
42580
|
+
},
|
|
42581
|
+
{
|
|
42582
|
+
kind: "directive",
|
|
42583
|
+
type: i1.MaxLengthValidator,
|
|
42584
|
+
selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
|
|
42585
|
+
inputs: ["maxlength"]
|
|
42586
|
+
},
|
|
42587
|
+
{
|
|
42588
|
+
kind: "directive",
|
|
42589
|
+
type: i1.NgModel,
|
|
42590
|
+
selector: "[ngModel]:not([formControlName]):not([formControl])",
|
|
42591
|
+
inputs: [
|
|
42592
|
+
"name",
|
|
42593
|
+
"disabled",
|
|
42594
|
+
"ngModel",
|
|
42595
|
+
"ngModelOptions"
|
|
42596
|
+
],
|
|
42597
|
+
outputs: ["ngModelChange"],
|
|
42598
|
+
exportAs: ["ngModel"]
|
|
42599
|
+
},
|
|
42600
|
+
{
|
|
42601
|
+
kind: "component",
|
|
42602
|
+
type: AlertComponent,
|
|
42603
|
+
selector: "ui-alert",
|
|
42604
|
+
inputs: [
|
|
42605
|
+
"variant",
|
|
42606
|
+
"title",
|
|
42607
|
+
"dismissible",
|
|
42608
|
+
"dismissAriaLabel",
|
|
42609
|
+
"quiet",
|
|
42610
|
+
"compact",
|
|
42611
|
+
"inline"
|
|
42612
|
+
],
|
|
42613
|
+
outputs: ["dismissed"]
|
|
42614
|
+
},
|
|
42615
|
+
{
|
|
42616
|
+
kind: "component",
|
|
42617
|
+
type: BadgeComponent,
|
|
42618
|
+
selector: "ui-badge",
|
|
42619
|
+
inputs: ["label", "variant"]
|
|
42620
|
+
},
|
|
42621
|
+
{
|
|
42622
|
+
kind: "component",
|
|
42623
|
+
type: ButtonComponent,
|
|
42624
|
+
selector: "ui-button",
|
|
42625
|
+
inputs: [
|
|
42626
|
+
"variant",
|
|
42627
|
+
"routerLink",
|
|
42628
|
+
"queryParams",
|
|
42629
|
+
"fragment",
|
|
42630
|
+
"state",
|
|
42631
|
+
"href",
|
|
42632
|
+
"target",
|
|
42633
|
+
"rel",
|
|
42634
|
+
"shortLabel",
|
|
42635
|
+
"shortBelow",
|
|
42636
|
+
"color",
|
|
42637
|
+
"active",
|
|
42638
|
+
"size",
|
|
42639
|
+
"type",
|
|
42640
|
+
"disabled",
|
|
42641
|
+
"loading",
|
|
42642
|
+
"fullWidth",
|
|
42643
|
+
"ariaLabel",
|
|
42644
|
+
"ariaExpanded",
|
|
42645
|
+
"ariaPressed",
|
|
42646
|
+
"ariaDescribedBy",
|
|
42647
|
+
"modal-footer-left",
|
|
42648
|
+
"modal-footer-right"
|
|
42649
|
+
]
|
|
42650
|
+
},
|
|
42651
|
+
{
|
|
42652
|
+
kind: "component",
|
|
42653
|
+
type: ImportExportComponent,
|
|
42654
|
+
selector: "ui-import-export",
|
|
42655
|
+
inputs: [
|
|
42656
|
+
"config",
|
|
42657
|
+
"importSources",
|
|
42658
|
+
"exportParams",
|
|
42659
|
+
"exportOnly"
|
|
42660
|
+
],
|
|
42661
|
+
outputs: ["imported"]
|
|
42662
|
+
},
|
|
42663
|
+
{
|
|
42664
|
+
kind: "component",
|
|
42665
|
+
type: ListItemComponent,
|
|
42666
|
+
selector: "ui-list-item",
|
|
42667
|
+
inputs: [
|
|
42668
|
+
"leading",
|
|
42669
|
+
"inputName",
|
|
42670
|
+
"inputId",
|
|
42671
|
+
"checked",
|
|
42672
|
+
"highlighted",
|
|
42673
|
+
"bordered",
|
|
42674
|
+
"interactive",
|
|
42675
|
+
"alwaysShowBadge",
|
|
42676
|
+
"wrapText",
|
|
42677
|
+
"title",
|
|
42678
|
+
"badge",
|
|
42679
|
+
"badgeVariant",
|
|
42680
|
+
"details",
|
|
42681
|
+
"icon",
|
|
42682
|
+
"iconColor"
|
|
42683
|
+
],
|
|
42684
|
+
outputs: ["select"]
|
|
42685
|
+
},
|
|
42686
|
+
{
|
|
42687
|
+
kind: "component",
|
|
42688
|
+
type: PanelInfoListComponent,
|
|
42689
|
+
selector: "ui-panel-info-list",
|
|
42690
|
+
inputs: [
|
|
42691
|
+
"sections",
|
|
42692
|
+
"showEmptyState",
|
|
42693
|
+
"labelColon",
|
|
42694
|
+
"fill"
|
|
42695
|
+
]
|
|
42696
|
+
},
|
|
42697
|
+
{
|
|
42698
|
+
kind: "component",
|
|
42699
|
+
type: ProgressBarComponent,
|
|
42700
|
+
selector: "ui-progress-bar",
|
|
42701
|
+
inputs: [
|
|
42702
|
+
"value",
|
|
42703
|
+
"max",
|
|
42704
|
+
"label",
|
|
42705
|
+
"variant",
|
|
42706
|
+
"color",
|
|
42707
|
+
"tooltip",
|
|
42708
|
+
"ariaLabel",
|
|
42709
|
+
"ariaLabelledby",
|
|
42710
|
+
"valueText"
|
|
42711
|
+
]
|
|
42712
|
+
},
|
|
42713
|
+
{
|
|
42714
|
+
kind: "component",
|
|
42715
|
+
type: StepperComponent,
|
|
42716
|
+
selector: "ui-stepper",
|
|
42717
|
+
inputs: [
|
|
42718
|
+
"steps",
|
|
42719
|
+
"current",
|
|
42720
|
+
"orientation",
|
|
42721
|
+
"clickable",
|
|
42722
|
+
"linear",
|
|
42723
|
+
"size",
|
|
42724
|
+
"accentColor",
|
|
42725
|
+
"completeColor",
|
|
42726
|
+
"accentLabels",
|
|
42727
|
+
"ariaLabel",
|
|
42728
|
+
"labelsOnSmallScreens",
|
|
42729
|
+
"labelPosition",
|
|
42730
|
+
"stackBelow",
|
|
42731
|
+
"minStepWidth",
|
|
42732
|
+
"scrollable",
|
|
42733
|
+
"leadingWidth",
|
|
42734
|
+
"compact"
|
|
42735
|
+
],
|
|
42736
|
+
outputs: ["currentChange", "stepClick"]
|
|
42737
|
+
},
|
|
42738
|
+
{
|
|
42739
|
+
kind: "component",
|
|
42740
|
+
type: TextInputComponent,
|
|
42741
|
+
selector: "ui-text-input",
|
|
42742
|
+
inputs: [
|
|
42743
|
+
"label",
|
|
42744
|
+
"ariaLabel",
|
|
42745
|
+
"inputTestId",
|
|
42746
|
+
"placeholder",
|
|
42747
|
+
"prefix",
|
|
42748
|
+
"revealable",
|
|
42749
|
+
"passwordRequirements",
|
|
42750
|
+
"passwordRules",
|
|
42751
|
+
"type",
|
|
42752
|
+
"maxlength",
|
|
42753
|
+
"required",
|
|
42754
|
+
"autocomplete",
|
|
42755
|
+
"inputmode",
|
|
42756
|
+
"readonly",
|
|
42757
|
+
"hint",
|
|
42758
|
+
"error",
|
|
42759
|
+
"phoneFormat",
|
|
42760
|
+
"min",
|
|
42761
|
+
"max",
|
|
42762
|
+
"maxDecimals",
|
|
42763
|
+
"step",
|
|
42764
|
+
"valueType",
|
|
42765
|
+
"formatter",
|
|
42766
|
+
"integer",
|
|
42767
|
+
"showErrors",
|
|
42768
|
+
"disabled"
|
|
42769
|
+
],
|
|
42770
|
+
outputs: [
|
|
42771
|
+
"valueChange",
|
|
42772
|
+
"focused",
|
|
42773
|
+
"blurred",
|
|
42774
|
+
"invalidChange"
|
|
42775
|
+
]
|
|
42776
|
+
}
|
|
42777
|
+
],
|
|
42778
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush,
|
|
42779
|
+
encapsulation: i0.ViewEncapsulation.None
|
|
42780
|
+
});
|
|
42781
|
+
}
|
|
42782
|
+
};
|
|
42783
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
42784
|
+
minVersion: "12.0.0",
|
|
42785
|
+
version: "22.2.2",
|
|
42786
|
+
ngImport: i0,
|
|
42787
|
+
type: SetupWizardComponent,
|
|
42788
|
+
decorators: [{
|
|
42789
|
+
type: Component,
|
|
42790
|
+
args: [{
|
|
42791
|
+
selector: "ui-setup-wizard",
|
|
42792
|
+
standalone: true,
|
|
42793
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
42794
|
+
encapsulation: ViewEncapsulation.None,
|
|
42795
|
+
imports: [
|
|
42796
|
+
NgTemplateOutlet,
|
|
42797
|
+
FormsModule,
|
|
42798
|
+
AlertComponent,
|
|
42799
|
+
BadgeComponent,
|
|
42800
|
+
ButtonComponent,
|
|
42801
|
+
ImportExportComponent,
|
|
42802
|
+
ListItemComponent,
|
|
42803
|
+
PanelInfoListComponent,
|
|
42804
|
+
ProgressBarComponent,
|
|
42805
|
+
StepperComponent,
|
|
42806
|
+
TextInputComponent
|
|
42807
|
+
],
|
|
42808
|
+
template: `
|
|
42809
|
+
<div class="sw-page" data-testid="setup-wizard-page">
|
|
42810
|
+
<aside class="sw-sidebar">
|
|
42811
|
+
<div class="sw-sidebar-top">
|
|
42812
|
+
<div class="sw-brand">
|
|
42813
|
+
<ng-content select="[setup-wizard-logo]" />
|
|
42814
|
+
<span class="sw-brand-text" data-testid="setup-wizard-brand">{{ t().brand }}</span>
|
|
42815
|
+
</div>
|
|
42816
|
+
<ui-stepper class="sw-step-nav" [ariaLabel]="t().stepperLabel" orientation="vertical" size="sm" compact="md"
|
|
42817
|
+
[steps]="stepperSteps()" [current]="currentKey()" [clickable]="true" [linear]="false" (stepClick)="goTo($event)" />
|
|
42818
|
+
</div>
|
|
42819
|
+
<div class="sw-sidebar-bottom">
|
|
42820
|
+
<ui-progress-bar [value]="completedCount()" [max]="allSteps().length" ariaLabel="Setup progress" />
|
|
42821
|
+
<span class="sw-progress-text" data-testid="wizard-progress-text">{{ t().progress(completedCount(), allSteps().length) }}</span>
|
|
42822
|
+
</div>
|
|
42823
|
+
</aside>
|
|
42824
|
+
|
|
42825
|
+
<main class="sw-content">
|
|
42826
|
+
<div class="sw-content-header">
|
|
42827
|
+
<h1 class="sw-step-title" data-testid="wizard-step-title">{{ current().label }}</h1>
|
|
42828
|
+
<ui-button variant="ghost" size="sm" data-testid="wizard-exit" (click)="exited.emit()">{{ t().exit }}</ui-button>
|
|
42829
|
+
</div>
|
|
42830
|
+
|
|
42831
|
+
@if (isDone()) {
|
|
42832
|
+
<div class="sw-step-content sw-done" data-testid="wizard-done">
|
|
42833
|
+
<p class="sw-step-desc">{{ t().doneDescription }}</p>
|
|
42834
|
+
<ui-panel-info-list class="sw-summary" data-testid="wizard-summary" [sections]="summarySections()" />
|
|
42835
|
+
<p class="sw-done-hint">{{ t().doneHint }}</p>
|
|
42836
|
+
<ui-button variant="primary" size="lg" data-testid="wizard-finish" (click)="finished.emit()">{{ t().finish }}</ui-button>
|
|
42837
|
+
</div>
|
|
42838
|
+
} @else {
|
|
42839
|
+
<div class="sw-step-content" [attr.data-step]="current().key">
|
|
42840
|
+
@if (current().description) { <p class="sw-step-desc">{{ current().description }}</p> }
|
|
42841
|
+
|
|
42842
|
+
@switch (current().kind) {
|
|
42843
|
+
@case ('custom') {
|
|
42844
|
+
@if (templateFor(current().key); as tpl) { <ng-container [ngTemplateOutlet]="tpl" /> }
|
|
42845
|
+
}
|
|
42846
|
+
@case ('import') {
|
|
42847
|
+
@if (current().import; as imp) {
|
|
42848
|
+
@if (imp.tags?.length) {
|
|
42849
|
+
<div class="sw-import-tags" role="group" [attr.aria-label]="t().whatGetsImported">
|
|
42850
|
+
<span class="sw-tag">{{ t().whatGetsImported }}</span>
|
|
42851
|
+
@for (tag of imp.tags; track tag) { <ui-badge [label]="tag" variant="slate" /> }
|
|
42852
|
+
</div>
|
|
42853
|
+
}
|
|
42854
|
+
@if (importResult(current().key); as res) {
|
|
42855
|
+
<ui-alert variant="success" [title]="importTitle(current().key)" [attr.data-testid]="'import-result-' + current().key">
|
|
42856
|
+
{{ imp.resultText }}
|
|
42857
|
+
<ui-button variant="link" size="sm" data-testid="import-more" (click)="clearImport(current().key)">{{ t().importMore }}</ui-button>
|
|
42858
|
+
</ui-alert>
|
|
42859
|
+
} @else {
|
|
42860
|
+
@if (imp.hint) { <p class="sw-hint">{{ imp.hint }}</p> }
|
|
42861
|
+
<ui-button variant="primary" size="lg" [attr.data-testid]="'open-import-' + current().key" (click)="openImport(current().key)">{{ imp.buttonLabel }}</ui-button>
|
|
42862
|
+
}
|
|
42863
|
+
<ui-import-export #importer style="display:none" [config]="imp.config" [importSources]="imp.sources" (imported)="onImported(current().key)" />
|
|
42864
|
+
}
|
|
42865
|
+
}
|
|
42866
|
+
@case ('integrations') {
|
|
42867
|
+
<div class="sw-provider-grid" role="group" [attr.aria-label]="current().label">
|
|
42868
|
+
@for (it of current().integrations ?? []; track it.key) {
|
|
42869
|
+
<ui-list-item [attr.data-testid]="'integration-' + it.key" [interactive]="false" [bordered]="true"
|
|
42870
|
+
[icon]="it.icon ?? 'link'" iconColor="blue" [title]="it.title" [details]="[detailFor(it)]"
|
|
42871
|
+
[badge]="t().connected" badgeVariant="green" [alwaysShowBadge]="isConnected(it)">
|
|
42872
|
+
<ui-button list-item-action variant="secondary" size="sm" [ariaLabel]="(isConnected(it) ? t().reconnect : t().connect) + ' ' + it.title" (click)="connect(it)">
|
|
42873
|
+
{{ isConnected(it) ? t().reconnect : t().connect }}
|
|
42874
|
+
</ui-button>
|
|
42875
|
+
</ui-list-item>
|
|
42876
|
+
@if (areaCodeOpen() === it.key) {
|
|
42877
|
+
<div class="sw-area-code" [attr.data-testid]="'area-code-' + it.key">
|
|
42878
|
+
<ui-text-input [label]="areaCodeLabelFor(it)" [maxlength]="3" inputmode="numeric" [ngModel]="areaCode()" (ngModelChange)="areaCode.set($event ?? '')" [inputTestId]="'area-code-input-' + it.key" />
|
|
42879
|
+
<ui-button variant="primary" size="sm" [loading]="busy()" [disabled]="!validAreaCode()" (click)="submitAreaCode(it)">{{ t().connect }}</ui-button>
|
|
42880
|
+
</div>
|
|
42881
|
+
}
|
|
42882
|
+
}
|
|
42883
|
+
</div>
|
|
42884
|
+
}
|
|
42885
|
+
}
|
|
42886
|
+
|
|
42887
|
+
@if (error()) { <ui-alert variant="danger" data-testid="wizard-save-error">{{ error() }}</ui-alert> }
|
|
42888
|
+
|
|
42889
|
+
<div class="sw-step-actions">
|
|
42890
|
+
@if (index() > 0) { <ui-button variant="ghost" size="md" data-testid="wizard-back" [disabled]="busy()" (click)="back()">{{ t().back }}</ui-button> } @else { <span></span> }
|
|
42891
|
+
<div class="sw-actions-right">
|
|
42892
|
+
@if (current().skippable !== false) {
|
|
42893
|
+
<ui-button variant="ghost" size="md" data-testid="wizard-skip" [disabled]="busy()" (click)="skip()">{{ t().skip }}</ui-button>
|
|
42894
|
+
}
|
|
42895
|
+
@if (current().kind === 'custom') {
|
|
42896
|
+
<ui-button variant="primary" size="lg" data-testid="wizard-save" [loading]="busy()" [disabled]="current().canContinue ? !current().canContinue!() : false" (click)="saveAndContinue()">
|
|
42897
|
+
{{ current().saveLabel ?? t().saveAndContinue }}
|
|
42898
|
+
</ui-button>
|
|
42899
|
+
} @else if (current().kind === 'integrations') {
|
|
42900
|
+
<ui-button variant="primary" size="lg" data-testid="wizard-save" [loading]="busy()" (click)="saveAndContinue()">{{ t().saveAndContinue }}</ui-button>
|
|
42901
|
+
} @else if (importResult(current().key)) {
|
|
42902
|
+
<ui-button variant="primary" size="lg" data-testid="wizard-continue" (click)="continueFromImport()">{{ t().continue }}</ui-button>
|
|
42903
|
+
}
|
|
42904
|
+
</div>
|
|
42905
|
+
</div>
|
|
42906
|
+
</div>
|
|
42907
|
+
}
|
|
42908
|
+
</main>
|
|
42909
|
+
</div>
|
|
42910
|
+
`,
|
|
42911
|
+
styles: [".sw-page{display:flex;min-height:100vh;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.sw-sidebar{width:260px;min-width:260px;background:#faf9f7;border-right:1px solid #e5e2dc;display:flex;flex-direction:column;justify-content:space-between;padding:28px 20px}.sw-brand{display:flex;align-items:center;gap:10px;margin-bottom:28px}.sw-brand-text{font-size:1.125rem;font-weight:700;color:#1e293b}.sw-step-nav{display:block}.sw-sidebar-bottom{margin-top:24px}.sw-progress-text{display:block;margin-top:8px;font-size:.75rem;color:#94a3b8}.sw-content{flex:1;min-width:0;background:#fff;display:flex;flex-direction:column;overflow-y:auto}.sw-content-header{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:20px 32px;border-bottom:1px solid #f1f5f9}.sw-step-title{font-size:1.25rem;font-weight:700;color:#111827;margin:0}.sw-step-content{max-width:780px;padding:16px 48px 60px}.sw-step-desc{font-size:.9375rem;color:#6b7280;line-height:1.5;margin:0 0 32px}.sw-hint{font-size:.8125rem;color:#94a3b8;margin:0 0 16px}.sw-import-tags{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:28px}.sw-tag{font-size:.6875rem;font-weight:700;letter-spacing:1px;color:#6b7280}.sw-provider-grid{display:flex;flex-direction:column;gap:8px;margin-bottom:24px}.sw-area-code{display:flex;align-items:flex-end;gap:12px;padding:0 0 8px 4px;max-width:320px}.sw-area-code ui-text-input{flex:1}.sw-content ui-alert{display:block;margin-bottom:16px}.sw-step-actions{display:flex;justify-content:space-between;align-items:center;margin-top:16px;padding-top:20px;border-top:1px solid #f1f5f9}.sw-actions-right{display:flex;align-items:center;gap:16px}.sw-done{text-align:center;display:flex;flex-direction:column;align-items:center;max-width:none}.sw-done .sw-step-desc{max-width:480px}.sw-summary{display:block;width:100%;max-width:480px;margin-bottom:24px;text-align:left}.sw-done-hint{font-size:.8125rem;color:#9ca3af;margin-bottom:24px}@media(max-width:768px){.sw-page{flex-direction:column}.sw-sidebar{width:100%;min-width:unset;padding:16px}.sw-brand{margin-bottom:12px}.sw-sidebar-bottom{display:none}.sw-step-content{padding:12px 20px 40px}}\n"]
|
|
42912
|
+
}]
|
|
42913
|
+
}],
|
|
42914
|
+
ctorParameters: () => [],
|
|
42915
|
+
propDecorators: {
|
|
42916
|
+
steps: [{
|
|
42917
|
+
type: i0.Input,
|
|
42918
|
+
args: [{
|
|
42919
|
+
isSignal: true,
|
|
42920
|
+
alias: "steps",
|
|
42921
|
+
required: true
|
|
42922
|
+
}]
|
|
42923
|
+
}],
|
|
42924
|
+
texts: [{
|
|
42925
|
+
type: i0.Input,
|
|
42926
|
+
args: [{
|
|
42927
|
+
isSignal: true,
|
|
42928
|
+
alias: "texts",
|
|
42929
|
+
required: false
|
|
42930
|
+
}]
|
|
42931
|
+
}],
|
|
42932
|
+
finished: [{
|
|
42933
|
+
type: i0.Output,
|
|
42934
|
+
args: ["finished"]
|
|
42935
|
+
}],
|
|
42936
|
+
exited: [{
|
|
42937
|
+
type: i0.Output,
|
|
42938
|
+
args: ["exited"]
|
|
42939
|
+
}],
|
|
42940
|
+
stepChange: [{
|
|
42941
|
+
type: i0.Output,
|
|
42942
|
+
args: ["stepChange"]
|
|
42943
|
+
}],
|
|
42944
|
+
stepTemplates: [{
|
|
42945
|
+
type: i0.ContentChildren,
|
|
42946
|
+
args: [i0.forwardRef(() => SetupWizardStepDirective), { isSignal: true }]
|
|
42947
|
+
}],
|
|
42948
|
+
importers: [{
|
|
42949
|
+
type: i0.ViewChildren,
|
|
42950
|
+
args: ["importer", { isSignal: true }]
|
|
42951
|
+
}]
|
|
42952
|
+
}
|
|
42953
|
+
});
|
|
42954
|
+
const EMAIL_TEMPLATE_START_TEXT = "Start writing your email...";
|
|
42955
|
+
const EMAIL_TEMPLATE_MAX_BODY = 9e3;
|
|
42956
|
+
const EMAIL_TEMPLATE_MAX_TEXT = 255;
|
|
42957
|
+
const MAX_EMAIL_ADDRESS_LENGTH = 254;
|
|
42958
|
+
const EMAIL_RE = /^[^\s@,;<>"]+@[^\s@,;<>"]+\.[^\s@,;<>"]+$/;
|
|
42959
|
+
const isValidEmailAddress = (address) => address.length <= 254 && EMAIL_RE.test(address);
|
|
42960
|
+
function isWebUrl(raw) {
|
|
42961
|
+
if (/[\s"<>]/.test(raw)) return false;
|
|
42962
|
+
try {
|
|
42963
|
+
const u = new URL(raw);
|
|
42964
|
+
return (u.protocol === "http:" || u.protocol === "https:") && (u.hostname.includes(".") || u.hostname === "localhost");
|
|
42965
|
+
} catch {
|
|
42966
|
+
return false;
|
|
42967
|
+
}
|
|
42968
|
+
}
|
|
42969
|
+
function escapeHtml(value) {
|
|
42970
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
42971
|
+
}
|
|
42972
|
+
const CONTENT_MEMO = /* @__PURE__ */ new Map();
|
|
42973
|
+
function blockHasContent(html) {
|
|
42974
|
+
const hit = CONTENT_MEMO.get(html);
|
|
42975
|
+
if (hit !== void 0) return hit;
|
|
42976
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
42977
|
+
let result;
|
|
42978
|
+
if (doc.querySelector("img, video, iframe")) result = true;
|
|
42979
|
+
else {
|
|
42980
|
+
const text = (doc.body.textContent ?? "").replace(/ /g, " ").trim();
|
|
42981
|
+
result = text !== "" && text !== "Start writing your email...";
|
|
42982
|
+
}
|
|
42983
|
+
if (CONTENT_MEMO.size >= 200) CONTENT_MEMO.clear();
|
|
42984
|
+
CONTENT_MEMO.set(html, result);
|
|
42985
|
+
return result;
|
|
42986
|
+
}
|
|
42987
|
+
const EMAIL_BLOCK_DRAG_TYPE = "application/x-tpl-block";
|
|
42988
|
+
function isBlockDrag(event) {
|
|
42989
|
+
const types = event.dataTransfer?.types;
|
|
42990
|
+
return !!types && Array.from(types).includes("application/x-tpl-block");
|
|
42991
|
+
}
|
|
42992
|
+
function insideEditorBlock(node) {
|
|
42993
|
+
return !!(node instanceof Element ? node : node?.parentElement)?.closest(".block-content");
|
|
42994
|
+
}
|
|
42995
|
+
function formatVideoDuration(seconds) {
|
|
42996
|
+
if (!seconds) return "";
|
|
42997
|
+
return `${Math.floor(seconds / 60)}:${(seconds % 60).toString().padStart(2, "0")}`;
|
|
42998
|
+
}
|
|
42999
|
+
function formatCategoryLabel(cat) {
|
|
43000
|
+
return (cat ?? "").split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
43001
|
+
}
|
|
43002
|
+
function thumbnailSrc(video, origin) {
|
|
43003
|
+
const t = video.thumbnailUrl;
|
|
43004
|
+
if (!t) return null;
|
|
43005
|
+
return t.startsWith("http") ? t : `${origin}${t}`;
|
|
43006
|
+
}
|
|
43007
|
+
function buildVideoBlockHtml(video, link, caption, origin) {
|
|
43008
|
+
const title = escapeHtml(video.title);
|
|
43009
|
+
const href = escapeHtml(link);
|
|
43010
|
+
const thumb = thumbnailSrc(video, origin);
|
|
43011
|
+
return `<div style="max-width:560px;margin:16px auto;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden;font-family:inherit"><a href="${href}" target="_blank" style="display:block;text-decoration:none;">${`<div style="position:relative;display:block;line-height:0;">${thumb ? `<img src="${escapeHtml(thumb)}" alt="${title}" style="width:100%;height:200px;object-fit:cover;display:block;border-radius:0;"/>` : `<div style="width:100%;height:200px;background:linear-gradient(135deg,#1e293b,#334155);display:flex;align-items:center;justify-content:center;"></div>`}<div style="position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.25);"><div style="width:64px;height:64px;background:rgba(0,0,0,0.65);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 12px rgba(0,0,0,0.4);"><div style="width:0;height:0;border-style:solid;border-width:12px 0 12px 22px;border-color:transparent transparent transparent #ffffff;margin-left:5px;"></div></div></div></div>`}<div style="padding:14px 16px;background:#fff;display:flex;align-items:center;gap:10px;"><div style="width:32px;height:32px;background:#ef4444;border-radius:6px;flex-shrink:0;display:flex;align-items:center;justify-content:center;"><div style="width:0;height:0;border-style:solid;border-width:6px 0 6px 11px;border-color:transparent transparent transparent #ffffff;margin-left:2px;"></div></div><div><p style="font-size:14px;font-weight:600;color:#1e293b;margin:0 0 2px;">${title}</p><p style="font-size:12px;color:#64748b;margin:0;">${escapeHtml(caption)}</p></div></div></a></div>`;
|
|
43012
|
+
}
|
|
43013
|
+
const TH = "padding:8px 12px;text-align:left;background:#f8fafc;border:1px solid #e2e8f0";
|
|
43014
|
+
const TD = "padding:8px 12px;border:1px solid #e2e8f0";
|
|
43015
|
+
const DEFAULT_EMAIL_TEMPLATE_BLOCK_GROUPS = [
|
|
43016
|
+
{
|
|
43017
|
+
name: "Buttons",
|
|
43018
|
+
blocks: [
|
|
43019
|
+
{
|
|
43020
|
+
key: "btn-primary",
|
|
43021
|
+
label: "Primary Button",
|
|
43022
|
+
html: "<div style=\"text-align:center;margin:16px 0\"><a href=\"#\" style=\"display:inline-block;padding:12px 28px;background:#3B82F6;color:#fff;border-radius:6px;text-decoration:none;font-weight:600\">Button Text</a></div>"
|
|
43023
|
+
},
|
|
43024
|
+
{
|
|
43025
|
+
key: "btn-secondary",
|
|
43026
|
+
label: "Secondary Button",
|
|
43027
|
+
html: "<div style=\"text-align:center;margin:16px 0\"><a href=\"#\" style=\"display:inline-block;padding:12px 28px;background:#64748b;color:#fff;border-radius:6px;text-decoration:none;font-weight:600\">Button Text</a></div>"
|
|
43028
|
+
},
|
|
43029
|
+
{
|
|
43030
|
+
key: "btn-outline",
|
|
43031
|
+
label: "Outline Button",
|
|
43032
|
+
html: "<div style=\"text-align:center;margin:16px 0\"><a href=\"#\" style=\"display:inline-block;padding:12px 28px;border:2px solid #3B82F6;color:#3B82F6;border-radius:6px;text-decoration:none;font-weight:600\">Button Text</a></div>"
|
|
43033
|
+
},
|
|
43034
|
+
{
|
|
43035
|
+
key: "btn-link",
|
|
43036
|
+
label: "Link Button",
|
|
43037
|
+
html: "<div style=\"margin:8px 0\"><a href=\"#\" style=\"color:#3B82F6;text-decoration:underline\">Link Text</a></div>"
|
|
43038
|
+
}
|
|
43039
|
+
]
|
|
43040
|
+
},
|
|
43041
|
+
{
|
|
43042
|
+
name: "Spacing",
|
|
43043
|
+
blocks: [
|
|
43044
|
+
{
|
|
43045
|
+
key: "spacer-sm",
|
|
43046
|
+
label: "Small Spacer",
|
|
43047
|
+
html: "<div style=\"height:8px\"> </div>"
|
|
43048
|
+
},
|
|
43049
|
+
{
|
|
43050
|
+
key: "spacer-md",
|
|
43051
|
+
label: "Medium Spacer",
|
|
43052
|
+
html: "<div style=\"height:24px\"> </div>"
|
|
43053
|
+
},
|
|
43054
|
+
{
|
|
43055
|
+
key: "spacer-lg",
|
|
43056
|
+
label: "Large Spacer",
|
|
43057
|
+
html: "<div style=\"height:48px\"> </div>"
|
|
43058
|
+
},
|
|
43059
|
+
{
|
|
43060
|
+
key: "divider",
|
|
43061
|
+
label: "Divider Line",
|
|
43062
|
+
html: "<hr style=\"border:none;border-top:1px solid #e2e8f0;margin:16px 0\"/>"
|
|
43063
|
+
}
|
|
43064
|
+
]
|
|
43065
|
+
},
|
|
43066
|
+
{
|
|
43067
|
+
name: "Layout",
|
|
43068
|
+
blocks: [{
|
|
43069
|
+
key: "layout-2col",
|
|
43070
|
+
label: "Two Columns",
|
|
43071
|
+
html: "<table style=\"width:100%;border-collapse:collapse\"><tr><td style=\"width:50%;padding:8px;vertical-align:top\"><p>Left column</p></td><td style=\"width:50%;padding:8px;vertical-align:top\"><p>Right column</p></td></tr></table>"
|
|
43072
|
+
}, {
|
|
43073
|
+
key: "layout-3col",
|
|
43074
|
+
label: "Three Columns",
|
|
43075
|
+
html: "<table style=\"width:100%;border-collapse:collapse\"><tr><td style=\"width:33%;padding:8px;vertical-align:top\"><p>Col 1</p></td><td style=\"width:33%;padding:8px;vertical-align:top\"><p>Col 2</p></td><td style=\"width:34%;padding:8px;vertical-align:top\"><p>Col 3</p></td></tr></table>"
|
|
43076
|
+
}]
|
|
43077
|
+
},
|
|
43078
|
+
{
|
|
43079
|
+
name: "Content",
|
|
43080
|
+
blocks: [{
|
|
43081
|
+
key: "text-block",
|
|
43082
|
+
label: "Text Block",
|
|
43083
|
+
html: "<p>Enter your text here...</p>"
|
|
43084
|
+
}, {
|
|
43085
|
+
key: "image-block",
|
|
43086
|
+
label: "Image Block",
|
|
43087
|
+
html: "<div style=\"text-align:center;margin:8px 0\"><img src=\"https://placehold.co/600x200/e2e8f0/94a3b8?text=Image\" alt=\"Image\" style=\"max-width:100%;border-radius:8px\"/></div>"
|
|
43088
|
+
}]
|
|
43089
|
+
},
|
|
43090
|
+
{
|
|
43091
|
+
name: "Tables",
|
|
43092
|
+
blocks: [{
|
|
43093
|
+
key: "table-2col",
|
|
43094
|
+
label: "Two-Column Table",
|
|
43095
|
+
html: `<table style="width:100%;border-collapse:collapse;border:1px solid #e2e8f0"><tr><th style="${TH}">Header 1</th><th style="${TH}">Header 2</th></tr><tr><td style="${TD}">Data</td><td style="${TD}">Data</td></tr></table>`
|
|
43096
|
+
}, {
|
|
43097
|
+
key: "table-3col",
|
|
43098
|
+
label: "Three-Column Table",
|
|
43099
|
+
html: `<table style="width:100%;border-collapse:collapse;border:1px solid #e2e8f0"><tr><th style="${TH}">Header 1</th><th style="${TH}">Header 2</th><th style="${TH}">Header 3</th></tr><tr><td style="${TD}">Data</td><td style="${TD}">Data</td><td style="${TD}">Data</td></tr></table>`
|
|
43100
|
+
}]
|
|
43101
|
+
}
|
|
43102
|
+
];
|
|
43103
|
+
const DEFAULT_EMAIL_TEMPLATE_VARIABLE_GROUPS = [{
|
|
43104
|
+
name: "Contact",
|
|
43105
|
+
variables: [
|
|
43106
|
+
{
|
|
43107
|
+
key: "first_name",
|
|
43108
|
+
label: "First Name",
|
|
43109
|
+
example: "John"
|
|
43110
|
+
},
|
|
43111
|
+
{
|
|
43112
|
+
key: "last_name",
|
|
43113
|
+
label: "Last Name",
|
|
43114
|
+
example: "Smith"
|
|
43115
|
+
},
|
|
43116
|
+
{
|
|
43117
|
+
key: "full_name",
|
|
43118
|
+
label: "Full Name",
|
|
43119
|
+
example: "John Smith"
|
|
43120
|
+
},
|
|
43121
|
+
{
|
|
43122
|
+
key: "email",
|
|
43123
|
+
label: "Email",
|
|
43124
|
+
example: "john@example.com"
|
|
43125
|
+
},
|
|
43126
|
+
{
|
|
43127
|
+
key: "phone",
|
|
43128
|
+
label: "Phone",
|
|
43129
|
+
example: "(512) 555-0100"
|
|
43130
|
+
}
|
|
43131
|
+
]
|
|
43132
|
+
}];
|
|
43133
|
+
const FONT_SIZE_OPTIONS = [
|
|
43134
|
+
"10",
|
|
43135
|
+
"12",
|
|
43136
|
+
"14",
|
|
43137
|
+
"16",
|
|
43138
|
+
"18",
|
|
43139
|
+
"20",
|
|
43140
|
+
"24",
|
|
43141
|
+
"28",
|
|
43142
|
+
"32",
|
|
43143
|
+
"36"
|
|
43144
|
+
].map((n) => ({
|
|
43145
|
+
value: `${n}px`,
|
|
43146
|
+
label: `${n}px`
|
|
43147
|
+
}));
|
|
43148
|
+
const HEADING_OPTIONS = [
|
|
43149
|
+
{
|
|
43150
|
+
value: "p",
|
|
43151
|
+
label: "Normal"
|
|
43152
|
+
},
|
|
43153
|
+
{
|
|
43154
|
+
value: "h1",
|
|
43155
|
+
label: "Heading 1"
|
|
43156
|
+
},
|
|
43157
|
+
{
|
|
43158
|
+
value: "h2",
|
|
43159
|
+
label: "Heading 2"
|
|
43160
|
+
},
|
|
43161
|
+
{
|
|
43162
|
+
value: "h3",
|
|
43163
|
+
label: "Heading 3"
|
|
43164
|
+
}
|
|
43165
|
+
];
|
|
43166
|
+
const SPACING_OPTIONS = [
|
|
43167
|
+
{
|
|
43168
|
+
value: "",
|
|
43169
|
+
label: "None"
|
|
43170
|
+
},
|
|
43171
|
+
{
|
|
43172
|
+
value: "8px",
|
|
43173
|
+
label: "Small (8px)"
|
|
43174
|
+
},
|
|
43175
|
+
{
|
|
43176
|
+
value: "16px",
|
|
43177
|
+
label: "Medium (16px)"
|
|
43178
|
+
},
|
|
43179
|
+
{
|
|
43180
|
+
value: "32px",
|
|
43181
|
+
label: "Large (32px)"
|
|
43182
|
+
}
|
|
43183
|
+
];
|
|
43184
|
+
const CTA_TYPE_OPTIONS = [
|
|
43185
|
+
{
|
|
43186
|
+
value: "",
|
|
43187
|
+
label: "None (use video default)"
|
|
43188
|
+
},
|
|
43189
|
+
{
|
|
43190
|
+
value: "schedule",
|
|
43191
|
+
label: "Schedule a Call"
|
|
43192
|
+
},
|
|
43193
|
+
{
|
|
43194
|
+
value: "contact",
|
|
43195
|
+
label: "Contact Me"
|
|
43196
|
+
},
|
|
43197
|
+
{
|
|
43198
|
+
value: "custom",
|
|
43199
|
+
label: "Custom"
|
|
43200
|
+
}
|
|
43201
|
+
];
|
|
43202
|
+
const PLAYBACK_OPTIONS = [{
|
|
43203
|
+
value: "click",
|
|
43204
|
+
label: "Click to Play"
|
|
43205
|
+
}, {
|
|
43206
|
+
value: "autoplay",
|
|
43207
|
+
label: "Autoplay"
|
|
43208
|
+
}];
|
|
43209
|
+
const DEFAULT_AI_EXAMPLES = [
|
|
43210
|
+
"Follow up after a showing",
|
|
43211
|
+
"Open house invitation",
|
|
43212
|
+
"Market update",
|
|
43213
|
+
"Thank you after closing"
|
|
43214
|
+
];
|
|
43215
|
+
const DEFAULT_VIDEO_LINK = (video) => video.shareLink;
|
|
43216
|
+
const EMPTY_VIDEO_DEFAULTS = {
|
|
43217
|
+
autoplay: false,
|
|
43218
|
+
caption: "",
|
|
43219
|
+
ctaType: "",
|
|
43220
|
+
ctaUrl: "",
|
|
43221
|
+
ctaLabel: "",
|
|
43222
|
+
hideReactions: false
|
|
43223
|
+
};
|
|
43224
|
+
var EmailTemplateEditorComponent = class EmailTemplateEditorComponent {
|
|
43225
|
+
constructor() {
|
|
43226
|
+
this.host = inject(ElementRef);
|
|
43227
|
+
this.injector = inject(Injector);
|
|
43228
|
+
this.toast = inject(ToastService);
|
|
43229
|
+
this.template = input(null, ...ngDevMode ? [{ debugName: "template" }] : /* istanbul ignore next */ []);
|
|
43230
|
+
this.categories = input([], ...ngDevMode ? [{ debugName: "categories" }] : /* istanbul ignore next */ []);
|
|
43231
|
+
this.categoryPlaceholder = input("Select category...", ...ngDevMode ? [{ debugName: "categoryPlaceholder" }] : /* istanbul ignore next */ []);
|
|
43232
|
+
this.assignmentOptions = input([], ...ngDevMode ? [{ debugName: "assignmentOptions" }] : /* istanbul ignore next */ []);
|
|
43233
|
+
this.featuredAssignmentKey = input(null, ...ngDevMode ? [{ debugName: "featuredAssignmentKey" }] : /* istanbul ignore next */ []);
|
|
43234
|
+
this.assignmentsDescription = input("Assign this template to automated email triggers. When triggered, the system will send this template.", ...ngDevMode ? [{ debugName: "assignmentsDescription" }] : /* istanbul ignore next */ []);
|
|
43235
|
+
this.textEnabled = input(false, ...ngDevMode ? [{ debugName: "textEnabled" }] : /* istanbul ignore next */ []);
|
|
43236
|
+
this.variableGroups = input(DEFAULT_EMAIL_TEMPLATE_VARIABLE_GROUPS, ...ngDevMode ? [{ debugName: "variableGroups" }] : /* istanbul ignore next */ []);
|
|
43237
|
+
this.blockGroups = input(DEFAULT_EMAIL_TEMPLATE_BLOCK_GROUPS, ...ngDevMode ? [{ debugName: "blockGroups" }] : /* istanbul ignore next */ []);
|
|
43238
|
+
this.aiExamples = input(DEFAULT_AI_EXAMPLES, ...ngDevMode ? [{ debugName: "aiExamples" }] : /* istanbul ignore next */ []);
|
|
43239
|
+
this.namePlaceholder = input("e.g. Follow-Up — Friendly", ...ngDevMode ? [{ debugName: "namePlaceholder" }] : /* istanbul ignore next */ []);
|
|
43240
|
+
this.subjectPlaceholder = input("e.g. Following up on {{property_address}}", ...ngDevMode ? [{ debugName: "subjectPlaceholder" }] : /* istanbul ignore next */ []);
|
|
43241
|
+
this.backLabel = input("Email Messages", ...ngDevMode ? [{ debugName: "backLabel" }] : /* istanbul ignore next */ []);
|
|
43242
|
+
this.createLabel = input("Create Template", ...ngDevMode ? [{ debugName: "createLabel" }] : /* istanbul ignore next */ []);
|
|
43243
|
+
this.editLabel = input("Edit Template", ...ngDevMode ? [{ debugName: "editLabel" }] : /* istanbul ignore next */ []);
|
|
43244
|
+
this.systemNote = input("This is a built-in system template, so it can be viewed but not changed.", ...ngDevMode ? [{ debugName: "systemNote" }] : /* istanbul ignore next */ []);
|
|
43245
|
+
this.readOnlyNote = input("You have read-only access to email templates, so this template can be viewed but not saved.", ...ngDevMode ? [{ debugName: "readOnlyNote" }] : /* istanbul ignore next */ []);
|
|
43246
|
+
this.idPrefix = input("tpl", ...ngDevMode ? [{ debugName: "idPrefix" }] : /* istanbul ignore next */ []);
|
|
43247
|
+
this.maxBodyLength = input(EMAIL_TEMPLATE_MAX_BODY, ...ngDevMode ? [{ debugName: "maxBodyLength" }] : /* istanbul ignore next */ []);
|
|
43248
|
+
this.maxTextLength = input(255, ...ngDevMode ? [{ debugName: "maxTextLength" }] : /* istanbul ignore next */ []);
|
|
43249
|
+
this.fullAccess = input(true, ...ngDevMode ? [{ debugName: "fullAccess" }] : /* istanbul ignore next */ []);
|
|
43250
|
+
this.systemTemplate = input(false, ...ngDevMode ? [{ debugName: "systemTemplate" }] : /* istanbul ignore next */ []);
|
|
43251
|
+
this.aiEnabled = input(true, ...ngDevMode ? [{ debugName: "aiEnabled" }] : /* istanbul ignore next */ []);
|
|
43252
|
+
this.testSendEnabled = input(true, ...ngDevMode ? [{ debugName: "testSendEnabled" }] : /* istanbul ignore next */ []);
|
|
43253
|
+
this.videosEnabled = input(true, ...ngDevMode ? [{ debugName: "videosEnabled" }] : /* istanbul ignore next */ []);
|
|
43254
|
+
this.saving = input(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
|
|
43255
|
+
this.testSending = input(false, ...ngDevMode ? [{ debugName: "testSending" }] : /* istanbul ignore next */ []);
|
|
43256
|
+
this.aiGenerating = input(false, ...ngDevMode ? [{ debugName: "aiGenerating" }] : /* istanbul ignore next */ []);
|
|
43257
|
+
this.videos = input([], ...ngDevMode ? [{ debugName: "videos" }] : /* istanbul ignore next */ []);
|
|
43258
|
+
this.videosLoading = input(false, ...ngDevMode ? [{ debugName: "videosLoading" }] : /* istanbul ignore next */ []);
|
|
43259
|
+
this.videoDefaults = input(EMPTY_VIDEO_DEFAULTS, ...ngDevMode ? [{ debugName: "videoDefaults" }] : /* istanbul ignore next */ []);
|
|
43260
|
+
this.videoLink = input(DEFAULT_VIDEO_LINK, ...ngDevMode ? [{ debugName: "videoLink" }] : /* istanbul ignore next */ []);
|
|
43261
|
+
this.noVideosHint = input("Upload videos on the Video Messages page.", ...ngDevMode ? [{ debugName: "noVideosHint" }] : /* istanbul ignore next */ []);
|
|
43262
|
+
this.back = output();
|
|
43263
|
+
this.save = output();
|
|
43264
|
+
this.testSend = output();
|
|
43265
|
+
this.aiGenerate = output();
|
|
43266
|
+
this.videosRequested = output();
|
|
43267
|
+
this.useAsMine = output();
|
|
43268
|
+
this.editingId = signal(null, ...ngDevMode ? [{ debugName: "editingId" }] : /* istanbul ignore next */ []);
|
|
43269
|
+
this.isNew = computed(() => !this.editingId(), ...ngDevMode ? [{ debugName: "isNew" }] : /* istanbul ignore next */ []);
|
|
43270
|
+
this.name = signal("", ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
|
|
43271
|
+
this.subject = signal("", ...ngDevMode ? [{ debugName: "subject" }] : /* istanbul ignore next */ []);
|
|
43272
|
+
this.category = signal("", ...ngDevMode ? [{ debugName: "category" }] : /* istanbul ignore next */ []);
|
|
43273
|
+
this.useAllYear = signal(true, ...ngDevMode ? [{ debugName: "useAllYear" }] : /* istanbul ignore next */ []);
|
|
43274
|
+
this.activeFrom = signal("", ...ngDevMode ? [{ debugName: "activeFrom" }] : /* istanbul ignore next */ []);
|
|
43275
|
+
this.activeTo = signal("", ...ngDevMode ? [{ debugName: "activeTo" }] : /* istanbul ignore next */ []);
|
|
43276
|
+
this.videoId = signal(null, ...ngDevMode ? [{ debugName: "videoId" }] : /* istanbul ignore next */ []);
|
|
43277
|
+
this.videoTitle = signal(null, ...ngDevMode ? [{ debugName: "videoTitle" }] : /* istanbul ignore next */ []);
|
|
43278
|
+
this.assignmentChannels = signal({}, ...ngDevMode ? [{ debugName: "assignmentChannels" }] : /* istanbul ignore next */ []);
|
|
43279
|
+
this.assignmentState = computed(() => this.assignmentOptions().map((o) => ({
|
|
43280
|
+
...o,
|
|
43281
|
+
email: !!this.assignmentChannels()[o.key]?.email,
|
|
43282
|
+
text: !!this.assignmentChannels()[o.key]?.text
|
|
43283
|
+
})), ...ngDevMode ? [{ debugName: "assignmentState" }] : /* istanbul ignore next */ []);
|
|
43284
|
+
this.blocks = signal([], ...ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []);
|
|
43285
|
+
this.selectedBlockId = signal(null, ...ngDevMode ? [{ debugName: "selectedBlockId" }] : /* istanbul ignore next */ []);
|
|
43286
|
+
this.initializedBlockIds = /* @__PURE__ */ new Set();
|
|
43287
|
+
this.savedRange = null;
|
|
43288
|
+
this.blockEditors = viewChildren("blockEditor", ...ngDevMode ? [{ debugName: "blockEditors" }] : /* istanbul ignore next */ []);
|
|
43289
|
+
this.colorInputRef = viewChild("colorInput", ...ngDevMode ? [{ debugName: "colorInputRef" }] : /* istanbul ignore next */ []);
|
|
43290
|
+
this.loadedBody = signal(null, ...ngDevMode ? [{ debugName: "loadedBody" }] : /* istanbul ignore next */ []);
|
|
43291
|
+
this.bodyEdited = signal(false, ...ngDevMode ? [{ debugName: "bodyEdited" }] : /* istanbul ignore next */ []);
|
|
43292
|
+
this.savedSnapshot = signal(null, ...ngDevMode ? [{ debugName: "savedSnapshot" }] : /* istanbul ignore next */ []);
|
|
43293
|
+
this.dragFromPalette = null;
|
|
43294
|
+
this.dragFromCanvas = signal(null, ...ngDevMode ? [{ debugName: "dragFromCanvas" }] : /* istanbul ignore next */ []);
|
|
43295
|
+
this.dragOverIndex = signal(null, ...ngDevMode ? [{ debugName: "dragOverIndex" }] : /* istanbul ignore next */ []);
|
|
43296
|
+
this.currentColor = signal("#1e293b", ...ngDevMode ? [{ debugName: "currentColor" }] : /* istanbul ignore next */ []);
|
|
43297
|
+
this.blockSpacing = signal("", ...ngDevMode ? [{ debugName: "blockSpacing" }] : /* istanbul ignore next */ []);
|
|
43298
|
+
this.headingKey = signal(0, ...ngDevMode ? [{ debugName: "headingKey" }] : /* istanbul ignore next */ []);
|
|
43299
|
+
this.sizeKey = signal(0, ...ngDevMode ? [{ debugName: "sizeKey" }] : /* istanbul ignore next */ []);
|
|
43300
|
+
this.headingOptions = HEADING_OPTIONS;
|
|
43301
|
+
this.fontSizeOptions = FONT_SIZE_OPTIONS;
|
|
43302
|
+
this.spacingOptions = SPACING_OPTIONS;
|
|
43303
|
+
this.ctaTypeOptions = CTA_TYPE_OPTIONS;
|
|
43304
|
+
this.playbackOptions = PLAYBACK_OPTIONS;
|
|
43305
|
+
this.showLinkDialog = signal(false, ...ngDevMode ? [{ debugName: "showLinkDialog" }] : /* istanbul ignore next */ []);
|
|
43306
|
+
this.linkUrl = signal("", ...ngDevMode ? [{ debugName: "linkUrl" }] : /* istanbul ignore next */ []);
|
|
43307
|
+
this.linkText = signal("", ...ngDevMode ? [{ debugName: "linkText" }] : /* istanbul ignore next */ []);
|
|
43308
|
+
this.linkTextFromSelection = signal(false, ...ngDevMode ? [{ debugName: "linkTextFromSelection" }] : /* istanbul ignore next */ []);
|
|
43309
|
+
this.showImageDialog = signal(false, ...ngDevMode ? [{ debugName: "showImageDialog" }] : /* istanbul ignore next */ []);
|
|
43310
|
+
this.imageUrl = signal("", ...ngDevMode ? [{ debugName: "imageUrl" }] : /* istanbul ignore next */ []);
|
|
43311
|
+
this.imageAlt = signal("", ...ngDevMode ? [{ debugName: "imageAlt" }] : /* istanbul ignore next */ []);
|
|
43312
|
+
this.showTestSendDialog = signal(false, ...ngDevMode ? [{ debugName: "showTestSendDialog" }] : /* istanbul ignore next */ []);
|
|
43313
|
+
this.testSendEmail = signal("", ...ngDevMode ? [{ debugName: "testSendEmail" }] : /* istanbul ignore next */ []);
|
|
43314
|
+
this.showAiPanel = signal(false, ...ngDevMode ? [{ debugName: "showAiPanel" }] : /* istanbul ignore next */ []);
|
|
43315
|
+
this.aiPrompt = signal("", ...ngDevMode ? [{ debugName: "aiPrompt" }] : /* istanbul ignore next */ []);
|
|
43316
|
+
this.showAssignmentConfig = signal(false, ...ngDevMode ? [{ debugName: "showAssignmentConfig" }] : /* istanbul ignore next */ []);
|
|
43317
|
+
this.configTarget = signal(null, ...ngDevMode ? [{ debugName: "configTarget" }] : /* istanbul ignore next */ []);
|
|
43318
|
+
this.configEmail = signal(false, ...ngDevMode ? [{ debugName: "configEmail" }] : /* istanbul ignore next */ []);
|
|
43319
|
+
this.configText = signal(false, ...ngDevMode ? [{ debugName: "configText" }] : /* istanbul ignore next */ []);
|
|
43320
|
+
this.activeEditorTab = signal("edit", ...ngDevMode ? [{ debugName: "activeEditorTab" }] : /* istanbul ignore next */ []);
|
|
43321
|
+
this.activeRightTab = signal("blocks", ...ngDevMode ? [{ debugName: "activeRightTab" }] : /* istanbul ignore next */ []);
|
|
43322
|
+
this.blockSearch = signal("", ...ngDevMode ? [{ debugName: "blockSearch" }] : /* istanbul ignore next */ []);
|
|
43323
|
+
this.assignmentSearch = signal("", ...ngDevMode ? [{ debugName: "assignmentSearch" }] : /* istanbul ignore next */ []);
|
|
43324
|
+
this.toggledGroups = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "toggledGroups" }] : /* istanbul ignore next */ []);
|
|
43325
|
+
this.videoAutoplay = signal(false, ...ngDevMode ? [{ debugName: "videoAutoplay" }] : /* istanbul ignore next */ []);
|
|
43326
|
+
this.videoCaption = signal("", ...ngDevMode ? [{ debugName: "videoCaption" }] : /* istanbul ignore next */ []);
|
|
43327
|
+
this.videoCtaType = signal("", ...ngDevMode ? [{ debugName: "videoCtaType" }] : /* istanbul ignore next */ []);
|
|
43328
|
+
this.videoCtaUrl = signal("", ...ngDevMode ? [{ debugName: "videoCtaUrl" }] : /* istanbul ignore next */ []);
|
|
43329
|
+
this.videoCtaLabel = signal("", ...ngDevMode ? [{ debugName: "videoCtaLabel" }] : /* istanbul ignore next */ []);
|
|
43330
|
+
this.videoHideReactions = signal(false, ...ngDevMode ? [{ debugName: "videoHideReactions" }] : /* istanbul ignore next */ []);
|
|
43331
|
+
this.canWrite = computed(() => this.fullAccess() && !this.systemTemplate(), ...ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []);
|
|
43332
|
+
this.canCopySystem = computed(() => this.fullAccess() && this.systemTemplate() && !!this.editingId(), ...ngDevMode ? [{ debugName: "canCopySystem" }] : /* istanbul ignore next */ []);
|
|
43333
|
+
this.filteredAssignments = computed(() => {
|
|
43334
|
+
const featured = this.featuredAssignmentKey();
|
|
43335
|
+
const all = this.assignmentState();
|
|
43336
|
+
const ordered = featured ? [...all.filter((a) => a.key === featured), ...all.filter((a) => a.key !== featured)] : all;
|
|
43337
|
+
const q = this.assignmentSearch().trim().toLowerCase();
|
|
43338
|
+
return q ? ordered.filter((a) => a.name.toLowerCase().includes(q) || a.description.toLowerCase().includes(q)) : ordered;
|
|
43339
|
+
}, ...ngDevMode ? [{ debugName: "filteredAssignments" }] : /* istanbul ignore next */ []);
|
|
43340
|
+
this.filteredBlockGroups = computed(() => {
|
|
43341
|
+
const q = this.blockSearch().trim().toLowerCase();
|
|
43342
|
+
const groups = this.blockGroups();
|
|
43343
|
+
if (!q) return groups;
|
|
43344
|
+
return groups.map((g) => ({
|
|
43345
|
+
...g,
|
|
43346
|
+
blocks: g.blocks.filter((b) => b.label.toLowerCase().includes(q))
|
|
43347
|
+
})).filter((g) => g.blocks.length > 0);
|
|
43348
|
+
}, ...ngDevMode ? [{ debugName: "filteredBlockGroups" }] : /* istanbul ignore next */ []);
|
|
43349
|
+
this.serialized = computed(() => this.serialize(this.blocks()), ...ngDevMode ? [{ debugName: "serialized" }] : /* istanbul ignore next */ []);
|
|
43350
|
+
this.previewHtml = this.serialized;
|
|
43351
|
+
this.bodyLength = computed(() => this.serialized().trim().length, ...ngDevMode ? [{ debugName: "bodyLength" }] : /* istanbul ignore next */ []);
|
|
43352
|
+
this.bodyHasContent = computed(() => this.blocks().some((b) => blockHasContent(b.html)), ...ngDevMode ? [{ debugName: "bodyHasContent" }] : /* istanbul ignore next */ []);
|
|
43353
|
+
this.bodyUnchanged = computed(() => this.loadedBody() !== null && !this.bodyEdited(), ...ngDevMode ? [{ debugName: "bodyUnchanged" }] : /* istanbul ignore next */ []);
|
|
43354
|
+
this.nameError = computed(() => {
|
|
43355
|
+
const v = this.name().trim();
|
|
43356
|
+
if (!v) return "Template name is required.";
|
|
43357
|
+
return v.length > this.maxTextLength() ? `Name is too long (max ${this.maxTextLength()} characters).` : "";
|
|
43358
|
+
}, ...ngDevMode ? [{ debugName: "nameError" }] : /* istanbul ignore next */ []);
|
|
43359
|
+
this.subjectError = computed(() => {
|
|
43360
|
+
const v = this.subject().trim();
|
|
43361
|
+
if (!v) return "Subject line is required.";
|
|
43362
|
+
return v.length > this.maxTextLength() ? `Subject is too long (max ${this.maxTextLength()} characters).` : "";
|
|
43363
|
+
}, ...ngDevMode ? [{ debugName: "subjectError" }] : /* istanbul ignore next */ []);
|
|
43364
|
+
this.bodyError = computed(() => {
|
|
43365
|
+
if (!this.bodyHasContent()) return "Add a message: the email needs some text or an image.";
|
|
43366
|
+
const n = this.bodyLength();
|
|
43367
|
+
const max = this.maxBodyLength();
|
|
43368
|
+
return n > max && !this.bodyUnchanged() ? `Message is too long (max ${max.toLocaleString("en-US")} characters; it is ${n.toLocaleString("en-US")}).` : "";
|
|
43369
|
+
}, ...ngDevMode ? [{ debugName: "bodyError" }] : /* istanbul ignore next */ []);
|
|
43370
|
+
this.datesError = computed(() => {
|
|
43371
|
+
if (this.useAllYear() || !this.activeFrom() || !this.activeTo()) return "";
|
|
43372
|
+
return this.activeTo() < this.activeFrom() ? "Active To must be on or after Active From." : "";
|
|
43373
|
+
}, ...ngDevMode ? [{ debugName: "datesError" }] : /* istanbul ignore next */ []);
|
|
43374
|
+
this.canSave = computed(() => !this.nameError() && !this.subjectError() && !this.bodyError() && !this.datesError(), ...ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []);
|
|
43375
|
+
this.saveDisabled = computed(() => !this.canWrite() || !this.canSave(), ...ngDevMode ? [{ debugName: "saveDisabled" }] : /* istanbul ignore next */ []);
|
|
43376
|
+
this.normalizedLink = computed(() => {
|
|
43377
|
+
const url = this.linkUrl().trim();
|
|
43378
|
+
if (!url) return "";
|
|
43379
|
+
return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("mailto:") ? url : "https://" + url;
|
|
43380
|
+
}, ...ngDevMode ? [{ debugName: "normalizedLink" }] : /* istanbul ignore next */ []);
|
|
43381
|
+
this.linkUrlError = computed(() => {
|
|
43382
|
+
const url = this.normalizedLink();
|
|
43383
|
+
if (!url) return "";
|
|
43384
|
+
if (url.startsWith("mailto:")) return /^mailto:[^\s@"<>]+@[^\s@"<>]+\.[^\s@"<>]+$/.test(url) ? "" : "Enter a valid email address.";
|
|
43385
|
+
return isWebUrl(url) ? "" : "Enter a valid web address, like https://example.com.";
|
|
43386
|
+
}, ...ngDevMode ? [{ debugName: "linkUrlError" }] : /* istanbul ignore next */ []);
|
|
43387
|
+
this.canInsertLink = computed(() => !!this.linkUrl().trim() && !this.linkUrlError(), ...ngDevMode ? [{ debugName: "canInsertLink" }] : /* istanbul ignore next */ []);
|
|
43388
|
+
this.imageUrlError = computed(() => {
|
|
43389
|
+
const url = this.imageUrl().trim();
|
|
43390
|
+
return !url || isWebUrl(url) ? "" : "Enter a valid image address, like https://example.com/image.jpg.";
|
|
43391
|
+
}, ...ngDevMode ? [{ debugName: "imageUrlError" }] : /* istanbul ignore next */ []);
|
|
43392
|
+
this.canInsertImage = computed(() => !!this.imageUrl().trim() && !this.imageUrlError(), ...ngDevMode ? [{ debugName: "canInsertImage" }] : /* istanbul ignore next */ []);
|
|
43393
|
+
this.testEmailError = computed(() => {
|
|
43394
|
+
const email = this.testSendEmail().trim();
|
|
43395
|
+
return !email || isValidEmailAddress(email) ? "" : "Enter a valid email address.";
|
|
43396
|
+
}, ...ngDevMode ? [{ debugName: "testEmailError" }] : /* istanbul ignore next */ []);
|
|
43397
|
+
this.canSendTest = computed(() => this.canWrite() && !!this.editingId() && !!this.testSendEmail().trim() && !this.testEmailError() && !this.testSending(), ...ngDevMode ? [{ debugName: "canSendTest" }] : /* istanbul ignore next */ []);
|
|
43398
|
+
this.hasUnsavedChanges = computed(() => !!this.editingId() && this.savedSnapshot() !== null && this.savedSnapshot() !== this.snapshot(), ...ngDevMode ? [{ debugName: "hasUnsavedChanges" }] : /* istanbul ignore next */ []);
|
|
43399
|
+
this.canGenerate = computed(() => !!this.aiPrompt().trim() && !this.aiGenerating(), ...ngDevMode ? [{ debugName: "canGenerate" }] : /* istanbul ignore next */ []);
|
|
43400
|
+
effect(() => {
|
|
43401
|
+
const t = this.template();
|
|
43402
|
+
untracked(() => this.applyTemplate(t));
|
|
43403
|
+
});
|
|
43404
|
+
effect(() => {
|
|
43405
|
+
const d = this.videoDefaults();
|
|
43406
|
+
untracked(() => {
|
|
43407
|
+
this.videoAutoplay.set(d.autoplay);
|
|
43408
|
+
this.videoCaption.set(d.caption);
|
|
43409
|
+
this.videoCtaType.set(d.ctaType);
|
|
43410
|
+
this.videoCtaUrl.set(d.ctaUrl);
|
|
43411
|
+
this.videoCtaLabel.set(d.ctaLabel);
|
|
43412
|
+
this.videoHideReactions.set(d.hideReactions);
|
|
43413
|
+
});
|
|
43414
|
+
});
|
|
43415
|
+
effect(() => {
|
|
43416
|
+
if (this.showLinkDialog() || this.showImageDialog()) afterNextRender(() => this.host.nativeElement.querySelector(".link-dialog input")?.focus(), { injector: this.injector });
|
|
43417
|
+
});
|
|
43418
|
+
afterEveryRender(() => {
|
|
43419
|
+
const current = this.blocks();
|
|
43420
|
+
for (const ref of this.blockEditors()) {
|
|
43421
|
+
const el = ref.nativeElement;
|
|
43422
|
+
const blockId = el.dataset["blockId"];
|
|
43423
|
+
if (blockId && !this.initializedBlockIds.has(blockId)) {
|
|
43424
|
+
const block = current.find((b) => b.id === blockId);
|
|
43425
|
+
if (block !== void 0) {
|
|
43426
|
+
el.innerHTML = block.html;
|
|
43427
|
+
if (block.spacing) el.style.padding = `${block.spacing} 0`;
|
|
43428
|
+
this.initializedBlockIds.add(blockId);
|
|
43429
|
+
}
|
|
43430
|
+
}
|
|
43431
|
+
}
|
|
43432
|
+
});
|
|
43433
|
+
}
|
|
43434
|
+
markSaved(id = this.editingId()) {
|
|
43435
|
+
this.syncActiveBlockHtml();
|
|
43436
|
+
this.editingId.set(id);
|
|
43437
|
+
this.savedSnapshot.set(this.snapshot());
|
|
43438
|
+
}
|
|
43439
|
+
closeTestSend() {
|
|
43440
|
+
this.showTestSendDialog.set(false);
|
|
43441
|
+
}
|
|
43442
|
+
addGeneratedHtml(html) {
|
|
43443
|
+
const block = {
|
|
43444
|
+
id: this.genId(),
|
|
43445
|
+
html: this.stripBlockWrappers(html)
|
|
43446
|
+
};
|
|
43447
|
+
this.blocks.update((b) => [...b, block]);
|
|
43448
|
+
this.bodyEdited.set(true);
|
|
43449
|
+
this.selectedBlockId.set(block.id);
|
|
43450
|
+
this.showAiPanel.set(false);
|
|
43451
|
+
this.toast.show("AI content added to your email");
|
|
43452
|
+
}
|
|
43453
|
+
applyTemplate(t) {
|
|
43454
|
+
this.initializedBlockIds = /* @__PURE__ */ new Set();
|
|
43455
|
+
this.editingId.set(t?.id ?? null);
|
|
43456
|
+
this.name.set(t?.name ?? "");
|
|
43457
|
+
this.subject.set(t?.subject ?? "");
|
|
43458
|
+
this.category.set(t?.category ?? "");
|
|
43459
|
+
this.useAllYear.set(t?.useAllYear ?? true);
|
|
43460
|
+
this.activeFrom.set(t?.activeFrom ?? "");
|
|
43461
|
+
this.activeTo.set(t?.activeTo ?? "");
|
|
43462
|
+
this.videoId.set(t?.videoId ?? null);
|
|
43463
|
+
this.videoTitle.set(t?.videoTitle ?? null);
|
|
43464
|
+
this.blocks.set(!t || !t.id && !(t.body ?? "").trim() ? [{
|
|
43465
|
+
id: this.genId(),
|
|
43466
|
+
html: `<p>${EMAIL_TEMPLATE_START_TEXT}</p>`
|
|
43467
|
+
}] : this.htmlToBlocks(t.body ?? ""));
|
|
43468
|
+
this.selectedBlockId.set(null);
|
|
43469
|
+
this.blockSpacing.set("");
|
|
43470
|
+
this.loadedBody.set(t?.id ? t.body ?? "" : null);
|
|
43471
|
+
this.bodyEdited.set(false);
|
|
43472
|
+
this.assignmentChannels.set(Object.fromEntries((t?.assignments ?? []).map((c) => [c.key, {
|
|
43473
|
+
email: !!c.email,
|
|
43474
|
+
text: !!c.text
|
|
43475
|
+
}])));
|
|
43476
|
+
this.savedSnapshot.set(t?.id ? this.snapshot() : null);
|
|
43477
|
+
}
|
|
43478
|
+
onSave() {
|
|
43479
|
+
if (this.saving() || !this.canSave() || !this.canWrite()) return;
|
|
43480
|
+
const keepStoredBody = this.bodyUnchanged() && this.bodyLength() > this.maxBodyLength();
|
|
43481
|
+
this.syncActiveBlockHtml();
|
|
43482
|
+
this.save.emit({
|
|
43483
|
+
name: this.name(),
|
|
43484
|
+
subject: this.subject(),
|
|
43485
|
+
body: keepStoredBody ? this.loadedBody() ?? "" : this.serialized(),
|
|
43486
|
+
category: this.category(),
|
|
43487
|
+
useAllYear: this.useAllYear(),
|
|
43488
|
+
activeFrom: !this.useAllYear() && this.activeFrom() ? this.activeFrom() : null,
|
|
43489
|
+
activeTo: !this.useAllYear() && this.activeTo() ? this.activeTo() : null,
|
|
43490
|
+
videoId: this.videoId(),
|
|
43491
|
+
assignments: this.assignmentState().filter((a) => a.email || a.text).map((a) => ({
|
|
43492
|
+
key: a.key,
|
|
43493
|
+
email: a.email,
|
|
43494
|
+
text: a.text
|
|
43495
|
+
}))
|
|
43496
|
+
});
|
|
43497
|
+
}
|
|
43498
|
+
snapshot() {
|
|
43499
|
+
return JSON.stringify([
|
|
43500
|
+
this.name(),
|
|
43501
|
+
this.subject(),
|
|
43502
|
+
this.category(),
|
|
43503
|
+
this.useAllYear(),
|
|
43504
|
+
this.activeFrom(),
|
|
43505
|
+
this.activeTo(),
|
|
43506
|
+
this.serialized()
|
|
43507
|
+
]);
|
|
43508
|
+
}
|
|
43509
|
+
serialize(blocks) {
|
|
43510
|
+
return blocks.map((b) => {
|
|
43511
|
+
return `<div data-block="true">${b.spacing ? `<div data-spacing="true" style="padding:${b.spacing} 0">${b.html}</div>` : b.html}</div>`;
|
|
43512
|
+
}).join("");
|
|
43513
|
+
}
|
|
43514
|
+
htmlToBlocks(html) {
|
|
43515
|
+
if (!html || !html.trim()) return [{
|
|
43516
|
+
id: this.genId(),
|
|
43517
|
+
html: "<p></p>"
|
|
43518
|
+
}];
|
|
43519
|
+
const div = new DOMParser().parseFromString(html, "text/html").body;
|
|
43520
|
+
const blockDivs = Array.from(div.querySelectorAll(":scope > [data-block]"));
|
|
43521
|
+
if (blockDivs.length > 0) return blockDivs.map((el) => {
|
|
43522
|
+
const spacingEl = el.querySelector(":scope > [data-spacing]");
|
|
43523
|
+
if (spacingEl) {
|
|
43524
|
+
const spacing = spacingEl.style.padding?.split(" ")[0] || void 0;
|
|
43525
|
+
return {
|
|
43526
|
+
id: this.genId(),
|
|
43527
|
+
html: spacingEl.innerHTML,
|
|
43528
|
+
spacing
|
|
43529
|
+
};
|
|
43530
|
+
}
|
|
43531
|
+
return {
|
|
43532
|
+
id: this.genId(),
|
|
43533
|
+
html: el.innerHTML
|
|
43534
|
+
};
|
|
43535
|
+
});
|
|
43536
|
+
return [{
|
|
43537
|
+
id: this.genId(),
|
|
43538
|
+
html
|
|
43539
|
+
}];
|
|
43540
|
+
}
|
|
43541
|
+
stripBlockWrappers(html) {
|
|
43542
|
+
if (!html || !html.includes("data-block")) return html;
|
|
43543
|
+
return html.replace(/<\/div>\s*(?=<div[^>]*data-block)/g, "").replace(/<div[^>]*data-block[^>]*>/g, "").replace(/<\/div>\s*$/, "");
|
|
43544
|
+
}
|
|
43545
|
+
genId() {
|
|
43546
|
+
return Math.random().toString(36).slice(2, 10);
|
|
43547
|
+
}
|
|
43548
|
+
editorFor(id) {
|
|
43549
|
+
return id ? this.blockEditors().find((r) => r.nativeElement.dataset["blockId"] === id) : void 0;
|
|
43550
|
+
}
|
|
43551
|
+
syncActiveBlockHtml() {
|
|
43552
|
+
const dom = /* @__PURE__ */ new Map();
|
|
43553
|
+
for (const ref of this.blockEditors()) {
|
|
43554
|
+
const id = ref.nativeElement.dataset["blockId"];
|
|
43555
|
+
if (id) dom.set(id, ref.nativeElement.innerHTML);
|
|
43556
|
+
}
|
|
43557
|
+
if (dom.size === 0) return;
|
|
43558
|
+
this.blocks.update((list) => list.map((b) => dom.has(b.id) && dom.get(b.id) !== b.html ? {
|
|
43559
|
+
...b,
|
|
43560
|
+
html: dom.get(b.id)
|
|
43561
|
+
} : b));
|
|
43562
|
+
}
|
|
43563
|
+
syncBlock() {
|
|
43564
|
+
const id = this.selectedBlockId();
|
|
43565
|
+
const ref = this.editorFor(id);
|
|
43566
|
+
if (!id || !ref) return;
|
|
43567
|
+
const html = ref.nativeElement.innerHTML;
|
|
43568
|
+
this.blocks.update((list) => list.map((b) => b.id === id ? {
|
|
43569
|
+
...b,
|
|
43570
|
+
html
|
|
43571
|
+
} : b));
|
|
43572
|
+
}
|
|
43573
|
+
setEditorTab(tab) {
|
|
43574
|
+
const previous = this.activeEditorTab();
|
|
43575
|
+
if (tab !== "edit") this.syncActiveBlockHtml();
|
|
43576
|
+
if (tab === "edit" && previous !== "edit") this.initializedBlockIds = /* @__PURE__ */ new Set();
|
|
43577
|
+
this.activeEditorTab.set(tab);
|
|
43578
|
+
}
|
|
43579
|
+
addBlock(blockKey = "text-block") {
|
|
43580
|
+
if (!this.canWrite()) return;
|
|
43581
|
+
this.addBlockHtml(this.blockHtml(blockKey));
|
|
43582
|
+
}
|
|
43583
|
+
addBlockHtml(html, at) {
|
|
43584
|
+
this.bodyEdited.set(true);
|
|
43585
|
+
const block = {
|
|
43586
|
+
id: this.genId(),
|
|
43587
|
+
html
|
|
43588
|
+
};
|
|
43589
|
+
this.blocks.update((list) => at === void 0 ? [...list, block] : [
|
|
43590
|
+
...list.slice(0, at),
|
|
43591
|
+
block,
|
|
43592
|
+
...list.slice(at)
|
|
43593
|
+
]);
|
|
43594
|
+
this.selectedBlockId.set(block.id);
|
|
43595
|
+
return block;
|
|
43596
|
+
}
|
|
43597
|
+
deleteBlock(index) {
|
|
43598
|
+
if (!this.canWrite()) return;
|
|
43599
|
+
const list = this.blocks();
|
|
43600
|
+
if (index < 0 || index >= list.length) return;
|
|
43601
|
+
if (list.length === 1) {
|
|
43602
|
+
const ref = this.editorFor(list[0].id);
|
|
43603
|
+
const alreadyEmpty = list[0].html === "<p></p>" && (!ref || ref.nativeElement.innerHTML === "<p></p>");
|
|
43604
|
+
this.blocks.set([{
|
|
43605
|
+
...list[0],
|
|
43606
|
+
html: "<p></p>"
|
|
43607
|
+
}]);
|
|
43608
|
+
if (ref) ref.nativeElement.innerHTML = "<p></p>";
|
|
43609
|
+
if (!alreadyEmpty) this.bodyEdited.set(true);
|
|
43610
|
+
return;
|
|
43611
|
+
}
|
|
43612
|
+
this.bodyEdited.set(true);
|
|
43613
|
+
this.initializedBlockIds.delete(list[index].id);
|
|
43614
|
+
const next = list.filter((_, i) => i !== index);
|
|
43615
|
+
this.blocks.set(next);
|
|
43616
|
+
this.selectedBlockId.set(next[Math.min(index, next.length - 1)].id);
|
|
43617
|
+
}
|
|
43618
|
+
duplicateBlock(index) {
|
|
43619
|
+
if (!this.canWrite()) return;
|
|
43620
|
+
const list = this.blocks();
|
|
43621
|
+
if (index < 0 || index >= list.length) return;
|
|
43622
|
+
this.bodyEdited.set(true);
|
|
43623
|
+
const src = list[index];
|
|
43624
|
+
const html = this.editorFor(src.id)?.nativeElement.innerHTML ?? src.html;
|
|
43625
|
+
const copy = {
|
|
43626
|
+
id: this.genId(),
|
|
43627
|
+
html,
|
|
43628
|
+
spacing: src.spacing
|
|
43629
|
+
};
|
|
43630
|
+
this.blocks.set([
|
|
43631
|
+
...list.slice(0, index + 1),
|
|
43632
|
+
copy,
|
|
43633
|
+
...list.slice(index + 1)
|
|
43634
|
+
]);
|
|
43635
|
+
this.selectedBlockId.set(copy.id);
|
|
43636
|
+
}
|
|
43637
|
+
moveBlock(index, direction) {
|
|
43638
|
+
if (!this.canWrite()) return;
|
|
43639
|
+
const list = this.blocks();
|
|
43640
|
+
const target = index + direction;
|
|
43641
|
+
if (index < 0 || index >= list.length || target < 0 || target >= list.length) return;
|
|
43642
|
+
this.bodyEdited.set(true);
|
|
43643
|
+
const next = [...list];
|
|
43644
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
43645
|
+
this.blocks.set(next);
|
|
43646
|
+
}
|
|
43647
|
+
onBlockInput(event, block) {
|
|
43648
|
+
if (!this.canWrite()) return;
|
|
43649
|
+
this.bodyEdited.set(true);
|
|
43650
|
+
const html = event.target.innerHTML;
|
|
43651
|
+
this.blocks.update((list) => list.map((b) => b.id === block.id ? {
|
|
43652
|
+
...b,
|
|
43653
|
+
html
|
|
43654
|
+
} : b));
|
|
43655
|
+
}
|
|
43656
|
+
onBlockBeforeInput(event) {
|
|
43657
|
+
if (!this.canWrite()) {
|
|
43658
|
+
event.preventDefault();
|
|
43659
|
+
return;
|
|
43660
|
+
}
|
|
43661
|
+
if (event.inputType.startsWith("delete") || event.inputType.startsWith("history")) return;
|
|
43662
|
+
if (isBlockDrag(event)) {
|
|
43663
|
+
event.preventDefault();
|
|
43664
|
+
return;
|
|
43665
|
+
}
|
|
43666
|
+
const incoming = event.data ?? event.dataTransfer?.getData("text/plain") ?? "";
|
|
43667
|
+
if (!incoming) return;
|
|
43668
|
+
const sel = window.getSelection();
|
|
43669
|
+
const replaced = sel && !sel.isCollapsed && insideEditorBlock(sel.anchorNode) ? sel.toString().length : 0;
|
|
43670
|
+
if (this.bodyLength() - replaced + escapeHtml(incoming).length > this.maxBodyLength()) event.preventDefault();
|
|
43671
|
+
}
|
|
43672
|
+
onBlockFocus(block) {
|
|
43673
|
+
this.selectedBlockId.set(block.id);
|
|
43674
|
+
this.blockSpacing.set(block.spacing ?? "");
|
|
43675
|
+
}
|
|
43676
|
+
onPaletteDragStart(event, block) {
|
|
43677
|
+
if (!this.canWrite()) {
|
|
43678
|
+
event.preventDefault();
|
|
43679
|
+
return;
|
|
43680
|
+
}
|
|
43681
|
+
this.dragFromPalette = block;
|
|
43682
|
+
this.dragFromCanvas.set(null);
|
|
43683
|
+
event.dataTransfer.effectAllowed = "copy";
|
|
43684
|
+
event.dataTransfer.setData(EMAIL_BLOCK_DRAG_TYPE, block.key);
|
|
43685
|
+
}
|
|
43686
|
+
onBlockDragStart(event, index, wrapper) {
|
|
43687
|
+
if (!this.canWrite()) {
|
|
43688
|
+
event.preventDefault();
|
|
43689
|
+
return;
|
|
43690
|
+
}
|
|
43691
|
+
if (event.target !== event.currentTarget) return;
|
|
43692
|
+
this.dragFromCanvas.set(index);
|
|
43693
|
+
this.dragFromPalette = null;
|
|
43694
|
+
event.dataTransfer.effectAllowed = "move";
|
|
43695
|
+
event.dataTransfer.setData(EMAIL_BLOCK_DRAG_TYPE, String(index));
|
|
43696
|
+
if (wrapper) event.dataTransfer.setDragImage?.(wrapper, 0, 0);
|
|
43697
|
+
event.stopPropagation();
|
|
43698
|
+
}
|
|
43699
|
+
onBlockDragEnd() {
|
|
43700
|
+
this.dragFromCanvas.set(null);
|
|
43701
|
+
this.dragFromPalette = null;
|
|
43702
|
+
this.dragOverIndex.set(null);
|
|
43703
|
+
}
|
|
43704
|
+
onCanvasDragOver(event) {
|
|
43705
|
+
if (this.canWrite() && isBlockDrag(event)) event.preventDefault();
|
|
43706
|
+
}
|
|
43707
|
+
onDropZoneDragOver(event, index) {
|
|
43708
|
+
if (!this.canWrite() || !isBlockDrag(event)) return;
|
|
43709
|
+
event.preventDefault();
|
|
43710
|
+
event.dataTransfer.dropEffect = this.dragFromPalette ? "copy" : "move";
|
|
43711
|
+
if (this.dragOverIndex() !== index) this.dragOverIndex.set(index);
|
|
43712
|
+
}
|
|
43713
|
+
onDropZoneLeave() {
|
|
43714
|
+
this.dragOverIndex.set(null);
|
|
43715
|
+
}
|
|
43716
|
+
onDropZoneDrop(event, insertBefore) {
|
|
43717
|
+
if (!this.canWrite()) {
|
|
43718
|
+
event.preventDefault();
|
|
43719
|
+
return;
|
|
43720
|
+
}
|
|
43721
|
+
const fromCanvas = this.dragFromCanvas();
|
|
43722
|
+
if (this.dragFromPalette === null && fromCanvas === null) return;
|
|
43723
|
+
event.preventDefault();
|
|
43724
|
+
this.dragOverIndex.set(null);
|
|
43725
|
+
if (this.dragFromPalette) {
|
|
43726
|
+
const html = this.dragFromPalette.html;
|
|
43727
|
+
this.dragFromPalette = null;
|
|
43728
|
+
this.addBlockHtml(html, insertBefore);
|
|
43729
|
+
} else if (fromCanvas !== null) {
|
|
43730
|
+
const to = fromCanvas < insertBefore ? insertBefore - 1 : insertBefore;
|
|
43731
|
+
if (fromCanvas !== to) {
|
|
43732
|
+
const next = [...this.blocks()];
|
|
43733
|
+
const [moved] = next.splice(fromCanvas, 1);
|
|
43734
|
+
next.splice(to, 0, moved);
|
|
43735
|
+
this.blocks.set(next);
|
|
43736
|
+
this.bodyEdited.set(true);
|
|
43737
|
+
}
|
|
43738
|
+
this.dragFromCanvas.set(null);
|
|
43739
|
+
}
|
|
43740
|
+
}
|
|
43741
|
+
preventFocusLoss(event) {
|
|
43742
|
+
if (event.target?.closest?.("select, input")) return;
|
|
43743
|
+
event.preventDefault();
|
|
43744
|
+
}
|
|
43745
|
+
saveSelection() {
|
|
43746
|
+
const sel = window.getSelection();
|
|
43747
|
+
const range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
|
|
43748
|
+
this.savedRange = range && insideEditorBlock(range.commonAncestorContainer) ? range.cloneRange() : null;
|
|
43749
|
+
}
|
|
43750
|
+
restoreSelection() {
|
|
43751
|
+
const saved = this.savedRange;
|
|
43752
|
+
this.savedRange = null;
|
|
43753
|
+
const sel = window.getSelection();
|
|
43754
|
+
if (!!sel && sel.rangeCount > 0 && insideEditorBlock(sel.getRangeAt(0).commonAncestorContainer) || !saved || !saved.commonAncestorContainer.isConnected || !insideEditorBlock(saved.commonAncestorContainer)) return;
|
|
43755
|
+
sel?.removeAllRanges();
|
|
43756
|
+
sel?.addRange(saved);
|
|
43757
|
+
}
|
|
43758
|
+
activeBlockHtml() {
|
|
43759
|
+
return this.editorFor(this.selectedBlockId())?.nativeElement.innerHTML ?? null;
|
|
43760
|
+
}
|
|
43761
|
+
runEditCommand(run) {
|
|
43762
|
+
this.restoreSelection();
|
|
43763
|
+
const sel = window.getSelection();
|
|
43764
|
+
const overSelection = !!sel && sel.rangeCount > 0 && !sel.isCollapsed && insideEditorBlock(sel.getRangeAt(0).commonAncestorContainer);
|
|
43765
|
+
const before = this.activeBlockHtml();
|
|
43766
|
+
run();
|
|
43767
|
+
if (overSelection || before === null || before !== this.activeBlockHtml()) this.bodyEdited.set(true);
|
|
43768
|
+
}
|
|
43769
|
+
execCommand(cmd, value) {
|
|
43770
|
+
if (!this.canWrite()) return;
|
|
43771
|
+
this.runEditCommand(() => {
|
|
43772
|
+
document.execCommand(cmd, false, value ?? void 0);
|
|
43773
|
+
});
|
|
43774
|
+
this.syncBlock();
|
|
43775
|
+
}
|
|
43776
|
+
onHeading(value) {
|
|
43777
|
+
this.execCommand("formatBlock", value);
|
|
43778
|
+
this.headingKey.update((k) => k + 1);
|
|
43779
|
+
}
|
|
43780
|
+
onFontSize(value) {
|
|
43781
|
+
this.applyFontSize(value);
|
|
43782
|
+
this.sizeKey.update((k) => k + 1);
|
|
43783
|
+
}
|
|
43784
|
+
applyFontSize(size) {
|
|
43785
|
+
if (!this.canWrite() || !size) return;
|
|
43786
|
+
this.restoreSelection();
|
|
43787
|
+
const sel = window.getSelection();
|
|
43788
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
43789
|
+
const range = sel.getRangeAt(0);
|
|
43790
|
+
if (range.collapsed || !insideEditorBlock(range.commonAncestorContainer)) return;
|
|
43791
|
+
this.bodyEdited.set(true);
|
|
43792
|
+
try {
|
|
43793
|
+
const fragment = range.extractContents();
|
|
43794
|
+
const span = document.createElement("span");
|
|
43795
|
+
span.style.fontSize = size;
|
|
43796
|
+
span.appendChild(fragment);
|
|
43797
|
+
range.insertNode(span);
|
|
43798
|
+
sel.collapse(span, 1);
|
|
43799
|
+
} catch {
|
|
43800
|
+
document.execCommand("insertHTML", false, `<span style="font-size:${escapeHtml(size)}">${escapeHtml(sel.toString())}</span>`);
|
|
43801
|
+
}
|
|
43802
|
+
this.syncBlock();
|
|
43803
|
+
}
|
|
43804
|
+
applyTextColor(color) {
|
|
43805
|
+
if (!this.canWrite()) return;
|
|
43806
|
+
this.currentColor.set(color);
|
|
43807
|
+
this.runEditCommand(() => {
|
|
43808
|
+
document.execCommand("styleWithCSS", false, "true");
|
|
43809
|
+
document.execCommand("foreColor", false, color);
|
|
43810
|
+
});
|
|
43811
|
+
this.syncBlock();
|
|
43812
|
+
}
|
|
43813
|
+
openColorPicker() {
|
|
43814
|
+
this.saveSelection();
|
|
43815
|
+
this.colorInputRef()?.nativeElement.click();
|
|
43816
|
+
}
|
|
43817
|
+
applySpacing(value) {
|
|
43818
|
+
if (!this.canWrite()) return;
|
|
43819
|
+
this.blockSpacing.set(value);
|
|
43820
|
+
const id = this.selectedBlockId();
|
|
43821
|
+
if (!id) return;
|
|
43822
|
+
const ref = this.editorFor(id);
|
|
43823
|
+
if (ref) ref.nativeElement.style.padding = value ? `${value} 0` : "";
|
|
43824
|
+
const block = this.blocks().find((b) => b.id === id);
|
|
43825
|
+
if (!block) return;
|
|
43826
|
+
const changed = (block.spacing ?? "") !== (value ?? "");
|
|
43827
|
+
this.blocks.update((list) => list.map((b) => b.id === id ? {
|
|
43828
|
+
...b,
|
|
43829
|
+
spacing: value || void 0
|
|
43830
|
+
} : b));
|
|
43831
|
+
if (changed) this.bodyEdited.set(true);
|
|
43832
|
+
}
|
|
43833
|
+
openLinkDialog() {
|
|
43834
|
+
if (!this.canWrite()) return;
|
|
43835
|
+
this.saveSelection();
|
|
43836
|
+
const selected = window.getSelection()?.toString() || "";
|
|
43837
|
+
this.linkText.set(selected);
|
|
43838
|
+
this.linkTextFromSelection.set(!!selected);
|
|
43839
|
+
this.linkUrl.set("");
|
|
43840
|
+
this.showLinkDialog.set(true);
|
|
43841
|
+
}
|
|
43842
|
+
insertLink() {
|
|
43843
|
+
if (!this.canWrite() || !this.canInsertLink()) return;
|
|
43844
|
+
this.bodyEdited.set(true);
|
|
43845
|
+
const url = this.normalizedLink();
|
|
43846
|
+
this.restoreSelection();
|
|
43847
|
+
const sel = window.getSelection();
|
|
43848
|
+
if (sel && sel.rangeCount > 0 && !sel.isCollapsed) {
|
|
43849
|
+
document.execCommand("createLink", false, url);
|
|
43850
|
+
const range = sel.getRangeAt(0);
|
|
43851
|
+
(range.commonAncestorContainer instanceof Element ? range.commonAncestorContainer : range.commonAncestorContainer.parentElement)?.querySelectorAll("a").forEach((l) => {
|
|
43852
|
+
if (l.getAttribute("href") === url) l.setAttribute("target", "_blank");
|
|
43853
|
+
});
|
|
43854
|
+
} else {
|
|
43855
|
+
const a = document.createElement("a");
|
|
43856
|
+
a.setAttribute("href", url);
|
|
43857
|
+
a.setAttribute("target", "_blank");
|
|
43858
|
+
a.textContent = this.linkText().trim() || url;
|
|
43859
|
+
document.execCommand("insertHTML", false, a.outerHTML);
|
|
43860
|
+
}
|
|
43861
|
+
this.syncBlock();
|
|
43862
|
+
this.showLinkDialog.set(false);
|
|
43863
|
+
this.linkUrl.set("");
|
|
43864
|
+
this.linkText.set("");
|
|
43865
|
+
}
|
|
43866
|
+
openImageDialog() {
|
|
43867
|
+
if (!this.canWrite()) return;
|
|
43868
|
+
this.saveSelection();
|
|
43869
|
+
this.imageUrl.set("");
|
|
43870
|
+
this.imageAlt.set("");
|
|
43871
|
+
this.showImageDialog.set(true);
|
|
43872
|
+
}
|
|
43873
|
+
insertImageFromUrl() {
|
|
43874
|
+
if (!this.canWrite() || !this.canInsertImage()) return;
|
|
43875
|
+
this.bodyEdited.set(true);
|
|
43876
|
+
this.restoreSelection();
|
|
43877
|
+
const img = document.createElement("img");
|
|
43878
|
+
img.setAttribute("src", this.imageUrl().trim());
|
|
43879
|
+
img.setAttribute("alt", this.imageAlt() || "Image");
|
|
43880
|
+
img.setAttribute("style", "max-width:100%;border-radius:8px;display:block;margin:8px 0");
|
|
43881
|
+
document.execCommand("insertHTML", false, img.outerHTML);
|
|
43882
|
+
this.syncBlock();
|
|
43883
|
+
this.showImageDialog.set(false);
|
|
43884
|
+
this.imageUrl.set("");
|
|
43885
|
+
this.imageAlt.set("");
|
|
43886
|
+
}
|
|
43887
|
+
openTestSendDialog() {
|
|
43888
|
+
if (!this.canWrite()) return;
|
|
43889
|
+
this.testSendEmail.set("");
|
|
43890
|
+
this.showTestSendDialog.set(true);
|
|
43891
|
+
}
|
|
43892
|
+
onTestSend() {
|
|
43893
|
+
if (!this.canSendTest()) return;
|
|
43894
|
+
this.testSend.emit(this.testSendEmail().trim());
|
|
43895
|
+
}
|
|
43896
|
+
openAiPanel() {
|
|
43897
|
+
if (!this.canWrite() || !this.aiEnabled()) return;
|
|
43898
|
+
this.aiPrompt.set("");
|
|
43899
|
+
this.showAiPanel.set(true);
|
|
43900
|
+
}
|
|
43901
|
+
onAiGenerate() {
|
|
43902
|
+
if (!this.canWrite() || !this.canGenerate()) return;
|
|
43903
|
+
this.aiGenerate.emit(this.aiPrompt());
|
|
43904
|
+
}
|
|
43905
|
+
openVideoTab() {
|
|
43906
|
+
this.activeRightTab.set("video");
|
|
43907
|
+
this.videosRequested.emit();
|
|
43908
|
+
}
|
|
43909
|
+
insertVideo(video) {
|
|
43910
|
+
if (!this.canWrite()) return;
|
|
43911
|
+
this.videoId.set(video.id);
|
|
43912
|
+
this.videoTitle.set(video.title);
|
|
43913
|
+
const link = this.videoLink()(video, {
|
|
43914
|
+
autoplay: this.videoAutoplay(),
|
|
43915
|
+
caption: this.videoCaption(),
|
|
43916
|
+
ctaType: this.videoCtaType(),
|
|
43917
|
+
ctaUrl: this.videoCtaUrl(),
|
|
43918
|
+
ctaLabel: this.videoCtaLabel(),
|
|
43919
|
+
hideReactions: this.videoHideReactions()
|
|
43920
|
+
});
|
|
43921
|
+
const caption = this.videoCaption() || (this.videoAutoplay() ? "Video will autoplay" : "Click to watch video");
|
|
43922
|
+
this.addBlockHtml(buildVideoBlockHtml(video, link, caption, window.location.origin));
|
|
43923
|
+
this.activeRightTab.set("blocks");
|
|
43924
|
+
}
|
|
43925
|
+
formatDuration(seconds) {
|
|
43926
|
+
return formatVideoDuration(seconds);
|
|
43927
|
+
}
|
|
43928
|
+
categoryLabel(cat) {
|
|
43929
|
+
return formatCategoryLabel(cat);
|
|
43930
|
+
}
|
|
43931
|
+
openAssignmentConfig(item) {
|
|
43932
|
+
if (!this.canWrite()) return;
|
|
43933
|
+
this.configTarget.set(item);
|
|
43934
|
+
this.configEmail.set(item.email);
|
|
43935
|
+
this.configText.set(item.text);
|
|
43936
|
+
this.showAssignmentConfig.set(true);
|
|
43937
|
+
}
|
|
43938
|
+
saveAssignmentConfig() {
|
|
43939
|
+
const target = this.configTarget();
|
|
43940
|
+
if (!this.canWrite() || !target) return;
|
|
43941
|
+
const email = this.configEmail();
|
|
43942
|
+
const text = this.configText();
|
|
43943
|
+
this.assignmentChannels.update((m) => ({
|
|
43944
|
+
...m,
|
|
43945
|
+
[target.key]: {
|
|
43946
|
+
email,
|
|
43947
|
+
text
|
|
43948
|
+
}
|
|
43949
|
+
}));
|
|
43950
|
+
this.showAssignmentConfig.set(false);
|
|
43951
|
+
this.toast.show(`${target.name} automation configured`);
|
|
43952
|
+
}
|
|
43953
|
+
isOpen(id) {
|
|
43954
|
+
return id === "v:" + (this.variableGroups()[0]?.name ?? "") !== this.toggledGroups().has(id);
|
|
43955
|
+
}
|
|
43956
|
+
toggleGroup(id) {
|
|
43957
|
+
this.toggledGroups.update((s) => {
|
|
43958
|
+
const next = new Set(s);
|
|
43959
|
+
if (next.has(id)) next.delete(id);
|
|
43960
|
+
else next.add(id);
|
|
43961
|
+
return next;
|
|
43962
|
+
});
|
|
43963
|
+
}
|
|
43964
|
+
insertBlock(block) {
|
|
43965
|
+
if (!this.canWrite()) return;
|
|
43966
|
+
this.addBlockHtml(block.html);
|
|
43967
|
+
}
|
|
43968
|
+
insertVariable(variable) {
|
|
43969
|
+
if (!this.canWrite()) return;
|
|
43970
|
+
this.runEditCommand(() => {
|
|
43971
|
+
document.execCommand("insertText", false, `{{${variable.key}}}`);
|
|
43972
|
+
});
|
|
43973
|
+
this.syncBlock();
|
|
43974
|
+
}
|
|
43975
|
+
blockHtml(key) {
|
|
43976
|
+
for (const g of this.blockGroups()) {
|
|
43977
|
+
const found = g.blocks.find((b) => b.key === key);
|
|
43978
|
+
if (found) return found.html;
|
|
43979
|
+
}
|
|
43980
|
+
return "<p>Block</p>";
|
|
43981
|
+
}
|
|
43982
|
+
static {
|
|
43983
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
43984
|
+
minVersion: "12.0.0",
|
|
43985
|
+
version: "22.2.2",
|
|
43986
|
+
ngImport: i0,
|
|
43987
|
+
type: EmailTemplateEditorComponent,
|
|
43988
|
+
deps: [],
|
|
43989
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
43990
|
+
});
|
|
43991
|
+
}
|
|
43992
|
+
static {
|
|
43993
|
+
this.ɵcmp = i0.ɵɵngDeclareComponent({
|
|
43994
|
+
minVersion: "17.0.0",
|
|
43995
|
+
version: "22.2.2",
|
|
43996
|
+
type: EmailTemplateEditorComponent,
|
|
43997
|
+
isStandalone: true,
|
|
43998
|
+
selector: "ui-email-template-editor",
|
|
43999
|
+
inputs: {
|
|
44000
|
+
template: {
|
|
44001
|
+
classPropertyName: "template",
|
|
44002
|
+
publicName: "template",
|
|
44003
|
+
isSignal: true,
|
|
44004
|
+
isRequired: false,
|
|
44005
|
+
transformFunction: null
|
|
44006
|
+
},
|
|
44007
|
+
categories: {
|
|
44008
|
+
classPropertyName: "categories",
|
|
44009
|
+
publicName: "categories",
|
|
44010
|
+
isSignal: true,
|
|
44011
|
+
isRequired: false,
|
|
44012
|
+
transformFunction: null
|
|
44013
|
+
},
|
|
44014
|
+
categoryPlaceholder: {
|
|
44015
|
+
classPropertyName: "categoryPlaceholder",
|
|
44016
|
+
publicName: "categoryPlaceholder",
|
|
44017
|
+
isSignal: true,
|
|
44018
|
+
isRequired: false,
|
|
44019
|
+
transformFunction: null
|
|
44020
|
+
},
|
|
44021
|
+
assignmentOptions: {
|
|
44022
|
+
classPropertyName: "assignmentOptions",
|
|
44023
|
+
publicName: "assignmentOptions",
|
|
44024
|
+
isSignal: true,
|
|
44025
|
+
isRequired: false,
|
|
44026
|
+
transformFunction: null
|
|
44027
|
+
},
|
|
44028
|
+
featuredAssignmentKey: {
|
|
44029
|
+
classPropertyName: "featuredAssignmentKey",
|
|
44030
|
+
publicName: "featuredAssignmentKey",
|
|
44031
|
+
isSignal: true,
|
|
44032
|
+
isRequired: false,
|
|
44033
|
+
transformFunction: null
|
|
44034
|
+
},
|
|
44035
|
+
assignmentsDescription: {
|
|
44036
|
+
classPropertyName: "assignmentsDescription",
|
|
44037
|
+
publicName: "assignmentsDescription",
|
|
44038
|
+
isSignal: true,
|
|
44039
|
+
isRequired: false,
|
|
44040
|
+
transformFunction: null
|
|
44041
|
+
},
|
|
44042
|
+
textEnabled: {
|
|
44043
|
+
classPropertyName: "textEnabled",
|
|
44044
|
+
publicName: "textEnabled",
|
|
44045
|
+
isSignal: true,
|
|
44046
|
+
isRequired: false,
|
|
44047
|
+
transformFunction: null
|
|
44048
|
+
},
|
|
44049
|
+
variableGroups: {
|
|
44050
|
+
classPropertyName: "variableGroups",
|
|
44051
|
+
publicName: "variableGroups",
|
|
44052
|
+
isSignal: true,
|
|
44053
|
+
isRequired: false,
|
|
44054
|
+
transformFunction: null
|
|
44055
|
+
},
|
|
44056
|
+
blockGroups: {
|
|
44057
|
+
classPropertyName: "blockGroups",
|
|
44058
|
+
publicName: "blockGroups",
|
|
44059
|
+
isSignal: true,
|
|
44060
|
+
isRequired: false,
|
|
44061
|
+
transformFunction: null
|
|
44062
|
+
},
|
|
44063
|
+
aiExamples: {
|
|
44064
|
+
classPropertyName: "aiExamples",
|
|
44065
|
+
publicName: "aiExamples",
|
|
44066
|
+
isSignal: true,
|
|
44067
|
+
isRequired: false,
|
|
44068
|
+
transformFunction: null
|
|
44069
|
+
},
|
|
44070
|
+
namePlaceholder: {
|
|
44071
|
+
classPropertyName: "namePlaceholder",
|
|
44072
|
+
publicName: "namePlaceholder",
|
|
44073
|
+
isSignal: true,
|
|
44074
|
+
isRequired: false,
|
|
44075
|
+
transformFunction: null
|
|
44076
|
+
},
|
|
44077
|
+
subjectPlaceholder: {
|
|
44078
|
+
classPropertyName: "subjectPlaceholder",
|
|
44079
|
+
publicName: "subjectPlaceholder",
|
|
44080
|
+
isSignal: true,
|
|
44081
|
+
isRequired: false,
|
|
44082
|
+
transformFunction: null
|
|
44083
|
+
},
|
|
44084
|
+
backLabel: {
|
|
44085
|
+
classPropertyName: "backLabel",
|
|
44086
|
+
publicName: "backLabel",
|
|
44087
|
+
isSignal: true,
|
|
44088
|
+
isRequired: false,
|
|
44089
|
+
transformFunction: null
|
|
44090
|
+
},
|
|
44091
|
+
createLabel: {
|
|
44092
|
+
classPropertyName: "createLabel",
|
|
44093
|
+
publicName: "createLabel",
|
|
44094
|
+
isSignal: true,
|
|
44095
|
+
isRequired: false,
|
|
44096
|
+
transformFunction: null
|
|
44097
|
+
},
|
|
44098
|
+
editLabel: {
|
|
44099
|
+
classPropertyName: "editLabel",
|
|
44100
|
+
publicName: "editLabel",
|
|
44101
|
+
isSignal: true,
|
|
44102
|
+
isRequired: false,
|
|
44103
|
+
transformFunction: null
|
|
44104
|
+
},
|
|
44105
|
+
systemNote: {
|
|
44106
|
+
classPropertyName: "systemNote",
|
|
44107
|
+
publicName: "systemNote",
|
|
44108
|
+
isSignal: true,
|
|
44109
|
+
isRequired: false,
|
|
44110
|
+
transformFunction: null
|
|
44111
|
+
},
|
|
44112
|
+
readOnlyNote: {
|
|
44113
|
+
classPropertyName: "readOnlyNote",
|
|
44114
|
+
publicName: "readOnlyNote",
|
|
44115
|
+
isSignal: true,
|
|
44116
|
+
isRequired: false,
|
|
44117
|
+
transformFunction: null
|
|
44118
|
+
},
|
|
44119
|
+
idPrefix: {
|
|
44120
|
+
classPropertyName: "idPrefix",
|
|
44121
|
+
publicName: "idPrefix",
|
|
44122
|
+
isSignal: true,
|
|
44123
|
+
isRequired: false,
|
|
44124
|
+
transformFunction: null
|
|
44125
|
+
},
|
|
44126
|
+
maxBodyLength: {
|
|
44127
|
+
classPropertyName: "maxBodyLength",
|
|
44128
|
+
publicName: "maxBodyLength",
|
|
44129
|
+
isSignal: true,
|
|
44130
|
+
isRequired: false,
|
|
44131
|
+
transformFunction: null
|
|
44132
|
+
},
|
|
44133
|
+
maxTextLength: {
|
|
44134
|
+
classPropertyName: "maxTextLength",
|
|
44135
|
+
publicName: "maxTextLength",
|
|
44136
|
+
isSignal: true,
|
|
44137
|
+
isRequired: false,
|
|
44138
|
+
transformFunction: null
|
|
44139
|
+
},
|
|
44140
|
+
fullAccess: {
|
|
44141
|
+
classPropertyName: "fullAccess",
|
|
44142
|
+
publicName: "fullAccess",
|
|
44143
|
+
isSignal: true,
|
|
44144
|
+
isRequired: false,
|
|
44145
|
+
transformFunction: null
|
|
44146
|
+
},
|
|
44147
|
+
systemTemplate: {
|
|
44148
|
+
classPropertyName: "systemTemplate",
|
|
44149
|
+
publicName: "systemTemplate",
|
|
44150
|
+
isSignal: true,
|
|
44151
|
+
isRequired: false,
|
|
44152
|
+
transformFunction: null
|
|
44153
|
+
},
|
|
44154
|
+
aiEnabled: {
|
|
44155
|
+
classPropertyName: "aiEnabled",
|
|
44156
|
+
publicName: "aiEnabled",
|
|
44157
|
+
isSignal: true,
|
|
44158
|
+
isRequired: false,
|
|
44159
|
+
transformFunction: null
|
|
44160
|
+
},
|
|
44161
|
+
testSendEnabled: {
|
|
44162
|
+
classPropertyName: "testSendEnabled",
|
|
44163
|
+
publicName: "testSendEnabled",
|
|
44164
|
+
isSignal: true,
|
|
44165
|
+
isRequired: false,
|
|
44166
|
+
transformFunction: null
|
|
44167
|
+
},
|
|
44168
|
+
videosEnabled: {
|
|
44169
|
+
classPropertyName: "videosEnabled",
|
|
44170
|
+
publicName: "videosEnabled",
|
|
44171
|
+
isSignal: true,
|
|
44172
|
+
isRequired: false,
|
|
44173
|
+
transformFunction: null
|
|
44174
|
+
},
|
|
44175
|
+
saving: {
|
|
44176
|
+
classPropertyName: "saving",
|
|
44177
|
+
publicName: "saving",
|
|
44178
|
+
isSignal: true,
|
|
44179
|
+
isRequired: false,
|
|
44180
|
+
transformFunction: null
|
|
44181
|
+
},
|
|
44182
|
+
testSending: {
|
|
44183
|
+
classPropertyName: "testSending",
|
|
44184
|
+
publicName: "testSending",
|
|
44185
|
+
isSignal: true,
|
|
44186
|
+
isRequired: false,
|
|
44187
|
+
transformFunction: null
|
|
44188
|
+
},
|
|
44189
|
+
aiGenerating: {
|
|
44190
|
+
classPropertyName: "aiGenerating",
|
|
44191
|
+
publicName: "aiGenerating",
|
|
44192
|
+
isSignal: true,
|
|
44193
|
+
isRequired: false,
|
|
44194
|
+
transformFunction: null
|
|
44195
|
+
},
|
|
44196
|
+
videos: {
|
|
44197
|
+
classPropertyName: "videos",
|
|
44198
|
+
publicName: "videos",
|
|
44199
|
+
isSignal: true,
|
|
44200
|
+
isRequired: false,
|
|
44201
|
+
transformFunction: null
|
|
44202
|
+
},
|
|
44203
|
+
videosLoading: {
|
|
44204
|
+
classPropertyName: "videosLoading",
|
|
44205
|
+
publicName: "videosLoading",
|
|
44206
|
+
isSignal: true,
|
|
44207
|
+
isRequired: false,
|
|
44208
|
+
transformFunction: null
|
|
44209
|
+
},
|
|
44210
|
+
videoDefaults: {
|
|
44211
|
+
classPropertyName: "videoDefaults",
|
|
44212
|
+
publicName: "videoDefaults",
|
|
44213
|
+
isSignal: true,
|
|
44214
|
+
isRequired: false,
|
|
44215
|
+
transformFunction: null
|
|
44216
|
+
},
|
|
44217
|
+
videoLink: {
|
|
44218
|
+
classPropertyName: "videoLink",
|
|
44219
|
+
publicName: "videoLink",
|
|
44220
|
+
isSignal: true,
|
|
44221
|
+
isRequired: false,
|
|
44222
|
+
transformFunction: null
|
|
44223
|
+
},
|
|
44224
|
+
noVideosHint: {
|
|
44225
|
+
classPropertyName: "noVideosHint",
|
|
44226
|
+
publicName: "noVideosHint",
|
|
44227
|
+
isSignal: true,
|
|
44228
|
+
isRequired: false,
|
|
44229
|
+
transformFunction: null
|
|
44230
|
+
}
|
|
44231
|
+
},
|
|
44232
|
+
outputs: {
|
|
44233
|
+
back: "back",
|
|
44234
|
+
save: "save",
|
|
44235
|
+
testSend: "testSend",
|
|
44236
|
+
aiGenerate: "aiGenerate",
|
|
44237
|
+
videosRequested: "videosRequested",
|
|
44238
|
+
useAsMine: "useAsMine"
|
|
44239
|
+
},
|
|
44240
|
+
viewQueries: [{
|
|
44241
|
+
propertyName: "blockEditors",
|
|
44242
|
+
predicate: ["blockEditor"],
|
|
44243
|
+
descendants: true,
|
|
44244
|
+
isSignal: true
|
|
44245
|
+
}, {
|
|
44246
|
+
propertyName: "colorInputRef",
|
|
44247
|
+
first: true,
|
|
44248
|
+
predicate: ["colorInput"],
|
|
44249
|
+
descendants: true,
|
|
44250
|
+
isSignal: true
|
|
44251
|
+
}],
|
|
44252
|
+
ngImport: i0,
|
|
44253
|
+
template: `
|
|
44254
|
+
<div class="editor-page">
|
|
44255
|
+
<!-- Top Bar -->
|
|
44256
|
+
<div class="editor-topbar">
|
|
44257
|
+
<div class="topbar-left">
|
|
44258
|
+
<ui-button variant="ghost" size="sm" ariaLabel="Go back" (click)="back.emit()">
|
|
44259
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>
|
|
44260
|
+
</ui-button>
|
|
44261
|
+
<nav class="breadcrumb" aria-label="Breadcrumb">
|
|
44262
|
+
<a href="#" (click)="$event.preventDefault(); back.emit()">{{ backLabel() }}</a>
|
|
44263
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>
|
|
44264
|
+
<span>{{ isNew() ? createLabel() : editLabel() }}</span>
|
|
44265
|
+
</nav>
|
|
44266
|
+
</div>
|
|
44267
|
+
<div class="topbar-right">
|
|
44268
|
+
@if (canWrite() && testSendEnabled()) {
|
|
44269
|
+
<ui-button variant="secondary" size="sm" (click)="openTestSendDialog()">
|
|
44270
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
44271
|
+
Test Send
|
|
44272
|
+
</ui-button>
|
|
44273
|
+
}
|
|
44274
|
+
<ui-button variant="primary" size="sm" [loading]="saving()" [disabled]="saveDisabled()" (click)="onSave()">
|
|
44275
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
|
44276
|
+
Save Template
|
|
44277
|
+
</ui-button>
|
|
44278
|
+
</div>
|
|
44279
|
+
</div>
|
|
44280
|
+
|
|
44281
|
+
@if (systemTemplate()) {
|
|
44282
|
+
<div class="readonly-note system-note" role="note" data-testid="editor-system-note">
|
|
44283
|
+
<span>{{ systemNote() }}</span>
|
|
44284
|
+
@if (canCopySystem()) {
|
|
44285
|
+
<ui-button variant="secondary" size="sm" data-testid="editor-use-as-mine" (click)="useAsMine.emit()">Use as My Template</ui-button>
|
|
44286
|
+
}
|
|
44287
|
+
</div>
|
|
44288
|
+
} @else if (!canWrite()) {
|
|
44289
|
+
<p class="readonly-note" role="note" data-testid="editor-readonly-note">{{ readOnlyNote() }}</p>
|
|
44290
|
+
}
|
|
44291
|
+
|
|
44292
|
+
<!-- 3-Column Layout -->
|
|
44293
|
+
<div class="editor-body">
|
|
44294
|
+
<!-- LEFT PANEL: Details -->
|
|
44295
|
+
<aside class="panel-left" aria-label="Template details">
|
|
44296
|
+
<h3 class="panel-title">Details</h3>
|
|
44297
|
+
|
|
44298
|
+
<div class="form-field">
|
|
44299
|
+
<ui-text-input label="Name" [required]="true" [(ngModel)]="name" [readonly]="!canWrite()" [maxlength]="maxTextLength()"
|
|
44300
|
+
[placeholder]="namePlaceholder()" [error]="nameError()" />
|
|
44301
|
+
</div>
|
|
44302
|
+
|
|
44303
|
+
<div class="form-field">
|
|
44304
|
+
<ui-text-input label="Subject" [required]="true" [(ngModel)]="subject" [readonly]="!canWrite()" [maxlength]="maxTextLength()"
|
|
44305
|
+
[placeholder]="subjectPlaceholder()" [error]="subjectError()">
|
|
44306
|
+
@if (canWrite()) {
|
|
44307
|
+
<span uiLabelEnd><ng-content select="[uiSubjectAction]" /></span>
|
|
44308
|
+
}
|
|
44309
|
+
</ui-text-input>
|
|
44310
|
+
</div>
|
|
44311
|
+
|
|
44312
|
+
<div class="form-field">
|
|
44313
|
+
<ui-select label="Category" [options]="categories()" [placeholder]="categoryPlaceholder()" [(ngModel)]="category" [disabled]="!canWrite()" />
|
|
44314
|
+
</div>
|
|
44315
|
+
|
|
44316
|
+
<ui-checkbox label="Use this template all year" [(checked)]="useAllYear" [disabled]="!canWrite()" />
|
|
44317
|
+
|
|
44318
|
+
@if (!useAllYear()) {
|
|
44319
|
+
<div class="form-row-dates">
|
|
44320
|
+
<ui-date-input label="Active From" [(ngModel)]="activeFrom" [disabled]="!canWrite()" />
|
|
44321
|
+
<ui-date-input label="Active To" [(ngModel)]="activeTo" [disabled]="!canWrite()" [min]="activeFrom()" [error]="datesError()" />
|
|
44322
|
+
</div>
|
|
44323
|
+
}
|
|
44324
|
+
|
|
44325
|
+
<!-- Assignments -->
|
|
44326
|
+
@if (assignmentOptions().length > 0) {
|
|
44327
|
+
<div class="assignments-section">
|
|
44328
|
+
<h4 class="section-title">Assignments</h4>
|
|
44329
|
+
<p class="section-desc">{{ assignmentsDescription() }}</p>
|
|
44330
|
+
<ui-search-input placeholder="Search..." ariaLabel="Search assignments" [(value)]="assignmentSearch" />
|
|
44331
|
+
<div class="assignment-list">
|
|
44332
|
+
@for (item of filteredAssignments(); track item.key) {
|
|
44333
|
+
<div class="assignment-item" [class.assignment-readonly]="!canWrite()" [attr.role]="canWrite() ? 'button' : null" [attr.tabindex]="canWrite() ? 0 : null"
|
|
44334
|
+
[attr.aria-label]="canWrite() ? 'Configure ' + item.name : null"
|
|
44335
|
+
(click)="openAssignmentConfig(item)" (keydown.enter)="openAssignmentConfig(item); $event.preventDefault()" (keydown.space)="openAssignmentConfig(item); $event.preventDefault()">
|
|
44336
|
+
<div class="assignment-text">
|
|
44337
|
+
<strong>{{ item.name }}</strong>
|
|
44338
|
+
<span>{{ item.description }}</span>
|
|
44339
|
+
</div>
|
|
44340
|
+
<div class="assignment-channels">
|
|
44341
|
+
<span class="channel-badge" [class.active]="item.email" role="img" [attr.aria-label]="'Email ' + (item.email ? 'on' : 'off')">
|
|
44342
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
|
|
44343
|
+
</span>
|
|
44344
|
+
<span class="channel-badge" [class.active]="item.text" [class.disabled]="!textEnabled()" role="img" [attr.aria-label]="'Text ' + (item.text ? 'on' : 'off')">
|
|
44345
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
44346
|
+
</span>
|
|
44347
|
+
</div>
|
|
44348
|
+
</div>
|
|
44349
|
+
}
|
|
44350
|
+
</div>
|
|
44351
|
+
</div>
|
|
44352
|
+
}
|
|
44353
|
+
</aside>
|
|
44354
|
+
|
|
44355
|
+
<!-- CENTER: Editor -->
|
|
44356
|
+
<main class="panel-center">
|
|
44357
|
+
<div class="editor-tabs" role="tablist" aria-label="Editor view">
|
|
44358
|
+
<button type="button" class="editor-tab" role="tab" [class.active]="activeEditorTab() === 'edit'" [attr.aria-selected]="activeEditorTab() === 'edit'" (click)="setEditorTab('edit')">
|
|
44359
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
|
|
44360
|
+
Edit
|
|
44361
|
+
</button>
|
|
44362
|
+
<button type="button" class="editor-tab" role="tab" [class.active]="activeEditorTab() === 'preview'" [attr.aria-selected]="activeEditorTab() === 'preview'" (click)="setEditorTab('preview')">
|
|
44363
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
44364
|
+
Preview
|
|
44365
|
+
</button>
|
|
44366
|
+
<button type="button" class="editor-tab" role="tab" [class.active]="activeEditorTab() === 'mobile'" [attr.aria-selected]="activeEditorTab() === 'mobile'" (click)="setEditorTab('mobile')">
|
|
44367
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
|
44368
|
+
Mobile
|
|
44369
|
+
</button>
|
|
44370
|
+
</div>
|
|
44371
|
+
|
|
44372
|
+
@if (activeEditorTab() === 'edit') {
|
|
44373
|
+
@if (canWrite()) {
|
|
44374
|
+
<!-- Toolbar row 1: mousedown preventDefault keeps focus (and the selection) in the editor -->
|
|
44375
|
+
<div class="editor-toolbar" role="toolbar" aria-label="Text formatting" (mousedown)="preventFocusLoss($event)">
|
|
44376
|
+
<div class="toolbar-group">
|
|
44377
|
+
<ui-button variant="icon" size="sm" ariaLabel="Bold" uiTooltip="Bold" (click)="execCommand('bold')"><strong>B</strong></ui-button>
|
|
44378
|
+
<ui-button variant="icon" size="sm" ariaLabel="Italic" uiTooltip="Italic" (click)="execCommand('italic')"><em>I</em></ui-button>
|
|
44379
|
+
<ui-button variant="icon" size="sm" ariaLabel="Underline" uiTooltip="Underline" (click)="execCommand('underline')"><u>U</u></ui-button>
|
|
44380
|
+
</div>
|
|
44381
|
+
<div class="toolbar-group">
|
|
44382
|
+
@for (k of [headingKey()]; track k) {
|
|
44383
|
+
<ui-select class="toolbar-select" size="sm" ariaLabel="Heading level" [options]="headingOptions" ngModel="p"
|
|
44384
|
+
(mousedown)="saveSelection()" (ngModelChange)="onHeading($event)" />
|
|
44385
|
+
}
|
|
44386
|
+
</div>
|
|
44387
|
+
<div class="toolbar-group">
|
|
44388
|
+
@for (k of [sizeKey()]; track k) {
|
|
44389
|
+
<ui-select class="toolbar-select" size="sm" ariaLabel="Font size" placeholder="Size" [options]="fontSizeOptions" ngModel=""
|
|
44390
|
+
(mousedown)="saveSelection()" (ngModelChange)="onFontSize($event)" />
|
|
44391
|
+
}
|
|
44392
|
+
</div>
|
|
44393
|
+
<div class="toolbar-group">
|
|
44394
|
+
<ui-button variant="icon" size="sm" ariaLabel="Align left" uiTooltip="Align Left" (click)="execCommand('justifyLeft')">
|
|
44395
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg>
|
|
44396
|
+
</ui-button>
|
|
44397
|
+
<ui-button variant="icon" size="sm" ariaLabel="Align center" uiTooltip="Align Center" (click)="execCommand('justifyCenter')">
|
|
44398
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="10" x2="6" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="18" y1="18" x2="6" y2="18"/></svg>
|
|
44399
|
+
</ui-button>
|
|
44400
|
+
<ui-button variant="icon" size="sm" ariaLabel="Align right" uiTooltip="Align Right" (click)="execCommand('justifyRight')">
|
|
44401
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="21" y1="10" x2="7" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="7" y2="18"/></svg>
|
|
44402
|
+
</ui-button>
|
|
44403
|
+
</div>
|
|
44404
|
+
<div class="toolbar-group">
|
|
44405
|
+
<ui-button variant="icon" size="sm" ariaLabel="Bulleted list" uiTooltip="Bulleted List" (click)="execCommand('insertUnorderedList')">
|
|
44406
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
|
44407
|
+
</ui-button>
|
|
44408
|
+
<ui-button variant="icon" size="sm" ariaLabel="Numbered list" uiTooltip="Numbered List" (click)="execCommand('insertOrderedList')">
|
|
44409
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>
|
|
44410
|
+
</ui-button>
|
|
44411
|
+
</div>
|
|
44412
|
+
<div class="toolbar-group">
|
|
44413
|
+
<!-- The browser's own colour dialog is the picker; a button opens it so the toolbar keeps one look. -->
|
|
44414
|
+
<input #colorInput type="color" class="hidden-color-input" aria-label="Text color" tabindex="-1" [value]="currentColor()" (change)="applyTextColor($any($event.target).value)" (mousedown)="$event.stopPropagation()" />
|
|
44415
|
+
<ui-button variant="icon" size="sm" ariaLabel="Text color" uiTooltip="Text Color" (mousedown)="openColorPicker()">
|
|
44416
|
+
<span class="color-indicator">
|
|
44417
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><text y="20" font-size="20" font-weight="700">A</text></svg>
|
|
44418
|
+
<span class="color-bar" [style.background]="currentColor()"></span>
|
|
44419
|
+
</span>
|
|
44420
|
+
</ui-button>
|
|
44421
|
+
</div>
|
|
44422
|
+
<div class="toolbar-group">
|
|
44423
|
+
<ui-button variant="icon" size="sm" ariaLabel="Insert link" uiTooltip="Insert Link" (mousedown)="saveSelection()" (click)="openLinkDialog()">
|
|
44424
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
44425
|
+
</ui-button>
|
|
44426
|
+
<ui-button variant="icon" size="sm" ariaLabel="Insert image" uiTooltip="Insert Image" (mousedown)="saveSelection()" (click)="openImageDialog()">
|
|
44427
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
|
44428
|
+
</ui-button>
|
|
44429
|
+
</div>
|
|
44430
|
+
<div class="toolbar-group">
|
|
44431
|
+
<ui-button variant="icon" size="sm" ariaLabel="Undo" uiTooltip="Undo" (click)="execCommand('undo')">
|
|
44432
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6.69 3L3 13"/></svg>
|
|
44433
|
+
</ui-button>
|
|
44434
|
+
<ui-button variant="icon" size="sm" ariaLabel="Redo" uiTooltip="Redo" (click)="execCommand('redo')">
|
|
44435
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6.69 3L21 13"/></svg>
|
|
44436
|
+
</ui-button>
|
|
44437
|
+
</div>
|
|
44438
|
+
</div>
|
|
44439
|
+
|
|
44440
|
+
<!-- Toolbar row 2: block spacing + AI -->
|
|
44441
|
+
<div class="toolbar-row-2">
|
|
44442
|
+
<div class="toolbar-group" (mousedown)="preventFocusLoss($event)">
|
|
44443
|
+
<span class="toolbar-label" id="tpl-spacing-label">Block spacing</span>
|
|
44444
|
+
<ui-select class="toolbar-select" size="sm" ariaLabel="Block spacing" [options]="spacingOptions" [ngModel]="blockSpacing()" (ngModelChange)="applySpacing($event)" />
|
|
44445
|
+
</div>
|
|
44446
|
+
@if (aiEnabled()) {
|
|
44447
|
+
<ui-button variant="secondary" size="sm" (click)="openAiPanel()">
|
|
44448
|
+
<span class="ai-dot" aria-hidden="true"></span>
|
|
44449
|
+
Generate with AI
|
|
44450
|
+
</ui-button>
|
|
44451
|
+
}
|
|
44452
|
+
</div>
|
|
44453
|
+
}
|
|
44454
|
+
|
|
44455
|
+
<!-- Link dialog (inline popover) -->
|
|
44456
|
+
@if (canWrite() && showLinkDialog()) {
|
|
44457
|
+
<div class="link-dialog" role="group" aria-label="Insert link">
|
|
44458
|
+
<div class="link-dialog-inner">
|
|
44459
|
+
<span class="link-dialog-title">Insert Link</span>
|
|
44460
|
+
<ui-text-input class="link-dialog-input" ariaLabel="Link address" placeholder="https://example.com" [(ngModel)]="linkUrl" [error]="linkUrlError()" (keydown.enter)="insertLink()" />
|
|
44461
|
+
@if (!linkTextFromSelection()) {
|
|
44462
|
+
<ui-text-input class="link-dialog-input" ariaLabel="Link text" placeholder="Link text (optional)" [(ngModel)]="linkText" (keydown.enter)="insertLink()" />
|
|
44463
|
+
}
|
|
44464
|
+
<div class="link-dialog-actions">
|
|
44465
|
+
<ui-button variant="secondary" size="sm" (click)="showLinkDialog.set(false)">Cancel</ui-button>
|
|
44466
|
+
<ui-button variant="primary" size="sm" [disabled]="!canInsertLink()" (click)="insertLink()">Insert</ui-button>
|
|
44467
|
+
</div>
|
|
44468
|
+
</div>
|
|
44469
|
+
</div>
|
|
44470
|
+
}
|
|
44471
|
+
|
|
44472
|
+
<!-- Image URL dialog -->
|
|
44473
|
+
@if (canWrite() && showImageDialog()) {
|
|
44474
|
+
<div class="link-dialog" role="group" aria-label="Insert image">
|
|
44475
|
+
<div class="link-dialog-inner">
|
|
44476
|
+
<span class="link-dialog-title">Insert Image</span>
|
|
44477
|
+
<ui-text-input class="link-dialog-input" ariaLabel="Image address" placeholder="https://example.com/image.jpg" [(ngModel)]="imageUrl" [error]="imageUrlError()" (keydown.enter)="insertImageFromUrl()" />
|
|
44478
|
+
<ui-text-input class="link-dialog-input" ariaLabel="Image alt text" placeholder="Alt text (optional)" [(ngModel)]="imageAlt" (keydown.enter)="insertImageFromUrl()" />
|
|
44479
|
+
<div class="link-dialog-actions">
|
|
44480
|
+
<ui-button variant="secondary" size="sm" (click)="showImageDialog.set(false)">Cancel</ui-button>
|
|
44481
|
+
<ui-button variant="primary" size="sm" [disabled]="!canInsertImage()" (click)="insertImageFromUrl()">Insert</ui-button>
|
|
44482
|
+
</div>
|
|
44483
|
+
</div>
|
|
44484
|
+
</div>
|
|
44485
|
+
}
|
|
44486
|
+
|
|
44487
|
+
<ui-error [errorId]="idPrefix() + '-body-error'" [message]="bodyError()" />
|
|
44488
|
+
|
|
44489
|
+
<!-- Block Canvas with DnD -->
|
|
44490
|
+
<div class="blocks-canvas" (dragover)="onCanvasDragOver($event)" (drop)="onDropZoneDrop($event, blocks().length)">
|
|
44491
|
+
<!-- Initial drop zone (before first block) -->
|
|
44492
|
+
<div class="drop-zone" [class.active]="dragOverIndex() === 0" (dragover)="onDropZoneDragOver($event, 0)" (dragleave)="onDropZoneLeave()" (drop)="onDropZoneDrop($event, 0)">
|
|
44493
|
+
<span class="drop-zone-label">Drop here</span>
|
|
44494
|
+
</div>
|
|
44495
|
+
|
|
44496
|
+
@for (block of blocks(); track block.id; let i = $index) {
|
|
44497
|
+
<div class="block-wrapper" #blockWrapper [class.selected]="selectedBlockId() === block.id" [class.dragging]="dragFromCanvas() === i" (dragend)="onBlockDragEnd()">
|
|
44498
|
+
@if (canWrite()) {
|
|
44499
|
+
<!-- Only the handle is draggable: a draggable ancestor of the contenteditable breaks text selection in Firefox / Safari. Move up / down below are the keyboard and touch alternative. -->
|
|
44500
|
+
<div class="block-drag-handle" title="Drag to reorder" draggable="true" (dragstart)="onBlockDragStart($event, i, blockWrapper)">
|
|
44501
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>
|
|
44502
|
+
</div>
|
|
44503
|
+
}
|
|
44504
|
+
|
|
44505
|
+
<!-- Block content (contenteditable, initialised ONCE after render so Angular never rewrites it and the caret never jumps) -->
|
|
44506
|
+
<div #blockEditor class="block-content" role="textbox" aria-multiline="true" [attr.aria-label]="'Email block ' + (i + 1)"
|
|
44507
|
+
[attr.data-block-id]="block.id" [attr.contenteditable]="canWrite() ? 'true' : 'false'"
|
|
44508
|
+
(input)="onBlockInput($event, block)" (beforeinput)="onBlockBeforeInput($any($event))" (focus)="onBlockFocus(block)" (click)="selectedBlockId.set(block.id)"></div>
|
|
44509
|
+
|
|
44510
|
+
@if (canWrite()) {
|
|
44511
|
+
<div class="block-controls">
|
|
44512
|
+
<ui-button variant="icon" size="xs" ariaLabel="Move block up" uiTooltip="Move up" [disabled]="i === 0" (mousedown)="preventFocusLoss($event)" (click)="moveBlock(i, -1)">
|
|
44513
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M18 15l-6-6-6 6"/></svg>
|
|
44514
|
+
</ui-button>
|
|
44515
|
+
<ui-button variant="icon" size="xs" ariaLabel="Move block down" uiTooltip="Move down" [disabled]="i === blocks().length - 1" (mousedown)="preventFocusLoss($event)" (click)="moveBlock(i, 1)">
|
|
44516
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
|
44517
|
+
</ui-button>
|
|
44518
|
+
<ui-button variant="icon" size="xs" ariaLabel="Duplicate block" uiTooltip="Duplicate" (mousedown)="preventFocusLoss($event)" (click)="duplicateBlock(i)">
|
|
44519
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
44520
|
+
</ui-button>
|
|
44521
|
+
<ui-button variant="icon-danger" size="xs" ariaLabel="Delete block" uiTooltip="Delete block" (mousedown)="preventFocusLoss($event)" (click)="deleteBlock(i)">
|
|
44522
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/></svg>
|
|
44523
|
+
</ui-button>
|
|
44524
|
+
</div>
|
|
44525
|
+
}
|
|
44526
|
+
</div>
|
|
44527
|
+
|
|
44528
|
+
<!-- Drop zone after each block -->
|
|
44529
|
+
<div class="drop-zone" [class.active]="dragOverIndex() === i + 1" (dragover)="onDropZoneDragOver($event, i + 1)" (dragleave)="onDropZoneLeave()" (drop)="onDropZoneDrop($event, i + 1)">
|
|
44530
|
+
<span class="drop-zone-label">Drop here</span>
|
|
44531
|
+
</div>
|
|
44532
|
+
}
|
|
44533
|
+
|
|
44534
|
+
@if (canWrite()) {
|
|
44535
|
+
<button type="button" class="add-block-btn" (click)="addBlock('text-block')">
|
|
44536
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
44537
|
+
Add Block
|
|
44538
|
+
</button>
|
|
44539
|
+
}
|
|
44540
|
+
</div>
|
|
44541
|
+
}
|
|
44542
|
+
|
|
44543
|
+
@if (activeEditorTab() === 'preview') {
|
|
44544
|
+
<div class="preview-frame">
|
|
44545
|
+
<div class="preview-content" [innerHTML]="previewHtml()"></div>
|
|
44546
|
+
</div>
|
|
44547
|
+
}
|
|
44548
|
+
|
|
44549
|
+
@if (activeEditorTab() === 'mobile') {
|
|
44550
|
+
<div class="preview-frame mobile">
|
|
44551
|
+
<div class="mobile-shell">
|
|
44552
|
+
<div class="preview-content" [innerHTML]="previewHtml()"></div>
|
|
44553
|
+
</div>
|
|
44554
|
+
</div>
|
|
44555
|
+
}
|
|
44556
|
+
</main>
|
|
44557
|
+
|
|
44558
|
+
<!-- RIGHT PANEL: Blocks, Variables, Video -->
|
|
44559
|
+
<aside class="panel-right" aria-label="Blocks, variables and video">
|
|
44560
|
+
@if (!canWrite()) {
|
|
44561
|
+
<div class="right-panel-body" data-testid="tpl-readonly-tools-note">
|
|
44562
|
+
<h4 class="right-section-title">Read only</h4>
|
|
44563
|
+
<p class="right-section-hint">Blocks, variables and videos can only be added to a template you can edit.</p>
|
|
44564
|
+
</div>
|
|
44565
|
+
} @else {
|
|
44566
|
+
<div class="right-tabs" role="tablist" aria-label="Right panel sections">
|
|
44567
|
+
<button type="button" class="right-tab" role="tab" [class.active]="activeRightTab() === 'blocks'" [attr.aria-selected]="activeRightTab() === 'blocks'" (click)="activeRightTab.set('blocks')">
|
|
44568
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
|
44569
|
+
Blocks
|
|
44570
|
+
</button>
|
|
44571
|
+
<button type="button" class="right-tab" role="tab" [class.active]="activeRightTab() === 'variables'" [attr.aria-selected]="activeRightTab() === 'variables'" (click)="activeRightTab.set('variables')">
|
|
44572
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3v18M3 12h18"/></svg>
|
|
44573
|
+
Variables
|
|
44574
|
+
</button>
|
|
44575
|
+
@if (videosEnabled()) {
|
|
44576
|
+
<button type="button" class="right-tab" role="tab" [class.active]="activeRightTab() === 'video'" [attr.aria-selected]="activeRightTab() === 'video'" (click)="openVideoTab()">
|
|
44577
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
|
44578
|
+
Video
|
|
44579
|
+
</button>
|
|
44580
|
+
}
|
|
44581
|
+
</div>
|
|
44582
|
+
|
|
44583
|
+
@if (activeRightTab() === 'blocks') {
|
|
44584
|
+
<div class="right-panel-body">
|
|
44585
|
+
<h4 class="right-section-title">Content Blocks</h4>
|
|
44586
|
+
<p class="right-section-desc">Click to insert or drag into the editor.</p>
|
|
44587
|
+
<ui-search-input placeholder="Search blocks..." ariaLabel="Search blocks" [(value)]="blockSearch" />
|
|
44588
|
+
|
|
44589
|
+
@for (group of filteredBlockGroups(); track group.name) {
|
|
44590
|
+
<div class="block-group">
|
|
44591
|
+
<button type="button" class="block-group-header" [attr.aria-expanded]="isOpen('b:' + group.name)" (click)="toggleGroup('b:' + group.name)">
|
|
44592
|
+
<svg class="chevron" [class.expanded]="isOpen('b:' + group.name)" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>
|
|
44593
|
+
<span>{{ group.name }}</span>
|
|
44594
|
+
<span class="block-count">{{ group.blocks.length }}</span>
|
|
44595
|
+
</button>
|
|
44596
|
+
@if (isOpen('b:' + group.name)) {
|
|
44597
|
+
<div class="block-list">
|
|
44598
|
+
@for (block of group.blocks; track block.key) {
|
|
44599
|
+
<!-- A div, not a button: Firefox does not start a drag from a <button>. Role, tabindex and Enter / Space keep it a keyboard control; a click inserts the block. -->
|
|
44600
|
+
<div role="button" tabindex="0" class="block-item" draggable="true" [attr.aria-label]="'Insert ' + block.label"
|
|
44601
|
+
(dragstart)="onPaletteDragStart($event, block)" (dragend)="onBlockDragEnd()" (click)="insertBlock(block)"
|
|
44602
|
+
(keydown.enter)="insertBlock(block); $event.preventDefault()" (keydown.space)="insertBlock(block); $event.preventDefault()">
|
|
44603
|
+
<svg class="block-item-drag" width="10" height="10" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>
|
|
44604
|
+
{{ block.label }}
|
|
44605
|
+
</div>
|
|
44606
|
+
}
|
|
44607
|
+
</div>
|
|
44608
|
+
}
|
|
44609
|
+
</div>
|
|
44610
|
+
}
|
|
44611
|
+
</div>
|
|
44612
|
+
}
|
|
44613
|
+
|
|
44614
|
+
@if (activeRightTab() === 'variables') {
|
|
44615
|
+
<div class="right-panel-body">
|
|
44616
|
+
<h4 class="right-section-title">Template Variables</h4>
|
|
44617
|
+
<p class="right-section-desc">Click a variable to insert it at the cursor position.</p>
|
|
44618
|
+
|
|
44619
|
+
@for (group of variableGroups(); track group.name) {
|
|
44620
|
+
<div class="block-group">
|
|
44621
|
+
<button type="button" class="block-group-header" [attr.aria-expanded]="isOpen('v:' + group.name)" (click)="toggleGroup('v:' + group.name)">
|
|
44622
|
+
<svg class="chevron" [class.expanded]="isOpen('v:' + group.name)" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>
|
|
44623
|
+
<span>{{ group.name }}</span>
|
|
44624
|
+
<span class="block-count">{{ group.variables.length }}</span>
|
|
44625
|
+
</button>
|
|
44626
|
+
@if (isOpen('v:' + group.name)) {
|
|
44627
|
+
<div class="variable-list">
|
|
44628
|
+
@for (v of group.variables; track v.key) {
|
|
44629
|
+
<button type="button" class="variable-item" (mousedown)="saveSelection()" (click)="insertVariable(v)">
|
|
44630
|
+
<span class="var-tag">{{ '{{' + v.key + '}}' }}</span>
|
|
44631
|
+
<span class="var-label">{{ v.label }}</span>
|
|
44632
|
+
<span class="var-example">{{ v.example }}</span>
|
|
44633
|
+
</button>
|
|
44634
|
+
}
|
|
44635
|
+
</div>
|
|
44636
|
+
}
|
|
44637
|
+
</div>
|
|
44638
|
+
}
|
|
44639
|
+
</div>
|
|
44640
|
+
}
|
|
44641
|
+
|
|
44642
|
+
@if (activeRightTab() === 'video' && videosEnabled()) {
|
|
44643
|
+
<div class="right-panel-body video-library-panel">
|
|
44644
|
+
<h4 class="right-section-title">Video Library</h4>
|
|
44645
|
+
<p class="right-section-hint">Click a video to add it as a new block in your email.</p>
|
|
44646
|
+
|
|
44647
|
+
<div class="video-config-section">
|
|
44648
|
+
<ui-radio-group label="Playback" orientation="horizontal" [options]="playbackOptions" [ngModel]="videoAutoplay() ? 'autoplay' : 'click'" (ngModelChange)="videoAutoplay.set($event === 'autoplay')" />
|
|
44649
|
+
</div>
|
|
44650
|
+
<div class="video-config-section">
|
|
44651
|
+
<ui-text-input label="Caption" placeholder="e.g. Click to watch my video for you" [maxlength]="200" [(ngModel)]="videoCaption" />
|
|
44652
|
+
</div>
|
|
44653
|
+
<div class="video-config-section">
|
|
44654
|
+
<ui-select label="Call-to-Action" [options]="ctaTypeOptions" [(ngModel)]="videoCtaType" />
|
|
44655
|
+
@if (videoCtaType()) {
|
|
44656
|
+
<ui-text-input ariaLabel="Call-to-action address" type="url" placeholder="https://..." [(ngModel)]="videoCtaUrl" />
|
|
44657
|
+
<ui-text-input ariaLabel="Call-to-action button label" placeholder="Button label" [maxlength]="100" [(ngModel)]="videoCtaLabel" />
|
|
44658
|
+
}
|
|
44659
|
+
</div>
|
|
44660
|
+
<div class="video-config-section">
|
|
44661
|
+
<ui-checkbox label="Hide Reactions" [(checked)]="videoHideReactions" />
|
|
44662
|
+
</div>
|
|
44663
|
+
|
|
44664
|
+
@if (videosLoading()) {
|
|
44665
|
+
<div class="video-lib-loading" role="status" aria-busy="true" aria-label="Loading videos">
|
|
44666
|
+
@for (n of [1, 2, 3]; track n) {
|
|
44667
|
+
<div class="video-lib-skeleton">
|
|
44668
|
+
<ui-skeleton [height]="40" width="56px" [radius]="6" />
|
|
44669
|
+
<div class="vls-info"><ui-skeleton [height]="10" width="75%" [radius]="5" /><ui-skeleton [height]="10" width="45%" [radius]="5" /></div>
|
|
44670
|
+
</div>
|
|
44671
|
+
}
|
|
44672
|
+
</div>
|
|
44673
|
+
} @else if (videos().length === 0) {
|
|
44674
|
+
<div class="video-lib-empty">
|
|
44675
|
+
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.5" aria-hidden="true"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
|
44676
|
+
<p>No videos yet</p>
|
|
44677
|
+
<span>{{ noVideosHint() }}</span>
|
|
44678
|
+
</div>
|
|
44679
|
+
} @else {
|
|
44680
|
+
<div class="video-lib-list">
|
|
44681
|
+
@for (video of videos(); track video.id) {
|
|
44682
|
+
<button type="button" class="video-lib-item" [class.selected]="video.id === videoId()" [attr.aria-pressed]="video.id === videoId()" (click)="insertVideo(video)">
|
|
44683
|
+
<span class="vli-thumb">
|
|
44684
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.5" aria-hidden="true"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
|
44685
|
+
@if (video.durationSeconds) {
|
|
44686
|
+
<span class="vli-dur">{{ formatDuration(video.durationSeconds) }}</span>
|
|
44687
|
+
}
|
|
44688
|
+
</span>
|
|
44689
|
+
<span class="vli-info">
|
|
44690
|
+
<span class="vli-title">{{ video.title }}</span>
|
|
44691
|
+
<span class="vli-cat">{{ categoryLabel(video.category) }}</span>
|
|
44692
|
+
</span>
|
|
44693
|
+
@if (video.id === videoId()) {
|
|
44694
|
+
<svg class="vli-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2.5" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
|
|
44695
|
+
}
|
|
44696
|
+
</button>
|
|
44697
|
+
}
|
|
44698
|
+
</div>
|
|
44699
|
+
}
|
|
44700
|
+
</div>
|
|
44701
|
+
}
|
|
44702
|
+
}
|
|
44703
|
+
</aside>
|
|
44704
|
+
</div>
|
|
44705
|
+
</div>
|
|
44706
|
+
|
|
44707
|
+
<!-- Test Send -->
|
|
44708
|
+
<ui-side-panel [open]="showTestSendDialog()" title="Send Test Email" primaryLabel="Send Test" [primaryLoading]="testSending()" [primaryDisabled]="!canSendTest()"
|
|
44709
|
+
(closed)="showTestSendDialog.set(false)" (primaryClick)="onTestSend()">
|
|
44710
|
+
<p class="config-desc">Send this template to an email address to preview how it will look in an inbox.</p>
|
|
44711
|
+
@if (!editingId()) {
|
|
44712
|
+
<div class="test-save-note">
|
|
44713
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
44714
|
+
Save the template first before sending a test.
|
|
44715
|
+
</div>
|
|
44716
|
+
}
|
|
44717
|
+
<div class="config-field config-field-mt">
|
|
44718
|
+
<ui-text-input label="Recipient Email" type="email" placeholder="you@example.com" [(ngModel)]="testSendEmail" [error]="testEmailError()" (keydown.enter)="onTestSend()" />
|
|
44719
|
+
</div>
|
|
44720
|
+
@if (hasUnsavedChanges()) {
|
|
44721
|
+
<div class="test-save-note" role="note" data-testid="test-unsaved-note">
|
|
44722
|
+
You have unsaved changes. The test email sends the last saved version of this template.
|
|
44723
|
+
</div>
|
|
44724
|
+
}
|
|
44725
|
+
</ui-side-panel>
|
|
44726
|
+
|
|
44727
|
+
<!-- AI Generate -->
|
|
44728
|
+
<ui-side-panel [open]="showAiPanel()" title="Generate with AI" primaryLabel="Generate" [primaryLoading]="aiGenerating()" [primaryDisabled]="!canGenerate()"
|
|
44729
|
+
(closed)="showAiPanel.set(false)" (primaryClick)="onAiGenerate()">
|
|
44730
|
+
<p class="config-desc">Describe the email you want to write. The AI will generate professional content and add it as a new block.</p>
|
|
44731
|
+
<div class="ai-prompt-examples">
|
|
44732
|
+
<span class="ai-example-label">Try:</span>
|
|
44733
|
+
@for (ex of aiExamples(); track ex) {
|
|
44734
|
+
<ui-chip [label]="ex" size="sm" (select)="aiPrompt.set(ex)" />
|
|
44735
|
+
}
|
|
44736
|
+
</div>
|
|
44737
|
+
<div class="config-field config-field-mt">
|
|
44738
|
+
<ui-textarea-input label="What do you want to write?" [rows]="5" placeholder="e.g. A friendly follow-up email for a buyer who attended an open house last weekend" [(ngModel)]="aiPrompt" />
|
|
44739
|
+
</div>
|
|
44740
|
+
</ui-side-panel>
|
|
44741
|
+
|
|
44742
|
+
<!-- Assignment config -->
|
|
44743
|
+
<ui-side-panel [wide]="true" [open]="showAssignmentConfig()" [title]="configTarget()?.name || 'Configure Automation'" primaryLabel="Save Configuration"
|
|
44744
|
+
[primaryDisabled]="!configEmail() && !configText()" (closed)="showAssignmentConfig.set(false)" (primaryClick)="saveAssignmentConfig()">
|
|
44745
|
+
@if (configTarget(); as target) {
|
|
44746
|
+
<p class="config-desc">{{ target.description }}</p>
|
|
44747
|
+
<hr class="config-divider" />
|
|
44748
|
+
<div class="config-section">
|
|
44749
|
+
<span class="config-section-label">EMAIL TEMPLATE</span>
|
|
44750
|
+
<div class="config-field">
|
|
44751
|
+
<span class="config-label">Assigned Template</span>
|
|
44752
|
+
<div class="config-readonly">{{ name() || 'Current template' }}</div>
|
|
44753
|
+
<span class="config-hint">This template will be used when this automation is triggered.</span>
|
|
44754
|
+
</div>
|
|
44755
|
+
</div>
|
|
44756
|
+
<hr class="config-divider" />
|
|
44757
|
+
<div class="config-section">
|
|
44758
|
+
<span class="config-section-label">CHANNELS <span class="required" aria-hidden="true">*</span></span>
|
|
44759
|
+
<ui-checkbox label="Email" [(checked)]="configEmail" />
|
|
44760
|
+
<ui-checkbox label="Text (SMS)" [(checked)]="configText" [disabled]="!textEnabled()" [hint]="textEnabled() ? '' : '(not enabled)'" />
|
|
44761
|
+
</div>
|
|
44762
|
+
}
|
|
44763
|
+
</ui-side-panel>
|
|
44764
|
+
`,
|
|
44765
|
+
isInline: true,
|
|
44766
|
+
styles: [":host{display:block}.required{color:#ef4444;font-weight:400}.editor-page{display:flex;flex-direction:column;height:100vh;height:100dvh;background:#f8fafc;overflow:hidden}.editor-topbar{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;padding:12px 24px;background:#fff;border-bottom:1px solid #e2e8f0;flex-shrink:0}.topbar-left{display:flex;align-items:center;gap:8px;min-width:0}.topbar-right{display:flex;align-items:center;gap:8px}.breadcrumb{display:flex;align-items:center;gap:6px;font-size:.9375rem;font-weight:700;color:#1e293b;min-width:0}.breadcrumb a{color:#1e293b;text-decoration:none;cursor:pointer}.breadcrumb a:hover{color:#3b82f6}.breadcrumb svg{color:#94a3b8;flex-shrink:0}.breadcrumb span{color:#64748b;font-weight:500}.readonly-note{margin:0;padding:8px 16px;font-size:.8125rem;color:#92400e;background:#fffbeb;border-bottom:1px solid #fde68a}.readonly-note.system-note{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.editor-body{display:grid;grid-template-columns:340px minmax(0,1fr) 300px;flex:1;min-height:0;overflow:hidden}.panel-left{background:#fff;border-right:1px solid #e2e8f0;padding:24px;overflow-y:auto}.panel-title{font-size:1.0625rem;font-weight:700;color:#1e293b;margin:0 0 20px}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}.form-row-dates{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px}.assignments-section{border-top:1px solid #e2e8f0;padding-top:20px;margin-top:16px}.section-title{font-size:.9375rem;font-weight:700;color:#1e293b;margin:0 0 6px}.section-desc{font-size:.8125rem;color:#64748b;line-height:1.5;margin:0 0 12px}.assignment-list{max-height:340px;overflow-y:auto;margin-top:12px;border:1px solid #e2e8f0;border-radius:8px}.assignment-item{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 14px;cursor:pointer;border-bottom:1px solid #f1f5f9;transition:background .1s}.assignment-item:last-child{border-bottom:none}.assignment-item:hover{background:#f8fafc}.assignment-item:focus-visible{outline:2px solid #3b82f6;outline-offset:-2px}.assignment-item.assignment-readonly{cursor:default}.assignment-item.assignment-readonly:hover{background:transparent}.assignment-text{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}.assignment-text strong{font-size:.8125rem;color:#1e293b}.assignment-text span{font-size:.75rem;color:#64748b}.assignment-channels{display:flex;gap:4px;flex-shrink:0}.channel-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:4px;background:#f1f5f9;color:#94a3b8;transition:all .15s}.channel-badge.active{background:#3b82f6;color:#fff}.channel-badge.disabled{opacity:.4;cursor:not-allowed}.config-desc{font-size:.875rem;color:#64748b;line-height:1.5;margin:0}.config-divider{border:none;border-top:1px solid #e2e8f0;margin:20px 0}.config-section{display:flex;flex-direction:column;gap:12px}.config-section-label{font-size:.6875rem;font-weight:700;color:#3b82f6;text-transform:uppercase;letter-spacing:.04em}.config-field{display:flex;flex-direction:column;gap:6px}.config-field-mt{margin-top:16px}.config-label{font-size:.8125rem;font-weight:600;color:#1e293b}.config-readonly{padding:9px 12px;border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;font-size:.875rem;color:#1e293b}.config-hint{font-size:.75rem;color:#64748b}.test-save-note{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;font-size:.8125rem;color:#92400e;margin-top:12px}.test-save-note svg{flex-shrink:0}.ai-prompt-examples{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:12px}.ai-example-label{font-size:.75rem;color:#64748b;font-weight:600}.panel-center{display:flex;flex-direction:column;min-height:0;min-width:0;overflow:hidden}.editor-tabs{display:flex;gap:2px;padding:12px 20px 0;background:#fff;border-bottom:1px solid #e2e8f0;overflow-x:auto;-webkit-overflow-scrolling:touch}.editor-tab{display:flex;align-items:center;gap:6px;padding:10px 16px;border:1px solid #e2e8f0;border-bottom:none;border-radius:8px 8px 0 0;background:#f8fafc;font-size:.8125rem;font-weight:600;color:#64748b;cursor:pointer;font-family:inherit;white-space:nowrap}.editor-tab:hover{background:#fff;color:#1e293b}.editor-tab.active{background:#eff6ff;color:#3b82f6;border-color:#93c5fd}.editor-tab:focus-visible,.right-tab:focus-visible,.block-group-header:focus-visible,.variable-item:focus-visible,.add-block-btn:focus-visible,.block-item:focus-visible,.video-lib-item:focus-visible{outline:2px solid #3b82f6;outline-offset:-2px}.editor-toolbar{display:flex;align-items:center;gap:8px;padding:10px 20px;background:#fff;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}.toolbar-row-2{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 20px;background:#fff;border-bottom:1px solid #e2e8f0}.toolbar-group{display:flex;align-items:center;gap:2px;padding-right:8px;border-right:1px solid #e2e8f0}.toolbar-group:last-child{border-right:none;padding-right:0}.toolbar-select{display:block;min-width:96px}.toolbar-label{font-size:.75rem;font-weight:600;color:#64748b;white-space:nowrap;margin-right:6px}.ai-dot{width:8px;height:8px;border-radius:50%;background:#3b82f6;display:inline-block;margin-right:6px}.hidden-color-input{position:absolute;width:0;height:0;opacity:0;pointer-events:none;border:none;padding:0}.color-indicator{display:flex;flex-direction:column;align-items:center;gap:2px;color:#475569}.color-bar{display:block;width:16px;height:3px;border-radius:2px;border:1px solid rgba(0,0,0,.1)}.link-dialog{background:#fff;border-bottom:1px solid #e2e8f0;padding:12px 20px}.link-dialog-inner{display:flex;align-items:flex-start;gap:8px;flex-wrap:wrap}.link-dialog-title{font-size:.8125rem;font-weight:700;color:#1e293b;white-space:nowrap;padding-top:10px}.link-dialog-input{display:block;width:240px;max-width:100%}.link-dialog-actions{display:flex;gap:6px}.preview-frame{flex:1;display:flex;justify-content:center;padding:24px;overflow-y:auto;background:#f1f5f9}.preview-frame.mobile{align-items:flex-start}.preview-content{max-width:600px;width:100%;box-sizing:border-box;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:32px;min-height:400px;font-size:.9375rem;line-height:1.6;color:#1e293b;overflow-wrap:anywhere}.preview-content p{margin:0 0 8px}.mobile-shell{width:375px;max-width:100%;box-sizing:border-box;min-height:600px;border:2px solid #1e293b;border-radius:24px;padding:40px 12px 24px;background:#fff;box-shadow:0 8px 32px #0000001f}.mobile-shell .preview-content{border:none;border-radius:0;padding:16px;max-width:100%}.blocks-canvas{flex:1;padding:16px 20px 24px;overflow-y:auto;background:#f1f5f9;display:flex;flex-direction:column}.block-wrapper{position:relative;display:flex;align-items:flex-start;background:#fff;border:2px solid transparent;border-radius:8px;transition:border-color .15s,box-shadow .15s}.block-wrapper:hover,.block-wrapper.selected,.block-wrapper:focus-within{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.block-wrapper:hover .block-drag-handle,.block-wrapper.selected .block-drag-handle,.block-wrapper:focus-within .block-drag-handle{opacity:1}.block-wrapper:hover .block-controls,.block-wrapper.selected .block-controls,.block-wrapper:focus-within .block-controls{opacity:1;pointer-events:all}.block-wrapper.dragging{opacity:.4}.block-drag-handle{flex-shrink:0;width:28px;display:flex;align-items:flex-start;justify-content:center;padding-top:14px;opacity:0;transition:opacity .15s;cursor:grab;color:#94a3b8}.block-drag-handle:active{cursor:grabbing}.block-content{flex:1;min-width:0;padding:12px 16px;outline:none;font-size:.9375rem;color:#1e293b;line-height:1.6;min-height:48px;overflow-wrap:anywhere}.block-content:empty:before{content:\"Click to edit...\";color:#94a3b8;font-style:italic;pointer-events:none}.block-content p{margin:0 0 8px}.block-content p:last-child{margin:0}.block-content h1,.block-content h2,.block-content h3{margin:8px 0 6px;color:#1e293b}.block-content a{color:#3b82f6}.block-content img{max-width:100%;border-radius:6px}.block-content ul,.block-content ol{padding-left:20px;margin:0 0 8px}.block-controls{position:absolute;top:6px;right:6px;display:flex;gap:2px;opacity:0;pointer-events:none;transition:opacity .15s;background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:3px;box-shadow:0 2px 6px #00000014}@media(hover:none){.block-wrapper.selected .block-controls{opacity:1;pointer-events:all}}.drop-zone{height:8px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:height .15s,background .15s;position:relative}.drop-zone-label{font-size:.75rem;font-weight:600;color:#3b82f6;opacity:0;transition:opacity .1s}.drop-zone.active{height:40px;background:#3b82f614;border:2px dashed #3b82f6}.drop-zone.active .drop-zone-label{opacity:1}.add-block-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;min-height:44px;padding:12px;margin-top:8px;border:2px dashed #e2e8f0;border-radius:8px;background:none;font-size:.875rem;font-weight:600;color:#64748b;cursor:pointer;font-family:inherit;transition:border-color .15s,color .15s,background .15s}.add-block-btn:hover{border-color:#3b82f6;color:#3b82f6;background:#3b82f60a}.panel-right{background:#fff;border-left:1px solid #e2e8f0;display:flex;flex-direction:column;min-height:0;min-width:0}.right-tabs{display:flex;border-bottom:1px solid #e2e8f0}.right-tab{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:12px;min-height:44px;border:none;background:none;font-size:.8125rem;font-weight:600;color:#64748b;cursor:pointer;font-family:inherit;border-bottom:2px solid transparent}.right-tab:hover{color:#1e293b}.right-tab.active{color:#3b82f6;background:#eff6ff;border-bottom-color:#3b82f6}.right-panel-body{flex:1;padding:20px;overflow-y:auto}.right-section-title{font-size:.9375rem;font-weight:700;color:#1e293b;margin:0 0 12px}.right-section-desc{font-size:.8125rem;color:#64748b;margin:0 0 16px;line-height:1.5}.right-section-hint{font-size:.8125rem;color:#64748b;margin:0 0 12px;line-height:1.5}.block-group{border-bottom:1px solid #f1f5f9}.block-group-header{display:flex;align-items:center;gap:8px;width:100%;min-height:44px;padding:12px 0;border:none;background:none;font-size:.875rem;font-weight:600;color:#1e293b;cursor:pointer;font-family:inherit;text-align:left}.block-group-header .chevron{transition:transform .2s;color:#94a3b8}.block-group-header .chevron.expanded{transform:rotate(90deg)}.block-group-header span:nth-of-type(1){flex:1}.block-count{font-size:.75rem;font-weight:600;color:#64748b;background:#f1f5f9;padding:2px 8px;border-radius:10px;margin-left:auto}.block-list,.variable-list{display:flex;flex-direction:column;gap:2px;padding-bottom:8px}.block-item{display:flex;align-items:center;gap:6px;box-sizing:border-box;width:100%;min-height:44px;padding:8px 12px 8px 28px;font-size:.8125rem;color:#64748b;cursor:pointer;text-align:left;border-radius:4px;font-family:inherit;-webkit-user-select:none;user-select:none}.block-item:hover{background:#f8fafc;color:#1e293b}.block-item-drag{color:#cbd5e1;flex-shrink:0;cursor:grab}.variable-item{display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:center;min-height:44px;padding:8px 12px 8px 28px;border:none;background:none;cursor:pointer;text-align:left;border-radius:4px;font-family:inherit}.variable-item:hover{background:#f8fafc}.var-tag{font-size:.75rem;font-weight:600;color:#3b82f6;background:#3b82f614;padding:2px 6px;border-radius:4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.var-label{font-size:.8125rem;color:#1e293b}.var-example{font-size:.75rem;color:#64748b;overflow-wrap:anywhere}.video-library-panel{padding:0}.video-library-panel .right-section-title,.video-library-panel .right-section-hint{padding:0 16px}.video-library-panel .right-section-title{padding-top:16px}.video-config-section{display:flex;flex-direction:column;gap:8px;padding:12px 16px;border-bottom:1px solid #f1f5f9}.video-config-section:last-of-type{border-bottom:none}.video-lib-list{display:flex;flex-direction:column;gap:2px;padding:8px 8px 16px}.video-lib-item{display:flex;align-items:center;gap:10px;width:100%;min-height:44px;padding:8px;border:1px solid transparent;border-radius:8px;background:none;cursor:pointer;text-align:left;font-family:inherit;transition:background .15s,border-color .15s}.video-lib-item:hover{background:#f8fafc;border-color:#e2e8f0}.video-lib-item.selected{background:#eff6ff;border-color:#93c5fd}.vli-thumb{position:relative;flex-shrink:0;width:56px;height:40px;border-radius:6px;background:linear-gradient(135deg,#f1f5f9,#e2e8f0);display:flex;align-items:center;justify-content:center}.vli-dur{position:absolute;bottom:2px;right:3px;font-size:.625rem;font-weight:600;color:#fff;background:#000000a6;padding:1px 4px;border-radius:3px}.vli-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.vli-title{font-size:.8125rem;font-weight:600;color:#1e293b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vli-cat{font-size:.75rem;color:#64748b}.vli-check{flex-shrink:0}.video-lib-empty{display:flex;flex-direction:column;align-items:center;padding:32px 16px;text-align:center;gap:8px}.video-lib-empty p{font-size:.875rem;font-weight:500;color:#475569;margin:0}.video-lib-empty span{font-size:.8125rem;color:#64748b}.video-lib-loading{padding:8px}.video-lib-skeleton{display:flex;align-items:center;gap:10px;padding:8px;margin-bottom:4px}.vls-info{flex:1;display:flex;flex-direction:column;gap:6px}@media(max-width:1200px){.editor-body{grid-template-columns:280px minmax(0,1fr) 260px}}@media(max-width:900px){.editor-page{height:auto;min-height:100vh;min-height:100dvh;overflow:visible}.editor-body{display:flex;flex-direction:column;overflow:visible}.editor-topbar{padding:12px 16px}.panel-left{border-right:none;border-bottom:1px solid #e2e8f0;overflow:visible;padding:16px}.panel-center{overflow:visible;min-height:480px}.blocks-canvas{overflow:visible;min-height:320px}.panel-right{border-left:none;border-top:1px solid #e2e8f0}.right-panel-body{overflow:visible}.editor-toolbar,.toolbar-row-2,.link-dialog,.editor-tabs{padding-left:12px;padding-right:12px}.preview-frame{padding:12px}.preview-content{padding:16px}.form-row-dates{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*{transition:none!important}}\n"],
|
|
44767
|
+
dependencies: [
|
|
44768
|
+
{
|
|
44769
|
+
kind: "ngmodule",
|
|
44770
|
+
type: FormsModule
|
|
44771
|
+
},
|
|
44772
|
+
{
|
|
44773
|
+
kind: "directive",
|
|
44774
|
+
type: i1.NgControlStatus,
|
|
44775
|
+
selector: "[formControlName],[ngModel],[formControl]"
|
|
44776
|
+
},
|
|
44777
|
+
{
|
|
44778
|
+
kind: "directive",
|
|
44779
|
+
type: i1.RequiredValidator,
|
|
44780
|
+
selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",
|
|
44781
|
+
inputs: ["required"]
|
|
44782
|
+
},
|
|
44783
|
+
{
|
|
44784
|
+
kind: "directive",
|
|
44785
|
+
type: i1.MaxLengthValidator,
|
|
44786
|
+
selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
|
|
44787
|
+
inputs: ["maxlength"]
|
|
44788
|
+
},
|
|
44789
|
+
{
|
|
44790
|
+
kind: "directive",
|
|
44791
|
+
type: i1.NgModel,
|
|
44792
|
+
selector: "[ngModel]:not([formControlName]):not([formControl])",
|
|
44793
|
+
inputs: [
|
|
44794
|
+
"name",
|
|
44795
|
+
"disabled",
|
|
44796
|
+
"ngModel",
|
|
44797
|
+
"ngModelOptions"
|
|
44798
|
+
],
|
|
44799
|
+
outputs: ["ngModelChange"],
|
|
44800
|
+
exportAs: ["ngModel"]
|
|
44801
|
+
},
|
|
44802
|
+
{
|
|
44803
|
+
kind: "component",
|
|
44804
|
+
type: ButtonComponent,
|
|
44805
|
+
selector: "ui-button",
|
|
44806
|
+
inputs: [
|
|
44807
|
+
"variant",
|
|
44808
|
+
"routerLink",
|
|
44809
|
+
"queryParams",
|
|
44810
|
+
"fragment",
|
|
44811
|
+
"state",
|
|
44812
|
+
"href",
|
|
44813
|
+
"target",
|
|
44814
|
+
"rel",
|
|
44815
|
+
"shortLabel",
|
|
44816
|
+
"shortBelow",
|
|
44817
|
+
"color",
|
|
44818
|
+
"active",
|
|
44819
|
+
"size",
|
|
44820
|
+
"type",
|
|
44821
|
+
"disabled",
|
|
44822
|
+
"loading",
|
|
44823
|
+
"fullWidth",
|
|
44824
|
+
"ariaLabel",
|
|
44825
|
+
"ariaExpanded",
|
|
44826
|
+
"ariaPressed",
|
|
44827
|
+
"ariaDescribedBy",
|
|
44828
|
+
"modal-footer-left",
|
|
44829
|
+
"modal-footer-right"
|
|
44830
|
+
]
|
|
44831
|
+
},
|
|
44832
|
+
{
|
|
44833
|
+
kind: "component",
|
|
44834
|
+
type: CheckboxComponent,
|
|
44835
|
+
selector: "ui-checkbox, ui-checkbox-group",
|
|
44836
|
+
inputs: [
|
|
44837
|
+
"label",
|
|
44838
|
+
"ariaLabel",
|
|
44839
|
+
"testId",
|
|
44840
|
+
"labelTestId",
|
|
44841
|
+
"headingLevel",
|
|
44842
|
+
"indeterminate",
|
|
44843
|
+
"checked",
|
|
44844
|
+
"options",
|
|
44845
|
+
"legend",
|
|
44846
|
+
"orientation",
|
|
44847
|
+
"values",
|
|
44848
|
+
"description",
|
|
44849
|
+
"indent",
|
|
44850
|
+
"required",
|
|
44851
|
+
"requiredMessage",
|
|
44852
|
+
"hint",
|
|
44853
|
+
"error",
|
|
44854
|
+
"showErrors",
|
|
44855
|
+
"disabled"
|
|
44856
|
+
],
|
|
44857
|
+
outputs: [
|
|
44858
|
+
"checkedChange",
|
|
44859
|
+
"valuesChange",
|
|
44860
|
+
"invalidChange"
|
|
44861
|
+
]
|
|
44862
|
+
},
|
|
44863
|
+
{
|
|
44864
|
+
kind: "component",
|
|
44865
|
+
type: ChipComponent,
|
|
44866
|
+
selector: "ui-chip",
|
|
44867
|
+
inputs: [
|
|
44868
|
+
"label",
|
|
44869
|
+
"size",
|
|
44870
|
+
"dismissible",
|
|
44871
|
+
"active"
|
|
44872
|
+
],
|
|
44873
|
+
outputs: ["select", "dismiss"]
|
|
44874
|
+
},
|
|
44875
|
+
{
|
|
44876
|
+
kind: "component",
|
|
44877
|
+
type: DateInputComponent,
|
|
44878
|
+
selector: "ui-date-input",
|
|
44879
|
+
inputs: [
|
|
44880
|
+
"label",
|
|
44881
|
+
"required",
|
|
44882
|
+
"min",
|
|
44883
|
+
"max",
|
|
44884
|
+
"hint",
|
|
44885
|
+
"error",
|
|
44886
|
+
"showErrors",
|
|
44887
|
+
"disabled"
|
|
44888
|
+
],
|
|
44889
|
+
outputs: ["invalidChange", "valueChange"]
|
|
44890
|
+
},
|
|
44891
|
+
{
|
|
44892
|
+
kind: "component",
|
|
44893
|
+
type: ErrorComponent,
|
|
44894
|
+
selector: "ui-error",
|
|
44895
|
+
inputs: ["message", "errorId"]
|
|
44896
|
+
},
|
|
44897
|
+
{
|
|
44898
|
+
kind: "component",
|
|
44899
|
+
type: RadioGroupComponent,
|
|
44900
|
+
selector: "ui-radio-group",
|
|
44901
|
+
inputs: [
|
|
44902
|
+
"label",
|
|
44903
|
+
"options",
|
|
44904
|
+
"required",
|
|
44905
|
+
"hint",
|
|
44906
|
+
"error",
|
|
44907
|
+
"showErrors",
|
|
44908
|
+
"orientation",
|
|
44909
|
+
"name",
|
|
44910
|
+
"disabled"
|
|
44911
|
+
],
|
|
44912
|
+
outputs: ["invalidChange"]
|
|
44913
|
+
},
|
|
44914
|
+
{
|
|
44915
|
+
kind: "component",
|
|
44916
|
+
type: SearchInputComponent,
|
|
44917
|
+
selector: "ui-search-input",
|
|
44918
|
+
inputs: [
|
|
44919
|
+
"placeholder",
|
|
44920
|
+
"ariaLabel",
|
|
44921
|
+
"value",
|
|
44922
|
+
"maxlength",
|
|
44923
|
+
"endpoint",
|
|
44924
|
+
"loading"
|
|
44925
|
+
],
|
|
44926
|
+
outputs: [
|
|
44927
|
+
"valueChange",
|
|
44928
|
+
"searchChange",
|
|
44929
|
+
"filteredData"
|
|
44930
|
+
]
|
|
44931
|
+
},
|
|
44932
|
+
{
|
|
44933
|
+
kind: "component",
|
|
44934
|
+
type: SelectComponent,
|
|
44935
|
+
selector: "ui-select",
|
|
44936
|
+
inputs: [
|
|
44937
|
+
"label",
|
|
44938
|
+
"ariaLabel",
|
|
44939
|
+
"placeholder",
|
|
44940
|
+
"options",
|
|
44941
|
+
"valueType",
|
|
44942
|
+
"required",
|
|
44943
|
+
"hint",
|
|
44944
|
+
"error",
|
|
44945
|
+
"showErrors",
|
|
44946
|
+
"size",
|
|
44947
|
+
"disabled"
|
|
44948
|
+
],
|
|
44949
|
+
outputs: ["invalidChange", "valueChange"]
|
|
44950
|
+
},
|
|
44951
|
+
{
|
|
44952
|
+
kind: "component",
|
|
44953
|
+
type: SidePanelComponent,
|
|
44954
|
+
selector: "ui-side-panel",
|
|
44955
|
+
inputs: [
|
|
44956
|
+
"open",
|
|
44957
|
+
"title",
|
|
44958
|
+
"subtitle",
|
|
44959
|
+
"wide",
|
|
44960
|
+
"extraWide",
|
|
44961
|
+
"cancelLabel",
|
|
44962
|
+
"primaryLabel",
|
|
44963
|
+
"primaryVariant",
|
|
44964
|
+
"primaryLoading",
|
|
44965
|
+
"primaryDisabled",
|
|
44966
|
+
"cancelTestId",
|
|
44967
|
+
"primaryTestId",
|
|
44968
|
+
"returnFocusTo"
|
|
44969
|
+
],
|
|
44970
|
+
outputs: ["closed", "primaryClick"]
|
|
44971
|
+
},
|
|
44972
|
+
{
|
|
44973
|
+
kind: "component",
|
|
44974
|
+
type: SkeletonComponent,
|
|
44975
|
+
selector: "ui-skeleton",
|
|
44976
|
+
inputs: [
|
|
44977
|
+
"height",
|
|
44978
|
+
"width",
|
|
44979
|
+
"radius",
|
|
44980
|
+
"mb",
|
|
44981
|
+
"ariaLabel"
|
|
44982
|
+
]
|
|
44983
|
+
},
|
|
44984
|
+
{
|
|
44985
|
+
kind: "component",
|
|
44986
|
+
type: TextInputComponent,
|
|
44987
|
+
selector: "ui-text-input",
|
|
44988
|
+
inputs: [
|
|
44989
|
+
"label",
|
|
44990
|
+
"ariaLabel",
|
|
44991
|
+
"inputTestId",
|
|
44992
|
+
"placeholder",
|
|
44993
|
+
"prefix",
|
|
44994
|
+
"revealable",
|
|
44995
|
+
"passwordRequirements",
|
|
44996
|
+
"passwordRules",
|
|
44997
|
+
"type",
|
|
44998
|
+
"maxlength",
|
|
44999
|
+
"required",
|
|
45000
|
+
"autocomplete",
|
|
45001
|
+
"inputmode",
|
|
45002
|
+
"readonly",
|
|
45003
|
+
"hint",
|
|
45004
|
+
"error",
|
|
45005
|
+
"phoneFormat",
|
|
45006
|
+
"min",
|
|
45007
|
+
"max",
|
|
45008
|
+
"maxDecimals",
|
|
45009
|
+
"step",
|
|
45010
|
+
"valueType",
|
|
45011
|
+
"formatter",
|
|
45012
|
+
"integer",
|
|
45013
|
+
"showErrors",
|
|
45014
|
+
"disabled"
|
|
45015
|
+
],
|
|
45016
|
+
outputs: [
|
|
45017
|
+
"valueChange",
|
|
45018
|
+
"focused",
|
|
45019
|
+
"blurred",
|
|
45020
|
+
"invalidChange"
|
|
45021
|
+
]
|
|
45022
|
+
},
|
|
45023
|
+
{
|
|
45024
|
+
kind: "component",
|
|
45025
|
+
type: TextareaInputComponent,
|
|
45026
|
+
selector: "ui-textarea-input",
|
|
45027
|
+
inputs: [
|
|
45028
|
+
"label",
|
|
45029
|
+
"ariaLabel",
|
|
45030
|
+
"placeholder",
|
|
45031
|
+
"rows",
|
|
45032
|
+
"maxlength",
|
|
45033
|
+
"required",
|
|
45034
|
+
"readonly",
|
|
45035
|
+
"hint",
|
|
45036
|
+
"hintAsDescription",
|
|
45037
|
+
"error",
|
|
45038
|
+
"monospace",
|
|
45039
|
+
"showErrors",
|
|
45040
|
+
"disabled"
|
|
45041
|
+
],
|
|
45042
|
+
outputs: ["invalidChange"]
|
|
45043
|
+
},
|
|
45044
|
+
{
|
|
45045
|
+
kind: "directive",
|
|
45046
|
+
type: TooltipDirective,
|
|
45047
|
+
selector: "[uiTooltip]",
|
|
45048
|
+
inputs: ["uiTooltip", "tooltipPosition"]
|
|
45049
|
+
}
|
|
45050
|
+
],
|
|
45051
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
45052
|
+
});
|
|
45053
|
+
}
|
|
45054
|
+
};
|
|
45055
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
45056
|
+
minVersion: "12.0.0",
|
|
45057
|
+
version: "22.2.2",
|
|
45058
|
+
ngImport: i0,
|
|
45059
|
+
type: EmailTemplateEditorComponent,
|
|
45060
|
+
decorators: [{
|
|
45061
|
+
type: Component,
|
|
45062
|
+
args: [{
|
|
45063
|
+
selector: "ui-email-template-editor",
|
|
45064
|
+
standalone: true,
|
|
45065
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
45066
|
+
imports: [
|
|
45067
|
+
FormsModule,
|
|
45068
|
+
ButtonComponent,
|
|
45069
|
+
CheckboxComponent,
|
|
45070
|
+
ChipComponent,
|
|
45071
|
+
DateInputComponent,
|
|
45072
|
+
ErrorComponent,
|
|
45073
|
+
RadioGroupComponent,
|
|
45074
|
+
SearchInputComponent,
|
|
45075
|
+
SelectComponent,
|
|
45076
|
+
SidePanelComponent,
|
|
45077
|
+
SkeletonComponent,
|
|
45078
|
+
TextInputComponent,
|
|
45079
|
+
TextareaInputComponent,
|
|
45080
|
+
TooltipDirective
|
|
45081
|
+
],
|
|
45082
|
+
template: `
|
|
45083
|
+
<div class="editor-page">
|
|
45084
|
+
<!-- Top Bar -->
|
|
45085
|
+
<div class="editor-topbar">
|
|
45086
|
+
<div class="topbar-left">
|
|
45087
|
+
<ui-button variant="ghost" size="sm" ariaLabel="Go back" (click)="back.emit()">
|
|
45088
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>
|
|
45089
|
+
</ui-button>
|
|
45090
|
+
<nav class="breadcrumb" aria-label="Breadcrumb">
|
|
45091
|
+
<a href="#" (click)="$event.preventDefault(); back.emit()">{{ backLabel() }}</a>
|
|
45092
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>
|
|
45093
|
+
<span>{{ isNew() ? createLabel() : editLabel() }}</span>
|
|
45094
|
+
</nav>
|
|
45095
|
+
</div>
|
|
45096
|
+
<div class="topbar-right">
|
|
45097
|
+
@if (canWrite() && testSendEnabled()) {
|
|
45098
|
+
<ui-button variant="secondary" size="sm" (click)="openTestSendDialog()">
|
|
45099
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
45100
|
+
Test Send
|
|
45101
|
+
</ui-button>
|
|
45102
|
+
}
|
|
45103
|
+
<ui-button variant="primary" size="sm" [loading]="saving()" [disabled]="saveDisabled()" (click)="onSave()">
|
|
45104
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
|
45105
|
+
Save Template
|
|
45106
|
+
</ui-button>
|
|
45107
|
+
</div>
|
|
45108
|
+
</div>
|
|
45109
|
+
|
|
45110
|
+
@if (systemTemplate()) {
|
|
45111
|
+
<div class="readonly-note system-note" role="note" data-testid="editor-system-note">
|
|
45112
|
+
<span>{{ systemNote() }}</span>
|
|
45113
|
+
@if (canCopySystem()) {
|
|
45114
|
+
<ui-button variant="secondary" size="sm" data-testid="editor-use-as-mine" (click)="useAsMine.emit()">Use as My Template</ui-button>
|
|
45115
|
+
}
|
|
45116
|
+
</div>
|
|
45117
|
+
} @else if (!canWrite()) {
|
|
45118
|
+
<p class="readonly-note" role="note" data-testid="editor-readonly-note">{{ readOnlyNote() }}</p>
|
|
45119
|
+
}
|
|
45120
|
+
|
|
45121
|
+
<!-- 3-Column Layout -->
|
|
45122
|
+
<div class="editor-body">
|
|
45123
|
+
<!-- LEFT PANEL: Details -->
|
|
45124
|
+
<aside class="panel-left" aria-label="Template details">
|
|
45125
|
+
<h3 class="panel-title">Details</h3>
|
|
45126
|
+
|
|
45127
|
+
<div class="form-field">
|
|
45128
|
+
<ui-text-input label="Name" [required]="true" [(ngModel)]="name" [readonly]="!canWrite()" [maxlength]="maxTextLength()"
|
|
45129
|
+
[placeholder]="namePlaceholder()" [error]="nameError()" />
|
|
45130
|
+
</div>
|
|
45131
|
+
|
|
45132
|
+
<div class="form-field">
|
|
45133
|
+
<ui-text-input label="Subject" [required]="true" [(ngModel)]="subject" [readonly]="!canWrite()" [maxlength]="maxTextLength()"
|
|
45134
|
+
[placeholder]="subjectPlaceholder()" [error]="subjectError()">
|
|
45135
|
+
@if (canWrite()) {
|
|
45136
|
+
<span uiLabelEnd><ng-content select="[uiSubjectAction]" /></span>
|
|
45137
|
+
}
|
|
45138
|
+
</ui-text-input>
|
|
45139
|
+
</div>
|
|
45140
|
+
|
|
45141
|
+
<div class="form-field">
|
|
45142
|
+
<ui-select label="Category" [options]="categories()" [placeholder]="categoryPlaceholder()" [(ngModel)]="category" [disabled]="!canWrite()" />
|
|
45143
|
+
</div>
|
|
45144
|
+
|
|
45145
|
+
<ui-checkbox label="Use this template all year" [(checked)]="useAllYear" [disabled]="!canWrite()" />
|
|
45146
|
+
|
|
45147
|
+
@if (!useAllYear()) {
|
|
45148
|
+
<div class="form-row-dates">
|
|
45149
|
+
<ui-date-input label="Active From" [(ngModel)]="activeFrom" [disabled]="!canWrite()" />
|
|
45150
|
+
<ui-date-input label="Active To" [(ngModel)]="activeTo" [disabled]="!canWrite()" [min]="activeFrom()" [error]="datesError()" />
|
|
45151
|
+
</div>
|
|
45152
|
+
}
|
|
45153
|
+
|
|
45154
|
+
<!-- Assignments -->
|
|
45155
|
+
@if (assignmentOptions().length > 0) {
|
|
45156
|
+
<div class="assignments-section">
|
|
45157
|
+
<h4 class="section-title">Assignments</h4>
|
|
45158
|
+
<p class="section-desc">{{ assignmentsDescription() }}</p>
|
|
45159
|
+
<ui-search-input placeholder="Search..." ariaLabel="Search assignments" [(value)]="assignmentSearch" />
|
|
45160
|
+
<div class="assignment-list">
|
|
45161
|
+
@for (item of filteredAssignments(); track item.key) {
|
|
45162
|
+
<div class="assignment-item" [class.assignment-readonly]="!canWrite()" [attr.role]="canWrite() ? 'button' : null" [attr.tabindex]="canWrite() ? 0 : null"
|
|
45163
|
+
[attr.aria-label]="canWrite() ? 'Configure ' + item.name : null"
|
|
45164
|
+
(click)="openAssignmentConfig(item)" (keydown.enter)="openAssignmentConfig(item); $event.preventDefault()" (keydown.space)="openAssignmentConfig(item); $event.preventDefault()">
|
|
45165
|
+
<div class="assignment-text">
|
|
45166
|
+
<strong>{{ item.name }}</strong>
|
|
45167
|
+
<span>{{ item.description }}</span>
|
|
45168
|
+
</div>
|
|
45169
|
+
<div class="assignment-channels">
|
|
45170
|
+
<span class="channel-badge" [class.active]="item.email" role="img" [attr.aria-label]="'Email ' + (item.email ? 'on' : 'off')">
|
|
45171
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
|
|
45172
|
+
</span>
|
|
45173
|
+
<span class="channel-badge" [class.active]="item.text" [class.disabled]="!textEnabled()" role="img" [attr.aria-label]="'Text ' + (item.text ? 'on' : 'off')">
|
|
45174
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
45175
|
+
</span>
|
|
45176
|
+
</div>
|
|
45177
|
+
</div>
|
|
45178
|
+
}
|
|
45179
|
+
</div>
|
|
45180
|
+
</div>
|
|
45181
|
+
}
|
|
45182
|
+
</aside>
|
|
45183
|
+
|
|
45184
|
+
<!-- CENTER: Editor -->
|
|
45185
|
+
<main class="panel-center">
|
|
45186
|
+
<div class="editor-tabs" role="tablist" aria-label="Editor view">
|
|
45187
|
+
<button type="button" class="editor-tab" role="tab" [class.active]="activeEditorTab() === 'edit'" [attr.aria-selected]="activeEditorTab() === 'edit'" (click)="setEditorTab('edit')">
|
|
45188
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
|
|
45189
|
+
Edit
|
|
45190
|
+
</button>
|
|
45191
|
+
<button type="button" class="editor-tab" role="tab" [class.active]="activeEditorTab() === 'preview'" [attr.aria-selected]="activeEditorTab() === 'preview'" (click)="setEditorTab('preview')">
|
|
45192
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
45193
|
+
Preview
|
|
45194
|
+
</button>
|
|
45195
|
+
<button type="button" class="editor-tab" role="tab" [class.active]="activeEditorTab() === 'mobile'" [attr.aria-selected]="activeEditorTab() === 'mobile'" (click)="setEditorTab('mobile')">
|
|
45196
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
|
45197
|
+
Mobile
|
|
45198
|
+
</button>
|
|
45199
|
+
</div>
|
|
45200
|
+
|
|
45201
|
+
@if (activeEditorTab() === 'edit') {
|
|
45202
|
+
@if (canWrite()) {
|
|
45203
|
+
<!-- Toolbar row 1: mousedown preventDefault keeps focus (and the selection) in the editor -->
|
|
45204
|
+
<div class="editor-toolbar" role="toolbar" aria-label="Text formatting" (mousedown)="preventFocusLoss($event)">
|
|
45205
|
+
<div class="toolbar-group">
|
|
45206
|
+
<ui-button variant="icon" size="sm" ariaLabel="Bold" uiTooltip="Bold" (click)="execCommand('bold')"><strong>B</strong></ui-button>
|
|
45207
|
+
<ui-button variant="icon" size="sm" ariaLabel="Italic" uiTooltip="Italic" (click)="execCommand('italic')"><em>I</em></ui-button>
|
|
45208
|
+
<ui-button variant="icon" size="sm" ariaLabel="Underline" uiTooltip="Underline" (click)="execCommand('underline')"><u>U</u></ui-button>
|
|
45209
|
+
</div>
|
|
45210
|
+
<div class="toolbar-group">
|
|
45211
|
+
@for (k of [headingKey()]; track k) {
|
|
45212
|
+
<ui-select class="toolbar-select" size="sm" ariaLabel="Heading level" [options]="headingOptions" ngModel="p"
|
|
45213
|
+
(mousedown)="saveSelection()" (ngModelChange)="onHeading($event)" />
|
|
45214
|
+
}
|
|
45215
|
+
</div>
|
|
45216
|
+
<div class="toolbar-group">
|
|
45217
|
+
@for (k of [sizeKey()]; track k) {
|
|
45218
|
+
<ui-select class="toolbar-select" size="sm" ariaLabel="Font size" placeholder="Size" [options]="fontSizeOptions" ngModel=""
|
|
45219
|
+
(mousedown)="saveSelection()" (ngModelChange)="onFontSize($event)" />
|
|
45220
|
+
}
|
|
45221
|
+
</div>
|
|
45222
|
+
<div class="toolbar-group">
|
|
45223
|
+
<ui-button variant="icon" size="sm" ariaLabel="Align left" uiTooltip="Align Left" (click)="execCommand('justifyLeft')">
|
|
45224
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg>
|
|
45225
|
+
</ui-button>
|
|
45226
|
+
<ui-button variant="icon" size="sm" ariaLabel="Align center" uiTooltip="Align Center" (click)="execCommand('justifyCenter')">
|
|
45227
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="10" x2="6" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="18" y1="18" x2="6" y2="18"/></svg>
|
|
45228
|
+
</ui-button>
|
|
45229
|
+
<ui-button variant="icon" size="sm" ariaLabel="Align right" uiTooltip="Align Right" (click)="execCommand('justifyRight')">
|
|
45230
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="21" y1="10" x2="7" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="7" y2="18"/></svg>
|
|
45231
|
+
</ui-button>
|
|
45232
|
+
</div>
|
|
45233
|
+
<div class="toolbar-group">
|
|
45234
|
+
<ui-button variant="icon" size="sm" ariaLabel="Bulleted list" uiTooltip="Bulleted List" (click)="execCommand('insertUnorderedList')">
|
|
45235
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
|
45236
|
+
</ui-button>
|
|
45237
|
+
<ui-button variant="icon" size="sm" ariaLabel="Numbered list" uiTooltip="Numbered List" (click)="execCommand('insertOrderedList')">
|
|
45238
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>
|
|
45239
|
+
</ui-button>
|
|
45240
|
+
</div>
|
|
45241
|
+
<div class="toolbar-group">
|
|
45242
|
+
<!-- The browser's own colour dialog is the picker; a button opens it so the toolbar keeps one look. -->
|
|
45243
|
+
<input #colorInput type="color" class="hidden-color-input" aria-label="Text color" tabindex="-1" [value]="currentColor()" (change)="applyTextColor($any($event.target).value)" (mousedown)="$event.stopPropagation()" />
|
|
45244
|
+
<ui-button variant="icon" size="sm" ariaLabel="Text color" uiTooltip="Text Color" (mousedown)="openColorPicker()">
|
|
45245
|
+
<span class="color-indicator">
|
|
45246
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><text y="20" font-size="20" font-weight="700">A</text></svg>
|
|
45247
|
+
<span class="color-bar" [style.background]="currentColor()"></span>
|
|
45248
|
+
</span>
|
|
45249
|
+
</ui-button>
|
|
45250
|
+
</div>
|
|
45251
|
+
<div class="toolbar-group">
|
|
45252
|
+
<ui-button variant="icon" size="sm" ariaLabel="Insert link" uiTooltip="Insert Link" (mousedown)="saveSelection()" (click)="openLinkDialog()">
|
|
45253
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
45254
|
+
</ui-button>
|
|
45255
|
+
<ui-button variant="icon" size="sm" ariaLabel="Insert image" uiTooltip="Insert Image" (mousedown)="saveSelection()" (click)="openImageDialog()">
|
|
45256
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
|
45257
|
+
</ui-button>
|
|
45258
|
+
</div>
|
|
45259
|
+
<div class="toolbar-group">
|
|
45260
|
+
<ui-button variant="icon" size="sm" ariaLabel="Undo" uiTooltip="Undo" (click)="execCommand('undo')">
|
|
45261
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6.69 3L3 13"/></svg>
|
|
45262
|
+
</ui-button>
|
|
45263
|
+
<ui-button variant="icon" size="sm" ariaLabel="Redo" uiTooltip="Redo" (click)="execCommand('redo')">
|
|
45264
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6.69 3L21 13"/></svg>
|
|
45265
|
+
</ui-button>
|
|
45266
|
+
</div>
|
|
45267
|
+
</div>
|
|
45268
|
+
|
|
45269
|
+
<!-- Toolbar row 2: block spacing + AI -->
|
|
45270
|
+
<div class="toolbar-row-2">
|
|
45271
|
+
<div class="toolbar-group" (mousedown)="preventFocusLoss($event)">
|
|
45272
|
+
<span class="toolbar-label" id="tpl-spacing-label">Block spacing</span>
|
|
45273
|
+
<ui-select class="toolbar-select" size="sm" ariaLabel="Block spacing" [options]="spacingOptions" [ngModel]="blockSpacing()" (ngModelChange)="applySpacing($event)" />
|
|
45274
|
+
</div>
|
|
45275
|
+
@if (aiEnabled()) {
|
|
45276
|
+
<ui-button variant="secondary" size="sm" (click)="openAiPanel()">
|
|
45277
|
+
<span class="ai-dot" aria-hidden="true"></span>
|
|
45278
|
+
Generate with AI
|
|
45279
|
+
</ui-button>
|
|
45280
|
+
}
|
|
45281
|
+
</div>
|
|
45282
|
+
}
|
|
45283
|
+
|
|
45284
|
+
<!-- Link dialog (inline popover) -->
|
|
45285
|
+
@if (canWrite() && showLinkDialog()) {
|
|
45286
|
+
<div class="link-dialog" role="group" aria-label="Insert link">
|
|
45287
|
+
<div class="link-dialog-inner">
|
|
45288
|
+
<span class="link-dialog-title">Insert Link</span>
|
|
45289
|
+
<ui-text-input class="link-dialog-input" ariaLabel="Link address" placeholder="https://example.com" [(ngModel)]="linkUrl" [error]="linkUrlError()" (keydown.enter)="insertLink()" />
|
|
45290
|
+
@if (!linkTextFromSelection()) {
|
|
45291
|
+
<ui-text-input class="link-dialog-input" ariaLabel="Link text" placeholder="Link text (optional)" [(ngModel)]="linkText" (keydown.enter)="insertLink()" />
|
|
45292
|
+
}
|
|
45293
|
+
<div class="link-dialog-actions">
|
|
45294
|
+
<ui-button variant="secondary" size="sm" (click)="showLinkDialog.set(false)">Cancel</ui-button>
|
|
45295
|
+
<ui-button variant="primary" size="sm" [disabled]="!canInsertLink()" (click)="insertLink()">Insert</ui-button>
|
|
45296
|
+
</div>
|
|
45297
|
+
</div>
|
|
45298
|
+
</div>
|
|
45299
|
+
}
|
|
45300
|
+
|
|
45301
|
+
<!-- Image URL dialog -->
|
|
45302
|
+
@if (canWrite() && showImageDialog()) {
|
|
45303
|
+
<div class="link-dialog" role="group" aria-label="Insert image">
|
|
45304
|
+
<div class="link-dialog-inner">
|
|
45305
|
+
<span class="link-dialog-title">Insert Image</span>
|
|
45306
|
+
<ui-text-input class="link-dialog-input" ariaLabel="Image address" placeholder="https://example.com/image.jpg" [(ngModel)]="imageUrl" [error]="imageUrlError()" (keydown.enter)="insertImageFromUrl()" />
|
|
45307
|
+
<ui-text-input class="link-dialog-input" ariaLabel="Image alt text" placeholder="Alt text (optional)" [(ngModel)]="imageAlt" (keydown.enter)="insertImageFromUrl()" />
|
|
45308
|
+
<div class="link-dialog-actions">
|
|
45309
|
+
<ui-button variant="secondary" size="sm" (click)="showImageDialog.set(false)">Cancel</ui-button>
|
|
45310
|
+
<ui-button variant="primary" size="sm" [disabled]="!canInsertImage()" (click)="insertImageFromUrl()">Insert</ui-button>
|
|
45311
|
+
</div>
|
|
45312
|
+
</div>
|
|
45313
|
+
</div>
|
|
45314
|
+
}
|
|
45315
|
+
|
|
45316
|
+
<ui-error [errorId]="idPrefix() + '-body-error'" [message]="bodyError()" />
|
|
45317
|
+
|
|
45318
|
+
<!-- Block Canvas with DnD -->
|
|
45319
|
+
<div class="blocks-canvas" (dragover)="onCanvasDragOver($event)" (drop)="onDropZoneDrop($event, blocks().length)">
|
|
45320
|
+
<!-- Initial drop zone (before first block) -->
|
|
45321
|
+
<div class="drop-zone" [class.active]="dragOverIndex() === 0" (dragover)="onDropZoneDragOver($event, 0)" (dragleave)="onDropZoneLeave()" (drop)="onDropZoneDrop($event, 0)">
|
|
45322
|
+
<span class="drop-zone-label">Drop here</span>
|
|
45323
|
+
</div>
|
|
45324
|
+
|
|
45325
|
+
@for (block of blocks(); track block.id; let i = $index) {
|
|
45326
|
+
<div class="block-wrapper" #blockWrapper [class.selected]="selectedBlockId() === block.id" [class.dragging]="dragFromCanvas() === i" (dragend)="onBlockDragEnd()">
|
|
45327
|
+
@if (canWrite()) {
|
|
45328
|
+
<!-- Only the handle is draggable: a draggable ancestor of the contenteditable breaks text selection in Firefox / Safari. Move up / down below are the keyboard and touch alternative. -->
|
|
45329
|
+
<div class="block-drag-handle" title="Drag to reorder" draggable="true" (dragstart)="onBlockDragStart($event, i, blockWrapper)">
|
|
45330
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>
|
|
45331
|
+
</div>
|
|
45332
|
+
}
|
|
45333
|
+
|
|
45334
|
+
<!-- Block content (contenteditable, initialised ONCE after render so Angular never rewrites it and the caret never jumps) -->
|
|
45335
|
+
<div #blockEditor class="block-content" role="textbox" aria-multiline="true" [attr.aria-label]="'Email block ' + (i + 1)"
|
|
45336
|
+
[attr.data-block-id]="block.id" [attr.contenteditable]="canWrite() ? 'true' : 'false'"
|
|
45337
|
+
(input)="onBlockInput($event, block)" (beforeinput)="onBlockBeforeInput($any($event))" (focus)="onBlockFocus(block)" (click)="selectedBlockId.set(block.id)"></div>
|
|
45338
|
+
|
|
45339
|
+
@if (canWrite()) {
|
|
45340
|
+
<div class="block-controls">
|
|
45341
|
+
<ui-button variant="icon" size="xs" ariaLabel="Move block up" uiTooltip="Move up" [disabled]="i === 0" (mousedown)="preventFocusLoss($event)" (click)="moveBlock(i, -1)">
|
|
45342
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M18 15l-6-6-6 6"/></svg>
|
|
45343
|
+
</ui-button>
|
|
45344
|
+
<ui-button variant="icon" size="xs" ariaLabel="Move block down" uiTooltip="Move down" [disabled]="i === blocks().length - 1" (mousedown)="preventFocusLoss($event)" (click)="moveBlock(i, 1)">
|
|
45345
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
|
45346
|
+
</ui-button>
|
|
45347
|
+
<ui-button variant="icon" size="xs" ariaLabel="Duplicate block" uiTooltip="Duplicate" (mousedown)="preventFocusLoss($event)" (click)="duplicateBlock(i)">
|
|
45348
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
45349
|
+
</ui-button>
|
|
45350
|
+
<ui-button variant="icon-danger" size="xs" ariaLabel="Delete block" uiTooltip="Delete block" (mousedown)="preventFocusLoss($event)" (click)="deleteBlock(i)">
|
|
45351
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/></svg>
|
|
45352
|
+
</ui-button>
|
|
45353
|
+
</div>
|
|
45354
|
+
}
|
|
45355
|
+
</div>
|
|
45356
|
+
|
|
45357
|
+
<!-- Drop zone after each block -->
|
|
45358
|
+
<div class="drop-zone" [class.active]="dragOverIndex() === i + 1" (dragover)="onDropZoneDragOver($event, i + 1)" (dragleave)="onDropZoneLeave()" (drop)="onDropZoneDrop($event, i + 1)">
|
|
45359
|
+
<span class="drop-zone-label">Drop here</span>
|
|
45360
|
+
</div>
|
|
45361
|
+
}
|
|
45362
|
+
|
|
45363
|
+
@if (canWrite()) {
|
|
45364
|
+
<button type="button" class="add-block-btn" (click)="addBlock('text-block')">
|
|
45365
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
45366
|
+
Add Block
|
|
45367
|
+
</button>
|
|
45368
|
+
}
|
|
45369
|
+
</div>
|
|
45370
|
+
}
|
|
45371
|
+
|
|
45372
|
+
@if (activeEditorTab() === 'preview') {
|
|
45373
|
+
<div class="preview-frame">
|
|
45374
|
+
<div class="preview-content" [innerHTML]="previewHtml()"></div>
|
|
45375
|
+
</div>
|
|
45376
|
+
}
|
|
45377
|
+
|
|
45378
|
+
@if (activeEditorTab() === 'mobile') {
|
|
45379
|
+
<div class="preview-frame mobile">
|
|
45380
|
+
<div class="mobile-shell">
|
|
45381
|
+
<div class="preview-content" [innerHTML]="previewHtml()"></div>
|
|
45382
|
+
</div>
|
|
45383
|
+
</div>
|
|
45384
|
+
}
|
|
45385
|
+
</main>
|
|
45386
|
+
|
|
45387
|
+
<!-- RIGHT PANEL: Blocks, Variables, Video -->
|
|
45388
|
+
<aside class="panel-right" aria-label="Blocks, variables and video">
|
|
45389
|
+
@if (!canWrite()) {
|
|
45390
|
+
<div class="right-panel-body" data-testid="tpl-readonly-tools-note">
|
|
45391
|
+
<h4 class="right-section-title">Read only</h4>
|
|
45392
|
+
<p class="right-section-hint">Blocks, variables and videos can only be added to a template you can edit.</p>
|
|
45393
|
+
</div>
|
|
45394
|
+
} @else {
|
|
45395
|
+
<div class="right-tabs" role="tablist" aria-label="Right panel sections">
|
|
45396
|
+
<button type="button" class="right-tab" role="tab" [class.active]="activeRightTab() === 'blocks'" [attr.aria-selected]="activeRightTab() === 'blocks'" (click)="activeRightTab.set('blocks')">
|
|
45397
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
|
45398
|
+
Blocks
|
|
45399
|
+
</button>
|
|
45400
|
+
<button type="button" class="right-tab" role="tab" [class.active]="activeRightTab() === 'variables'" [attr.aria-selected]="activeRightTab() === 'variables'" (click)="activeRightTab.set('variables')">
|
|
45401
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3v18M3 12h18"/></svg>
|
|
45402
|
+
Variables
|
|
45403
|
+
</button>
|
|
45404
|
+
@if (videosEnabled()) {
|
|
45405
|
+
<button type="button" class="right-tab" role="tab" [class.active]="activeRightTab() === 'video'" [attr.aria-selected]="activeRightTab() === 'video'" (click)="openVideoTab()">
|
|
45406
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
|
45407
|
+
Video
|
|
45408
|
+
</button>
|
|
45409
|
+
}
|
|
45410
|
+
</div>
|
|
45411
|
+
|
|
45412
|
+
@if (activeRightTab() === 'blocks') {
|
|
45413
|
+
<div class="right-panel-body">
|
|
45414
|
+
<h4 class="right-section-title">Content Blocks</h4>
|
|
45415
|
+
<p class="right-section-desc">Click to insert or drag into the editor.</p>
|
|
45416
|
+
<ui-search-input placeholder="Search blocks..." ariaLabel="Search blocks" [(value)]="blockSearch" />
|
|
45417
|
+
|
|
45418
|
+
@for (group of filteredBlockGroups(); track group.name) {
|
|
45419
|
+
<div class="block-group">
|
|
45420
|
+
<button type="button" class="block-group-header" [attr.aria-expanded]="isOpen('b:' + group.name)" (click)="toggleGroup('b:' + group.name)">
|
|
45421
|
+
<svg class="chevron" [class.expanded]="isOpen('b:' + group.name)" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>
|
|
45422
|
+
<span>{{ group.name }}</span>
|
|
45423
|
+
<span class="block-count">{{ group.blocks.length }}</span>
|
|
45424
|
+
</button>
|
|
45425
|
+
@if (isOpen('b:' + group.name)) {
|
|
45426
|
+
<div class="block-list">
|
|
45427
|
+
@for (block of group.blocks; track block.key) {
|
|
45428
|
+
<!-- A div, not a button: Firefox does not start a drag from a <button>. Role, tabindex and Enter / Space keep it a keyboard control; a click inserts the block. -->
|
|
45429
|
+
<div role="button" tabindex="0" class="block-item" draggable="true" [attr.aria-label]="'Insert ' + block.label"
|
|
45430
|
+
(dragstart)="onPaletteDragStart($event, block)" (dragend)="onBlockDragEnd()" (click)="insertBlock(block)"
|
|
45431
|
+
(keydown.enter)="insertBlock(block); $event.preventDefault()" (keydown.space)="insertBlock(block); $event.preventDefault()">
|
|
45432
|
+
<svg class="block-item-drag" width="10" height="10" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>
|
|
45433
|
+
{{ block.label }}
|
|
45434
|
+
</div>
|
|
45435
|
+
}
|
|
45436
|
+
</div>
|
|
45437
|
+
}
|
|
45438
|
+
</div>
|
|
45439
|
+
}
|
|
45440
|
+
</div>
|
|
45441
|
+
}
|
|
45442
|
+
|
|
45443
|
+
@if (activeRightTab() === 'variables') {
|
|
45444
|
+
<div class="right-panel-body">
|
|
45445
|
+
<h4 class="right-section-title">Template Variables</h4>
|
|
45446
|
+
<p class="right-section-desc">Click a variable to insert it at the cursor position.</p>
|
|
45447
|
+
|
|
45448
|
+
@for (group of variableGroups(); track group.name) {
|
|
45449
|
+
<div class="block-group">
|
|
45450
|
+
<button type="button" class="block-group-header" [attr.aria-expanded]="isOpen('v:' + group.name)" (click)="toggleGroup('v:' + group.name)">
|
|
45451
|
+
<svg class="chevron" [class.expanded]="isOpen('v:' + group.name)" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>
|
|
45452
|
+
<span>{{ group.name }}</span>
|
|
45453
|
+
<span class="block-count">{{ group.variables.length }}</span>
|
|
45454
|
+
</button>
|
|
45455
|
+
@if (isOpen('v:' + group.name)) {
|
|
45456
|
+
<div class="variable-list">
|
|
45457
|
+
@for (v of group.variables; track v.key) {
|
|
45458
|
+
<button type="button" class="variable-item" (mousedown)="saveSelection()" (click)="insertVariable(v)">
|
|
45459
|
+
<span class="var-tag">{{ '{{' + v.key + '}}' }}</span>
|
|
45460
|
+
<span class="var-label">{{ v.label }}</span>
|
|
45461
|
+
<span class="var-example">{{ v.example }}</span>
|
|
45462
|
+
</button>
|
|
45463
|
+
}
|
|
45464
|
+
</div>
|
|
45465
|
+
}
|
|
45466
|
+
</div>
|
|
45467
|
+
}
|
|
45468
|
+
</div>
|
|
45469
|
+
}
|
|
45470
|
+
|
|
45471
|
+
@if (activeRightTab() === 'video' && videosEnabled()) {
|
|
45472
|
+
<div class="right-panel-body video-library-panel">
|
|
45473
|
+
<h4 class="right-section-title">Video Library</h4>
|
|
45474
|
+
<p class="right-section-hint">Click a video to add it as a new block in your email.</p>
|
|
45475
|
+
|
|
45476
|
+
<div class="video-config-section">
|
|
45477
|
+
<ui-radio-group label="Playback" orientation="horizontal" [options]="playbackOptions" [ngModel]="videoAutoplay() ? 'autoplay' : 'click'" (ngModelChange)="videoAutoplay.set($event === 'autoplay')" />
|
|
45478
|
+
</div>
|
|
45479
|
+
<div class="video-config-section">
|
|
45480
|
+
<ui-text-input label="Caption" placeholder="e.g. Click to watch my video for you" [maxlength]="200" [(ngModel)]="videoCaption" />
|
|
45481
|
+
</div>
|
|
45482
|
+
<div class="video-config-section">
|
|
45483
|
+
<ui-select label="Call-to-Action" [options]="ctaTypeOptions" [(ngModel)]="videoCtaType" />
|
|
45484
|
+
@if (videoCtaType()) {
|
|
45485
|
+
<ui-text-input ariaLabel="Call-to-action address" type="url" placeholder="https://..." [(ngModel)]="videoCtaUrl" />
|
|
45486
|
+
<ui-text-input ariaLabel="Call-to-action button label" placeholder="Button label" [maxlength]="100" [(ngModel)]="videoCtaLabel" />
|
|
45487
|
+
}
|
|
45488
|
+
</div>
|
|
45489
|
+
<div class="video-config-section">
|
|
45490
|
+
<ui-checkbox label="Hide Reactions" [(checked)]="videoHideReactions" />
|
|
45491
|
+
</div>
|
|
45492
|
+
|
|
45493
|
+
@if (videosLoading()) {
|
|
45494
|
+
<div class="video-lib-loading" role="status" aria-busy="true" aria-label="Loading videos">
|
|
45495
|
+
@for (n of [1, 2, 3]; track n) {
|
|
45496
|
+
<div class="video-lib-skeleton">
|
|
45497
|
+
<ui-skeleton [height]="40" width="56px" [radius]="6" />
|
|
45498
|
+
<div class="vls-info"><ui-skeleton [height]="10" width="75%" [radius]="5" /><ui-skeleton [height]="10" width="45%" [radius]="5" /></div>
|
|
45499
|
+
</div>
|
|
45500
|
+
}
|
|
45501
|
+
</div>
|
|
45502
|
+
} @else if (videos().length === 0) {
|
|
45503
|
+
<div class="video-lib-empty">
|
|
45504
|
+
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.5" aria-hidden="true"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
|
45505
|
+
<p>No videos yet</p>
|
|
45506
|
+
<span>{{ noVideosHint() }}</span>
|
|
45507
|
+
</div>
|
|
45508
|
+
} @else {
|
|
45509
|
+
<div class="video-lib-list">
|
|
45510
|
+
@for (video of videos(); track video.id) {
|
|
45511
|
+
<button type="button" class="video-lib-item" [class.selected]="video.id === videoId()" [attr.aria-pressed]="video.id === videoId()" (click)="insertVideo(video)">
|
|
45512
|
+
<span class="vli-thumb">
|
|
45513
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.5" aria-hidden="true"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
|
45514
|
+
@if (video.durationSeconds) {
|
|
45515
|
+
<span class="vli-dur">{{ formatDuration(video.durationSeconds) }}</span>
|
|
45516
|
+
}
|
|
45517
|
+
</span>
|
|
45518
|
+
<span class="vli-info">
|
|
45519
|
+
<span class="vli-title">{{ video.title }}</span>
|
|
45520
|
+
<span class="vli-cat">{{ categoryLabel(video.category) }}</span>
|
|
45521
|
+
</span>
|
|
45522
|
+
@if (video.id === videoId()) {
|
|
45523
|
+
<svg class="vli-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2.5" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
|
|
45524
|
+
}
|
|
45525
|
+
</button>
|
|
45526
|
+
}
|
|
45527
|
+
</div>
|
|
45528
|
+
}
|
|
45529
|
+
</div>
|
|
45530
|
+
}
|
|
45531
|
+
}
|
|
45532
|
+
</aside>
|
|
45533
|
+
</div>
|
|
45534
|
+
</div>
|
|
45535
|
+
|
|
45536
|
+
<!-- Test Send -->
|
|
45537
|
+
<ui-side-panel [open]="showTestSendDialog()" title="Send Test Email" primaryLabel="Send Test" [primaryLoading]="testSending()" [primaryDisabled]="!canSendTest()"
|
|
45538
|
+
(closed)="showTestSendDialog.set(false)" (primaryClick)="onTestSend()">
|
|
45539
|
+
<p class="config-desc">Send this template to an email address to preview how it will look in an inbox.</p>
|
|
45540
|
+
@if (!editingId()) {
|
|
45541
|
+
<div class="test-save-note">
|
|
45542
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
45543
|
+
Save the template first before sending a test.
|
|
45544
|
+
</div>
|
|
45545
|
+
}
|
|
45546
|
+
<div class="config-field config-field-mt">
|
|
45547
|
+
<ui-text-input label="Recipient Email" type="email" placeholder="you@example.com" [(ngModel)]="testSendEmail" [error]="testEmailError()" (keydown.enter)="onTestSend()" />
|
|
45548
|
+
</div>
|
|
45549
|
+
@if (hasUnsavedChanges()) {
|
|
45550
|
+
<div class="test-save-note" role="note" data-testid="test-unsaved-note">
|
|
45551
|
+
You have unsaved changes. The test email sends the last saved version of this template.
|
|
45552
|
+
</div>
|
|
45553
|
+
}
|
|
45554
|
+
</ui-side-panel>
|
|
45555
|
+
|
|
45556
|
+
<!-- AI Generate -->
|
|
45557
|
+
<ui-side-panel [open]="showAiPanel()" title="Generate with AI" primaryLabel="Generate" [primaryLoading]="aiGenerating()" [primaryDisabled]="!canGenerate()"
|
|
45558
|
+
(closed)="showAiPanel.set(false)" (primaryClick)="onAiGenerate()">
|
|
45559
|
+
<p class="config-desc">Describe the email you want to write. The AI will generate professional content and add it as a new block.</p>
|
|
45560
|
+
<div class="ai-prompt-examples">
|
|
45561
|
+
<span class="ai-example-label">Try:</span>
|
|
45562
|
+
@for (ex of aiExamples(); track ex) {
|
|
45563
|
+
<ui-chip [label]="ex" size="sm" (select)="aiPrompt.set(ex)" />
|
|
45564
|
+
}
|
|
45565
|
+
</div>
|
|
45566
|
+
<div class="config-field config-field-mt">
|
|
45567
|
+
<ui-textarea-input label="What do you want to write?" [rows]="5" placeholder="e.g. A friendly follow-up email for a buyer who attended an open house last weekend" [(ngModel)]="aiPrompt" />
|
|
45568
|
+
</div>
|
|
45569
|
+
</ui-side-panel>
|
|
45570
|
+
|
|
45571
|
+
<!-- Assignment config -->
|
|
45572
|
+
<ui-side-panel [wide]="true" [open]="showAssignmentConfig()" [title]="configTarget()?.name || 'Configure Automation'" primaryLabel="Save Configuration"
|
|
45573
|
+
[primaryDisabled]="!configEmail() && !configText()" (closed)="showAssignmentConfig.set(false)" (primaryClick)="saveAssignmentConfig()">
|
|
45574
|
+
@if (configTarget(); as target) {
|
|
45575
|
+
<p class="config-desc">{{ target.description }}</p>
|
|
45576
|
+
<hr class="config-divider" />
|
|
45577
|
+
<div class="config-section">
|
|
45578
|
+
<span class="config-section-label">EMAIL TEMPLATE</span>
|
|
45579
|
+
<div class="config-field">
|
|
45580
|
+
<span class="config-label">Assigned Template</span>
|
|
45581
|
+
<div class="config-readonly">{{ name() || 'Current template' }}</div>
|
|
45582
|
+
<span class="config-hint">This template will be used when this automation is triggered.</span>
|
|
45583
|
+
</div>
|
|
45584
|
+
</div>
|
|
45585
|
+
<hr class="config-divider" />
|
|
45586
|
+
<div class="config-section">
|
|
45587
|
+
<span class="config-section-label">CHANNELS <span class="required" aria-hidden="true">*</span></span>
|
|
45588
|
+
<ui-checkbox label="Email" [(checked)]="configEmail" />
|
|
45589
|
+
<ui-checkbox label="Text (SMS)" [(checked)]="configText" [disabled]="!textEnabled()" [hint]="textEnabled() ? '' : '(not enabled)'" />
|
|
45590
|
+
</div>
|
|
45591
|
+
}
|
|
45592
|
+
</ui-side-panel>
|
|
45593
|
+
`,
|
|
45594
|
+
styles: [":host{display:block}.required{color:#ef4444;font-weight:400}.editor-page{display:flex;flex-direction:column;height:100vh;height:100dvh;background:#f8fafc;overflow:hidden}.editor-topbar{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;padding:12px 24px;background:#fff;border-bottom:1px solid #e2e8f0;flex-shrink:0}.topbar-left{display:flex;align-items:center;gap:8px;min-width:0}.topbar-right{display:flex;align-items:center;gap:8px}.breadcrumb{display:flex;align-items:center;gap:6px;font-size:.9375rem;font-weight:700;color:#1e293b;min-width:0}.breadcrumb a{color:#1e293b;text-decoration:none;cursor:pointer}.breadcrumb a:hover{color:#3b82f6}.breadcrumb svg{color:#94a3b8;flex-shrink:0}.breadcrumb span{color:#64748b;font-weight:500}.readonly-note{margin:0;padding:8px 16px;font-size:.8125rem;color:#92400e;background:#fffbeb;border-bottom:1px solid #fde68a}.readonly-note.system-note{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.editor-body{display:grid;grid-template-columns:340px minmax(0,1fr) 300px;flex:1;min-height:0;overflow:hidden}.panel-left{background:#fff;border-right:1px solid #e2e8f0;padding:24px;overflow-y:auto}.panel-title{font-size:1.0625rem;font-weight:700;color:#1e293b;margin:0 0 20px}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}.form-row-dates{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px}.assignments-section{border-top:1px solid #e2e8f0;padding-top:20px;margin-top:16px}.section-title{font-size:.9375rem;font-weight:700;color:#1e293b;margin:0 0 6px}.section-desc{font-size:.8125rem;color:#64748b;line-height:1.5;margin:0 0 12px}.assignment-list{max-height:340px;overflow-y:auto;margin-top:12px;border:1px solid #e2e8f0;border-radius:8px}.assignment-item{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 14px;cursor:pointer;border-bottom:1px solid #f1f5f9;transition:background .1s}.assignment-item:last-child{border-bottom:none}.assignment-item:hover{background:#f8fafc}.assignment-item:focus-visible{outline:2px solid #3b82f6;outline-offset:-2px}.assignment-item.assignment-readonly{cursor:default}.assignment-item.assignment-readonly:hover{background:transparent}.assignment-text{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}.assignment-text strong{font-size:.8125rem;color:#1e293b}.assignment-text span{font-size:.75rem;color:#64748b}.assignment-channels{display:flex;gap:4px;flex-shrink:0}.channel-badge{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:4px;background:#f1f5f9;color:#94a3b8;transition:all .15s}.channel-badge.active{background:#3b82f6;color:#fff}.channel-badge.disabled{opacity:.4;cursor:not-allowed}.config-desc{font-size:.875rem;color:#64748b;line-height:1.5;margin:0}.config-divider{border:none;border-top:1px solid #e2e8f0;margin:20px 0}.config-section{display:flex;flex-direction:column;gap:12px}.config-section-label{font-size:.6875rem;font-weight:700;color:#3b82f6;text-transform:uppercase;letter-spacing:.04em}.config-field{display:flex;flex-direction:column;gap:6px}.config-field-mt{margin-top:16px}.config-label{font-size:.8125rem;font-weight:600;color:#1e293b}.config-readonly{padding:9px 12px;border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;font-size:.875rem;color:#1e293b}.config-hint{font-size:.75rem;color:#64748b}.test-save-note{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;font-size:.8125rem;color:#92400e;margin-top:12px}.test-save-note svg{flex-shrink:0}.ai-prompt-examples{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:12px}.ai-example-label{font-size:.75rem;color:#64748b;font-weight:600}.panel-center{display:flex;flex-direction:column;min-height:0;min-width:0;overflow:hidden}.editor-tabs{display:flex;gap:2px;padding:12px 20px 0;background:#fff;border-bottom:1px solid #e2e8f0;overflow-x:auto;-webkit-overflow-scrolling:touch}.editor-tab{display:flex;align-items:center;gap:6px;padding:10px 16px;border:1px solid #e2e8f0;border-bottom:none;border-radius:8px 8px 0 0;background:#f8fafc;font-size:.8125rem;font-weight:600;color:#64748b;cursor:pointer;font-family:inherit;white-space:nowrap}.editor-tab:hover{background:#fff;color:#1e293b}.editor-tab.active{background:#eff6ff;color:#3b82f6;border-color:#93c5fd}.editor-tab:focus-visible,.right-tab:focus-visible,.block-group-header:focus-visible,.variable-item:focus-visible,.add-block-btn:focus-visible,.block-item:focus-visible,.video-lib-item:focus-visible{outline:2px solid #3b82f6;outline-offset:-2px}.editor-toolbar{display:flex;align-items:center;gap:8px;padding:10px 20px;background:#fff;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}.toolbar-row-2{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 20px;background:#fff;border-bottom:1px solid #e2e8f0}.toolbar-group{display:flex;align-items:center;gap:2px;padding-right:8px;border-right:1px solid #e2e8f0}.toolbar-group:last-child{border-right:none;padding-right:0}.toolbar-select{display:block;min-width:96px}.toolbar-label{font-size:.75rem;font-weight:600;color:#64748b;white-space:nowrap;margin-right:6px}.ai-dot{width:8px;height:8px;border-radius:50%;background:#3b82f6;display:inline-block;margin-right:6px}.hidden-color-input{position:absolute;width:0;height:0;opacity:0;pointer-events:none;border:none;padding:0}.color-indicator{display:flex;flex-direction:column;align-items:center;gap:2px;color:#475569}.color-bar{display:block;width:16px;height:3px;border-radius:2px;border:1px solid rgba(0,0,0,.1)}.link-dialog{background:#fff;border-bottom:1px solid #e2e8f0;padding:12px 20px}.link-dialog-inner{display:flex;align-items:flex-start;gap:8px;flex-wrap:wrap}.link-dialog-title{font-size:.8125rem;font-weight:700;color:#1e293b;white-space:nowrap;padding-top:10px}.link-dialog-input{display:block;width:240px;max-width:100%}.link-dialog-actions{display:flex;gap:6px}.preview-frame{flex:1;display:flex;justify-content:center;padding:24px;overflow-y:auto;background:#f1f5f9}.preview-frame.mobile{align-items:flex-start}.preview-content{max-width:600px;width:100%;box-sizing:border-box;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:32px;min-height:400px;font-size:.9375rem;line-height:1.6;color:#1e293b;overflow-wrap:anywhere}.preview-content p{margin:0 0 8px}.mobile-shell{width:375px;max-width:100%;box-sizing:border-box;min-height:600px;border:2px solid #1e293b;border-radius:24px;padding:40px 12px 24px;background:#fff;box-shadow:0 8px 32px #0000001f}.mobile-shell .preview-content{border:none;border-radius:0;padding:16px;max-width:100%}.blocks-canvas{flex:1;padding:16px 20px 24px;overflow-y:auto;background:#f1f5f9;display:flex;flex-direction:column}.block-wrapper{position:relative;display:flex;align-items:flex-start;background:#fff;border:2px solid transparent;border-radius:8px;transition:border-color .15s,box-shadow .15s}.block-wrapper:hover,.block-wrapper.selected,.block-wrapper:focus-within{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.block-wrapper:hover .block-drag-handle,.block-wrapper.selected .block-drag-handle,.block-wrapper:focus-within .block-drag-handle{opacity:1}.block-wrapper:hover .block-controls,.block-wrapper.selected .block-controls,.block-wrapper:focus-within .block-controls{opacity:1;pointer-events:all}.block-wrapper.dragging{opacity:.4}.block-drag-handle{flex-shrink:0;width:28px;display:flex;align-items:flex-start;justify-content:center;padding-top:14px;opacity:0;transition:opacity .15s;cursor:grab;color:#94a3b8}.block-drag-handle:active{cursor:grabbing}.block-content{flex:1;min-width:0;padding:12px 16px;outline:none;font-size:.9375rem;color:#1e293b;line-height:1.6;min-height:48px;overflow-wrap:anywhere}.block-content:empty:before{content:\"Click to edit...\";color:#94a3b8;font-style:italic;pointer-events:none}.block-content p{margin:0 0 8px}.block-content p:last-child{margin:0}.block-content h1,.block-content h2,.block-content h3{margin:8px 0 6px;color:#1e293b}.block-content a{color:#3b82f6}.block-content img{max-width:100%;border-radius:6px}.block-content ul,.block-content ol{padding-left:20px;margin:0 0 8px}.block-controls{position:absolute;top:6px;right:6px;display:flex;gap:2px;opacity:0;pointer-events:none;transition:opacity .15s;background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:3px;box-shadow:0 2px 6px #00000014}@media(hover:none){.block-wrapper.selected .block-controls{opacity:1;pointer-events:all}}.drop-zone{height:8px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:height .15s,background .15s;position:relative}.drop-zone-label{font-size:.75rem;font-weight:600;color:#3b82f6;opacity:0;transition:opacity .1s}.drop-zone.active{height:40px;background:#3b82f614;border:2px dashed #3b82f6}.drop-zone.active .drop-zone-label{opacity:1}.add-block-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;min-height:44px;padding:12px;margin-top:8px;border:2px dashed #e2e8f0;border-radius:8px;background:none;font-size:.875rem;font-weight:600;color:#64748b;cursor:pointer;font-family:inherit;transition:border-color .15s,color .15s,background .15s}.add-block-btn:hover{border-color:#3b82f6;color:#3b82f6;background:#3b82f60a}.panel-right{background:#fff;border-left:1px solid #e2e8f0;display:flex;flex-direction:column;min-height:0;min-width:0}.right-tabs{display:flex;border-bottom:1px solid #e2e8f0}.right-tab{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:12px;min-height:44px;border:none;background:none;font-size:.8125rem;font-weight:600;color:#64748b;cursor:pointer;font-family:inherit;border-bottom:2px solid transparent}.right-tab:hover{color:#1e293b}.right-tab.active{color:#3b82f6;background:#eff6ff;border-bottom-color:#3b82f6}.right-panel-body{flex:1;padding:20px;overflow-y:auto}.right-section-title{font-size:.9375rem;font-weight:700;color:#1e293b;margin:0 0 12px}.right-section-desc{font-size:.8125rem;color:#64748b;margin:0 0 16px;line-height:1.5}.right-section-hint{font-size:.8125rem;color:#64748b;margin:0 0 12px;line-height:1.5}.block-group{border-bottom:1px solid #f1f5f9}.block-group-header{display:flex;align-items:center;gap:8px;width:100%;min-height:44px;padding:12px 0;border:none;background:none;font-size:.875rem;font-weight:600;color:#1e293b;cursor:pointer;font-family:inherit;text-align:left}.block-group-header .chevron{transition:transform .2s;color:#94a3b8}.block-group-header .chevron.expanded{transform:rotate(90deg)}.block-group-header span:nth-of-type(1){flex:1}.block-count{font-size:.75rem;font-weight:600;color:#64748b;background:#f1f5f9;padding:2px 8px;border-radius:10px;margin-left:auto}.block-list,.variable-list{display:flex;flex-direction:column;gap:2px;padding-bottom:8px}.block-item{display:flex;align-items:center;gap:6px;box-sizing:border-box;width:100%;min-height:44px;padding:8px 12px 8px 28px;font-size:.8125rem;color:#64748b;cursor:pointer;text-align:left;border-radius:4px;font-family:inherit;-webkit-user-select:none;user-select:none}.block-item:hover{background:#f8fafc;color:#1e293b}.block-item-drag{color:#cbd5e1;flex-shrink:0;cursor:grab}.variable-item{display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:center;min-height:44px;padding:8px 12px 8px 28px;border:none;background:none;cursor:pointer;text-align:left;border-radius:4px;font-family:inherit}.variable-item:hover{background:#f8fafc}.var-tag{font-size:.75rem;font-weight:600;color:#3b82f6;background:#3b82f614;padding:2px 6px;border-radius:4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.var-label{font-size:.8125rem;color:#1e293b}.var-example{font-size:.75rem;color:#64748b;overflow-wrap:anywhere}.video-library-panel{padding:0}.video-library-panel .right-section-title,.video-library-panel .right-section-hint{padding:0 16px}.video-library-panel .right-section-title{padding-top:16px}.video-config-section{display:flex;flex-direction:column;gap:8px;padding:12px 16px;border-bottom:1px solid #f1f5f9}.video-config-section:last-of-type{border-bottom:none}.video-lib-list{display:flex;flex-direction:column;gap:2px;padding:8px 8px 16px}.video-lib-item{display:flex;align-items:center;gap:10px;width:100%;min-height:44px;padding:8px;border:1px solid transparent;border-radius:8px;background:none;cursor:pointer;text-align:left;font-family:inherit;transition:background .15s,border-color .15s}.video-lib-item:hover{background:#f8fafc;border-color:#e2e8f0}.video-lib-item.selected{background:#eff6ff;border-color:#93c5fd}.vli-thumb{position:relative;flex-shrink:0;width:56px;height:40px;border-radius:6px;background:linear-gradient(135deg,#f1f5f9,#e2e8f0);display:flex;align-items:center;justify-content:center}.vli-dur{position:absolute;bottom:2px;right:3px;font-size:.625rem;font-weight:600;color:#fff;background:#000000a6;padding:1px 4px;border-radius:3px}.vli-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.vli-title{font-size:.8125rem;font-weight:600;color:#1e293b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vli-cat{font-size:.75rem;color:#64748b}.vli-check{flex-shrink:0}.video-lib-empty{display:flex;flex-direction:column;align-items:center;padding:32px 16px;text-align:center;gap:8px}.video-lib-empty p{font-size:.875rem;font-weight:500;color:#475569;margin:0}.video-lib-empty span{font-size:.8125rem;color:#64748b}.video-lib-loading{padding:8px}.video-lib-skeleton{display:flex;align-items:center;gap:10px;padding:8px;margin-bottom:4px}.vls-info{flex:1;display:flex;flex-direction:column;gap:6px}@media(max-width:1200px){.editor-body{grid-template-columns:280px minmax(0,1fr) 260px}}@media(max-width:900px){.editor-page{height:auto;min-height:100vh;min-height:100dvh;overflow:visible}.editor-body{display:flex;flex-direction:column;overflow:visible}.editor-topbar{padding:12px 16px}.panel-left{border-right:none;border-bottom:1px solid #e2e8f0;overflow:visible;padding:16px}.panel-center{overflow:visible;min-height:480px}.blocks-canvas{overflow:visible;min-height:320px}.panel-right{border-left:none;border-top:1px solid #e2e8f0}.right-panel-body{overflow:visible}.editor-toolbar,.toolbar-row-2,.link-dialog,.editor-tabs{padding-left:12px;padding-right:12px}.preview-frame{padding:12px}.preview-content{padding:16px}.form-row-dates{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*{transition:none!important}}\n"]
|
|
45595
|
+
}]
|
|
45596
|
+
}],
|
|
45597
|
+
ctorParameters: () => [],
|
|
45598
|
+
propDecorators: {
|
|
45599
|
+
template: [{
|
|
45600
|
+
type: i0.Input,
|
|
45601
|
+
args: [{
|
|
45602
|
+
isSignal: true,
|
|
45603
|
+
alias: "template",
|
|
45604
|
+
required: false
|
|
45605
|
+
}]
|
|
45606
|
+
}],
|
|
45607
|
+
categories: [{
|
|
45608
|
+
type: i0.Input,
|
|
45609
|
+
args: [{
|
|
45610
|
+
isSignal: true,
|
|
45611
|
+
alias: "categories",
|
|
45612
|
+
required: false
|
|
45613
|
+
}]
|
|
45614
|
+
}],
|
|
45615
|
+
categoryPlaceholder: [{
|
|
45616
|
+
type: i0.Input,
|
|
45617
|
+
args: [{
|
|
45618
|
+
isSignal: true,
|
|
45619
|
+
alias: "categoryPlaceholder",
|
|
45620
|
+
required: false
|
|
45621
|
+
}]
|
|
45622
|
+
}],
|
|
45623
|
+
assignmentOptions: [{
|
|
45624
|
+
type: i0.Input,
|
|
45625
|
+
args: [{
|
|
45626
|
+
isSignal: true,
|
|
45627
|
+
alias: "assignmentOptions",
|
|
45628
|
+
required: false
|
|
45629
|
+
}]
|
|
45630
|
+
}],
|
|
45631
|
+
featuredAssignmentKey: [{
|
|
45632
|
+
type: i0.Input,
|
|
45633
|
+
args: [{
|
|
45634
|
+
isSignal: true,
|
|
45635
|
+
alias: "featuredAssignmentKey",
|
|
45636
|
+
required: false
|
|
45637
|
+
}]
|
|
45638
|
+
}],
|
|
45639
|
+
assignmentsDescription: [{
|
|
45640
|
+
type: i0.Input,
|
|
45641
|
+
args: [{
|
|
45642
|
+
isSignal: true,
|
|
45643
|
+
alias: "assignmentsDescription",
|
|
45644
|
+
required: false
|
|
45645
|
+
}]
|
|
45646
|
+
}],
|
|
45647
|
+
textEnabled: [{
|
|
45648
|
+
type: i0.Input,
|
|
45649
|
+
args: [{
|
|
45650
|
+
isSignal: true,
|
|
45651
|
+
alias: "textEnabled",
|
|
45652
|
+
required: false
|
|
45653
|
+
}]
|
|
45654
|
+
}],
|
|
45655
|
+
variableGroups: [{
|
|
45656
|
+
type: i0.Input,
|
|
45657
|
+
args: [{
|
|
45658
|
+
isSignal: true,
|
|
45659
|
+
alias: "variableGroups",
|
|
45660
|
+
required: false
|
|
45661
|
+
}]
|
|
45662
|
+
}],
|
|
45663
|
+
blockGroups: [{
|
|
45664
|
+
type: i0.Input,
|
|
45665
|
+
args: [{
|
|
45666
|
+
isSignal: true,
|
|
45667
|
+
alias: "blockGroups",
|
|
45668
|
+
required: false
|
|
45669
|
+
}]
|
|
45670
|
+
}],
|
|
45671
|
+
aiExamples: [{
|
|
45672
|
+
type: i0.Input,
|
|
45673
|
+
args: [{
|
|
45674
|
+
isSignal: true,
|
|
45675
|
+
alias: "aiExamples",
|
|
45676
|
+
required: false
|
|
45677
|
+
}]
|
|
45678
|
+
}],
|
|
45679
|
+
namePlaceholder: [{
|
|
45680
|
+
type: i0.Input,
|
|
45681
|
+
args: [{
|
|
45682
|
+
isSignal: true,
|
|
45683
|
+
alias: "namePlaceholder",
|
|
45684
|
+
required: false
|
|
45685
|
+
}]
|
|
45686
|
+
}],
|
|
45687
|
+
subjectPlaceholder: [{
|
|
45688
|
+
type: i0.Input,
|
|
45689
|
+
args: [{
|
|
45690
|
+
isSignal: true,
|
|
45691
|
+
alias: "subjectPlaceholder",
|
|
45692
|
+
required: false
|
|
45693
|
+
}]
|
|
45694
|
+
}],
|
|
45695
|
+
backLabel: [{
|
|
45696
|
+
type: i0.Input,
|
|
45697
|
+
args: [{
|
|
45698
|
+
isSignal: true,
|
|
45699
|
+
alias: "backLabel",
|
|
45700
|
+
required: false
|
|
45701
|
+
}]
|
|
45702
|
+
}],
|
|
45703
|
+
createLabel: [{
|
|
45704
|
+
type: i0.Input,
|
|
45705
|
+
args: [{
|
|
45706
|
+
isSignal: true,
|
|
45707
|
+
alias: "createLabel",
|
|
45708
|
+
required: false
|
|
45709
|
+
}]
|
|
45710
|
+
}],
|
|
45711
|
+
editLabel: [{
|
|
45712
|
+
type: i0.Input,
|
|
45713
|
+
args: [{
|
|
45714
|
+
isSignal: true,
|
|
45715
|
+
alias: "editLabel",
|
|
45716
|
+
required: false
|
|
45717
|
+
}]
|
|
45718
|
+
}],
|
|
45719
|
+
systemNote: [{
|
|
45720
|
+
type: i0.Input,
|
|
45721
|
+
args: [{
|
|
45722
|
+
isSignal: true,
|
|
45723
|
+
alias: "systemNote",
|
|
45724
|
+
required: false
|
|
45725
|
+
}]
|
|
45726
|
+
}],
|
|
45727
|
+
readOnlyNote: [{
|
|
45728
|
+
type: i0.Input,
|
|
45729
|
+
args: [{
|
|
45730
|
+
isSignal: true,
|
|
45731
|
+
alias: "readOnlyNote",
|
|
45732
|
+
required: false
|
|
45733
|
+
}]
|
|
45734
|
+
}],
|
|
45735
|
+
idPrefix: [{
|
|
45736
|
+
type: i0.Input,
|
|
45737
|
+
args: [{
|
|
45738
|
+
isSignal: true,
|
|
45739
|
+
alias: "idPrefix",
|
|
45740
|
+
required: false
|
|
45741
|
+
}]
|
|
45742
|
+
}],
|
|
45743
|
+
maxBodyLength: [{
|
|
45744
|
+
type: i0.Input,
|
|
45745
|
+
args: [{
|
|
45746
|
+
isSignal: true,
|
|
45747
|
+
alias: "maxBodyLength",
|
|
45748
|
+
required: false
|
|
45749
|
+
}]
|
|
45750
|
+
}],
|
|
45751
|
+
maxTextLength: [{
|
|
45752
|
+
type: i0.Input,
|
|
45753
|
+
args: [{
|
|
45754
|
+
isSignal: true,
|
|
45755
|
+
alias: "maxTextLength",
|
|
45756
|
+
required: false
|
|
45757
|
+
}]
|
|
45758
|
+
}],
|
|
45759
|
+
fullAccess: [{
|
|
45760
|
+
type: i0.Input,
|
|
45761
|
+
args: [{
|
|
45762
|
+
isSignal: true,
|
|
45763
|
+
alias: "fullAccess",
|
|
45764
|
+
required: false
|
|
45765
|
+
}]
|
|
45766
|
+
}],
|
|
45767
|
+
systemTemplate: [{
|
|
45768
|
+
type: i0.Input,
|
|
45769
|
+
args: [{
|
|
45770
|
+
isSignal: true,
|
|
45771
|
+
alias: "systemTemplate",
|
|
45772
|
+
required: false
|
|
45773
|
+
}]
|
|
45774
|
+
}],
|
|
45775
|
+
aiEnabled: [{
|
|
45776
|
+
type: i0.Input,
|
|
45777
|
+
args: [{
|
|
45778
|
+
isSignal: true,
|
|
45779
|
+
alias: "aiEnabled",
|
|
45780
|
+
required: false
|
|
45781
|
+
}]
|
|
45782
|
+
}],
|
|
45783
|
+
testSendEnabled: [{
|
|
45784
|
+
type: i0.Input,
|
|
45785
|
+
args: [{
|
|
45786
|
+
isSignal: true,
|
|
45787
|
+
alias: "testSendEnabled",
|
|
45788
|
+
required: false
|
|
45789
|
+
}]
|
|
45790
|
+
}],
|
|
45791
|
+
videosEnabled: [{
|
|
45792
|
+
type: i0.Input,
|
|
45793
|
+
args: [{
|
|
45794
|
+
isSignal: true,
|
|
45795
|
+
alias: "videosEnabled",
|
|
45796
|
+
required: false
|
|
45797
|
+
}]
|
|
45798
|
+
}],
|
|
45799
|
+
saving: [{
|
|
45800
|
+
type: i0.Input,
|
|
45801
|
+
args: [{
|
|
45802
|
+
isSignal: true,
|
|
45803
|
+
alias: "saving",
|
|
45804
|
+
required: false
|
|
45805
|
+
}]
|
|
45806
|
+
}],
|
|
45807
|
+
testSending: [{
|
|
45808
|
+
type: i0.Input,
|
|
45809
|
+
args: [{
|
|
45810
|
+
isSignal: true,
|
|
45811
|
+
alias: "testSending",
|
|
45812
|
+
required: false
|
|
45813
|
+
}]
|
|
45814
|
+
}],
|
|
45815
|
+
aiGenerating: [{
|
|
45816
|
+
type: i0.Input,
|
|
45817
|
+
args: [{
|
|
45818
|
+
isSignal: true,
|
|
45819
|
+
alias: "aiGenerating",
|
|
45820
|
+
required: false
|
|
45821
|
+
}]
|
|
45822
|
+
}],
|
|
45823
|
+
videos: [{
|
|
45824
|
+
type: i0.Input,
|
|
45825
|
+
args: [{
|
|
45826
|
+
isSignal: true,
|
|
45827
|
+
alias: "videos",
|
|
45828
|
+
required: false
|
|
45829
|
+
}]
|
|
45830
|
+
}],
|
|
45831
|
+
videosLoading: [{
|
|
45832
|
+
type: i0.Input,
|
|
45833
|
+
args: [{
|
|
45834
|
+
isSignal: true,
|
|
45835
|
+
alias: "videosLoading",
|
|
45836
|
+
required: false
|
|
45837
|
+
}]
|
|
45838
|
+
}],
|
|
45839
|
+
videoDefaults: [{
|
|
45840
|
+
type: i0.Input,
|
|
45841
|
+
args: [{
|
|
45842
|
+
isSignal: true,
|
|
45843
|
+
alias: "videoDefaults",
|
|
45844
|
+
required: false
|
|
45845
|
+
}]
|
|
45846
|
+
}],
|
|
45847
|
+
videoLink: [{
|
|
45848
|
+
type: i0.Input,
|
|
45849
|
+
args: [{
|
|
45850
|
+
isSignal: true,
|
|
45851
|
+
alias: "videoLink",
|
|
45852
|
+
required: false
|
|
45853
|
+
}]
|
|
45854
|
+
}],
|
|
45855
|
+
noVideosHint: [{
|
|
45856
|
+
type: i0.Input,
|
|
45857
|
+
args: [{
|
|
45858
|
+
isSignal: true,
|
|
45859
|
+
alias: "noVideosHint",
|
|
45860
|
+
required: false
|
|
45861
|
+
}]
|
|
45862
|
+
}],
|
|
45863
|
+
back: [{
|
|
45864
|
+
type: i0.Output,
|
|
45865
|
+
args: ["back"]
|
|
45866
|
+
}],
|
|
45867
|
+
save: [{
|
|
45868
|
+
type: i0.Output,
|
|
45869
|
+
args: ["save"]
|
|
45870
|
+
}],
|
|
45871
|
+
testSend: [{
|
|
45872
|
+
type: i0.Output,
|
|
45873
|
+
args: ["testSend"]
|
|
45874
|
+
}],
|
|
45875
|
+
aiGenerate: [{
|
|
45876
|
+
type: i0.Output,
|
|
45877
|
+
args: ["aiGenerate"]
|
|
45878
|
+
}],
|
|
45879
|
+
videosRequested: [{
|
|
45880
|
+
type: i0.Output,
|
|
45881
|
+
args: ["videosRequested"]
|
|
45882
|
+
}],
|
|
45883
|
+
useAsMine: [{
|
|
45884
|
+
type: i0.Output,
|
|
45885
|
+
args: ["useAsMine"]
|
|
45886
|
+
}],
|
|
45887
|
+
blockEditors: [{
|
|
45888
|
+
type: i0.ViewChildren,
|
|
45889
|
+
args: ["blockEditor", { isSignal: true }]
|
|
45890
|
+
}],
|
|
45891
|
+
colorInputRef: [{
|
|
45892
|
+
type: i0.ViewChild,
|
|
45893
|
+
args: ["colorInput", { isSignal: true }]
|
|
45894
|
+
}]
|
|
45895
|
+
}
|
|
45896
|
+
});
|
|
45897
|
+
export { ActionBarComponent, AlertComponent, AppShellComponent, AudioPlayerComponent, AvatarComponent, BADGE_VARIANT_STYLES, BadgeComponent, BarRowComponent, BoardCardDirective, BoardComponent, BulkMessagePanelComponent, BusinessHoursComponent, ButtonComponent, CalendarComponent, CardListItemComponent, CheckboxComponent, CheckboxTrailingDirective, ChipComponent, ChipGroupComponent, ColorInputComponent, ColumnFilterComponent, ColumnsComponent, ComboboxComponent, ConfirmOutletComponent, ConfirmService, ContactPickerComponent, DEFAULT_EMAIL_TEMPLATE_BLOCK_GROUPS, DEFAULT_EMAIL_TEMPLATE_VARIABLE_GROUPS, DEFAULT_PASSWORD_RULES, DROPDOWN_GROUP, DataTableComponent, DateInputComponent, DateRangePickerComponent, DatetimeInputComponent, DetailCardComponent, DiscountSelectorComponent, DividerComponent, DropdownComponent, EMAIL_BLOCK_DRAG_TYPE, EMAIL_TEMPLATE_MAX_BODY, EMAIL_TEMPLATE_MAX_TEXT, EMAIL_TEMPLATE_START_TEXT, EmailTemplateEditorComponent, EmptyStateComponent, ErrorComponent, ErrorStateComponent, FORGEROUTE_CONFIG, FadeComponent, FileInputComponent, FormColSpanDirective, FormComponent, FormRowComponent, FormSectionComponent, HintComponent, IDX_BEHAVIOR_SIGNALS, INTEGRATION_CATALOG, INTEGRATION_FIELD_DEFS, INTEGRATION_GROUP_ORDER, INTEGRATION_OPTION_DEFS, IconComponent, ImageComponent, ImageGalleryComponent, ImageGalleryDialogComponent, ImagePreviewComponent, ImageRepositionComponent, ImportExportComponent, KpiCardComponent, LAYOUT_GAP_PX, ListComponent, ListItemComponent, LiveRegionComponent, MAX_EMAIL_ADDRESS_LENGTH, ModalComponent, MoreMenuComponent, MultiSelectComponent, NOTES_MAX_LENGTH, NotesComponent, PHONE_NUMBER_AREA_CODE_LENGTH, PHONE_NUMBER_ZIP_LENGTH, PageComponent, PaginationComponent, PanelInfoListComponent, PasswordRequirementsComponent, PdfHotspotComponent, PhoneNumberPickerComponent, PillTabsComponent, ProgressBarComponent, ProgressRingComponent, QrCodeComponent, QuickFilterTabsComponent, RICH_TEXT_OPTIONS, RadioComponent, RadioGroupComponent, RadioMatrixComponent, RadioOptionContentDirective, ReadonlyFieldsetComponent, RichTextEditorComponent, SEMANTIC_BADGE_MAP, SETUP_DONE_KEY, SETUP_INTEGRATIONS, SearchInputComponent, SectionHeaderComponent, SelectComponent, SetupWizardComponent, SetupWizardStepDirective, ShowComponent, SidePanelComponent, SidebarLayoutComponent, SignaturePadComponent, SkeletonComponent, StackComponent, StarComponent, StepperComponent, TableToolbarComponent, TextComponent, TextInputComponent, TextareaInputComponent, TimeInputComponent, TimelineComponent, ToastComponent, ToastService, ToggleBadgeComponent, ToggleComponent, TooltipDirective, US_STATES, UiFieldValidity, acceptLabel, appShellGroupsStorageKey, autoVariant, blockHasContent, buildDefaultSchedule, buildVideoBlockHtml, builtInActionIcon, businessTimeOptions, createFieldReveal, decodeFilterValues, digitsOnly, downloadBlob, encodeFilterValue, encodeFilterValues, errorContextInterceptor, escapeHtml, fileMatchesAccept, formatAddress, formatCategoryLabel, formatCurrency, formatFileSize, formatIsoDate, formatIsoDateTime, formatTime, formatVideoDuration, getEligibleIds, getEligibleOptions, getErrorMessage, humanReadableMessage, iconPath, insideEditorBlock, integrationPlainText, integrationsForApp, integrationsOnlyFor, isBlockDrag, isValidEmailAddress, isWebUrl, isoKey, issueToErrors, maxLengthIssue, normalizeHexColor, parseBizHours, parseIso, parseTime, passwordMeetsRules, pluralize, provideForgerouteToast, reorderItems, requiredIssue, resolveLayoutGap, resolvePreset, serializeBizHours, setupIntegrations };
|
|
41671
45898
|
|
|
41672
45899
|
//# sourceMappingURL=forgeroute-ui.mjs.map
|