Adds a fully extensible command(ctrl)+k interface for Cal (#3346)

* init

* action function added

* kbar trigger placed

* UI improvements to KBar, added command + K tooltip

* renamed quick find to commandbar

* replaced window router with nextjs router

* keyboard up n down nav shows up

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
pull/3360/head
Syed Ali Shahbaz 2022-07-14 12:15:07 +05:30 committed by GitHub
parent bf7c86ffe5
commit 318ec316d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 268 additions and 3 deletions

View File

@ -0,0 +1,259 @@
import { SearchIcon } from "@heroicons/react/solid";
import {
KBarProvider,
KBarPortal,
KBarPositioner,
KBarAnimator,
KBarSearch,
KBarResults,
useMatches,
useKBar,
} from "kbar";
import { useRouter } from "next/router";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { isMac } from "@calcom/lib/isMac";
import { Tooltip } from "@calcom/ui";
type shortcutArrayType = {
shortcuts?: string[];
};
export const KBarRoot = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
// grab link to events
// quick nested actions would be extremely useful
const actions = [
// {
// id: "toggle-idle",
// name: "Test Function",
// section: "Status",
// shortcut: ["t", "f"],
// keywords: "set yourself away bookings",
// perform: () => alert("Hello World"),
// },
{
id: "upcoming-bookings",
name: "Upcoming Bookings",
section: "Booking",
shortcut: ["u", "b"],
keywords: "upcoming bookings",
perform: () => router.push("/bookings/upcoming"),
},
{
id: "event-types",
name: "Event Types",
section: "Event Types",
shortcut: ["e", "t"],
keywords: "event types",
perform: () => router.push("/event-types"),
},
{
id: "app-store",
name: "App Store",
section: "Apps",
shortcut: ["a", "s"],
keywords: "app store",
perform: () => router.push("/apps"),
},
{
id: "recurring-bookings",
name: "Recurring Bookings",
section: "Booking",
shortcut: ["r", "b"],
keywords: "recurring bookings",
perform: () => router.push("/bookings/recurring"),
},
{
id: "past-bookings",
name: "Past Bookings",
section: "Booking",
shortcut: ["p", "b"],
keywords: "past bookings",
perform: () => router.push("/bookings/past"),
},
{
id: "cancelled-bookings",
name: "Cancelled Bookings",
section: "Booking",
shortcut: ["c", "b"],
keywords: "cancelled bookings",
perform: () => router.push("/bookings/cancelled"),
},
{
id: "schedule",
name: "Schedule",
section: "Availability",
shortcut: ["s", "a"],
keywords: "schedule availability",
perform: () => router.push("/availability"),
},
{
id: "profile",
name: "Profile",
section: "Profile Settings",
shortcut: ["p", "s"],
keywords: "setting profile",
perform: () => router.push("/settings"),
},
{
id: "avatar",
name: "Change Avatar",
section: "Profile Settings",
shortcut: ["c", "a"],
keywords: "remove change modify avatar",
perform: () => router.push("/settings"),
},
{
id: "timezone",
name: "Change Timezone",
section: "Profile Settings",
shortcut: ["c", "t"],
keywords: "change modify timezone",
perform: () => router.push("/settings"),
},
{
id: "brand-color",
name: "Change Brand Color",
section: "Profile Settings",
shortcut: ["b", "c"],
keywords: "change modify brand color",
perform: () => router.push("/settings"),
},
{
id: "teams",
name: "Teams",
shortcut: ["t", "s"],
keywords: "add manage modify team",
perform: () => router.push("/settings/teams"),
},
{
id: "password",
name: "Change Password",
section: "Security Settings",
shortcut: ["c", "p"],
keywords: "change modify password",
perform: () => router.push("/settings/security"),
},
{
id: "two-factor",
name: "Two Factor Authentication",
section: "Security Settings",
shortcut: ["t", "f", "a"],
keywords: "two factor authentication",
perform: () => router.push("/settings/security"),
},
{
id: "impersonation",
name: "User Impersonation",
section: "Security Settings",
shortcut: ["u", "i"],
keywords: "user impersonation",
perform: () => router.push("/settings/security"),
},
{
id: "webhooks",
name: "Webhook",
section: "Developer Settings",
shortcut: ["w", "h"],
keywords: "webhook automation",
perform: () => router.push("/settings/developer"),
},
{
id: "api-keys",
name: "API Keys",
section: "Developer Settings",
shortcut: ["a", "p", "i"],
keywords: "api keys",
perform: () => router.push("/settings/developer"),
},
{
id: "billing",
name: "View and Manage Billing",
section: "Billing",
shortcut: ["m", "b"],
keywords: "billing view manage",
perform: () => router.push("/settings/billing"),
},
];
return <KBarProvider actions={actions}>{children}</KBarProvider>;
};
export const KBarContent = () => {
return (
<KBarPortal>
<KBarPositioner>
<KBarAnimator className="bg-white shadow-lg">
<KBarSearch className="min-w-96 rounded-sm px-4 py-2.5 focus-visible:outline-none" />
<RenderResults />
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
);
};
export const KBarTrigger = () => {
const { query } = useKBar();
const { t } = useLocale();
return (
<div className="flex">
<button
color="minimal"
onClick={query.toggle}
className="group flex w-full items-center rounded-sm px-2 py-2 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900">
<span className="h-5 w-5 flex-shrink-0 text-neutral-400 group-hover:text-neutral-500 ltr:mr-3 rtl:ml-3">
<SearchIcon />
</span>
<Tooltip content={isMac ? "⌘ + K" : "CTRL + K"}>
<span className="hidden lg:inline">{t("commandbar")}</span>
</Tooltip>
</button>
</div>
);
};
const DisplayShortcuts = (item: shortcutArrayType) => {
const shortcuts = item.shortcuts;
return (
<span className="space-x-1">
{shortcuts?.map((shortcut) => {
return (
<kbd key={shortcut} className="rounded-sm bg-gray-700 px-2 py-1 text-white">
{shortcut}
</kbd>
);
})}
</span>
);
};
function RenderResults() {
const { results } = useMatches();
console.log(results);
return (
<KBarResults
items={results}
onRender={({ item, active }) =>
typeof item === "string" ? (
<div className="bg-white p-4 text-xs uppercase text-gray-500">{item}</div>
) : (
<div
// For seeing keyboard up & down navigation in action, we need visual feedback based on "active" prop
style={{
background: active ? "rgb(245,245,245)" : "#fff",
borderLeft: active ? "2px solid black" : "2px solid transparent",
}}
className="flex items-center justify-between px-4 py-2.5 text-sm hover:cursor-pointer">
<span>{item.name}</span>
<DisplayShortcuts shortcuts={item.shortcut} />
</div>
)
}
/>
);
}

