From 85b88822b4bd1d6fc4d2ec80b07b6ced5709fb9b Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Mon, 19 Jun 2023 09:25:03 +0200 Subject: [PATCH] fix: improved UI --- packages/sdk/src/hooks/index.ts | 5 + packages/sdk/src/widgets/hooks.ts | 52 ++++++++- packages/sdk/src/widgets/index.ts | 5 + packages/sdk/src/widgets/widget-context.tsx | 56 +++++++++- packages/ui/src/base/dialog/index.tsx | 2 + packages/ui/src/base/dropdown/index.tsx | 13 +++ .../ui/src/github/action/index.stories.tsx | 20 ---- packages/ui/src/github/action/index.tsx | 56 ---------- packages/ui/src/github/index.ts | 1 + .../github/{action => workflow-run}/data.json | 0 .../src/github/workflow-run/index.stories.tsx | 20 ++++ packages/ui/src/github/workflow-run/index.tsx | 72 ++++++++++++ packages/ui/src/interface/board/index.tsx | 3 - packages/ui/src/interface/widget/index.tsx | 103 +++++++++++++----- packages/ui/src/theme/provider.tsx | 2 +- packages/widgets/src/github/index.ts | 4 + .../src/github/pull-request-comments/view.tsx | 3 + .../widgets/src/github/pull-request/view.tsx | 9 +- .../widgets/src/github/workflow-run/edit.tsx | 44 ++++++++ .../src/github/workflow-run/index.widget.tsx | 28 +++++ .../widgets/src/github/workflow-run/schema.ts | 12 ++ .../widgets/src/github/workflow-run/view.tsx | 53 +++++++++ .../widgets/src/github/workflow-runs/edit.tsx | 37 +++++++ .../src/github/workflow-runs/index.widget.tsx | 28 +++++ .../src/github/workflow-runs/schema.ts | 11 ++ .../widgets/src/github/workflow-runs/view.tsx | 56 ++++++++++ .../src/linear/issue-with-comments/view.tsx | 3 + .../widgets/src/slack/conversation/view.tsx | 44 +++++--- 28 files changed, 618 insertions(+), 124 deletions(-) delete mode 100644 packages/ui/src/github/action/index.stories.tsx delete mode 100644 packages/ui/src/github/action/index.tsx rename packages/ui/src/github/{action => workflow-run}/data.json (100%) create mode 100644 packages/ui/src/github/workflow-run/index.stories.tsx create mode 100644 packages/ui/src/github/workflow-run/index.tsx create mode 100644 packages/widgets/src/github/workflow-run/edit.tsx create mode 100644 packages/widgets/src/github/workflow-run/index.widget.tsx create mode 100644 packages/widgets/src/github/workflow-run/schema.ts create mode 100644 packages/widgets/src/github/workflow-run/view.tsx create mode 100644 packages/widgets/src/github/workflow-runs/edit.tsx create mode 100644 packages/widgets/src/github/workflow-runs/index.widget.tsx create mode 100644 packages/widgets/src/github/workflow-runs/schema.ts create mode 100644 packages/widgets/src/github/workflow-runs/view.tsx diff --git a/packages/sdk/src/hooks/index.ts b/packages/sdk/src/hooks/index.ts index 8f96600..64f6ddd 100644 --- a/packages/sdk/src/hooks/index.ts +++ b/packages/sdk/src/hooks/index.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef } from 'react'; +import { useUpdateEffect } from '../widgets'; type AutoUpdateOptions = { interval: number; @@ -36,6 +37,10 @@ const useAutoUpdate = ( }; }, [interval, actionWithCallback, callbackWithCallback]); + useUpdateEffect(async () => { + await update(); + }, [update]); + return update; }; diff --git a/packages/sdk/src/widgets/hooks.ts b/packages/sdk/src/widgets/hooks.ts index 6fffa77..1433df1 100644 --- a/packages/sdk/src/widgets/hooks.ts +++ b/packages/sdk/src/widgets/hooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useContext, useMemo, useState } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { WidgetsContext } from './context'; import { WidgetContext } from './widget-context'; @@ -111,9 +111,58 @@ const useWidgetId = () => { return context.id; }; +const useName = () => { + const context = useContext(WidgetContext); + if (!context) { + throw new Error('useName must be used within a WidgetProvider'); + } + return [context.name, context.setName] as const; +}; + +const useHasUpdate = () => { + const context = useContext(WidgetContext); + if (!context) { + throw new Error('useHasUpdate must be used within a WidgetProvider'); + } + return context.hasUpdater; +}; + +const useUpdateEffect = (fn: () => Promise, deps: any[]) => { + const context = useContext(WidgetContext); + if (!context) { + throw new Error('useUpdateEffect must be used within a WidgetProvider'); + } + useEffect(() => { + const unsubscribe = context.addUpdater(() => fn()); + return () => { + unsubscribe(); + }; + }, [context.addUpdater, ...deps]); +}; + +const useReloadWidget = () => { + const context = useContext(WidgetContext); + if (!context) { + throw new Error('useUpdateWidget must be used within a WidgetProvider'); + } + return context.updateWidget; +}; + +const useIsUpdating = () => { + const context = useContext(WidgetContext); + if (!context) { + throw new Error('useIsUpdating must be used within a WidgetProvider'); + } + return context.updating; +}; + export { useWidget, useWidgets, + useName, + useUpdateEffect, + useHasUpdate, + useReloadWidget, useGetWidgetsFromUrl, useWidgetNotifications, useDismissWidgetNotification, @@ -121,4 +170,5 @@ export { useWidgetData, useSetWidgetData, useWidgetId, + useIsUpdating, }; diff --git a/packages/sdk/src/widgets/index.ts b/packages/sdk/src/widgets/index.ts index 0288abf..88fc5e6 100644 --- a/packages/sdk/src/widgets/index.ts +++ b/packages/sdk/src/widgets/index.ts @@ -10,6 +10,11 @@ export { useWidgetId, useWidgetData, useSetWidgetData, + useName, + useUpdateEffect, + useReloadWidget, + useHasUpdate, + useIsUpdating, } from './hooks'; export { WidgetProvider } from './widget-context'; export { WidgetView } from './view'; diff --git a/packages/sdk/src/widgets/widget-context.tsx b/packages/sdk/src/widgets/widget-context.tsx index 56ad609..74dc546 100644 --- a/packages/sdk/src/widgets/widget-context.tsx +++ b/packages/sdk/src/widgets/widget-context.tsx @@ -1,4 +1,4 @@ -import { createContext, useCallback, useMemo, useRef } from 'react'; +import { createContext, useCallback, useMemo, useRef, useState } from 'react'; import { Notification as BaseNotification, useNotificationAdd, @@ -14,8 +14,16 @@ type WidgetContextValue = { dismissNotification: (id: string) => void; notifications: Notification[]; setData?: (data: any) => void; + addUpdater: (updater: Updater) => () => void; + updating: boolean; + hasUpdater: boolean; + name: string; + setName: (name: string) => void; + updateWidget: () => Promise; }; +type Updater = () => Promise | void; + type WidgetProviderProps = { id: string; data?: any; @@ -32,6 +40,9 @@ const WidgetProvider = ({ children, }: WidgetProviderProps) => { const ref = useRef(Symbol('WidgetRender')); + const [updating, setUpdating] = useState(false); + const [name, setName] = useState(''); + const [updaters, setUpdaters] = useState([]); const globalNotifications = useNotifications(); const addGlobalNotification = useNotificationAdd(); const dissmissGlobalNotification = useNotificationDismiss(); @@ -46,6 +57,16 @@ const WidgetProvider = ({ [addGlobalNotification], ); + const addUpdater = useCallback( + (updater: Updater) => { + setUpdaters((prev) => [...prev, updater]); + return () => { + setUpdaters((prev) => prev.filter((u) => u !== updater)); + }; + }, + [setUpdaters], + ); + const dismissNotification = useCallback( (dismissId: string) => { dissmissGlobalNotification(dismissId); @@ -53,6 +74,18 @@ const WidgetProvider = ({ [dissmissGlobalNotification], ); + const updateWidget = useCallback(async () => { + setUpdating(true); + for (const updater of updaters) { + try { + await updater(); + } catch (e) { + console.error(e); + } + } + setUpdating(false); + }, [updaters]); + const value = useMemo( () => ({ addNotification, @@ -61,8 +94,27 @@ const WidgetProvider = ({ id, data, setData, + name, + setName, + addUpdater, + updateWidget, + updating, + hasUpdater: updaters.length > 0, }), - [addNotification, notifications, id, data, setData, dismissNotification], + [ + addNotification, + notifications, + id, + data, + setData, + updating, + dismissNotification, + name, + setName, + addUpdater, + updateWidget, + updaters.length, + ], ); return ( diff --git a/packages/ui/src/base/dialog/index.tsx b/packages/ui/src/base/dialog/index.tsx index c51b159..163e977 100644 --- a/packages/ui/src/base/dialog/index.tsx +++ b/packages/ui/src/base/dialog/index.tsx @@ -11,6 +11,7 @@ const Overlay = styled(DialogPrimitives.Overlay)` position: fixed; inset: 0; backdrop-filter: blur(5px); + background-color: rgba(0, 0, 0, 0.5); `; const Portal = styled(DialogPrimitives.Portal)``; @@ -30,6 +31,7 @@ const Content = styled(DialogPrimitives.Content)` max-width: 450px; max-height: 85vh; padding: 25px; + box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight}; `; const Title = styled(DialogPrimitives.Title)` diff --git a/packages/ui/src/base/dropdown/index.tsx b/packages/ui/src/base/dropdown/index.tsx index 4dc08ba..06a20bb 100644 --- a/packages/ui/src/base/dropdown/index.tsx +++ b/packages/ui/src/base/dropdown/index.tsx @@ -1,4 +1,5 @@ import * as DropdownMenuPrimitives from '@radix-ui/react-dropdown-menu'; +import { motion } from 'framer-motion'; import { styled, css } from 'styled-components'; const RightSlot = styled.div` @@ -62,6 +63,17 @@ const Trigger = styled(DropdownMenuPrimitives.Trigger)` const Portal = styled(DropdownMenuPrimitives.Portal)``; +const OverlayComponent = styled(motion.div)` + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(8px); +`; + +const Overlay: React.FC = () => ( + +); + const Item = styled(DropdownMenuPrimitives.Item)` ${item} `; @@ -147,6 +159,7 @@ const DropdownMenu = Object.assign(Root, { Item, Sub, SubTrigger, + Overlay, SubContent, Separator, CheckboxItem, diff --git a/packages/ui/src/github/action/index.stories.tsx b/packages/ui/src/github/action/index.stories.tsx deleted file mode 100644 index f6d60a4..0000000 --- a/packages/ui/src/github/action/index.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { StoryFn, Meta } from '@storybook/react'; -import { Action } from './index'; -import action from './data.json'; - -const meta = { - title: 'GitHub/Action', - component: Action, -} satisfies Meta; - -type Story = StoryFn; - -const Normal: Story = { - args: { - action: action, - onPress: () => {}, - }, -} as any; - -export { Normal }; -export default meta; diff --git a/packages/ui/src/github/action/index.tsx b/packages/ui/src/github/action/index.tsx deleted file mode 100644 index f8cfad1..0000000 --- a/packages/ui/src/github/action/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useCallback } from 'react'; -import { GithubTypes } from '@refocus/sdk'; -import { - IoCheckmarkDoneCircleOutline, - IoCloseCircleOutline, -} from 'react-icons/io5'; -import { RxTimer } from 'react-icons/rx'; -import { FiPlayCircle } from 'react-icons/fi'; -import { GoQuestion } from 'react-icons/go'; -import { Avatar, Card, View } from '../../base'; -import { Typography } from '../../typography'; - -type ActionProps = { - action: GithubTypes.WorkflowRun; - onPress?: (action: GithubTypes.WorkflowRun) => void; -}; - -const getIcon = (status: string | null) => { - switch (status) { - case 'success': - return ; - case 'failure': - return ; - case 'in_progress': - return ; - case 'queued': - return ; - default: - return ; - } -}; - -const Action: React.FC = ({ action, onPress }) => { - const onPressHandler = useCallback(() => { - onPress?.(action); - }, [action, onPress]); - return ( - - - - - {action.name} - {action.actor?.name || action.actor?.login} - - {action.display_title} - {action.status} - - {getIcon(action.status)} - - ); -}; - -export { Action }; diff --git a/packages/ui/src/github/index.ts b/packages/ui/src/github/index.ts index 0dfb8db..9d38f1d 100644 --- a/packages/ui/src/github/index.ts +++ b/packages/ui/src/github/index.ts @@ -3,3 +3,4 @@ export * from './profile'; export * from './pull-request'; export * from './login'; export * from './not-logged-in'; +export * from './workflow-run'; diff --git a/packages/ui/src/github/action/data.json b/packages/ui/src/github/workflow-run/data.json similarity index 100% rename from packages/ui/src/github/action/data.json rename to packages/ui/src/github/workflow-run/data.json diff --git a/packages/ui/src/github/workflow-run/index.stories.tsx b/packages/ui/src/github/workflow-run/index.stories.tsx new file mode 100644 index 0000000..44a9af4 --- /dev/null +++ b/packages/ui/src/github/workflow-run/index.stories.tsx @@ -0,0 +1,20 @@ +import type { StoryFn, Meta } from '@storybook/react'; +import { WorkflowRun } from './index'; +import workflowRun from './data.json'; + +const meta = { + title: 'GitHub/Workflow Run', + component: WorkflowRun, +} satisfies Meta; + +type Story = StoryFn; + +const Normal: Story = { + args: { + workflowRun, + onPress: () => {}, + }, +} as any; + +export { Normal }; +export default meta; diff --git a/packages/ui/src/github/workflow-run/index.tsx b/packages/ui/src/github/workflow-run/index.tsx new file mode 100644 index 0000000..65a9202 --- /dev/null +++ b/packages/ui/src/github/workflow-run/index.tsx @@ -0,0 +1,72 @@ +import { useCallback } from 'react'; +import { GithubTypes } from '@refocus/sdk'; +import { + IoCheckmarkDoneCircleOutline, + IoCloseCircleOutline, +} from 'react-icons/io5'; +import { RxTimer } from 'react-icons/rx'; +import { FiPlayCircle } from 'react-icons/fi'; +import { GoQuestion } from 'react-icons/go'; +import { Avatar, Card, View } from '../../base'; +import { Typography } from '../../typography'; + +type WorkflowRunProps = { + workflowRun: GithubTypes.WorkflowRun; + onPress?: (action: GithubTypes.WorkflowRun) => void; +}; + +const getIcon = (workflowRun: GithubTypes.WorkflowRun) => { + const { status, conclusion } = workflowRun; + if (status === 'completed' && conclusion === 'success') { + return ; + } else if (status === 'completed' && conclusion === 'failure') { + return ; + } else if (status === 'completed' && conclusion === 'cancelled') { + return ; + } else if (status === 'completed' && conclusion === 'skipped') { + return ; + } else if (status === 'completed' && conclusion === 'timed_out') { + return ; + } else if (status === 'completed' && conclusion === 'action_required') { + return ; + } else if (status === 'in_progress') { + return ; + } else if (status === 'queued') { + return ; + } else { + return ; + } +}; + +const WorkflowRun: React.FC = ({ workflowRun, onPress }) => { + const onPressHandler = useCallback(() => { + onPress?.(workflowRun); + }, [workflowRun, onPress]); + return ( + + + + + {workflowRun.repository.full_name} -{' '} + {workflowRun.actor?.name || workflowRun.actor?.login} + + {workflowRun.display_title} + {workflowRun.name} + + {getIcon(workflowRun)} + + ); +}; + +export { WorkflowRun }; diff --git a/packages/ui/src/interface/board/index.tsx b/packages/ui/src/interface/board/index.tsx index b8f4919..3e82c49 100644 --- a/packages/ui/src/interface/board/index.tsx +++ b/packages/ui/src/interface/board/index.tsx @@ -16,10 +16,7 @@ type BoardProps = { }; const ItemWrapper = styled(View)` - overflow-y: auto; - max-height: 500px; max-width: 100%; - box-shadow: 0 0 4px 0px ${({ theme }) => theme.colors.bg.highlight}; border-radius: ${({ theme }) => theme.radii.md}px; `; diff --git a/packages/ui/src/interface/widget/index.tsx b/packages/ui/src/interface/widget/index.tsx index bb9e6fd..df1a76e 100644 --- a/packages/ui/src/interface/widget/index.tsx +++ b/packages/ui/src/interface/widget/index.tsx @@ -3,15 +3,20 @@ import { WidgetEditor, WidgetProvider, WidgetView, + useHasUpdate, + useName, + useReloadWidget, useWidget, + useIsUpdating, } from '@refocus/sdk'; import { MdKeyboardArrowUp } from 'react-icons/md'; import { motion } from 'framer-motion'; import { VscTrash } from 'react-icons/vsc'; -import { CgMoreO } from 'react-icons/cg'; +import { CgMoreO, CgSync } from 'react-icons/cg'; import { Dialog, View } from '../../base'; import { DropdownMenu } from '../../base'; import { useCallback, useMemo, useState } from 'react'; +import { Typography } from '../../typography'; type WidgetProps = { id: string; @@ -23,18 +28,55 @@ type WidgetProps = { const Wrapper = styled(View)` background: ${({ theme }) => theme.colors.bg.base}; + box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight}; + margin: 5px; + border-radius: 5px; `; const WidgetWrapper = styled(View)` flex-grow: 0; overflow: hidden; flex: 1; + max-height: 500px; + overflow-y: auto; `; const Spacer = styled(View)` flex: 1; `; +const SingleLine = styled(Typography)` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +const Title: React.FC = () => { + const [name] = useName(); + return {name}; +}; + +const Update: React.FC = () => { + const hasUpdate = useHasUpdate(); + const reload = useReloadWidget(); + const updating = useIsUpdating(); + + if (!hasUpdate) { + return null; + } + + return ( + + + + + + ); +}; + const Widget: React.FC = ({ id, data, @@ -59,7 +101,7 @@ const Widget: React.FC = ({ ); return ( - + = ({ + <Spacer /> + <Update /> {hasMenu && ( <DropdownMenu> <DropdownMenu.Trigger> @@ -80,27 +124,32 @@ const Widget: React.FC<WidgetProps> = ({ </View> </DropdownMenu.Trigger> <DropdownMenu.Portal> - <DropdownMenu.Content alignOffset={50}> - {!!onRemove && ( - <DropdownMenu.Item onClick={onRemove}> - <DropdownMenu.Icon> - <VscTrash color={theme?.colors.simple.red} /> - </DropdownMenu.Icon> - Remove - </DropdownMenu.Item> - )} - {!!widget?.edit && !!setData && ( - <DropdownMenu.Item onClick={() => setShowEdit(true)}> - Edit - </DropdownMenu.Item> - )} - <DropdownMenu.Arrow /> - </DropdownMenu.Content> + <> + <DropdownMenu.Overlay /> + <DropdownMenu.Content alignOffset={50}> + {!!onRemove && ( + <DropdownMenu.Item onClick={onRemove}> + <DropdownMenu.Icon> + <VscTrash color={theme?.colors.simple.red} /> + </DropdownMenu.Icon> + Remove + </DropdownMenu.Item> + )} + {!!widget?.edit && !!setData && ( + <DropdownMenu.Item onClick={() => setShowEdit(true)}> + Edit + </DropdownMenu.Item> + )} + <DropdownMenu.Arrow /> + </DropdownMenu.Content> + </> </DropdownMenu.Portal> </DropdownMenu> )} </View> - <motion.div animate={{ height: open ? 'auto' : 0 }}> + <motion.div + animate={{ height: open ? 'auto' : 0, opacity: open ? 1 : 0 }} + > <Wrapper className={className} $fr> <WidgetWrapper $f={1}> <WidgetView /> @@ -108,13 +157,15 @@ const Widget: React.FC<WidgetProps> = ({ </Wrapper> </motion.div> <Dialog open={showEdit} onOpenChange={setShowEdit}> - <Dialog.Overlay /> - <Dialog.Content> - <Dialog.Title>Edit Widget</Dialog.Title> - <Dialog.Description> - <WidgetEditor onSave={onSave} /> - </Dialog.Description> - </Dialog.Content> + <Dialog.Portal> + <Dialog.Overlay /> + <Dialog.Content> + <Dialog.Title>Edit Widget</Dialog.Title> + <Dialog.Description> + <WidgetEditor onSave={onSave} /> + </Dialog.Description> + </Dialog.Content> + </Dialog.Portal> </Dialog> </WidgetProvider> ); diff --git a/packages/ui/src/theme/provider.tsx b/packages/ui/src/theme/provider.tsx index 32caf65..f41e115 100644 --- a/packages/ui/src/theme/provider.tsx +++ b/packages/ui/src/theme/provider.tsx @@ -6,7 +6,6 @@ type UIProviderProps = { }; // @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700&display=swap'); const GlobalStyle = createGlobalStyle` - * { box-sizing: border-box; overflow-wrap: break-word; @@ -24,6 +23,7 @@ const GlobalStyle = createGlobalStyle` body { background-color: ${({ theme }) => theme.colors.bg.base}; + background-image: linear-gradient(to right, rgb(38, 39, 54), rgb(25, 26, 35)); color: ${({ theme }) => theme.colors.text.base}; margin: 0; padding: 0; diff --git a/packages/widgets/src/github/index.ts b/packages/widgets/src/github/index.ts index 45383a6..68fefb9 100644 --- a/packages/widgets/src/github/index.ts +++ b/packages/widgets/src/github/index.ts @@ -2,11 +2,15 @@ import { Widget } from '@refocus/sdk'; import githubProfileWidget from './profile/index.widget'; import pullRequest from './pull-request/index.widget'; import pullRequstComments from './pull-request-comments/index.widget'; +import workflowRun from './workflow-run/index.widget'; +import workflowRuns from './workflow-runs/index.widget'; const github = [ githubProfileWidget, pullRequest, pullRequstComments, + workflowRun, + workflowRuns, ] satisfies Widget<any>[]; export { github }; diff --git a/packages/widgets/src/github/pull-request-comments/view.tsx b/packages/widgets/src/github/pull-request-comments/view.tsx index 230e7b2..3b2d8ab 100644 --- a/packages/widgets/src/github/pull-request-comments/view.tsx +++ b/packages/widgets/src/github/pull-request-comments/view.tsx @@ -3,6 +3,7 @@ import { useAutoUpdate, useGithubQuery, withGithub, + useName, } from '@refocus/sdk'; import { Chat, Github, List } from '@refocus/ui'; import { Props } from './schema'; @@ -16,12 +17,14 @@ type QueryData = { const View = withGithub<Props>(({ owner, repo, pr }) => { const addNotification = useAddWidgetNotification(); + const [, setName] = useName(); const { data, fetch } = useGithubQuery(async (client, params: QueryData) => { const response = await client.rest.pulls.listReviewComments({ owner: params.owner, repo: params.repo, pull_number: params.pr, }); + setName(`${params.owner}/${params.repo} #${params.pr}`); return response.data.slice(0, 5); }); diff --git a/packages/widgets/src/github/pull-request/view.tsx b/packages/widgets/src/github/pull-request/view.tsx index 13103d0..1aec92a 100644 --- a/packages/widgets/src/github/pull-request/view.tsx +++ b/packages/widgets/src/github/pull-request/view.tsx @@ -1,4 +1,9 @@ -import { useAutoUpdate, useGithubQuery, withGithub } from '@refocus/sdk'; +import { + useAutoUpdate, + useGithubQuery, + useName, + withGithub, +} from '@refocus/sdk'; import { Props } from './schema'; import { Github } from '@refocus/ui'; @@ -9,12 +14,14 @@ type QueryData = { }; const View = withGithub<Props>(({ owner, repo, pr }) => { + const [, setName] = useName(); const { data, fetch } = useGithubQuery(async (client, params: QueryData) => { const response = await client.rest.pulls.get({ owner: params.owner, repo: params.repo, pull_number: params.pr, }); + setName(`${params.owner}/${params.repo} #${params.pr}`); return response.data; }); diff --git a/packages/widgets/src/github/workflow-run/edit.tsx b/packages/widgets/src/github/workflow-run/edit.tsx new file mode 100644 index 0000000..ebe166e --- /dev/null +++ b/packages/widgets/src/github/workflow-run/edit.tsx @@ -0,0 +1,44 @@ +import { useCallback, useState } from 'react'; +import { Props } from './schema'; + +type EditorProps = { + value?: Props; + save: (data: Props) => void; +}; + +const Edit: React.FC<EditorProps> = ({ value, save }) => { + const [owner, setOwner] = useState(value?.owner || ''); + const [repo, setRepo] = useState(value?.repo || ''); + const [id, setId] = useState(value?.id || ''); + + const handleSave = useCallback(() => { + save({ + owner, + repo, + id: typeof id === 'string' ? parseInt(id, 10) : id, + }); + }, [owner, repo, id, save]); + + return ( + <div> + <input + placeholder="Owner" + value={owner} + onChange={(e) => setOwner(e.target.value)} + /> + <input + placeholder="Repo" + value={repo} + onChange={(e) => setRepo(e.target.value)} + /> + <input + placeholder="PR" + value={id} + onChange={(e) => setId(e.target.value)} + /> + <button onClick={handleSave}>Save</button> + </div> + ); +}; + +export { Edit }; diff --git a/packages/widgets/src/github/workflow-run/index.widget.tsx b/packages/widgets/src/github/workflow-run/index.widget.tsx new file mode 100644 index 0000000..d020ebc --- /dev/null +++ b/packages/widgets/src/github/workflow-run/index.widget.tsx @@ -0,0 +1,28 @@ +import { Widget } from '@refocus/sdk'; +import { SiGithub } from 'react-icons/si'; +import { schema } from './schema'; +import { Edit } from './edit'; +import { View } from './view'; + +const widget: Widget<typeof schema> = { + name: 'Github Workflow Run', + description: 'Display information about a specific workflow run.', + icon: <SiGithub />, + id: 'github.workflow-run', + parseUrl: (url) => { + if (url.hostname !== 'github.com') { + return; + } + const pathParts = url.pathname.split('/').filter(Boolean); + const [owner, repo, type, subtype, id] = pathParts.slice(0); + if (type !== 'actions' || subtype !== 'runs' || !id) { + return; + } + return { owner, repo, id: parseInt(id, 10) }; + }, + schema, + component: View, + edit: Edit, +}; + +export default widget; diff --git a/packages/widgets/src/github/workflow-run/schema.ts b/packages/widgets/src/github/workflow-run/schema.ts new file mode 100644 index 0000000..5e49748 --- /dev/null +++ b/packages/widgets/src/github/workflow-run/schema.ts @@ -0,0 +1,12 @@ +import { Type, Static } from '@sinclair/typebox'; + +const schema = Type.Object({ + owner: Type.String(), + repo: Type.String(), + id: Type.Number(), +}); + +type Props = Static<typeof schema>; + +export type { Props }; +export { schema }; diff --git a/packages/widgets/src/github/workflow-run/view.tsx b/packages/widgets/src/github/workflow-run/view.tsx new file mode 100644 index 0000000..66f53a3 --- /dev/null +++ b/packages/widgets/src/github/workflow-run/view.tsx @@ -0,0 +1,53 @@ +import { + useAutoUpdate, + useGithubQuery, + useName, + withGithub, +} from '@refocus/sdk'; +import { Props } from './schema'; +import { Github } from '@refocus/ui'; + +type QueryData = { + id: number; + owner: string; + repo: string; +}; + +const WidgetView = withGithub<Props>(({ owner, repo, id }) => { + const [, setName] = useName(); + const { data, fetch } = useGithubQuery(async (client, params: QueryData) => { + const response = await client.rest.actions.getWorkflowRun({ + owner: params.owner, + repo: params.repo, + run_id: params.id, + }); + setName(`${response.data.repository.full_name} ${response.data.name}`); + return response.data; + }); + + useAutoUpdate( + { + interval: 1000 * 60 * 5, + action: async () => fetch({ owner, repo, id }), + }, + [owner, repo, id], + ); + + if (!data) { + return null; + } + + return ( + <Github.WorkflowRun + workflowRun={data} + onPress={() => + window.open( + `https://github.com/${owner}/${repo}/actions/runs/${id}`, + '_blank', + ) + } + /> + ); +}, Github.NotLoggedIn); + +export { WidgetView as View }; diff --git a/packages/widgets/src/github/workflow-runs/edit.tsx b/packages/widgets/src/github/workflow-runs/edit.tsx new file mode 100644 index 0000000..6e5b0ad --- /dev/null +++ b/packages/widgets/src/github/workflow-runs/edit.tsx @@ -0,0 +1,37 @@ +import { useCallback, useState } from 'react'; +import { Props } from './schema'; + +type EditorProps = { + value?: Props; + save: (data: Props) => void; +}; + +const Edit: React.FC<EditorProps> = ({ value, save }) => { + const [owner, setOwner] = useState(value?.owner || ''); + const [repo, setRepo] = useState(value?.repo || ''); + + const handleSave = useCallback(() => { + save({ + owner, + repo, + }); + }, [owner, repo, save]); + + return ( + <div> + <input + placeholder="Owner" + value={owner} + onChange={(e) => setOwner(e.target.value)} + /> + <input + placeholder="Repo" + value={repo} + onChange={(e) => setRepo(e.target.value)} + /> + <button onClick={handleSave}>Save</button> + </div> + ); +}; + +export { Edit }; diff --git a/packages/widgets/src/github/workflow-runs/index.widget.tsx b/packages/widgets/src/github/workflow-runs/index.widget.tsx new file mode 100644 index 0000000..064be9c --- /dev/null +++ b/packages/widgets/src/github/workflow-runs/index.widget.tsx @@ -0,0 +1,28 @@ +import { Widget } from '@refocus/sdk'; +import { SiGithub } from 'react-icons/si'; +import { schema } from './schema'; +import { Edit } from './edit'; +import { View } from './view'; + +const widget: Widget<typeof schema> = { + name: 'Github Workflow Runs', + description: 'Display the last 5 workflow runs', + icon: <SiGithub />, + id: 'github.workflow-runs', + parseUrl: (url) => { + if (url.hostname !== 'github.com') { + return; + } + const pathParts = url.pathname.split('/').filter(Boolean); + const [owner, repo, type, subtype] = pathParts.slice(0); + if (type !== 'actions' || subtype) { + return; + } + return { owner, repo }; + }, + schema, + component: View, + edit: Edit, +}; + +export default widget; diff --git a/packages/widgets/src/github/workflow-runs/schema.ts b/packages/widgets/src/github/workflow-runs/schema.ts new file mode 100644 index 0000000..f82a253 --- /dev/null +++ b/packages/widgets/src/github/workflow-runs/schema.ts @@ -0,0 +1,11 @@ +import { Type, Static } from '@sinclair/typebox'; + +const schema = Type.Object({ + owner: Type.String(), + repo: Type.String(), +}); + +type Props = Static<typeof schema>; + +export type { Props }; +export { schema }; diff --git a/packages/widgets/src/github/workflow-runs/view.tsx b/packages/widgets/src/github/workflow-runs/view.tsx new file mode 100644 index 0000000..de87646 --- /dev/null +++ b/packages/widgets/src/github/workflow-runs/view.tsx @@ -0,0 +1,56 @@ +import { + useAutoUpdate, + useGithubQuery, + useName, + withGithub, +} from '@refocus/sdk'; +import { Props } from './schema'; +import { Github, List } from '@refocus/ui'; + +type QueryData = { + owner: string; + repo: string; +}; + +const WidgetView = withGithub<Props>(({ owner, repo }) => { + const [, setName] = useName(); + const { data, fetch } = useGithubQuery(async (client, params: QueryData) => { + const response = await client.rest.actions.listWorkflowRunsForRepo({ + owner: params.owner, + repo: params.repo, + }); + setName(`${params.owner}/${params.repo} workflow runs`); + return response.data.workflow_runs.slice(0, 5); + }); + console.log(data); + + useAutoUpdate( + { + interval: 1000 * 60 * 5, + action: async () => fetch({ owner, repo }), + }, + [owner, repo], + ); + + if (!data) { + return null; + } + + return ( + <List> + {data?.map((run) => ( + <Github.WorkflowRun + workflowRun={run} + onPress={() => + window.open( + `https://github.com/${owner}/${repo}/actions/runs/${run.id}`, + '_blank', + ) + } + /> + ))} + </List> + ); +}, Github.NotLoggedIn); + +export { WidgetView as View }; diff --git a/packages/widgets/src/linear/issue-with-comments/view.tsx b/packages/widgets/src/linear/issue-with-comments/view.tsx index b847386..a7f01ed 100644 --- a/packages/widgets/src/linear/issue-with-comments/view.tsx +++ b/packages/widgets/src/linear/issue-with-comments/view.tsx @@ -2,6 +2,7 @@ import { useAddWidgetNotification, useAutoUpdate, useLinearQuery, + useName, withLinear, } from '@refocus/sdk'; import { Chat, Linear, List, View } from '@refocus/ui'; @@ -13,9 +14,11 @@ type LinearIssueProps = { const WidgetView = withLinear<LinearIssueProps>(({ id }) => { const addNotification = useAddWidgetNotification(); + const [, setName] = useName(); const { data, fetch } = useLinearQuery(async (client) => { const issue = await client.issue(id); const comments = await issue.comments(); + setName(id); return comments.nodes; }); diff --git a/packages/widgets/src/slack/conversation/view.tsx b/packages/widgets/src/slack/conversation/view.tsx index 7e9fbd4..9954fb7 100644 --- a/packages/widgets/src/slack/conversation/view.tsx +++ b/packages/widgets/src/slack/conversation/view.tsx @@ -1,9 +1,11 @@ import { useAddWidgetNotification, useAutoUpdate, + useName, useSlackQuery, withSlack, } from '@refocus/sdk'; +import styled from 'styled-components'; import { Props } from './schema'; import { Message } from './message/view'; import { useState } from 'react'; @@ -13,7 +15,24 @@ type PostMessageOptions = { message: string; }; +const MessageList = styled(View)` + transform: scaleY(-1); + flex: 1; + overflow-y: auto; + flex-grow: 1; + flex-shrink: 1; + + & > * { + transform: scaleY(-1); + } +`; + +const Wrapper = styled(View)` + max-height: 100%; + overflow: hidden; +`; const WidgetView = withSlack<Props>(({ conversationId }) => { + const [, setName] = useName(); const addNotification = useAddWidgetNotification(); const [message, setMessage] = useState(''); const { fetch, data } = useSlackQuery(async (client, props: Props) => { @@ -27,6 +46,7 @@ const WidgetView = withSlack<Props>(({ conversationId }) => { const response = await client.send('conversations.info', { channel: props.conversationId, }); + setName(response.channel!.name || 'Direct message'); return response.channel!; }); @@ -39,7 +59,7 @@ const WidgetView = withSlack<Props>(({ conversationId }) => { }, ); - const update = useAutoUpdate( + useAutoUpdate( { action: async () => { await info.fetch({ conversationId }); @@ -68,17 +88,8 @@ const WidgetView = withSlack<Props>(({ conversationId }) => { ); return ( - <View $p="md"> - <button onClick={update}>Update</button> - <Typography variant="header"> - {info.data?.name || 'Direct message'} - </Typography> - <Chat.Compose - value={message} - onValueChange={setMessage} - onSend={() => post({ message })} - /> - <List> + <Wrapper $p="sm" $fc $gap="sm"> + <MessageList $gap="md" $fc> {data?.map((message) => ( <Message key={message.ts} @@ -86,8 +97,13 @@ const WidgetView = withSlack<Props>(({ conversationId }) => { userId={message.user} /> ))} - </List> - </View> + </MessageList> + <Chat.Compose + value={message} + onValueChange={setMessage} + onSend={() => post({ message })} + /> + </Wrapper> ); }, Slack.NotLoggedIn);