@crisp-ui-kit/crisp 0.54.2 → 0.54.4
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/dist/board.css +13 -1
- package/dist/board.layered.css +13 -1
- package/dist/chip.css +7 -1
- package/dist/chip.d.ts +2 -1
- package/dist/chip.layered.css +7 -1
- package/dist/cjs/components/chip/chip.js +3 -2
- package/dist/cjs/components/chip/chip.recipe.js +37 -1
- package/dist/cjs/components/datatable/datatable.js +489 -111
- package/dist/cjs/components/emptystate/emptystate.js +3 -2
- package/dist/cjs/components/emptystate/emptystate.recipe.js +26 -1
- package/dist/cjs/components/personchip/index.js +17 -0
- package/dist/cjs/components/personchip/personchip.js +50 -0
- package/dist/cjs/components/table/table.js +2 -2
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +41 -4
- package/dist/cjs/components/tabs/tabs.js +2 -2
- package/dist/cjs/components/tabs/tabs.recipe.js +30 -3
- package/dist/cjs/components/text/index.js +7 -0
- package/dist/cjs/components/text/text.js +55 -0
- package/dist/cjs/components/text/text.recipe.js +34 -0
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/product/board/board.js +60 -2
- package/dist/cjs/product/board/board.recipe.js +10 -0
- package/dist/cjs/product/comments/comments.js +426 -0
- package/dist/cjs/product/comments/comments.recipe.js +126 -0
- package/dist/cjs/product/comments/index.js +8 -0
- package/dist/cjs/product/headerbar/headerbar.recipe.js +9 -1
- package/dist/cjs/product/listpage/listpage.recipe.js +10 -1
- package/dist/cjs/product/objectsettings/index.js +16 -0
- package/dist/cjs/product/objectsettings/objectsettings.js +365 -0
- package/dist/cjs/product/objectsettings/objectsettings.recipe.js +65 -0
- package/dist/cjs/product/recordpage/recordpage.recipe.js +10 -1
- package/dist/cjs/product/recordstable/recordstable.js +89 -6
- package/dist/cjs/product/richtext/richtext.recipe.js +26 -1
- package/dist/cjs/product/taskstable/taskstable.recipe.js +10 -1
- package/dist/comments.css +80 -0
- package/dist/comments.d.ts +7 -0
- package/dist/comments.layered.css +82 -0
- package/dist/emptystate.css +15 -1
- package/dist/emptystate.d.ts +2 -2
- package/dist/emptystate.layered.css +15 -1
- package/dist/esm/components/chip/chip.js +3 -2
- package/dist/esm/components/chip/chip.recipe.js +37 -1
- package/dist/esm/components/datatable/datatable.js +488 -111
- package/dist/esm/components/emptystate/emptystate.js +4 -3
- package/dist/esm/components/emptystate/emptystate.recipe.js +26 -1
- package/dist/esm/components/emptystate/index.js +1 -1
- package/dist/esm/components/personchip/index.js +1 -0
- package/dist/esm/components/personchip/personchip.js +47 -0
- package/dist/esm/components/table/table.js +2 -2
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +43 -6
- package/dist/esm/components/tabs/tabs.js +2 -2
- package/dist/esm/components/tabs/tabs.recipe.js +30 -3
- package/dist/esm/components/text/index.js +2 -0
- package/dist/esm/components/text/text.js +19 -0
- package/dist/esm/components/text/text.recipe.js +31 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/product/board/board.js +61 -3
- package/dist/esm/product/board/board.recipe.js +10 -0
- package/dist/esm/product/comments/comments.js +388 -0
- package/dist/esm/product/comments/comments.recipe.js +123 -0
- package/dist/esm/product/comments/index.js +2 -0
- package/dist/esm/product/headerbar/headerbar.recipe.js +9 -1
- package/dist/esm/product/listpage/listpage.recipe.js +10 -1
- package/dist/esm/product/objectsettings/index.js +2 -0
- package/dist/esm/product/objectsettings/objectsettings.js +322 -0
- package/dist/esm/product/objectsettings/objectsettings.recipe.js +62 -0
- package/dist/esm/product/recordpage/recordpage.recipe.js +10 -1
- package/dist/esm/product/recordstable/recordstable.js +90 -8
- package/dist/esm/product/richtext/richtext.recipe.js +26 -1
- package/dist/esm/product/taskstable/taskstable.recipe.js +10 -1
- package/dist/headerbar.css +1 -1
- package/dist/headerbar.layered.css +1 -1
- package/dist/listpage.css +8 -1
- package/dist/listpage.layered.css +8 -1
- package/dist/objectsettings.css +49 -0
- package/dist/objectsettings.d.ts +7 -0
- package/dist/objectsettings.layered.css +51 -0
- package/dist/recordpage.css +13 -2
- package/dist/recordpage.layered.css +13 -2
- package/dist/richtext.css +11 -1
- package/dist/richtext.layered.css +11 -1
- package/dist/styles.css +224 -10
- package/dist/styles.layered.css +224 -10
- package/dist/tabs.css +7 -1
- package/dist/tabs.d.ts +2 -2
- package/dist/tabs.layered.css +7 -1
- package/dist/taskstable.css +8 -1
- package/dist/taskstable.layered.css +8 -1
- package/dist/text.css +9 -0
- package/dist/text.d.ts +7 -0
- package/dist/text.layered.css +11 -0
- package/dist/types/components/chip/chip.d.ts +32 -0
- package/dist/types/components/chip/chip.recipe.d.ts +31 -0
- package/dist/types/components/datatable/datatable.d.ts +28 -4
- package/dist/types/components/emptystate/emptystate.d.ts +29 -4
- package/dist/types/components/emptystate/emptystate.recipe.d.ts +2 -0
- package/dist/types/components/emptystate/index.d.ts +1 -1
- package/dist/types/components/personchip/index.d.ts +1 -0
- package/dist/types/components/personchip/personchip.d.ts +37 -0
- package/dist/types/components/table/table.d.ts +36 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +8 -2
- package/dist/types/components/tabs/tabs.d.ts +4 -2
- package/dist/types/components/tabs/tabs.recipe.d.ts +18 -3
- package/dist/types/components/text/index.d.ts +2 -0
- package/dist/types/components/text/text.d.ts +32 -0
- package/dist/types/components/text/text.recipe.d.ts +40 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/product/board/board.d.ts +20 -1
- package/dist/types/product/comments/comments.d.ts +192 -0
- package/dist/types/product/comments/comments.recipe.d.ts +3 -0
- package/dist/types/product/comments/index.d.ts +2 -0
- package/dist/types/product/objectsettings/index.d.ts +2 -0
- package/dist/types/product/objectsettings/objectsettings.d.ts +261 -0
- package/dist/types/product/objectsettings/objectsettings.recipe.d.ts +3 -0
- package/dist/types/product/recordstable/recordstable.d.ts +24 -2
- package/package.json +21 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Calendar, CheckSquare, Clock, DollarSign, FileText, Flag, Hash, Link2, ListChecks, MapPin, MoreHorizontal, Pencil, Phone, Search, SlidersHorizontal, Sparkles, Star, Type as TypeIcon, User as UserIcon, } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Badge } from "../../components/badge/index.js";
|
|
5
|
+
import { DataTable } from "../../components/datatable/index.js";
|
|
6
|
+
import { FormField } from "../../components/formfield/index.js";
|
|
7
|
+
import { Input } from "../../components/input/index.js";
|
|
8
|
+
import { Menu, MenuItem } from "../../components/menu/index.js";
|
|
9
|
+
import { Select } from "../../components/select/index.js";
|
|
10
|
+
import { Switch } from "../../components/switch/index.js";
|
|
11
|
+
import { Tabs } from "../../components/tabs/index.js";
|
|
12
|
+
import { Textarea } from "../../components/textarea/index.js";
|
|
13
|
+
import { useCrispMessage } from "../../core/index.js";
|
|
14
|
+
import { FormDialog } from "../formdialog/index.js";
|
|
15
|
+
import { SettingsSection } from "../settingssection/index.js";
|
|
16
|
+
import { objectsettingsRecipe } from "./objectsettings.recipe.js";
|
|
17
|
+
/**
|
|
18
|
+
* Settings → Objects (crisp#637, P-122): H1 + subtitle, a search+Filter row,
|
|
19
|
+
* and a DataTable of every object (icon+name, type, record count,
|
|
20
|
+
* attribute count, a per-row "…" menu). Measured live against
|
|
21
|
+
* app.attio.com/<workspace>/settings/data/objects — the plan-gate banner
|
|
22
|
+
* above the table and the "5/3" object-count pill in the "Object" header
|
|
23
|
+
* (both billing-tier chrome) are skipped by decision, not oversight.
|
|
24
|
+
*/
|
|
25
|
+
export function ObjectsList({ objects, onOpenObject, onEditObject, onCopyObjectId, onCreateObject, title: titleProp, subtitle: subtitleProp, newObjectLabel: newObjectLabelProp, searchPlaceholder: searchPlaceholderProp, filterLabel: filterLabelProp, objectColumnLabel: objectColumnLabelProp, typeColumnLabel: typeColumnLabelProp, recordsColumnLabel: recordsColumnLabelProp, attributesColumnLabel: attributesColumnLabelProp, noRecordsLabel: noRecordsLabelProp, editLabel: editLabelProp, copyIdLabel: copyIdLabelProp, rowMenuLabel, emptyLabel, className, ...rest }) {
|
|
26
|
+
const title = useCrispMessage(titleProp, "Objects");
|
|
27
|
+
const subtitle = useCrispMessage(subtitleProp, "Modify and add objects in your workspace");
|
|
28
|
+
const newObjectLabel = useCrispMessage(newObjectLabelProp, "New custom object");
|
|
29
|
+
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search objects");
|
|
30
|
+
const filterLabel = useCrispMessage(filterLabelProp, "Filter");
|
|
31
|
+
const objectColumnLabel = useCrispMessage(objectColumnLabelProp, "Object");
|
|
32
|
+
const typeColumnLabel = useCrispMessage(typeColumnLabelProp, "Type");
|
|
33
|
+
const recordsColumnLabel = useCrispMessage(recordsColumnLabelProp, "Records");
|
|
34
|
+
const attributesColumnLabel = useCrispMessage(attributesColumnLabelProp, "Attributes");
|
|
35
|
+
const noRecordsLabel = useCrispMessage(noRecordsLabelProp, "No records");
|
|
36
|
+
const editLabel = useCrispMessage(editLabelProp, "Edit");
|
|
37
|
+
const copyIdLabel = useCrispMessage(copyIdLabelProp, "Copy ID");
|
|
38
|
+
const [query, setQuery] = React.useState("");
|
|
39
|
+
const filtered = React.useMemo(() => {
|
|
40
|
+
if (!query.trim())
|
|
41
|
+
return objects;
|
|
42
|
+
const q = query.trim().toLowerCase();
|
|
43
|
+
return objects.filter((o) => String(o.name ?? "")
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.includes(q));
|
|
46
|
+
}, [objects, query]);
|
|
47
|
+
const columns = React.useMemo(() => [
|
|
48
|
+
{
|
|
49
|
+
key: "name",
|
|
50
|
+
header: objectColumnLabel,
|
|
51
|
+
render: (_v, row) => (_jsxs("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { className: "crisp-objectsettings-icon", style: { background: row.iconColor ?? "currentColor" }, children: row.icon }), row.name] })),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: "typeLabel",
|
|
55
|
+
header: typeColumnLabel,
|
|
56
|
+
render: (_v, row) => (_jsx("span", { className: "crisp-objectsettings-typebadge", children: row.typeLabel })),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "recordCount",
|
|
60
|
+
header: recordsColumnLabel,
|
|
61
|
+
align: "left",
|
|
62
|
+
render: (_v, row) => row.recordCount ? (row.recordCount) : (_jsx("span", { className: "crisp-objectsettings-muted", children: noRecordsLabel })),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "attributeCount",
|
|
66
|
+
header: attributesColumnLabel,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "actions",
|
|
70
|
+
header: "",
|
|
71
|
+
align: "right",
|
|
72
|
+
render: (_v, row) => (_jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", "aria-label": rowMenuLabel?.(row) ?? `${row.name}`, style: {
|
|
73
|
+
display: "inline-flex",
|
|
74
|
+
border: "none",
|
|
75
|
+
background: "none",
|
|
76
|
+
cursor: "pointer",
|
|
77
|
+
color: "inherit",
|
|
78
|
+
padding: 4,
|
|
79
|
+
}, children: _jsx(MoreHorizontal, { size: 16 }) }), children: [_jsx(MenuItem, { icon: _jsx(Pencil, { size: 14 }), onSelect: () => onEditObject?.(row.id), children: editLabel }), _jsx(MenuItem, { onSelect: () => onCopyObjectId?.(row.id), children: copyIdLabel })] })),
|
|
80
|
+
},
|
|
81
|
+
], [
|
|
82
|
+
objectColumnLabel,
|
|
83
|
+
typeColumnLabel,
|
|
84
|
+
recordsColumnLabel,
|
|
85
|
+
attributesColumnLabel,
|
|
86
|
+
noRecordsLabel,
|
|
87
|
+
editLabel,
|
|
88
|
+
copyIdLabel,
|
|
89
|
+
onEditObject,
|
|
90
|
+
onCopyObjectId,
|
|
91
|
+
rowMenuLabel,
|
|
92
|
+
]);
|
|
93
|
+
return (_jsx("div", { className: objectsettingsRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-objectsettings-page", children: [_jsxs("div", { className: "crisp-objectsettings-head", children: [_jsxs("div", { className: "crisp-objectsettings-heading", children: [_jsx("h1", { className: "crisp-objectsettings-title", children: title }), _jsx("p", { className: "crisp-objectsettings-subtitle", children: subtitle })] }), onCreateObject && (_jsx("button", { type: "button", className: "crisp-objectsettings-filterbtn", onClick: onCreateObject, children: newObjectLabel }))] }), _jsxs("div", { className: "crisp-objectsettings-toolbar", children: [_jsxs("span", { className: "crisp-objectsettings-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchPlaceholder })] }), _jsxs("button", { type: "button", className: "crisp-objectsettings-filterbtn", children: [_jsx(SlidersHorizontal, { size: 14 }), filterLabel] })] }), _jsx(DataTable, { columns: columns, data: filtered, onRowClick: onOpenObject ? (row) => onOpenObject(row.id) : undefined, empty: emptyLabel })] }) }));
|
|
94
|
+
}
|
|
95
|
+
ObjectsList.displayName = "ObjectsList";
|
|
96
|
+
/**
|
|
97
|
+
* Settings → Objects → an object (crisp#637, P-123, verified live against
|
|
98
|
+
* .../objects/companies/general): 48px icon tile + name + type badge, a
|
|
99
|
+
* description line, then a Configuration/Attributes tab bar (Attio's own
|
|
100
|
+
* tab bar also carries Permissions/Templates/Notifications/Imports — out of
|
|
101
|
+
* scope here, see #613/#638; only the two rows this task covers are built).
|
|
102
|
+
* Tab content is a render prop, the same shape crisp's own `Tabs` already
|
|
103
|
+
* takes, so a consumer composes `ObjectGeneralSection`/
|
|
104
|
+
* `ObjectAppearanceSection` and `AttributesTable` underneath.
|
|
105
|
+
*/
|
|
106
|
+
export function ObjectDetailPage({ icon, iconColor, name, typeLabel, description, tab, defaultTab = "configuration", onTabChange, attributeCount, configurationLabel: configurationLabelProp, attributesLabel: attributesLabelProp, children, }) {
|
|
107
|
+
const configurationLabel = useCrispMessage(configurationLabelProp, "Configuration");
|
|
108
|
+
const attributesLabel = useCrispMessage(attributesLabelProp, "Attributes");
|
|
109
|
+
const [internalTab, setInternalTab] = React.useState(defaultTab);
|
|
110
|
+
const active = tab ?? internalTab;
|
|
111
|
+
const setTab = (v) => {
|
|
112
|
+
const next = v;
|
|
113
|
+
if (tab === undefined)
|
|
114
|
+
setInternalTab(next);
|
|
115
|
+
onTabChange?.(next);
|
|
116
|
+
};
|
|
117
|
+
return (_jsx("div", { className: objectsettingsRecipe(), children: _jsxs("div", { className: "crisp-objectsettings-page", children: [_jsx("div", { className: "crisp-objectsettings-head", children: _jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 }, children: [_jsx("span", { className: "crisp-objectsettings-icon crisp-objectsettings-icon--lg", style: { background: iconColor ?? "currentColor" }, children: icon }), _jsxs("div", { className: "crisp-objectsettings-heading", children: [_jsxs("h1", { className: "crisp-objectsettings-title", children: [name, typeLabel != null && (_jsx(Badge, { tone: "neutral", size: "small", children: typeLabel }))] }), description != null && (_jsx("p", { className: "crisp-objectsettings-subtitle", children: description }))] })] }) }), _jsx(Tabs, { value: active, onValueChange: setTab, tabs: [
|
|
118
|
+
{ value: "configuration", label: configurationLabel },
|
|
119
|
+
{
|
|
120
|
+
value: "attributes",
|
|
121
|
+
label: attributesLabel,
|
|
122
|
+
badge: attributeCount,
|
|
123
|
+
},
|
|
124
|
+
], children: (v) => children(v) })] }) }));
|
|
125
|
+
}
|
|
126
|
+
ObjectDetailPage.displayName = "ObjectDetailPage";
|
|
127
|
+
/** The Configuration tab's "General" section (crisp#637, P-123): plural/
|
|
128
|
+
* singular noun inputs side by side, then a slug input Attio locks for
|
|
129
|
+
* system objects. Composes `SettingsSection` + `FormField`, no new chrome. */
|
|
130
|
+
export function ObjectGeneralSection({ pluralNoun, onPluralNounChange, singularNoun, onSingularNounChange, slug, slugEditable = false, onSlugChange, titleLabel: titleLabelProp, descriptionLabel: descriptionLabelProp, pluralLabel: pluralLabelProp, singularLabel: singularLabelProp, slugLabel: slugLabelProp, slugHelpLabel: slugHelpLabelProp, }) {
|
|
131
|
+
const titleLabel = useCrispMessage(titleLabelProp, "General");
|
|
132
|
+
const descriptionLabel = useCrispMessage(descriptionLabelProp, "Set words to describe a single and multiple objects of this type");
|
|
133
|
+
const pluralLabel = useCrispMessage(pluralLabelProp, "Plural noun");
|
|
134
|
+
const singularLabel = useCrispMessage(singularLabelProp, "Singular noun");
|
|
135
|
+
const slugLabel = useCrispMessage(slugLabelProp, "Slug");
|
|
136
|
+
const slugHelpLabel = useCrispMessage(slugHelpLabelProp, "You can't change the slug of a system object");
|
|
137
|
+
const pluralId = React.useId();
|
|
138
|
+
const singularId = React.useId();
|
|
139
|
+
const slugId = React.useId();
|
|
140
|
+
return (_jsxs(SettingsSection, { title: titleLabel, description: descriptionLabel, children: [_jsxs("div", { className: "crisp-objectsettings-fieldrow", children: [_jsx(FormField, { label: pluralLabel, htmlFor: pluralId, children: _jsx(Input, { id: pluralId, value: pluralNoun, onChange: (e) => onPluralNounChange?.(e.target.value) }) }), _jsx(FormField, { label: singularLabel, htmlFor: singularId, children: _jsx(Input, { id: singularId, value: singularNoun, onChange: (e) => onSingularNounChange?.(e.target.value) }) })] }), _jsx(FormField, { label: slugLabel, htmlFor: slugId, description: !slugEditable ? slugHelpLabel : undefined, children: _jsx(Input, { id: slugId, value: slug, disabled: !slugEditable, onChange: (e) => onSlugChange?.(e.target.value) }) })] }));
|
|
141
|
+
}
|
|
142
|
+
ObjectGeneralSection.displayName = "ObjectGeneralSection";
|
|
143
|
+
/** The Configuration tab's "Appearance" section (crisp#637, P-123): a single
|
|
144
|
+
* "Record page layout" row with a "Configure record page" button — the
|
|
145
|
+
* target flow itself (crisp's own record-page-layout editor) is a separate,
|
|
146
|
+
* unbuilt surface; this only renders the row and its click. */
|
|
147
|
+
export function ObjectAppearanceSection({ onConfigureRecordPage, titleLabel: titleLabelProp, rowLabel: rowLabelProp, rowDescriptionLabel: rowDescriptionLabelProp, actionLabel: actionLabelProp, }) {
|
|
148
|
+
const titleLabel = useCrispMessage(titleLabelProp, "Appearance");
|
|
149
|
+
const rowLabel = useCrispMessage(rowLabelProp, "Record page layout");
|
|
150
|
+
const rowDescriptionLabel = useCrispMessage(rowDescriptionLabelProp, "Configure tabs, widgets, and layout for this object's record pages");
|
|
151
|
+
const actionLabel = useCrispMessage(actionLabelProp, "Configure record page");
|
|
152
|
+
return (_jsx(SettingsSection, { title: titleLabel, children: _jsxs("div", { className: "crisp-objectsettings-fieldrow", style: { alignItems: "center" }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 500 }, children: rowLabel }), _jsx("p", { className: "crisp-objectsettings-subtitle", children: rowDescriptionLabel })] }), onConfigureRecordPage && (_jsx("button", { type: "button", className: "crisp-objectsettings-filterbtn", style: { justifySelf: "end" }, onClick: onConfigureRecordPage, children: actionLabel }))] }) }));
|
|
153
|
+
}
|
|
154
|
+
ObjectAppearanceSection.displayName = "ObjectAppearanceSection";
|
|
155
|
+
/**
|
|
156
|
+
* The Attributes tab's table (crisp#637, P-124, verified live): H2 +
|
|
157
|
+
* "Create attribute", a search+Filter row, then a DataTable of every
|
|
158
|
+
* attribute — drag handle (via `DataTable`'s own `onRowsReorder`) + type
|
|
159
|
+
* icon + name, Data type, a constraint label, and System (grey)/Enriched
|
|
160
|
+
* (purple) property badges. Row height is DataTable's own 36px; Attio's
|
|
161
|
+
* measures 40px here — a real, small divergence left to DataTable's owner
|
|
162
|
+
* rather than patched locally (see the PR).
|
|
163
|
+
*/
|
|
164
|
+
export function AttributesTable({ attributes, onOpenAttribute, onEditAttribute, onArchiveAttribute, onReorder, onCreateAttribute, title: titleProp, subtitle: subtitleProp, createLabel: createLabelProp, searchPlaceholder: searchPlaceholderProp, filterLabel: filterLabelProp, nameColumnLabel: nameColumnLabelProp, dataTypeColumnLabel: dataTypeColumnLabelProp, constraintsColumnLabel: constraintsColumnLabelProp, propertiesColumnLabel: propertiesColumnLabelProp, systemLabel: systemLabelProp, enrichedLabel: enrichedLabelProp, editLabel: editLabelProp, archiveLabel: archiveLabelProp, rowActions, emptyLabel, }) {
|
|
165
|
+
const title = useCrispMessage(titleProp, "Attributes");
|
|
166
|
+
const subtitle = useCrispMessage(subtitleProp, "Modify and add object attributes");
|
|
167
|
+
const createLabel = useCrispMessage(createLabelProp, "Create attribute");
|
|
168
|
+
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search attributes");
|
|
169
|
+
const filterLabel = useCrispMessage(filterLabelProp, "Filter");
|
|
170
|
+
const nameColumnLabel = useCrispMessage(nameColumnLabelProp, "Name");
|
|
171
|
+
const dataTypeColumnLabel = useCrispMessage(dataTypeColumnLabelProp, "Data type");
|
|
172
|
+
const constraintsColumnLabel = useCrispMessage(constraintsColumnLabelProp, "Constraints");
|
|
173
|
+
const propertiesColumnLabel = useCrispMessage(propertiesColumnLabelProp, "Properties");
|
|
174
|
+
const systemLabel = useCrispMessage(systemLabelProp, "System");
|
|
175
|
+
const enrichedLabel = useCrispMessage(enrichedLabelProp, "Enriched");
|
|
176
|
+
const editLabel = useCrispMessage(editLabelProp, "Edit");
|
|
177
|
+
const archiveLabel = useCrispMessage(archiveLabelProp, "Archive");
|
|
178
|
+
const [query, setQuery] = React.useState("");
|
|
179
|
+
const filtered = React.useMemo(() => {
|
|
180
|
+
if (!query.trim())
|
|
181
|
+
return attributes;
|
|
182
|
+
const q = query.trim().toLowerCase();
|
|
183
|
+
return attributes.filter((a) => String(a.name ?? "")
|
|
184
|
+
.toLowerCase()
|
|
185
|
+
.includes(q));
|
|
186
|
+
}, [attributes, query]);
|
|
187
|
+
const columns = React.useMemo(() => [
|
|
188
|
+
{
|
|
189
|
+
key: "name",
|
|
190
|
+
header: nameColumnLabel,
|
|
191
|
+
render: (_v, row) => (_jsxs("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { style: {
|
|
192
|
+
display: "inline-flex",
|
|
193
|
+
color: "var(--crisp-fg-muted, inherit)",
|
|
194
|
+
}, children: row.icon }), row.name] })),
|
|
195
|
+
},
|
|
196
|
+
{ key: "dataType", header: dataTypeColumnLabel },
|
|
197
|
+
{
|
|
198
|
+
key: "constraint",
|
|
199
|
+
header: constraintsColumnLabel,
|
|
200
|
+
render: (_v, row) => row.constraint ?? null,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
key: "properties",
|
|
204
|
+
header: propertiesColumnLabel,
|
|
205
|
+
render: (_v, row) => (_jsxs("span", { style: { display: "flex", gap: 6 }, children: [row.system && (_jsx(Badge, { tone: "neutral", size: "small", children: systemLabel })), row.enriched && (_jsx(Badge, { tone: "purple", size: "small", children: enrichedLabel }))] })),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
key: "actions",
|
|
209
|
+
header: "",
|
|
210
|
+
align: "right",
|
|
211
|
+
render: (_v, row) => (_jsx(Menu, { align: "end", trigger: _jsx("button", { type: "button", "aria-label": `${row.name}`, style: {
|
|
212
|
+
display: "inline-flex",
|
|
213
|
+
border: "none",
|
|
214
|
+
background: "none",
|
|
215
|
+
cursor: "pointer",
|
|
216
|
+
color: "inherit",
|
|
217
|
+
padding: 4,
|
|
218
|
+
}, children: _jsx(MoreHorizontal, { size: 16 }) }), children: rowActions ? (rowActions(row)) : (_jsxs(_Fragment, { children: [_jsx(MenuItem, { onSelect: () => onEditAttribute?.(row.id), children: editLabel }), _jsx(MenuItem, { danger: true, onSelect: () => onArchiveAttribute?.(row.id), children: archiveLabel })] })) })),
|
|
219
|
+
},
|
|
220
|
+
], [
|
|
221
|
+
nameColumnLabel,
|
|
222
|
+
dataTypeColumnLabel,
|
|
223
|
+
constraintsColumnLabel,
|
|
224
|
+
propertiesColumnLabel,
|
|
225
|
+
systemLabel,
|
|
226
|
+
enrichedLabel,
|
|
227
|
+
editLabel,
|
|
228
|
+
archiveLabel,
|
|
229
|
+
onEditAttribute,
|
|
230
|
+
onArchiveAttribute,
|
|
231
|
+
rowActions,
|
|
232
|
+
]);
|
|
233
|
+
return (_jsx("div", { className: objectsettingsRecipe(), children: _jsxs("div", { className: "crisp-objectsettings-page", children: [_jsxs("div", { className: "crisp-objectsettings-head", children: [_jsxs("div", { className: "crisp-objectsettings-heading", children: [_jsx("h2", { className: "crisp-objectsettings-title", style: { fontSize: 16, lineHeight: "20px" }, children: title }), _jsx("p", { className: "crisp-objectsettings-subtitle", children: subtitle })] }), onCreateAttribute && (_jsx("button", { type: "button", className: "crisp-objectsettings-filterbtn", onClick: onCreateAttribute, children: createLabel }))] }), _jsxs("div", { className: "crisp-objectsettings-toolbar", children: [_jsxs("span", { className: "crisp-objectsettings-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchPlaceholder })] }), _jsxs("button", { type: "button", className: "crisp-objectsettings-filterbtn", children: [_jsx(SlidersHorizontal, { size: 14 }), filterLabel] })] }), _jsx(DataTable, { columns: columns, data: filtered, onRowClick: onOpenAttribute ? (row) => onOpenAttribute(row.id) : undefined, onRowsReorder: onReorder, empty: emptyLabel })] }) }));
|
|
234
|
+
}
|
|
235
|
+
AttributesTable.displayName = "AttributesTable";
|
|
236
|
+
/** Attio's own "Type" group, verbatim (crisp#637, P-126) — icons are
|
|
237
|
+
* crisp's Lucide choices, not Attio's proprietary set (golden rule 4). */
|
|
238
|
+
export const ATTRIBUTE_TYPE_OPTIONS = [
|
|
239
|
+
{ value: "checkbox", label: "Checkbox", icon: CheckSquare },
|
|
240
|
+
{ value: "currency", label: "Currency", icon: DollarSign },
|
|
241
|
+
{ value: "date", label: "Date", icon: Calendar },
|
|
242
|
+
{ value: "location", label: "Location", icon: MapPin },
|
|
243
|
+
{ value: "multiselect", label: "Multi-select", icon: ListChecks },
|
|
244
|
+
{ value: "number", label: "Number", icon: Hash },
|
|
245
|
+
{ value: "phone", label: "Phone Number", icon: Phone },
|
|
246
|
+
{ value: "rating", label: "Rating", icon: Star },
|
|
247
|
+
{ value: "record", label: "Record", icon: FileText },
|
|
248
|
+
{ value: "relationship", label: "Relationship", icon: Link2, badge: "Pro" },
|
|
249
|
+
{ value: "select", label: "Select", icon: ListChecks },
|
|
250
|
+
{ value: "status", label: "Status", icon: Flag },
|
|
251
|
+
{ value: "text", label: "Text", icon: TypeIcon },
|
|
252
|
+
{ value: "timestamp", label: "Timestamp", icon: Clock },
|
|
253
|
+
{ value: "user", label: "User", icon: UserIcon },
|
|
254
|
+
];
|
|
255
|
+
/** Attio's own "AI Autofill" group, verbatim (crisp#637, P-126) — opt in via
|
|
256
|
+
* `AttributeTypeSelect`'s `aiAutofillOptions`; omitted by default, since a
|
|
257
|
+
* consumer with no autofill backend of its own (GRC) has nothing to wire
|
|
258
|
+
* these to. */
|
|
259
|
+
export const ATTRIBUTE_AI_AUTOFILL_OPTIONS = [
|
|
260
|
+
{ value: "classify-record", label: "Classify record", icon: Sparkles },
|
|
261
|
+
{ value: "prompt-completion", label: "Prompt completion", icon: Sparkles },
|
|
262
|
+
{ value: "summarize-record", label: "Summarize record", icon: Sparkles },
|
|
263
|
+
{ value: "web-agent", label: "Web agent", icon: Sparkles },
|
|
264
|
+
];
|
|
265
|
+
function toSelectOption(o, group) {
|
|
266
|
+
const Icon = o.icon;
|
|
267
|
+
return {
|
|
268
|
+
value: o.value,
|
|
269
|
+
keywords: o.label,
|
|
270
|
+
group,
|
|
271
|
+
label: (_jsxs("span", { className: "crisp-objectsettings-typeoption", children: [_jsx(Icon, { size: 14 }), _jsx("span", { className: "crisp-objectsettings-typeoption-label", children: o.label }), o.badge != null && (_jsx("span", { className: "crisp-objectsettings-typeoption-badge", children: o.badge }))] })),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** The "Attribute Type" dropdown (crisp#637, P-126, verified live): a
|
|
275
|
+
* searchable, grouped `Select` — reused as-is, not a new picker primitive.
|
|
276
|
+
* Grouping and the per-item trailing "Pro" badge are composed through
|
|
277
|
+
* `Select`'s existing `group` field and rich `label` ReactNode. */
|
|
278
|
+
export function AttributeTypeSelect({ value, onValueChange, options = ATTRIBUTE_TYPE_OPTIONS, aiAutofillOptions, aiAutofillGroupLabel: aiAutofillGroupLabelProp, typeGroupLabel: typeGroupLabelProp, searchPlaceholder: searchPlaceholderProp, id, }) {
|
|
279
|
+
const aiAutofillGroupLabel = useCrispMessage(aiAutofillGroupLabelProp, "AI Autofill");
|
|
280
|
+
const typeGroupLabel = useCrispMessage(typeGroupLabelProp, "Type");
|
|
281
|
+
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search attribute types...");
|
|
282
|
+
const selectOptions = React.useMemo(() => {
|
|
283
|
+
const ai = (aiAutofillOptions ?? []).map((o) => toSelectOption(o, aiAutofillGroupLabel));
|
|
284
|
+
const type = options.map((o) => toSelectOption(o, typeGroupLabel));
|
|
285
|
+
return [...ai, ...type];
|
|
286
|
+
}, [aiAutofillOptions, options, aiAutofillGroupLabel, typeGroupLabel]);
|
|
287
|
+
return (_jsx(Select, { id: id, options: selectOptions, value: value, onValueChange: onValueChange, searchable: true, searchPlaceholder: searchPlaceholder, width: "full" }));
|
|
288
|
+
}
|
|
289
|
+
AttributeTypeSelect.displayName = "AttributeTypeSelect";
|
|
290
|
+
/**
|
|
291
|
+
* "Create attribute" (crisp#637, P-125, verified live): Attribute Type
|
|
292
|
+
* select (defaults to Text) + Name (autofocused) + Description (optional)
|
|
293
|
+
* + an AI-autofill switch row, footer Cancel/Create. Built on `FormDialog`
|
|
294
|
+
* in its plain (non-`createMode`) shape — Attio's own footer here has a
|
|
295
|
+
* real Cancel button, unlike the record-create dialog `createMode` models
|
|
296
|
+
* (crisp#548), so `createMode` would be the wrong fit even though the
|
|
297
|
+
* button carries the same ⌘↵ hint; that hint is rendered locally instead.
|
|
298
|
+
*/
|
|
299
|
+
export function CreateAttributeDialog({ type, onTypeChange, typeOptions, aiAutofillTypeOptions, name, onNameChange, description, onDescriptionChange, aiAutofillEnabled = false, onAiAutofillChange, onSubmit, onClose, busy, error, title: titleProp, attributeTypeLabel: attributeTypeLabelProp, nameLabel: nameLabelProp, descriptionLabel: descriptionLabelProp, descriptionPlaceholder: descriptionPlaceholderProp, aiAutofillLabel: aiAutofillLabelProp, setUpAiAutofillLabel: setUpAiAutofillLabelProp, createLabel: createLabelProp, }) {
|
|
300
|
+
const title = useCrispMessage(titleProp, "Create attribute");
|
|
301
|
+
const attributeTypeLabel = useCrispMessage(attributeTypeLabelProp, "Attribute Type");
|
|
302
|
+
const nameLabel = useCrispMessage(nameLabelProp, "Name");
|
|
303
|
+
const descriptionLabel = useCrispMessage(descriptionLabelProp, "Description");
|
|
304
|
+
const descriptionPlaceholder = useCrispMessage(descriptionPlaceholderProp, "Add a description for this attribute");
|
|
305
|
+
const aiAutofillLabel = useCrispMessage(aiAutofillLabelProp, "AI autofill");
|
|
306
|
+
const setUpAiAutofillLabel = useCrispMessage(setUpAiAutofillLabelProp, "Set up AI autofill");
|
|
307
|
+
const createLabel = useCrispMessage(createLabelProp, "Create attribute");
|
|
308
|
+
const switchId = React.useId();
|
|
309
|
+
const nameId = React.useId();
|
|
310
|
+
return (_jsxs(FormDialog, { title: title, submitLabel: _jsxs(_Fragment, { children: [createLabel, _jsxs("span", { className: "crisp-objectsettings-kbd", "aria-hidden": "true", children: [_jsx("kbd", { children: "\u2318" }), _jsx("kbd", { children: "\u21B5" })] })] }), onSubmit: onSubmit, onClose: onClose, busy: busy, error: error, size: 520, className: objectsettingsRecipe(), children: [_jsx(FormField, { label: attributeTypeLabel, children: _jsx(AttributeTypeSelect, { value: type, onValueChange: onTypeChange, options: typeOptions, aiAutofillOptions: aiAutofillTypeOptions }) }), _jsx(FormField, { label: nameLabel, htmlFor: nameId, children: _jsx(Input, { id: nameId, autoFocus: true, value: name, onChange: (e) => onNameChange(e.target.value) }) }), _jsx(FormField, { label: descriptionLabel, optional: true, children: _jsx(Textarea, { placeholder: descriptionPlaceholder, value: description, onChange: (e) => onDescriptionChange(e.target.value) }) }), _jsxs("div", { className: "crisp-objectsettings-aiautofill", children: [_jsx("span", { className: "crisp-objectsettings-aiautofill-label", children: aiAutofillLabel }), _jsxs("label", { className: "crisp-objectsettings-aiautofill-row", htmlFor: switchId, children: [_jsx(Sparkles, { size: 14 }), _jsx("span", { children: setUpAiAutofillLabel }), _jsx(Switch, { id: switchId, checked: aiAutofillEnabled, onCheckedChange: onAiAutofillChange })] })] })] }));
|
|
311
|
+
}
|
|
312
|
+
CreateAttributeDialog.displayName = "CreateAttributeDialog";
|
|
313
|
+
/**
|
|
314
|
+
* This folder holds several components (`ObjectsList`, `ObjectDetailPage`,
|
|
315
|
+
* `AttributesTable`, `CreateAttributeDialog`, …), not one named after the
|
|
316
|
+
* slug — unlike every other product dir, whose docs `<ManualInstallation>`
|
|
317
|
+
* resolves an import binding from the folder slug ("avatargroup" →
|
|
318
|
+
* `AvatarGroup`) by reading this file's exports. This alias exists only so
|
|
319
|
+
* that resolver finds a match for "objectsettings"; consumers should import
|
|
320
|
+
* the real names above, never this one.
|
|
321
|
+
*/
|
|
322
|
+
export const ObjectSettings = ObjectsList;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// Measured live against app.attio.com/<workspace>/settings/data/objects and
|
|
4
|
+
// .../objects/companies/{general,attributes} (crisp#637, P-122/P-123/P-124/
|
|
5
|
+
// P-125/P-126). Values with no matching scale token are one-off literals,
|
|
6
|
+
// same precedent as authshell's own 20px title (crisp#637 golden rule 3):
|
|
7
|
+
// the 24/600/28/-0.48 page title, the 12px/16px badge dot sizes, and the
|
|
8
|
+
// 30% squircle radius on the object-icon tile.
|
|
9
|
+
const config = defineRecipe({
|
|
10
|
+
name: "objectsettings",
|
|
11
|
+
base: `& .crisp-objectsettings-page { display: flex; flex-direction: column; gap: ${c.space["6"]}; }
|
|
12
|
+
& .crisp-objectsettings-head { display: flex; align-items: flex-start; justify-content: space-between; gap: ${c.space["4"]}; }
|
|
13
|
+
& .crisp-objectsettings-heading { display: flex; flex-direction: column; gap: ${c.space["0.5"]}; min-width: 0; }
|
|
14
|
+
& .crisp-objectsettings-title { display: flex; align-items: center; gap: ${c.space["2"]}; font-size: 24px; line-height: 28px; font-weight: ${c.weight.semibold}; letter-spacing: -0.48px; color: ${c.fgPrimary}; margin: 0; }
|
|
15
|
+
& .crisp-objectsettings-subtitle { font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; margin: 0; }
|
|
16
|
+
|
|
17
|
+
& .crisp-objectsettings-toolbar { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
18
|
+
& .crisp-objectsettings-search { display: flex; align-items: center; flex: 1; gap: ${c.space["1.5"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2.5"]}; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderDefault}; color: ${c.fgMuted}; transition: box-shadow .15s; }
|
|
19
|
+
& .crisp-objectsettings-search:focus-within { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
|
|
20
|
+
& .crisp-objectsettings-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
|
|
21
|
+
& .crisp-objectsettings-search input::placeholder { color: ${c.fgMuted}; }
|
|
22
|
+
& .crisp-objectsettings-filterbtn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2.5"]} 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.lg}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderDefault}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; cursor: pointer; white-space: nowrap; }
|
|
23
|
+
& .crisp-objectsettings-filterbtn:hover { background: ${c.bgRaised}; }
|
|
24
|
+
|
|
25
|
+
/* Object / attribute icon tile — Attio's own squircle (measured
|
|
26
|
+
border-radius: 30%, not a scale token: it's a proportional radius
|
|
27
|
+
tied to the tile's own size, unlike every other radius crisp uses,
|
|
28
|
+
which is a fixed px). */
|
|
29
|
+
& .crisp-objectsettings-icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 16px; height: 16px; border-radius: 30%; color: ${c.fgOnBrand}; }
|
|
30
|
+
& .crisp-objectsettings-icon--lg { width: 48px; height: 48px; border-radius: 16px; }
|
|
31
|
+
|
|
32
|
+
/* "Standard"/"Custom" type badge and the muted "No records" cell —
|
|
33
|
+
plain text, not a Badge pill (measured: transparent background). */
|
|
34
|
+
& .crisp-objectsettings-typebadge { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }
|
|
35
|
+
& .crisp-objectsettings-muted { color: ${c.fgMuted}; }
|
|
36
|
+
|
|
37
|
+
/* Attribute-type option row (icon + label + optional trailing badge),
|
|
38
|
+
composed as a Select option's \`label\` ReactNode. */
|
|
39
|
+
& .crisp-objectsettings-typeoption { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; min-width: 0; }
|
|
40
|
+
& .crisp-objectsettings-typeoption-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
41
|
+
& .crisp-objectsettings-typeoption-badge { flex-shrink: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgBrandSoft}; }
|
|
42
|
+
|
|
43
|
+
/* Create-attribute dialog's AI-autofill row and the submit button's
|
|
44
|
+
⌘↵ hint (measured off the same dialog, same shape as Dialog's own
|
|
45
|
+
.crisp-dialog-submit-key, kept local rather than reaching into
|
|
46
|
+
another component's scoped classes). */
|
|
47
|
+
& .crisp-objectsettings-aiautofill { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; }
|
|
48
|
+
& .crisp-objectsettings-aiautofill-label { font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
49
|
+
& .crisp-objectsettings-aiautofill-row { display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2.5"]}; border-radius: ${c.radius.md}; box-shadow: inset 0 0 0 1px ${c.borderDefault}; font-size: ${c.text.lg}; color: ${c.fgPrimary}; cursor: pointer; }
|
|
50
|
+
& .crisp-objectsettings-aiautofill-row > svg { flex-shrink: 0; color: ${c.fgMuted}; }
|
|
51
|
+
& .crisp-objectsettings-aiautofill-row > span { flex: 1; }
|
|
52
|
+
& .crisp-objectsettings-kbd { display: inline-flex; align-items: center; gap: ${c.space["1"]}; margin-inline-start: ${c.space["1"]}; }
|
|
53
|
+
& .crisp-objectsettings-kbd kbd { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0 3px; border-radius: 4px; background: transparent; color: inherit; font-family: inherit; font-style: normal; font-size: 10px; }
|
|
54
|
+
|
|
55
|
+
& .crisp-objectsettings-fieldrow { display: grid; grid-template-columns: 1fr 1fr; gap: ${c.space["4"]}; }
|
|
56
|
+
`,
|
|
57
|
+
variants: {},
|
|
58
|
+
defaultVariants: {},
|
|
59
|
+
compoundVariants: [],
|
|
60
|
+
});
|
|
61
|
+
export const objectsettingsRecipe = resolveRecipe(config);
|
|
62
|
+
export default config;
|
|
@@ -6,6 +6,14 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
|
6
6
|
// column (transparent, border-right) and a flexible main area whose tab bar
|
|
7
7
|
// (14/500 labels over a sliding ink underline) switches content — Overview shows
|
|
8
8
|
// a bordered Highlights stat-grid and an Activity feed.
|
|
9
|
+
//
|
|
10
|
+
// `.crisp-recordpage-panel`'s own `padding-top` (c.space["4"], 16px) was
|
|
11
|
+
// dropped (your-moon/crisp#588): this panel is only ever rendered as Tabs'
|
|
12
|
+
// own children (every branch of `builtinPanel` in recordpage.tsx, all fed to
|
|
13
|
+
// `<Tabs>` below), so it sat inside `.crisp-tabs-panel`, which now carries
|
|
14
|
+
// its own measured 32px top-inset by default — keeping both would have
|
|
15
|
+
// stacked to 48px. Caught by tracing every consumer of `Tabs` before
|
|
16
|
+
// shipping its new default, not by a screenshot.
|
|
9
17
|
const config = defineRecipe({
|
|
10
18
|
name: "recordpage",
|
|
11
19
|
base: `
|
|
@@ -125,7 +133,8 @@ const config = defineRecipe({
|
|
|
125
133
|
|
|
126
134
|
& .crisp-recordpage-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; padding: ${c.space["3"]} ${c.space["5"]}; }
|
|
127
135
|
|
|
128
|
-
|
|
136
|
+
/* no padding-top: Tabs' own panel supplies it now (#588) */
|
|
137
|
+
& .crisp-recordpage-panel { display: flex; flex-direction: column; gap: ${c.space["4"]}; }
|
|
129
138
|
& .crisp-recordpage-card { border: 1px solid ${c.borderSubtle}; border-radius: ${c.radius["2xl"]}; background: ${c.bgSurface}; overflow: hidden; }
|
|
130
139
|
& .crisp-recordpage-card-title { font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; letter-spacing: -0.14px; color: ${c.fgPrimary}; padding: ${c.space["3"]} ${c.space["3"]} ${c.space["2"]}; }
|
|
131
140
|
& .crisp-recordpage-hl-card { border: 1px solid ${c.borderSubtle}; border-radius: ${c.radius["2xl"]}; overflow: hidden; }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowDown, ArrowDownWideNarrow, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpNarrowWide, ArrowUpRight, AtSign, Briefcase, Building2, Calendar, Check, CircleHelp, CircleUserRound, Copy, CopyPlus, CornerDownLeft, Download, EyeOff, Globe, GripVertical, Hash, Inbox, ListFilter, ListPlus, Mail, MapPin, MoreHorizontal, Pencil, Plus, Star, Tag, Trash2, TrendingUp, Users, Workflow, X, } from "lucide-react";
|
|
2
|
+
import { ArrowDown, ArrowDownWideNarrow, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpNarrowWide, ArrowUpRight, AtSign, Briefcase, Building2, Calendar, Check, CircleHelp, CircleUserRound, Copy, CopyPlus, CornerDownLeft, Download, EyeOff, Globe, GripVertical, Hash, Inbox, ListFilter, ListPlus, Mail, MapPin, MoreHorizontal, Pencil, Plug, Plus, Star, Tag, Trash2, TrendingUp, Upload, Users, Workflow, X, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { Avatar } from "../../components/avatar/index.js";
|
|
@@ -8,15 +8,67 @@ import { Button } from "../../components/button/index.js";
|
|
|
8
8
|
import { Checkbox } from "../../components/checkbox/index.js";
|
|
9
9
|
import { DatePickerField } from "../../components/datepickerfield/index.js";
|
|
10
10
|
import { Dialog, DialogBody, DialogFooter } from "../../components/dialog/index.js";
|
|
11
|
-
import { EmptyState } from "../../components/emptystate/index.js";
|
|
11
|
+
import { EmptyState, } from "../../components/emptystate/index.js";
|
|
12
12
|
import { Input } from "../../components/input/index.js";
|
|
13
13
|
import { Menu, MenuItem, useMenuKeyboard } from "../../components/menu/index.js";
|
|
14
14
|
import { MultiSelect } from "../../components/multiselect/index.js";
|
|
15
15
|
import { Select } from "../../components/select/index.js";
|
|
16
16
|
import { Switch } from "../../components/switch/index.js";
|
|
17
|
+
import { Tooltip } from "../../components/tooltip/index.js";
|
|
17
18
|
import { isInsideLaterFloatingLayer, isRtlElement, useCrispComponentDefaults, usePrefersReducedMotion, } from "../../core/index.js";
|
|
18
19
|
import { companyLogos } from "./company-logos.js";
|
|
19
20
|
import { recordstableRecipe } from "./recordstable.recipe.js";
|
|
21
|
+
// #534: whether an element's own content is being cut off by its own box --
|
|
22
|
+
// the same fact `.crisp-recordstable-th-label`'s CSS ellipsis is silently
|
|
23
|
+
// applying, exported so the comparison itself (not layout) can be
|
|
24
|
+
// mutation-tested without a real browser. Identical to DataTable's own
|
|
25
|
+
// `isLabelTruncated` (components/datatable/datatable.tsx) -- not shared
|
|
26
|
+
// because `product/` can depend on `components/` but not the other way, and
|
|
27
|
+
// this file already duplicates DataTable's other measured header behaviour
|
|
28
|
+
// rather than reach across that boundary.
|
|
29
|
+
export function isLabelTruncated(el) {
|
|
30
|
+
return el.scrollWidth > el.clientWidth;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The header label span: plain when it fits, wrapped in the shared `Tooltip`
|
|
34
|
+
* (full text, `asChild` so the wrapper never displaces the ellipsis CSS the
|
|
35
|
+
* span already carries -- see `Tooltip`'s own `asChild` doc) once it's
|
|
36
|
+
* actually cut off. Re-measures on mount and on resize (column drag,
|
|
37
|
+
* viewport change, label change) via `ResizeObserver`; SSR-safe the same way
|
|
38
|
+
* `MiddleTruncate` is -- the DOM is only touched inside an effect, so the
|
|
39
|
+
* server (and the first client paint) renders the plain span.
|
|
40
|
+
*/
|
|
41
|
+
function ThLabel({ label }) {
|
|
42
|
+
const ref = React.useRef(null);
|
|
43
|
+
const [overflowing, setOverflowing] = React.useState(false);
|
|
44
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `label` isn't read in the body, but a rename (col.label changing with the column's own width unchanged) needs a re-measure a ResizeObserver on `el` won't fire for -- same reasoning as Tabs' own re-measure effect.
|
|
45
|
+
React.useLayoutEffect(() => {
|
|
46
|
+
const el = ref.current;
|
|
47
|
+
if (!el)
|
|
48
|
+
return;
|
|
49
|
+
const measure = () => setOverflowing(isLabelTruncated(el));
|
|
50
|
+
measure();
|
|
51
|
+
if (typeof ResizeObserver === "undefined")
|
|
52
|
+
return;
|
|
53
|
+
const ro = new ResizeObserver(measure);
|
|
54
|
+
ro.observe(el);
|
|
55
|
+
return () => ro.disconnect();
|
|
56
|
+
}, [label]);
|
|
57
|
+
// `Tooltip` always wraps the SAME span (never swapped in/out of the tree
|
|
58
|
+
// based on `overflowing`) so the span's own DOM node identity survives
|
|
59
|
+
// every toggle -- a real-browser-only bug (happy-dom has no
|
|
60
|
+
// ResizeObserver, so the test suite never exercised this path): swapping
|
|
61
|
+
// between a bare `<span>` and `<Tooltip asChild><span/></Tooltip>` changes
|
|
62
|
+
// the element type at this position, so React unmounts the old span and
|
|
63
|
+
// mounts a new one; the effect above never re-runs (its deps are
|
|
64
|
+
// `[label]`, unchanged), so its ResizeObserver keeps watching the now
|
|
65
|
+
// -detached old node, whose deferred initial callback then reports 0x0 and
|
|
66
|
+
// flips `overflowing` straight back to false -- the tooltip would never
|
|
67
|
+
// actually show. `open={false}` when it fits keeps the Tooltip mounted
|
|
68
|
+
// (asChild costs no extra DOM either way) but pins it permanently closed,
|
|
69
|
+
// so hovering a non-overflowing header still opens nothing.
|
|
70
|
+
return (_jsx(Tooltip, { content: label, asChild: true, open: overflowing ? undefined : false, children: _jsx("span", { ref: ref, className: "crisp-recordstable-th-label", children: label }) }));
|
|
71
|
+
}
|
|
20
72
|
const EDITABLE = new Set(["text", "number", "url", "currency"]);
|
|
21
73
|
// types whose cell is an always-interactive picker (not double-click-to-edit)
|
|
22
74
|
const PICKER = new Set(["status", "reference", "date", "tags"]);
|
|
@@ -100,6 +152,8 @@ export const recordsTableLabels = {
|
|
|
100
152
|
setRecordPlaceholder: "Set a record…",
|
|
101
153
|
noRecords: "No records yet",
|
|
102
154
|
untitled: "Untitled",
|
|
155
|
+
importCsv: "Import CSV",
|
|
156
|
+
connectDataSource: "Connect data source",
|
|
103
157
|
agg: {
|
|
104
158
|
none: "None",
|
|
105
159
|
count: "Count all",
|
|
@@ -111,6 +165,9 @@ export const recordsTableLabels = {
|
|
|
111
165
|
},
|
|
112
166
|
op: {
|
|
113
167
|
contains: "contains",
|
|
168
|
+
doesnotcontain: "does not contain",
|
|
169
|
+
startswith: "starts with",
|
|
170
|
+
endswith: "ends with",
|
|
114
171
|
is: "is",
|
|
115
172
|
isnot: "is not",
|
|
116
173
|
empty: "is empty",
|
|
@@ -137,8 +194,17 @@ const compact = new Intl.NumberFormat("en", {
|
|
|
137
194
|
const CHECK_W = 40;
|
|
138
195
|
const ADDCOL_W = 44;
|
|
139
196
|
const colWidth = (col) => col.width ?? (col.type === "entity" ? 240 : 180);
|
|
140
|
-
const OP_ORDER = [
|
|
141
|
-
|
|
197
|
+
const OP_ORDER = [
|
|
198
|
+
"contains",
|
|
199
|
+
"doesnotcontain",
|
|
200
|
+
"startswith",
|
|
201
|
+
"endswith",
|
|
202
|
+
"is",
|
|
203
|
+
"isnot",
|
|
204
|
+
"empty",
|
|
205
|
+
"notempty",
|
|
206
|
+
];
|
|
207
|
+
const opNeedsValue = (op) => op !== "empty" && op !== "notempty";
|
|
142
208
|
function matchesFilter(col, row, cond) {
|
|
143
209
|
if (cond.op === "empty")
|
|
144
210
|
return !isFilled(col, row);
|
|
@@ -152,6 +218,12 @@ function matchesFilter(col, row, cond) {
|
|
|
152
218
|
return true;
|
|
153
219
|
if (cond.op === "contains")
|
|
154
220
|
return t.includes(v);
|
|
221
|
+
if (cond.op === "doesnotcontain")
|
|
222
|
+
return !t.includes(v);
|
|
223
|
+
if (cond.op === "startswith")
|
|
224
|
+
return t.startsWith(v);
|
|
225
|
+
if (cond.op === "endswith")
|
|
226
|
+
return t.endsWith(v);
|
|
155
227
|
if (cond.op === "is")
|
|
156
228
|
return t === v;
|
|
157
229
|
return t !== v; // isnot
|
|
@@ -433,7 +505,7 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
|
|
|
433
505
|
* and a floating bulk-action bar (delete / deselect) when rows are selected.
|
|
434
506
|
* Selection can be controlled via `selectedIds`/`onSelectionChange`.
|
|
435
507
|
*/
|
|
436
|
-
export function RecordsTable({ labels, columns: columnsProp = defaultColumns, rows: rowsProp = defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", className, ...rest }) {
|
|
508
|
+
export function RecordsTable({ labels, columns: columnsProp = defaultColumns, rows: rowsProp = defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", emptyStateExtra, className, ...rest }) {
|
|
437
509
|
const [cols, setCols] = React.useState(columnsProp);
|
|
438
510
|
const [data, setData] = React.useState(rowsProp);
|
|
439
511
|
// The table owns `cols`/`data` so it can rename a column, edit a cell, or
|
|
@@ -1101,7 +1173,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
|
|
|
1101
1173
|
setDragCol(col.key);
|
|
1102
1174
|
e.dataTransfer.effectAllowed = "move";
|
|
1103
1175
|
}
|
|
1104
|
-
: undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx(
|
|
1176
|
+
: undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx(ThLabel, { label: col.label }), _jsx("span", { className: "crisp-recordstable-sort", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
|
|
1105
1177
|
rootRef.current &&
|
|
1106
1178
|
createPortal(_jsxs("div", { ref: menuRef, className: "crisp-recordstable-menu crisp-recordstable-menu--fixed crisp-recordstable-menu--hdr", role: "menu", "aria-label": `${col.label} column`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: [_jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [_jsx(ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [_jsx(ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [_jsx(ArrowLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveLeft] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [_jsx(ArrowRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveRight] }), _jsx("div", { className: "crisp-recordstable-menu-sep" }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
|
|
1107
1179
|
setRenamingCol(col.key);
|
|
@@ -1170,9 +1242,19 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
|
|
|
1170
1242
|
}, children: res ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-recordstable-sum-label", children: t.agg[a] }), _jsx("span", { className: "crisp-recordstable-sum-value", children: res.value })] })) : (_jsx("span", { className: "crisp-recordstable-sum-label", children: t.calculate })) }), sumOpen &&
|
|
1171
1243
|
rootRef.current &&
|
|
1172
1244
|
createPortal(_jsx("div", { ref: menuRef, className: "crisp-recordstable-menu crisp-recordstable-menu--fixed crisp-recordstable-menu--up", role: "menu", "aria-label": `${col.label} summary`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: aggOptions(col).map((opt) => (_jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitemradio", tabIndex: -1, "aria-checked": a === opt, "data-on": a === opt ? "" : undefined, onClick: () => setAggValue(col.key, opt), children: [t.agg[opt], a === opt ? (_jsx(Check, { className: "crisp-recordstable-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, opt))) }), rootRef.current)] }) }, col.key));
|
|
1173
|
-
}), _jsx("td", { className: "crisp-recordstable-sum crisp-recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? (_jsx("div", { className: "crisp-recordstable-empty", children: _jsx(EmptyState
|
|
1245
|
+
}), _jsx("td", { className: "crisp-recordstable-sum crisp-recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? (_jsx("div", { className: "crisp-recordstable-empty", children: _jsx(EmptyState
|
|
1246
|
+
// #603: the unfiltered zero-records state, when the consumer
|
|
1247
|
+
// opts into the extra CTAs, uses Attio's own larger measured
|
|
1248
|
+
// tile (Users/Workspaces, 138px) instead of the 45px
|
|
1249
|
+
// filtered-to-zero ring -- two different Attio surfaces.
|
|
1250
|
+
, {
|
|
1251
|
+
// #603: the unfiltered zero-records state, when the consumer
|
|
1252
|
+
// opts into the extra CTAs, uses Attio's own larger measured
|
|
1253
|
+
// tile (Users/Workspaces, 138px) instead of the 45px
|
|
1254
|
+
// filtered-to-zero ring -- two different Attio surfaces.
|
|
1255
|
+
variant: !activeFilters && emptyStateExtra ? "records" : "page", icon: activeFilters ? (_jsx(ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : (_jsx(Inbox, { size: !activeFilters && emptyStateExtra ? 40 : 18, strokeWidth: 1.25, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
|
|
1174
1256
|
? "None of your records match the filters that are set."
|
|
1175
|
-
: `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : (_jsxs("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), _jsx("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsxs(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), _jsx(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
|
|
1257
|
+
: `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(_Fragment, { children: [_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] }), emptyStateExtra?.onImportCsv && (_jsxs(Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onImportCsv, children: [_jsx(Upload, { size: 15, strokeWidth: 1.75 }), t.importCsv] })), emptyStateExtra?.onConnectDataSource && (_jsxs(Button, { intent: "neutral", size: "sm", onClick: emptyStateExtra.onConnectDataSource, children: [_jsx(Plug, { size: 15, strokeWidth: 1.75 }), t.connectDataSource] }))] })), learnMore: !activeFilters ? emptyStateExtra?.learnMore : undefined }) })) : (_jsxs("button", { type: "button", className: "crisp-recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), _jsx("div", { className: "crisp-recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsxs(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), _jsx(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
|
|
1176
1258
|
createPortal(_jsxs("div", { ref: ctxRef, className: "crisp-recordstable-menu crisp-recordstable-ctx", role: "menu", "aria-label": t.recordActions, style: { position: "fixed", top: ctxMenu.y, left: ctxMenu.x }, onKeyDown: onCtxKeyDown, children: [_jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => copyRow(ctxMenu.id), children: [_jsx(Copy, { size: 15, strokeWidth: 1.75 }), t.copy] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => duplicateRow(ctxMenu.id), children: [_jsx(CopyPlus, { size: 15, strokeWidth: 1.75 }), t.duplicate] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => pinRow(ctxMenu.id), children: [_jsx(Star, { size: 15, strokeWidth: 1.75 }), t.addToList] }), _jsxs("button", { type: "button", className: "crisp-recordstable-menu-item crisp-recordstable-menu-item--danger", role: "menuitem", tabIndex: -1, onClick: () => deleteRow(ctxMenu.id), children: [_jsx(Trash2, { size: 15, strokeWidth: 1.75 }), t.delete] })] }), rootRef.current ?? document.body)] }));
|
|
1177
1259
|
}
|
|
1178
1260
|
export const defaultColumns = [
|