Files
supabase/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
T
Francesco Sansalvadore 25708ed67a chore(studio): storage versioning override conflict (#51593)
## Problem

When uploading a file with same name of already existing file, the ui
automatically creates a new file with incremental number and doesn't
consider the scenario of giving the possibility of storing the new
upload as a version of the already existing file.

## Solution

Provide modal to chose the behaviour when name conflict happens when
uploading a file:
- Replace existing file (same file with version history)
- Keep both (new file with incremental numbering)


https://github.com/user-attachments/assets/ee192be9-4087-4e14-89f1-1c82acf550cd

Manage edge case of clashes with files created when bucket had
versioning enabled (so with `is_versioned` flag to true) and clashing
upload happens when bucket is in SUSPENDED state.


https://github.com/user-attachments/assets/00694c77-07c1-405e-bd54-bdf0bf99d274

Unrelated fix:
- add "Delete permanently" action when selecting multiple files with
versioning enabled

<img width="510" height="203" alt="Screenshot 2026-10-13 at 15 59 27"
src="https://github.com/user-attachments/assets/5692fc23-2fd3-4259-9de2-3932aecbccd0"
/>
2026-10-13 17:14:30 +02:00

230 lines
7.6 KiB
TypeScript

import {
FeatureFlagContext,
LOCAL_STORAGE_KEYS,
safeLocalStorage,
useFlag,
useParams,
} from 'common'
import { noop } from 'lodash'
import { useQueryState } from 'nuqs'
import {
createContext,
useCallback,
useContext,
useEffect,
useEffectEvent,
useMemo,
useState,
type PropsWithChildren,
} from 'react'
import { useFeaturePreviews } from './useFeaturePreviews'
import { useProjectStorageConfigQuery } from '@/data/config/project-storage-config-query'
import { IS_PLATFORM } from '@/lib/constants'
import { EMPTY_OBJ } from '@/lib/void'
type FeaturePreviewContextType = {
flags: { [key: string]: boolean }
isInitialized: boolean
onUpdateFlag: (key: string, value: boolean) => void
}
const FeaturePreviewContext = createContext<FeaturePreviewContextType>({
flags: EMPTY_OBJ,
isInitialized: false,
onUpdateFlag: noop,
})
export const useFeaturePreviewContext = () => useContext(FeaturePreviewContext)
export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren) => {
const { hasLoaded } = useContext(FeatureFlagContext)
const featurePreviews = useFeaturePreviews()
const [flags, setFlags] = useState(() =>
featurePreviews.reduce((a, b) => ({ ...a, [b.key]: false }), {})
)
// Tracks whether `flags` reflects the loaded feature flags (vs. the pre-load
// defaults). Only set true once `initializeFlags` runs with `hasLoaded`.
const [isInitialized, setIsInitialized] = useState(false)
const initializeFlags = useEffectEvent(() => {
setFlags(
featurePreviews.reduce((a, b) => {
// Platform-only previews can never be enabled outside the hosted platform
if (!IS_PLATFORM && b.isPlatformOnly) {
return { ...a, [b.key]: false }
}
// A forced preview has become the default behavior, so it's on whatever
// the user stored previously — including an explicit opt-out. Applied
// here rather than in the individual `useIsXEnabled` helpers so that the
// feature preview modal reflects it too.
if (b.isForced) {
return { ...a, [b.key]: true }
}
const defaultOptIn = b.isDefaultOptIn
const localStorageValue = safeLocalStorage.getItem(b.key)
return {
...a,
[b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true',
}
}, {})
)
})
useEffect(() => {
if (typeof window !== 'undefined') {
initializeFlags()
// Defer marking initialized until the underlying flags have loaded, so
// flag-derived defaults (e.g. default opt-in) are reflected in `flags`.
if (hasLoaded) setIsInitialized(true)
}
}, [hasLoaded])
const value = {
flags,
isInitialized,
onUpdateFlag: (key: string, value: boolean) => {
safeLocalStorage.setItem(key, value ? 'true' : 'false')
setFlags((prevFlags) => ({ ...prevFlags, [key]: value }))
},
}
return <FeaturePreviewContext.Provider value={value}>{children}</FeaturePreviewContext.Provider>
}
// Helpers
export const usePipelineCreationPreview = () => {
const { flags, isInitialized } = useFeaturePreviewContext()
const isAvailable = useFlag('pipelineCreationWizard')
const isEnabled =
IS_PLATFORM &&
isInitialized &&
isAvailable &&
flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PIPELINE_CREATION] === true
return { isEnabled, isLoading: !isInitialized }
}
export const useIsColumnLevelPrivilegesEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]
}
export const useUnifiedLogsPreview = () => {
const { flags, isInitialized, onUpdateFlag } = useFeaturePreviewContext()
const isLoading = !isInitialized
const isEnabled = IS_PLATFORM && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]
const hasToggledPreview = !!safeLocalStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS)
const isDefaultOptIn = IS_PLATFORM && !hasToggledPreview
const enable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, true)
const disable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, false)
return { isEnabled, isLoading, isDefaultOptIn, enable, disable }
}
export const useIsPgDeltaDiffEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PG_DELTA_DIFF]
}
export const useIsAdvisorRulesEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES]
}
export const useIsPlatformWebhooksEnabled = () => {
const { flags } = useFeaturePreviewContext()
const platformWebhooksEnabled = useFlag('platformWebhooks')
return platformWebhooksEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]
}
export const useIsJitDbAccessEnabled = () => {
const { flags } = useFeaturePreviewContext()
const jitDbAccessEnabled = useFlag('jitDbAccess')
return jitDbAccessEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]
}
/**
* Whether the SQL Editor saves snippets only on request. True when the user
* opted into the preview themselves *or* the `sqlEditorManualSaveForced` rollout
* has reached them — the preview's `isForced` flag folds the latter into `flags`.
*/
export const useIsSqlEditorManualSaveEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]
}
export const useIsMarketplaceEnabled = () => {
const { flags } = useFeaturePreviewContext()
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
return isMarketplaceEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]
}
export const useIsExplorerEnabled = () => {
const { flags } = useFeaturePreviewContext()
const isExplorerEnabled = useFlag('explorer')
return isExplorerEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_EXPLORER]
}
/** Three gates: the ConfigCat kill switch, the project's capability, and the user's opt-in. */
export const useIsStorageVersioningEnabled = () => {
const { ref } = useParams()
const { flags } = useFeaturePreviewContext()
const isStorageVersioningEnabled = useFlag('storageVersioningPrivateAlpha')
const { data } = useProjectStorageConfigQuery({ projectRef: ref })
const isObjectVersioningAvailable = !!data?.capabilities?.object_versioning
return (
isStorageVersioningEnabled &&
isObjectVersioningAvailable &&
flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_STORAGE_VERSIONING]
)
}
export const useFeaturePreviewModal = () => {
const featurePreviews = useFeaturePreviews()
const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal')
const selectedFeatureKeyFromQuery = featurePreviewModal?.trim() ?? null
const showFeaturePreviewModal = selectedFeatureKeyFromQuery !== null
const selectedFeatureKey = (
!selectedFeatureKeyFromQuery ? featurePreviews[0].key : selectedFeatureKeyFromQuery
) as (typeof featurePreviews)[number]['key']
const selectFeaturePreview = useCallback(
(featureKey: (typeof featurePreviews)[number]['key']) => {
setFeaturePreviewModal(featureKey)
},
[setFeaturePreviewModal]
)
const toggleFeaturePreviewModal = useCallback(
(value: boolean) => {
if (!value) {
setFeaturePreviewModal(null)
} else {
selectFeaturePreview(selectedFeatureKey)
}
},
[selectFeaturePreview, setFeaturePreviewModal, selectedFeatureKey]
)
return useMemo(
() => ({
showFeaturePreviewModal,
selectedFeatureKey,
selectFeaturePreview,
toggleFeaturePreviewModal,
}),
[showFeaturePreviewModal, selectedFeatureKey, selectFeaturePreview, toggleFeaturePreviewModal]
)
}