@aglyn/plugins-marketing 1.0.0-beta.198 → 1.0.0-beta.200

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.
Files changed (33) hide show
  1. package/package.json +15 -15
  2. package/src/lib/components/campaign-detail-card.js +106 -36
  3. package/src/lib/components/campaign-detail-card.js.map +1 -1
  4. package/src/lib/components/campaign-queries.d.ts +11 -0
  5. package/src/lib/components/campaign-queries.js +13 -0
  6. package/src/lib/components/campaign-queries.js.map +1 -1
  7. package/src/lib/components/campaigns-card.d.ts +8 -6
  8. package/src/lib/components/campaigns-card.js +283 -125
  9. package/src/lib/components/campaigns-card.js.map +1 -1
  10. package/src/lib/components/emails-list-card.d.ts +11 -15
  11. package/src/lib/components/emails-list-card.js +139 -142
  12. package/src/lib/components/emails-list-card.js.map +1 -1
  13. package/src/lib/components/host-experiments-card.component.js +107 -78
  14. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  15. package/src/lib/components/org-experiments-card.js +7 -3
  16. package/src/lib/components/org-experiments-card.js.map +1 -1
  17. package/src/lib/model/campaign-list-query.d.ts +99 -0
  18. package/src/lib/model/campaign-list-query.js +373 -0
  19. package/src/lib/model/campaign-list-query.js.map +1 -0
  20. package/src/lib/model/experiment-list-query.d.ts +22 -0
  21. package/src/lib/model/experiment-list-query.js +85 -0
  22. package/src/lib/model/experiment-list-query.js.map +1 -0
  23. package/src/lib/server/campaign-manage.d.ts +1 -1
  24. package/src/lib/server/campaign-manage.js +30 -18
  25. package/src/lib/server/campaign-manage.js.map +1 -1
  26. package/src/lib/server/campaign-send.d.ts +7 -0
  27. package/src/lib/server/campaign-send.js +17 -5
  28. package/src/lib/server/campaign-send.js.map +1 -1
  29. package/src/lib/server/email-events.js +3 -1
  30. package/src/lib/server/email-events.js.map +1 -1
  31. package/src/lib/components/email-send-filter.d.ts +0 -32
  32. package/src/lib/components/email-send-filter.js +0 -55
  33. package/src/lib/components/email-send-filter.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-marketing",
3
- "version": "1.0.0-beta.198",
3
+ "version": "1.0.0-beta.200",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,20 +25,20 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.198",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.198",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.198",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.198",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.198",
33
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.198",
34
- "@aglyn/shared-ui-next": "1.0.0-beta.198",
35
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.198",
36
- "@aglyn/shared-util-email": "1.0.0-beta.198",
37
- "@aglyn/shared-util-http": "1.0.0-beta.198",
38
- "@aglyn/shared-util-timestamp": "1.0.0-beta.198",
39
- "@aglyn/tenant-data-admin": "1.0.0-beta.198",
40
- "@aglyn/tenant-feature-instance": "1.0.0-beta.198",
41
- "@aglyn/tenant-runtime": "1.0.0-beta.198",
28
+ "@aglyn/aglyn": "1.0.0-beta.200",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.200",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.200",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.200",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.200",
33
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.200",
34
+ "@aglyn/shared-ui-next": "1.0.0-beta.200",
35
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.200",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.200",
37
+ "@aglyn/shared-util-http": "1.0.0-beta.200",
38
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.200",
39
+ "@aglyn/tenant-data-admin": "1.0.0-beta.200",
40
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.200",
41
+ "@aglyn/tenant-runtime": "1.0.0-beta.200",
42
42
  "@swc/helpers": "0.5.23"
43
43
  },
