diff --git a/.vscode/settings.json b/.vscode/settings.json index 81d4d0b4d4..351bc719e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,5 +22,20 @@ }, "tailwindCSS.experimental.classRegex": [ ["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] - ] + ], + "workbench.colorCustomizations": { + "editorIndentGuide.background": "#00000000", + "[Material Theme Palenight High Contrast]": { + "tab.activeBorder": "#00000000", + "tab.inactiveBackground": "#1b1e2b", + "activityBar.activeBorder": "#a6accd", + "editorGroupHeader.tabsBackground": "#1b1e2b", + "tab.border": "#00000000", + "sideBarSectionHeader.border": "#00000000", + "terminal.border": "#a6accd" + }, + "activityBar.background": "#0C3429", + "titleBar.activeBackground": "#114939", + "titleBar.activeForeground": "#F4FDFA" + } } diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json index cd85fc4095..b4c2922050 100644 --- a/apps/admin-x-settings/package.json +++ b/apps/admin-x-settings/package.json @@ -46,6 +46,7 @@ "@dnd-kit/core": "6.0.8", "@dnd-kit/sortable": "7.0.2", "@ebay/nice-modal-react": "1.2.10", + "@tanstack/react-query": "4.29.25", "@tryghost/timezone-data": "0.3.0", "clsx": "2.0.0", "react": "18.2.0", diff --git a/apps/admin-x-settings/src/App.tsx b/apps/admin-x-settings/src/App.tsx index 815604a274..adbe86f618 100644 --- a/apps/admin-x-settings/src/App.tsx +++ b/apps/admin-x-settings/src/App.tsx @@ -5,57 +5,61 @@ import NiceModal from '@ebay/nice-modal-react'; import RoutingProvider from './components/providers/RoutingProvider'; import Settings from './components/Settings'; import Sidebar from './components/Sidebar'; -import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState'; -import {OfficialTheme} from './models/themes'; -import {ServicesProvider} from './components/providers/ServiceProvider'; -import {Toaster} from 'react-hot-toast'; +import { GlobalDirtyStateProvider } from './hooks/useGlobalDirtyState'; +import { OfficialTheme } from './models/themes'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ServicesProvider } from './components/providers/ServiceProvider'; +import { Toaster } from 'react-hot-toast'; interface AppProps { ghostVersion: string; officialThemes: OfficialTheme[]; } +const queryClient = new QueryClient(); + function App({ghostVersion, officialThemes}: AppProps) { return ( - - - - -
- - -
- -
- - {/* Main container */} -
- - {/* Sidebar */} -
-
- Settings -
-
- -
+ + + + + +
+ + +
+
-
-
- + {/* Main container */} +
+ + {/* Sidebar */} +
+
+ Settings +
+
+ +
+
+
+
+ +
-
-
-
-
-
-
-
+ +
+ + + + + ); } diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.stories.tsx index 5ec68ef354..7e0ed38a7f 100644 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.stories.tsx @@ -1,11 +1,12 @@ import TaskList from './Tasklist'; +import { ReactNode } from 'react'; import * as TaskStories from './Task.stories'; const story = { component: TaskList, title: 'Experimental / Task List', - decorators: [(_story: any) =>
{_story()}
], + decorators: [(_story: () => ReactNode) =>
{_story()}
], tags: ['autodocs'] }; @@ -43,4 +44,4 @@ export const Empty = { ...Loading.args, loading: false } -}; \ No newline at end of file +}; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Icon.tsx b/apps/admin-x-settings/src/admin-x-ds/global/Icon.tsx index ecce0d5bf8..424571539a 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Icon.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/Icon.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, { useEffect, useState } from 'react'; import clsx from 'clsx'; interface UseDynamicSVGImportOptions { @@ -27,9 +27,13 @@ function useDynamicSVGImport( ).ReactComponent; setSvgComponent(() => SvgIcon); onCompleted?.(name, SvgIcon); - } catch (err: any) { - onError?.(err); - setError(() => err); + } catch (err) { + if (err instanceof Error) { + onError?.(err); + setError(err); + } else { + throw err; + } } finally { setLoading(() => false); } @@ -103,4 +107,4 @@ const Icon: React.FC = ({name, size = 'md', colorClass = 'text-black' return null; }; -export default Icon; \ No newline at end of file +export default Icon; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/List.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/List.stories.tsx index 7c7839ecf0..318c74f05e 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/List.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/List.stories.tsx @@ -1,8 +1,9 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react'; import * as ListItemStories from './ListItem.stories'; import List from './List'; import ListItem from './ListItem'; +import { ReactNode } from 'react'; const meta = { title: 'Global / List', @@ -29,7 +30,7 @@ export const Default: Story = { children: listItems, hint: 'And here is a hint for the whole list' }, - decorators: [(_story: any) => (
{_story()}
)] + decorators: [(_story: () => ReactNode) => (
{_story()}
)] }; export const PageLevel: Story = { diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ListItem.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/ListItem.stories.tsx index b1307354ca..f43812eeed 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/ListItem.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/ListItem.stories.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import type {Meta, StoryObj} from '@storybook/react'; +import React, { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import Avatar from './Avatar'; import Button from './Button'; @@ -9,7 +9,7 @@ const meta = { title: 'Global / List / List Item', component: ListItem, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)], + decorators: [(_story: () => ReactNode) => (
{_story()}
)], argTypes: { title: {control: 'text'}, detail: {control: 'text'} diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Menu.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/Menu.stories.tsx index 2c1b1ca10f..143bd79fdc 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Menu.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/Menu.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import Button from './Button'; import Menu from './Menu'; @@ -7,7 +8,7 @@ const meta = { title: 'Global / Menu', component: Menu, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)] + decorators: [(_story: () => ReactNode) => (
{_story()}
)] } satisfies Meta; export default meta; @@ -59,4 +60,4 @@ export const LongLabels: Story = { items: longItems, position: 'right' } -}; \ No newline at end of file +}; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/SortableList.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/SortableList.stories.tsx index 9d5a48f942..af381eef42 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/SortableList.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/SortableList.stories.tsx @@ -1,12 +1,12 @@ -import {useArgs} from '@storybook/preview-api'; -import type {Meta, StoryObj} from '@storybook/react'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; -import SortableList, {SortableListProps} from './SortableList'; +import SortableList, { SortableListProps } from './SortableList'; import clsx from 'clsx'; -import {arrayMove} from '@dnd-kit/sortable'; -import {useState} from 'react'; +import { arrayMove } from '@dnd-kit/sortable'; +import { useState } from 'react'; -const Wrapper = (props: SortableListProps & {updateArgs: (args: Partial>) => void}) => { +const Wrapper = (props: SortableListProps<{id: string}> & {updateArgs: (args: Partial>) => void}) => { // Seems like Storybook recreates items on every render, so we need to keep our own state const [items, setItems] = useState(props.items); diff --git a/apps/admin-x-settings/src/admin-x-ds/global/StickyFooter.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/StickyFooter.stories.tsx index 16b6746f08..6e455b8dfa 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/StickyFooter.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/StickyFooter.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import StickyFooter from './StickyFooter'; @@ -6,7 +7,7 @@ const meta = { title: 'Global / Sticky Footer', component: StickyFooter, tags: ['autodocs'], - decorators: [(_story: any) => ( + decorators: [(_story: () => ReactNode) => (
(
{_story()}
)] + decorators: [(_story: () => ReactNode) => (
{_story()}
)] }; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Toast.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/Toast.stories.tsx index 6ab21d5e18..42798ac19a 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Toast.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/Toast.stories.tsx @@ -1,13 +1,14 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import ToastContainer from './ToastContainer'; -import {Toaster} from 'react-hot-toast'; +import { Toaster } from 'react-hot-toast'; const meta = { title: 'Global / Toast', component: ToastContainer, tags: ['autodocs'], - decorators: [(_story: any) => ( + decorators: [(_story: () => ReactNode) => ( <> {_story()} diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Toast.tsx b/apps/admin-x-settings/src/admin-x-ds/global/Toast.tsx index 34d54c76bc..a51490074c 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Toast.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/Toast.tsx @@ -1,7 +1,7 @@ import Icon from './Icon'; import React from 'react'; import clsx from 'clsx'; -import {ToastOptions, toast} from 'react-hot-toast'; +import { Toast as HotToast, ToastOptions, toast } from 'react-hot-toast'; export type ToastType = 'neutral' | 'success' | 'error' | 'pageError'; @@ -13,7 +13,7 @@ export interface ShowToastProps { } interface ToastProps { - t: any; + t: HotToast; /** * Can be a name of an icon from the icon library or a react component diff --git a/apps/admin-x-settings/src/admin-x-ds/global/chrome/DesktopChrome.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/chrome/DesktopChrome.stories.tsx index 344bacb075..e4ed1565e9 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/chrome/DesktopChrome.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/chrome/DesktopChrome.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import DesktopChrome from './DesktopChrome'; @@ -6,7 +7,7 @@ const meta = { title: 'Global / Chrome / Desktop Chrome', component: DesktopChrome, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)] + decorators: [(_story: () => ReactNode) => (
{_story()}
)] } satisfies Meta; export default meta; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/chrome/MobileChrome.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/chrome/MobileChrome.stories.tsx index 951324adc0..d0432309c5 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/chrome/MobileChrome.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/chrome/MobileChrome.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import MobileChrome from './MobileChrome'; @@ -6,7 +7,7 @@ const meta = { title: 'Global / Chrome / Mobile Chrome', component: MobileChrome, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)] + decorators: [(_story: () => ReactNode) => (
{_story()}
)] } satisfies Meta; export default meta; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/Checkbox.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/Checkbox.stories.tsx index 398f91af93..114d9c554d 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/Checkbox.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/Checkbox.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import Checkbox from './Checkbox'; @@ -6,7 +7,7 @@ const meta = { title: 'Global / Form / Checkbox', component: Checkbox, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)], + decorators: [(_story: () => ReactNode) => (
{_story()}
)], argTypes: { hint: { control: 'text' diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx index 2a75d0011b..646e083aae 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx @@ -1,4 +1,4 @@ -import React, {ReactNode, Suspense, useCallback, useMemo} from 'react'; +import React, { ReactNode, Suspense, useCallback, useMemo } from 'react'; export interface HtmlEditorProps { value?: string @@ -10,12 +10,14 @@ export interface HtmlEditorProps { declare global { interface Window { + // eslint-disable-next-line @typescript-eslint/no-explicit-any '@tryghost/koenig-lexical': any; } } const fetchKoenig = function ({editorUrl, editorVersion}: { editorUrl: string; editorVersion: string; }) { let status = 'pending'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any let response: any; const fetchPackage = async () => { @@ -64,7 +66,7 @@ class ErrorHandler extends React.Component<{ children: ReactNode }> { return {hasError: true}; } - componentDidCatch(error: any, errorInfo: any) { + componentDidCatch(error: unknown, errorInfo: unknown) { console.error(error, errorInfo); // eslint-disable-line } @@ -87,7 +89,7 @@ const KoenigWrapper: React.FC = ({ placeholder, nodes }) => { - const onError = useCallback((error: any) => { + const onError = useCallback((error: unknown) => { // ensure we're still showing errors in development console.error(error); // eslint-disable-line @@ -105,6 +107,7 @@ const KoenigWrapper: React.FC = ({ // don't rethrow, Lexical will attempt to gracefully recover }, []); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const koenig = useMemo(() => new Proxy({} as { [key: string]: any }, { get: (_target, prop) => { return editor.read()[prop]; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlField.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlField.tsx index 260cb5b313..1ce3fc595e 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlField.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlField.tsx @@ -1,14 +1,16 @@ import Heading from '../Heading'; import Hint from '../Hint'; -import HtmlEditor, {HtmlEditorProps} from './HtmlEditor'; +import HtmlEditor, { HtmlEditorProps } from './HtmlEditor'; import React from 'react'; import clsx from 'clsx'; +export type EditorConfig = { editor: { url: string; version: string; } } + export type HtmlFieldProps = HtmlEditorProps & { /** * Should be passed the Ghost instance config to get the editor JS URL */ - config: { editor: { url: string; version: string; } }; + config: EditorConfig; title?: string; hideTitle?: boolean; error?: boolean; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/ImageUpload.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/ImageUpload.stories.tsx index 6e1f7c32e4..673ab05618 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/ImageUpload.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/ImageUpload.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import ImageUpload from './ImageUpload'; @@ -6,7 +7,7 @@ const meta = { title: 'Global / Form / Image upload', component: ImageUpload, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)] + decorators: [(_story: () => ReactNode) => (
{_story()}
)] } satisfies Meta; export default meta; @@ -48,4 +49,4 @@ export const ImageUploaded: Story = { alert('Delete image'); } } -}; \ No newline at end of file +}; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/Radio.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/Radio.stories.tsx index a59fe86390..6f079f9bd0 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/Radio.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/Radio.stories.tsx @@ -1,12 +1,13 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; -import Radio, {RadioOption} from './Radio'; +import Radio, { RadioOption } from './Radio'; const meta = { title: 'Global / Form / Radio', component: Radio, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)], + decorators: [(_story: () => ReactNode) => (
{_story()}
)], argTypes: { hint: { control: 'text' diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/Select.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/Select.stories.tsx index 852a7721d9..76109ab859 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/Select.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/Select.stories.tsx @@ -1,13 +1,14 @@ -import {useArgs} from '@storybook/preview-api'; -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; -import Select, {SelectOption} from './Select'; +import Select, { SelectOption } from './Select'; const meta = { title: 'Global / Form / Select', component: Select, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)], + decorators: [(_story: () => ReactNode) => (
{_story()}
)], argTypes: { hint: { control: 'text' diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/TextArea.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/TextArea.stories.tsx index ee83b04eb3..7b7bfced39 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/TextArea.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/TextArea.stories.tsx @@ -1,5 +1,6 @@ -import {useArgs} from '@storybook/preview-api'; -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; import TextArea from './TextArea'; @@ -7,7 +8,7 @@ const meta = { title: 'Global / Form / Textarea', component: TextArea, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)], + decorators: [(_story: () => ReactNode) => (
{_story()}
)], argTypes: { hint: { control: 'text' diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx index c0efb1481f..0a48d87429 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx @@ -1,5 +1,6 @@ -import {useArgs} from '@storybook/preview-api'; -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; import TextField from './TextField'; @@ -7,7 +8,7 @@ const meta = { title: 'Global / Form / Textfield', component: TextField, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)], + decorators: [(_story: () => ReactNode) => (
{_story()}
)], argTypes: { hint: { control: 'text' diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/Toggle.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/Toggle.stories.tsx index c673d9e52d..567cf031cd 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/Toggle.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/Toggle.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import Toggle from './Toggle'; @@ -6,7 +7,7 @@ const meta = { title: 'Global / Form / Toggle', component: Toggle, tags: ['autodocs'], - decorators: [(_story: any) => (
{_story()}
)] + decorators: [(_story: () => ReactNode) => (
{_story()}
)] } satisfies Meta; export default meta; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/modal/ConfirmationModal.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/modal/ConfirmationModal.stories.tsx index 868982fa08..01a0619b50 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/modal/ConfirmationModal.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/modal/ConfirmationModal.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryContext, StoryObj } from '@storybook/react'; import ConfirmationModal from './ConfirmationModal'; import ConfirmationModalContainer from './ConfirmationModalContainer'; @@ -8,7 +9,7 @@ const meta = { title: 'Global / Modal / Confirmation Modal', component: ConfirmationModal, tags: ['autodocs'], - decorators: [(_story: any, context: any) => ( + decorators: [(_story: () => ReactNode, context: StoryContext) => ( diff --git a/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.stories.tsx index 5730994db7..98dcb5f26c 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryContext, StoryObj } from '@storybook/react'; import Modal from './Modal'; import ModalContainer from './ModalContainer'; @@ -9,7 +10,7 @@ const meta = { title: 'Global / Modal', component: Modal, tags: ['autodocs'], - decorators: [(_story: any, context: any) => ( + decorators: [(_story: () => ReactNode, context: StoryContext) => ( @@ -166,4 +167,4 @@ export const Dirty: Story = { title: 'Dirty modal', children:

Simulates if there were unsaved changes of a form. Click on Cancel

} -}; \ No newline at end of file +}; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/modal/PreviewModal.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/modal/PreviewModal.stories.tsx index d3d15ee241..09d4e2a4a1 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/modal/PreviewModal.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/modal/PreviewModal.stories.tsx @@ -1,16 +1,17 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryContext, StoryObj } from '@storybook/react'; import Heading from '../Heading'; import NiceModal from '@ebay/nice-modal-react'; import PreviewModal from './PreviewModal'; import PreviewModalContainer from './PreviewModalContainer'; -import {Tab} from '../TabView'; +import { Tab } from '../TabView'; const meta = { title: 'Global / Modal / Preview Modal', component: PreviewModal, tags: ['autodocs'], - decorators: [(_story: any, context: any) => ( + decorators: [(_story: () => ReactNode, context: StoryContext) => ( @@ -86,4 +87,4 @@ export const FullBleed: Story = { ...Default.args, size: 'bleed' } -}; \ No newline at end of file +}; diff --git a/apps/admin-x-settings/src/admin-x-ds/settings/SettingGroup.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/settings/SettingGroup.stories.tsx index de0829c417..8921e213ab 100644 --- a/apps/admin-x-settings/src/admin-x-ds/settings/SettingGroup.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/settings/SettingGroup.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import * as SettingGroupContentStories from './SettingGroupContent.stories'; import * as SettingGroupHeaderStories from './SettingGroupHeader.stories'; @@ -12,7 +13,7 @@ const meta = { title: 'Settings / Setting Group', component: SettingGroup, tags: ['autodocs'], - decorators: [(_story: any) =>
{_story()}
], + decorators: [(_story: () => ReactNode) =>
{_story()}
], argTypes: { description: { control: 'text' diff --git a/apps/admin-x-settings/src/admin-x-ds/settings/SettingSection.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/settings/SettingSection.stories.tsx index c86f6a7d07..bd6b6c8625 100644 --- a/apps/admin-x-settings/src/admin-x-ds/settings/SettingSection.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/settings/SettingSection.stories.tsx @@ -1,4 +1,5 @@ -import type {Meta, StoryObj} from '@storybook/react'; +import { ReactNode } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; import * as SettingGroupStories from './SettingGroup.stories'; import SettingGroup from './SettingGroup'; @@ -8,7 +9,7 @@ const meta = { title: 'Settings / Setting Section', component: SettingSection, tags: ['autodocs'], - decorators: [(_story: any) =>
{_story()}
] + decorators: [(_story: () => ReactNode) =>
{_story()}
] } satisfies Meta; export default meta; diff --git a/apps/admin-x-settings/src/components/Settings.tsx b/apps/admin-x-settings/src/components/Settings.tsx index cc37db1ced..6ad45c4fc9 100644 --- a/apps/admin-x-settings/src/components/Settings.tsx +++ b/apps/admin-x-settings/src/components/Settings.tsx @@ -1,23 +1,11 @@ -import React, {useContext} from 'react'; +import React from 'react'; import EmailSettings from './settings/email/EmailSettings'; import GeneralSettings from './settings/general/GeneralSettings'; import MembershipSettings from './settings/membership/MembershipSettings'; import SiteSettings from './settings/site/SiteSettings'; -import {SettingsContext} from './providers/SettingsProvider'; const Settings: React.FC = () => { - const {settings} = useContext(SettingsContext) || {}; - - // Show loader while settings is first fetched - if (!settings) { - return ( -
-
Loading...
-
- ); - } - return ( <> diff --git a/apps/admin-x-settings/src/components/providers/DataProvider.tsx b/apps/admin-x-settings/src/components/providers/DataProvider.tsx index 2617c5b498..59ebf72b70 100644 --- a/apps/admin-x-settings/src/components/providers/DataProvider.tsx +++ b/apps/admin-x-settings/src/components/providers/DataProvider.tsx @@ -1,22 +1,82 @@ -import React from 'react'; -import {RolesProvider} from './RolesProvider'; -import {SettingsProvider} from './SettingsProvider'; -import {UsersProvider} from './UsersProvider'; +import React, {ReactNode, createContext, useContext} from 'react'; +import {Config, Setting, SiteData, Tier, User} from '../../types/api'; +import {UserInvite, useBrowseInvites} from '../../utils/api/invites'; +import {useBrowseConfig} from '../../utils/api/config'; +import {useBrowseSettings} from '../../utils/api/settings'; +import {useBrowseSite} from '../../utils/api/site'; +import {useBrowseTiers} from '../../utils/api/tiers'; +import {useBrowseUsers, useCurrentUser} from '../../utils/api/users'; type DataProviderProps = { children: React.ReactNode; }; +interface GlobalData { + settings: Setting[] + siteData: SiteData + config: Config + users: User[] + currentUser: User + invites: UserInvite[] + tiers: Tier[] +} + +const GlobalDataContext = createContext(undefined); + +const GlobalDataProvider = ({children}: { children: ReactNode }) => { + const settings = useBrowseSettings(); + const site = useBrowseSite(); + const config = useBrowseConfig(); + const users = useBrowseUsers(); + const currentUser = useCurrentUser(); + const invites = useBrowseInvites(); + const tiers = useBrowseTiers(); + + const requests = [ + settings, + site, + config, + users, + currentUser, + invites, + tiers + ]; + + const error = requests.map(request => request.error).find(Boolean); + + if (error) { + throw error; + } + + if (requests.some(request => request.isLoading)) { + return ( +
+
Loading...
+
+ ); + } + + return + {children} + ; +}; + +export const useGlobalData = () => useContext(GlobalDataContext)!; + const DataProvider: React.FC = ({children}) => { return ( - - - - {children} - - - + + {children} + ); }; -export default DataProvider; \ No newline at end of file +export default DataProvider; diff --git a/apps/admin-x-settings/src/components/providers/RolesProvider.tsx b/apps/admin-x-settings/src/components/providers/RolesProvider.tsx deleted file mode 100644 index e7b8b840db..0000000000 --- a/apps/admin-x-settings/src/components/providers/RolesProvider.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, {createContext, useContext, useEffect, useState} from 'react'; -import {ServicesContext} from './ServiceProvider'; -import {UserRole} from '../../types/api'; - -interface RolesContextProps { - roles: UserRole[]; - assignableRoles: UserRole[]; -} - -interface RolesProviderProps { - children?: React.ReactNode; -} - -const RolesContext = createContext({ - roles: [], - assignableRoles: [] -}); - -const RolesProvider: React.FC = ({children}) => { - const {api} = useContext(ServicesContext); - const [roles, setRoles] = useState ([]); - const [assignableRoles, setAssignableRoles] = useState ([]); - - useEffect(() => { - const fetchRoles = async (): Promise => { - try { - const rolesData = await api.roles.browse(); - const assignableRolesData = await api.roles.browse({ - queryParams: { - permissions: 'assign' - } - }); - setRoles(rolesData.roles); - setAssignableRoles(assignableRolesData.roles); - } catch (error) { - // Log error in API - } - }; - - fetchRoles(); - }, [api]); - - return ( - - {children} - - ); -}; - -export {RolesContext, RolesProvider}; diff --git a/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx b/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx index 7a59b272bd..5e10cd07bb 100644 --- a/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx +++ b/apps/admin-x-settings/src/components/providers/RoutingProvider.tsx @@ -1,13 +1,12 @@ -import ChangeThemeModal from '../settings/site/ThemeModal'; -import DesignModal from '../settings/site/DesignModal'; -import InviteUserModal from '../settings/general/InviteUserModal'; -import NavigationModal from '../settings/site/NavigationModal'; -import NiceModal from '@ebay/nice-modal-react'; -import PortalModal from '../settings/membership/portal/PortalModal'; -import React, {createContext, useCallback, useContext, useEffect, useState} from 'react'; -import StripeConnectModal from '../settings/membership/stripe/StripeConnectModal'; -import TierDetailModal from '../settings/membership/tiers/TierDetailModal'; -import {SettingsContext} from './SettingsProvider'; +import ChangeThemeModal from "../settings/site/ThemeModal"; +import DesignModal from "../settings/site/DesignModal"; +import InviteUserModal from "../settings/general/InviteUserModal"; +import NavigationModal from "../settings/site/NavigationModal"; +import NiceModal from "@ebay/nice-modal-react"; +import PortalModal from "../settings/membership/portal/PortalModal"; +import React, { createContext, useCallback, useEffect, useState } from "react"; +import StripeConnectModal from "../settings/membership/stripe/StripeConnectModal"; +import TierDetailModal from "../settings/membership/tiers/TierDetailModal"; type RoutingContextProps = { route: string; @@ -18,11 +17,11 @@ type RoutingContextProps = { }; export const RouteContext = createContext({ - route: '', - scrolledRoute: '', + route: "", + scrolledRoute: "", yScroll: 0, updateRoute: () => {}, - updateScrolled: () => {} + updateScrolled: () => {}, }); function getHashPath(urlPath: string | undefined) { @@ -42,9 +41,9 @@ function getHashPath(urlPath: string | undefined) { const scrollToSectionGroup = (pathName: string) => { const element = document.getElementById(pathName); if (element) { - element.scrollIntoView({behavior: 'smooth'}); + element.scrollIntoView({ behavior: "smooth" }); } -} +}; const handleNavigation = (scroll: boolean = true) => { // Get the hash from the URL @@ -57,19 +56,19 @@ const handleNavigation = (scroll: boolean = true) => { const pathName = getHashPath(hash); if (pathName) { - if (pathName === 'design/edit/themes') { + if (pathName === "design/edit/themes") { NiceModal.show(ChangeThemeModal); - } else if (pathName === 'design/edit') { + } else if (pathName === "design/edit") { NiceModal.show(DesignModal); - } else if (pathName === 'navigation/edit') { + } else if (pathName === "navigation/edit") { NiceModal.show(NavigationModal); - } else if (pathName === 'users/invite') { + } else if (pathName === "users/invite") { NiceModal.show(InviteUserModal); - } else if (pathName === 'portal/edit') { + } else if (pathName === "portal/edit") { NiceModal.show(PortalModal); - } else if (pathName === 'tiers/add') { + } else if (pathName === "tiers/add") { NiceModal.show(TierDetailModal); - } else if (pathName === 'stripe-connect') { + } else if (pathName === "stripe-connect") { NiceModal.show(StripeConnectModal); } @@ -79,31 +78,32 @@ const handleNavigation = (scroll: boolean = true) => { return pathName; } - return ''; + return ""; }; type RouteProviderProps = { children: React.ReactNode; }; -const RoutingProvider: React.FC = ({children}) => { - const [route, setRoute] = useState(''); +const RoutingProvider: React.FC = ({ children }) => { + const [route, setRoute] = useState(""); const [yScroll, setYScroll] = useState(0); - const [scrolledRoute, setScrolledRoute] = useState(''); + const [scrolledRoute, setScrolledRoute] = useState(""); - const {settingsLoaded} = useContext(SettingsContext) || {}; - - const updateRoute = useCallback((newPath: string) => { - if (newPath) { - if (newPath === route) { - scrollToSectionGroup(newPath); + const updateRoute = useCallback( + (newPath: string) => { + if (newPath) { + if (newPath === route) { + scrollToSectionGroup(newPath); + } else { + window.location.hash = `/settings-x/${newPath}`; + } } else { - window.location.hash = `/settings-x/${newPath}`; + window.location.hash = `/settings-x`; } - } else { - window.location.hash = `/settings-x`; - } - }, [route]); + }, + [route] + ); const updateScrolled = useCallback((newPath: string) => { setScrolledRoute(newPath); @@ -116,37 +116,37 @@ const RoutingProvider: React.FC = ({children}) => { }; const handleScroll = () => { - const element = document.getElementById('admin-x-root'); + const element = document.getElementById("admin-x-root"); const scrollPosition = element!.scrollTop; setYScroll(scrollPosition); }; - const element = document.getElementById('admin-x-root'); - if (settingsLoaded) { - const matchedRoute = handleNavigation(); - setRoute(matchedRoute); - element!.addEventListener('scroll', handleScroll); - } + const element = document.getElementById("admin-x-root"); + const matchedRoute = handleNavigation(); + setRoute(matchedRoute); + element!.addEventListener("scroll", handleScroll); - window.addEventListener('hashchange', handleHashChange); + window.addEventListener("hashchange", handleHashChange); return () => { - element!.removeEventListener('scroll', handleScroll); - window.removeEventListener('hashchange', handleHashChange); + element!.removeEventListener("scroll", handleScroll); + window.removeEventListener("hashchange", handleHashChange); }; - }, [settingsLoaded]); + }, []); return ( - + {children} ); }; -export default RoutingProvider; \ No newline at end of file +export default RoutingProvider; diff --git a/apps/admin-x-settings/src/components/providers/ServiceProvider.tsx b/apps/admin-x-settings/src/components/providers/ServiceProvider.tsx index 337ef40e70..1c29c0b703 100644 --- a/apps/admin-x-settings/src/components/providers/ServiceProvider.tsx +++ b/apps/admin-x-settings/src/components/providers/ServiceProvider.tsx @@ -1,19 +1,11 @@ -import React, {createContext, useContext, useMemo} from 'react'; -import setupGhostApi from '../../utils/api'; -import useDataService, {DataService, bulkEdit, placeholderDataService} from '../../utils/dataService'; +import React, {createContext, useContext} from 'react'; import useSearchService, {SearchService} from '../../utils/search'; import {OfficialTheme} from '../../models/themes'; -import {Tier} from '../../types/api'; -export interface FileService { - uploadImage: (file: File) => Promise; -} interface ServicesContextProps { - api: ReturnType; - fileService: FileService|null; + ghostVersion: string officialThemes: OfficialTheme[]; search: SearchService - tiers: DataService } interface ServicesProviderProps { @@ -23,36 +15,19 @@ interface ServicesProviderProps { } const ServicesContext = createContext({ - api: setupGhostApi({ghostVersion: ''}), - fileService: null, + ghostVersion: '', officialThemes: [], - search: {filter: '', setFilter: () => {}, checkVisible: () => true}, - tiers: placeholderDataService + search: {filter: '', setFilter: () => {}, checkVisible: () => true} }); const ServicesProvider: React.FC = ({children, ghostVersion, officialThemes}) => { - const apiService = useMemo(() => setupGhostApi({ghostVersion}), [ghostVersion]); - const fileService = useMemo(() => ({ - uploadImage: async (file: File): Promise => { - const response = await apiService.images.upload({file}); - return response.images[0].url; - } - }), [apiService]); const search = useSearchService(); - const tiers = useDataService({ - key: 'tiers', - browse: apiService.tiers.browse, - edit: bulkEdit('tiers', apiService.tiers.edit), - add: apiService.tiers.add - }); return ( {children} @@ -63,10 +38,6 @@ export {ServicesContext, ServicesProvider}; export const useServices = () => useContext(ServicesContext); -export const useApi = () => useServices().api; - export const useOfficialThemes = () => useServices().officialThemes; export const useSearch = () => useServices().search; - -export const useTiers = () => useServices().tiers; diff --git a/apps/admin-x-settings/src/components/providers/SettingsProvider.tsx b/apps/admin-x-settings/src/components/providers/SettingsProvider.tsx deleted file mode 100644 index f33caeb767..0000000000 --- a/apps/admin-x-settings/src/components/providers/SettingsProvider.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React, {createContext, useCallback, useContext, useEffect, useState} from 'react'; -import {Config, Setting, SiteData} from '../../types/api'; -import {ServicesContext} from './ServiceProvider'; -import {SettingsResponseType} from '../../utils/api'; - -// Define the Settings Context -interface SettingsContextProps { - settings: Setting[] | null; - saveSettings: (updatedSettings: Setting[]) => Promise; - siteData: SiteData | null; - config: Config | null; - settingsLoaded: boolean; -} - -interface SettingsProviderProps { - children?: React.ReactNode; -} - -const SettingsContext = createContext({ - settings: null, - siteData: null, - config: null, - settingsLoaded: false, - saveSettings: async () => ({settings: []}) -}); - -function serialiseSettingsData(settings: Setting[]): Setting[] { - return settings.map((setting) => { - if (setting.key === 'facebook' && setting.value) { - const value = setting.value as string; - let [, user] = value.match(/(\S+)/) || []; - - return { - key: setting.key, - value: `https://www.facebook.com/${user}` - }; - } - if (setting.key === 'twitter' && setting.value) { - const value = setting.value as string; - let [, user] = value.match(/@?([^/]*)/) || []; - - return { - key: setting.key, - value: `https://twitter.com/${user}` - }; - } - - return { - key: setting.key, - value: setting.value - }; - }); -} - -function deserializeSettings(settings: Setting[]): Setting[] { - return settings.map((setting) => { - if (setting.key === 'facebook' && setting.value) { - const deserialized = setting.value as string; - let [, user] = deserialized.match(/(?:https:\/\/)(?:www\.)(?:facebook\.com)\/(?:#!\/)?(\w+\/?\S+)/mi) || []; - - return { - key: setting.key, - value: user - }; - } - - if (setting.key === 'twitter' && setting.value) { - const deserialized = setting.value as string; - let [, user] = deserialized.match(/(?:https:\/\/)(?:twitter\.com)\/(?:#!\/)?@?([^/]*)/) || []; - - return { - key: setting.key, - value: `@${user}` - }; - } - - return { - key: setting.key, - value: setting.value - }; - }); -} - -// Create a Settings Provider component -const SettingsProvider: React.FC = ({children}) => { - const {api} = useContext(ServicesContext); - const [settings, setSettings] = useState (null); - const [siteData, setSiteData] = useState (null); - const [config, setConfig] = useState (null); - const [settingsLoaded, setSettingsLoaded] = useState (false); - - useEffect(() => { - const fetchSettings = async (): Promise => { - try { - // Make an API call to fetch the settings - const [settingsData, siteDataResponse, configData] = await Promise.all([ - api.settings.browse(), - api.site.browse(), - api.config.browse() - ]); - - setSettings(serialiseSettingsData(settingsData.settings)); - setSiteData(siteDataResponse.site); - setConfig(configData.config); - setSettingsLoaded(true); - } catch (error) { - // Log error in settings API - } - }; - - // Fetch the initial settings from the API - fetchSettings(); - }, [api]); - - const saveSettings = useCallback(async (updatedSettings: Setting[]) => { - try { - // handle transformation for settings before save - updatedSettings = deserializeSettings(updatedSettings); - // Make an API call to save the updated settings - const data = await api.settings.edit(updatedSettings); - const newSettings = serialiseSettingsData(data.settings); - - setSettings(newSettings); - - return { - settings: newSettings, - meta: data.meta - }; - } catch (error) { - // Log error in settings API - return {settings: []}; - } - }, [api]); - - // Provide the settings and the saveSettings function to the children components - return ( - - {children} - - ); -}; - -export {SettingsContext, SettingsProvider}; - diff --git a/apps/admin-x-settings/src/components/providers/UsersProvider.tsx b/apps/admin-x-settings/src/components/providers/UsersProvider.tsx deleted file mode 100644 index 812472097f..0000000000 --- a/apps/admin-x-settings/src/components/providers/UsersProvider.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, {createContext, useCallback, useContext, useEffect, useState} from 'react'; -import {ServicesContext} from './ServiceProvider'; -import {User} from '../../types/api'; -import {UserInvite} from '../../utils/api'; - -interface UsersContextProps { - users: User[]; - invites: UserInvite[]; - currentUser: User|null; - updateUser?: (user: User) => Promise; - setInvites: (invites: UserInvite[]) => void; - setUsers: React.Dispatch> -} - -interface UsersProviderProps { - children?: React.ReactNode; -} - -const UsersContext = createContext({ - users: [], - invites: [], - currentUser: null, - setInvites: () => {}, - setUsers: () => {} -}); - -const UsersProvider: React.FC = ({children}) => { - const {api} = useContext(ServicesContext); - const [users, setUsers] = useState ([]); - const [invites, setInvites] = useState ([]); - const [currentUser, setCurrentUser] = useState (null); - - useEffect(() => { - const fetchUsers = async (): Promise => { - try { - // get list of staff users from the API - const data = await api.users.browse(); - const user = await api.users.currentUser(); - const invitesRes = await api.invites.browse(); - setUsers(data.users); - setCurrentUser(user); - setInvites(invitesRes.invites); - } catch (error) { - // Log error in API - } - }; - - fetchUsers(); - }, [api]); - - const updateUser = useCallback(async (user: User): Promise => { - try { - // Make an API call to save the updated settings - const data = await api.users.edit(user); - setUsers((usersState) => { - return usersState.map((u) => { - if (u.id === user.id) { - return data.users[0]; - } - return u; - }); - }); - } catch (error) { - // Log error in settings API - } - }, [api]); - - return ( - - {children} - - ); -}; - -export {UsersContext, UsersProvider}; diff --git a/apps/admin-x-settings/src/components/settings/email/DefaultRecipients.tsx b/apps/admin-x-settings/src/components/settings/email/DefaultRecipients.tsx index f1913e9664..1991e6be89 100644 --- a/apps/admin-x-settings/src/components/settings/email/DefaultRecipients.tsx +++ b/apps/admin-x-settings/src/components/settings/email/DefaultRecipients.tsx @@ -1,13 +1,14 @@ import MultiSelect, {MultiSelectOption} from '../../../admin-x-ds/global/form/MultiSelect'; -import React, {useContext, useEffect, useState} from 'react'; +import React, {useState} from 'react'; import Select from '../../../admin-x-ds/global/form/Select'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent'; import useSettingGroup from '../../../hooks/useSettingGroup'; import {GroupBase, MultiValue} from 'react-select'; -import {Label, Offer, Tier} from '../../../types/api'; -import {ServicesContext} from '../../providers/ServiceProvider'; -import {getOptionLabel, getPaidActiveTiers, getSettingValues} from '../../../utils/helpers'; +import {getOptionLabel, getSettingValues} from '../../../utils/helpers'; +import {useBrowseLabels} from '../../../utils/api/labels'; +import {useBrowseOffers} from '../../../utils/api/offers'; +import {useGlobalData} from '../../providers/DataProvider'; type RefipientValueArgs = { defaultEmailRecipients: string; @@ -80,24 +81,9 @@ const DefaultRecipients: React.FC<{ keywords: string[] }> = ({keywords}) => { defaultEmailRecipientsFilter })); - const {api} = useContext(ServicesContext); - const [tiers, setTiers] = useState([]); - const [labels, setLabels] = useState([]); - const [offers, setOffers] = useState([]); - - useEffect(() => { - api.tiers.browse().then((response) => { - setTiers(getPaidActiveTiers(response.tiers)); - }); - - api.labels.browse().then((response) => { - setLabels(response.labels); - }); - - api.offers.browse().then((response) => { - setOffers(response.offers); - }); - }, [api]); + const {tiers} = useGlobalData(); + const {data: {labels} = {}} = useBrowseLabels(); + const {data: {offers} = {}} = useBrowseOffers(); const setDefaultRecipientValue = (value: string) => { if (['visibility', 'disabled'].includes(value)) { @@ -136,11 +122,11 @@ const DefaultRecipients: React.FC<{ keywords: string[] }> = ({keywords}) => { }, { label: 'Labels', - options: labels.map(label => ({value: `label:${label.slug}`, label: label.name, color: 'grey'})) + options: labels?.map(label => ({value: `label:${label.slug}`, label: label.name, color: 'grey'})) || [] }, { label: 'Offers', - options: offers.map(offer => ({value: `offer_redemptions:${offer.id}`, label: offer.name, color: 'black'})) + options: offers?.map(offer => ({value: `offer_redemptions:${offer.id}`, label: offer.name, color: 'black'})) || [] } ]; diff --git a/apps/admin-x-settings/src/components/settings/general/Facebook.tsx b/apps/admin-x-settings/src/components/settings/general/Facebook.tsx index b35520e32a..de2ca12ce0 100644 --- a/apps/admin-x-settings/src/components/settings/general/Facebook.tsx +++ b/apps/admin-x-settings/src/components/settings/general/Facebook.tsx @@ -1,11 +1,11 @@ import ImageUpload from '../../../admin-x-ds/global/form/ImageUpload'; -import React, {useContext} from 'react'; +import React from 'react'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent'; import TextField from '../../../admin-x-ds/global/form/TextField'; import useSettingGroup from '../../../hooks/useSettingGroup'; import {ReactComponent as FacebookLogo} from '../../../admin-x-ds/assets/images/facebook-logo.svg'; -import {FileService, ServicesContext} from '../../providers/ServiceProvider'; +import {getImageUrl, useUploadImage} from '../../../utils/api/images'; import {getSettingValues} from '../../../utils/helpers'; const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => { @@ -20,7 +20,7 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => { handleEditingChange } = useSettingGroup(); - const {fileService} = useContext(ServicesContext) as {fileService: FileService}; + const {mutateAsync: uploadImage} = useUploadImage(); const [ facebookTitle, facebookDescription, facebookImage, siteTitle, siteDescription @@ -35,7 +35,7 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => { }; const handleImageUpload = async (file: File) => { - const imageUrl = await fileService.uploadImage(file); + const imageUrl = getImageUrl(await uploadImage({file})); updateSetting('og_image', imageUrl); }; diff --git a/apps/admin-x-settings/src/components/settings/general/InviteUserModal.tsx b/apps/admin-x-settings/src/components/settings/general/InviteUserModal.tsx index 9c2feb01b5..6d92fa53e9 100644 --- a/apps/admin-x-settings/src/components/settings/general/InviteUserModal.tsx +++ b/apps/admin-x-settings/src/components/settings/general/InviteUserModal.tsx @@ -2,20 +2,19 @@ import Modal from '../../../admin-x-ds/global/modal/Modal'; import NiceModal from '@ebay/nice-modal-react'; import Radio from '../../../admin-x-ds/global/form/Radio'; import TextField from '../../../admin-x-ds/global/form/TextField'; -import useRoles from '../../../hooks/useRoles'; import useRouting from '../../../hooks/useRouting'; -import useStaffUsers from '../../../hooks/useStaffUsers'; import validator from 'validator'; -import {ServicesContext} from '../../providers/ServiceProvider'; -import {showToast} from '../../../admin-x-ds/global/Toast'; -import {useContext, useEffect, useRef, useState} from 'react'; +import { showToast } from '../../../admin-x-ds/global/Toast'; +import { useAddInvite } from '../../../utils/api/invites'; +import { useBrowseRoles } from '../../../utils/api/roles'; +import { useEffect, useRef, useState } from 'react'; type RoleType = 'administrator' | 'editor' | 'author' | 'contributor'; const InviteUserModal = NiceModal.create(() => { - const {api} = useContext(ServicesContext); - const {roles, assignableRoles, getRoleId} = useRoles(); - const {invites, setInvites} = useStaffUsers(); + const rolesQuery = useBrowseRoles(); + const assignableRolesQuery = useBrowseRoles({limit: 'all', permissions: 'assign'}); + const {updateRoute} = useRouting(); const focusRef = useRef(null); @@ -26,6 +25,8 @@ const InviteUserModal = NiceModal.create(() => { email?: string; }>({}); + const {mutateAsync: addInvite} = useAddInvite(); + useEffect(() => { if (focusRef.current) { focusRef.current.focus(); @@ -40,6 +41,13 @@ const InviteUserModal = NiceModal.create(() => { } }, [saveState]); + if (!rolesQuery.data?.roles || !assignableRolesQuery.data?.roles) { + return null; + } + + const roles = rolesQuery.data.roles; + const assignableRoles = assignableRolesQuery.data.roles; + let okLabel = 'Send invitation now'; if (saveState === 'saving') { okLabel = 'Sending...'; @@ -62,21 +70,18 @@ const InviteUserModal = NiceModal.create(() => { } setSaveState('saving'); try { - const res = await api.invites.add({ + await addInvite({ email, - roleId: getRoleId(role, roles) + roleId: roles.find(({name}) => name.toLowerCase() === role.toLowerCase())!.id }); - // Update invites list - setInvites([...invites, res.invites[0]]); - setSaveState('saved'); showToast({ message: `Invitation successfully sent to ${email}`, type: 'success' }); - } catch (e: any) { + } catch (e) { setSaveState('error'); showToast({ diff --git a/apps/admin-x-settings/src/components/settings/general/SocialAccounts.tsx b/apps/admin-x-settings/src/components/settings/general/SocialAccounts.tsx index 939e9731b6..c5e5f1146d 100644 --- a/apps/admin-x-settings/src/components/settings/general/SocialAccounts.tsx +++ b/apps/admin-x-settings/src/components/settings/general/SocialAccounts.tsx @@ -1,10 +1,10 @@ -import React, {useRef, useState} from 'react'; +import React, { useRef, useState } from 'react'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent'; import TextField from '../../../admin-x-ds/global/form/TextField'; import useSettingGroup from '../../../hooks/useSettingGroup'; import validator from 'validator'; -import {getSettingValues} from '../../../utils/helpers'; +import { getSettingValues } from '../../../utils/helpers'; function validateFacebookUrl(newUrl: string) { const errMessage = 'The URL must be in a format like https://www.facebook.com/yourPage'; @@ -121,7 +121,7 @@ const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => { if (focusRef.current) { focusRef.current.value = newUrl; } - } catch (err: any) { + } catch (err) { // ignore error } }} @@ -143,7 +143,7 @@ const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => { if (twitterInputRef.current) { twitterInputRef.current.value = newUrl; } - } catch (err: any) { + } catch (err) { // ignore error } }} @@ -172,14 +172,18 @@ const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => { } = {}; try { validateFacebookUrl(facebookUrl); - } catch (e: any) { - formErrors.facebook = e?.message; + } catch (e) { + if (e instanceof Error) { + formErrors.facebook = e.message; + } } try { validateTwitterUrl(twitterUrl); - } catch (e: any) { - formErrors.twitter = e?.message; + } catch (e) { + if (e instanceof Error) { + formErrors.twitter = e.message; + } } setErrors(formErrors); diff --git a/apps/admin-x-settings/src/components/settings/general/Twitter.tsx b/apps/admin-x-settings/src/components/settings/general/Twitter.tsx index c3036c8142..fb5629ab26 100644 --- a/apps/admin-x-settings/src/components/settings/general/Twitter.tsx +++ b/apps/admin-x-settings/src/components/settings/general/Twitter.tsx @@ -1,12 +1,12 @@ import ImageUpload from '../../../admin-x-ds/global/form/ImageUpload'; -import React, {useContext} from 'react'; +import React from 'react'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent'; import TextField from '../../../admin-x-ds/global/form/TextField'; import useSettingGroup from '../../../hooks/useSettingGroup'; -import {FileService, ServicesContext} from '../../providers/ServiceProvider'; -import {ReactComponent as TwitterLogo} from '../../../admin-x-ds/assets/images/twitter-logo.svg'; -import {getSettingValues} from '../../../utils/helpers'; +import { ReactComponent as TwitterLogo } from '../../../admin-x-ds/assets/images/twitter-logo.svg'; +import { getImageUrl, useUploadImage } from '../../../utils/api/images'; +import { getSettingValues } from '../../../utils/helpers'; const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => { const { @@ -20,7 +20,7 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => { handleEditingChange } = useSettingGroup(); - const {fileService} = useContext(ServicesContext) as {fileService: FileService}; + const {mutateAsync: uploadImage} = useUploadImage(); const [ twitterTitle, twitterDescription, twitterImage, siteTitle, siteDescription @@ -36,10 +36,10 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => { const handleImageUpload = async (file: File) => { try { - const imageUrl = await fileService.uploadImage(file); + const imageUrl = getImageUrl(await uploadImage({file})); updateSetting('twitter_image', imageUrl); - } catch (err: any) { - // handle error + } catch (err) { + // TODO: handle error } }; diff --git a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx index 1a4aec4448..5bd53445cf 100644 --- a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx @@ -3,23 +3,24 @@ import ConfirmationModal from '../../../admin-x-ds/global/modal/ConfirmationModa import Heading from '../../../admin-x-ds/global/Heading'; import Icon from '../../../admin-x-ds/global/Icon'; import ImageUpload from '../../../admin-x-ds/global/form/ImageUpload'; -import Menu, {MenuItem} from '../../../admin-x-ds/global/Menu'; +import Menu, { MenuItem } from '../../../admin-x-ds/global/Menu'; import Modal from '../../../admin-x-ds/global/modal/Modal'; -import NiceModal, {useModal} from '@ebay/nice-modal-react'; +import NiceModal, { useModal } from '@ebay/nice-modal-react'; import Radio from '../../../admin-x-ds/global/form/Radio'; -import React, {useContext, useEffect, useRef, useState} from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent'; import TextField from '../../../admin-x-ds/global/form/TextField'; import Toggle from '../../../admin-x-ds/global/form/Toggle'; -import useRoles from '../../../hooks/useRoles'; import useStaffUsers from '../../../hooks/useStaffUsers'; import validator from 'validator'; -import {FileService, ServicesContext} from '../../providers/ServiceProvider'; -import {User} from '../../../types/api'; -import {isAdminUser, isOwnerUser} from '../../../utils/helpers'; -import {showToast} from '../../../admin-x-ds/global/Toast'; +import { User } from '../../../types/api'; +import { getImageUrl, useUploadImage } from '../../../utils/api/images'; +import { isAdminUser, isOwnerUser } from '../../../utils/helpers'; +import { showToast } from '../../../admin-x-ds/global/Toast'; import { toast } from 'react-hot-toast'; +import { useBrowseRoles } from '../../../utils/api/roles'; +import { useDeleteUser, useEditUser, useMakeOwner, useUpdatePassword } from '../../../utils/api/users'; interface CustomHeadingProps { children?: React.ReactNode; @@ -47,7 +48,8 @@ const CustomHeader: React.FC = ({children}) => { }; const RoleSelector: React.FC = ({user, setUserData}) => { - const {roles} = useRoles(); + const {data: {roles} = {}} = useBrowseRoles(); + if (isOwnerUser(user)) { return ( <> @@ -303,7 +305,8 @@ const Password: React.FC = ({user}) => { }>({}); const newPasswordRef = useRef(null); const confirmNewPasswordRef = useRef(null); - const {api} = useContext(ServicesContext); + + const {mutateAsync: updatePassword} = useUpdatePassword(); useEffect(() => { if (saveState === 'saved') { @@ -378,7 +381,7 @@ const Password: React.FC = ({user}) => { return; } try { - await api.users.updatePassword({ + await updatePassword({ newPassword, confirmNewPassword, oldPassword: '', @@ -408,7 +411,6 @@ const Password: React.FC = ({user}) => { interface UserDetailModalProps { user: User; - updateUser?: (user: User) => void; } const UserMenuTrigger = () => ( @@ -418,9 +420,8 @@ const UserMenuTrigger = () => ( ); -const UserDetailModal:React.FC = ({user, updateUser}) => { - const {api} = useContext(ServicesContext); - const {users, setUsers, ownerUser} = useStaffUsers(); +const UserDetailModal:React.FC = ({user}) => { + const {ownerUser} = useStaffUsers(); const [userData, setUserData] = useState(user); const [saveState, setSaveState] = useState(''); const [errors, setErrors] = useState<{ @@ -429,8 +430,11 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { url?: string; }>({}); - const {fileService} = useContext(ServicesContext) as {fileService: FileService}; const mainModal = useModal(); + const {mutateAsync: uploadImage} = useUploadImage(); + const {mutateAsync: updateUser} = useEditUser(); + const {mutateAsync: deleteUser} = useDeleteUser(); + const {mutateAsync: makeOwner} = useMakeOwner(); const confirmSuspend = (_user: User) => { let warningText = 'This user will no longer be able to log in but their posts will be kept.'; @@ -452,16 +456,7 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { ..._user, status: _user.status === 'inactive' ? 'active' : 'inactive' }; - const res = await api.users.edit(updatedUserData); - const updatedUser = res.users[0]; - setUsers((_users) => { - return _users.map((u) => { - if (u.id === updatedUser.id) { - return updatedUser; - } - return u; - }); - }); + await updateUser(updatedUserData); setUserData(updatedUserData); modal?.remove(); showToast({ @@ -484,9 +479,7 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { okLabel: 'Delete user', okColor: 'red', onOk: async (modal) => { - await api.users.delete(_user?.id); - const newUsers = users.filter(u => u.id !== _user.id); - setUsers(newUsers); + await deleteUser(_user?.id); modal?.remove(); mainModal?.remove(); showToast({ @@ -504,8 +497,7 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { okLabel: 'Yep — I\'m sure', okColor: 'red', onOk: async (modal) => { - const res = await api.users.makeOwner(user.id); - setUsers(res.users); + await makeOwner(user.id); modal?.remove(); showToast({ message: 'Ownership transferred', @@ -517,7 +509,7 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { const handleImageUpload = async (image: string, file: File) => { try { - const imageUrl = await fileService.uploadImage(file); + const imageUrl = getImageUrl(await uploadImage({file})); switch (image) { case 'cover_image': @@ -531,8 +523,8 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { }); break; } - } catch (err: any) { - // handle error + } catch (err) { + // TODO: handle error } }; diff --git a/apps/admin-x-settings/src/components/settings/general/Users.tsx b/apps/admin-x-settings/src/components/settings/general/Users.tsx index 6a06d163e6..8fd8f4462b 100644 --- a/apps/admin-x-settings/src/components/settings/general/Users.tsx +++ b/apps/admin-x-settings/src/components/settings/general/Users.tsx @@ -4,15 +4,14 @@ import List from '../../../admin-x-ds/global/List'; import ListItem from '../../../admin-x-ds/global/ListItem'; import NiceModal from '@ebay/nice-modal-react'; import NoValueLabel from '../../../admin-x-ds/global/NoValueLabel'; -import React, {useContext, useState} from 'react'; +import React, {useState} from 'react'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import TabView from '../../../admin-x-ds/global/TabView'; import UserDetailModal from './UserDetailModal'; import useRouting from '../../../hooks/useRouting'; import useStaffUsers from '../../../hooks/useStaffUsers'; -import {ServicesContext} from '../../providers/ServiceProvider'; import {User} from '../../../types/api'; -import {UserInvite} from '../../../utils/api'; +import {UserInvite, useAddInvite, useDeleteInvite} from '../../../utils/api/invites'; import {generateAvatarColor, getInitials} from '../../../utils/helpers'; import {showToast} from '../../../admin-x-ds/global/Toast'; @@ -31,9 +30,9 @@ interface InviteListProps { updateUser?: (user: User) => void; } -const Owner: React.FC = ({user, updateUser}) => { +const Owner: React.FC = ({user}) => { const showDetailModal = () => { - NiceModal.show(UserDetailModal, {user, updateUser}); + NiceModal.show(UserDetailModal, {user}); }; if (!user) { @@ -51,9 +50,9 @@ const Owner: React.FC = ({user, updateUser}) => { ); }; -const UsersList: React.FC = ({users, updateUser}) => { +const UsersList: React.FC = ({users}) => { const showDetailModal = (user: User) => { - NiceModal.show(UserDetailModal, {user, updateUser}); + NiceModal.show(UserDetailModal, {user}); }; if (!users || !users.length) { @@ -91,10 +90,12 @@ const UsersList: React.FC = ({users, updateUser}) => { }; const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { - const {api} = useContext(ServicesContext); - const {setInvites} = useStaffUsers(); const [revokeState, setRevokeState] = useState<'progress'|''>(''); const [resendState, setResendState] = useState<'progress'|''>(''); + + const {mutateAsync: deleteInvite} = useDeleteInvite(); + const {mutateAsync: addInvite} = useAddInvite(); + let revokeActionLabel = 'Revoke'; if (revokeState === 'progress') { revokeActionLabel = 'Revoking...'; @@ -111,9 +112,7 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { link={true} onClick={async () => { setRevokeState('progress'); - await api.invites.delete(invite.id); - const res = await api.invites.browse(); - setInvites(res.invites); + await deleteInvite(invite.id); setRevokeState(''); showToast({ message: `Invitation revoked (${invite.email})`, @@ -128,13 +127,11 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { link={true} onClick={async () => { setResendState('progress'); - await api.invites.delete(invite.id); - await api.invites.add({ + await deleteInvite(invite.id); + await addInvite({ email: invite.email, roleId: invite.role_id }); - const res = await api.invites.browse(); - setInvites(res.invites); setResendState(''); showToast({ message: `Invitation resent! (${invite.email})`, @@ -187,8 +184,7 @@ const Users: React.FC<{ keywords: string[] }> = ({keywords}) => { editorUsers, authorUsers, contributorUsers, - invites, - updateUser + invites } = useStaffUsers(); const {updateRoute} = useRouting(); const showInviteModal = () => { @@ -207,27 +203,27 @@ const Users: React.FC<{ keywords: string[] }> = ({keywords}) => { { id: 'users-admins', title: 'Administrators', - contents: () + contents: () }, { id: 'users-editors', title: 'Editors', - contents: () + contents: () }, { id: 'users-authors', title: 'Authors', - contents: () + contents: () }, { id: 'users-contributors', title: 'Contributors', - contents: () + contents: () }, { id: 'users-invited', title: 'Invited', - contents: () + contents: () } ]; @@ -239,7 +235,7 @@ const Users: React.FC<{ keywords: string[] }> = ({keywords}) => { testId='users' title='Users and permissions' > - + ); diff --git a/apps/admin-x-settings/src/components/settings/membership/Access.tsx b/apps/admin-x-settings/src/components/settings/membership/Access.tsx index 7797b703c3..70a8589f16 100644 --- a/apps/admin-x-settings/src/components/settings/membership/Access.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/Access.tsx @@ -1,13 +1,12 @@ import MultiSelect, {MultiSelectOption} from '../../../admin-x-ds/global/form/MultiSelect'; -import React, {useContext, useEffect, useState} from 'react'; +import React from 'react'; import Select from '../../../admin-x-ds/global/form/Select'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent'; import useSettingGroup from '../../../hooks/useSettingGroup'; import {GroupBase, MultiValue} from 'react-select'; -import {ServicesContext} from '../../providers/ServiceProvider'; -import {Tier} from '../../../types/api'; -import {getOptionLabel, getPaidActiveTiers, getSettingValues} from '../../../utils/helpers'; +import {getOptionLabel, getSettingValues} from '../../../utils/helpers'; +import {useGlobalData} from '../../providers/DataProvider'; const MEMBERS_SIGNUP_ACCESS_OPTIONS = [ {value: 'all', label: 'Anyone can sign up'}, @@ -47,14 +46,7 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => { const defaultContentVisibilityLabel = getOptionLabel(DEFAULT_CONTENT_VISIBILITY_OPTIONS, defaultContentVisibility); const commentsEnabledLabel = getOptionLabel(COMMENTS_ENABLED_OPTIONS, commentsEnabled); - const {api} = useContext(ServicesContext); - const [tiers, setTiers] = useState([]); - - useEffect(() => { - api.tiers.browse().then((response) => { - setTiers(getPaidActiveTiers(response.tiers)); - }); - }, [api]); + const {tiers} = useGlobalData(); const tierOptionGroups: GroupBase[] = [ { diff --git a/apps/admin-x-settings/src/components/settings/membership/Tiers.tsx b/apps/admin-x-settings/src/components/settings/membership/Tiers.tsx index 082b1d87c1..f9a73a5d2b 100644 --- a/apps/admin-x-settings/src/components/settings/membership/Tiers.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/Tiers.tsx @@ -1,16 +1,16 @@ -import React, {useState} from 'react'; +import React, { useState } from 'react'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import StripeButton from '../../../admin-x-ds/settings/StripeButton'; import TabView from '../../../admin-x-ds/global/TabView'; import TiersList from './tiers/TiersList'; import useRouting from '../../../hooks/useRouting'; -import {Tier} from '../../../types/api'; -import {getActiveTiers, getArchivedTiers} from '../../../utils/helpers'; -import {useTiers} from '../../providers/ServiceProvider'; +import { Tier } from '../../../types/api'; +import { getActiveTiers, getArchivedTiers } from '../../../utils/helpers'; +import { useGlobalData } from '../../providers/DataProvider'; const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => { const [selectedTab, setSelectedTab] = useState('active-tiers'); - const {data: tiers, update: updateTier} = useTiers(); + const {tiers} = useGlobalData(); const activeTiers = getActiveTiers(tiers); const archivedTiers = getArchivedTiers(tiers); const {updateRoute} = useRouting(); @@ -34,12 +34,12 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => { { id: 'active-tiers', title: 'Active', - contents: () + contents: () }, { id: 'archived-tiers', title: 'Archived', - contents: () + contents: () } ]; diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/AccountPage.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/AccountPage.tsx index b00db33fd7..cccd774721 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/AccountPage.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/AccountPage.tsx @@ -1,9 +1,9 @@ import Form from '../../../../admin-x-ds/global/form/Form'; -import React, {FocusEventHandler, useContext, useState} from 'react'; +import React, {FocusEventHandler, useState} from 'react'; import TextField from '../../../../admin-x-ds/global/form/TextField'; import {Setting, SettingValue} from '../../../../types/api'; -import {SettingsContext} from '../../../providers/SettingsProvider'; import {fullEmailAddress, getEmailDomain, getSettingValues} from '../../../../utils/helpers'; +import {useGlobalData} from '../../../providers/DataProvider'; const AccountPage: React.FC<{ localSettings: Setting[] @@ -11,7 +11,7 @@ const AccountPage: React.FC<{ }> = ({localSettings, updateSetting}) => { const [membersSupportAddress] = getSettingValues(localSettings, ['members_support_address']); - const {siteData} = useContext(SettingsContext) || {}; + const {siteData} = useGlobalData(); const emailDomain = getEmailDomain(siteData!); const [value, setValue] = useState(fullEmailAddress(membersSupportAddress?.toString() || '', siteData!)); diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/LookAndFeel.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/LookAndFeel.tsx index 6272dcabda..c9ed12f35d 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/LookAndFeel.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/LookAndFeel.tsx @@ -1,5 +1,5 @@ import Form from '../../../../admin-x-ds/global/form/Form'; -import React, {useContext, useState} from 'react'; +import React, {useState} from 'react'; import Select from '../../../../admin-x-ds/global/form/Select'; import TextField from '../../../../admin-x-ds/global/form/TextField'; import Toggle from '../../../../admin-x-ds/global/form/Toggle'; @@ -15,7 +15,7 @@ import {ReactComponent as PortalIcon2} from '../../../../assets/icons/portal-ico import {ReactComponent as PortalIcon3} from '../../../../assets/icons/portal-icon-3.svg'; import {ReactComponent as PortalIcon4} from '../../../../assets/icons/portal-icon-4.svg'; import {ReactComponent as PortalIcon5} from '../../../../assets/icons/portal-icon-5.svg'; -import {ServicesContext} from '../../../providers/ServiceProvider'; +import {getImageUrl, useUploadImage} from '../../../../utils/api/images'; const defaultButtonIcons = [ { @@ -44,7 +44,7 @@ const LookAndFeel: React.FC<{ localSettings: Setting[] updateSetting: (key: string, setting: SettingValue) => void }> = ({localSettings, updateSetting}) => { - const {fileService} = useContext(ServicesContext); + const {mutateAsync: uploadImage} = useUploadImage(); const [portalButton, portalButtonStyle, portalButtonIcon, portalButtonSignupText] = getSettingValues(localSettings, ['portal_button', 'portal_button_style', 'portal_button_icon', 'portal_button_signup_text']); @@ -54,7 +54,7 @@ const LookAndFeel: React.FC<{ const [uploadedIcon, setUploadedIcon] = useState(isDefaultIcon ? undefined : currentIcon); const handleImageUpload = async (file: File) => { - const imageUrl = await fileService!.uploadImage(file); + const imageUrl = getImageUrl(await uploadImage({file})); updateSetting('portal_button_icon', imageUrl); setUploadedIcon(imageUrl); }; diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/PortalFrame.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/PortalFrame.tsx index d243de165d..c69a20e889 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/PortalFrame.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/PortalFrame.tsx @@ -1,7 +1,7 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import useSettingGroup from '../../../../hooks/useSettingGroup'; -import {Setting, SiteData, Tier} from '../../../../types/api'; -import {getSettingValue} from '../../../../utils/helpers'; +import { Setting, SiteData, Tier } from '../../../../types/api'; +import { getSettingValue } from '../../../../utils/helpers'; type PortalFrameProps = { settings: Setting[]; @@ -91,7 +91,7 @@ const PortalFrame: React.FC = ({settings, tiers, selectedTab}) }); useEffect(() => { - const messageListener = (event: any) => { + const messageListener = (event: MessageEvent<'portal-ready' | {type: string}>) => { if (!href) { return; } diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/PortalLinks.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/PortalLinks.tsx index 11cca717d9..b138800eb1 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/PortalLinks.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/PortalLinks.tsx @@ -2,12 +2,11 @@ import Button from '../../../../admin-x-ds/global/Button'; import List from '../../../../admin-x-ds/global/List'; import ListItem from '../../../../admin-x-ds/global/ListItem'; import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage'; -import React, {useContext, useEffect, useState} from 'react'; +import React, {useEffect, useState} from 'react'; import Select from '../../../../admin-x-ds/global/form/Select'; import TextField from '../../../../admin-x-ds/global/form/TextField'; -import {SettingsContext} from '../../../providers/SettingsProvider'; import {getHomepageUrl, getPaidActiveTiers} from '../../../../utils/helpers'; -import {useTiers} from '../../../providers/ServiceProvider'; +import {useGlobalData} from '../../../providers/DataProvider'; interface PortalLinkPrefs { name: string; @@ -39,8 +38,7 @@ const PortalLink: React.FC = ({name, value}) => { const PortalLinks: React.FC = () => { const [isDataAttributes, setIsDataAttributes] = useState(false); const [selectedTier, setSelectedTier] = useState(''); - const {siteData} = useContext(SettingsContext); - const {data: allTiers} = useTiers(); + const {siteData, tiers: allTiers} = useGlobalData(); const tiers = getPaidActiveTiers(allTiers); const toggleIsDataAttributes = () => { @@ -104,4 +102,4 @@ const PortalLinks: React.FC = () => { ); }; -export default PortalLinks; \ No newline at end of file +export default PortalLinks; diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/PortalModal.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/PortalModal.tsx index 06b62b5a06..c3c7ec81e6 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/PortalModal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/PortalModal.tsx @@ -3,16 +3,17 @@ import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationM import LookAndFeel from './LookAndFeel'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import PortalPreview from './PortalPreview'; -import React, {useContext, useState} from 'react'; +import React, {useState} from 'react'; import SignupOptions from './SignupOptions'; import TabView, {Tab} from '../../../../admin-x-ds/global/TabView'; import useForm, {Dirtyable} from '../../../../hooks/useForm'; import useRouting from '../../../../hooks/useRouting'; +import useSettings from '../../../../hooks/useSettings'; import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal'; import {Setting, SettingValue, Tier} from '../../../../types/api'; -import {SettingsContext} from '../../../providers/SettingsProvider'; import {fullEmailAddress, getPaidActiveTiers} from '../../../../utils/helpers'; -import {useTiers} from '../../../providers/ServiceProvider'; +import {useEditTier} from '../../../../utils/api/tiers'; +import {useGlobalData} from '../../../providers/DataProvider'; const Sidebar: React.FC<{ localSettings: Setting[] @@ -66,10 +67,12 @@ const PortalModal: React.FC = () => { const [selectedPreviewTab, setSelectedPreviewTab] = useState('signup'); - const {settings, saveSettings, siteData} = useContext(SettingsContext); - const {data: allTiers, update: updateTiers} = useTiers(); + const {settings, saveSettings, siteData} = useSettings(); + const {tiers: allTiers} = useGlobalData(); const tiers = getPaidActiveTiers(allTiers); + const {mutateAsync: editTier} = useEditTier(); + const {formState, saveState, handleSave, updateForm} = useForm({ initialState: { settings: settings as Dirtyable[], @@ -77,7 +80,8 @@ const PortalModal: React.FC = () => { }, onSave: async () => { - await updateTiers(...formState.tiers.filter(tier => tier.dirty)); + await Promise.all(formState.tiers.filter(({dirty}) => dirty).map(tier => editTier(tier))); + const {meta, settings: currentSettings} = await saveSettings(formState.settings.filter(setting => setting.dirty)); if (meta?.sent_email_verification) { diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/SignupOptions.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/SignupOptions.tsx index 24f3273dac..9914071bf6 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/SignupOptions.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/SignupOptions.tsx @@ -1,12 +1,12 @@ import CheckboxGroup from '../../../../admin-x-ds/global/form/CheckboxGroup'; import Form from '../../../../admin-x-ds/global/form/Form'; -import HtmlField from '../../../../admin-x-ds/global/form/HtmlField'; -import React, {useContext, useEffect, useMemo} from 'react'; +import HtmlField, { EditorConfig } from '../../../../admin-x-ds/global/form/HtmlField'; +import React, { useEffect, useMemo } from 'react'; import Toggle from '../../../../admin-x-ds/global/form/Toggle'; -import {CheckboxProps} from '../../../../admin-x-ds/global/form/Checkbox'; -import {Setting, SettingValue, Tier} from '../../../../types/api'; -import {SettingsContext} from '../../../providers/SettingsProvider'; -import {checkStripeEnabled, getSettingValues} from '../../../../utils/helpers'; +import { CheckboxProps } from '../../../../admin-x-ds/global/form/Checkbox'; +import { Setting, SettingValue, Tier } from '../../../../types/api'; +import { checkStripeEnabled, getSettingValues } from '../../../../utils/helpers'; +import { useGlobalData } from '../../../providers/DataProvider'; const SignupOptions: React.FC<{ localSettings: Setting[] @@ -16,7 +16,7 @@ const SignupOptions: React.FC<{ errors: Record setError: (key: string, error: string | undefined) => void }> = ({localSettings, updateSetting, localTiers, updateTier, errors, setError}) => { - const {config} = useContext(SettingsContext); + const {config} = useGlobalData(); const [membersSignupAccess, portalName, portalSignupTermsHtml, portalSignupCheckboxRequired, portalPlansJson] = getSettingValues( localSettings, ['members_signup_access', 'portal_name', 'portal_signup_terms_html', 'portal_signup_checkbox_required', 'portal_plans'] @@ -119,7 +119,7 @@ const SignupOptions: React.FC<{ )} Recommended: 115 characters. You've used {signupTermsLength}} nodes='MINIMAL_NODES' diff --git a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx index 78a6d5cb24..8b668718a1 100644 --- a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx @@ -3,8 +3,8 @@ import Form from '../../../../admin-x-ds/global/form/Form'; import Heading from '../../../../admin-x-ds/global/Heading'; import Icon from '../../../../admin-x-ds/global/Icon'; import Modal from '../../../../admin-x-ds/global/modal/Modal'; -import NiceModal, {useModal} from '@ebay/nice-modal-react'; -import React, {useState} from 'react'; +import NiceModal, { useModal } from '@ebay/nice-modal-react'; +import React, { useState } from 'react'; import Select from '../../../../admin-x-ds/global/form/Select'; import SortableList from '../../../../admin-x-ds/global/SortableList'; import TextField from '../../../../admin-x-ds/global/form/TextField'; @@ -14,12 +14,12 @@ import useForm from '../../../../hooks/useForm'; import useRouting from '../../../../hooks/useRouting'; import useSettingGroup from '../../../../hooks/useSettingGroup'; import useSortableIndexedList from '../../../../hooks/useSortableIndexedList'; -import {Tier} from '../../../../types/api'; -import {currencies, currencyFromDecimal, currencyGroups, currencyToDecimal, getSymbol} from '../../../../utils/currency'; -import {getSettingValues} from '../../../../utils/helpers'; -import {showToast} from '../../../../admin-x-ds/global/Toast'; +import { Tier } from '../../../../types/api'; +import { currencies, currencyFromDecimal, currencyGroups, currencyToDecimal, getSymbol } from '../../../../utils/currency'; +import { getSettingValues } from '../../../../utils/helpers'; +import { showToast } from '../../../../admin-x-ds/global/Toast'; import { toast } from 'react-hot-toast'; -import {useTiers} from '../../../providers/ServiceProvider'; +import { useAddTier, useEditTier } from '../../../../utils/api/tiers'; interface TierDetailModalProps { tier?: Tier @@ -36,7 +36,8 @@ const TierDetailModal: React.FC = ({tier}) => { const modal = useModal(); const {updateRoute} = useRouting(); - const {update: updateTier, create: createTier} = useTiers(); + const {mutateAsync: updateTier} = useEditTier(); + const {mutateAsync: createTier} = useAddTier(); const [hasFreeTrial, setHasFreeTrial] = React.useState(!!tier?.trial_days); const {localSettings} = useSettingGroup(); const siteTitle = getSettingValues(localSettings, ['title']) as string[]; diff --git a/apps/admin-x-settings/src/components/settings/membership/tiers/TiersList.tsx b/apps/admin-x-settings/src/components/settings/membership/tiers/TiersList.tsx index 07a32d8ad0..bdf559a6a7 100644 --- a/apps/admin-x-settings/src/components/settings/membership/tiers/TiersList.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/tiers/TiersList.tsx @@ -5,27 +5,24 @@ import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel'; import React from 'react'; import TierDetailModal from './TierDetailModal'; import useRouting from '../../../../hooks/useRouting'; -import {Tier} from '../../../../types/api'; -import {currencyToDecimal, getSymbol} from '../../../../utils/currency'; -import {numberWithCommas} from '../../../../utils/helpers'; +import { Tier } from '../../../../types/api'; +import { currencyToDecimal, getSymbol } from '../../../../utils/currency'; +import { numberWithCommas } from '../../../../utils/helpers'; +import { useEditTier } from '../../../../utils/api/tiers'; interface TiersListProps { tab?: string; tiers: Tier[]; - updateTier: (data: Tier) => Promise; } interface TierCardProps { tier: Tier; - updateTier: (data: Tier) => Promise; } const cardContainerClasses = 'group flex min-h-[200px] flex-col items-start justify-between gap-4 self-stretch rounded-sm border border-grey-300 p-4 transition-all hover:border-grey-400'; -const TierCard: React.FC = ({ - tier, - updateTier -}) => { +const TierCard: React.FC = ({tier}) => { + const {mutateAsync: updateTier} = useEditTier(); const currency = tier?.currency || 'USD'; const currencySymbol = currency ? getSymbol(currency) : '$'; @@ -60,8 +57,7 @@ const TierCard: React.FC = ({ const TiersList: React.FC = ({ tab, - tiers, - updateTier + tiers }) => { const {updateRoute} = useRouting(); const openTierModal = () => { @@ -79,7 +75,7 @@ const TiersList: React.FC = ({ return (
{tiers.map((tier) => { - return ; + return ; })} {tab === 'active-tiers' && (