Skip to content

Commit 91fc630

Browse files
committed
Add proposal implementation for discussion of bindings as first class in menu
1 parent 4e821ce commit 91fc630

23 files changed

+1751
-144
lines changed

apps/mesh/src/web/components/add-binding/add-binding-modal.tsx

Lines changed: 559 additions & 0 deletions
Large diffs are not rendered by default.

apps/mesh/src/web/components/add-binding/binding-definitions.ts

Lines changed: 425 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export { AddBindingModal } from "./add-binding-modal";
2+
export {
3+
type BindingDefinition,
4+
type BindingImplementation,
5+
type BindingStatus,
6+
BINDING_DEFINITIONS,
7+
getBindingById,
8+
getAvailableBindings,
9+
getComingSoonBindings,
10+
} from "./binding-definitions";

apps/mesh/src/web/components/collections/collection-card.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ import {
1010
} from "@deco/ui/components/dropdown-menu.tsx";
1111
import { Button } from "@deco/ui/components/button.tsx";
1212
import { DotsVertical, Eye, Edit01, Copy01, Trash01 } from "@untitledui/icons";
13+
import type { ReactNode } from "react";
1314

1415
interface CollectionCardProps<T extends BaseCollectionEntity> {
1516
item: T;
1617
schema: JsonSchema;
1718
readOnly?: boolean;
1819
actions?: Record<string, (item: T) => void | Promise<void>>;
20+
/** Custom icon renderer for the item */
21+
renderIcon?: (item: T) => ReactNode;
1922
}
2023

