Skip to content
Closed
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
24 changes: 17 additions & 7 deletions apps/ui/src/components/dialogs/board-background-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import { useAppStore, defaultBackgroundSettings } from '@/store/app-store';
import { getHttpApiClient, getServerUrlSync } from '@/lib/http-api-client';
import { getHttpApiClient } from '@/lib/http-api-client';
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
import { useBoardBackgroundSettings } from '@/hooks/use-board-background-settings';
import { toast } from 'sonner';
import {
Expand All @@ -28,6 +29,14 @@ interface BoardBackgroundModalProps {
onOpenChange: (open: boolean) => void;
}

/**
* Render a right-side modal for configuring a board's background image and appearance settings.
*
* Provides image upload (drag-and-drop or file picker), preview with cache-busting, clear/delete,
* and live controls for card/column opacity, column/card borders, card glassmorphism, and hiding the board scrollbar.
*
* @returns The sheet modal React element for board background settings, or `null` when no project is selected.
*/
export function BoardBackgroundModal({ open, onOpenChange }: BoardBackgroundModalProps) {
const { currentProject, boardBackgroundByProject } = useAppStore();
const {
Expand Down Expand Up @@ -62,12 +71,13 @@ export function BoardBackgroundModal({ open, onOpenChange }: BoardBackgroundModa
// Update preview image when background settings change
useEffect(() => {
if (currentProject && backgroundSettings.imagePath) {
const serverUrl = import.meta.env.VITE_SERVER_URL || getServerUrlSync();
// Add cache-busting query parameter to force browser to reload image
const cacheBuster = imageVersion ? `&v=${imageVersion}` : `&v=${Date.now()}`;
const imagePath = `${serverUrl}/api/fs/image?path=${encodeURIComponent(
backgroundSettings.imagePath
)}&projectPath=${encodeURIComponent(currentProject.path)}${cacheBuster}`;
const cacheBuster = imageVersion ?? Date.now().toString();
const imagePath = getAuthenticatedImageUrl(
backgroundSettings.imagePath,
currentProject.path,
cacheBuster
);
setPreviewImage(imagePath);
} else {
setPreviewImage(null);
Expand Down Expand Up @@ -469,4 +479,4 @@ export function BoardBackgroundModal({ open, onOpenChange }: BoardBackgroundModa
</SheetContent>
</Sheet>
);
}
}
14 changes: 10 additions & 4 deletions apps/ui/src/components/ui/description-image-dropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { cn } from '@/lib/utils';
import { ImageIcon, X, Loader2, FileText } from 'lucide-react';
import { Textarea } from '@/components/ui/textarea';
import { getElectronAPI } from '@/lib/electron';
import { getServerUrlSync } from '@/lib/http-api-client';
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
import { useAppStore, type FeatureImagePath, type FeatureTextFilePath } from '@/store/app-store';
import {
sanitizeFilename,
Expand Down Expand Up @@ -46,6 +46,13 @@ interface DescriptionImageDropZoneProps {
error?: boolean; // Show error state with red border
}

/**
* Textarea input that accepts image and text attachments via paste, drag-and-drop, or file browse and displays previews and removal controls.
*
* The component accepts images and text files (.txt, .md), enforces `maxFiles` and `maxFileSize`, and calls `onImagesChange` / `onTextFilesChange` with newly added items. Image previews are kept in an internal map unless an external `previewMap` and `onPreviewMapChange` are provided to control previews. Pasted clipboard images are detected and handled; text pasted without images falls back to the default paste behavior. When an image is added it is saved to a temporary path (or a fallback path) and its preview stored as base64 until a server URL is used to load it later.
*
* @returns A React element rendering the description textarea, file input/drop zone, processing state, and previews for attached images and text files.
*/
export function DescriptionImageDropZone({
value,
onChange,
Expand Down Expand Up @@ -94,9 +101,8 @@ export function DescriptionImageDropZone({
// Construct server URL for loading saved images
const getImageServerUrl = useCallback(
(imagePath: string): string => {
const serverUrl = import.meta.env.VITE_SERVER_URL || getServerUrlSync();
const projectPath = currentProject?.path || '';
return `${serverUrl}/api/fs/image?path=${encodeURIComponent(imagePath)}&projectPath=${encodeURIComponent(projectPath)}`;
return getAuthenticatedImageUrl(imagePath, projectPath);
},
[currentProject?.path]
);
Expand Down Expand Up @@ -542,4 +548,4 @@ export function DescriptionImageDropZone({
)}
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { useMemo } from 'react';
import { useAppStore, defaultBackgroundSettings } from '@/store/app-store';
import { getServerUrlSync } from '@/lib/http-api-client';
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';

interface UseBoardBackgroundProps {
currentProject: { path: string; id: string } | null;
}

/**
* Selects the board background settings for the given project and produces a CSS style object for the project's background image when present.
*
* @param currentProject - The active project (contains `path` and `id`), or `null` if no project is selected
* @returns An object with:
* - `backgroundSettings`: the resolved background settings for the current project or the default settings
* - `backgroundImageStyle`: a React CSS properties object with `backgroundImage`, `backgroundSize`, `backgroundPosition`, and `backgroundRepeat` when an image is available, or an empty object otherwise
*/
export function useBoardBackground({ currentProject }: UseBoardBackgroundProps) {
const boardBackgroundByProject = useAppStore((state) => state.boardBackgroundByProject);

Expand All @@ -22,14 +30,14 @@ export function useBoardBackground({ currentProject }: UseBoardBackgroundProps)
return {};
}

const imageUrl = getAuthenticatedImageUrl(
backgroundSettings.imagePath,
currentProject.path,
backgroundSettings.imageVersion
);

return {
backgroundImage: `url(${
import.meta.env.VITE_SERVER_URL || getServerUrlSync()
}/api/fs/image?path=${encodeURIComponent(
backgroundSettings.imagePath
)}&projectPath=${encodeURIComponent(currentProject.path)}${
backgroundSettings.imageVersion ? `&v=${backgroundSettings.imageVersion}` : ''
})`,
backgroundImage: `url(${imageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
Expand All @@ -40,4 +48,4 @@ export function useBoardBackground({ currentProject }: UseBoardBackgroundProps)
backgroundSettings,
backgroundImageStyle,
};
}
}
39 changes: 38 additions & 1 deletion apps/ui/src/lib/api-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,48 @@ export async function apiDelete<T>(endpoint: string, options: ApiFetchOptions =
}

/**
* Make an authenticated DELETE request (returns raw response for status checking)
* Perform a DELETE request to the given endpoint and return the raw fetch response.
*
* @returns The raw `Response` from the fetch call for status and header inspection.
*/
export async function apiDeleteRaw(
endpoint: string,
options: ApiFetchOptions = {}
): Promise<Response> {
return apiFetch(endpoint, 'DELETE', options);
}

/**
* Construct a URL for loading an image that includes any necessary authentication.
*
* If an API key is available it is appended as an `apiKey` query parameter; otherwise browser cookies (session token) are relied on for authentication. Optionally includes a `v` query parameter for cache busting.
*
* @param path - The image file path on the server
* @param projectPath - The project namespace or folder containing the image
* @param version - Optional value added as the `v` query parameter to bust caches
* @returns The full image URL including `path`, `projectPath`, optional `v`, and `apiKey` when present
*/
export function getAuthenticatedImageUrl(
path: string,
projectPath: string,
version?: string | number
): string {
const serverUrl = getServerUrl();
const params = new URLSearchParams({
path,
projectPath,
});

if (version !== undefined) {
params.set('v', String(version));
}

// Add auth credential as query param (needed for image loads that can't set headers)
const apiKey = getApiKey();
if (apiKey) {
params.set('apiKey', apiKey);
}
// Note: Session token auth relies on cookies which are sent automatically by the browser

return `${serverUrl}/api/fs/image?${params.toString()}`;
}
Loading