View File

@ -42,6 +42,7 @@ import useTheme from "@lib/hooks/useTheme";
import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding";
import { KBarRoot, KBarContent, KBarTrigger } from "@components/Kbar";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";
import Badge from "@components/ui/Badge";
@ -260,6 +261,7 @@ const Layout = ({
})}
</Fragment>
))}
<KBarTrigger />
</nav>
</div>
<TrialBanner />
@ -454,11 +456,12 @@ export default function Shell(props: LayoutProps) {
if (!session && !props.isPublic) return null;
return (
<>
<KBarRoot>
<Theme />
<CustomBranding lightVal={user?.brandColor} darkVal={user?.darkBrandColor} />
<MemoizedLayout plan={user?.plan} status={status} {...props} isLoading={isLoading} />
</>
<KBarContent />
</KBarRoot>
);
}

View File

@ -75,6 +75,7 @@
"ical.js": "^1.4.0",
"ics": "^2.31.0",
"jimp": "^0.16.1",
"kbar": "^0.1.0-beta.36",
"libphonenumber-js": "^1.9.53",
"lodash": "^4.17.21",
"memory-cache": "^0.2.0",

View File

@ -985,5 +985,6 @@
"no_active_event_types": "No active event types",
"new_seat_subject": "New Attendee {{name}} on {{eventType}} at {{date}}",
"new_seat_title": "Someone has added themselves to an event",
"invalid_number": "Invalid phone number"
"invalid_number": "Invalid phone number",
"commandbar": "Command Bar"
}

1
packages/lib/isMac.ts Normal file
View File

@ -0,0 +1 @@
export const isMac = typeof window !== "undefined" ? navigator.userAgent.indexOf("Mac") != -1 : false;