44
44
  "peerDependencies": {
@@ -20,11 +20,16 @@ import { mdiDeleteOutline, mdiEyeOutline, mdiPaletteOutline, mdiPencilOutline }
20
20
  import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
21
21
  import { Figure, RateRow, Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
22
22
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
23
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
24
+ import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
23
25
  import { ListRowActions, ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
24
- import { useListRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-rows-filter";
26
+ import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
27
+ import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
28
+ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
25
29
  import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
26
30
  import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
27
31
  import { ceilingedWindow } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
32
+ import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
28
33
  import { PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
29
34
  import { Alert, Box, Button, Chip, Divider, MenuItem, Stack, TextField, Typography } from "@mui/material";
30
35
  import { collection, deleteField, documentId, limit, orderBy, query, updateDoc, where } from "firebase/firestore";
@@ -33,13 +38,13 @@ import { useRouter } from "next/navigation";
33
38
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
34
39
  import { useFirestore, useFirestoreCollection, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
35
40
  import { campaignForcedListUnsubscribeSends, campaignListUnsubscribe, campaignRollup, campaignSendDisplay, campaignSiteIds, campaignVisibleTo, campaignWindowState, emailListTimeMs } from "@aglyn/shared-ui-email-campaigns/model";
36
- import { EMAIL_STATE_FILTER_FIELD, EMAIL_STATE_OPTIONS, EMAIL_SUBJECT_FILTER_FIELD, emailFilterValues } from "./email-send-filter.js";
41
+ import { CAMPAIGN_EMAILS_QUERY, CAMPAIGN_SEND_STATUS_OPTIONS, campaignContainerSearchFields, campaignSendsScope } from "../model/campaign-list-query.js";
37
42
  import CampaignComposer from "./campaign-composer.js";
38
43
  import CampaignEditDrawer from "./campaign-edit-drawer.js";
39
44
  import { CampaignConversionsSection, CampaignDestinationsSection, CampaignRevenueSection, CampaignSequencesSection } from "./campaign-reach-sections.js";
40
45
  import CampaignMembersSection from "./campaign-members-section.js";
41
46
  import CampaignReportCard from "./campaign-report-card.js";
42
- import { campaignContainerDoc, campaignSendsQuery } from "./campaign-queries.js";
47
+ import { campaignContainerDoc, campaignSendsCollection, campaignSendsQuery } from "./campaign-queries.js";
43
48
  import { orgSiteHubPath, orgSiteName, orgSiteOptions, topicCatalogHostId, useMarketingOrgId, useMarketingOrgMount } from "./marketing-org-mount.js";
44
49
  import { useCampaignManageApi } from "./use-campaign-send-api.js";
45
50
  import { useEmailsHubPath } from "./use-emails-hub-path.js";
@@ -61,20 +66,20 @@ const detailDocsHelp = pluginDocsHelp('emailCampaigns', {
61
66
  value: value.value,
62
67
  note: value.value !== null && value.recorded < value.sends ? `across ${value.recorded} of ${value.sends} emails` : `across ${value.sends} email${value.sends === 1 ? '' : 's'}`
63
68
  });
64
- /* What the campaign's emails table filters by (AGL-3317). */ const SEND_FILTER_FIELDS = [
65
- EMAIL_SUBJECT_FILTER_FIELD,
66
- EMAIL_STATE_FILTER_FIELD
67
- ];
69
+ /*
70
+ * What the campaign's emails table filters by, each clause and the search on
71
+ * the table's own Firestore query (AGL-3321, `CAMPAIGN_EMAILS_QUERY`). The
72
+ * State column is derived from an email's counters and cannot be asked of a
73
+ * query, so the panel offers the Status it stores.
74
+ */ const SEND_FILTER_FIELDS = CAMPAIGN_EMAILS_QUERY.fields;
68
75
  const SEND_FILTER_OPTIONS = {
69
- state: EMAIL_STATE_OPTIONS
76
+ status: CAMPAIGN_SEND_STATUS_OPTIONS
70
77
  };
71
78
  const SEND_FILTER_HEADERS = {
72
79
  subject: 'Subject',
73
- state: 'State'
80
+ status: 'Status',
81
+ createdAtMs: 'Created'
74
82
  };
75
- const SEND_SEARCH_FIELDS = [
76
- 'subjectText'
77
- ];
78
83
  /**
79
84
  * ONE CAMPAIGN: its lists, what it caused, what it earned, where it sent
80
85
  * people, and the emails it did all of that with.
@@ -232,17 +237,43 @@ const SEND_SEARCH_FIELDS = [
232
237
  sends
233
238
  ]);
234
239
  /*
235
- * The emails table's Filters panel and search (AGL-3317), answered over
236
- * every email the card read — the same window the figures above cover.
237
- */ const sendFilterRows = useMemo(()=>sends.map((send)=>_extends({}, send, emailFilterValues(send))), [
238
- sends
239
- ]);
240
- const sendFilter = useListRowsFilter({
241
- rows: sendFilterRows,
242
- fields: SEND_FILTER_FIELDS,
243
- options: SEND_FILTER_OPTIONS,
244
- headers: SEND_FILTER_HEADERS,
245
- search: SEND_SEARCH_FIELDS
240
+ * THE EMAILS TABLE IS ITS OWN QUERY (AGL-3321).
241
+ *
242
+ * The figures above are a sum over the window read above; the table under
243
+ * them is a list a reader filters and searches, so each clause and the
244
+ * search word go onto a query of their own — this campaign's emails
245
+ * (`emailCampaignId == id`, and under a site the ones sent as it), newest
246
+ * first, paged by the query. A match on the fifth page is found; nothing
247
+ * is matched over the rows a read happened to hold. Gated on the container
248
+ * like the window: a single send's report reads none of it.
249
+ */ const gridFilter = useListGridFilter({
250
+ selectFields: [
251
+ 'status'
252
+ ]
253
+ });
254
+ const sendPage = useListQuery({
255
+ collection: campaign ? campaignSendsCollection(firestore, orgId) : null,
256
+ declaration: CAMPAIGN_EMAILS_QUERY,
257
+ request: {
258
+ clauses: gridFilter.clauses,
259
+ search: gridFilter.searchWords,
260
+ base: [
261
+ {
262
+ path: 'emailCampaignId',
263
+ op: '==',
264
+ value: campaignId
265
+ },
266
+ ...campaignSendsScope(hostId)
267
+ ]
268
+ },
269
+ deps: [
270
+ firestore,
271
+ orgId,
272
+ hostId,
273
+ campaignId,
274
+ Boolean(campaign)
275
+ ],
276
+ idField: '$id'
246
277
  });
247
278
  // The ids the two sections beneath the figures join on. Derived from the
248
279
  // window this card already holds, so neither of them reads the send list
@@ -278,16 +309,18 @@ const SEND_SEARCH_FIELDS = [
278
309
  setSaving(true);
279
310
  setSaveError(null);
280
311
  try {
312
+ const placement = !hostId && values.siteIds !== undefined ? {
313
+ visibleTo: campaignVisibleTo(values.siteIds)
314
+ } : {};
281
315
  await updateDoc(campaignContainerDoc(firestore, orgId, campaignId), _extends({
282
- name: values.name,
316
+ name: values.name
317
+ }, campaignContainerSearchFields(values.name), {
283
318
  startAtMs: values.startAtMs,
284
319
  endAtMs: values.endAtMs,
285
320
  listIds: values.listIds,
286
321
  topicId: values.topicId ? values.topicId : deleteField(),
287
322
  listUnsubscribe: values.listUnsubscribe
288
- }, !hostId && values.siteIds !== undefined ? {
289
- visibleTo: campaignVisibleTo(values.siteIds)
290
- } : {}));
323
+ }, placement));
291
324
  setEditing(false);
292
325
  enqueueSnackbar('Campaign updated', {
293
326
  variant: 'success',
@@ -470,7 +503,7 @@ const SEND_SEARCH_FIELDS = [
470
503
  * The campaign's emails, on the surface's own row grammar: the row opens
471
504
  * the email's report, its subject is also a real link so it can be
472
505
  * middle-clicked and copied, and the trailing cluster holds the actions.
473
- * The window is one the card already holds, so the grid pages it.
506
+ * A page of the table's own query, paged by that query.
474
507
  */ const sendColumns = [
475
508
  {
476
509
  field: 'subject',
@@ -845,22 +878,59 @@ const SEND_SEARCH_FIELDS = [
845
878
  sends.length ? /*#__PURE__*/ _jsxs(Stack, {
846
879
  spacing: 0.5,
847
880
  children: [
848
- /*#__PURE__*/ _jsx(ListFilterChips, _extends({}, sendFilter.chipsProps)),
849
- /*#__PURE__*/ _jsx(ListTable, _extends({
881
+ /*#__PURE__*/ _jsx(ListFilterChips, {
882
+ fields: SEND_FILTER_FIELDS,
883
+ headers: SEND_FILTER_HEADERS,
884
+ clauses: gridFilter.clauses,
885
+ onChange: gridFilter.setClauses,
886
+ options: SEND_FILTER_OPTIONS
887
+ }),
888
+ /*#__PURE__*/ _jsx(ListQueryNotices, {
889
+ refused: listQueryRefusals(sendPage.plan.refused, {
890
+ fields: SEND_FILTER_FIELDS,
891
+ headers: SEND_FILTER_HEADERS,
892
+ options: SEND_FILTER_OPTIONS
893
+ }),
894
+ notices: sendPage.plan.notices
895
+ }),
896
+ /*#__PURE__*/ _jsx(ListTable, {
850
897
  "aria-label": "The campaign's emails",
851
- rows: sendFilter.rows,
852
- columns: sendFilter.filterColumns(sendColumns),
898
+ rows: sendPage.rows,
899
+ columns: listFilterGridColumns(sendColumns, SEND_FILTER_FIELDS, SEND_FILTER_OPTIONS, SEND_FILTER_HEADERS),
853
900
  rowHeight: TABLE_ROW_HEIGHT,
854
901
  onOpen: (_id, send)=>{
855
902
  const href = sendHref(send);
856
903
  if (href) router.push(href);
857
- }
858
- }, sendFilter.gridProps, {
904
+ },
905
+ loading: sendPage.status === 'loading',
906
+ /*
907
+ * The panel and the search are the grid's; every clause and the
908
+ * search word are on the table's query (AGL-3321), and the grid
909
+ * neither filters nor sorts the page it is handed.
910
+ */ filterMode: "server",
911
+ filterModel: gridFilter.filterModel,
912
+ onFilterModelChange: gridFilter.onFilterModelChange,
913
+ quickFilter: true,
914
+ disableColumnSorting: true,
915
+ hideFooter: true,
916
+ initialState: {
917
+ columns: {
918
+ columnVisibilityModel: hiddenFilterVisibility(SEND_FILTER_FIELDS, sendColumns.map((column)=>String(column.field)))
919
+ }
920
+ },
859
921
  noRowsLabel: "No emails match these filters"
860
- })),
922
+ }),
923
+ /*#__PURE__*/ _jsx(ListPagination, {
924
+ page: sendPage.page,
925
+ pageSize: sendPage.pageSize,
926
+ rowCount: sendPage.rows.length,
927
+ hasMore: sendPage.hasMore,
928
+ onPageChange: sendPage.setPage,
929
+ onPageSizeChange: sendPage.setPageSize
930
+ }),
861
931
  sendsTruncated ? /*#__PURE__*/ _jsx(Alert, {
862
932
  severity: "info",
863
- children: `Showing ${CAMPAIGN_EMAIL_CEILING} of this campaign's ` + 'emails. It has sent more — the ones listed are not ' + 'necessarily the most recent, because a send carries no ' + 'date field that every writer stamps, and the figures above ' + 'cover the emails listed.'
933
+ children: `The figures above cover ${CAMPAIGN_EMAIL_CEILING} of this ` + 'campaign’s emails. It has sent more; the table lists ' + 'every one of them.'
864
934
  }) : null
865
935
  ]
866
936
  }) : /*#__PURE__*/ _jsx(Typography, {