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.
@ -4,7 +4,7 @@ import MainContent from './MainContent';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import RoutingProvider, {ExternalLink} from './components/providers/RoutingProvider';
|
||||
import clsx from 'clsx';
|
||||
import {DefaultHeaderTypes} from './utils/unsplash/UnsplashTypes';
|
||||
import {DefaultHeaderTypes} from './unsplash/UnsplashTypes';
|
||||
import {FetchKoenigLexical, OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider';
|
||||
import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState';
|
||||
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, {createContext, useContext} from 'react';
|
||||
import useSearchService, {SearchService} from '../../utils/search';
|
||||
import {DefaultHeaderTypes} from '../../utils/unsplash/UnsplashTypes';
|
||||
import {DefaultHeaderTypes} from '../../unsplash/UnsplashTypes';
|
||||
import {UpgradeStatusType} from '../../utils/globalTypes';
|
||||
import {ZapierTemplate} from '../settings/advanced/integrations/ZapierModal';
|
||||
|
||||
|
@ -5,7 +5,7 @@ import ImageUpload from '../../../../admin-x-ds/global/form/ImageUpload';
|
||||
import React, {useRef, useState} from 'react';
|
||||
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||
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 usePinturaEditor from '../../../../hooks/usePinturaEditor';
|
||||
import {SettingValue, getSettingValues} from '../../../../api/settings';
|
||||
@ -144,7 +144,7 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
||||
}
|
||||
}
|
||||
unsplashButtonClassName='!top-1 !right-1'
|
||||
unsplashEnabled={true}
|
||||
unsplashEnabled={unsplashEnabled}
|
||||
onDelete={() => updateSetting('cover_image', null)}
|
||||
onUpload={async (file) => {
|
||||
try {
|
||||
|
@ -2,7 +2,7 @@ import './styles/demo.css';
|
||||
import App from './App.tsx';
|
||||
import React from 'react';
|
||||
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(
|
||||
<React.StrictMode>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import MasonryService from './masonry/MasonryService';
|
||||
import Portal from '../portal';
|
||||
import Portal from './portal';
|
||||
import React, {useMemo, useRef, useState} from 'react';
|
||||
import UnsplashGallery from '../../admin-x-ds/unsplash/ui/UnsplashGallery';
|
||||
import UnsplashSelector from '../../admin-x-ds/unsplash/ui/UnsplashSelector';
|
||||
import UnsplashGallery from './ui/UnsplashGallery';
|
||||
import UnsplashSelector from './ui/UnsplashSelector';
|
||||
import {DefaultHeaderTypes, Photo} from './UnsplashTypes';
|
||||
import {PhotoUseCases} from './photo/PhotoUseCase';
|
||||
import {UnsplashRepository} from './api/UnsplashRepository';
|
||||
import {UnsplashProvider} from './api/UnsplashProvider';
|
||||
import {UnsplashService} from './UnsplashService';
|
||||
|
||||
interface UnsplashModalProps {
|
||||
@ -17,7 +17,7 @@ interface UnsplashModalProps {
|
||||
}
|
||||
|
||||
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 masonryService = useMemo(() => new MasonryService(3), []);
|
||||
const UnsplashLib = useMemo(() => new UnsplashService(photoUseCase, masonryService), [photoUseCase, masonryService]);
|
||||
@ -170,7 +170,7 @@ const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onImageIns
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Portal>
|
||||
<Portal classNames='admin-x-settings'>
|
||||
<UnsplashSelector
|
||||
closeModal={onClose}
|
||||
handleSearch={handleSearch}
|
@ -1,9 +1,8 @@
|
||||
// for testing purposes
|
||||
import {IUnsplashRepository} from './UnsplashRepository';
|
||||
import {Photo} from '../UnsplashTypes';
|
||||
import {fixturePhotos} from './unsplashFixtures';
|
||||
|
||||
export class InMemoryUnsplashRepository implements IUnsplashRepository {
|
||||
export class InMemoryUnsplashProvider {
|
||||
photos: Photo[] = fixturePhotos;
|
||||
PAGINATION: { [key: string]: string } = {};
|
||||
REQUEST_IS_RUNNING: boolean = false;
|
@ -1,14 +1,6 @@
|
||||
import {DefaultHeaderTypes, Photo} from '../UnsplashTypes';
|
||||
|
||||
export interface IUnsplashRepository {
|
||||
fetchPhotos(): Promise<Photo[]>;
|
||||
fetchNextPage(): Promise<Photo[] | null>;
|
||||
searchPhotos(term: string): Promise<Photo[]>;
|
||||
triggerDownload(photo: Photo): void;
|
||||
searchIsRunning(): boolean;
|
||||
}
|
||||
|
||||
export class UnsplashRepository implements IUnsplashRepository {
|
||||
export class UnsplashProvider {
|
||||
API_URL: string = 'https://api.unsplash.com';
|
||||
HEADERS: DefaultHeaderTypes;
|
||||
ERROR: string | null = null;
|
Before Width: | Height: | Size: 176 B After Width: | Height: | Size: 176 B |
Before Width: | Height: | Size: 226 B After Width: | Height: | Size: 226 B |
Before Width: | Height: | Size: 216 B After Width: | Height: | Size: 216 B |
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 197 B |
37
apps/admin-x-settings/src/unsplash/photo/PhotoUseCase.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -4,9 +4,10 @@ import {createPortal} from 'react-dom';
|
||||
interface PortalProps {
|
||||
children: ReactNode;
|
||||
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;
|
||||
|
||||
if (!container) {
|
||||
@ -18,7 +19,7 @@ const Portal: React.FC<PortalProps> = ({children, to}) => {
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className='admin-x-settings' onMouseDown={cancelEvents}>
|
||||
<div className={classNames} onMouseDown={cancelEvents}>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
@ -1,7 +1,7 @@
|
||||
import React, {ReactNode, RefObject} from 'react';
|
||||
import UnsplashImage from './UnsplashImage';
|
||||
import UnsplashZoomed from './UnsplashZoomed';
|
||||
import {Photo} from '../../../utils/unsplash/UnsplashTypes';
|
||||
import {Photo} from '../UnsplashTypes';
|
||||
|
||||
interface MasonryColumnProps {
|
||||
children: ReactNode;
|
@ -1,6 +1,6 @@
|
||||
import UnsplashButton from './UnsplashButton';
|
||||
import {FC, MouseEvent} from 'react';
|
||||
import {Links, Photo, User} from '../../../utils/unsplash/UnsplashTypes';
|
||||
import {Links, Photo, User} from '../UnsplashTypes';
|
||||
|
||||
export interface UnsplashImageProps {
|
||||
payload: Photo;
|
@ -1,7 +1,6 @@
|
||||
import UnsplashImage, {UnsplashImageProps} from './UnsplashImage';
|
||||
import {FC} from 'react';
|
||||
|
||||
import {Photo} from '../../../utils/unsplash/UnsplashTypes';
|
||||
import {Photo} from '../UnsplashTypes';
|
||||
|
||||
interface UnsplashZoomedProps extends Omit<UnsplashImageProps, 'zoomed'> {
|
||||
zoomed: Photo | null;
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import MasonryService from '../../../src/utils/unsplash/masonry/MasonryService';
|
||||
import {Photo} from '../../../src/utils/unsplash/UnsplashTypes';
|
||||
import {fixturePhotos} from '../../../src/utils/unsplash/api/unsplashFixtures';
|
||||
import MasonryService from '../../../src/unsplash/masonry/MasonryService';
|
||||
import {Photo} from '../../../src/unsplash/UnsplashTypes';
|
||||
import {fixturePhotos} from '../../../src/unsplash/api/unsplashFixtures';
|
||||
|
||||
describe('MasonryService', () => {
|
||||
let service: MasonryService;
|
||||
|
@ -1,20 +1,19 @@
|
||||
import MasonryService from '../../../src/utils/unsplash/masonry/MasonryService';
|
||||
import {IUnsplashRepository} from '../../../src/utils/unsplash/api/UnsplashRepository';
|
||||
import {IUnsplashService, UnsplashService} from '../../../src/utils/unsplash/UnsplashService';
|
||||
import {InMemoryUnsplashRepository} from '../../../src/utils/unsplash/api/InMemoryUnsplashRepository';
|
||||
import {PhotoUseCases} from '../../../src/utils/unsplash/photo/PhotoUseCase';
|
||||
import {fixturePhotos} from '../../../src/utils/unsplash/api/unsplashFixtures';
|
||||
import MasonryService from '../../../src/unsplash/masonry/MasonryService';
|
||||
import {IUnsplashService, UnsplashService} from '../../../src/unsplash/UnsplashService';
|
||||
import {InMemoryUnsplashProvider} from '../../../src/unsplash/api/InMemoryUnsplashProvider';
|
||||
import {PhotoUseCases} from '../../../src/unsplash/photo/PhotoUseCase';
|
||||
import {fixturePhotos} from '../../../src/unsplash/api/unsplashFixtures';
|
||||
|
||||
describe('UnsplashService', () => {
|
||||
let unsplashService: IUnsplashService;
|
||||
let unsplashRepository: IUnsplashRepository;
|
||||
let UnsplashProvider: InMemoryUnsplashProvider;
|
||||
let masonryService: MasonryService;
|
||||
let photoUseCases: PhotoUseCases;
|
||||
|
||||
beforeEach(() => {
|
||||
unsplashRepository = new InMemoryUnsplashRepository();
|
||||
UnsplashProvider = new InMemoryUnsplashProvider();
|
||||
masonryService = new MasonryService(3);
|
||||
photoUseCases = new PhotoUseCases(unsplashRepository);
|
||||
photoUseCases = new PhotoUseCases(UnsplashProvider);
|
||||
unsplashService = new UnsplashService(photoUseCases, masonryService);
|
||||
});
|
||||
|
||||
|