@aglyn/plugins-crm 1.0.0-beta.218 → 1.0.0-beta.219
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/package.json +14 -13
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.js +12 -4
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/company-detail-page.js +11 -0
- package/src/lib/components/company-detail-page.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +12 -2
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-import-drawer.d.ts +1 -1
- package/src/lib/components/company-picker.js +10 -3
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -4
- package/src/lib/components/contact-detail-page.js +50 -53
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -0
- package/src/lib/components/contact-list-columns.js +21 -5
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +2 -1
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +4 -3
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
- package/src/lib/components/contacts-bulk-bar.js +24 -51
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-export-all-button.d.ts +1 -1
- package/src/lib/components/crm-export-all-button.js.map +1 -1
- package/src/lib/components/crm-send-email-dialog.js +9 -6
- package/src/lib/components/crm-send-email-dialog.js.map +1 -1
- package/src/lib/components/csv-import-drawer.js +2 -2
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/deal-detail-page.js +18 -2
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +10 -0
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.js +13 -5
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +6 -0
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +25 -5
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +6 -0
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +4 -5
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.js +6 -0
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +28 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +36 -7
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/org-lead-surfaces-note.js +6 -2
- package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
- package/src/lib/components/recipes-card.js +9 -4
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-sharing-card.d.ts +57 -0
- package/src/lib/components/record-sharing-card.js +244 -0
- package/src/lib/components/record-sharing-card.js.map +1 -0
- package/src/lib/components/settings-section.js +7 -0
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/share-records-dialog.d.ts +28 -0
- package/src/lib/components/share-records-dialog.js +184 -0
- package/src/lib/components/share-records-dialog.js.map +1 -0
- package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
- package/src/lib/components/sharing-rule-drawer.js +374 -0
- package/src/lib/components/sharing-rule-drawer.js.map +1 -0
- package/src/lib/components/sharing-rules-card.d.ts +24 -0
- package/src/lib/components/sharing-rules-card.js +287 -0
- package/src/lib/components/sharing-rules-card.js.map +1 -0
- package/src/lib/components/use-crm-api.d.ts +1 -1
- package/src/lib/components/use-crm-api.js.map +1 -1
- package/src/lib/components/use-site-route-context.d.ts +25 -0
- package/src/lib/components/use-site-route-context.js +58 -0
- package/src/lib/components/use-site-route-context.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +31 -0
- package/src/lib/constants/api-routes.js +27 -1
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +37 -0
- package/src/lib/declarations.console-server.js +103 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.d.ts +6 -0
- package/src/lib/declarations.server.js +33 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-contact-remove.d.ts +35 -0
- package/src/lib/hooks/use-contact-remove.js +77 -0
- package/src/lib/hooks/use-contact-remove.js.map +1 -0
- package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
- package/src/lib/hooks/use-crm-sharing.js +109 -0
- package/src/lib/hooks/use-crm-sharing.js.map +1 -0
- package/src/lib/model/companies-csv.d.ts +1 -1
- package/src/lib/model/companies-csv.js +2 -2
- package/src/lib/model/companies-csv.js.map +1 -1
- package/src/lib/model/contact-holder.d.ts +68 -0
- package/src/lib/model/contact-holder.js +129 -0
- package/src/lib/model/contact-holder.js.map +1 -0
- package/src/lib/model/contact-interaction-links.d.ts +46 -0
- package/src/lib/model/contact-interaction-links.js +65 -0
- package/src/lib/model/contact-interaction-links.js.map +1 -0
- package/src/lib/model/contact-merge.d.ts +136 -0
- package/src/lib/model/contact-merge.js +412 -0
- package/src/lib/model/contact-merge.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +7 -1
- package/src/lib/model/contact-record.js +6 -5
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-remove.d.ts +48 -0
- package/src/lib/model/contact-remove.js +22 -0
- package/src/lib/model/contact-remove.js.map +1 -0
- package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
- package/src/lib/model/contacts-bulk-writes.js +2 -82
- package/src/lib/model/contacts-bulk-writes.js.map +1 -1
- package/src/lib/model/contacts-csv.d.ts +2 -2
- package/src/lib/model/contacts-csv.js +2 -2
- package/src/lib/model/contacts-csv.js.map +1 -1
- package/src/lib/model/crm-csv.d.ts +239 -0
- package/src/lib/model/crm-csv.js +467 -0
- package/src/lib/model/crm-csv.js.map +1 -0
- package/src/lib/model/crm-digest.d.ts +185 -0
- package/src/lib/model/crm-digest.js +272 -0
- package/src/lib/model/crm-digest.js.map +1 -0
- package/src/lib/model/crm-import.d.ts +7 -1
- package/src/lib/model/crm-import.js +2 -1
- package/src/lib/model/crm-import.js.map +1 -1
- package/src/lib/model/crm-record-routes.d.ts +5 -0
- package/src/lib/model/crm-record-routes.js +7 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-routes.d.ts +6 -5
- package/src/lib/model/crm-routes.js +7 -7
- package/src/lib/model/crm-routes.js.map +1 -1
- package/src/lib/model/crm-search-sources.d.ts +15 -0
- package/src/lib/model/crm-search-sources.js +162 -0
- package/src/lib/model/crm-search-sources.js.map +1 -0
- package/src/lib/model/crm-sharing-api.d.ts +32 -0
- package/src/lib/model/crm-sharing-api.js +59 -0
- package/src/lib/model/crm-sharing-api.js.map +1 -0
- package/src/lib/model/crm-sharing.d.ts +333 -0
- package/src/lib/model/crm-sharing.js +603 -0
- package/src/lib/model/crm-sharing.js.map +1 -0
- package/src/lib/model/deals-csv.d.ts +1 -1
- package/src/lib/model/deals-csv.js +4 -5
- package/src/lib/model/deals-csv.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +1 -1
- package/src/lib/model/leads-csv.d.ts +1 -1
- package/src/lib/model/leads-csv.js +1 -1
- package/src/lib/model/leads-csv.js.map +1 -1
- package/src/lib/model/tasks-csv.d.ts +1 -1
- package/src/lib/model/tasks-csv.js +1 -1
- package/src/lib/model/tasks-csv.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +75 -0
- package/src/lib/plan-entitlements.js +56 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
- package/src/lib/server/api-v1/contacts-merge.js +120 -0
- package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
- package/src/lib/server/api-v1/contacts.d.ts +18 -0
- package/src/lib/server/api-v1/contacts.js +1076 -0
- package/src/lib/server/api-v1/contacts.js.map +1 -0
- package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
- package/src/lib/server/api-v1/crm-activities.js +269 -0
- package/src/lib/server/api-v1/crm-activities.js.map +1 -0
- package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
- package/src/lib/server/api-v1/crm-companies.js +391 -0
- package/src/lib/server/api-v1/crm-companies.js.map +1 -0
- package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
- package/src/lib/server/api-v1/crm-deals.js +631 -0
- package/src/lib/server/api-v1/crm-deals.js.map +1 -0
- package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
- package/src/lib/server/api-v1/crm-email-templates.js +336 -0
- package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
- package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
- package/src/lib/server/api-v1/crm-leads.js +832 -0
- package/src/lib/server/api-v1/crm-leads.js.map +1 -0
- package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
- package/src/lib/server/api-v1/crm-pipelines.js +146 -0
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
- package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
- package/src/lib/server/api-v1/crm-shared.js +436 -0
- package/src/lib/server/api-v1/crm-shared.js.map +1 -0
- package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
- package/src/lib/server/api-v1/crm-tasks.js +398 -0
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +29 -0
- package/src/lib/server/api-v1/openapi.js +985 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/usage.d.ts +26 -0
- package/src/lib/server/api-v1/usage.js +73 -0
- package/src/lib/server/api-v1/usage.js.map +1 -0
- package/src/lib/server/consent-group-participant.js +1 -1
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +91 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-remove.d.ts +75 -0
- package/src/lib/server/contact-remove.js +244 -0
- package/src/lib/server/contact-remove.js.map +1 -0
- package/src/lib/server/contact-update.js +2 -1
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.d.ts +47 -11
- package/src/lib/server/contacts-import.js +171 -71
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/contacts-merge.d.ts +1 -1
- package/src/lib/server/contacts-merge.js +3 -2
- package/src/lib/server/contacts-merge.js.map +1 -1
- package/src/lib/server/convert-host-lead.d.ts +189 -0
- package/src/lib/server/convert-host-lead.js +518 -0
- package/src/lib/server/convert-host-lead.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +108 -0
- package/src/lib/server/crm-sharing.js +790 -0
- package/src/lib/server/crm-sharing.js.map +1 -0
- package/src/lib/server/daily-digest-route.d.ts +107 -0
- package/src/lib/server/daily-digest-route.js +588 -0
- package/src/lib/server/daily-digest-route.js.map +1 -0
- package/src/lib/server/export-route.d.ts +60 -0
- package/src/lib/server/export-route.js +447 -0
- package/src/lib/server/export-route.js.map +1 -0
- package/src/lib/server/inbound-route.d.ts +74 -0
- package/src/lib/server/inbound-route.js +257 -0
- package/src/lib/server/inbound-route.js.map +1 -0
- package/src/lib/server/lead-convert.d.ts +1 -1
- package/src/lib/server/lead-convert.js +2 -2
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/merge-contacts.d.ts +55 -0
- package/src/lib/server/merge-contacts.js +274 -0
- package/src/lib/server/merge-contacts.js.map +1 -0
- package/src/lib/server/pipeline-record-index.d.ts +36 -0
- package/src/lib/server/pipeline-record-index.js +87 -0
- package/src/lib/server/pipeline-record-index.js.map +1 -0
- package/src/lib/server/record-facts.js +2 -1
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +82 -0
- package/src/lib/server/task-reminders-route.js +497 -0
- package/src/lib/server/task-reminders-route.js.map +1 -0
- package/src/lib/server-deal-stage.js +2 -1
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +5 -2
- package/src/lib/server.js +47 -2
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
- package/src/lib/testing/stand-in-site-record-routes.js +55 -0
- package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
- package/src/lib/usage-axes.d.ts +35 -0
- package/src/lib/usage-axes.js +68 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +0 -24
- package/src/lib/components/use-emails-hub-path.js +0 -59
- package/src/lib/components/use-emails-hub-path.js.map +0 -1
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* CRM SHARING FOR MULTI-BRAND ORGS (AGL-3336).
|
|
18
|
+
*
|
|
19
|
+
* An org manager shares a lead, a contact, a company or a deal with other
|
|
20
|
+
* sites — by hand, from the record or a list's bulk bar, or by a SHARING
|
|
21
|
+
* RULE that applies to every record it matches, now and later. The model is
|
|
22
|
+
* Salesforce's three layers:
|
|
23
|
+
*
|
|
24
|
+
* 1. the org-wide default — the consent-group scope a record is captured
|
|
25
|
+
* with (`consentGroupScope`), untouched by anything here;
|
|
26
|
+
* 2. sharing rules — `crm.sharingRules` on the org document;
|
|
27
|
+
* 3. manual shares — per record, per target.
|
|
28
|
+
*
|
|
29
|
+
* ## One field is enforced, so one field is widened
|
|
30
|
+
*
|
|
31
|
+
* Both enforcement layers read `visibleTo`: the rules with `hasAny`, every
|
|
32
|
+
* list with `array-contains-any`. A share therefore ADDS its tokens to
|
|
33
|
+
* `visibleTo`, which is what puts the record on the target site's lists as a
|
|
34
|
+
* real query — no second field, no second index, no loaded-window match.
|
|
35
|
+
* `'org'` is the token for "all sites", and it covers a site created later
|
|
36
|
+
* with nothing to recompute, because every site's read set carries it.
|
|
37
|
+
*
|
|
38
|
+
* ## …and the sources stay distinguishable
|
|
39
|
+
*
|
|
40
|
+
* The record keeps its grants under `sharing` (`SCOPE_GRANTS_FIELD` in
|
|
41
|
+
* core): one grant per rule and one per hand-shared target, and `added`,
|
|
42
|
+
* the tokens the grants put on `visibleTo` that were not there already.
|
|
43
|
+
* The record's own scope — its HELD tokens — is `visibleTo` without
|
|
44
|
+
* `added`, so taking a grant away removes only what grants alone
|
|
45
|
+
* contributed, and never a consent-group token. A token a site later
|
|
46
|
+
* captures the person on is held from then on: the capturing sites
|
|
47
|
+
* (`hostId`, `capturedByHostIds`) are never counted as added.
|
|
48
|
+
*
|
|
49
|
+
* ## Visibility is not consent, and not write access
|
|
50
|
+
*
|
|
51
|
+
* Nothing here reads or writes a consent entry. A site a record is shared
|
|
52
|
+
* with sees it; it holds no basis to market to the person, and the
|
|
53
|
+
* audience paths read `heldByHost`, not `visibleTo`. Write access is its
|
|
54
|
+
* own field, `writeTo`, present only on a shared record: the held tokens
|
|
55
|
+
* and the tokens of grants whose access is `edit`. The Firestore rules and
|
|
56
|
+
* the CRM's routes check it; a read-only grant is the default.
|
|
57
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
58
|
+
import { heldScopeTokens, hostIdsFromScope, hostScopeToken, isScopeToken, MAX_SCOPE_HOSTS, ORG_SCOPE_TOKEN, parseScopeToken, SCOPE_GRANTS_FIELD } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
59
|
+
/*==========================================
|
|
60
|
+
* WHAT IS SHARED.
|
|
61
|
+
*=========================================*/ /** The four record types a share or a rule applies to, by their collection. */ export const CRM_SHARING_OBJECTS = [
|
|
62
|
+
'leads',
|
|
63
|
+
'contacts',
|
|
64
|
+
'companies',
|
|
65
|
+
'deals'
|
|
66
|
+
];
|
|
67
|
+
export function isCrmSharingObject(value) {
|
|
68
|
+
return typeof value === 'string' && CRM_SHARING_OBJECTS.includes(value);
|
|
69
|
+
}
|
|
70
|
+
/** How each object reads in a sentence. */ export const CRM_SHARING_OBJECT_LABELS = {
|
|
71
|
+
leads: {
|
|
72
|
+
one: 'lead',
|
|
73
|
+
many: 'Leads'
|
|
74
|
+
},
|
|
75
|
+
contacts: {
|
|
76
|
+
one: 'contact',
|
|
77
|
+
many: 'Contacts'
|
|
78
|
+
},
|
|
79
|
+
companies: {
|
|
80
|
+
one: 'company',
|
|
81
|
+
many: 'Companies'
|
|
82
|
+
},
|
|
83
|
+
deals: {
|
|
84
|
+
one: 'deal',
|
|
85
|
+
many: 'Deals'
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
export function isCrmShareAccess(value) {
|
|
89
|
+
return value === 'read' || value === 'edit';
|
|
90
|
+
}
|
|
91
|
+
/** The record field its grants live under — the core's name for it. */ export const CRM_SHARING_FIELD = SCOPE_GRANTS_FIELD;
|
|
92
|
+
/** The record field naming who may WRITE it, present only while it is shared. */ export const CRM_WRITE_SCOPE_FIELD = 'writeTo';
|
|
93
|
+
/** The array field a rule's removal queries. */ export const CRM_SHARING_RULE_IDS_PATH = `${CRM_SHARING_FIELD}.ruleIds`;
|
|
94
|
+
/** A hand share is one grant per target, so each is unshared on its own. */ export function manualGrantKey(token) {
|
|
95
|
+
const parsed = parseScopeToken(token);
|
|
96
|
+
if (!parsed) throw new Error(`not a scope token: ${token}`);
|
|
97
|
+
return parsed.kind === 'org' ? 'manual_org' : `manual_${parsed.hostId}`;
|
|
98
|
+
}
|
|
99
|
+
export function ruleGrantKey(ruleId) {
|
|
100
|
+
return `rule_${ruleId}`;
|
|
101
|
+
}
|
|
102
|
+
function tokenList(raw) {
|
|
103
|
+
if (!Array.isArray(raw)) return [];
|
|
104
|
+
return [
|
|
105
|
+
...new Set(raw.filter(isScopeToken))
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
function readGrant(raw) {
|
|
109
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
110
|
+
const value = raw;
|
|
111
|
+
const tokens = tokenList(value['tokens']);
|
|
112
|
+
if (!tokens.length) return null;
|
|
113
|
+
const source = value['source'] === 'rule' ? 'rule' : value['source'] === 'manual' ? 'manual' : null;
|
|
114
|
+
if (!source) return null;
|
|
115
|
+
const ruleId = typeof value['ruleId'] === 'string' ? value['ruleId'] : '';
|
|
116
|
+
if (source === 'rule' && !ruleId) return null;
|
|
117
|
+
const byUid = typeof value['byUid'] === 'string' ? value['byUid'] : '';
|
|
118
|
+
const byName = typeof value['byName'] === 'string' ? value['byName'] : '';
|
|
119
|
+
const atMs = typeof value['atMs'] === 'number' && Number.isFinite(value['atMs']) ? value['atMs'] : 0;
|
|
120
|
+
return _extends({
|
|
121
|
+
source,
|
|
122
|
+
tokens,
|
|
123
|
+
access: value['access'] === 'edit' ? 'edit' : 'read'
|
|
124
|
+
}, ruleId ? {
|
|
125
|
+
ruleId
|
|
126
|
+
} : {}, byUid ? {
|
|
127
|
+
byUid
|
|
128
|
+
} : {}, byName ? {
|
|
129
|
+
byName
|
|
130
|
+
} : {}, {
|
|
131
|
+
atMs
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/** A record's grants, cleaned; `{}` for a record never shared. */ export function readRecordGrants(record) {
|
|
135
|
+
const sharing = (record != null ? record : {})[CRM_SHARING_FIELD];
|
|
136
|
+
if (!sharing || typeof sharing !== 'object' || Array.isArray(sharing)) return {};
|
|
137
|
+
const grants = sharing['grants'];
|
|
138
|
+
if (!grants || typeof grants !== 'object' || Array.isArray(grants)) return {};
|
|
139
|
+
const out = {};
|
|
140
|
+
for (const [key, raw] of Object.entries(grants)){
|
|
141
|
+
const grant = readGrant(raw);
|
|
142
|
+
if (grant) out[key] = grant;
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/*==========================================
|
|
147
|
+
* THE PLAN: grants in, the three fields out.
|
|
148
|
+
*=========================================*/ /**
|
|
149
|
+
* The sites that captured or created the record. Their tokens are HELD,
|
|
150
|
+
* whatever `added` says: a person shared to site B and later captured on B
|
|
151
|
+
* is B's own from then on, and unsharing must not hide them from B.
|
|
152
|
+
*/ export function recordHolderHostIds(record) {
|
|
153
|
+
const ids = new Set();
|
|
154
|
+
const hostId = record['hostId'];
|
|
155
|
+
if (typeof hostId === 'string' && hostId) ids.add(hostId);
|
|
156
|
+
const captured = record['capturedByHostIds'];
|
|
157
|
+
if (Array.isArray(captured)) {
|
|
158
|
+
for (const id of captured)if (typeof id === 'string' && id) ids.add(id);
|
|
159
|
+
}
|
|
160
|
+
return [
|
|
161
|
+
...ids
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The record's own scope: `visibleTo` less what grants added, its capturing
|
|
166
|
+
* sites kept — the core's `heldScopeTokens`, as scope tokens.
|
|
167
|
+
*/ export function recordHeldScope(record) {
|
|
168
|
+
return tokenList(heldScopeTokens(record));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The fields a record carries under `grants`.
|
|
172
|
+
*
|
|
173
|
+
* A grant naming a token the record already holds adds nothing, and a
|
|
174
|
+
* record held org-wide adds nothing at all — every site sees it already, so
|
|
175
|
+
* a share there would only make a later unshare look like it hid something.
|
|
176
|
+
* The token order is the held scope first and the additions after it, so an
|
|
177
|
+
* unchanged record plans the `visibleTo` it has.
|
|
178
|
+
*/ export function planRecordSharing(record, grants) {
|
|
179
|
+
const held = recordHeldScope(record);
|
|
180
|
+
const entries = Object.entries(grants);
|
|
181
|
+
if (!entries.length) return {
|
|
182
|
+
visibleTo: held,
|
|
183
|
+
writeTo: null,
|
|
184
|
+
sharing: null
|
|
185
|
+
};
|
|
186
|
+
const heldSet = new Set(held);
|
|
187
|
+
const heldOrgWide = heldSet.has(ORG_SCOPE_TOKEN);
|
|
188
|
+
const tokens = [];
|
|
189
|
+
const editTokens = [];
|
|
190
|
+
const ruleIds = [];
|
|
191
|
+
for (const [, grant] of entries.sort(([a], [b])=>a < b ? -1 : a > b ? 1 : 0)){
|
|
192
|
+
for (const token of grant.tokens){
|
|
193
|
+
if (!tokens.includes(token)) tokens.push(token);
|
|
194
|
+
if (grant.access === 'edit' && !editTokens.includes(token)) editTokens.push(token);
|
|
195
|
+
}
|
|
196
|
+
if (grant.source === 'rule' && grant.ruleId && !ruleIds.includes(grant.ruleId)) {
|
|
197
|
+
ruleIds.push(grant.ruleId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const added = heldOrgWide ? [] : tokens.filter((token)=>!heldSet.has(token));
|
|
201
|
+
const writeTo = [
|
|
202
|
+
...held
|
|
203
|
+
];
|
|
204
|
+
for (const token of editTokens)if (!writeTo.includes(token)) writeTo.push(token);
|
|
205
|
+
return {
|
|
206
|
+
visibleTo: [
|
|
207
|
+
...held,
|
|
208
|
+
...added
|
|
209
|
+
],
|
|
210
|
+
writeTo,
|
|
211
|
+
sharing: {
|
|
212
|
+
grants: Object.fromEntries(entries),
|
|
213
|
+
tokens,
|
|
214
|
+
added,
|
|
215
|
+
ruleIds,
|
|
216
|
+
orgWideHeldBy: added.includes(ORG_SCOPE_TOKEN) ? [
|
|
217
|
+
...held
|
|
218
|
+
] : []
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const sameList = (a, b)=>(a != null ? a : null) === null ? (b != null ? b : null) === null : (b != null ? b : null) !== null && a.length === b.length && a.every((entry, at)=>entry === b[at]);
|
|
223
|
+
const sameGrant = (a, b)=>{
|
|
224
|
+
var _a_ruleId, _b_ruleId, _a_byUid, _b_byUid, _a_byName, _b_byName;
|
|
225
|
+
return !!a && !!b && a.source === b.source && a.access === b.access && ((_a_ruleId = a.ruleId) != null ? _a_ruleId : '') === ((_b_ruleId = b.ruleId) != null ? _b_ruleId : '') && ((_a_byUid = a.byUid) != null ? _a_byUid : '') === ((_b_byUid = b.byUid) != null ? _b_byUid : '') && ((_a_byName = a.byName) != null ? _a_byName : '') === ((_b_byName = b.byName) != null ? _b_byName : '') && a.atMs === b.atMs && sameList(a.tokens, b.tokens);
|
|
226
|
+
};
|
|
227
|
+
/** Whether writing `plan` would change what the record stores. */ export function sharingPlanChanges(record, plan) {
|
|
228
|
+
if (!sameList(tokenList(record['visibleTo']), plan.visibleTo)) return true;
|
|
229
|
+
const storedWrite = record[CRM_WRITE_SCOPE_FIELD];
|
|
230
|
+
if (!sameList(Array.isArray(storedWrite) ? tokenList(storedWrite) : null, plan.writeTo)) return true;
|
|
231
|
+
const stored = record[CRM_SHARING_FIELD];
|
|
232
|
+
if (!plan.sharing) return stored !== undefined && stored !== null;
|
|
233
|
+
const grants = readRecordGrants(record);
|
|
234
|
+
const keys = Object.keys(plan.sharing.grants);
|
|
235
|
+
if (keys.length !== Object.keys(grants).length) return true;
|
|
236
|
+
if (!keys.every((key)=>sameGrant(grants[key], plan.sharing.grants[key]))) return true;
|
|
237
|
+
const sharing = stored != null ? stored : {};
|
|
238
|
+
return !sameList(tokenList(sharing['tokens']), plan.sharing.tokens) || !sameList(tokenList(sharing['added']), plan.sharing.added) || !sameList(Array.isArray(sharing['ruleIds']) ? sharing['ruleIds'] : [], plan.sharing.ruleIds) || !sameList(tokenList(sharing['orgWideHeldBy']), plan.sharing.orgWideHeldBy);
|
|
239
|
+
}
|
|
240
|
+
/*==========================================
|
|
241
|
+
* SHARING RULES: `crm.sharingRules` on the org document.
|
|
242
|
+
*=========================================*/ /** The org document path the rules are stored under. */ export const CRM_SHARING_RULES_PATH = 'crm.sharingRules';
|
|
243
|
+
/** A workspace keeps at most this many. */ export const CRM_SHARING_RULES_MAX = 20;
|
|
244
|
+
/** At most this many values per criterion. */ export const CRM_SHARING_CRITERION_MAX = 20;
|
|
245
|
+
export const CRM_SHARING_CRITERIA_KEYS = [
|
|
246
|
+
'leadSources',
|
|
247
|
+
'tags',
|
|
248
|
+
'campaignIds',
|
|
249
|
+
'stages',
|
|
250
|
+
'ownerUids'
|
|
251
|
+
];
|
|
252
|
+
/** Which criteria an object's records can answer. */ export const CRM_SHARING_CRITERIA_FOR = {
|
|
253
|
+
leads: [
|
|
254
|
+
'leadSources',
|
|
255
|
+
'tags',
|
|
256
|
+
'campaignIds',
|
|
257
|
+
'stages',
|
|
258
|
+
'ownerUids'
|
|
259
|
+
],
|
|
260
|
+
contacts: [
|
|
261
|
+
'leadSources',
|
|
262
|
+
'tags',
|
|
263
|
+
'campaignIds',
|
|
264
|
+
'stages',
|
|
265
|
+
'ownerUids'
|
|
266
|
+
],
|
|
267
|
+
companies: [
|
|
268
|
+
'tags',
|
|
269
|
+
'ownerUids'
|
|
270
|
+
],
|
|
271
|
+
deals: [
|
|
272
|
+
'stages',
|
|
273
|
+
'ownerUids'
|
|
274
|
+
]
|
|
275
|
+
};
|
|
276
|
+
const RULE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
277
|
+
function stringList(raw, max = CRM_SHARING_CRITERION_MAX) {
|
|
278
|
+
if (!Array.isArray(raw)) return [];
|
|
279
|
+
const out = [];
|
|
280
|
+
for (const entry of raw){
|
|
281
|
+
const value = String(entry != null ? entry : '').trim().slice(0, 200);
|
|
282
|
+
if (value && !out.includes(value)) out.push(value);
|
|
283
|
+
if (out.length >= max) break;
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
/** One rule as stored, cleaned, or null when it cannot be applied. */ export function readCrmSharingRule(raw) {
|
|
288
|
+
var _value_id, _value_criteria, _value_name;
|
|
289
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
290
|
+
const value = raw;
|
|
291
|
+
const id = String((_value_id = value['id']) != null ? _value_id : '');
|
|
292
|
+
if (!RULE_ID.test(id)) return null;
|
|
293
|
+
if (!isCrmSharingObject(value['object'])) return null;
|
|
294
|
+
const targets = normalizeShareTargets(tokenList(value['targets']));
|
|
295
|
+
if (!targets) return null;
|
|
296
|
+
const rawCriteria = (_value_criteria = value['criteria']) != null ? _value_criteria : {};
|
|
297
|
+
const criteria = {};
|
|
298
|
+
for (const key of CRM_SHARING_CRITERIA_FOR[value['object']]){
|
|
299
|
+
const list = stringList(rawCriteria[key]);
|
|
300
|
+
if (list.length) criteria[key] = key === 'tags' ? list.map((tag)=>tag.toLowerCase()) : list;
|
|
301
|
+
}
|
|
302
|
+
const run = value['run'];
|
|
303
|
+
return _extends({
|
|
304
|
+
id,
|
|
305
|
+
name: String((_value_name = value['name']) != null ? _value_name : '').trim().slice(0, 120) || 'Sharing rule',
|
|
306
|
+
object: value['object'],
|
|
307
|
+
enabled: value['enabled'] !== false,
|
|
308
|
+
sourceHostIds: stringList(value['sourceHostIds'], MAX_SCOPE_HOSTS),
|
|
309
|
+
criteria,
|
|
310
|
+
targets,
|
|
311
|
+
access: value['access'] === 'edit' ? 'edit' : 'read'
|
|
312
|
+
}, value['deleting'] === true ? {
|
|
313
|
+
deleting: true
|
|
314
|
+
} : {}, typeof value['createdByUid'] === 'string' ? {
|
|
315
|
+
createdByUid: value['createdByUid']
|
|
316
|
+
} : {}, {
|
|
317
|
+
createdAtMs: Number(value['createdAtMs']) || 0,
|
|
318
|
+
updatedAtMs: Number(value['updatedAtMs']) || 0
|
|
319
|
+
}, run && typeof run === 'object' ? {
|
|
320
|
+
run: _extends({
|
|
321
|
+
status: run['status'] === 'done' || run['status'] === 'failed' ? run['status'] : 'running',
|
|
322
|
+
mode: run['mode'] === 'remove' ? 'remove' : 'apply',
|
|
323
|
+
processed: Number(run['processed']) || 0,
|
|
324
|
+
changed: Number(run['changed']) || 0,
|
|
325
|
+
cursor: typeof run['cursor'] === 'string' ? run['cursor'] : null,
|
|
326
|
+
startedAtMs: Number(run['startedAtMs']) || 0
|
|
327
|
+
}, typeof run['finishedAtMs'] === 'number' ? {
|
|
328
|
+
finishedAtMs: run['finishedAtMs']
|
|
329
|
+
} : {}, typeof run['error'] === 'string' ? {
|
|
330
|
+
error: run['error']
|
|
331
|
+
} : {})
|
|
332
|
+
} : {});
|
|
333
|
+
}
|
|
334
|
+
/** The org's sharing rules, in their stored order. */ export function readCrmSharingRules(org) {
|
|
335
|
+
const crm = (org != null ? org : {})['crm'];
|
|
336
|
+
const raw = crm && typeof crm === 'object' && !Array.isArray(crm) ? crm['sharingRules'] : undefined;
|
|
337
|
+
if (!Array.isArray(raw)) return [];
|
|
338
|
+
const out = [];
|
|
339
|
+
for (const entry of raw){
|
|
340
|
+
const rule = readCrmSharingRule(entry);
|
|
341
|
+
if (rule && !out.some((existing)=>existing.id === rule.id)) out.push(rule);
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
/** The rules that grant anything now: enabled, and not being deleted. */ export function activeSharingRules(rules, object) {
|
|
346
|
+
return rules.filter((rule)=>rule.enabled && !rule.deleting && (!object || rule.object === object));
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* A share's targets, cleaned for storage: `['org']` when all sites are
|
|
350
|
+
* named, else the host tokens, capped. `null` when nothing is left.
|
|
351
|
+
*/ export function normalizeShareTargets(tokens) {
|
|
352
|
+
const clean = tokenList(tokens);
|
|
353
|
+
if (!clean.length) return null;
|
|
354
|
+
if (clean.includes(ORG_SCOPE_TOKEN)) return [
|
|
355
|
+
ORG_SCOPE_TOKEN
|
|
356
|
+
];
|
|
357
|
+
if (clean.length > MAX_SCOPE_HOSTS) return null;
|
|
358
|
+
return clean;
|
|
359
|
+
}
|
|
360
|
+
/** Targets from a request: `'all'`, or a list of host ids. */ export function shareTargetsFrom(input) {
|
|
361
|
+
if (input === 'all') return [
|
|
362
|
+
ORG_SCOPE_TOKEN
|
|
363
|
+
];
|
|
364
|
+
if (!Array.isArray(input)) return null;
|
|
365
|
+
return normalizeShareTargets(input.map((id)=>String(id != null ? id : '').trim()).filter((id)=>id && !id.includes('/') && !id.includes(':')).map(hostScopeToken));
|
|
366
|
+
}
|
|
367
|
+
const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
368
|
+
const strs = (value)=>Array.isArray(value) ? value.map(str).filter(Boolean) : [];
|
|
369
|
+
/**
|
|
370
|
+
* The facts a rule's criteria are matched against. A contact keeps each
|
|
371
|
+
* holder's working fields in its own facet, so a contact answers with the
|
|
372
|
+
* union of its facets: a rule about "contacts tagged vip" matches a person
|
|
373
|
+
* any holder tagged.
|
|
374
|
+
*/ export function crmSharingFacts(object, record) {
|
|
375
|
+
const facts = {
|
|
376
|
+
hostIds: recordHolderHostIds(record),
|
|
377
|
+
leadSources: [],
|
|
378
|
+
tags: [],
|
|
379
|
+
campaignIds: [],
|
|
380
|
+
stages: [],
|
|
381
|
+
ownerUids: []
|
|
382
|
+
};
|
|
383
|
+
const take = (source, stageField)=>{
|
|
384
|
+
const leadSource = str(source['leadSource']);
|
|
385
|
+
if (leadSource) facts.leadSources.push(leadSource);
|
|
386
|
+
facts.tags.push(...strs(source['tags']).map((tag)=>tag.toLowerCase()));
|
|
387
|
+
facts.campaignIds.push(...strs(source['campaignIds']));
|
|
388
|
+
const stage = stageField ? str(source[stageField]) : '';
|
|
389
|
+
if (stage) facts.stages.push(stage);
|
|
390
|
+
const owner = str(source['ownerUid']);
|
|
391
|
+
if (owner) facts.ownerUids.push(owner);
|
|
392
|
+
};
|
|
393
|
+
if (object === 'leads') take(record, 'status');
|
|
394
|
+
else if (object === 'companies') take(record, null);
|
|
395
|
+
else if (object === 'deals') take(record, 'stageId');
|
|
396
|
+
else {
|
|
397
|
+
const facets = record['facets'];
|
|
398
|
+
if (facets && typeof facets === 'object' && !Array.isArray(facets)) {
|
|
399
|
+
for (const facet of Object.values(facets)){
|
|
400
|
+
if (facet && typeof facet === 'object' && !Array.isArray(facet)) {
|
|
401
|
+
take(facet, 'lifecycleStage');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// A lead with no status is New, as every lead reader answers it.
|
|
407
|
+
if (object === 'leads' && !facts.stages.length) facts.stages.push('new');
|
|
408
|
+
return facts;
|
|
409
|
+
}
|
|
410
|
+
const anyOf = (wanted, have, fold = false)=>{
|
|
411
|
+
if (!(wanted == null ? void 0 : wanted.length)) return true;
|
|
412
|
+
const set = new Set(fold ? have.map((value)=>value.toLowerCase()) : have);
|
|
413
|
+
return wanted.some((value)=>set.has(fold ? value.toLowerCase() : value));
|
|
414
|
+
};
|
|
415
|
+
/** Whether a rule shares this record. */ export function crmSharingRuleMatches(rule, object, record) {
|
|
416
|
+
if (!rule.enabled || rule.deleting || rule.object !== object) return false;
|
|
417
|
+
const facts = crmSharingFacts(object, record);
|
|
418
|
+
if (rule.sourceHostIds.length && !facts.hostIds.some((id)=>rule.sourceHostIds.includes(id))) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
const { criteria } = rule;
|
|
422
|
+
return anyOf(criteria.leadSources, facts.leadSources, true) && anyOf(criteria.tags, facts.tags, true) && anyOf(criteria.campaignIds, facts.campaignIds) && anyOf(criteria.stages, facts.stages) && anyOf(criteria.ownerUids, facts.ownerUids);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The grants a record should carry: its hand shares as they are, and one
|
|
426
|
+
* grant per active rule that matches it now. A rule grant already present
|
|
427
|
+
* keeps its date, so re-evaluating an unchanged record writes nothing.
|
|
428
|
+
*
|
|
429
|
+
* A rule grant whose rule is not among `rules` is dropped — the caller
|
|
430
|
+
* passes every active rule of the org, so a rule absent from them has been
|
|
431
|
+
* switched off or deleted.
|
|
432
|
+
*/ export function evaluateRecordGrants(object, record, rules, atMs) {
|
|
433
|
+
const current = readRecordGrants(record);
|
|
434
|
+
const next = {};
|
|
435
|
+
for (const [key, grant] of Object.entries(current)){
|
|
436
|
+
if (grant.source === 'manual') next[key] = grant;
|
|
437
|
+
}
|
|
438
|
+
for (const rule of activeSharingRules(rules, object)){
|
|
439
|
+
if (!crmSharingRuleMatches(rule, object, record)) continue;
|
|
440
|
+
const key = ruleGrantKey(rule.id);
|
|
441
|
+
const before = current[key];
|
|
442
|
+
next[key] = {
|
|
443
|
+
source: 'rule',
|
|
444
|
+
ruleId: rule.id,
|
|
445
|
+
tokens: [
|
|
446
|
+
...rule.targets
|
|
447
|
+
],
|
|
448
|
+
access: rule.access,
|
|
449
|
+
atMs: before && sameList(before.tokens, rule.targets) && before.access === rule.access ? before.atMs : atMs
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
return next;
|
|
453
|
+
}
|
|
454
|
+
/** The hand shares after sharing with `targets`, by `by`. */ export function withManualShares(grants, targets, access, by, atMs) {
|
|
455
|
+
const next = _extends({}, grants);
|
|
456
|
+
for (const token of targets){
|
|
457
|
+
var _by_name;
|
|
458
|
+
const name = String((_by_name = by.name) != null ? _by_name : '').trim();
|
|
459
|
+
next[manualGrantKey(token)] = _extends({
|
|
460
|
+
source: 'manual',
|
|
461
|
+
tokens: [
|
|
462
|
+
token
|
|
463
|
+
],
|
|
464
|
+
access,
|
|
465
|
+
byUid: by.uid
|
|
466
|
+
}, name ? {
|
|
467
|
+
byName: name
|
|
468
|
+
} : {}, {
|
|
469
|
+
atMs
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return next;
|
|
473
|
+
}
|
|
474
|
+
/** The hand shares after unsharing `targets`. Rule grants are the rule's to take away. */ export function withoutManualShares(grants, targets) {
|
|
475
|
+
const drop = new Set(targets.filter(isScopeToken).map(manualGrantKey));
|
|
476
|
+
return Object.fromEntries(Object.entries(grants).filter(([key])=>!drop.has(key)));
|
|
477
|
+
}
|
|
478
|
+
/*==========================================
|
|
479
|
+
* WHAT A READER SEES.
|
|
480
|
+
*=========================================*/ /**
|
|
481
|
+
* Whether a caller holding `tokens` may write the record: `writeTo` on a
|
|
482
|
+
* shared record, else `visibleTo` — the Firestore rules' own predicate.
|
|
483
|
+
*/ export function crmRecordWritableBy(record, tokens) {
|
|
484
|
+
const writeTo = record[CRM_WRITE_SCOPE_FIELD];
|
|
485
|
+
const scope = Array.isArray(writeTo) ? writeTo : record['visibleTo'];
|
|
486
|
+
if (!Array.isArray(scope) || !tokens.length) return false;
|
|
487
|
+
return scope.some((token)=>tokens.includes(token));
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Why a site seeing this record sees it, when the answer is a share — or
|
|
491
|
+
* `null` when the site holds it (or cannot see it at all). `hostIds` are
|
|
492
|
+
* the viewing group's sites; at the organization level there is none.
|
|
493
|
+
*/ export function crmShareChipFor(record, hostIds) {
|
|
494
|
+
var _ref, _ref1, _reaching_find;
|
|
495
|
+
if (!record || !hostIds.length) return null;
|
|
496
|
+
const held = recordHeldScope(record);
|
|
497
|
+
if (held.includes(ORG_SCOPE_TOKEN)) return null;
|
|
498
|
+
const viewing = new Set(hostIds.map(hostScopeToken));
|
|
499
|
+
if (held.some((token)=>viewing.has(token))) return null;
|
|
500
|
+
const grants = Object.values(readRecordGrants(record));
|
|
501
|
+
const reaching = grants.filter((grant)=>grant.tokens.some((token)=>token === ORG_SCOPE_TOKEN || viewing.has(token)));
|
|
502
|
+
if (!reaching.length) return null;
|
|
503
|
+
// A hand share names a person, so it leads; an edit grant outranks read.
|
|
504
|
+
const pick = (_ref = (_ref1 = (_reaching_find = reaching.find((grant)=>grant.source === 'manual' && grant.access === 'edit')) != null ? _reaching_find : reaching.find((grant)=>grant.source === 'manual')) != null ? _ref1 : reaching.find((grant)=>grant.access === 'edit')) != null ? _ref : reaching[0];
|
|
505
|
+
return _extends({
|
|
506
|
+
source: pick.source,
|
|
507
|
+
access: reaching.some((grant)=>grant.access === 'edit') ? 'edit' : 'read'
|
|
508
|
+
}, pick.byName ? {
|
|
509
|
+
byName: pick.byName
|
|
510
|
+
} : {}, pick.ruleId ? {
|
|
511
|
+
ruleId: pick.ruleId
|
|
512
|
+
} : {}, {
|
|
513
|
+
allSites: pick.tokens.includes(ORG_SCOPE_TOKEN)
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/** The chip's words: "Shared by Dana", "Shared by rule Brand A leads". */ export function crmShareChipLabel(chip, rules = []) {
|
|
517
|
+
if (chip.source === 'manual') return chip.byName ? `Shared by ${chip.byName}` : 'Shared by hand';
|
|
518
|
+
const rule = rules.find((entry)=>entry.id === chip.ruleId);
|
|
519
|
+
return rule ? `Shared by rule ${rule.name}` : 'Shared by a rule';
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Where a record is visible and why: the sites that hold it (its consent
|
|
523
|
+
* group or its creating site), then one line per hand share, then one per
|
|
524
|
+
* rule. The held line is never unsharable; a hand share is, in one click.
|
|
525
|
+
*/ export function describeRecordSharing(record) {
|
|
526
|
+
if (!record) return [];
|
|
527
|
+
const held = recordHeldScope(record);
|
|
528
|
+
const lines = [
|
|
529
|
+
{
|
|
530
|
+
kind: 'held',
|
|
531
|
+
hostIds: hostIdsFromScope(held),
|
|
532
|
+
allSites: held.includes(ORG_SCOPE_TOKEN)
|
|
533
|
+
}
|
|
534
|
+
];
|
|
535
|
+
const grants = Object.entries(readRecordGrants(record));
|
|
536
|
+
for (const [key, grant] of grants){
|
|
537
|
+
if (grant.source !== 'manual') continue;
|
|
538
|
+
lines.push(_extends({
|
|
539
|
+
kind: 'manual',
|
|
540
|
+
key,
|
|
541
|
+
token: grant.tokens[0],
|
|
542
|
+
access: grant.access
|
|
543
|
+
}, grant.byName ? {
|
|
544
|
+
byName: grant.byName
|
|
545
|
+
} : {}, {
|
|
546
|
+
atMs: grant.atMs
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
for (const [key, grant] of grants){
|
|
550
|
+
if (grant.source !== 'rule' || !grant.ruleId) continue;
|
|
551
|
+
lines.push({
|
|
552
|
+
kind: 'rule',
|
|
553
|
+
key,
|
|
554
|
+
ruleId: grant.ruleId,
|
|
555
|
+
tokens: grant.tokens,
|
|
556
|
+
access: grant.access,
|
|
557
|
+
atMs: grant.atMs
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return lines;
|
|
561
|
+
}
|
|
562
|
+
/** "All sites", a site's name, or "N sites". */ export function describeShareTargets(tokens, siteName) {
|
|
563
|
+
if (tokens.includes(ORG_SCOPE_TOKEN)) return 'All sites';
|
|
564
|
+
const ids = hostIdsFromScope(tokens);
|
|
565
|
+
if (ids.length === 1) return siteName(ids[0]);
|
|
566
|
+
if (ids.length <= 3) return ids.map(siteName).join(', ');
|
|
567
|
+
return `${ids.length} sites`;
|
|
568
|
+
}
|
|
569
|
+
/** A rule as one sentence of its "which records" half. */ export function describeSharingRuleScope(rule, siteName, labels = {}) {
|
|
570
|
+
var _criteria_leadSources, _criteria_tags, _criteria_campaignIds, _criteria_stages, _criteria_ownerUids;
|
|
571
|
+
const parts = [
|
|
572
|
+
CRM_SHARING_OBJECT_LABELS[rule.object].many
|
|
573
|
+
];
|
|
574
|
+
if (rule.sourceHostIds.length) {
|
|
575
|
+
parts.push(`captured on ${rule.sourceHostIds.map(siteName).join(' or ')}`);
|
|
576
|
+
}
|
|
577
|
+
const { criteria } = rule;
|
|
578
|
+
if ((_criteria_leadSources = criteria.leadSources) == null ? void 0 : _criteria_leadSources.length) parts.push(`from ${criteria.leadSources.join(' or ')}`);
|
|
579
|
+
if ((_criteria_tags = criteria.tags) == null ? void 0 : _criteria_tags.length) parts.push(`tagged ${criteria.tags.join(' or ')}`);
|
|
580
|
+
if ((_criteria_campaignIds = criteria.campaignIds) == null ? void 0 : _criteria_campaignIds.length) {
|
|
581
|
+
parts.push(`in ${criteria.campaignIds.length === 1 ? 'a campaign' : `${criteria.campaignIds.length} campaigns`}`);
|
|
582
|
+
}
|
|
583
|
+
if ((_criteria_stages = criteria.stages) == null ? void 0 : _criteria_stages.length) {
|
|
584
|
+
parts.push(`at ${criteria.stages.map((id)=>{
|
|
585
|
+
var _ref;
|
|
586
|
+
return (_ref = labels.stage == null ? void 0 : labels.stage.call(labels, id)) != null ? _ref : id;
|
|
587
|
+
}).join(' or ')}`);
|
|
588
|
+
}
|
|
589
|
+
if ((_criteria_ownerUids = criteria.ownerUids) == null ? void 0 : _criteria_ownerUids.length) {
|
|
590
|
+
parts.push(`owned by ${criteria.ownerUids.map((uid)=>{
|
|
591
|
+
var _ref;
|
|
592
|
+
return (_ref = labels.owner == null ? void 0 : labels.owner.call(labels, uid)) != null ? _ref : uid;
|
|
593
|
+
}).join(' or ')}`);
|
|
594
|
+
}
|
|
595
|
+
return parts.join(', ');
|
|
596
|
+
}
|
|
597
|
+
/*==========================================
|
|
598
|
+
* THE ROUTE.
|
|
599
|
+
*=========================================*/ /** `POST /api/crm/sharing` — every sharing act, by `action`. */ export const CRM_SHARING_ROUTE = 'crm/sharing';
|
|
600
|
+
export const crmSharingRouteUrl = ()=>`/api/${CRM_SHARING_ROUTE}`;
|
|
601
|
+
/** Records one share or unshare may name. */ export const CRM_SHARING_IDS_MAX = 200;
|
|
602
|
+
|
|
603
|
+
//# sourceMappingURL=crm-sharing.js.map
|