@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.201
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 +13 -13
- package/src/lib/components/activity-list.d.ts +36 -5
- package/src/lib/components/activity-list.js +159 -137
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/activity-timeline.d.ts +84 -0
- package/src/lib/components/activity-timeline.js +224 -0
- package/src/lib/components/activity-timeline.js.map +1 -0
- package/src/lib/components/companies-section.d.ts +6 -6
- package/src/lib/components/companies-section.js +54 -84
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +17 -4
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-picker.js +6 -2
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/company-properties-card.js +53 -36
- package/src/lib/components/company-properties-card.js.map +1 -1
- package/src/lib/components/contact-associations-card.d.ts +3 -2
- package/src/lib/components/contact-associations-card.js +12 -12
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -3
- package/src/lib/components/contact-detail-page.js +18 -12
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -5
- package/src/lib/components/contact-list-columns.js +6 -8
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +21 -4
- package/src/lib/components/contact-properties-card.js +129 -29
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.d.ts +12 -6
- package/src/lib/components/contact-timeline-card.js +132 -174
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.js +10 -1
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +282 -331
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-email-state-chip.js +4 -1
- package/src/lib/components/crm-email-state-chip.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +4 -2
- package/src/lib/components/crm-next-activity-column.js +6 -3
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +8 -4
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deal-products-card.js +15 -13
- package/src/lib/components/deal-products-card.js.map +1 -1
- package/src/lib/components/deal-properties-card.js +7 -0
- package/src/lib/components/deal-properties-card.js.map +1 -1
- package/src/lib/components/deal-stage-card.js +31 -29
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +2 -2
- package/src/lib/components/deals-section.js +67 -97
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.d.ts +4 -0
- package/src/lib/components/fields-section.js +368 -154
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/form-contact-fields-card.js +24 -20
- package/src/lib/components/form-contact-fields-card.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +1 -1
- package/src/lib/components/lead-campaigns-card.js +43 -38
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +14 -6
- package/src/lib/components/lead-properties-card.js +323 -311
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +13 -2
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +16 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.d.ts +2 -1
- package/src/lib/components/leads-section.js +110 -148
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/linked-deals-card.js +12 -10
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/recent-activity-feed.d.ts +12 -8
- package/src/lib/components/recent-activity-feed.js +54 -29
- package/src/lib/components/recent-activity-feed.js.map +1 -1
- package/src/lib/components/record-activity-card.d.ts +4 -2
- package/src/lib/components/record-activity-card.js +29 -11
- package/src/lib/components/record-activity-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +18 -12
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +47 -37
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +22 -16
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-export.d.ts +4 -11
- package/src/lib/components/reports/report-export.js +16 -33
- package/src/lib/components/reports/report-export.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +18 -12
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +35 -25
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/tasks-section.js +151 -144
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +13 -0
- package/src/lib/constants/company-filters.js +47 -17
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +61 -10
- package/src/lib/constants/contact-filters.js +373 -141
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +44 -0
- package/src/lib/constants/deal-filters.js +78 -0
- package/src/lib/constants/deal-filters.js.map +1 -0
- package/src/lib/constants/field-list-query.d.ts +23 -0
- package/src/lib/constants/field-list-query.js +80 -0
- package/src/lib/constants/field-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
- package/src/lib/hooks/use-crm-list-query.js +182 -0
- package/src/lib/hooks/use-crm-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
- package/src/lib/hooks/use-crm-tasks.js +44 -42
- package/src/lib/hooks/use-crm-tasks.js.map +1 -1
- package/src/lib/hooks/use-deals.d.ts +2 -34
- package/src/lib/hooks/use-deals.js +3 -62
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +91 -0
- package/src/lib/model/crm-list-query.js +182 -0
- package/src/lib/model/crm-list-query.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +50 -44
- package/src/lib/model/lead-filters.js +194 -88
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/list-fields-api.d.ts +26 -0
- package/src/lib/model/list-fields-api.js +47 -0
- package/src/lib/model/list-fields-api.js.map +1 -0
- package/src/lib/model/list-fields.d.ts +47 -0
- package/src/lib/model/list-fields.js +39 -0
- package/src/lib/model/list-fields.js.map +1 -0
- package/src/lib/model/task-views.d.ts +15 -10
- package/src/lib/model/task-views.js +68 -9
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/capture-contact.js +7 -5
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/companies-import.js +3 -3
- package/src/lib/server/companies-import.js.map +1 -1
- package/src/lib/server/company-delete.js +3 -1
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/consent-group-participant.js +4 -0
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-update.js +14 -8
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.js +4 -2
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/deals-import.js +4 -2
- package/src/lib/server/deals-import.js.map +1 -1
- package/src/lib/server/erase-person.js +2 -1
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/lead-create.js +4 -1
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-values.js +9 -5
- package/src/lib/server/lead-source-values.js.map +1 -1
- package/src/lib/server/leads-import.js +4 -1
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/list-fields-routes.d.ts +29 -0
- package/src/lib/server/list-fields-routes.js +85 -0
- package/src/lib/server/list-fields-routes.js.map +1 -0
- package/src/lib/server/record-email-state.js +5 -2
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-timeline.js +3 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-routes.js +6 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server/tasks-import.js +5 -2
- package/src/lib/server/tasks-import.js.map +1 -1
- package/src/lib/server.js +10 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
- package/src/lib/components/contact-custom-fields-card.js +0 -204
- package/src/lib/components/contact-custom-fields-card.js.map +0 -1
- package/src/lib/hooks/use-org-leads.d.ts +0 -50
- package/src/lib/hooks/use-org-leads.js +0 -104
- package/src/lib/hooks/use-org-leads.js.map +0 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ListPaginationProps } from '@aglyn/shared-ui-jsx/components/list-pagination.component';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
/** Which entries of one history are open. */
|
|
4
|
+
export interface TimelineExpansion {
|
|
5
|
+
isExpanded: (key: string) => boolean;
|
|
6
|
+
toggle: (key: string) => void;
|
|
7
|
+
/** Every entry is open and none has been closed since. */
|
|
8
|
+
allExpanded: boolean;
|
|
9
|
+
setAllExpanded: (expanded: boolean) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The open/closed state of a history's entries, held by the CARD so that
|
|
13
|
+
* the header's "Expand all" and each row's chevron act on one state.
|
|
14
|
+
*
|
|
15
|
+
* Kept as a default plus the keys that differ from it, so "Expand all"
|
|
16
|
+
* reaches the entries of pages the reader has not turned to yet, and a row
|
|
17
|
+
* closed after it stays closed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useTimelineExpansion(): TimelineExpansion;
|
|
20
|
+
/** The card header's "Expand all" / "Collapse all". */
|
|
21
|
+
export declare function TimelineExpandAllButton(props: {
|
|
22
|
+
expansion: TimelineExpansion;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
export declare namespace TimelineExpandAllButton {
|
|
26
|
+
var displayName: string;
|
|
27
|
+
}
|
|
28
|
+
/** The listener window a history pages over. */
|
|
29
|
+
export interface TimelineWindow {
|
|
30
|
+
/** A further page exists past the rows the listener holds. */
|
|
31
|
+
hasMore?: boolean;
|
|
32
|
+
/** Widens the listener by one step. */
|
|
33
|
+
showMore?: () => void;
|
|
34
|
+
/** The listener is reading; the page asked for is held rather than pulled back. */
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Newest-first pages of a history, over rows a listener window holds.
|
|
39
|
+
*
|
|
40
|
+
* The window is the query's bound — a hundred activities on a record, a
|
|
41
|
+
* page's worth on the feed — and a page the reader turns to past what it
|
|
42
|
+
* holds widens it, so the footer's Next is live while the probe row says
|
|
43
|
+
* more exists and the count stays "of more than" until it does not. Paged
|
|
44
|
+
* in memory otherwise: the contact timeline merges three sources that share
|
|
45
|
+
* no cursor, so only a slice of the merged stream can be turned.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useTimelinePages(total: number, reader?: TimelineWindow): {
|
|
48
|
+
page: number;
|
|
49
|
+
pageSize: number;
|
|
50
|
+
slice: <T>(rows: readonly T[]) => T[];
|
|
51
|
+
footer: ListPaginationProps;
|
|
52
|
+
};
|
|
53
|
+
export interface TimelineEntryProps {
|
|
54
|
+
/** The glyph for the entry's kind. */
|
|
55
|
+
icon: ReactNode;
|
|
56
|
+
/** The chips that say what the entry is: its kind, its direction, its delivery state. */
|
|
57
|
+
chips?: ReactNode;
|
|
58
|
+
/** The one line a collapsed entry reads as: a subject, a summary, a note's first line. */
|
|
59
|
+
title?: ReactNode;
|
|
60
|
+
/** A subject reads as one; a note's own words as body text. */
|
|
61
|
+
titleVariant?: 'subtitle2' | 'body2';
|
|
62
|
+
/** The line under it: who, to or from whom, when. */
|
|
63
|
+
meta?: ReactNode;
|
|
64
|
+
/** What the entry opens to. Absent, there is nothing more to show and no chevron. */
|
|
65
|
+
body?: ReactNode;
|
|
66
|
+
/** Controls on this one entry — edit, delete. */
|
|
67
|
+
actions?: ReactNode;
|
|
68
|
+
expanded?: boolean;
|
|
69
|
+
onToggle?: () => void;
|
|
70
|
+
/** Names the chevron, "Expand {label}", so a screen reader knows which entry. */
|
|
71
|
+
label?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* One entry of a history: collapsed to its heading and caption, opened in
|
|
75
|
+
* place to its body.
|
|
76
|
+
*
|
|
77
|
+
* An entry with no body has no chevron — a spacer keeps its glyph in line
|
|
78
|
+
* with the rows that have one — and its heading wraps rather than
|
|
79
|
+
* truncating, since there is nowhere else to read the rest of it.
|
|
80
|
+
*/
|
|
81
|
+
export declare function TimelineEntry(props: TimelineEntryProps): import("react").JSX.Element;
|
|
82
|
+
export declare namespace TimelineEntry {
|
|
83
|
+
var displayName: string;
|
|
84
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
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
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { mdiChevronDown, mdiChevronRight, mdiUnfoldLessHorizontal, mdiUnfoldMoreHorizontal } from "@aglyn/shared-data-mdi";
|
|
19
|
+
import { MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
20
|
+
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
21
|
+
import { Box, Button, Collapse, IconButton, Stack, Typography } from "@mui/material";
|
|
22
|
+
import { useCallback, useEffect, useId, useMemo, useState } from "react";
|
|
23
|
+
/**
|
|
24
|
+
* The open/closed state of a history's entries, held by the CARD so that
|
|
25
|
+
* the header's "Expand all" and each row's chevron act on one state.
|
|
26
|
+
*
|
|
27
|
+
* Kept as a default plus the keys that differ from it, so "Expand all"
|
|
28
|
+
* reaches the entries of pages the reader has not turned to yet, and a row
|
|
29
|
+
* closed after it stays closed.
|
|
30
|
+
*/ export function useTimelineExpansion() {
|
|
31
|
+
const [state, setState] = useState(()=>({
|
|
32
|
+
all: false,
|
|
33
|
+
flipped: new Set()
|
|
34
|
+
}));
|
|
35
|
+
const isExpanded = useCallback((key)=>state.all !== state.flipped.has(key), [
|
|
36
|
+
state
|
|
37
|
+
]);
|
|
38
|
+
const toggle = useCallback((key)=>{
|
|
39
|
+
setState((current)=>{
|
|
40
|
+
const flipped = new Set(current.flipped);
|
|
41
|
+
if (flipped.has(key)) flipped.delete(key);
|
|
42
|
+
else flipped.add(key);
|
|
43
|
+
return {
|
|
44
|
+
all: current.all,
|
|
45
|
+
flipped
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}, []);
|
|
49
|
+
const setAllExpanded = useCallback((all)=>{
|
|
50
|
+
setState({
|
|
51
|
+
all,
|
|
52
|
+
flipped: new Set()
|
|
53
|
+
});
|
|
54
|
+
}, []);
|
|
55
|
+
return useMemo(()=>({
|
|
56
|
+
isExpanded,
|
|
57
|
+
toggle,
|
|
58
|
+
allExpanded: state.all && state.flipped.size === 0,
|
|
59
|
+
setAllExpanded
|
|
60
|
+
}), [
|
|
61
|
+
isExpanded,
|
|
62
|
+
toggle,
|
|
63
|
+
state,
|
|
64
|
+
setAllExpanded
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
/** The card header's "Expand all" / "Collapse all". */ export function TimelineExpandAllButton(props) {
|
|
68
|
+
const { expansion, disabled } = props;
|
|
69
|
+
const open = expansion.allExpanded;
|
|
70
|
+
return /*#__PURE__*/ _jsx(Button, {
|
|
71
|
+
size: "small",
|
|
72
|
+
disabled: disabled,
|
|
73
|
+
startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
74
|
+
path: (open ? mdiUnfoldLessHorizontal : mdiUnfoldMoreHorizontal).path,
|
|
75
|
+
size: 0.8
|
|
76
|
+
}),
|
|
77
|
+
onClick: ()=>expansion.setAllExpanded(!open),
|
|
78
|
+
children: open ? 'Collapse all' : 'Expand all'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
TimelineExpandAllButton.displayName = 'TimelineExpandAllButton';
|
|
82
|
+
/**
|
|
83
|
+
* Newest-first pages of a history, over rows a listener window holds.
|
|
84
|
+
*
|
|
85
|
+
* The window is the query's bound — a hundred activities on a record, a
|
|
86
|
+
* page's worth on the feed — and a page the reader turns to past what it
|
|
87
|
+
* holds widens it, so the footer's Next is live while the probe row says
|
|
88
|
+
* more exists and the count stays "of more than" until it does not. Paged
|
|
89
|
+
* in memory otherwise: the contact timeline merges three sources that share
|
|
90
|
+
* no cursor, so only a slice of the merged stream can be turned.
|
|
91
|
+
*/ export function useTimelinePages(total, reader = {}) {
|
|
92
|
+
const { hasMore = false, showMore, loading = false } = reader;
|
|
93
|
+
const [page, setPage] = useState(0);
|
|
94
|
+
const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
|
|
95
|
+
const lastPage = Math.max(0, Math.ceil(total / pageSize) - 1);
|
|
96
|
+
// A page left empty by a delete shows the last one with rows; while the
|
|
97
|
+
// window is reading, or holds less than exists, the page asked for stands.
|
|
98
|
+
const shown = hasMore || loading ? page : Math.min(page, lastPage);
|
|
99
|
+
const start = shown * pageSize;
|
|
100
|
+
const short = start + pageSize > total;
|
|
101
|
+
useEffect(()=>{
|
|
102
|
+
if (short && hasMore && !loading && showMore) showMore();
|
|
103
|
+
}, [
|
|
104
|
+
short,
|
|
105
|
+
hasMore,
|
|
106
|
+
loading,
|
|
107
|
+
showMore
|
|
108
|
+
]);
|
|
109
|
+
const slice = useCallback((rows)=>rows.slice(start, start + pageSize), [
|
|
110
|
+
start,
|
|
111
|
+
pageSize
|
|
112
|
+
]);
|
|
113
|
+
const footer = {
|
|
114
|
+
page: shown,
|
|
115
|
+
pageSize,
|
|
116
|
+
rowCount: Math.max(0, Math.min(pageSize, total - start)),
|
|
117
|
+
count: hasMore ? undefined : total,
|
|
118
|
+
hasMore,
|
|
119
|
+
onPageChange: setPage,
|
|
120
|
+
onPageSizeChange: setPageSize
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
page: shown,
|
|
124
|
+
pageSize,
|
|
125
|
+
slice,
|
|
126
|
+
footer
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One entry of a history: collapsed to its heading and caption, opened in
|
|
131
|
+
* place to its body.
|
|
132
|
+
*
|
|
133
|
+
* An entry with no body has no chevron — a spacer keeps its glyph in line
|
|
134
|
+
* with the rows that have one — and its heading wraps rather than
|
|
135
|
+
* truncating, since there is nowhere else to read the rest of it.
|
|
136
|
+
*/ export function TimelineEntry(props) {
|
|
137
|
+
const { icon, chips, title, titleVariant = 'body2', meta, body, actions, expanded = false, onToggle, label } = props;
|
|
138
|
+
const bodyId = useId();
|
|
139
|
+
const expandable = Boolean(body) && Boolean(onToggle);
|
|
140
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
141
|
+
direction: "row",
|
|
142
|
+
spacing: 1,
|
|
143
|
+
sx: {
|
|
144
|
+
alignItems: 'flex-start'
|
|
145
|
+
},
|
|
146
|
+
"data-testid": "timeline-entry",
|
|
147
|
+
"data-expanded": expandable ? String(expanded) : undefined,
|
|
148
|
+
children: [
|
|
149
|
+
expandable ? /*#__PURE__*/ _jsx(IconButton, {
|
|
150
|
+
size: "small",
|
|
151
|
+
"aria-expanded": expanded,
|
|
152
|
+
"aria-controls": bodyId,
|
|
153
|
+
"aria-label": `${expanded ? 'Collapse' : 'Expand'} ${label != null ? label : 'entry'}`,
|
|
154
|
+
onClick: onToggle,
|
|
155
|
+
sx: {
|
|
156
|
+
mt: -0.25
|
|
157
|
+
},
|
|
158
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
159
|
+
path: (expanded ? mdiChevronDown : mdiChevronRight).path,
|
|
160
|
+
size: 0.8
|
|
161
|
+
})
|
|
162
|
+
}) : /*#__PURE__*/ _jsx(Box, {
|
|
163
|
+
"aria-hidden": true,
|
|
164
|
+
sx: {
|
|
165
|
+
width: 30,
|
|
166
|
+
flexShrink: 0
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
170
|
+
sx: {
|
|
171
|
+
color: 'text.secondary',
|
|
172
|
+
pt: 0.25,
|
|
173
|
+
fontSize: (theme)=>theme.typography.h6.fontSize
|
|
174
|
+
},
|
|
175
|
+
children: icon
|
|
176
|
+
}),
|
|
177
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
178
|
+
spacing: 0.5,
|
|
179
|
+
sx: {
|
|
180
|
+
flex: 1,
|
|
181
|
+
minWidth: 0
|
|
182
|
+
},
|
|
183
|
+
children: [
|
|
184
|
+
chips ? /*#__PURE__*/ _jsx(Stack, {
|
|
185
|
+
direction: "row",
|
|
186
|
+
spacing: 1,
|
|
187
|
+
sx: {
|
|
188
|
+
alignItems: 'center',
|
|
189
|
+
flexWrap: 'wrap',
|
|
190
|
+
rowGap: 0.5
|
|
191
|
+
},
|
|
192
|
+
children: chips
|
|
193
|
+
}) : null,
|
|
194
|
+
title ? /*#__PURE__*/ _jsx(Typography, {
|
|
195
|
+
variant: titleVariant,
|
|
196
|
+
component: "div",
|
|
197
|
+
noWrap: expandable && !expanded,
|
|
198
|
+
sx: {
|
|
199
|
+
overflowWrap: 'anywhere'
|
|
200
|
+
},
|
|
201
|
+
children: title
|
|
202
|
+
}) : null,
|
|
203
|
+
meta,
|
|
204
|
+
expandable ? /*#__PURE__*/ _jsx(Collapse, {
|
|
205
|
+
in: expanded,
|
|
206
|
+
unmountOnExit: true,
|
|
207
|
+
children: /*#__PURE__*/ _jsx(Box, {
|
|
208
|
+
id: bodyId,
|
|
209
|
+
children: body
|
|
210
|
+
})
|
|
211
|
+
}) : body
|
|
212
|
+
]
|
|
213
|
+
}),
|
|
214
|
+
actions ? /*#__PURE__*/ _jsx(Stack, {
|
|
215
|
+
direction: "row",
|
|
216
|
+
spacing: 0.5,
|
|
217
|
+
children: actions
|
|
218
|
+
}) : null
|
|
219
|
+
]
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
TimelineEntry.displayName = 'TimelineEntry';
|
|
223
|
+
|
|
224
|
+
//# sourceMappingURL=activity-timeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/activity-timeline.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n mdiChevronDown,\n mdiChevronRight,\n mdiUnfoldLessHorizontal,\n mdiUnfoldMoreHorizontal,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { ListPaginationProps } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { Box, Button, Collapse, IconButton, Stack, Typography } from '@mui/material'\nimport { type ReactNode, useCallback, useEffect, useId, useMemo, useState } from 'react'\n\n/*\n * THE ROW GRAMMAR OF EVERY CRM HISTORY: the Timeline on a contact, the\n * Activity card on a lead, a company and a deal, and the Recent activity\n * feed under the contacts list.\n *\n * Salesforce's activity timeline is the model. Each entry is a collapsed\n * row — a chevron, the kind, a one-line heading and the who/whom/when under\n * it — that opens in place to what it holds: an email's body, a note's whole\n * text. The card header carries \"Expand all\", the list is paged by the\n * console's own footer, and nothing a row showed before is dropped: what\n * does not fit on the collapsed row is in the opened one.\n */\n\n/** Which entries of one history are open. */\nexport interface TimelineExpansion {\n isExpanded: (key: string) => boolean\n toggle: (key: string) => void\n /** Every entry is open and none has been closed since. */\n allExpanded: boolean\n setAllExpanded: (expanded: boolean) => void\n}\n\n/**\n * The open/closed state of a history's entries, held by the CARD so that\n * the header's \"Expand all\" and each row's chevron act on one state.\n *\n * Kept as a default plus the keys that differ from it, so \"Expand all\"\n * reaches the entries of pages the reader has not turned to yet, and a row\n * closed after it stays closed.\n */\nexport function useTimelineExpansion(): TimelineExpansion {\n const [state, setState] = useState<{ all: boolean; flipped: ReadonlySet<string> }>(\n () => ({ all: false, flipped: new Set<string>() }),\n )\n const isExpanded = useCallback(\n (key: string) => state.all !== state.flipped.has(key),\n [state],\n )\n const toggle = useCallback((key: string) => {\n setState((current) => {\n const flipped = new Set(current.flipped)\n if (flipped.has(key)) flipped.delete(key)\n else flipped.add(key)\n return { all: current.all, flipped }\n })\n }, [])\n const setAllExpanded = useCallback((all: boolean) => {\n setState({ all, flipped: new Set<string>() })\n }, [])\n return useMemo(\n () => ({\n isExpanded,\n toggle,\n allExpanded: state.all && state.flipped.size === 0,\n setAllExpanded,\n }),\n [isExpanded, toggle, state, setAllExpanded],\n )\n}\n\n/** The card header's \"Expand all\" / \"Collapse all\". */\nexport function TimelineExpandAllButton(props: {\n expansion: TimelineExpansion\n disabled?: boolean\n}) {\n const { expansion, disabled } = props\n const open = expansion.allExpanded\n return (\n <Button\n size=\"small\"\n disabled={disabled}\n startIcon={\n <MdiIcon\n path={(open ? mdiUnfoldLessHorizontal : mdiUnfoldMoreHorizontal).path}\n size={0.8}\n />\n }\n onClick={() => expansion.setAllExpanded(!open)}\n >\n {open ? 'Collapse all' : 'Expand all'}\n </Button>\n )\n}\nTimelineExpandAllButton.displayName = 'TimelineExpandAllButton'\n\n/** The listener window a history pages over. */\nexport interface TimelineWindow {\n /** A further page exists past the rows the listener holds. */\n hasMore?: boolean\n /** Widens the listener by one step. */\n showMore?: () => void\n /** The listener is reading; the page asked for is held rather than pulled back. */\n loading?: boolean\n}\n\n/**\n * Newest-first pages of a history, over rows a listener window holds.\n *\n * The window is the query's bound — a hundred activities on a record, a\n * page's worth on the feed — and a page the reader turns to past what it\n * holds widens it, so the footer's Next is live while the probe row says\n * more exists and the count stays \"of more than\" until it does not. Paged\n * in memory otherwise: the contact timeline merges three sources that share\n * no cursor, so only a slice of the merged stream can be turned.\n */\nexport function useTimelinePages(total: number, reader: TimelineWindow = {}) {\n const { hasMore = false, showMore, loading = false } = reader\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const lastPage = Math.max(0, Math.ceil(total / pageSize) - 1)\n // A page left empty by a delete shows the last one with rows; while the\n // window is reading, or holds less than exists, the page asked for stands.\n const shown = hasMore || loading ? page : Math.min(page, lastPage)\n const start = shown * pageSize\n const short = start + pageSize > total\n useEffect(() => {\n if (short && hasMore && !loading && showMore) showMore()\n }, [short, hasMore, loading, showMore])\n const slice = useCallback(\n <T,>(rows: readonly T[]): T[] => rows.slice(start, start + pageSize),\n [start, pageSize],\n )\n const footer: ListPaginationProps = {\n page: shown,\n pageSize,\n rowCount: Math.max(0, Math.min(pageSize, total - start)),\n count: hasMore ? undefined : total,\n hasMore,\n onPageChange: setPage,\n onPageSizeChange: setPageSize,\n }\n return { page: shown, pageSize, slice, footer }\n}\n\nexport interface TimelineEntryProps {\n /** The glyph for the entry's kind. */\n icon: ReactNode\n /** The chips that say what the entry is: its kind, its direction, its delivery state. */\n chips?: ReactNode\n /** The one line a collapsed entry reads as: a subject, a summary, a note's first line. */\n title?: ReactNode\n /** A subject reads as one; a note's own words as body text. */\n titleVariant?: 'subtitle2' | 'body2'\n /** The line under it: who, to or from whom, when. */\n meta?: ReactNode\n /** What the entry opens to. Absent, there is nothing more to show and no chevron. */\n body?: ReactNode\n /** Controls on this one entry — edit, delete. */\n actions?: ReactNode\n expanded?: boolean\n onToggle?: () => void\n /** Names the chevron, \"Expand {label}\", so a screen reader knows which entry. */\n label?: string\n}\n\n/**\n * One entry of a history: collapsed to its heading and caption, opened in\n * place to its body.\n *\n * An entry with no body has no chevron — a spacer keeps its glyph in line\n * with the rows that have one — and its heading wraps rather than\n * truncating, since there is nowhere else to read the rest of it.\n */\nexport function TimelineEntry(props: TimelineEntryProps) {\n const {\n icon,\n chips,\n title,\n titleVariant = 'body2',\n meta,\n body,\n actions,\n expanded = false,\n onToggle,\n label,\n } = props\n const bodyId = useId()\n const expandable = Boolean(body) && Boolean(onToggle)\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'flex-start' }}\n data-testid=\"timeline-entry\"\n data-expanded={expandable ? String(expanded) : undefined}\n >\n {expandable ? (\n <IconButton\n size=\"small\"\n aria-expanded={expanded}\n aria-controls={bodyId}\n aria-label={`${expanded ? 'Collapse' : 'Expand'} ${label ?? 'entry'}`}\n onClick={onToggle}\n sx={{ mt: -0.25 }}\n >\n <MdiIcon path={(expanded ? mdiChevronDown : mdiChevronRight).path} size={0.8} />\n </IconButton>\n ) : (\n <Box aria-hidden sx={{ width: 30, flexShrink: 0 }} />\n )}\n <Stack\n sx={{\n color: 'text.secondary',\n pt: 0.25,\n fontSize: (theme) => theme.typography.h6.fontSize,\n }}\n >\n {icon}\n </Stack>\n <Stack spacing={0.5} sx={{ flex: 1, minWidth: 0 }}>\n {chips ? (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n {chips}\n </Stack>\n ) : null}\n {title ? (\n <Typography\n variant={titleVariant}\n component=\"div\"\n noWrap={expandable && !expanded}\n sx={{ overflowWrap: 'anywhere' }}\n >\n {title}\n </Typography>\n ) : null}\n {meta}\n {expandable ? (\n <Collapse in={expanded} unmountOnExit>\n <Box id={bodyId}>{body}</Box>\n </Collapse>\n ) : (\n body\n )}\n </Stack>\n {actions ? (\n <Stack direction=\"row\" spacing={0.5}>\n {actions}\n </Stack>\n ) : null}\n </Stack>\n )\n}\nTimelineEntry.displayName = 'TimelineEntry'\n"],"names":["mdiChevronDown","mdiChevronRight","mdiUnfoldLessHorizontal","mdiUnfoldMoreHorizontal","MdiIcon","TABLE_PAGE_SIZE_DEFAULT","Box","Button","Collapse","IconButton","Stack","Typography","useCallback","useEffect","useId","useMemo","useState","useTimelineExpansion","state","setState","all","flipped","Set","isExpanded","key","has","toggle","current","delete","add","setAllExpanded","allExpanded","size","TimelineExpandAllButton","props","expansion","disabled","open","startIcon","path","onClick","displayName","useTimelinePages","total","reader","hasMore","showMore","loading","page","setPage","pageSize","setPageSize","lastPage","Math","max","ceil","shown","min","start","short","slice","rows","footer","rowCount","count","undefined","onPageChange","onPageSizeChange","TimelineEntry","icon","chips","title","titleVariant","meta","body","actions","expanded","onToggle","label","bodyId","expandable","Boolean","direction","spacing","sx","alignItems","data-testid","data-expanded","String","aria-expanded","aria-controls","aria-label","mt","aria-hidden","width","flexShrink","color","pt","fontSize","theme","typography","h6","flex","minWidth","flexWrap","rowGap","variant","component","noWrap","overflowWrap","in","unmountOnExit","id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,cAAc,EACdC,eAAe,EACfC,uBAAuB,EACvBC,uBAAuB,QAClB,yBAAwB;AAC/B,SAASC,OAAO,QAAQ,uBAAsB;AAE9C,SAASC,uBAAuB,QAAQ,8CAA6C;AACrF,SAASC,GAAG,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACpF,SAAyBC,WAAW,EAAEC,SAAS,EAAEC,KAAK,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AAwBxF;;;;;;;CAOC,GACD,OAAO,SAASC;IACd,MAAM,CAACC,OAAOC,SAAS,GAAGH,SACxB,IAAO,CAAA;YAAEI,KAAK;YAAOC,SAAS,IAAIC;QAAc,CAAA;IAElD,MAAMC,aAAaX,YACjB,CAACY,MAAgBN,MAAME,GAAG,KAAKF,MAAMG,OAAO,CAACI,GAAG,CAACD,MACjD;QAACN;KAAM;IAET,MAAMQ,SAASd,YAAY,CAACY;QAC1BL,SAAS,CAACQ;YACR,MAAMN,UAAU,IAAIC,IAAIK,QAAQN,OAAO;YACvC,IAAIA,QAAQI,GAAG,CAACD,MAAMH,QAAQO,MAAM,CAACJ;iBAChCH,QAAQQ,GAAG,CAACL;YACjB,OAAO;gBAAEJ,KAAKO,QAAQP,GAAG;gBAAEC;YAAQ;QACrC;IACF,GAAG,EAAE;IACL,MAAMS,iBAAiBlB,YAAY,CAACQ;QAClCD,SAAS;YAAEC;YAAKC,SAAS,IAAIC;QAAc;IAC7C,GAAG,EAAE;IACL,OAAOP,QACL,IAAO,CAAA;YACLQ;YACAG;YACAK,aAAab,MAAME,GAAG,IAAIF,MAAMG,OAAO,CAACW,IAAI,KAAK;YACjDF;QACF,CAAA,GACA;QAACP;QAAYG;QAAQR;QAAOY;KAAe;AAE/C;AAEA,qDAAqD,GACrD,OAAO,SAASG,wBAAwBC,KAGvC;IACC,MAAM,EAAEC,SAAS,EAAEC,QAAQ,EAAE,GAAGF;IAChC,MAAMG,OAAOF,UAAUJ,WAAW;IAClC,qBACE,KAACxB;QACCyB,MAAK;QACLI,UAAUA;QACVE,yBACE,KAAClC;YACCmC,MAAM,AAACF,CAAAA,OAAOnC,0BAA0BC,uBAAsB,EAAGoC,IAAI;YACrEP,MAAM;;QAGVQ,SAAS,IAAML,UAAUL,cAAc,CAAC,CAACO;kBAExCA,OAAO,iBAAiB;;AAG/B;AACAJ,wBAAwBQ,WAAW,GAAG;AAYtC;;;;;;;;;CASC,GACD,OAAO,SAASC,iBAAiBC,KAAa,EAAEC,SAAyB,CAAC,CAAC;IACzE,MAAM,EAAEC,UAAU,KAAK,EAAEC,QAAQ,EAAEC,UAAU,KAAK,EAAE,GAAGH;IACvD,MAAM,CAACI,MAAMC,QAAQ,GAAGjC,SAAS;IACjC,MAAM,CAACkC,UAAUC,YAAY,GAAGnC,SAASX;IACzC,MAAM+C,WAAWC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,IAAI,CAACZ,QAAQO,YAAY;IAC3D,wEAAwE;IACxE,2EAA2E;IAC3E,MAAMM,QAAQX,WAAWE,UAAUC,OAAOK,KAAKI,GAAG,CAACT,MAAMI;IACzD,MAAMM,QAAQF,QAAQN;IACtB,MAAMS,QAAQD,QAAQR,WAAWP;IACjC9B,UAAU;QACR,IAAI8C,SAASd,WAAW,CAACE,WAAWD,UAAUA;IAChD,GAAG;QAACa;QAAOd;QAASE;QAASD;KAAS;IACtC,MAAMc,QAAQhD,YACZ,CAAKiD,OAA4BA,KAAKD,KAAK,CAACF,OAAOA,QAAQR,WAC3D;QAACQ;QAAOR;KAAS;IAEnB,MAAMY,SAA8B;QAClCd,MAAMQ;QACNN;QACAa,UAAUV,KAAKC,GAAG,CAAC,GAAGD,KAAKI,GAAG,CAACP,UAAUP,QAAQe;QACjDM,OAAOnB,UAAUoB,YAAYtB;QAC7BE;QACAqB,cAAcjB;QACdkB,kBAAkBhB;IACpB;IACA,OAAO;QAAEH,MAAMQ;QAAON;QAAUU;QAAOE;IAAO;AAChD;AAuBA;;;;;;;CAOC,GACD,OAAO,SAASM,cAAclC,KAAyB;IACrD,MAAM,EACJmC,IAAI,EACJC,KAAK,EACLC,KAAK,EACLC,eAAe,OAAO,EACtBC,IAAI,EACJC,IAAI,EACJC,OAAO,EACPC,WAAW,KAAK,EAChBC,QAAQ,EACRC,KAAK,EACN,GAAG5C;IACJ,MAAM6C,SAASjE;IACf,MAAMkE,aAAaC,QAAQP,SAASO,QAAQJ;IAC5C,qBACE,MAACnE;QACCwE,WAAU;QACVC,SAAS;QACTC,IAAI;YAAEC,YAAY;QAAa;QAC/BC,eAAY;QACZC,iBAAeP,aAAaQ,OAAOZ,YAAYX;;YAE9Ce,2BACC,KAACvE;gBACCuB,MAAK;gBACLyD,iBAAeb;gBACfc,iBAAeX;gBACfY,cAAY,GAAGf,WAAW,aAAa,SAAS,CAAC,EAAEE,gBAAAA,QAAS,SAAS;gBACrEtC,SAASqC;gBACTO,IAAI;oBAAEQ,IAAI,CAAC;gBAAK;0BAEhB,cAAA,KAACxF;oBAAQmC,MAAM,AAACqC,CAAAA,WAAW5E,iBAAiBC,eAAc,EAAGsC,IAAI;oBAAEP,MAAM;;+BAG3E,KAAC1B;gBAAIuF,aAAW;gBAACT,IAAI;oBAAEU,OAAO;oBAAIC,YAAY;gBAAE;;0BAElD,KAACrF;gBACC0E,IAAI;oBACFY,OAAO;oBACPC,IAAI;oBACJC,UAAU,CAACC,QAAUA,MAAMC,UAAU,CAACC,EAAE,CAACH,QAAQ;gBACnD;0BAEC7B;;0BAEH,MAAC3D;gBAAMyE,SAAS;gBAAKC,IAAI;oBAAEkB,MAAM;oBAAGC,UAAU;gBAAE;;oBAC7CjC,sBACC,KAAC5D;wBACCwE,WAAU;wBACVC,SAAS;wBACTC,IAAI;4BAAEC,YAAY;4BAAUmB,UAAU;4BAAQC,QAAQ;wBAAI;kCAEzDnC;yBAED;oBACHC,sBACC,KAAC5D;wBACC+F,SAASlC;wBACTmC,WAAU;wBACVC,QAAQ5B,cAAc,CAACJ;wBACvBQ,IAAI;4BAAEyB,cAAc;wBAAW;kCAE9BtC;yBAED;oBACHE;oBACAO,2BACC,KAACxE;wBAASsG,IAAIlC;wBAAUmC,aAAa;kCACnC,cAAA,KAACzG;4BAAI0G,IAAIjC;sCAASL;;yBAGpBA;;;YAGHC,wBACC,KAACjE;gBAAMwE,WAAU;gBAAMC,SAAS;0BAC7BR;iBAED;;;AAGV;AACAP,cAAc3B,WAAW,GAAG"}
|
|
@@ -27,12 +27,12 @@ export interface CompaniesSectionProps {
|
|
|
27
27
|
*
|
|
28
28
|
* ## What a filter may be
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* property a
|
|
35
|
-
* missing. `
|
|
30
|
+
* Every clause and the search word are on the one query (AGL-3321): the
|
|
31
|
+
* search reads the name's and the domain's word prefixes, the name is also
|
|
32
|
+
* an exact match and a prefix, the owner a choice from the roster, and "No
|
|
33
|
+
* next activity" a `null` — each reaching the whole collection, which is
|
|
34
|
+
* the property a filter must have: a company on page four is found, not
|
|
35
|
+
* reported missing. `COMPANY_LIST_DECLARATION` states what the query asks.
|
|
36
36
|
*
|
|
37
37
|
* ## Creating is a drawer
|
|
38
38
|
*
|
|
@@ -16,31 +16,31 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
-
import { CRM_COLLECTIONS,
|
|
19
|
+
import { CRM_COLLECTIONS, pluginDocsHelp, crmMemberPickerLabel } from "@aglyn/aglyn";
|
|
20
20
|
import { mdiPlus } from "@aglyn/shared-data-mdi";
|
|
21
21
|
import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
22
22
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
23
23
|
import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
24
|
-
import { listFilterColumn } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
24
|
+
import { listFilterColumn } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
25
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
25
26
|
import { useCrmSavedView } from "../hooks/use-crm-saved-view.js";
|
|
26
27
|
import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
|
|
27
28
|
import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
|
|
28
29
|
import { CRM_NEXT_ACTIVITY_FILTER_FIELD, CRM_NEXT_ACTIVITY_FILTER_HEADER, nextActivityColumn } from "./crm-next-activity-column.js";
|
|
30
|
+
import { useCrmFoldsScope, useCrmListQuery } from "../hooks/use-crm-list-query.js";
|
|
29
31
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
30
32
|
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
31
33
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
32
34
|
import CrmViewsControl from "./crm-views-control.js";
|
|
33
35
|
import { CrmListActions, CrmListToolbar } from "./crm-list-toolbar.js";
|
|
34
36
|
import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
35
|
-
import { listFilterConstraints, usePagedCollection, useFirestore } from "@aglyn/tenant-feature-instance";
|
|
36
37
|
import { Button, Chip, Stack, Typography } from "@mui/material";
|
|
37
38
|
import { getGridSingleSelectOperators } from "@mui/x-data-grid";
|
|
38
|
-
import { collection, limit, orderBy, query } from "firebase/firestore";
|
|
39
39
|
import { useRouter } from "next/navigation";
|
|
40
|
-
import { useCallback, useMemo, useState } from "react";
|
|
41
|
-
import { COMPANY_LIST_FILTER_FIELDS } from "../constants/company-filters.js";
|
|
40
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
41
|
+
import { COMPANY_LIST_DECLARATION, COMPANY_LIST_FILTER_FIELDS, COMPANY_PREFIX_SEARCH } from "../constants/company-filters.js";
|
|
42
42
|
import { useContactFieldDefinitions } from "../hooks/use-contact-field-definitions.js";
|
|
43
|
-
import {
|
|
43
|
+
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
44
44
|
import { useOrgMemberOptions } from "../hooks/use-org-member-options.js";
|
|
45
45
|
import { companiesCsv } from "../model/companies-csv.js";
|
|
46
46
|
import { downloadTextFile } from "../model/contacts-csv.js";
|
|
@@ -49,10 +49,7 @@ import CompaniesBulkBar from "./companies-bulk-bar.js";
|
|
|
49
49
|
import CompanyEditDrawer from "./company-edit-drawer.js";
|
|
50
50
|
import { CompanyImportButton } from "./company-import-drawer.js";
|
|
51
51
|
import { customFieldColumns } from "./contact-custom-columns.js";
|
|
52
|
-
/** What the companies grid's panel offers
|
|
53
|
-
...COMPANY_LIST_FILTER_FIELDS,
|
|
54
|
-
CRM_NEXT_ACTIVITY_FILTER_FIELD
|
|
55
|
-
];
|
|
52
|
+
/** What the companies grid's panel offers — every field the query asks. */ const COMPANY_GRID_FILTER_FIELDS = COMPANY_LIST_FILTER_FIELDS;
|
|
56
53
|
const COMPANY_GRID_FILTER_HEADERS = {
|
|
57
54
|
name: 'Company',
|
|
58
55
|
ownerUid: 'Owner',
|
|
@@ -79,12 +76,12 @@ const COMPANY_GRID_FILTER_HEADERS = {
|
|
|
79
76
|
*
|
|
80
77
|
* ## What a filter may be
|
|
81
78
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* property a
|
|
87
|
-
* missing. `
|
|
79
|
+
* Every clause and the search word are on the one query (AGL-3321): the
|
|
80
|
+
* search reads the name's and the domain's word prefixes, the name is also
|
|
81
|
+
* an exact match and a prefix, the owner a choice from the roster, and "No
|
|
82
|
+
* next activity" a `null` — each reaching the whole collection, which is
|
|
83
|
+
* the property a filter must have: a company on page four is found, not
|
|
84
|
+
* reported missing. `COMPANY_LIST_DECLARATION` states what the query asks.
|
|
88
85
|
*
|
|
89
86
|
* ## Creating is a drawer
|
|
90
87
|
*
|
|
@@ -99,11 +96,9 @@ const COMPANY_GRID_FILTER_HEADERS = {
|
|
|
99
96
|
* the same file the bar writes over the selection, headed as the import
|
|
100
97
|
* reads it — and Import CSV opens the companies drawer beside it.
|
|
101
98
|
*/ export function CompaniesSection(props) {
|
|
102
|
-
var _viewFilters_find, _ref;
|
|
103
99
|
const { hostId, org, basePath } = props;
|
|
104
100
|
const routes = crmRoutes(basePath);
|
|
105
101
|
const router = useRouter();
|
|
106
|
-
const firestore = useFirestore();
|
|
107
102
|
const { scope, orgId, visibleTo } = useCrmScope({
|
|
108
103
|
hostId,
|
|
109
104
|
org
|
|
@@ -116,54 +111,36 @@ const COMPANY_GRID_FILTER_HEADERS = {
|
|
|
116
111
|
// The org's company fields, for the optional columns below (AGL-2661).
|
|
117
112
|
const companyFields = useContactFieldDefinitions(orgId, 'company');
|
|
118
113
|
/*
|
|
119
|
-
* The
|
|
120
|
-
* grid's panel
|
|
121
|
-
*
|
|
122
|
-
* as the filter always was — the query is rebuilt from it.
|
|
114
|
+
* The clauses are the saved VIEW'S (AGL-2617), beside its columns and
|
|
115
|
+
* sort; the grid's panel and the chips edit them, and the query answers
|
|
116
|
+
* every one and the search word together (AGL-3321).
|
|
123
117
|
*/ const views = useCrmSavedView({
|
|
124
118
|
section: 'companies',
|
|
125
119
|
hostId,
|
|
126
120
|
org,
|
|
127
121
|
basePath
|
|
128
122
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
* not it, and setting the grid's clause keeps it.
|
|
134
|
-
*/ const viewFilters = views.state.filters;
|
|
135
|
-
const filter = (_viewFilters_find = viewFilters.find((clause)=>!isNoNextActivityClause(clause))) != null ? _viewFilters_find : null;
|
|
136
|
-
const setFilter = useCallback((request)=>views.setFilters([
|
|
137
|
-
...request ? [
|
|
138
|
-
request
|
|
139
|
-
] : [],
|
|
140
|
-
...viewFilters.filter(isNoNextActivityClause)
|
|
141
|
-
]), [
|
|
142
|
-
views.setFilters,
|
|
143
|
-
viewFilters
|
|
144
|
-
]);
|
|
145
|
-
const { rows: loaded, status, hasMore, page, setPage, pageSize, setPageSize } = usePagedCollection((pageLimit)=>{
|
|
146
|
-
if (!scope) return null;
|
|
147
|
-
const constraints = listFilterConstraints(COMPANY_LIST_FILTER_FIELDS, filter);
|
|
148
|
-
return query(collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies), // Absent at the organization level, where the tokens are `null` (AGL-2630).
|
|
149
|
-
...crmVisibleToClause(visibleTo), ...constraints != null ? constraints : [
|
|
150
|
-
orderBy('updatedAt', 'desc')
|
|
151
|
-
], limit(pageLimit));
|
|
152
|
-
}, [
|
|
153
|
-
firestore,
|
|
123
|
+
const viewFilters = views.state.filters;
|
|
124
|
+
const [searchWords, setSearchWords] = useState([]);
|
|
125
|
+
const foldsScope = useCrmFoldsScope(orgId, visibleTo);
|
|
126
|
+
const { rows: companies, status, hasMore, page, setPage, pageSize, setPageSize, plan } = useCrmListQuery({
|
|
154
127
|
scope,
|
|
128
|
+
collection: CRM_COLLECTIONS.companies,
|
|
155
129
|
visibleTo,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
130
|
+
foldsScope,
|
|
131
|
+
declaration: COMPANY_LIST_DECLARATION,
|
|
132
|
+
clauses: viewFilters,
|
|
133
|
+
search: searchWords,
|
|
134
|
+
prefixSearch: COMPANY_PREFIX_SEARCH
|
|
159
135
|
});
|
|
160
|
-
const companies = useMemo(()=>filterByNextActivity(loaded, viewFilters), [
|
|
161
|
-
loaded,
|
|
162
|
-
viewFilters
|
|
163
|
-
]);
|
|
164
136
|
const nowMs = useMemo(()=>Date.now(), []);
|
|
165
137
|
const [createOpen, setCreateOpen] = useState(false);
|
|
166
138
|
const [selectedIds, setSelectedIds] = useState([]);
|
|
139
|
+
// A filter, a search or a page is a different set of rows.
|
|
140
|
+
useEffect(()=>setSelectedIds([]), [
|
|
141
|
+
plan,
|
|
142
|
+
page
|
|
143
|
+
]);
|
|
167
144
|
/*
|
|
168
145
|
* The owner is written by ADDRESS, because the companies import resolves
|
|
169
146
|
* an owner by email; the same options reach the bar, so the page's file
|
|
@@ -372,11 +349,7 @@ const COMPANY_GRID_FILTER_HEADERS = {
|
|
|
372
349
|
]);
|
|
373
350
|
/*
|
|
374
351
|
* The grid's own Filters panel and quick search edit the view's clauses
|
|
375
|
-
* (AGL-3313)
|
|
376
|
-
* narrow beyond the page — so the panel's clause replaces the last, and
|
|
377
|
-
* "No next activity", which narrows the loaded page, stands beside it.
|
|
378
|
-
* The search box is the name's prefix: the one text search the indexes
|
|
379
|
-
* serve here, so typing in it is the same clause as "Company starts with".
|
|
352
|
+
* (AGL-3313); the query answers them all (AGL-3321).
|
|
380
353
|
*/ const ownerOptions = useMemo(()=>members.options.map((option)=>({
|
|
381
354
|
value: option.uid,
|
|
382
355
|
label: crmMemberPickerLabel(option)
|
|
@@ -389,42 +362,35 @@ const COMPANY_GRID_FILTER_HEADERS = {
|
|
|
389
362
|
columns,
|
|
390
363
|
ownerOptions
|
|
391
364
|
]);
|
|
392
|
-
const searchWords = useMemo(()=>(filter == null ? void 0 : filter.field) === 'name' && filter.op === 'startsWith' ? [
|
|
393
|
-
filter.value
|
|
394
|
-
] : [], [
|
|
395
|
-
filter
|
|
396
|
-
]);
|
|
397
|
-
const setSearchWords = useCallback((words)=>{
|
|
398
|
-
const term = words.join(' ').trim();
|
|
399
|
-
if (term) setFilter({
|
|
400
|
-
field: 'name',
|
|
401
|
-
op: 'startsWith',
|
|
402
|
-
value: term
|
|
403
|
-
});
|
|
404
|
-
else if ((filter == null ? void 0 : filter.field) === 'name' && filter.op === 'startsWith') setFilter(null);
|
|
405
|
-
}, [
|
|
406
|
-
filter,
|
|
407
|
-
setFilter
|
|
408
|
-
]);
|
|
409
365
|
const gridFilter = useListGridFilter({
|
|
410
366
|
clauses: viewFilters,
|
|
411
367
|
onChange: views.setFilters,
|
|
412
368
|
selectFields: [
|
|
413
369
|
'ownerUid'
|
|
414
370
|
],
|
|
415
|
-
single: true,
|
|
416
|
-
keepAlongside: isNoNextActivityClause,
|
|
417
371
|
search: {
|
|
418
372
|
words: searchWords,
|
|
419
373
|
onChange: setSearchWords
|
|
420
374
|
}
|
|
421
375
|
});
|
|
376
|
+
// What the query could not hold, named by the clause the reader set.
|
|
377
|
+
const refused = useMemo(()=>listQueryRefusals(plan.refused, {
|
|
378
|
+
fields: COMPANY_GRID_FILTER_FIELDS,
|
|
379
|
+
headers: COMPANY_GRID_FILTER_HEADERS,
|
|
380
|
+
options: {
|
|
381
|
+
ownerUid: ownerOptions
|
|
382
|
+
}
|
|
383
|
+
}), [
|
|
384
|
+
plan.refused,
|
|
385
|
+
ownerOptions
|
|
386
|
+
]);
|
|
422
387
|
/*
|
|
423
388
|
* The grid's models are the view's (AGL-2617). The filter model shows the
|
|
424
389
|
* view's clause in the panel as a typed one would appear — the owner's
|
|
425
390
|
* stored `equals` back as the single-select `is` the panel offers — so a
|
|
426
391
|
* view opened from its address reads as filtered, not as a mystery.
|
|
427
392
|
*/ const grid = useCrmViewGrid(views, filterColumns);
|
|
393
|
+
/** Whether anything narrows the list, so an empty one is "no match", not "none yet". */ const narrowed = viewFilters.length > 0 || searchWords.some((word)=>word.trim());
|
|
428
394
|
const newCompanyButton = /*#__PURE__*/ _jsx(Button, {
|
|
429
395
|
size: "small",
|
|
430
396
|
variant: "contained",
|
|
@@ -487,10 +453,14 @@ const COMPANY_GRID_FILTER_HEADERS = {
|
|
|
487
453
|
options: {
|
|
488
454
|
ownerUid: ownerOptions
|
|
489
455
|
},
|
|
490
|
-
|
|
456
|
+
marksServed: false
|
|
491
457
|
})
|
|
492
458
|
]
|
|
493
459
|
}),
|
|
460
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
461
|
+
refused: refused,
|
|
462
|
+
notices: plan.notices
|
|
463
|
+
}),
|
|
494
464
|
/*#__PURE__*/ _jsx(CompaniesBulkBar, {
|
|
495
465
|
hostId: hostId,
|
|
496
466
|
scope: scope,
|
|
@@ -511,16 +481,16 @@ const COMPANY_GRID_FILTER_HEADERS = {
|
|
|
511
481
|
selected: selectedIds,
|
|
512
482
|
onChange: setSelectedIds
|
|
513
483
|
},
|
|
514
|
-
noRowsLabel:
|
|
515
|
-
noRowsDescription:
|
|
516
|
-
noRowsAction: newCompanyButton,
|
|
484
|
+
noRowsLabel: narrowed ? 'No companies match these filters' : 'No companies yet',
|
|
485
|
+
noRowsDescription: narrowed ? undefined : 'A company groups the contacts who work at one business, with its domain, owner and address. Create the first one, or link a contact to a company from their page.',
|
|
486
|
+
noRowsAction: narrowed ? undefined : newCompanyButton,
|
|
517
487
|
onOpen: (id)=>openCompany(String(id)),
|
|
518
488
|
// An empty table while the read is in flight reads as "you have
|
|
519
489
|
// none" rather than "these are on their way".
|
|
520
490
|
loading: !scope || status === 'loading',
|
|
521
491
|
/*
|
|
522
492
|
* The grid must NOT also filter. The query answers it, so a client
|
|
523
|
-
* pass could only drop rows the query already matched.
|
|
493
|
+
* pass could only drop rows the query already matched (AGL-3321).
|
|
524
494
|
*/ filterMode: "server",
|
|
525
495
|
filterModel: gridFilter.filterModel,
|
|
526
496
|
onFilterModelChange: gridFilter.onFilterModelChange,
|