mirror of
https://github.com/morten-olsen/refocus.dev.git
synced 2026-02-08 00:46:25 +01:00
init
This commit is contained in:
140
packages/sdk/src/boards/context.tsx
Normal file
140
packages/sdk/src/boards/context.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Boards, BoardsLoad, BoardsSave } from './types';
|
||||
|
||||
type BoardsContextValue = {
|
||||
selected?: string;
|
||||
boards: Boards;
|
||||
addBoard: (name: string) => void;
|
||||
selectBoard: (id: string) => void;
|
||||
removeBoard: (id: string) => void;
|
||||
addWidget: (boardId: string, type: string, data: string) => void;
|
||||
removeWidget: (boardId: string, widgetId: string) => void;
|
||||
updateWidget: (boardId: string, widgetId: string, data: string) => void;
|
||||
};
|
||||
|
||||
type BoardsProviderProps = {
|
||||
children: React.ReactNode;
|
||||
load: BoardsLoad;
|
||||
save: BoardsSave;
|
||||
};
|
||||
|
||||
const BoardsContext = createContext<BoardsContextValue | null>(null);
|
||||
|
||||
const BoardsProvider: React.FC<BoardsProviderProps> = ({
|
||||
children,
|
||||
load,
|
||||
save,
|
||||
}) => {
|
||||
const [boards, setBoards] = useState<Boards>(load().boards);
|
||||
const [selected, setSelected] = useState<string | undefined>(load().selected);
|
||||
|
||||
const addBoard = useCallback((name: string) => {
|
||||
const id = uuid();
|
||||
setBoards((currentBoards) => ({
|
||||
...currentBoards,
|
||||
[id]: {
|
||||
name,
|
||||
widgets: {},
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const removeBoard = useCallback((id: string) => {
|
||||
setBoards((currentBoards) => {
|
||||
const copy = { ...currentBoards };
|
||||
delete copy[id];
|
||||
return copy;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const addWidget = useCallback(
|
||||
(boardId: string, type: string, data: string) => {
|
||||
const id = uuid();
|
||||
setBoards((currentBoards) => ({
|
||||
...currentBoards,
|
||||
[boardId]: {
|
||||
...currentBoards[boardId],
|
||||
widgets: {
|
||||
...currentBoards[boardId].widgets,
|
||||
[id]: {
|
||||
type,
|
||||
data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const removeWidget = useCallback((boardId: string, widgetId: string) => {
|
||||
setBoards((currentBoards) => {
|
||||
const copy = { ...currentBoards };
|
||||
delete copy[boardId].widgets[widgetId];
|
||||
return copy;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateWidget = useCallback(
|
||||
(boardId: string, widgetId: string, data: string) => {
|
||||
setBoards((currentBoards) => ({
|
||||
...currentBoards,
|
||||
[boardId]: {
|
||||
...currentBoards[boardId],
|
||||
widgets: {
|
||||
...currentBoards[boardId].widgets,
|
||||
[widgetId]: {
|
||||
...currentBoards[boardId].widgets[widgetId],
|
||||
data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const selectBoard = useCallback((id: string) => {
|
||||
setSelected(id);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
save({ boards, selected });
|
||||
}, [boards, selected, save]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
boards,
|
||||
selected,
|
||||
addBoard,
|
||||
removeBoard,
|
||||
addWidget,
|
||||
removeWidget,
|
||||
selectBoard,
|
||||
updateWidget,
|
||||
}),
|
||||
[
|
||||
boards,
|
||||
selected,
|
||||
addBoard,
|
||||
removeBoard,
|
||||
addWidget,
|
||||
removeWidget,
|
||||
selectBoard,
|
||||
updateWidget,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<BoardsContext.Provider value={value}>{children}</BoardsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { BoardsContext, BoardsProvider };
|
||||
77
packages/sdk/src/boards/hooks.ts
Normal file
77
packages/sdk/src/boards/hooks.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useContext } from 'react';
|
||||
import { BoardsContext } from './context';
|
||||
|
||||
const useBoards = () => {
|
||||
const context = useContext(BoardsContext);
|
||||
if (!context) {
|
||||
throw new Error('useBoards must be used within a BoardsProvider');
|
||||
}
|
||||
return context.boards;
|
||||
};
|
||||
|
||||
const useSelectedBoard = () => {
|
||||
const context = useContext(BoardsContext);
|
||||
if (!context) {
|
||||
throw new Error('useCurrentBoard must be used within a BoardsProvider');
|
||||
}
|
||||
return context.selected;
|
||||
};
|
||||
|
||||
const useAddWidget = () => {
|
||||
const context = useContext(BoardsContext);
|
||||
if (!context) {
|
||||
throw new Error('useAddWidget must be used within a BoardsProvider');
|
||||
}
|
||||
return context.addWidget;
|
||||
};
|
||||
|
||||
const useRemoveWidget = () => {
|
||||
const context = useContext(BoardsContext);
|
||||
if (!context) {
|
||||
throw new Error('useRemoveWidget must be used within a BoardsProvider');
|
||||
}
|
||||
return context.removeWidget;
|
||||
};
|
||||
|
||||
const useAddBoard = () => {
|
||||
const context = useContext(BoardsContext);
|
||||
if (!context) {
|
||||
throw new Error('useAddBoard must be used within a BoardsProvider');
|
||||
}
|
||||
return context.addBoard;
|
||||
};
|
||||
|
||||
const useRemoveBoard = () => {
|
||||
const context = useContext(BoardsContext);
|
||||
if (!context) {
|
||||
throw new Error('useRemoveBoard must be used within a BoardsProvider');
|
||||
}
|
||||
return context.removeBoard;
|
||||
};
|
||||
|
||||
const useSelectBoard = () => {
|
||||
const context = useContext(BoardsContext);
|
||||
if (!context) {
|
||||
throw new Error('useSelectBoard must be used within a BoardsProvider');
|
||||
}
|
||||
return context.selectBoard;
|
||||
};
|
||||
|
||||
const useUpdateWidget = () => {
|
||||
const context = useContext(BoardsContext);
|
||||
if (!context) {
|
||||
throw new Error('useUpdateWidget must be used within a BoardsProvider');
|
||||
}
|
||||
return context.updateWidget;
|
||||
};
|
||||
|
||||
export {
|
||||
useBoards,
|
||||
useSelectedBoard,
|
||||
useAddWidget,
|
||||
useRemoveWidget,
|
||||
useAddBoard,
|
||||
useRemoveBoard,
|
||||
useSelectBoard,
|
||||
useUpdateWidget,
|
||||
};
|
||||
12
packages/sdk/src/boards/index.ts
Normal file
12
packages/sdk/src/boards/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { BoardsProvider } from './context';
|
||||
export {
|
||||
useBoards,
|
||||
useSelectedBoard,
|
||||
useAddWidget,
|
||||
useRemoveWidget,
|
||||
useAddBoard,
|
||||
useRemoveBoard,
|
||||
useSelectBoard,
|
||||
useUpdateWidget,
|
||||
} from './hooks';
|
||||
export * from './types';
|
||||
23
packages/sdk/src/boards/types.ts
Normal file
23
packages/sdk/src/boards/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
type BoardData = { boards: Boards; selected?: string };
|
||||
|
||||
type BoardsLoad = () => BoardData;
|
||||
|
||||
type BoardsSave = (data: BoardData) => void;
|
||||
|
||||
type BoardWidget = {
|
||||
type: string;
|
||||
data: any;
|
||||
};
|
||||
|
||||
type Board = {
|
||||
name: string;
|
||||
widgets: {
|
||||
[key: string]: BoardWidget;
|
||||
};
|
||||
};
|
||||
|
||||
type Boards = {
|
||||
[key: string]: Board;
|
||||
};
|
||||
|
||||
export type { Board, Boards, BoardWidget, BoardsLoad, BoardsSave };
|
||||
71
packages/sdk/src/clients/github/context.tsx
Normal file
71
packages/sdk/src/clients/github/context.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import { Octokit } from 'octokit';
|
||||
|
||||
type GithubLogin = React.ComponentType<{
|
||||
setToken: (token: string) => void;
|
||||
cancel: () => void;
|
||||
}>;
|
||||
|
||||
type GithubClientContextValue = {
|
||||
octokit?: Octokit;
|
||||
login?: () => void;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
type GithubClientProviderProps = {
|
||||
children: React.ReactNode;
|
||||
login: GithubLogin;
|
||||
};
|
||||
|
||||
const GithubClientContext = createContext<GithubClientContextValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const GithubClientProvider: React.FC<GithubClientProviderProps> = ({
|
||||
children,
|
||||
login: GithubLoginComponent,
|
||||
}) => {
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
const [pat, setPat] = useState(localStorage.getItem('github_pat') || '');
|
||||
const octokit = useMemo(() => {
|
||||
if (!pat) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('github_pat', pat);
|
||||
return new Octokit({ auth: pat });
|
||||
}, [pat]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setPat('');
|
||||
localStorage.removeItem('github_pat');
|
||||
}, [setPat]);
|
||||
|
||||
const login = useCallback(() => {
|
||||
setIsLoggingIn(true);
|
||||
}, [setIsLoggingIn]);
|
||||
|
||||
const onLogin = useCallback(
|
||||
(nextPat: string) => {
|
||||
setPat(nextPat);
|
||||
setIsLoggingIn(false);
|
||||
},
|
||||
[setPat, setIsLoggingIn],
|
||||
);
|
||||
|
||||
const cancelLogin = useCallback(() => {
|
||||
setIsLoggingIn(false);
|
||||
}, [setIsLoggingIn]);
|
||||
|
||||
return (
|
||||
<GithubClientContext.Provider value={{ octokit, logout, login }}>
|
||||
{isLoggingIn && (
|
||||
<GithubLoginComponent cancel={cancelLogin} setToken={onLogin} />
|
||||
)}
|
||||
{children}
|
||||
</GithubClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type { GithubLogin };
|
||||
export { GithubClientContext, GithubClientProvider };
|
||||
64
packages/sdk/src/clients/github/hooks.ts
Normal file
64
packages/sdk/src/clients/github/hooks.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { GithubClientContext } from './context';
|
||||
import { Octokit } from 'octokit';
|
||||
|
||||
const useGithub = () => {
|
||||
const context = useContext(GithubClientContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useGithubClient must be used within a GithubClientProvider',
|
||||
);
|
||||
}
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
client: context.octokit,
|
||||
logout: context.logout,
|
||||
login: context.login,
|
||||
}),
|
||||
[context.octokit, context.logout, context.login],
|
||||
);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const useGithubQuery = <P, T = unknown>(
|
||||
query: (client: Octokit, params: P) => Promise<T>,
|
||||
) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<unknown | null>(null);
|
||||
const [data, setData] = useState<T>();
|
||||
const { client } = useGithub();
|
||||
|
||||
const fetch = useCallback(
|
||||
async (params: P) => {
|
||||
if (!client) {
|
||||
throw new Error('Github client is not initialized');
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await query(client, params);
|
||||
setData(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[client, query],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
fetch,
|
||||
}),
|
||||
[loading, error, data, fetch],
|
||||
);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export { useGithub, useGithubQuery };
|
||||
4
packages/sdk/src/clients/github/index.ts
Normal file
4
packages/sdk/src/clients/github/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { GithubClientProvider, GithubLogin } from './context';
|
||||
export { useGithub, useGithubQuery } from './hooks';
|
||||
export { withGithub } from './with-github';
|
||||
export * as GithubTypes from './types';
|
||||
12
packages/sdk/src/clients/github/types.ts
Normal file
12
packages/sdk/src/clients/github/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Octokit } from 'octokit';
|
||||
import { AsyncResponse } from '../../utils/types';
|
||||
|
||||
type PullRequest = AsyncResponse<Octokit['rest']['pulls']['get']>['data'];
|
||||
type Commit = AsyncResponse<Octokit['rest']['repos']['getCommit']>['data'];
|
||||
type WorkflowRun = AsyncResponse<
|
||||
Octokit['rest']['actions']['listWorkflowRuns']
|
||||
>['data']['workflow_runs'][number];
|
||||
type Profile = PullRequest['user'];
|
||||
type Repository = PullRequest['base']['repo'];
|
||||
|
||||
export { PullRequest, Profile, Repository, WorkflowRun, Commit };
|
||||
19
packages/sdk/src/clients/github/with-github.tsx
Normal file
19
packages/sdk/src/clients/github/with-github.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useGithub } from '.';
|
||||
|
||||
const withGithub = <TProps extends object>(
|
||||
Component: React.ComponentType<TProps>,
|
||||
Fallback: React.ComponentType<object>,
|
||||
) => {
|
||||
const WrappedComponent: React.FC<TProps> = (props) => {
|
||||
const github = useGithub();
|
||||
|
||||
if (!github.client) {
|
||||
return <Fallback />;
|
||||
}
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
return WrappedComponent;
|
||||
};
|
||||
|
||||
export { withGithub };
|
||||
17
packages/sdk/src/clients/index.ts
Normal file
17
packages/sdk/src/clients/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export {
|
||||
useGithub,
|
||||
useGithubQuery,
|
||||
withGithub,
|
||||
GithubTypes,
|
||||
GithubLogin,
|
||||
} from './github';
|
||||
export { useLinear, useLinearQuery, withLinear, LinearLogin } from './linear';
|
||||
export {
|
||||
useSlack,
|
||||
useSlackQuery,
|
||||
withSlack,
|
||||
SlackClient,
|
||||
SlackTypes,
|
||||
SlackLogin,
|
||||
} from './slack';
|
||||
export { ClientProvider } from './provider';
|
||||
73
packages/sdk/src/clients/linear/context.tsx
Normal file
73
packages/sdk/src/clients/linear/context.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { LinearClient } from '@linear/sdk';
|
||||
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
type LinearLogin = React.ComponentType<{
|
||||
setApiKey: (apiKey: string) => void;
|
||||
cancel: () => void;
|
||||
}>;
|
||||
|
||||
type LinearClientContextValue = {
|
||||
client?: LinearClient;
|
||||
logout: () => void;
|
||||
login?: () => void;
|
||||
};
|
||||
|
||||
type LinearClientProviderProps = {
|
||||
children: React.ReactNode;
|
||||
login: LinearLogin;
|
||||
};
|
||||
|
||||
const LinearClientContext = createContext<LinearClientContextValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const LinearClientProvider: React.FC<LinearClientProviderProps> = ({
|
||||
children,
|
||||
login: LinearLoginComponent,
|
||||
}) => {
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
const [apiKey, setApiKey] = useState(
|
||||
localStorage.getItem('linear_token') || '',
|
||||
);
|
||||
const client = useMemo(() => {
|
||||
if (!apiKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('linear_token', apiKey);
|
||||
return new LinearClient({ apiKey: apiKey });
|
||||
}, [apiKey]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setApiKey('');
|
||||
localStorage.removeItem('linear_token');
|
||||
}, [setApiKey]);
|
||||
|
||||
const login = useCallback(() => {
|
||||
setIsLoggingIn(true);
|
||||
}, [setIsLoggingIn]);
|
||||
|
||||
const onLogin = useCallback(
|
||||
(nextApiKey: string) => {
|
||||
setApiKey(nextApiKey);
|
||||
setIsLoggingIn(false);
|
||||
},
|
||||
[setApiKey, setIsLoggingIn],
|
||||
);
|
||||
|
||||
const cancelLogin = useCallback(() => {
|
||||
setIsLoggingIn(false);
|
||||
}, [setIsLoggingIn]);
|
||||
|
||||
return (
|
||||
<LinearClientContext.Provider value={{ client, login, logout }}>
|
||||
{isLoggingIn && (
|
||||
<LinearLoginComponent setApiKey={onLogin} cancel={cancelLogin} />
|
||||
)}
|
||||
{children}
|
||||
</LinearClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type { LinearLogin };
|
||||
export { LinearClientContext, LinearClientProvider };
|
||||
61
packages/sdk/src/clients/linear/hooks.ts
Normal file
61
packages/sdk/src/clients/linear/hooks.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { LinearClientContext } from './context';
|
||||
import { LinearClient } from '@linear/sdk';
|
||||
|
||||
const useLinear = () => {
|
||||
const context = useContext(LinearClientContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useLinearClient must be used within a LinearClientProvider',
|
||||
);
|
||||
}
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
client: context.client,
|
||||
logout: context.logout,
|
||||
login: context.login,
|
||||
}),
|
||||
[context.client, context.logout, context.login],
|
||||
);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const useLinearQuery = <T = unknown>(
|
||||
query: (client: LinearClient) => Promise<T>,
|
||||
) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<unknown | null>(null);
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const { client } = useLinear();
|
||||
|
||||
const fetch = useCallback(async () => {
|
||||
if (!client) {
|
||||
throw new Error('Linear client is not initialized');
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await query(client);
|
||||
setData(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [client, query]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
fetch,
|
||||
}),
|
||||
[loading, error, data, fetch],
|
||||
);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export { useLinear, useLinearQuery };
|
||||
3
packages/sdk/src/clients/linear/index.ts
Normal file
3
packages/sdk/src/clients/linear/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { LinearClientProvider, LinearLogin } from './context';
|
||||
export { useLinear, useLinearQuery } from './hooks';
|
||||
export { withLinear } from './with-linear';
|
||||
19
packages/sdk/src/clients/linear/with-linear.tsx
Normal file
19
packages/sdk/src/clients/linear/with-linear.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useLinear } from '.';
|
||||
|
||||
const withLinear = <TProps extends object>(
|
||||
Component: React.ComponentType<TProps>,
|
||||
FallBack: React.ComponentType<object>,
|
||||
) => {
|
||||
const WrappedComponent: React.FC<TProps> = (props) => {
|
||||
const linear = useLinear();
|
||||
|
||||
if (!linear.client) {
|
||||
return <FallBack />;
|
||||
}
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
return WrappedComponent;
|
||||
};
|
||||
|
||||
export { withLinear };
|
||||
29
packages/sdk/src/clients/provider.tsx
Normal file
29
packages/sdk/src/clients/provider.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { GithubClientProvider, GithubLogin } from './github';
|
||||
import { LinearClientProvider, LinearLogin } from './linear';
|
||||
import { SlackClientProvider, SlackLogin } from './slack';
|
||||
|
||||
type ClientProviderProps = {
|
||||
children: React.ReactNode;
|
||||
logins: {
|
||||
github: GithubLogin;
|
||||
linear: LinearLogin;
|
||||
slack: SlackLogin;
|
||||
};
|
||||
};
|
||||
|
||||
const ClientProvider: React.FC<ClientProviderProps> = ({
|
||||
children,
|
||||
logins,
|
||||
}) => {
|
||||
return (
|
||||
<LinearClientProvider login={logins.linear}>
|
||||
<GithubClientProvider login={logins.github}>
|
||||
<SlackClientProvider login={logins.slack}>
|
||||
{children}
|
||||
</SlackClientProvider>
|
||||
</GithubClientProvider>
|
||||
</LinearClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { ClientProvider };
|
||||
59
packages/sdk/src/clients/slack/client.ts
Normal file
59
packages/sdk/src/clients/slack/client.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { WebClient } from '@slack/web-api';
|
||||
import type { Expand } from '../../utils/types';
|
||||
|
||||
type MethodFromString<T extends `${string}.${string}`> =
|
||||
T extends `${infer A}.${infer B}`
|
||||
? A extends keyof WebClient
|
||||
? B extends keyof WebClient[A]
|
||||
? WebClient[A][B] extends (...args: any) => any
|
||||
? (
|
||||
...args: Parameters<WebClient[A][B]>
|
||||
) => ReturnType<WebClient[A][B]>
|
||||
: never
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
type ParamsFromString<T extends `${string}.${string}`> =
|
||||
MethodFromString<T> extends (arg: infer P) => unknown
|
||||
? P extends Record<string, any>
|
||||
? P
|
||||
: never
|
||||
: never;
|
||||
type ReturnFromString<T extends `${string}.${string}`> =
|
||||
MethodFromString<T> extends () => infer R
|
||||
? R extends Promise<infer P>
|
||||
? P
|
||||
: never
|
||||
: never;
|
||||
|
||||
class SlackClient {
|
||||
#token: string;
|
||||
|
||||
constructor(token: string) {
|
||||
this.#token = token;
|
||||
}
|
||||
|
||||
public send = async <K extends `${string}.${string}`>(
|
||||
action: K,
|
||||
params: Expand<ParamsFromString<K>>,
|
||||
): Promise<Expand<ReturnFromString<K>>> => {
|
||||
const form = new FormData();
|
||||
form.append('token', this.#token);
|
||||
Object.keys(params).forEach((key) => {
|
||||
form.append(key, params[key as keyof typeof params]);
|
||||
});
|
||||
const response = await fetch(`https://slack.com/api/${action}`, {
|
||||
mode: 'cors',
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch: ${response.statusText} ${action}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
}
|
||||
|
||||
export type { ReturnFromString };
|
||||
export { SlackClient };
|
||||
69
packages/sdk/src/clients/slack/context.tsx
Normal file
69
packages/sdk/src/clients/slack/context.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import { SlackClient } from './client';
|
||||
|
||||
type SlackLogin = React.ComponentType<{
|
||||
setToken: (token: string) => void;
|
||||
cancel: () => void;
|
||||
}>;
|
||||
|
||||
type SlackClientContextValue = {
|
||||
client?: SlackClient;
|
||||
logout: () => void;
|
||||
login: () => void;
|
||||
};
|
||||
|
||||
type SlackClientProviderProps = {
|
||||
children: React.ReactNode;
|
||||
login: SlackLogin;
|
||||
};
|
||||
|
||||
const SlackClientContext = createContext<SlackClientContextValue | null>(null);
|
||||
|
||||
const SlackClientProvider: React.FC<SlackClientProviderProps> = ({
|
||||
children,
|
||||
login: SlackLoginComponent,
|
||||
}) => {
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
const [token, setToken] = useState(localStorage.getItem('slack_token') || '');
|
||||
const client = useMemo(() => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('slack_token', token);
|
||||
return new SlackClient(token);
|
||||
}, [token]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setToken('');
|
||||
localStorage.removeItem('slack_token');
|
||||
}, [setToken]);
|
||||
|
||||
const login = useCallback(() => {
|
||||
setIsLoggingIn(true);
|
||||
}, [setIsLoggingIn]);
|
||||
|
||||
const onLogin = useCallback(
|
||||
(nextToken: string) => {
|
||||
setToken(nextToken);
|
||||
setIsLoggingIn(false);
|
||||
},
|
||||
[setToken, setIsLoggingIn],
|
||||
);
|
||||
|
||||
const cancelLogin = useCallback(() => {
|
||||
setIsLoggingIn(false);
|
||||
}, [setIsLoggingIn]);
|
||||
|
||||
return (
|
||||
<SlackClientContext.Provider value={{ client, login, logout }}>
|
||||
{isLoggingIn && (
|
||||
<SlackLoginComponent setToken={onLogin} cancel={cancelLogin} />
|
||||
)}
|
||||
{children}
|
||||
</SlackClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type { SlackLogin };
|
||||
export { SlackClientContext, SlackClientProvider };
|
||||
62
packages/sdk/src/clients/slack/hooks.ts
Normal file
62
packages/sdk/src/clients/slack/hooks.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { SlackClientContext } from './context';
|
||||
import { SlackClient } from './client';
|
||||
|
||||
const useSlack = () => {
|
||||
const context = useContext(SlackClientContext);
|
||||
if (!context) {
|
||||
throw new Error('useSlack must be used within a SlackClientProvider');
|
||||
}
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
client: context.client,
|
||||
logout: context.logout,
|
||||
login: context.login,
|
||||
}),
|
||||
[context.client, context.logout, context.login],
|
||||
);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const useSlackQuery = <P, T = unknown>(
|
||||
query: (client: SlackClient, params: P) => Promise<T>,
|
||||
) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<unknown | null>(null);
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const { client } = useSlack();
|
||||
|
||||
const fetch = useCallback(
|
||||
async (params: P) => {
|
||||
if (!client) {
|
||||
throw new Error('Slack client is not initialized');
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await query(client, params);
|
||||
setData(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[client, query],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
fetch,
|
||||
}),
|
||||
[loading, error, data, fetch],
|
||||
);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export { useSlack, useSlackQuery };
|
||||
5
packages/sdk/src/clients/slack/index.ts
Normal file
5
packages/sdk/src/clients/slack/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { SlackClientProvider, SlackLogin } from './context';
|
||||
export { SlackClient } from './client';
|
||||
export { useSlack, useSlackQuery } from './hooks';
|
||||
export { withSlack } from './with-slack';
|
||||
export * as SlackTypes from './types';
|
||||
5
packages/sdk/src/clients/slack/types.ts
Normal file
5
packages/sdk/src/clients/slack/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ReturnFromString } from './client';
|
||||
|
||||
type Profile = Exclude<ReturnFromString<'users.info'>['user'], undefined>;
|
||||
|
||||
export type { Profile };
|
||||
19
packages/sdk/src/clients/slack/with-slack.tsx
Normal file
19
packages/sdk/src/clients/slack/with-slack.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useSlack } from '.';
|
||||
|
||||
const withSlack = <TProps extends object>(
|
||||
Component: React.ComponentType<TProps>,
|
||||
Fallback: React.ComponentType<object>,
|
||||
) => {
|
||||
const WrappedComponent: React.FC<TProps> = (props) => {
|
||||
const slack = useSlack();
|
||||
|
||||
if (!slack.client) {
|
||||
return <Fallback />;
|
||||
}
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
return WrappedComponent;
|
||||
};
|
||||
|
||||
export { withSlack };
|
||||
42
packages/sdk/src/hooks/index.ts
Normal file
42
packages/sdk/src/hooks/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
type AutoUpdateOptions<TReturn> = {
|
||||
interval: number;
|
||||
action: () => Promise<TReturn>;
|
||||
callback?: (next: TReturn, prev?: TReturn) => void;
|
||||
};
|
||||
const useAutoUpdate = <T>(
|
||||
{ interval, action, callback = () => {} }: AutoUpdateOptions<T>,
|
||||
deps: any[],
|
||||
) => {
|
||||
const prev = useRef<T>();
|
||||
const actionWithCallback = useCallback(action, [...deps]);
|
||||
const callbackWithCallback = useCallback(callback, [...deps]);
|
||||
|
||||
const update = useCallback(async () => {
|
||||
const next = await actionWithCallback();
|
||||
callbackWithCallback(next, prev.current);
|
||||
prev.current = next;
|
||||
}, [actionWithCallback, callbackWithCallback]);
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId: NodeJS.Timeout;
|
||||
|
||||
const update = async () => {
|
||||
const next = await actionWithCallback();
|
||||
callbackWithCallback(next, prev.current);
|
||||
prev.current = next;
|
||||
intervalId = setTimeout(update, interval);
|
||||
};
|
||||
|
||||
update();
|
||||
|
||||
return () => {
|
||||
clearTimeout(intervalId);
|
||||
};
|
||||
}, [interval, actionWithCallback, callbackWithCallback]);
|
||||
|
||||
return update;
|
||||
};
|
||||
|
||||
export { useAutoUpdate };
|
||||
6
packages/sdk/src/index.ts
Normal file
6
packages/sdk/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './clients';
|
||||
export * from './widgets';
|
||||
export * from './provider';
|
||||
export * from './notifications';
|
||||
export * from './hooks';
|
||||
export * from './boards';
|
||||
65
packages/sdk/src/notifications/context.tsx
Normal file
65
packages/sdk/src/notifications/context.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import type { Notification } from './types';
|
||||
|
||||
type NotificationsContextValue = {
|
||||
notifications: Notification[];
|
||||
add: (notification: Notification) => void;
|
||||
dismiss: (id: string) => void;
|
||||
};
|
||||
|
||||
type NotificationsProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const NotificationsContext = createContext<NotificationsContextValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
const NotificationsProvider: React.FC<NotificationsProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
|
||||
const add = useCallback((notification: Notification) => {
|
||||
setNotifications((current) => [
|
||||
...current,
|
||||
{
|
||||
id: String(nextId++),
|
||||
...notification,
|
||||
},
|
||||
]);
|
||||
if ('Notification' in window) {
|
||||
const notify = async () => {
|
||||
if (Notification.permission !== 'granted') {
|
||||
await Notification.requestPermission();
|
||||
}
|
||||
if (Notification.permission === 'granted') {
|
||||
const n = new Notification(notification.title || 'Notification', {
|
||||
body: notification.message,
|
||||
});
|
||||
setTimeout(() => n.close(), 10 * 1000);
|
||||
}
|
||||
};
|
||||
notify();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const dismiss = useCallback((id: string) => {
|
||||
setNotifications((current) => current.filter((n) => n.id !== id));
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ notifications, add, dismiss }),
|
||||
[notifications, add, dismiss],
|
||||
);
|
||||
|
||||
return (
|
||||
<NotificationsContext.Provider value={value}>
|
||||
{children}
|
||||
</NotificationsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { NotificationsContext, NotificationsProvider };
|
||||
37
packages/sdk/src/notifications/hooks.ts
Normal file
37
packages/sdk/src/notifications/hooks.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useContext } from 'react';
|
||||
import { NotificationsContext } from './context';
|
||||
|
||||
const useNotifications = () => {
|
||||
const context = useContext(NotificationsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useNotifications must be used within a NotificationsProvider',
|
||||
);
|
||||
}
|
||||
|
||||
return context.notifications;
|
||||
};
|
||||
|
||||
const useNotificationAdd = () => {
|
||||
const context = useContext(NotificationsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useNotificationAdd must be used within a NotificationsProvider',
|
||||
);
|
||||
}
|
||||
|
||||
return context.add;
|
||||
};
|
||||
|
||||
const useNotificationDismiss = () => {
|
||||
const context = useContext(NotificationsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useNotificationDismiss must be used within a NotificationsProvider',
|
||||
);
|
||||
}
|
||||
|
||||
return context.dismiss;
|
||||
};
|
||||
|
||||
export { useNotifications, useNotificationAdd, useNotificationDismiss };
|
||||
7
packages/sdk/src/notifications/index.ts
Normal file
7
packages/sdk/src/notifications/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { NotificationsProvider } from './context';
|
||||
export type { Notification } from './types';
|
||||
export {
|
||||
useNotifications,
|
||||
useNotificationAdd,
|
||||
useNotificationDismiss,
|
||||
} from './hooks';
|
||||
12
packages/sdk/src/notifications/types.ts
Normal file
12
packages/sdk/src/notifications/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
type Notification = {
|
||||
view: Symbol;
|
||||
id?: string;
|
||||
title?: string;
|
||||
message: string;
|
||||
actions?: {
|
||||
label: string;
|
||||
callback: () => void;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type { Notification };
|
||||
40
packages/sdk/src/provider.tsx
Normal file
40
packages/sdk/src/provider.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
import {
|
||||
ClientProvider,
|
||||
GithubLogin,
|
||||
LinearLogin,
|
||||
SlackLogin,
|
||||
} from './clients';
|
||||
import { Widget, WidgetsProvider } from './widgets';
|
||||
import { NotificationsProvider } from './notifications';
|
||||
import { BoardsLoad, BoardsProvider, BoardsSave } from './boards';
|
||||
|
||||
type DashboardProviderProps = {
|
||||
children: React.ReactNode;
|
||||
widgets?: Widget<TSchema>[];
|
||||
load: BoardsLoad;
|
||||
save: BoardsSave;
|
||||
logins: {
|
||||
github: GithubLogin;
|
||||
linear: LinearLogin;
|
||||
slack: SlackLogin;
|
||||
};
|
||||
};
|
||||
|
||||
const DashboardProvider: React.FC<DashboardProviderProps> = ({
|
||||
children,
|
||||
widgets,
|
||||
load,
|
||||
save,
|
||||
logins,
|
||||
}) => (
|
||||
<WidgetsProvider widgets={widgets}>
|
||||
<BoardsProvider load={load} save={save}>
|
||||
<NotificationsProvider>
|
||||
<ClientProvider logins={logins}>{children}</ClientProvider>
|
||||
</NotificationsProvider>
|
||||
</BoardsProvider>
|
||||
</WidgetsProvider>
|
||||
);
|
||||
|
||||
export { DashboardProvider };
|
||||
16
packages/sdk/src/utils/types.ts
Normal file
16
packages/sdk/src/utils/types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
||||
|
||||
// expands object types recursively
|
||||
type ExpandRecursively<T> = T extends object
|
||||
? T extends infer O
|
||||
? { [K in keyof O]: ExpandRecursively<O[K]> }
|
||||
: never
|
||||
: T;
|
||||
|
||||
type AsyncResponse<T> = T extends () => Promise<infer U> ? U : never;
|
||||
|
||||
type FirstParameter<T> = T extends (arg1: infer U, ...args: any[]) => any
|
||||
? U
|
||||
: never;
|
||||
|
||||
export type { Expand, ExpandRecursively, AsyncResponse, FirstParameter };
|
||||
29
packages/sdk/src/widgets/context.tsx
Normal file
29
packages/sdk/src/widgets/context.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
import { createContext, useState } from 'react';
|
||||
import { Widget } from './types';
|
||||
|
||||
type WidgetsContextValue = {
|
||||
widgets: Widget<TSchema>[];
|
||||
};
|
||||
|
||||
type WidgetsProviderProps = {
|
||||
children: React.ReactNode;
|
||||
widgets?: Widget<TSchema>[];
|
||||
};
|
||||
|
||||
const WidgetsContext = createContext<WidgetsContextValue | null>(null);
|
||||
|
||||
const WidgetsProvider: React.FC<WidgetsProviderProps> = ({
|
||||
children,
|
||||
widgets: initialWidgets,
|
||||
}) => {
|
||||
const [widgets] = useState<Widget<TSchema>[]>(initialWidgets || []);
|
||||
|
||||
return (
|
||||
<WidgetsContext.Provider value={{ widgets }}>
|
||||
{children}
|
||||
</WidgetsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { WidgetsContext, WidgetsProvider };
|
||||
26
packages/sdk/src/widgets/editor.tsx
Normal file
26
packages/sdk/src/widgets/editor.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useWidget } from '.';
|
||||
import { useWidgetData, useWidgetId } from './hooks';
|
||||
|
||||
type WidgetRenderProps = {
|
||||
onSave: (data: any) => void;
|
||||
};
|
||||
|
||||
const WidgetEditor: React.FC<WidgetRenderProps> = ({ onSave }) => {
|
||||
const id = useWidgetId();
|
||||
const data = useWidgetData();
|
||||
const widget = useWidget(id);
|
||||
|
||||
if (!widget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component = widget.edit;
|
||||
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Component value={data} save={onSave} />;
|
||||
};
|
||||
|
||||
export { WidgetEditor };
|
||||
124
packages/sdk/src/widgets/hooks.ts
Normal file
124
packages/sdk/src/widgets/hooks.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { WidgetsContext } from './context';
|
||||
import { WidgetContext } from './widget-context';
|
||||
|
||||
const useWidget = (id: string) => {
|
||||
const context = useContext(WidgetsContext);
|
||||
if (!context) {
|
||||
throw new Error('useWidget must be used within a WidgetsProvider');
|
||||
}
|
||||
const current = useMemo(() => {
|
||||
return context.widgets.find((widget) => widget.id === id);
|
||||
}, [context.widgets, id]);
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const useWidgets = () => {
|
||||
const context = useContext(WidgetsContext);
|
||||
if (!context) {
|
||||
throw new Error('useWidgets must be used within a WidgetsProvider');
|
||||
}
|
||||
return context.widgets;
|
||||
};
|
||||
|
||||
type WidgetResult = {
|
||||
id: string;
|
||||
data: any;
|
||||
description?: string;
|
||||
name: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
const useGetWidgetsFromUrl = () => {
|
||||
const [current, setCurrent] = useState<WidgetResult[]>([]);
|
||||
const widgets = useWidgets();
|
||||
|
||||
const update = useCallback(
|
||||
(url: URL) => {
|
||||
const result = widgets.map((widget) => {
|
||||
const parsed = widget.parseUrl?.(url);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: widget.id,
|
||||
name: widget.name,
|
||||
description: widget.description,
|
||||
icon: widget.icon,
|
||||
data: parsed,
|
||||
};
|
||||
});
|
||||
setCurrent(result.filter(Boolean) as WidgetResult[]);
|
||||
},
|
||||
[widgets],
|
||||
);
|
||||
|
||||
return [current, update] as const;
|
||||
};
|
||||
|
||||
const useDismissWidgetNotification = () => {
|
||||
const context = useContext(WidgetContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useDismissWidgetNotification must be used within a WidgetProvider',
|
||||
);
|
||||
}
|
||||
return context.dismissNotification;
|
||||
};
|
||||
|
||||
const useAddWidgetNotification = () => {
|
||||
const context = useContext(WidgetContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useAddWidgetNotification must be used within a WidgetProvider',
|
||||
);
|
||||
}
|
||||
return context.addNotification;
|
||||
};
|
||||
|
||||
const useWidgetNotifications = () => {
|
||||
const context = useContext(WidgetContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useWidgetNotifications must be used within a WidgetProvider',
|
||||
);
|
||||
}
|
||||
return context.notifications;
|
||||
};
|
||||
|
||||
const useWidgetData = () => {
|
||||
const context = useContext(WidgetContext);
|
||||
if (!context) {
|
||||
throw new Error('useWidgetData must be used within a WidgetProvider');
|
||||
}
|
||||
return context.data;
|
||||
};
|
||||
|
||||
const useSetWidgetData = () => {
|
||||
const context = useContext(WidgetContext);
|
||||
if (!context) {
|
||||
throw new Error('useSetWidgetData must be used within a WidgetProvider');
|
||||
}
|
||||
return context.setData;
|
||||
};
|
||||
|
||||
const useWidgetId = () => {
|
||||
const context = useContext(WidgetContext);
|
||||
if (!context) {
|
||||
throw new Error('useWidgetId must be used within a WidgetProvider');
|
||||
}
|
||||
return context.id;
|
||||
};
|
||||
|
||||
export {
|
||||
useWidget,
|
||||
useWidgets,
|
||||
useGetWidgetsFromUrl,
|
||||
useWidgetNotifications,
|
||||
useDismissWidgetNotification,
|
||||
useAddWidgetNotification,
|
||||
useWidgetData,
|
||||
useSetWidgetData,
|
||||
useWidgetId,
|
||||
};
|
||||
16
packages/sdk/src/widgets/index.ts
Normal file
16
packages/sdk/src/widgets/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export { WidgetsProvider } from './context';
|
||||
export type { Widget } from './types';
|
||||
export {
|
||||
useWidget,
|
||||
useWidgets,
|
||||
useGetWidgetsFromUrl,
|
||||
useAddWidgetNotification,
|
||||
useDismissWidgetNotification,
|
||||
useWidgetNotifications,
|
||||
useWidgetId,
|
||||
useWidgetData,
|
||||
useSetWidgetData,
|
||||
} from './hooks';
|
||||
export { WidgetProvider } from './widget-context';
|
||||
export { WidgetView } from './view';
|
||||
export { WidgetEditor } from './editor';
|
||||
17
packages/sdk/src/widgets/types.ts
Normal file
17
packages/sdk/src/widgets/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { TSchema, Static } from '@sinclair/typebox';
|
||||
|
||||
type Widget<TConfig extends TSchema> = {
|
||||
name: string;
|
||||
id: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
schema: TSchema;
|
||||
parseUrl?: (url: URL) => Static<TConfig> | undefined;
|
||||
component: React.ComponentType<Static<TConfig>>;
|
||||
edit?: React.ComponentType<{
|
||||
value?: Static<TConfig>;
|
||||
save: (next: Static<TConfig>) => void;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type { Widget };
|
||||
18
packages/sdk/src/widgets/view.tsx
Normal file
18
packages/sdk/src/widgets/view.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useWidget } from '.';
|
||||
import { useWidgetData, useWidgetId } from './hooks';
|
||||
|
||||
const WidgetView: React.FC = () => {
|
||||
const id = useWidgetId();
|
||||
const data = useWidgetData();
|
||||
const widget = useWidget(id);
|
||||
|
||||
if (!widget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component = widget.component;
|
||||
|
||||
return <Component {...data} />;
|
||||
};
|
||||
|
||||
export { WidgetView };
|
||||
73
packages/sdk/src/widgets/widget-context.tsx
Normal file
73
packages/sdk/src/widgets/widget-context.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createContext, useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
Notification as BaseNotification,
|
||||
useNotificationAdd,
|
||||
useNotifications,
|
||||
useNotificationDismiss,
|
||||
} from '../notifications';
|
||||
type Notification = Omit<BaseNotification, 'view'>;
|
||||
|
||||
type WidgetContextValue = {
|
||||
id: string;
|
||||
data?: any;
|
||||
addNotification: (notification: Notification) => void;
|
||||
dismissNotification: (id: string) => void;
|
||||
notifications: Notification[];
|
||||
setData?: (data: any) => void;
|
||||
};
|
||||
|
||||
type WidgetProviderProps = {
|
||||
id: string;
|
||||
data?: any;
|
||||
setData?: (data: any) => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const WidgetContext = createContext<WidgetContextValue | null>(null);
|
||||
|
||||
const WidgetProvider = ({
|
||||
id,
|
||||
data,
|
||||
setData,
|
||||
children,
|
||||
}: WidgetProviderProps) => {
|
||||
const ref = useRef(Symbol('WidgetRender'));
|
||||
const globalNotifications = useNotifications();
|
||||
const addGlobalNotification = useNotificationAdd();
|
||||
const dissmissGlobalNotification = useNotificationDismiss();
|
||||
const notifications = useMemo(() => {
|
||||
return globalNotifications.filter((n) => n.view !== ref.current);
|
||||
}, [globalNotifications]);
|
||||
|
||||
const addNotification = useCallback(
|
||||
(notification: Notification) => {
|
||||
addGlobalNotification({ ...notification, view: ref.current });
|
||||
},
|
||||
[addGlobalNotification],
|
||||
);
|
||||
|
||||
const dismissNotification = useCallback(
|
||||
(dismissId: string) => {
|
||||
dissmissGlobalNotification(dismissId);
|
||||
},
|
||||
[dissmissGlobalNotification],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
addNotification,
|
||||
dismissNotification,
|
||||
notifications,
|
||||
id,
|
||||
data,
|
||||
setData,
|
||||
}),
|
||||
[addNotification, notifications, id, data, setData, dismissNotification],
|
||||
);
|
||||
|
||||
return (
|
||||
<WidgetContext.Provider value={value}>{children}</WidgetContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { WidgetContext, WidgetProvider };
|
||||
Reference in New Issue
Block a user