mirror of
https://github.com/morten-olsen/refocus.dev.git
synced 2026-02-08 00:46:25 +01:00
fix: improved UI
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { useUpdateEffect } from '../widgets';
|
||||||
|
|
||||||
type AutoUpdateOptions<TReturn> = {
|
type AutoUpdateOptions<TReturn> = {
|
||||||
interval: number;
|
interval: number;
|
||||||
@@ -36,6 +37,10 @@ const useAutoUpdate = <T>(
|
|||||||
};
|
};
|
||||||
}, [interval, actionWithCallback, callbackWithCallback]);
|
}, [interval, actionWithCallback, callbackWithCallback]);
|
||||||
|
|
||||||
|
useUpdateEffect(async () => {
|
||||||
|
await update();
|
||||||
|
}, [update]);
|
||||||
|
|
||||||
return update;
|
return update;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { WidgetsContext } from './context';
|
import { WidgetsContext } from './context';
|
||||||
import { WidgetContext } from './widget-context';
|
import { WidgetContext } from './widget-context';
|
||||||
|
|
||||||
@@ -111,9 +111,58 @@ const useWidgetId = () => {
|
|||||||
return context.id;
|
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<void>, 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 {
|
export {
|
||||||
useWidget,
|
useWidget,
|
||||||
useWidgets,
|
useWidgets,
|
||||||
|
useName,
|
||||||
|
useUpdateEffect,
|
||||||
|
useHasUpdate,
|
||||||
|
useReloadWidget,
|
||||||
useGetWidgetsFromUrl,
|
useGetWidgetsFromUrl,
|
||||||
useWidgetNotifications,
|
useWidgetNotifications,
|
||||||
useDismissWidgetNotification,
|
useDismissWidgetNotification,
|
||||||
@@ -121,4 +170,5 @@ export {
|
|||||||
useWidgetData,
|
useWidgetData,
|
||||||
useSetWidgetData,
|
useSetWidgetData,
|
||||||
useWidgetId,
|
useWidgetId,
|
||||||
|
useIsUpdating,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ export {
|
|||||||
useWidgetId,
|
useWidgetId,
|
||||||
useWidgetData,
|
useWidgetData,
|
||||||
useSetWidgetData,
|
useSetWidgetData,
|
||||||
|
useName,
|
||||||
|
useUpdateEffect,
|
||||||
|
useReloadWidget,
|
||||||
|
useHasUpdate,
|
||||||
|
useIsUpdating,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
export { WidgetProvider } from './widget-context';
|
export { WidgetProvider } from './widget-context';
|
||||||
export { WidgetView } from './view';
|
export { WidgetView } from './view';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, useCallback, useMemo, useRef } from 'react';
|
import { createContext, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Notification as BaseNotification,
|
Notification as BaseNotification,
|
||||||
useNotificationAdd,
|
useNotificationAdd,
|
||||||
@@ -14,8 +14,16 @@ type WidgetContextValue = {
|
|||||||
dismissNotification: (id: string) => void;
|
dismissNotification: (id: string) => void;
|
||||||
notifications: Notification[];
|
notifications: Notification[];
|
||||||
setData?: (data: any) => void;
|
setData?: (data: any) => void;
|
||||||
|
addUpdater: (updater: Updater) => () => void;
|
||||||
|
updating: boolean;
|
||||||
|
hasUpdater: boolean;
|
||||||
|
name: string;
|
||||||
|
setName: (name: string) => void;
|
||||||
|
updateWidget: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Updater = () => Promise<void> | void;
|
||||||
|
|
||||||
type WidgetProviderProps = {
|
type WidgetProviderProps = {
|
||||||
id: string;
|
id: string;
|
||||||
data?: any;
|
data?: any;
|
||||||
@@ -32,6 +40,9 @@ const WidgetProvider = ({
|
|||||||
children,
|
children,
|
||||||
}: WidgetProviderProps) => {
|
}: WidgetProviderProps) => {
|
||||||
const ref = useRef(Symbol('WidgetRender'));
|
const ref = useRef(Symbol('WidgetRender'));
|
||||||
|
const [updating, setUpdating] = useState(false);
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [updaters, setUpdaters] = useState<Updater[]>([]);
|
||||||
const globalNotifications = useNotifications();
|
const globalNotifications = useNotifications();
|
||||||
const addGlobalNotification = useNotificationAdd();
|
const addGlobalNotification = useNotificationAdd();
|
||||||
const dissmissGlobalNotification = useNotificationDismiss();
|
const dissmissGlobalNotification = useNotificationDismiss();
|
||||||
@@ -46,6 +57,16 @@ const WidgetProvider = ({
|
|||||||
[addGlobalNotification],
|
[addGlobalNotification],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addUpdater = useCallback(
|
||||||
|
(updater: Updater) => {
|
||||||
|
setUpdaters((prev) => [...prev, updater]);
|
||||||
|
return () => {
|
||||||
|
setUpdaters((prev) => prev.filter((u) => u !== updater));
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[setUpdaters],
|
||||||
|
);
|
||||||
|
|
||||||
const dismissNotification = useCallback(
|
const dismissNotification = useCallback(
|
||||||
(dismissId: string) => {
|
(dismissId: string) => {
|
||||||
dissmissGlobalNotification(dismissId);
|
dissmissGlobalNotification(dismissId);
|
||||||
@@ -53,6 +74,18 @@ const WidgetProvider = ({
|
|||||||
[dissmissGlobalNotification],
|
[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(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
addNotification,
|
addNotification,
|
||||||
@@ -61,8 +94,27 @@ const WidgetProvider = ({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
setData,
|
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 (
|
return (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const Overlay = styled(DialogPrimitives.Overlay)`
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Portal = styled(DialogPrimitives.Portal)``;
|
const Portal = styled(DialogPrimitives.Portal)``;
|
||||||
@@ -30,6 +31,7 @@ const Content = styled(DialogPrimitives.Content)`
|
|||||||
max-width: 450px;
|
max-width: 450px;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
|
box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Title = styled(DialogPrimitives.Title)`
|
const Title = styled(DialogPrimitives.Title)`
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as DropdownMenuPrimitives from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenuPrimitives from '@radix-ui/react-dropdown-menu';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
import { styled, css } from 'styled-components';
|
import { styled, css } from 'styled-components';
|
||||||
|
|
||||||
const RightSlot = styled.div`
|
const RightSlot = styled.div`
|
||||||
@@ -62,6 +63,17 @@ const Trigger = styled(DropdownMenuPrimitives.Trigger)`
|
|||||||
|
|
||||||
const Portal = styled(DropdownMenuPrimitives.Portal)``;
|
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 = () => (
|
||||||
|
<OverlayComponent initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
|
||||||
|
);
|
||||||
|
|
||||||
const Item = styled(DropdownMenuPrimitives.Item)`
|
const Item = styled(DropdownMenuPrimitives.Item)`
|
||||||
${item}
|
${item}
|
||||||
`;
|
`;
|
||||||
@@ -147,6 +159,7 @@ const DropdownMenu = Object.assign(Root, {
|
|||||||
Item,
|
Item,
|
||||||
Sub,
|
Sub,
|
||||||
SubTrigger,
|
SubTrigger,
|
||||||
|
Overlay,
|
||||||
SubContent,
|
SubContent,
|
||||||
Separator,
|
Separator,
|
||||||
CheckboxItem,
|
CheckboxItem,
|
||||||
|
|||||||
@@ -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<typeof Action>;
|
|
||||||
|
|
||||||
type Story = StoryFn<typeof Action>;
|
|
||||||
|
|
||||||
const Normal: Story = {
|
|
||||||
args: {
|
|
||||||
action: action,
|
|
||||||
onPress: () => {},
|
|
||||||
},
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
export { Normal };
|
|
||||||
export default meta;
|
|
||||||
@@ -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 <IoCheckmarkDoneCircleOutline size={48} color="green" />;
|
|
||||||
case 'failure':
|
|
||||||
return <IoCloseCircleOutline size={48} color="red" />;
|
|
||||||
case 'in_progress':
|
|
||||||
return <FiPlayCircle size={48} />;
|
|
||||||
case 'queued':
|
|
||||||
return <RxTimer size={48} />;
|
|
||||||
default:
|
|
||||||
return <GoQuestion size={48} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Action: React.FC<ActionProps> = ({ action, onPress }) => {
|
|
||||||
const onPressHandler = useCallback(() => {
|
|
||||||
onPress?.(action);
|
|
||||||
}, [action, onPress]);
|
|
||||||
return (
|
|
||||||
<Card $fr $items="center" $p="md" $gap="md" onClick={onPressHandler}>
|
|
||||||
<Avatar
|
|
||||||
url={action.actor?.avatar_url}
|
|
||||||
name={action.actor?.name || action.actor?.login}
|
|
||||||
decal={`#${action.run_attempt}`}
|
|
||||||
/>
|
|
||||||
<View $fc $f={1}>
|
|
||||||
<Typography variant="overline">
|
|
||||||
{action.name} - {action.actor?.name || action.actor?.login}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="title">{action.display_title}</Typography>
|
|
||||||
<Typography variant="subtitle">{action.status}</Typography>
|
|
||||||
</View>
|
|
||||||
<View>{getIcon(action.status)}</View>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Action };
|
|
||||||
@@ -3,3 +3,4 @@ export * from './profile';
|
|||||||
export * from './pull-request';
|
export * from './pull-request';
|
||||||
export * from './login';
|
export * from './login';
|
||||||
export * from './not-logged-in';
|
export * from './not-logged-in';
|
||||||
|
export * from './workflow-run';
|
||||||
|
|||||||
20
packages/ui/src/github/workflow-run/index.stories.tsx
Normal file
20
packages/ui/src/github/workflow-run/index.stories.tsx
Normal file
@@ -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<typeof WorkflowRun>;
|
||||||
|
|
||||||
|
type Story = StoryFn<typeof WorkflowRun>;
|
||||||
|
|
||||||
|
const Normal: Story = {
|
||||||
|
args: {
|
||||||
|
workflowRun,
|
||||||
|
onPress: () => {},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export { Normal };
|
||||||
|
export default meta;
|
||||||
72
packages/ui/src/github/workflow-run/index.tsx
Normal file
72
packages/ui/src/github/workflow-run/index.tsx
Normal file
@@ -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 <IoCheckmarkDoneCircleOutline size={48} color="green" />;
|
||||||
|
} else if (status === 'completed' && conclusion === 'failure') {
|
||||||
|
return <IoCloseCircleOutline size={48} color="red" />;
|
||||||
|
} else if (status === 'completed' && conclusion === 'cancelled') {
|
||||||
|
return <IoCloseCircleOutline size={48} color="yellow" />;
|
||||||
|
} else if (status === 'completed' && conclusion === 'skipped') {
|
||||||
|
return <IoCloseCircleOutline size={48} color="gray" />;
|
||||||
|
} else if (status === 'completed' && conclusion === 'timed_out') {
|
||||||
|
return <IoCloseCircleOutline size={48} color="gray" />;
|
||||||
|
} else if (status === 'completed' && conclusion === 'action_required') {
|
||||||
|
return <IoCloseCircleOutline size={48} color="gray" />;
|
||||||
|
} else if (status === 'in_progress') {
|
||||||
|
return <FiPlayCircle size={48} />;
|
||||||
|
} else if (status === 'queued') {
|
||||||
|
return <RxTimer size={48} />;
|
||||||
|
} else {
|
||||||
|
return <GoQuestion size={48} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowRun: React.FC<WorkflowRunProps> = ({ workflowRun, onPress }) => {
|
||||||
|
const onPressHandler = useCallback(() => {
|
||||||
|
onPress?.(workflowRun);
|
||||||
|
}, [workflowRun, onPress]);
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
$fr
|
||||||
|
$items="center"
|
||||||
|
$p="md"
|
||||||
|
$gap="md"
|
||||||
|
onClick={onPressHandler}
|
||||||
|
$m="sm"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
url={workflowRun.actor?.avatar_url}
|
||||||
|
name={workflowRun.actor?.name || workflowRun.actor?.login}
|
||||||
|
decal={`#${workflowRun.run_attempt}`}
|
||||||
|
/>
|
||||||
|
<View $fc $f={1}>
|
||||||
|
<Typography variant="overline">
|
||||||
|
{workflowRun.repository.full_name} -{' '}
|
||||||
|
{workflowRun.actor?.name || workflowRun.actor?.login}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="title">{workflowRun.display_title}</Typography>
|
||||||
|
<Typography variant="subtitle">{workflowRun.name}</Typography>
|
||||||
|
</View>
|
||||||
|
<View>{getIcon(workflowRun)}</View>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WorkflowRun };
|
||||||
@@ -16,10 +16,7 @@ type BoardProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ItemWrapper = styled(View)`
|
const ItemWrapper = styled(View)`
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 500px;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
box-shadow: 0 0 4px 0px ${({ theme }) => theme.colors.bg.highlight};
|
|
||||||
border-radius: ${({ theme }) => theme.radii.md}px;
|
border-radius: ${({ theme }) => theme.radii.md}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ import {
|
|||||||
WidgetEditor,
|
WidgetEditor,
|
||||||
WidgetProvider,
|
WidgetProvider,
|
||||||
WidgetView,
|
WidgetView,
|
||||||
|
useHasUpdate,
|
||||||
|
useName,
|
||||||
|
useReloadWidget,
|
||||||
useWidget,
|
useWidget,
|
||||||
|
useIsUpdating,
|
||||||
} from '@refocus/sdk';
|
} from '@refocus/sdk';
|
||||||
import { MdKeyboardArrowUp } from 'react-icons/md';
|
import { MdKeyboardArrowUp } from 'react-icons/md';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { VscTrash } from 'react-icons/vsc';
|
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 { Dialog, View } from '../../base';
|
||||||
import { DropdownMenu } from '../../base';
|
import { DropdownMenu } from '../../base';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { Typography } from '../../typography';
|
||||||
|
|
||||||
type WidgetProps = {
|
type WidgetProps = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -23,18 +28,55 @@ type WidgetProps = {
|
|||||||
|
|
||||||
const Wrapper = styled(View)`
|
const Wrapper = styled(View)`
|
||||||
background: ${({ theme }) => theme.colors.bg.base};
|
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)`
|
const WidgetWrapper = styled(View)`
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Spacer = styled(View)`
|
const Spacer = styled(View)`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const SingleLine = styled(Typography)`
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title: React.FC = () => {
|
||||||
|
const [name] = useName();
|
||||||
|
return <SingleLine variant="overline">{name}</SingleLine>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Update: React.FC = () => {
|
||||||
|
const hasUpdate = useHasUpdate();
|
||||||
|
const reload = useReloadWidget();
|
||||||
|
const updating = useIsUpdating();
|
||||||
|
|
||||||
|
if (!hasUpdate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: updating ? 360 : 0 }}
|
||||||
|
transition={{ duration: 1, loop: Infinity }}
|
||||||
|
>
|
||||||
|
<View $p="sm" onClick={reload}>
|
||||||
|
<CgSync />
|
||||||
|
</View>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Widget: React.FC<WidgetProps> = ({
|
const Widget: React.FC<WidgetProps> = ({
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
@@ -59,7 +101,7 @@ const Widget: React.FC<WidgetProps> = ({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<WidgetProvider id={id} data={data} setData={setData}>
|
<WidgetProvider id={id} data={data} setData={setData}>
|
||||||
<View $fr>
|
<View $fr $items="center">
|
||||||
<motion.div animate={{ rotate: open ? 180 : 0 }}>
|
<motion.div animate={{ rotate: open ? 180 : 0 }}>
|
||||||
<View
|
<View
|
||||||
$items="center"
|
$items="center"
|
||||||
@@ -71,7 +113,9 @@ const Widget: React.FC<WidgetProps> = ({
|
|||||||
<MdKeyboardArrowUp size={22} />
|
<MdKeyboardArrowUp size={22} />
|
||||||
</View>
|
</View>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
<Title />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
<Update />
|
||||||
{hasMenu && (
|
{hasMenu && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
@@ -80,27 +124,32 @@ const Widget: React.FC<WidgetProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content alignOffset={50}>
|
<>
|
||||||
{!!onRemove && (
|
<DropdownMenu.Overlay />
|
||||||
<DropdownMenu.Item onClick={onRemove}>
|
<DropdownMenu.Content alignOffset={50}>
|
||||||
<DropdownMenu.Icon>
|
{!!onRemove && (
|
||||||
<VscTrash color={theme?.colors.simple.red} />
|
<DropdownMenu.Item onClick={onRemove}>
|
||||||
</DropdownMenu.Icon>
|
<DropdownMenu.Icon>
|
||||||
Remove
|
<VscTrash color={theme?.colors.simple.red} />
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Icon>
|
||||||
)}
|
Remove
|
||||||
{!!widget?.edit && !!setData && (
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={() => setShowEdit(true)}>
|
)}
|
||||||
Edit
|
{!!widget?.edit && !!setData && (
|
||||||
</DropdownMenu.Item>
|
<DropdownMenu.Item onClick={() => setShowEdit(true)}>
|
||||||
)}
|
Edit
|
||||||
<DropdownMenu.Arrow />
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
)}
|
||||||
|
<DropdownMenu.Arrow />
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<motion.div animate={{ height: open ? 'auto' : 0 }}>
|
<motion.div
|
||||||
|
animate={{ height: open ? 'auto' : 0, opacity: open ? 1 : 0 }}
|
||||||
|
>
|
||||||
<Wrapper className={className} $fr>
|
<Wrapper className={className} $fr>
|
||||||
<WidgetWrapper $f={1}>
|
<WidgetWrapper $f={1}>
|
||||||
<WidgetView />
|
<WidgetView />
|
||||||
@@ -108,13 +157,15 @@ const Widget: React.FC<WidgetProps> = ({
|
|||||||
</Wrapper>
|
</Wrapper>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<Dialog open={showEdit} onOpenChange={setShowEdit}>
|
<Dialog open={showEdit} onOpenChange={setShowEdit}>
|
||||||
<Dialog.Overlay />
|
<Dialog.Portal>
|
||||||
<Dialog.Content>
|
<Dialog.Overlay />
|
||||||
<Dialog.Title>Edit Widget</Dialog.Title>
|
<Dialog.Content>
|
||||||
<Dialog.Description>
|
<Dialog.Title>Edit Widget</Dialog.Title>
|
||||||
<WidgetEditor onSave={onSave} />
|
<Dialog.Description>
|
||||||
</Dialog.Description>
|
<WidgetEditor onSave={onSave} />
|
||||||
</Dialog.Content>
|
</Dialog.Description>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</WidgetProvider>
|
</WidgetProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ type UIProviderProps = {
|
|||||||
};
|
};
|
||||||
// @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700&display=swap');
|
// @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700&display=swap');
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
@@ -24,6 +23,7 @@ const GlobalStyle = createGlobalStyle`
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: ${({ theme }) => theme.colors.bg.base};
|
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};
|
color: ${({ theme }) => theme.colors.text.base};
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import { Widget } from '@refocus/sdk';
|
|||||||
import githubProfileWidget from './profile/index.widget';
|
import githubProfileWidget from './profile/index.widget';
|
||||||
import pullRequest from './pull-request/index.widget';
|
import pullRequest from './pull-request/index.widget';
|
||||||
import pullRequstComments from './pull-request-comments/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 = [
|
const github = [
|
||||||
githubProfileWidget,
|
githubProfileWidget,
|
||||||
pullRequest,
|
pullRequest,
|
||||||
pullRequstComments,
|
pullRequstComments,
|
||||||
|
workflowRun,
|
||||||
|
workflowRuns,
|
||||||
] satisfies Widget<any>[];
|
] satisfies Widget<any>[];
|
||||||
|
|
||||||
export { github };
|
export { github };
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
useAutoUpdate,
|
useAutoUpdate,
|
||||||
useGithubQuery,
|
useGithubQuery,
|
||||||
withGithub,
|
withGithub,
|
||||||
|
useName,
|
||||||
} from '@refocus/sdk';
|
} from '@refocus/sdk';
|
||||||
import { Chat, Github, List } from '@refocus/ui';
|
import { Chat, Github, List } from '@refocus/ui';
|
||||||
import { Props } from './schema';
|
import { Props } from './schema';
|
||||||
@@ -16,12 +17,14 @@ type QueryData = {
|
|||||||
|
|
||||||
const View = withGithub<Props>(({ owner, repo, pr }) => {
|
const View = withGithub<Props>(({ owner, repo, pr }) => {
|
||||||
const addNotification = useAddWidgetNotification();
|
const addNotification = useAddWidgetNotification();
|
||||||
|
const [, setName] = useName();
|
||||||
const { data, fetch } = useGithubQuery(async (client, params: QueryData) => {
|
const { data, fetch } = useGithubQuery(async (client, params: QueryData) => {
|
||||||
const response = await client.rest.pulls.listReviewComments({
|
const response = await client.rest.pulls.listReviewComments({
|
||||||
owner: params.owner,
|
owner: params.owner,
|
||||||
repo: params.repo,
|
repo: params.repo,
|
||||||
pull_number: params.pr,
|
pull_number: params.pr,
|
||||||
});
|
});
|
||||||
|
setName(`${params.owner}/${params.repo} #${params.pr}`);
|
||||||
return response.data.slice(0, 5);
|
return response.data.slice(0, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { useAutoUpdate, useGithubQuery, withGithub } from '@refocus/sdk';
|
import {
|
||||||
|
useAutoUpdate,
|
||||||
|
useGithubQuery,
|
||||||
|
useName,
|
||||||
|
withGithub,
|
||||||
|
} from '@refocus/sdk';
|
||||||
import { Props } from './schema';
|
import { Props } from './schema';
|
||||||
import { Github } from '@refocus/ui';
|
import { Github } from '@refocus/ui';
|
||||||
|
|
||||||
@@ -9,12 +14,14 @@ type QueryData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const View = withGithub<Props>(({ owner, repo, pr }) => {
|
const View = withGithub<Props>(({ owner, repo, pr }) => {
|
||||||
|
const [, setName] = useName();
|
||||||
const { data, fetch } = useGithubQuery(async (client, params: QueryData) => {
|
const { data, fetch } = useGithubQuery(async (client, params: QueryData) => {
|
||||||
const response = await client.rest.pulls.get({
|
const response = await client.rest.pulls.get({
|
||||||
owner: params.owner,
|
owner: params.owner,
|
||||||
repo: params.repo,
|
repo: params.repo,
|
||||||
pull_number: params.pr,
|
pull_number: params.pr,
|
||||||
});
|
});
|
||||||
|
setName(`${params.owner}/${params.repo} #${params.pr}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
44
packages/widgets/src/github/workflow-run/edit.tsx
Normal file
44
packages/widgets/src/github/workflow-run/edit.tsx
Normal file
@@ -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 };
|
||||||
28
packages/widgets/src/github/workflow-run/index.widget.tsx
Normal file
28
packages/widgets/src/github/workflow-run/index.widget.tsx
Normal file
@@ -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;
|
||||||
12
packages/widgets/src/github/workflow-run/schema.ts
Normal file
12
packages/widgets/src/github/workflow-run/schema.ts
Normal file
@@ -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 };
|
||||||
53
packages/widgets/src/github/workflow-run/view.tsx
Normal file
53
packages/widgets/src/github/workflow-run/view.tsx
Normal file
@@ -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 };
|
||||||
37
packages/widgets/src/github/workflow-runs/edit.tsx
Normal file
37
packages/widgets/src/github/workflow-runs/edit.tsx
Normal file
@@ -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 };
|
||||||
28
packages/widgets/src/github/workflow-runs/index.widget.tsx
Normal file
28
packages/widgets/src/github/workflow-runs/index.widget.tsx
Normal file
@@ -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;
|
||||||
11
packages/widgets/src/github/workflow-runs/schema.ts
Normal file
11
packages/widgets/src/github/workflow-runs/schema.ts
Normal file
@@ -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 };
|
||||||
56
packages/widgets/src/github/workflow-runs/view.tsx
Normal file
56
packages/widgets/src/github/workflow-runs/view.tsx
Normal file
@@ -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 };
|
||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
useAddWidgetNotification,
|
useAddWidgetNotification,
|
||||||
useAutoUpdate,
|
useAutoUpdate,
|
||||||
useLinearQuery,
|
useLinearQuery,
|
||||||
|
useName,
|
||||||
withLinear,
|
withLinear,
|
||||||
} from '@refocus/sdk';
|
} from '@refocus/sdk';
|
||||||
import { Chat, Linear, List, View } from '@refocus/ui';
|
import { Chat, Linear, List, View } from '@refocus/ui';
|
||||||
@@ -13,9 +14,11 @@ type LinearIssueProps = {
|
|||||||
|
|
||||||
const WidgetView = withLinear<LinearIssueProps>(({ id }) => {
|
const WidgetView = withLinear<LinearIssueProps>(({ id }) => {
|
||||||
const addNotification = useAddWidgetNotification();
|
const addNotification = useAddWidgetNotification();
|
||||||
|
const [, setName] = useName();
|
||||||
const { data, fetch } = useLinearQuery(async (client) => {
|
const { data, fetch } = useLinearQuery(async (client) => {
|
||||||
const issue = await client.issue(id);
|
const issue = await client.issue(id);
|
||||||
const comments = await issue.comments();
|
const comments = await issue.comments();
|
||||||
|
setName(id);
|
||||||
return comments.nodes;
|
return comments.nodes;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
useAddWidgetNotification,
|
useAddWidgetNotification,
|
||||||
useAutoUpdate,
|
useAutoUpdate,
|
||||||
|
useName,
|
||||||
useSlackQuery,
|
useSlackQuery,
|
||||||
withSlack,
|
withSlack,
|
||||||
} from '@refocus/sdk';
|
} from '@refocus/sdk';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { Props } from './schema';
|
import { Props } from './schema';
|
||||||
import { Message } from './message/view';
|
import { Message } from './message/view';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -13,7 +15,24 @@ type PostMessageOptions = {
|
|||||||
message: string;
|
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 WidgetView = withSlack<Props>(({ conversationId }) => {
|
||||||
|
const [, setName] = useName();
|
||||||
const addNotification = useAddWidgetNotification();
|
const addNotification = useAddWidgetNotification();
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const { fetch, data } = useSlackQuery(async (client, props: Props) => {
|
const { fetch, data } = useSlackQuery(async (client, props: Props) => {
|
||||||
@@ -27,6 +46,7 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
|
|||||||
const response = await client.send('conversations.info', {
|
const response = await client.send('conversations.info', {
|
||||||
channel: props.conversationId,
|
channel: props.conversationId,
|
||||||
});
|
});
|
||||||
|
setName(response.channel!.name || 'Direct message');
|
||||||
return response.channel!;
|
return response.channel!;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,7 +59,7 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const update = useAutoUpdate(
|
useAutoUpdate(
|
||||||
{
|
{
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await info.fetch({ conversationId });
|
await info.fetch({ conversationId });
|
||||||
@@ -68,17 +88,8 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View $p="md">
|
<Wrapper $p="sm" $fc $gap="sm">
|
||||||
<button onClick={update}>Update</button>
|
<MessageList $gap="md" $fc>
|
||||||
<Typography variant="header">
|
|
||||||
{info.data?.name || 'Direct message'}
|
|
||||||
</Typography>
|
|
||||||
<Chat.Compose
|
|
||||||
value={message}
|
|
||||||
onValueChange={setMessage}
|
|
||||||
onSend={() => post({ message })}
|
|
||||||
/>
|
|
||||||
<List>
|
|
||||||
{data?.map((message) => (
|
{data?.map((message) => (
|
||||||
<Message
|
<Message
|
||||||
key={message.ts}
|
key={message.ts}
|
||||||
@@ -86,8 +97,13 @@ const WidgetView = withSlack<Props>(({ conversationId }) => {
|
|||||||
userId={message.user}
|
userId={message.user}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</List>
|
</MessageList>
|
||||||
</View>
|
<Chat.Compose
|
||||||
|
value={message}
|
||||||
|
onValueChange={setMessage}
|
||||||
|
onSend={() => post({ message })}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}, Slack.NotLoggedIn);
|
}, Slack.NotLoggedIn);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user