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 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';
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
@ -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;
|
@ -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;
|
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 {
|
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>
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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 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;
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|