Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 42 additions & 23 deletions apps/web/src/components/links/CreateLinksCollectionDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"use client";

import { useEffect, useState } from "react";
import AppIcon from "@dashwise/app-icon";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import IconPickerComponent, { type IconResult } from "@/components/settings/IconPicker";
import useAuth from "@/context/useAuth";
import { createLinksCollectionAction, updateLinksCollectionAction } from '@/lib/apiClient';
import { LinksFormAlert, type LinksFormAlertState } from "./LinksFormAlert";
Expand All @@ -21,6 +24,7 @@ export default function CreateLinksCollectionDialog({ open, onOpenChange, collec
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [icon, setIcon] = useState("");
const [iconPickerOpen, setIconPickerOpen] = useState(false);
const [alert, setAlert] = useState<LinksFormAlertState>({ open: false, title: "", description: "", variant: "success" });
const isEditing = Boolean(collection?.id);

Expand All @@ -30,6 +34,7 @@ export default function CreateLinksCollectionDialog({ open, onOpenChange, collec
setName(collection?.name ?? "");
setDescription(collection?.description ?? "");
setIcon(collection?.icon ?? "");
setIconPickerOpen(false);
setAlert({ open: false, title: "", description: "", variant: "success" });
}, [collection, open]);

Expand Down Expand Up @@ -69,6 +74,7 @@ export default function CreateLinksCollectionDialog({ open, onOpenChange, collec
onOpenChange(false);
setName("");
setDescription("");
setIcon("");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAlert({
Expand All @@ -82,16 +88,42 @@ export default function CreateLinksCollectionDialog({ open, onOpenChange, collec
>
<div className="space-y-2">
<Label htmlFor="list-name">List name</Label>
<input
id="list-name"
name="name"
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
className="w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-foreground outline-none transition-colors placeholder:text-white/35 focus:border-primary"
placeholder="Design inspiration"
required
/>
<div className="flex items-center gap-2">
<input
id="list-name"
name="name"
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
className="min-w-0 flex-1 rounded-md border border-white/10 bg-white/5 px-3 py-2 text-foreground outline-none transition-colors placeholder:text-white/35 focus:border-primary"
placeholder="Design inspiration"
required
/>
{!renameOnly && (
<Popover open={iconPickerOpen} onOpenChange={setIconPickerOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
aria-label="Search icons or Enter URL"
title="Search icons or Enter URL"
className="h-9 w-9 shrink-0 rounded-md border-white/10 bg-white/5 p-0 text-white hover:bg-white/10"
>
{icon ? <AppIcon source={icon} alt="" size={20} imageClassName="object-contain" /> : null}
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="frosted w-[min(42rem,calc(100vw-2rem))] p-3 text-foreground">
<IconPickerComponent
initialSelection={icon ? { url: icon } : null}
onSelect={(selectedIcon: IconResult) => {
setIcon(selectedIcon.url?.trim() ?? "");
setIconPickerOpen(false);
}}
/>
</PopoverContent>
</Popover>
)}
</div>
</div>

{!renameOnly && (
Expand All @@ -108,19 +140,6 @@ export default function CreateLinksCollectionDialog({ open, onOpenChange, collec
placeholder="Optional note about what belongs here"
/>
</div>

<div className="space-y-2">
<Label htmlFor="list-icon">Icon</Label>
<input
id="list-icon"
name="icon"
type="text"
value={icon}
onChange={(event) => setIcon(event.target.value)}
className="w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-foreground outline-none transition-colors placeholder:text-white/35 focus:border-primary"
placeholder="fa6-solid:folder-open or url:https://..."
/>
</div>
</>
)}

Expand Down
24 changes: 23 additions & 1 deletion apps/web/src/components/links/CreateLinksItemDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ function IconPickerButton({
);
}

function parseMarkdownLink(value: string) {
const match = value.trim().match(/^\[([^\]]+)\]\(\s*<?(https?:\/\/[^\s)>]+)>?\s*\)$/i);
if (!match) return null;

return {
title: match[1].trim(),
url: match[2],
};
}

function buildTargetGroups(collections: CollectionRecord[], folders: FolderRecord[]): TargetGroup[] {
const foldersByCollection = new Map<string, FolderRecord[]>();
for (const folder of folders) {
Expand Down Expand Up @@ -717,7 +727,19 @@ defaultFolderId,
<Input
id="link-url"
value={url}
onChange={(event) => setUrl(event.target.value)}
onChange={(event) => {
const value = event.target.value;
const markdownLink = parseMarkdownLink(value);

if (markdownLink) {
setUrl(markdownLink.url);
autoMetadataRef.current.title = "";
setTitle(markdownLink.title);
return;
}

setUrl(value);
}}
placeholder="https://n8n.io"
required={activeTab === "link"}
/>
Expand Down
14 changes: 13 additions & 1 deletion packages/integrationskit/static-widgets/CalendarWidgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,18 @@ export function CalendarUpcomingWidget({ items = [], maxItems = 5, className = "
return date.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
};

const formatEventTime = (item: CalendarUpcomingItem) => {
if (!item.isAllDay) return formatTime(item.start);
if (!item.end) return "All day";

const start = new Date(item.start);
const end = new Date(item.end);
const inclusiveEnd = new Date(end.getTime() - 1);
const spansMultipleDays = start.toDateString() !== inclusiveEnd.toDateString();

return spansMultipleDays ? `Until ${formatDate(inclusiveEnd.toISOString())}` : "All day";
};

const resolved = {
header: {
title: "Upcoming Events",
Expand All @@ -133,7 +145,7 @@ export function CalendarUpcomingWidget({ items = [], maxItems = 5, className = "
list: upcoming.map((item) => ({
title: item.title,
subtitle: [
item.isAllDay ? "All day" : formatTime(item.start),
formatEventTime(item),
item.location,
].filter(Boolean) as string[],
group: formatDate(item.start),
Expand Down
Loading