From a85556ab5b898c98cfef02810dd4da6c48648d51 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Fri, 30 Jan 2026 18:53:56 -0800 Subject: [PATCH] Attach content (docs, images, pdf, etc.) from Google Drive. Fixes #943 --- Dockerfile | 4 + docs/config-feature-google-drive.md | 56 ++++++++ docs/environment-variables.md | 6 +- package-lock.json | 20 +++ package.json | 1 + .../chat/components/composer/Composer.tsx | 17 ++- .../buttons/ButtonAttachGoogleDrive.tsx | 50 +++++++ .../attachment-drafts/attachment.cloud.ts | 11 +- .../attachment-drafts/attachment.pipeline.ts | 9 +- .../attachment-drafts/useAttachmentDrafts.tsx | 48 +++---- .../useGoogleDrivePicker.tsx | 135 ++++++++++++++++++ src/common/styles/app.styles.css | 30 +++- src/server/env.server.ts | 6 + 13 files changed, 358 insertions(+), 35 deletions(-) create mode 100644 docs/config-feature-google-drive.md create mode 100644 src/apps/chat/components/composer/buttons/ButtonAttachGoogleDrive.tsx create mode 100644 src/common/attachment-drafts/useGoogleDrivePicker.tsx diff --git a/Dockerfile b/Dockerfile index f82bdb379..23991c497 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,10 @@ ENV NEXT_PUBLIC_GA4_MEASUREMENT_ID=${NEXT_PUBLIC_GA4_MEASUREMENT_ID} ARG NEXT_PUBLIC_POSTHOG_KEY ENV NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY} +# Optional argument to configure Google Drive Picker at build time (can reuse AUTH_GOOGLE_ID value) +ARG NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID +ENV NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID=${NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID} + # Copy development deps and source COPY --from=deps /app/node_modules ./node_modules COPY . . diff --git a/docs/config-feature-google-drive.md b/docs/config-feature-google-drive.md new file mode 100644 index 000000000..29f295ffa --- /dev/null +++ b/docs/config-feature-google-drive.md @@ -0,0 +1,56 @@ +# Google Drive Integration + +Attach files from Google Drive directly in the chat composer. + +## Setup + +### 1. Enable APIs + +In [Google Cloud Console](https://console.cloud.google.com/): + +1. Go to **APIs & Services > Library** +2. Enable **Google Drive API** and **Google Picker API** + +### 2. Configure OAuth + +1. Go to **APIs & Services > OAuth consent screen** +2. Create consent screen (External or Internal) +3. Add scope: `https://www.googleapis.com/auth/drive.file` +4. Add test users if in testing mode + +### 3. Create Credentials + +1. Go to **APIs & Services > Credentials** +2. Create **OAuth client ID** (Web application) +3. Add JavaScript origins: + - `http://localhost:3000` (dev) + - `https://your-domain.com` (prod) + +### 4. Set Environment Variable + +```bash +NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID=your-client-id.apps.googleusercontent.com +``` + +## Usage + +- Click **Drive** button in attachment menu +- Or press **Ctrl + Shift + G** + +## Supported Files + +| Type | Export Format | +|-----------------|---------------------| +| Regular files | Downloaded directly | +| Google Docs | Markdown (.md) | +| Google Sheets | CSV (.csv) | +| Google Slides | PDF (.pdf) | +| Google Drawings | SVG (.svg) | + +## Troubleshooting + +**Picker won't open**: Check `NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID` is set and APIs are enabled. + +**OAuth errors**: Verify your domain is in authorized JavaScript origins. Add yourself as test user if app is in testing mode. + +**Download fails**: Check file permissions and that Drive API is enabled. diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 8151ded40..88ffc256e 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -66,8 +66,9 @@ HTTP_BASIC_AUTH_PASSWORD= # Frontend variables NEXT_PUBLIC_MOTD= NEXT_PUBLIC_GA4_MEASUREMENT_ID= -NEXT_PUBLIC_POSTHOG_KEY= +NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID= NEXT_PUBLIC_PLANTUML_SERVER_URL= +NEXT_PUBLIC_POSTHOG_KEY= ``` ## Backend Variables @@ -155,8 +156,9 @@ The value of these variables are passed to the frontend (Web UI) - make sure the | `NEXT_PUBLIC_DEBUG_BREAKS` | (optional, development) When set to 'true', enables automatic debugger breaks on DEV/error/critical logs in development builds | | `NEXT_PUBLIC_MOTD` | Message of the Day - displays a dismissible banner at the top of the app (see [customizations](customizations.md) for the template variables). Example: 🔔 Welcome to our deployment! Version {{app_build_pkgver}} built on {{app_build_time}}. | | `NEXT_PUBLIC_GA4_MEASUREMENT_ID` | (optional) The measurement ID for Google Analytics 4. (see [deploy-analytics](deploy-analytics.md)) | -| `NEXT_PUBLIC_POSTHOG_KEY` | (optional) Key for PostHog analytics. (see [deploy-analytics](deploy-analytics.md)) | +| `NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID` | (optional) Google OAuth Client ID for Drive Picker. Can reuse `AUTH_GOOGLE_ID`. See [Google Drive](config-feature-google-drive.md) | | `NEXT_PUBLIC_PLANTUML_SERVER_URL` | The URL of the PlantUML server, used for rendering UML diagrams. Allows using custom local servers. | +| `NEXT_PUBLIC_POSTHOG_KEY` | (optional) Key for PostHog analytics. (see [deploy-analytics](deploy-analytics.md)) | > Important: these variables must be set at build time, which is required by Next.js to pass them to the frontend. > This is in contrast to the backend variables, which can be set when starting the local server/container. diff --git a/package-lock.json b/package-lock.json index 9cc0b5695..598fe22ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@emotion/react": "^11.14.0", "@emotion/server": "^11.11.0", "@emotion/styled": "^11.14.1", + "@googleworkspace/drive-picker-react": "^0.2.0", "@mui/icons-material": "^5.18.0", "@mui/joy": "^5.0.0-beta.52", "@next/bundle-analyzer": "~15.1.11", @@ -691,6 +692,25 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@googleworkspace/drive-picker-element": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@googleworkspace/drive-picker-element/-/drive-picker-element-0.7.3.tgz", + "integrity": "sha512-z1hZh1HsPAQ19lencw2x3FcUVoymWYexcWgq66iXum4mUfWWaQ37oGtQ6hGvM8dyrC81G79P26gq7HhRtbGb2Q==", + "license": "Apache-2.0" + }, + "node_modules/@googleworkspace/drive-picker-react": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@googleworkspace/drive-picker-react/-/drive-picker-react-0.2.0.tgz", + "integrity": "sha512-3CIEZ7U+HDKd8UoXG3l/fPSZFhxajC3MYNIqAZQSba2totuYKVTQMOJsgonO7OnnJq88YD35n7whCF4i5QUyvA==", + "license": "Apache-2.0", + "dependencies": { + "@googleworkspace/drive-picker-element": "0.7.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", diff --git a/package.json b/package.json index ec0f5639d..f4604fe32 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@emotion/react": "^11.14.0", "@emotion/server": "^11.11.0", "@emotion/styled": "^11.14.1", + "@googleworkspace/drive-picker-react": "^0.2.0", "@mui/icons-material": "^5.18.0", "@mui/joy": "^5.0.0-beta.52", "@next/bundle-analyzer": "~15.1.11", diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index ba7b59b14..b4687d258 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -63,8 +63,10 @@ import { chatExecuteModeCanAttach, useChatExecuteMode } from '../../execute-mode import { ButtonAttachCameraMemo, useCameraCaptureModalDialog } from './buttons/ButtonAttachCamera'; import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard'; +import { ButtonAttachGoogleDriveMemo } from './buttons/ButtonAttachGoogleDrive'; import { ButtonAttachScreenCaptureMemo } from './buttons/ButtonAttachScreenCapture'; import { ButtonAttachWebMemo } from './buttons/ButtonAttachWeb'; +import { hasGoogleDriveCapability, useGoogleDrivePicker } from '~/common/attachment-drafts/useGoogleDrivePicker'; import { ButtonBeamMemo } from './buttons/ButtonBeam'; import { ButtonCallMemo } from './buttons/ButtonCall'; import { ButtonGroupDrawRepeat } from './buttons/ButtonGroupDrawRepeat'; @@ -197,7 +199,7 @@ export function Composer(props: { const showChatAttachments = chatExecuteModeCanAttach(chatExecuteMode, props.capabilityHasT2IEdit); const { /* items */ attachmentDrafts, - /* append */ attachAppendClipboardItems, attachAppendDataTransfer, attachAppendEgoFragments, attachAppendFile, attachAppendUrl, + /* append */ attachAppendClipboardItems, attachAppendCloudFile, attachAppendDataTransfer, attachAppendEgoFragments, attachAppendFile, attachAppendUrl, /* take */ attachmentsRemoveAll, attachmentsTakeAllFragments, attachmentsTakeFragmentsByType, } = useAttachmentDrafts(conversationOverlayStore, enableLoadURLsInComposer, chatLLMSupportsImages, handleFilterAGIFile, showChatAttachments === 'only-images'); @@ -623,6 +625,8 @@ export function Composer(props: { const { openWebInputDialog, webInputDialogComponent } = useWebInputModal(handleAttachWebLinks, composeText); + const { openGoogleDrivePicker, googleDrivePickerComponent } = useGoogleDrivePicker(attachAppendCloudFile, isMobile); + // Attachments Down @@ -802,6 +806,11 @@ export function Composer(props: { + {/* Responsive Google Drive button */} + {hasGoogleDriveCapability && + + } + {/* Responsive Paste button */} {supportsClipboardRead() && @@ -831,6 +840,9 @@ export function Composer(props: { {/* Responsive Web button */} {showChatAttachments !== 'only-images' && } + {/* Responsive Google Drive button */} + {hasGoogleDriveCapability && showChatAttachments !== 'only-images' && } + {/* Responsive Paste button */} {supportsClipboardRead() && showChatAttachments !== 'only-images' && } @@ -1126,6 +1138,9 @@ export function Composer(props: { {/* Camera (when open) */} {cameraCaptureComponent} + {/* Google Drive Picker (when open) */} + {googleDrivePickerComponent} + {/* Web Input Dialog (when open) */} {webInputDialogComponent} diff --git a/src/apps/chat/components/composer/buttons/ButtonAttachGoogleDrive.tsx b/src/apps/chat/components/composer/buttons/ButtonAttachGoogleDrive.tsx new file mode 100644 index 000000000..481bfb37f --- /dev/null +++ b/src/apps/chat/components/composer/buttons/ButtonAttachGoogleDrive.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; + +import { Box, Button, ColorPaletteProp, IconButton, Tooltip } from '@mui/joy'; +import AddToDriveRoundedIcon from '@mui/icons-material/AddToDriveRounded'; + +import { buttonAttachSx } from '~/common/components/ButtonAttachFiles'; +import { KeyStroke } from '~/common/components/KeyStroke'; + + +export const ButtonAttachGoogleDriveMemo = React.memo(ButtonAttachGoogleDrive); + +function ButtonAttachGoogleDrive(props: { + color?: ColorPaletteProp, + isMobile?: boolean, + disabled?: boolean, + fullWidth?: boolean, + noToolTip?: boolean, + onOpenGoogleDrivePicker: () => void, +}) { + + const button = props.isMobile ? ( + + + + ) : ( + + ); + + return (props.noToolTip || props.isMobile) ? button : ( + + Add from Google Drive
+ Attach files from your Drive + + + }> + {button} +
+ ); +} diff --git a/src/common/attachment-drafts/attachment.cloud.ts b/src/common/attachment-drafts/attachment.cloud.ts index f8c86ba09..0a259bcc1 100644 --- a/src/common/attachment-drafts/attachment.cloud.ts +++ b/src/common/attachment-drafts/attachment.cloud.ts @@ -43,10 +43,10 @@ type _CloudFetchErrorCode = 'AUTH_EXPIRED' | 'NOT_FOUND' | 'FORBIDDEN' | 'RATE_L * @see https://developers.google.com/workspace/drive/api/guides/ref-export-formats */ const _GOOGLE_WORKSPACE_EXPORT: Record = { - 'application/vnd.google-apps.document': { mimeType: 'text/markdown', ext: '.md', converter: 'Doc → ' }, - 'application/vnd.google-apps.spreadsheet': { mimeType: 'text/csv', ext: '.csv', converter: 'Sheet → ' }, - 'application/vnd.google-apps.presentation': { mimeType: 'text/plain', ext: '.txt', converter: 'Slides → ' }, - 'application/vnd.google-apps.drawing': { mimeType: 'image/png', ext: '.png', converter: 'Drawing → ' }, + 'application/vnd.google-apps.document': { mimeType: 'text/markdown', ext: '.md', converter: 'Doc -> ' }, + 'application/vnd.google-apps.spreadsheet': { mimeType: 'text/csv', ext: '.csv', converter: 'Sheet -> ' }, + 'application/vnd.google-apps.presentation': { mimeType: 'application/pdf', ext: '.pdf', converter: 'Slides -> ' }, + 'application/vnd.google-apps.drawing': { mimeType: 'image/svg+xml', ext: '.svg', converter: 'Drawing -> ' }, }; export function attachmentCloudGoogleWorkspaceExportMIME(cloudMimeType: string): string | undefined { @@ -54,7 +54,7 @@ export function attachmentCloudGoogleWorkspaceExportMIME(cloudMimeType: string): } export function attachmentCloudConverterPrefix(cloudMimeType: string): string { - return _GOOGLE_WORKSPACE_EXPORT[cloudMimeType]?.converter || ''; + return _GOOGLE_WORKSPACE_EXPORT[cloudMimeType]?.converter || 'Drive -> '; } @@ -110,6 +110,7 @@ async function _fetchGoogleDriveFile( 'Authorization': `Bearer ${accessToken}`, }, }).catch((error) => { + console.log('[DEV] Network error while fetching Google Drive file:', { error }); throw new CloudFetchError('NETWORK_ERROR', error?.message || String(error)); }); diff --git a/src/common/attachment-drafts/attachment.pipeline.ts b/src/common/attachment-drafts/attachment.pipeline.ts index 01e85a085..05ba47b86 100644 --- a/src/common/attachment-drafts/attachment.pipeline.ts +++ b/src/common/attachment-drafts/attachment.pipeline.ts @@ -391,7 +391,7 @@ export function attachmentDefineConverters(source: AttachmentDraftSource, input: break; } - // cosmetic for cloud: prepend cloud label prefixes (e.g., "Doc → ", "Sheet → ") + // cosmetic for cloud: prepend cloud label prefixes const cloudLabelPrefix = source.media === 'cloud' ? attachmentCloudConverterPrefix(source.mimeType) : ''; if (cloudLabelPrefix) for (const converter of converters) @@ -440,7 +440,8 @@ function _prepareDocData(source: AttachmentDraftSource, input: Readonly, 'media' | 'origin'>; + + /** * @param attachmentsStoreApi A Per-Chat or standalone Attachment Drafts store. * @param enableLoadURLsOnPaste Only used if invoking attachAppendDataTransfer or attachAppendClipboardItems. @@ -321,6 +324,27 @@ export function useAttachmentDrafts(attachmentsStoreApi: AttachmentDraftsStoreAp } }, [_createAttachmentDraft, attachAppendFile, attachAppendUrl, enableLoadURLsOnPaste, filterOnlyImages, hintAddImages]); + /** + * Append a cloud file (Google Drive, OneDrive, etc.) to the attachments. + * This is the entry point for cloud file picker integrations. + */ + const attachAppendCloudFile = React.useCallback((cloudFile: AttachmentStoreCloudInput) => { + if (ATTACHMENTS_DEBUG_INTAKE) + console.log('attachAppendCloudFile', cloudFile); + + // only-images: ignore cloud files as they may not be images + if (filterOnlyImages && !cloudFile.mimeType.startsWith('image/')) { + notifyOnlyImages(cloudFile); + return Promise.resolve(); + } + + return _createAttachmentDraft({ + media: 'cloud', + origin: `picker-${cloudFile.provider}`, + ...cloudFile, + }, { hintAddImages }); + }, [_createAttachmentDraft, filterOnlyImages, hintAddImages]); + /** * Append ego content to the attachments. */ @@ -341,30 +365,6 @@ export function useAttachmentDrafts(attachmentsStoreApi: AttachmentDraftsStoreAp }, { hintAddImages }); }, [_createAttachmentDraft, hintAddImages]); - /** - * Append a cloud file (Google Drive, OneDrive, etc.) to the attachments. - * This is the entry point for cloud file picker integrations. - * - * @param cloudFile - Cloud file metadata from the picker (provider, fileId, token, etc.) - * @returns Promise with attachment creation info, or null if failed - */ - const attachAppendCloudFile = React.useCallback((cloudFile: Omit, 'media' | 'origin'>) => { - if (ATTACHMENTS_DEBUG_INTAKE) - console.log('attachAppendCloudFile', cloudFile); - - // only-images: ignore cloud files as they may not be images - if (filterOnlyImages && !cloudFile.mimeType.startsWith('image/')) { - notifyOnlyImages(cloudFile); - return null; - } - - return _createAttachmentDraft({ - media: 'cloud', - origin: `picker-${cloudFile.provider}`, - ...cloudFile, - }, { hintAddImages }); - }, [_createAttachmentDraft, filterOnlyImages, hintAddImages]); - return { // state diff --git a/src/common/attachment-drafts/useGoogleDrivePicker.tsx b/src/common/attachment-drafts/useGoogleDrivePicker.tsx new file mode 100644 index 000000000..974d48b61 --- /dev/null +++ b/src/common/attachment-drafts/useGoogleDrivePicker.tsx @@ -0,0 +1,135 @@ +import * as React from 'react'; +import type { OAuthResponseEvent, PickerCanceledEvent, PickerPickedEvent } from '@googleworkspace/drive-picker-element'; +import { DrivePicker, DrivePickerDocsView } from '@googleworkspace/drive-picker-react'; + +import { addSnackbar } from '~/common/components/snackbar/useSnackbarsStore'; + +import type { AttachmentStoreCloudInput } from './useAttachmentDrafts'; + + +// configuration +const GOOGLE_DRIVE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID || ''; +const MAX_FILE_SIZE_MB = 10; // skip files larger than this; 0 = no limit; note: Google Workspace files report 0 bytes +const MAX_PICKER_FILES = 8; // max files per picker session; 0 = unlimited + +export const hasGoogleDriveCapability = !!GOOGLE_DRIVE_CLIENT_ID; + + +// Simple in-mem token store +let _cachedToken: string | null = null; + +// Session storage for OAuth token persistence +// const GDRIVE_TOKEN_KEY = 'google-drive-oauth-token'; + +function getStoredToken(): string | null { + return _cachedToken; + // if (typeof window === 'undefined') return null; + // return sessionStorage.getItem(GDRIVE_TOKEN_KEY); +} + +function storeToken(token: string): void { + _cachedToken = token; + // if (typeof window !== 'undefined') + // sessionStorage.setItem(GDRIVE_TOKEN_KEY, token); +} + + +export function useGoogleDrivePicker(onCloudFileSelected: (cloudFile: AttachmentStoreCloudInput) => void, isMobile: boolean, loginHint?: string) { + + // state + const [isPickerOpen, setIsPickerOpen] = React.useState(false); + const [oauthToken, setOauthToken] = React.useState(getStoredToken); + + + const openGoogleDrivePicker = React.useCallback(() => setIsPickerOpen(true), []); + + + const handleOAuthResponse = React.useCallback((e: OAuthResponseEvent) => { + if (e.detail?.access_token) { + setOauthToken(e.detail.access_token); + storeToken(e.detail.access_token); + } + }, []); + + const handleOAuthError = React.useCallback(() => { + setIsPickerOpen(false); + addSnackbar({ key: 'gdrive-oauth-error', message: 'Google Drive authentication failed.', type: 'issue' }); + }, []); + + + const handleCanceled = React.useCallback((_e: PickerCanceledEvent) => { + setIsPickerOpen(false); + }, []); + + const handlePicked = React.useCallback((e: PickerPickedEvent) => { + setIsPickerOpen(false); + + const docs = e.detail?.docs; + if (!docs?.length) return; + + if (!oauthToken) + return addSnackbar({ key: 'gdrive-no-token', message: 'Unable to access Google Drive.', type: 'issue' }); + + // convert picker docs to cloud file metadata for the attachment system + const maxBytes = MAX_FILE_SIZE_MB * 1024 * 1024; + const skippedFiles: string[] = []; + + for (const doc of docs) { + // skip files that are too large (note: Google Workspace files report 0 bytes) + if (MAX_FILE_SIZE_MB && doc.sizeBytes && doc.sizeBytes > maxBytes) { + skippedFiles.push(doc.name); + continue; + } + onCloudFileSelected({ + accessToken: oauthToken, + provider: 'gdrive', + fileId: doc.id, + mimeType: doc.mimeType, + fileName: doc.name, + fileSize: doc.sizeBytes, + webViewLink: doc.url, + }); + } + + if (skippedFiles.length) + addSnackbar({ key: 'gdrive-size-limit', message: `Skipped ${skippedFiles.length} file(s) over ${MAX_FILE_SIZE_MB} MB: ${skippedFiles.join(', ')}`, type: 'issue' }); + + }, [oauthToken, onCloudFileSelected]); + + + const googleDrivePickerComponent = React.useMemo(() => !isPickerOpen || !GOOGLE_DRIVE_CLIENT_ID ? null : ( + + + + + + ), [isPickerOpen, loginHint, oauthToken, handleOAuthResponse, handleOAuthError, handlePicked, handleCanceled, isMobile]); + + return { + openGoogleDrivePicker, + googleDrivePickerComponent, + }; +} diff --git a/src/common/styles/app.styles.css b/src/common/styles/app.styles.css index 8977f9ca4..e2004d1ae 100644 --- a/src/common/styles/app.styles.css +++ b/src/common/styles/app.styles.css @@ -18,4 +18,32 @@ /* Prevents pull-to-refresh on mobile, so it's not triggered while scrolling the chat inadvertently */ body { overscroll-behavior-y: none; -} \ No newline at end of file +} + + +/* Customize the Google Drive Picker background */ + +.picker-dialog-bg { + /* fixes a weird scrollbars issue */ + margin-top: -1px !important; + margin-left: -1px !important; +} + +iframe.picker-dialog-bg { + background: none !important; +} + +div.picker-dialog-bg { + /* we have alpha in the background, don't also use opacity */ + opacity: 1 !important; + /*noinspection CssUnresolvedCustomProperty*/ + background-color: rgba(var(--joy-palette-neutral-darkChannel, 11 13 14) / 0.25) !important; + /*backdrop-filter: blur(8px);*/ +} + +div.picker-dialog { + /*noinspection CssUnresolvedCustomProperty*/ + /*box-shadow: var(--joy-shadow-md);*/ + border-radius: 1.25rem; + border: none !important; +} diff --git a/src/server/env.server.ts b/src/server/env.server.ts index aee556650..fcf3f4d8f 100644 --- a/src/server/env.server.ts +++ b/src/server/env.server.ts @@ -126,6 +126,8 @@ export const env = createEnv({ * Environment variables available on the client (and server). * You'll get type errors if these are not prefixed with NEXT_PUBLIC_. * + * This is here basically for validation, but seems to not be used anywhere in the client code. + * * NOTE: they must be set at build time, not runtime(!) */ client: { @@ -133,6 +135,9 @@ export const env = createEnv({ // Frontend: Google Analytics GA4 Measurement ID NEXT_PUBLIC_GA4_MEASUREMENT_ID: z.string().optional(), + // Google Drive Picker: download files from Google Drive + NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID: z.string().optional(), + // Frontend: server to use for PlantUML rendering NEXT_PUBLIC_PLANTUML_SERVER_URL: z.url().optional(), @@ -144,6 +149,7 @@ export const env = createEnv({ // with Noext.JS >= 13.4.4 we'd only need to destructure client variables experimental__runtimeEnv: { NEXT_PUBLIC_GA4_MEASUREMENT_ID: process.env.NEXT_PUBLIC_GA4_MEASUREMENT_ID, + NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_DRIVE_CLIENT_ID, NEXT_PUBLIC_PLANTUML_SERVER_URL: process.env.NEXT_PUBLIC_PLANTUML_SERVER_URL, }, });