2124
function findImageField(schema: JsonSchema, item: unknown): string | undefined {
@@ -65,6 +68,7 @@ export function CollectionCard<T extends BaseCollectionEntity>({
6568
item,
6669
schema,
6770
actions,
71+
renderIcon,
6872
}: CollectionCardProps<T>) {
6973
const iconUrl = findImageField(schema, item);
7074
const description =
@@ -80,12 +84,16 @@ export function CollectionCard<T extends BaseCollectionEntity>({
8084
return (
8185
<Card className="cursor-pointer transition-colors h-full flex flex-col group relative">
8286
<div className="flex flex-col gap-4 p-6 flex-1">
83-
<IntegrationIcon
84-
icon={iconUrl}
85-
name={item.title}
86-
size="md"
87-
className="shrink-0 shadow-sm"
88-
/>
87+
{renderIcon ? (
88+
renderIcon(item)
89+
) : (
90+
<IntegrationIcon
91+
icon={iconUrl}
92+
name={item.title}
93+
size="md"
94+
className="shrink-0 shadow-sm"
95+
/>
96+
)}
8997
<div className="flex flex-col gap-0 flex-1">
9098
<h3 className="text-base font-medium text-foreground truncate">
9199
{item.title}

apps/mesh/src/web/components/collections/collections-list.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type React from "react";
12
import type { BaseCollectionEntity } from "@decocms/bindings/collections";
23
import { CollectionCard } from "./collection-card.tsx";
34
import { CollectionTableWrapper } from "./collection-table-wrapper.tsx";
@@ -83,6 +84,7 @@ export function CollectionsList<T extends BaseCollectionEntity>({
8384
hideToolbar = false,
8485
sortableFields = undefined,
8586
simpleDeleteOnly = false,
87+
renderIcon,
8688
}: CollectionsListProps<T>) {
8789
// Generate sort options from columns or schema
8890
const sortOptions = columns
@@ -148,6 +150,7 @@ export function CollectionsList<T extends BaseCollectionEntity>({
148150
schema={schema}
149151
readOnly={readOnly}
150152
actions={actions}
153+
renderIcon={renderIcon}
151154
/>
152155
</div>
153156
))}
@@ -156,7 +159,14 @@ export function CollectionsList<T extends BaseCollectionEntity>({
156159
</div>
157160
) : (
158161
<CollectionTableWrapper
159-
columns={getTableColumns(columns, schema, sortableFields, actions, simpleDeleteOnly)}
162+
columns={getTableColumns(
163+
columns,
164+
schema,
165+
sortableFields,
166+
actions,
167+
simpleDeleteOnly,
168+
renderIcon,
169+
)}
160170
data={data}
161171
sortKey={sortKey}
162172
sortDirection={sortDirection}
@@ -433,33 +443,52 @@ function generateColumnsFromSchema<T extends BaseCollectionEntity>(
433443
});
434444
}
435445

446+
// Helper to generate icon column from renderIcon
447+
function generateIconColumn<T extends BaseCollectionEntity>(
448+
renderIcon: (item: T) => React.ReactNode,
449+
): TableColumn<T> {
450+
return {
451+
id: "_icon",
452+
header: "",
453+
render: (row) => renderIcon(row),
454+
cellClassName: "w-[50px] pr-0",
455+
sortable: false,
456+
};
457+
}
458+
436459
// Helper to get table columns with actions column appended
437460
function getTableColumns<T extends BaseCollectionEntity>(
438461
columns: TableColumn<T>[] | undefined,
439462
schema: JsonSchema,
440463
sortableFields: string[] | undefined,
441464
actions: Record<string, (item: T) => void | Promise<void>>,
442465
simpleDeleteOnly: boolean,
466+
renderIcon?: (item: T) => React.ReactNode,
443467
): TableColumn<T>[] {
444468
const baseColumns =
445469
columns || generateColumnsFromSchema(schema, sortableFields);
446470

471+
// Add icon column at the beginning if renderIcon is provided
472+
const columnsWithIcon = renderIcon
473+
? [generateIconColumn(renderIcon), ...baseColumns]
474+
: baseColumns;
475+
447476
// Check if actions column already exists
448-
const hasActionsColumn = baseColumns.some((col) => col.id === "actions");
477+
const hasActionsColumn = columnsWithIcon.some((col) => col.id === "actions");
449478

450479
if (hasActionsColumn) {
451-
return baseColumns;
480+
return columnsWithIcon;
452481
}
453482

454483
// For simpleDeleteOnly, show only a trash icon if delete action exists
455484
if (simpleDeleteOnly && actions.delete) {
456-
return [...baseColumns, generateSimpleDeleteColumn(actions.delete)];
485+
return [...columnsWithIcon, generateSimpleDeleteColumn(actions.delete)];
457486
}
458487

459488
// Append actions column only if there are any actions available
460489
const hasActions = Object.keys(actions).length > 0;
461490
if (hasActions) {
462-
return [...baseColumns, generateActionsColumn(actions)];
491+
return [...columnsWithIcon, generateActionsColumn(actions)];
463492
}
464-
return baseColumns;
493+
return columnsWithIcon;
465494
}

apps/mesh/src/web/components/collections/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,11 @@ export interface CollectionsListProps<T extends BaseCollectionEntity> {
123123
* instead of the dropdown menu. Useful for FILES collection.
124124
*/
125125
simpleDeleteOnly?: boolean;
126+
127+
/**
128+
* Custom icon renderer for collection items.
129+
* If provided, renders custom icons instead of the default IntegrationIcon.
130+
* Useful for file-specific icons, custom badges, etc.
131+
*/
132+
renderIcon?: (item: T) => ReactNode;
126133
}

apps/mesh/src/web/components/details/connection/collection-tab.tsx

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
generateSortOptionsFromSchema,
88
} from "@/web/components/collections/collections-list.tsx";
99
import { EmptyState } from "@/web/components/empty-state.tsx";
10+
import { FileIcon } from "@/web/components/files/file-icon";
1011
import type { ValidatedCollection } from "@/web/hooks/use-binding";
1112
import { PinToSidebarButton } from "@/web/components/pin-to-sidebar-button";
1213
import { useConnection } from "@/web/hooks/collections/use-connection";
@@ -240,24 +241,27 @@ export function CollectionTab({
240241
if (files.length === 0) return;
241242

242243
// Add files to uploading state
243-
const newUploads = files.map((f) => ({ name: f.name, status: "uploading" as const }));
244+
const newUploads = files.map((f) => ({
245+
name: f.name,
246+
status: "uploading" as const,
247+
}));
244248
setUploadingFiles((prev) => [...prev, ...newUploads]);
245249

246250
for (const file of files) {
247251
try {
248252
await uploadMutation.mutateAsync({ file });
249253
setUploadingFiles((prev) =>
250254
prev.map((f) =>
251-
f.name === file.name ? { ...f, status: "done" as const } : f
252-
)
255+
f.name === file.name ? { ...f, status: "done" as const } : f,
256+
),
253257
);
254258
toast.success(`Uploaded ${file.name}`);
255259
} catch (error) {
256260
console.error("Upload failed:", error);
257261
setUploadingFiles((prev) =>
258262
prev.map((f) =>
259-
f.name === file.name ? { ...f, status: "error" as const } : f
260-
)
263+
f.name === file.name ? { ...f, status: "error" as const } : f,
264+
),
261265
);
262266
toast.error(`Failed to upload ${file.name}`);
263267
}
@@ -286,24 +290,27 @@ export function CollectionTab({
286290
if (!files || files.length === 0) return;
287291

288292
const fileArray = Array.from(files);
289-
const newUploads = fileArray.map((f) => ({ name: f.name, status: "uploading" as const }));
293+
const newUploads = fileArray.map((f) => ({
294+
name: f.name,
295+
status: "uploading" as const,
296+
}));
290297
setUploadingFiles((prev) => [...prev, ...newUploads]);
291298

292299
for (const file of fileArray) {
293300
try {
294301
await uploadMutation.mutateAsync({ file });
295302
setUploadingFiles((prev) =>
296303
prev.map((f) =>
297-
f.name === file.name ? { ...f, status: "done" as const } : f
298-
)
304+
f.name === file.name ? { ...f, status: "done" as const } : f,
305+
),
299306
);
300307
toast.success(`Uploaded ${file.name}`);
301308
} catch (error) {
302309
console.error("Upload failed:", error);
303310
setUploadingFiles((prev) =>
304311
prev.map((f) =>
305-
f.name === file.name ? { ...f, status: "error" as const } : f
306-
)
312+
f.name === file.name ? { ...f, status: "error" as const } : f,
313+
),
307314
);
308315
toast.error(`Failed to upload ${file.name}`);
309316
}
@@ -333,7 +340,9 @@ export function CollectionTab({
333340
) : (
334341
<Upload01 className="mr-2 h-4 w-4" />
335342
)}
336-
{isUploading ? `Uploading (${uploadingFiles.filter((f) => f.status === "uploading").length})...` : "Upload"}
343+
{isUploading
344+
? `Uploading (${uploadingFiles.filter((f) => f.status === "uploading").length})...`
345+
: "Upload"}
337346
</span>
338347
</Button>
339348
<input
@@ -421,6 +430,25 @@ export function CollectionTab({
421430
onItemClick={(item) => handleEdit(item)}
422431
readOnly={isReadOnly}
423432
simpleDeleteOnly={isFilesCollection}
433+
renderIcon={
434+
isFilesCollection
435+
? (item) => {
436+
const fileItem = item as unknown as {
437+
path?: string;
438+
mimeType?: string;
439+
isDirectory?: boolean;
440+
};
441+
return (
442+
<FileIcon
443+
path={fileItem.path}
444+
mimeType={fileItem.mimeType}
445+
isDirectory={fileItem.isDirectory}
446+
size="sm"
447+
/>
448+
);
449+
}
450+
: undefined
451+
}
424452
emptyState={
425453
isFilesCollection ? (
426454
<div className="flex flex-col items-center justify-center py-12 text-center">
@@ -456,20 +484,28 @@ export function CollectionTab({
456484
<Loading01 className="h-4 w-4 animate-spin text-muted-foreground shrink-0" />
457485
<div className="flex-1 min-w-0">
458486
<div className="text-sm font-medium">
459-
Uploading {uploadingFiles.filter((f) => f.status === "uploading").length} file(s)
487+
Uploading{" "}
488+
{
489+
uploadingFiles.filter((f) => f.status === "uploading")
490+
.length
491+
}{" "}
492+
file(s)
460493
</div>
461494
<div className="flex items-center gap-2 overflow-x-auto text-xs text-muted-foreground">
462495
{uploadingFiles.map((f) => (
463496
<span
464497
key={f.name}
465498
className={cn(
466499
"inline-flex items-center gap-1 shrink-0 px-2 py-0.5 rounded-full",
467-
f.status === "uploading" && "bg-primary/10 text-primary",
500+
f.status === "uploading" &&
501+
"bg-primary/10 text-primary",
468502
f.status === "done" && "bg-green-100 text-green-700",
469-
f.status === "error" && "bg-red-100 text-red-700"
503+
f.status === "error" && "bg-red-100 text-red-700",
470504
)}
471505
>
472-
{f.status === "uploading" && <Loading01 className="h-3 w-3 animate-spin" />}
506+
{f.status === "uploading" && (
507+
<Loading01 className="h-3 w-3 animate-spin" />
508+
)}
473509
{f.status === "done" && "✓"}
474510
{f.status === "error" && "✗"}
475511
{f.name}
@@ -520,9 +556,11 @@ export function CollectionTab({
520556
}
521557
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
522558
>
523-
{(isFilesCollection
524-
? fileMutations.isDeleting
525-
: actions.delete.isPending)
559+
{(
560+
isFilesCollection
561+
? fileMutations.isDeleting
562+
: actions.delete.isPending
563+
)
526564
? "Deleting..."
527565
: "Delete"}
528566
</AlertDialogAction>

apps/mesh/src/web/components/details/file/index.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66

77
import { useParams } from "@tanstack/react-router";
88
import { Button } from "@deco/ui/components/button.tsx";
9-
import { Download01, Trash01, File06, Folder, Loading01 } from "@untitledui/icons";
9+
import {
10+
Download01,
11+
Trash01,
12+
File06,
13+
Folder,
14+
Loading01,
15+
} from "@untitledui/icons";
1016
import { FileBrowser } from "@/web/components/files/file-browser";
1117
import { FilePreview } from "@/web/components/files/file-preview";
1218
import { useFileContent, useFileMutations } from "@/web/hooks/use-file-storage";
@@ -96,7 +102,11 @@ export function FileDetailsView({ itemId, onBack }: FileDetailsProps) {
96102
</ViewActions>
97103

98104
<div className="h-full overflow-hidden">
99-
<FilePreview connectionId={connectionId} file={file} className="h-full" />
105+
<FilePreview
106+
connectionId={connectionId}
107+
file={file}
108+
className="h-full"
109+
/>
100110
</div>
101111
</ViewLayout>
102112
);
@@ -143,4 +153,3 @@ export function FolderDetailsView({ itemId, onBack }: FileDetailsProps) {
143153
</ViewLayout>
144154
);
145155
}
146-

apps/mesh/src/web/components/file-drop-zone.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ export function FileDropZone({ children, className }: FileDropZoneProps) {
113113
<div>
114114
<h3 className="text-lg font-medium">Drop files to upload</h3>
115115
<p className="text-sm text-muted-foreground">
116-
Files will be stored in {primaryStorage?.title ?? "local storage"}
116+
Files will be stored in{" "}
117+
{primaryStorage?.title ?? "local storage"}
117118
</p>
118119
</div>
119120
</div>
@@ -154,4 +155,3 @@ export function useFileDropUpload() {
154155
storageConnection: primaryStorage,
155156
};
156157
}
157-

0 commit comments

Comments
 (0)