Skip to content

Commit

Permalink
✨ feat(app): Add support for extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
orlowdev committed Aug 12, 2023
1 parent e125776 commit 5bdb8fe
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 105 deletions.
3 changes: 2 additions & 1 deletion srv/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Modal from "./components/modal"
import { useDefaultCommandPalette } from "./streams/command-palette"
import ContextMenu from "./components/context-menu/context-menu"
import { useContextMenu } from "./streams/context-menu"
import { useActivities } from "./streams/extensions"

type OnDragFn = Unary<[number, number], void>
type OnDragEndFn = OnDragFn
Expand Down Expand Up @@ -70,7 +71,7 @@ export default function App({ id, data, web }: Hosts) {

return (
<div className="flex" onClick={contextMenu.hide}>
<ActivityBar activities={[{ name: "file-explorer", version: "0.1.0", background: false }]} />
<ActivityBar />
{sidebar?.disabled ? (
<div className="max-h-screen h-full flex overflow-auto w-full">NO SIDEBAR</div>
) : (
Expand Down
14 changes: 5 additions & 9 deletions srv/app/src/components/activity-bar/activity-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ import { BsThreeDotsVertical } from "react-icons/bs"
import { useSidebar } from "src/streams/sidebar"
import { useUser } from "../../streams/auth"
import Null from "../null"
import Activity from "./activity"
import ActivityItem from "./activity"
import { useCommands } from "src/hooks/use-commands"
import { useCommandPaletteItems } from "src/streams/command-palette"
import { Activity, useActivities } from "src/streams/extensions"

type Props = {
activities: any[]
}

export default function ActivityBar({ activities }: Props) {
export default function ActivityBar() {
const user = useUser()
const sidebar = useSidebar()
const commands = useCommands()
const activities = useActivities()

const commandPaletteItems = useCommandPaletteItems()

Expand All @@ -38,9 +36,7 @@ export default function ActivityBar({ activities }: Props) {
</div>
<div className="flex flex-col space-y-4 items-center">
{activities.map(activity =>
activity.background ? null : (
<Activity key={activity.name} name={activity.name} version={activity.version} />
)
activity.background ? null : <ActivityItem key={activity.name} activity={activity} />
)}
</div>
<div>{user.fold(Null, user => user.email.slice(0, 1))}</div>
Expand Down
30 changes: 27 additions & 3 deletions srv/app/src/components/activity-bar/activity.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
type Props = any
import { Activity, useCurrentActivity } from "src/streams/extensions"
import { Link } from "../link"

export default function Activity({ name }: Props) {
return <div>{name.slice(0, 1)}</div>
type Props = {
activity: Activity
}

/**
* ActivityBarActivity is the Icon user can click to get to the activity.
*/
export default function ActivityBarActivity({ activity }: Props) {
const currentActivity = useCurrentActivity()

const activityRoute = activity.routes[0]
const Icon = activity.Icon

return (
<Link
className={`!text-neutral-700 dark:!text-neutral-300 hover:!text-purple-600 dark:hover:!text-purple-400 transition-all duration-300 text-2xl leading-[0] rounded-lg ${
currentActivity && currentActivity.name === activity.name
? "!text-pink-700 dark:!text-pink-500 hover:!text-purple-600 dark:hover:!text-purple-400"
: ""
}`}
href={activityRoute}
>
<Icon />
</Link>
)
}
3 changes: 3 additions & 0 deletions srv/app/src/hooks/asdf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Asdf() {
return <div>Hello there</div>
}
7 changes: 4 additions & 3 deletions srv/app/src/hooks/use-app-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { initModals } from "../streams/modal"
import { initCommandPalette } from "../streams/command-palette"
import { initRouter } from "src/streams/router"
import { initContextMenu } from "src/streams/context-menu"
import { initActivities, initExtensions } from "src/streams/extensions"

const refreshToken = (hosts: Hosts) => {
fetch(`${hosts.id}/refresh-token`, { method: "POST", credentials: "include" })
Expand All @@ -24,13 +25,13 @@ export const useAppInit = (hosts: Hosts) => {
useEffect(() => {
initHosts(hosts)
initCommands({ logger: ConsoleLogger })
initRouter({ logger: ConsoleLogger })
const router$ = initRouter({ logger: ConsoleLogger })
initModals({ logger: ConsoleLogger })
initContextMenu()
initCommandPalette()
initSidebar()
// initExtensions({ logger: ConsoleLogger, router$, contextMenu$, extensions: [] })
// initActivities()
initExtensions({ logger: ConsoleLogger, router$, extensions: [] })
initActivities()

// TODO: Allow native context menu if custom context menu is not available
const suppressRightClickBehavior = (event: MouseEvent) => event.preventDefault()
Expand Down
11 changes: 10 additions & 1 deletion srv/app/src/hooks/use-on-authenticated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import {
} from "../streams/auth"
import { useCommands } from "./use-commands"
import { useCommandPalette } from "src/streams/command-palette"
import { AiOutlineLogout } from "react-icons/ai"
import { AiOutlineLogout, AiOutlineAim } from "react-icons/ai"
import { useExtensions } from "src/streams/extensions"
import Asdf from "./asdf"

export const useOnAuthenticated = (hosts: Hosts) => {
const isAuthenticated = useAuthStatus()
const us0 = useUserAPIService0()
const ms0 = useMetadataAPIService0()
const commands = useCommands()
const commandPalette = useCommandPalette()
const exts = useExtensions()

useEffect(() => {
if (!isAuthenticated) return
Expand All @@ -38,6 +41,12 @@ export const useOnAuthenticated = (hosts: Hosts) => {
commands.on("core.refresh-metadata-root", handleRefreshMetadataRoot)
commands.on("core.sign-out", handleSignOut)

exts.activities.add("test", {
Component: Asdf,
Icon: AiOutlineAim,
routes: ["/asdf"],
})

commands.emit("core.refresh-user-info")
commands.emit("core.refresh-metadata-root")

Expand Down
6 changes: 4 additions & 2 deletions srv/app/src/streams/command-palette.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { RegisterCommandPaletteItemFn } from "./extensions"
import { Thunk, callOnce } from "#lib/tau/mod"
import { Thunk, Unary, callOnce } from "#lib/tau/mod"
import { BehaviorSubject, map, merge, scan, shareReplay, Subject } from "rxjs"
import { IconType } from "react-icons"
import { useSubscription } from "../hooks/use-subscription"
Expand All @@ -9,6 +8,9 @@ import { CommandPaletteModal } from "../components/command-palette"
import { useCommands } from "src/hooks/use-commands"
import { hideModal } from "./modal"

export type UnregisterCommandPaletteItemFn = Unary<string, void>
export type RegisterCommandPaletteItemFn = Unary<CommandPaletteItem, void>

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CommandPaletteItem = {
id: string
Expand Down
20 changes: 18 additions & 2 deletions srv/app/src/streams/context-menu.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import { Nullable, callOnce } from "#lib/tau/mod"
import { Binary, Nullable, Unary, callOnce } from "#lib/tau/mod"
import { IconType } from "react-icons"
import { combineLatestWith, map, merge, scan, shareReplay, Subject } from "rxjs"
import { BehaviorSubject } from "rxjs"
import { RegisterContextMenuItemFn, UnregisterContextMenuItemFn } from "./extensions"
import { useSubscription } from "src/hooks/use-subscription"
import { CommandListener } from "./commands"

export type UnregisterContextMenuItemFn = Unary<string, void>
export type RegisterContextMenuItemFn = Binary<
CommandListener,
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
shouldShow: (target: any) => boolean
Icon: IconType
accelerator?: string
type: "create" | "read" | "update" | "delete"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
disabled?: (target: any) => boolean
payloadCreator?: () => any
},
void
>

export type ShowContextMenuParams = {
x: number
Expand Down
114 changes: 30 additions & 84 deletions srv/app/src/streams/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Logger } from "#lib/logger/mod"
import { Binary, Nullable, Thunk, Unary, callOnce } from "#lib/tau/mod"
import i18next from "i18next"
import { UserInfo } from "os"
import { prop } from "ramda"
import { ComponentType } from "react"
import { mergeMap, BehaviorSubject, mergeAll, Observable, of, merge, Subject } from "rxjs"
import { map, filter, switchMap, scan, shareReplay } from "rxjs/operators"
import { Router } from "silkrouter"
import {
CommandListener,
RegisterCommandFn,
ExecuteCommandFn,
prependListener,
Expand All @@ -23,8 +20,9 @@ import { route, Route, noMatch } from "./router"
import { RegisterTranslationsFn, registerTranslations } from "./translations"
import { File, FileExtension } from "#lib/universal-data-service/mod"
import { IconType } from "react-icons"
import { User } from "#lib/backend-user-service/mod"
import { CommandPaletteItem } from "./command-palette"
import { RegisterContextMenuItemFn, UnregisterContextMenuItemFn } from "./context-menu"
import { RegisterCommandPaletteItemFn, UnregisterCommandPaletteItemFn } from "./command-palette"
import { useSubscription } from "src/hooks/use-subscription"

export type ContextMenuItemType = "create" | "read" | "update" | "delete"

Expand All @@ -49,7 +47,7 @@ export type Activity = {
Component: ComponentType
Icon: ComponentType
Sidebar?: ComponentType
show?: boolean
background?: boolean
}

export type FileAssociation = {
Expand All @@ -69,59 +67,25 @@ export enum IconSize {
}

export type RegisterActivityFn = Unary<string, Binary<string, Omit<Activity, "name">, void>>
export type RegisterContextMenuItemFn = Binary<
CommandListener,
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
shouldShow: (target: any) => boolean
Icon: IconType
accelerator?: string
type: "create" | "read" | "update" | "delete"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
disabled?: (target: any) => boolean
payloadCreator?: () => any
},
void
>

export type RegisterFileAssociationFn = Unary<
string,
Binary<string, Omit<FileAssociation, "name">, void>
>

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type RegisterCommandPaletteItemFn = Unary<CommandPaletteItem, void>

// export type RegisterEditorPluginFn = Unary<
// string,
// Binary<string, Omit<EditorPlugin, "name">, void>
// >

export type UnregisterEditorPluginFn = Unary<string, Unary<string, void>>
export type UnregisterCommandPaletteItemFn = Unary<string, void>

export type UnregisterActivityFn = Unary<string, Unary<string, void>>
export type UnregisterContextMenuItemFn = Unary<string, void>

export type UnregisterFileAssociationFn = Unary<string, Unary<string, void>>

export type ExtensionCreatorContext = {
commands: {
before: RegisterCommandFn
after: RegisterCommandFn
on: RegisterCommandFn
off: RegisterCommandFn
emit: ExecuteCommandFn
}
// registerEditorPlugin: RegisterEditorPluginFn
// unregisterEditorPlugin: UnregisterEditorPluginFn
// registerContextMenuItem: RegisterContextMenuItemFn
// unregisterContextMenuItem: UnregisterContextMenuItemFn
registerActivity: RegisterActivityFn
unregisterActivity: UnregisterActivityFn
// registerFileAssociation: RegisterFileAssociationFn
// unregisterFileAssociation: UnregisterFileAssociationFn
registerTranslations: RegisterTranslationsFn
// registerCommandPaletteItem: RegisterCommandPaletteItemFn
// unregisterCommandPaletteItem: UnregisterCommandPaletteItemFn
logger: Logger
}

Expand All @@ -144,46 +108,20 @@ const isFulfilled = <T>(x: PromiseSettledResult<T>): x is PromiseFulfilledResult

type InitExtensionsParams = {
logger: Logger
contextMenu$: Observable<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Nullable<{ x: number; y: number; target: any; structure: ContextMenuItem[] }>
>
extensions: Thunk<Promise<{ default: Unary<ExtensionCreatorContext, void> }>>[]
router$: Router
}

export const initExtensions = callOnce(({ logger, router$, extensions }: InitExtensionsParams) =>
of(extensions)
.pipe(
map(exts => exts.map(f => f())),
mergeMap(exts => Promise.allSettled(exts)),
mergeAll(),
filter(isFulfilled),
map(prop("value")),
map(prop("default")),
map(f =>
f({
commands: {
before: prependListener,
after: appendListener,
on: registerCommand,
off: unregisterCommand,
emit: executeCommand,
},
// registerEditorPlugin,
// unregisterEditorPlugin,
// registerCommandPaletteItem,
// unregisterCommandPaletteItem,
// registerContextMenuItem,
// unregisterContextMenuItem,
registerTranslations,
// registerFileAssociation,
// unregisterFileAssociation,
registerActivity,
unregisterActivity,
logger,
})
),
// map(exts => exts.map(f => f())),
// mergeMap(exts => Promise.allSettled(exts)),
// mergeAll(),
// filter(isFulfilled),
// map(prop("value")),
// map(prop("default")),
// map(f => f({ logger })),
switchMap(() => activities$),
map(activities => {
activities?.map(activity => {
Expand Down Expand Up @@ -235,18 +173,26 @@ export const initActivities = callOnce(() => {
return activities$
})

export const registerActivity =
(extensionName: string) => (name: string, activity: Omit<Activity, "name">) => {
addActivity$.next({
...activity,
name: `${extensionName}.${name}`,
})
}
export const registerActivity = (name: string, activity: Omit<Activity, "name">) => {
addActivity$.next({ ...activity, name })
}

export const unregisterActivity = (extensionName: string) => (name: string) => {
removeActivity$.next(`${extensionName}.${name}`)
export const unregisterActivity = (name: string) => {
removeActivity$.next(name)
}

export const clearActivities = () => {
clearActivities$.next(null)
}

export const useExtensions = () => ({
activities: {
add: registerActivity,
remove: unregisterActivity,
clear: clearActivities,
},
})

export const useCurrentActivity = () => useSubscription(currentActivity$)
export const useRoute = () => useSubscription(currentRoute$)
export const useActivities = () => useSubscription(activities$, [])

1 comment on commit 5bdb8fe

@deno-deploy
Copy link

@deno-deploy deno-deploy bot commented on 5bdb8fe Aug 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed to deploy:

Module not found "file:///src/etc/deno/lib/backend-dynamodb-user-repository/mod.ts".

Please sign in to comment.