Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 29 additions & 28 deletions packages/kg-unsplash-selector/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,54 @@
import {fixupPluginRules} from '@eslint/compat';
import {defineConfig} from 'eslint/config';
import eslint from '@eslint/js';
import ghostPlugin from 'eslint-plugin-ghost';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
import tailwindcssPlugin from 'eslint-plugin-tailwindcss';
import tseslint from 'typescript-eslint';

const ghost = fixupPluginRules(ghostPlugin);
import tailwindPlugin from 'eslint-plugin-tailwindcss';

export default tseslint.config(
{ignores: ['dist/**']},
export default defineConfig([
{ignores: ['dist/**', 'types/**']},
{
files: ['src/**/*.{ts,tsx}'],
files: ['**/*.{ts,tsx}'],
extends: [
eslint.configs.recommended,
tseslint.configs.recommended,
reactPlugin.configs.flat.recommended,
reactPlugin.configs.flat['jsx-runtime']
tseslint.configs.recommended
],
languageOptions: {
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {jsx: true}
}
},
plugins: {
ghost,
ghost: ghostPlugin,
react: reactPlugin,
'react-hooks': reactHooksPlugin,
'react-refresh': reactRefreshPlugin,
tailwindcss: tailwindcssPlugin
tailwindcss: tailwindPlugin
},
settings: {
react: {
version: 'detect'
}
react: {version: 'detect'}
},
rules: {
...ghostPlugin.configs.ts.rules,

// disable rules not in the original config
'@typescript-eslint/no-unused-expressions': 'off',

// react-hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',

// sort multiple import lines into alphabetical groups
'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', {
memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple']
}],

'prefer-const': 'off',
'react-refresh/only-export-components': 'off',

// suppress errors for missing 'import React' in JSX files, as we don't need it
'react/react-in-jsx-scope': 'off',
// ignore prop-types for now
'react/prop-types': 'off',

'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-empty-function': 'off',

// custom react rules
'react/jsx-sort-props': ['error', {
reservedFirst: true,
callbacksLast: true,
Expand All @@ -76,5 +67,15 @@ export default tseslint.config(
'tailwindcss/no-custom-classname': 'off',
'tailwindcss/no-contradicting-classname': ['error', {config: 'tailwind.config.cjs'}]
}
},
{
files: ['test/**/*.ts'],
rules: {
...ghostPlugin.configs['ts-test'].rules,
'ghost/mocha/no-global-tests': 'off',
'ghost/mocha/handle-done-callback': 'off',
'ghost/mocha/no-mocha-arrows': 'off',
'ghost/mocha/max-top-level-suites': 'off'
}
}
);
]);
5 changes: 4 additions & 1 deletion packages/kg-unsplash-selector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"build": "tsc && vite build",
"preview": "vite preview",
"prepare": "yarn build",
"lint": "eslint src --cache",
"pretest": "tsc -p tsconfig.test.json",
"lint": "eslint",
"test": "yarn test:unit && yarn test:acceptance",
"test:unit": "yarn nx build && vitest run",
"test:acceptance": "playwright test",
Expand All @@ -33,6 +34,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@eslint/js": "9.37.0",
"@playwright/test": "^1.58.0",
"@types/react": "18.3.28",
"@types/react-dom": "18.3.7",
Expand All @@ -51,6 +53,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.57.0",
"vite": "8.0.0",
"vite-plugin-css-injected-by-js": "3.5.2",
"vite-plugin-dts": "4.5.4",
"vite-plugin-svgr": "4.5.0",
"vitest": "4.1.0"
Expand Down
8 changes: 4 additions & 4 deletions packages/kg-unsplash-selector/src/UnsplashSearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import UnsplashGallery from './ui/UnsplashGallery';
import UnsplashSelector from './ui/UnsplashSelector';
import {DefaultHeaderTypes} from './UnsplashTypes';
import {InMemoryUnsplashProvider} from './api/InMemoryUnsplashProvider';
import {Photo} from './UnsplashTypes';
import {InsertImagePayload, Photo} from './UnsplashTypes';
import {PhotoUseCases} from './api/PhotoUseCase';
import {UnsplashProvider} from './api/UnsplashProvider';
import {UnsplashService} from './api/UnsplashService';

interface UnsplashModalProps {
onClose: () => void;
onImageInsert: (image: Photo) => void;
onImageInsert: (image: InsertImagePayload) => void;
unsplashProviderConfig: DefaultHeaderTypes | null;
}

Expand Down Expand Up @@ -154,7 +154,7 @@ export const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onI
}
}, [galleryRef, loadMorePhotos, zoomedImg]);

