Cleaned up Unsplash component in Admin (#18603)

no issue

- Fixed a bug where the Unsplash button in site design didn't hide if
the integration was disabled.
- Cleaned up Unsplash in preparation for moving it to it's own package
in future.

---

<!-- Leave the line below if you'd like GitHub Copilot to generate a
summary from your commit -->
<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at 4da5d12</samp>

Refactored and improved the Unsplash integration feature in the admin
settings app. Moved all files related to Unsplash to a separate
`unsplash` folder and renamed some classes and interfaces to avoid
confusion. Added a feature flag for Unsplash in the brand settings
component and a prop to customize the portal container for the Unsplash
search modal. Created a new class `PhotoUseCases` to handle the logic
for fetching, searching, and downloading photos from Unsplash.
This commit is contained in:
Ronald Langeveld 2023-10-13 14:15:14 +07:00 committed by GitHub
parent b3116aaa4f
commit 7d493d61db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 67 additions and 76 deletions

View File

@ -4,7 +4,7 @@ import MainContent from './MainContent';
import NiceModal from '@ebay/nice-modal-react'; import NiceModal from '@ebay/nice-modal-react';
import RoutingProvider, {ExternalLink} from './components/providers/RoutingProvider'; import RoutingProvider, {ExternalLink} from './components/providers/RoutingProvider';
import clsx from 'clsx'; import clsx from 'clsx';
import {DefaultHeaderTypes} from './utils/unsplash/UnsplashTypes'; import {DefaultHeaderTypes} from './unsplash/UnsplashTypes';
import {FetchKoenigLexical, OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider'; import {FetchKoenigLexical, OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider';
import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState'; import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query';

View File

@ -1,6 +1,6 @@
import React, {createContext, useContext} from 'react'; import React, {createContext, useContext} from 'react';
import useSearchService, {SearchService} from '../../utils/search'; import useSearchService, {SearchService} from '../../utils/search';
import {DefaultHeaderTypes} from '../../utils/unsplash/UnsplashTypes'; import {DefaultHeaderTypes} from '../../unsplash/UnsplashTypes';
import {UpgradeStatusType} from '../../utils/globalTypes'; import {UpgradeStatusType} from '../../utils/globalTypes';
import {ZapierTemplate} from '../settings/advanced/integrations/ZapierModal'; import {ZapierTemplate} from '../settings/advanced/integrations/ZapierModal';

View File

@ -5,7 +5,7 @@ import ImageUpload from '../../../../admin-x-ds/global/form/ImageUpload';
import React, {useRef, useState} from 'react'; import React, {useRef, useState} from 'react';
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent'; import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../../admin-x-ds/global/form/TextField'; import TextField from '../../../../admin-x-ds/global/form/TextField';
import UnsplashSearchModal from '../../../../utils/unsplash/UnsplashSearchModal'; import UnsplashSearchModal from '../../../../unsplash/UnsplashSearchModal';
import useHandleError from '../../../../utils/api/handleError'; import useHandleError from '../../../../utils/api/handleError';
import usePinturaEditor from '../../../../hooks/usePinturaEditor'; import usePinturaEditor from '../../../../hooks/usePinturaEditor';
import {SettingValue, getSettingValues} from '../../../../api/settings'; import {SettingValue, getSettingValues} from '../../../../api/settings';
@ -144,7 +144,7 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
} }
} }
unsplashButtonClassName='!top-1 !right-1' unsplashButtonClassName='!top-1 !right-1'
unsplashEnabled={true} unsplashEnabled={unsplashEnabled}
onDelete={() => updateSetting('cover_image', null)} onDelete={() => updateSetting('cover_image', null)}
onUpload={async (file) => { onUpload={async (file) => {
try { try {

View File

@ -2,7 +2,7 @@ import './styles/demo.css';
import App from './App.tsx'; import App from './App.tsx';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import {DefaultHeaderTypes} from './utils/unsplash/UnsplashTypes.ts'; import {DefaultHeaderTypes} from './unsplash/UnsplashTypes.ts';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode> <React.StrictMode>

View File

@ -1,11 +1,11 @@
import MasonryService from './masonry/MasonryService'; import MasonryService from './masonry/MasonryService';
import Portal from '../portal'; import Portal from './portal';
import React, {useMemo, useRef, useState} from 'react'; import React, {useMemo, useRef, useState} from 'react';
import UnsplashGallery from '../../admin-x-ds/unsplash/ui/UnsplashGallery'; import UnsplashGallery from './ui/UnsplashGallery';
import UnsplashSelector from '../../admin-x-ds/unsplash/ui/UnsplashSelector'; import UnsplashSelector from './ui/UnsplashSelector';
import {DefaultHeaderTypes, Photo} from './UnsplashTypes'; import {DefaultHeaderTypes, Photo} from './UnsplashTypes';
import {PhotoUseCases} from './photo/PhotoUseCase'; import {PhotoUseCases} from './photo/PhotoUseCase';
import {UnsplashRepository} from './api/UnsplashRepository'; import {UnsplashProvider} from './api/UnsplashProvider';
import {UnsplashService} from './UnsplashService'; import {UnsplashService} from './UnsplashService';
interface UnsplashModalProps { interface UnsplashModalProps {
@ -17,7 +17,7 @@ interface UnsplashModalProps {
} }
const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onImageInsert, unsplashConf}) => { const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onImageInsert, unsplashConf}) => {
const unsplashRepo = useMemo(() => new UnsplashRepository(unsplashConf.defaultHeaders), [unsplashConf.defaultHeaders]); const unsplashRepo = useMemo(() => new UnsplashProvider(unsplashConf.defaultHeaders), [unsplashConf.defaultHeaders]);
const photoUseCase = useMemo(() => new PhotoUseCases(unsplashRepo), [unsplashRepo]); const photoUseCase = useMemo(() => new PhotoUseCases(unsplashRepo), [unsplashRepo]);
const masonryService = useMemo(() => new MasonryService(3), []); const masonryService = useMemo(() => new MasonryService(3), []);
const UnsplashLib = useMemo(() => new UnsplashService(photoUseCase, masonryService), [photoUseCase, masonryService]); const UnsplashLib = useMemo(() => new UnsplashService(photoUseCase, masonryService), [photoUseCase, masonryService]);
@ -170,7 +170,7 @@ const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onImageIns
} }
} }
return ( return (
<Portal> <Portal classNames='admin-x-settings'>
<UnsplashSelector <UnsplashSelector
closeModal={onClose} closeModal={onClose}
handleSearch={handleSearch} handleSearch={handleSearch}

View File

@ -1,9 +1,8 @@
// for testing purposes // for testing purposes
import {IUnsplashRepository} from './UnsplashRepository';
import {Photo} from '../UnsplashTypes'; import {Photo} from '../UnsplashTypes';
import {fixturePhotos} from './unsplashFixtures'; import {fixturePhotos} from './unsplashFixtures';
export class InMemoryUnsplashRepository implements IUnsplashRepository { export class InMemoryUnsplashProvider {
photos: Photo[] = fixturePhotos; photos: Photo[] = fixturePhotos;
PAGINATION: { [key: string]: string } = {}; PAGINATION: { [key: string]: string } = {};
REQUEST_IS_RUNNING: boolean = false; REQUEST_IS_RUNNING: boolean = false;

View File

@ -1,14 +1,6 @@
import {DefaultHeaderTypes, Photo} from '../UnsplashTypes'; import {DefaultHeaderTypes, Photo} from '../UnsplashTypes';
export interface IUnsplashRepository { export class UnsplashProvider {
fetchPhotos(): Promise<Photo[]>;
fetchNextPage(): Promise<Photo[] | null>;
searchPhotos(term: string): Promise<Photo[]>;
triggerDownload(photo: Photo): void;
searchIsRunning(): boolean;
}
export class UnsplashRepository implements IUnsplashRepository {
API_URL: string = 'https://api.unsplash.com'; API_URL: string = 'https://api.unsplash.com';
HEADERS: DefaultHeaderTypes; HEADERS: DefaultHeaderTypes;
ERROR: string | null = null; ERROR: string | null = null;

View File

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 226 B

View File

Before

Width:  |  Height:  |  Size: 216 B

After

Width:  |  Height:  |  Size: 216 B

View File

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1,37 @@
import {InMemoryUnsplashProvider} from '../api/InMemoryUnsplashProvider';
import {Photo} from '../UnsplashTypes';
import {UnsplashProvider} from '../api/UnsplashProvider';
export class PhotoUseCases {
private _provider: UnsplashProvider | InMemoryUnsplashProvider; // InMemoryUnsplashProvider is for testing purposes
constructor(provider: UnsplashProvider | InMemoryUnsplashProvider) {
this._provider = provider;
}
async fetchPhotos(): Promise<Photo[]> {
return await this._provider.fetchPhotos();
}
async searchPhotos(term: string): Promise<Photo[]> {
return await this._provider.searchPhotos(term);
}
async triggerDownload(photo: Photo): Promise<void> {
this._provider.triggerDownload(photo);
}
async fetchNextPage(): Promise<Photo[] | null> {
let request = await this._provider.fetchNextPage();
if (request) {
return request;
}
return null;
}
searchIsRunning(): boolean {
return this._provider.searchIsRunning();
}
}

View File

@ -4,9 +4,10 @@ import {createPortal} from 'react-dom';
interface PortalProps { interface PortalProps {
children: ReactNode; children: ReactNode;
to?: Element; to?: Element;
classNames?: string;
} }
const Portal: React.FC<PortalProps> = ({children, to}) => { const Portal: React.FC<PortalProps> = ({children, to, classNames}) => {
const container: Element = to || document.body; const container: Element = to || document.body;
if (!container) { if (!container) {
@ -18,7 +19,7 @@ const Portal: React.FC<PortalProps> = ({children, to}) => {
}; };
return createPortal( return createPortal(
<div className='admin-x-settings' onMouseDown={cancelEvents}> <div className={classNames} onMouseDown={cancelEvents}>
<div> <div>
{children} {children}
</div> </div>

View File

@ -1,7 +1,7 @@
import React, {ReactNode, RefObject} from 'react'; import React, {ReactNode, RefObject} from 'react';
import UnsplashImage from './UnsplashImage'; import UnsplashImage from './UnsplashImage';
import UnsplashZoomed from './UnsplashZoomed'; import UnsplashZoomed from './UnsplashZoomed';
import {Photo} from '../../../utils/unsplash/UnsplashTypes'; import {Photo} from '../UnsplashTypes';
interface MasonryColumnProps { interface MasonryColumnProps {
children: ReactNode; children: ReactNode;

View File

@ -1,6 +1,6 @@
import UnsplashButton from './UnsplashButton'; import UnsplashButton from './UnsplashButton';
import {FC, MouseEvent} from 'react'; import {FC, MouseEvent} from 'react';
import {Links, Photo, User} from '../../../utils/unsplash/UnsplashTypes'; import {Links, Photo, User} from '../UnsplashTypes';
export interface UnsplashImageProps { export interface UnsplashImageProps {
payload: Photo; payload: Photo;

View File

@ -1,7 +1,6 @@
import UnsplashImage, {UnsplashImageProps} from './UnsplashImage'; import UnsplashImage, {UnsplashImageProps} from './UnsplashImage';
import {FC} from 'react'; import {FC} from 'react';
import {Photo} from '../UnsplashTypes';
import {Photo} from '../../../utils/unsplash/UnsplashTypes';
interface UnsplashZoomedProps extends Omit<UnsplashImageProps, 'zoomed'> { interface UnsplashZoomedProps extends Omit<UnsplashImageProps, 'zoomed'> {
zoomed: Photo | null; zoomed: Photo | null;

View File

@ -1,36 +0,0 @@
import {IUnsplashRepository} from '../api/UnsplashRepository';
import {Photo} from '../UnsplashTypes';
export class PhotoUseCases {
private repository: IUnsplashRepository;
constructor(repository: IUnsplashRepository) {
this.repository = repository;
}
async fetchPhotos(): Promise<Photo[]> {
return await this.repository.fetchPhotos();
}
async searchPhotos(term: string): Promise<Photo[]> {
return await this.repository.searchPhotos(term);
}
async triggerDownload(photo: Photo): Promise<void> {
this.repository.triggerDownload(photo);
}
async fetchNextPage(): Promise<Photo[] | null> {
let request = await this.repository.fetchNextPage();
if (request) {
return request;
}
return null;
}
searchIsRunning(): boolean {
return this.repository.searchIsRunning();
}
}

View File

@ -1,6 +1,6 @@
import MasonryService from '../../../src/utils/unsplash/masonry/MasonryService'; import MasonryService from '../../../src/unsplash/masonry/MasonryService';
import {Photo} from '../../../src/utils/unsplash/UnsplashTypes'; import {Photo} from '../../../src/unsplash/UnsplashTypes';
import {fixturePhotos} from '../../../src/utils/unsplash/api/unsplashFixtures'; import {fixturePhotos} from '../../../src/unsplash/api/unsplashFixtures';
describe('MasonryService', () => { describe('MasonryService', () => {
let service: MasonryService; let service: MasonryService;

View File

@ -1,20 +1,19 @@
import MasonryService from '../../../src/utils/unsplash/masonry/MasonryService'; import MasonryService from '../../../src/unsplash/masonry/MasonryService';
import {IUnsplashRepository} from '../../../src/utils/unsplash/api/UnsplashRepository'; import {IUnsplashService, UnsplashService} from '../../../src/unsplash/UnsplashService';
import {IUnsplashService, UnsplashService} from '../../../src/utils/unsplash/UnsplashService'; import {InMemoryUnsplashProvider} from '../../../src/unsplash/api/InMemoryUnsplashProvider';
import {InMemoryUnsplashRepository} from '../../../src/utils/unsplash/api/InMemoryUnsplashRepository'; import {PhotoUseCases} from '../../../src/unsplash/photo/PhotoUseCase';
import {PhotoUseCases} from '../../../src/utils/unsplash/photo/PhotoUseCase'; import {fixturePhotos} from '../../../src/unsplash/api/unsplashFixtures';
import {fixturePhotos} from '../../../src/utils/unsplash/api/unsplashFixtures';
describe('UnsplashService', () => { describe('UnsplashService', () => {
let unsplashService: IUnsplashService; let unsplashService: IUnsplashService;
let unsplashRepository: IUnsplashRepository; let UnsplashProvider: InMemoryUnsplashProvider;
let masonryService: MasonryService; let masonryService: MasonryService;
let photoUseCases: PhotoUseCases; let photoUseCases: PhotoUseCases;
beforeEach(() => { beforeEach(() => {
unsplashRepository = new InMemoryUnsplashRepository(); UnsplashProvider = new InMemoryUnsplashProvider();
masonryService = new MasonryService(3); masonryService = new MasonryService(3);
photoUseCases = new PhotoUseCases(unsplashRepository); photoUseCases = new PhotoUseCases(UnsplashProvider);
unsplashService = new UnsplashService(photoUseCases, masonryService); unsplashService = new UnsplashService(photoUseCases, masonryService);
}); });