const selectImg = (payload:Photo) => {
const selectImg = (payload:Photo | null) => {
if (payload) {
setZoomedImg(payload);
setLastScrollPos(scrollPos);
Expand All @@ -168,7 +168,7 @@ export const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onI
}
};

async function insertImage(image:Photo) {
async function insertImage(image:InsertImagePayload) {
if (image.src) {
UnsplashLib.triggerDownload(image);
onImageInsert(image);
Expand Down
12 changes: 12 additions & 0 deletions packages/kg-unsplash-selector/src/UnsplashTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ export type Photo = {
src? : string;
};

export interface InsertImagePayload {
src: string;
caption: string;
height: number;
width: number;
alt: string;
links: Links;
}

export type InsertImageFn = (payload: InsertImagePayload) => void;
export type SelectImgFn = (payload: Photo | null) => void;

export type DefaultHeaderTypes = {
Authorization: string;
'Accept-Version': string;
Expand Down
2 changes: 1 addition & 1 deletion packages/kg-unsplash-selector/src/api/IUnsplashProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export interface IUnsplashProvider {
fetchPhotos(): Promise<Photo[]>;
fetchNextPage(): Promise<Photo[] | null>;
searchPhotos(term: string): Promise<Photo[]>;
triggerDownload(photo: Photo): Promise<void> | void;
triggerDownload(photo: Pick<Photo, 'links'>): Promise<void> | void;
searchIsRunning(): boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class InMemoryUnsplashProvider implements IUnsplashProvider {

public async searchPhotos(term: string): Promise<Photo[]> {
this.SEARCH_IS_RUNNING = true;
const filteredPhotos = this.photos.filter(photo => (photo.description && photo.description.toLowerCase().includes(term.toLowerCase())) ||
const filteredPhotos = this.photos.filter(photo => (photo.description && photo.description.toLowerCase().includes(term.toLowerCase())) ||
(photo.alt_description && photo.alt_description.toLowerCase().includes(term.toLowerCase()))
);
this.SEARCH_IS_RUNNING = false;
Expand All @@ -47,9 +47,8 @@ export class InMemoryUnsplashProvider implements IUnsplashProvider {
return this.SEARCH_IS_RUNNING;
}

triggerDownload(photo: Photo): void {
() => {
photo;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
triggerDownload(_photo: Pick<Photo, 'links'>): void {
// no-op for in-memory provider
}
}
2 changes: 1 addition & 1 deletion packages/kg-unsplash-selector/src/api/PhotoUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class PhotoUseCases {
return await this._provider.searchPhotos(term);
}

async triggerDownload(photo: Photo): Promise<void> {
async triggerDownload(photo: Pick<Photo, 'links'>): Promise<void> {
this._provider.triggerDownload(photo);
Comment on lines +19 to 20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify triggerDownload chaining/handling across layers
rg -nP --type=ts -C3 '\btriggerDownload\s*\(' \
  packages/kg-unsplash-selector/src/api/IUnsplashProvider.ts \
  packages/kg-unsplash-selector/src/api/PhotoUseCase.ts \
  packages/kg-unsplash-selector/src/api/UnsplashService.ts \
  packages/kg-unsplash-selector/src/UnsplashSearchModal.tsx

Repository: TryGhost/Koenig

Length of output: 3273


triggerDownload drops provider failures due to missing await.

On line 20, this._provider.triggerDownload(photo) is invoked inside an async method but not awaited or returned. If the provider returns a rejecting promise, it will escape unhandled. Since the provider interface declares the return type as Promise<void> | void, use await to handle both synchronous and asynchronous cases.

Additionally, packages/kg-unsplash-selector/src/api/UnsplashService.ts needs attention: its triggerDownload method (lines 64-66) should either propagate the promise by returning it, or change the interface signature from void to Promise<void> to reflect the actual behavior after fixing PhotoUseCase.

✅ Proposed fix (PhotoUseCase.ts)
-    async triggerDownload(photo: Pick<Photo, 'links'>): Promise<void> {
-        this._provider.triggerDownload(photo);
-    }
+    async triggerDownload(photo: Pick<Photo, 'links'>): Promise<void> {
+        await this._provider.triggerDownload(photo);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async triggerDownload(photo: Pick<Photo, 'links'>): Promise<void> {
this._provider.triggerDownload(photo);
async triggerDownload(photo: Pick<Photo, 'links'>): Promise<void> {
await this._provider.triggerDownload(photo);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-unsplash-selector/src/api/PhotoUseCase.ts` around lines 19 - 20,
The PhotoUseCase.triggerDownload currently calls
this._provider.triggerDownload(photo) without awaiting or returning it, which
lets provider rejections go unhandled; update PhotoUseCase.triggerDownload to
await the provider call (i.e., await this._provider.triggerDownload(photo)) so
both synchronous and async implementations are handled. Also update
UnsplashService.triggerDownload to either return the provider promise (propagate
it) or change its signature from void to Promise<void> so the upstream await
works correctly; ensure the provider interface/type reflects Promise<void> |
void if needed and adjust UnsplashService.triggerDownload to return the
Promise<void> path.

}

Expand Down
2 changes: 1 addition & 1 deletion packages/kg-unsplash-selector/src/api/UnsplashProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class UnsplashProvider implements IUnsplashProvider {
return [];
}

public async triggerDownload(photo: Photo): Promise<void> {
public async triggerDownload(photo: Pick<Photo, 'links'>): Promise<void> {
if (photo.links.download_location) {
await this.makeRequest(photo.links.download_location);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/kg-unsplash-selector/src/api/UnsplashService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface IUnsplashService {
updateSearch(term: string): Promise<void>;
loadNextPage(): Promise<void>;
clearPhotos(): void;
triggerDownload(photo: Photo): void;
triggerDownload(photo: Pick<Photo, 'links'>): void;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Interface declares void but delegates to async method without awaiting.

The triggerDownload method calls this.photoUseCases.triggerDownload(photo) which returns Promise<void>, but neither awaits nor returns it. If the underlying provider rejects, the error will be unhandled.

Consider making this method async and returning the promise:

🔧 Proposed fix
 export interface IUnsplashService {
     // ...
-    triggerDownload(photo: Pick<Photo, 'links'>): void;
+    triggerDownload(photo: Pick<Photo, 'links'>): Promise<void>;
     // ...
 }
-    triggerDownload(photo: Pick<Photo, 'links'>) {
-        this.photoUseCases.triggerDownload(photo);
+    async triggerDownload(photo: Pick<Photo, 'links'>): Promise<void> {
+        await this.photoUseCases.triggerDownload(photo);
     }

Also applies to: 64-66

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-unsplash-selector/src/api/UnsplashService.ts` at line 12, The
interface method triggerDownload currently declares a void return but calls
photoUseCases.triggerDownload(photo) which returns Promise<void>, causing
unhandled rejections; change the method signature from void to Promise<void> and
update its implementation (e.g., make triggerDownload async or return the
promise) so it either awaits or returns photoUseCases.triggerDownload(photo);
apply the same change for the other occurrences referenced (lines 64–66) so all
declarations/implementations consistently return Promise<void>.

photos: Photo[];
searchIsRunning(): boolean;
}
Expand Down Expand Up @@ -61,7 +61,7 @@ export class UnsplashService implements IUnsplashService {
this.photos = [];
}

triggerDownload(photo: Photo) {
triggerDownload(photo: Pick<Photo, 'links'>) {
this.photoUseCases.triggerDownload(photo);
}

Expand Down
10 changes: 5 additions & 5 deletions packages/kg-unsplash-selector/src/ui/UnsplashGallery.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React, {ReactNode, RefObject} from 'react';
import UnsplashImage from './UnsplashImage';
import UnsplashZoomed from './UnsplashZoomed';
import {Photo} from '../UnsplashTypes';
import {InsertImageFn, Photo, SelectImgFn} from '../UnsplashTypes';

interface MasonryColumnProps {
children: ReactNode;
}

interface UnsplashGalleryColumnsProps {
columns?: Photo[][] | [];
insertImage?: any;
selectImg?: any;
insertImage: InsertImageFn;
selectImg: SelectImgFn;
zoomed?: Photo | null;
}

Expand All @@ -24,8 +24,8 @@ interface GalleryLayoutProps {
interface UnsplashGalleryProps extends GalleryLayoutProps {
error?: string | null;
dataset?: Photo[][] | [];
selectImg?: any;
insertImage?: any;
selectImg: SelectImgFn;
insertImage: InsertImageFn;
}

const UnsplashGalleryLoading: React.FC = () => {
Expand Down
15 changes: 4 additions & 11 deletions packages/kg-unsplash-selector/src/ui/UnsplashImage.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
import UnsplashButton from './UnsplashButton';
import {FC, MouseEvent} from 'react';
import {Links, Photo, User} from '../UnsplashTypes';
import {InsertImageFn, Photo, SelectImgFn, User} from '../UnsplashTypes';

export interface UnsplashImageProps {
payload: Photo;
srcUrl: string;
links: Links;
links: Photo['links'];
likes: number;
user: User;
alt: string;
urls: { regular: string };
height: number;
width: number;
zoomed: Photo | null;
insertImage: (options: {
src: string,
caption: string,
height: number,
width: number,
alt: string,
links: Links
}) => void;
selectImg: (payload: Photo | null) => void;
insertImage: InsertImageFn;
selectImg: SelectImgFn;
}

const UnsplashImage: FC<UnsplashImageProps> = ({payload, srcUrl, links, likes, user, alt, urls, height, width, zoomed, insertImage, selectImg}) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/kg-unsplash-selector/src/ui/UnsplashZoomed.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import UnsplashImage, {UnsplashImageProps} from './UnsplashImage';
import {FC} from 'react';
import {Photo} from '../UnsplashTypes';
import {Photo, SelectImgFn} from '../UnsplashTypes';

interface UnsplashZoomedProps extends Omit<UnsplashImageProps, 'zoomed'> {
zoomed: Photo | null;
selectImg: (photo: Photo | null) => void;
selectImg: SelectImgFn;
}

const UnsplashZoomed: FC<UnsplashZoomedProps> = ({payload, insertImage, selectImg, zoomed}) => {
Expand Down
30 changes: 11 additions & 19 deletions packages/kg-unsplash-selector/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"allowSyntheticDefaultImports": true,
"jsx": "react-jsx",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"skipLibCheck": true,
"esModuleInterop": true,
"declarationMap": true,
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "./types",
"baseUrl": ".",
"paths": {
"kg-unsplash-selector": ["src/index.ts"],
},
"typeRoots": ["node_modules/@types", "types/index.d.ts"],
"noEmit": true
Comment on lines +9 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether this package actually needs emitted declaration files.
# Expected:
# - If package publishes types from build output, keep declaration flags in a build-specific tsconfig.
# - If package is type-check only, declaration flags can be removed from this file.

set -euo pipefail

echo "== tsconfig compilerOptions =="
jq '.compilerOptions | {noEmit, declaration, declarationMap, sourceMap}' packages/kg-unsplash-selector/tsconfig.json

echo
echo "== package metadata that may require emitted types =="
jq '{name, types, typings, files, exports}' packages/kg-unsplash-selector/package.json

Repository: TryGhost/Koenig

Length of output: 412


🏁 Script executed:

#!/bin/bash

# Search for all tsconfig files in the package
echo "=== tsconfig files in kg-unsplash-selector package ==="
find packages/kg-unsplash-selector -name "tsconfig*.json" -type f

echo
echo "=== package.json build scripts ==="
jq '.scripts' packages/kg-unsplash-selector/package.json

echo
echo "=== Check if dist directory exists ==="
ls -la packages/kg-unsplash-selector/ | grep -E "^d|^-"

Repository: TryGhost/Koenig

Length of output: 2035


🏁 Script executed:

#!/bin/bash

echo "=== tsconfig.declaration.json ==="
cat packages/kg-unsplash-selector/tsconfig.declaration.json

echo
echo "=== tsconfig.node.json ==="
cat packages/kg-unsplash-selector/tsconfig.node.json

echo
echo "=== Main tsconfig.json (full file) ==="
cat packages/kg-unsplash-selector/tsconfig.json

Repository: TryGhost/Koenig

Length of output: 1126


🏁 Script executed:

#!/bin/bash

echo "=== vite.config.ts (check output paths) ==="
cat packages/kg-unsplash-selector/vite.config.ts

echo
echo "=== Check if 'types' output handling in package.json ==="
head -20 packages/kg-unsplash-selector/package.json

Repository: TryGhost/Koenig

Length of output: 1716


Remove inert emit-related flags from tsconfig.json.

The flags "declaration", "declarationMap", and "sourceMap" on lines 9–11 have no effect because "noEmit": true (line 17) prevents TypeScript from emitting any files. Declaration generation is handled by vite-plugin-dts during the vite build step, so these flags should be removed from the main tsconfig (which is used for type-checking only).

You can safely delete lines 9–11 to reduce configuration noise and prevent future confusion.

Cleanup
   "strict": true,
-  "declaration": true,
-  "declarationMap": true,
-  "sourceMap": true,
   "esModuleInterop": true,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"skipLibCheck": true,
"esModuleInterop": true,
"declarationMap": true,
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "./types",
"baseUrl": ".",
"paths": {
"kg-unsplash-selector": ["src/index.ts"],
},
"typeRoots": ["node_modules/@types", "types/index.d.ts"],
"noEmit": true
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kg-unsplash-selector/tsconfig.json` around lines 9 - 17, Remove the
inert emit-related compiler options from tsconfig.json: delete the
"declaration", "declarationMap", and "sourceMap" properties since "noEmit" is
true and declaration generation is handled by vite-plugin-dts; keep "noEmit" and
the other type-checking flags intact to avoid confusion.

},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src/**/*"],
"references": [{ "path": "./tsconfig.node.json" }]
}
16 changes: 16 additions & 0 deletions packages/kg-unsplash-selector/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": null,
"noEmit": true,
"declaration": false,
"declarationMap": false,
"sourceMap": false,
"incremental": false
},
"include": [
"src/**/*",
"test/**/*"
]
}
Loading
Loading