diff --git a/apps/design-system/content/docs/ui-patterns/markdown.mdx b/apps/design-system/content/docs/ui-patterns/markdown.mdx index 0b6e4a0d8b034..ac69e60d50654 100644 --- a/apps/design-system/content/docs/ui-patterns/markdown.mdx +++ b/apps/design-system/content/docs/ui-patterns/markdown.mdx @@ -37,17 +37,8 @@ import { Code, CodeBlockPre, DefaultPre, - H1, - H2, - H3, - H4, - H5, - H6, Hr, Img, - InlineCode, - ListItem, - OrderedList, Paragraph, Quote, SimplePre, diff --git a/apps/studio/components/grid/SupabaseGrid.utils.ts b/apps/studio/components/grid/SupabaseGrid.utils.ts index 002709c89711b..200fd880e88c8 100644 --- a/apps/studio/components/grid/SupabaseGrid.utils.ts +++ b/apps/studio/components/grid/SupabaseGrid.utils.ts @@ -1,4 +1,5 @@ import AwesomeDebouncePromise from 'awesome-debounce-promise' +import { safeLocalStorage, safeSessionStorage } from 'common' import { compact } from 'lodash' import { useSearchParams } from 'next/navigation' import { parseAsNativeArrayOf, parseAsString, useQueryStates } from 'nuqs' @@ -148,7 +149,7 @@ export function loadTableEditorStateFromLocalStorage( ): SavedState | undefined { const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef) // Prefer sessionStorage (scoped to current tab) over localStorage - const jsonStr = sessionStorage.getItem(storageKey) ?? localStorage.getItem(storageKey) + const jsonStr = safeSessionStorage.getItem(storageKey) ?? safeLocalStorage.getItem(storageKey) if (!jsonStr) return const json = JSON.parse(jsonStr) return json[tableId] @@ -198,7 +199,7 @@ export function saveTableEditorStateToLocalStorage({ filters?: string[] }) { const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef) - const savedStr = sessionStorage.getItem(storageKey) ?? localStorage.getItem(storageKey) + const savedStr = safeSessionStorage.getItem(storageKey) ?? safeLocalStorage.getItem(storageKey) const config = { ...(gridColumns !== undefined && { gridColumns }), @@ -215,8 +216,8 @@ export function saveTableEditorStateToLocalStorage({ savedJson = { [tableId]: config } } // Save to both localStorage and sessionStorage so it's consistent to current tab - localStorage.setItem(storageKey, JSON.stringify(savedJson)) - sessionStorage.setItem(storageKey, JSON.stringify(savedJson)) + safeLocalStorage.setItem(storageKey, JSON.stringify(savedJson)) + safeSessionStorage.setItem(storageKey, JSON.stringify(savedJson)) } export const saveTableEditorStateToLocalStorageDebounced = AwesomeDebouncePromise( diff --git a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx index 84e06cad8ed6a..244fdd24f2e94 100644 --- a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { SupportCategories } from '@supabase/shared-types/out/constants' -import { LOCAL_STORAGE_KEYS } from 'common' +import { LOCAL_STORAGE_KEYS, safeLocalStorage } from 'common' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' @@ -32,18 +32,18 @@ import { useProfile } from '@/lib/profile' const setDeletionRequestFlag = () => { const expiryDate = new Date() expiryDate.setDate(expiryDate.getDate() + 30) - localStorage.setItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST, expiryDate.toString()) + safeLocalStorage.setItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST, expiryDate.toString()) } const hasActiveDeletionRequest = () => { - const expiryDateStr = localStorage.getItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST) + const expiryDateStr = safeLocalStorage.getItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST) if (!expiryDateStr) return false const expiryDate = new Date(expiryDateStr) const now = new Date() if (now > expiryDate) { - localStorage.removeItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST) + safeLocalStorage.removeItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST) return false } diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index 472fe6ccf7973..fad16eb4c9ccd 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -1,4 +1,4 @@ -import { FeatureFlagContext, LOCAL_STORAGE_KEYS, useFlag } from 'common' +import { FeatureFlagContext, LOCAL_STORAGE_KEYS, safeLocalStorage, useFlag } from 'common' import { noop } from 'lodash' import { useQueryState } from 'nuqs' import { @@ -39,14 +39,10 @@ export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren) = setFlags( featurePreviews.reduce((a, b) => { const defaultOptIn = b.isDefaultOptIn - try { - const localStorageValue = window.localStorage.getItem(b.key) - return { - ...a, - [b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true', - } - } catch { - return { ...a, [b.key]: defaultOptIn } + const localStorageValue = safeLocalStorage.getItem(b.key) + return { + ...a, + [b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true', } }, {}) ) @@ -62,13 +58,7 @@ export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren) = const value = { flags, onUpdateFlag: (key: string, value: boolean) => { - try { - if (typeof window !== 'undefined' && window.localStorage) { - window.localStorage.setItem(key, value ? 'true' : 'false') - } - } catch { - // Silently fail in restricted storage modes (e.g. Safari private browsing) - } + safeLocalStorage.setItem(key, value ? 'true' : 'false') const updatedFlags = { ...flags, [key]: value } setFlags(updatedFlags) }, diff --git a/apps/studio/components/interfaces/BranchManagement/Overview.tsx b/apps/studio/components/interfaces/BranchManagement/Overview.tsx index af133208449d1..bec2163e23bee 100644 --- a/apps/studio/components/interfaces/BranchManagement/Overview.tsx +++ b/apps/studio/components/interfaces/BranchManagement/Overview.tsx @@ -8,6 +8,7 @@ import { Infinity, MoreVertical, Pencil, + Redo, RefreshCw, Shield, Trash2, @@ -30,6 +31,7 @@ import { EditBranchModal } from './EditBranchModal' import { PreviewBranchesEmptyState } from './EmptyStates' import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip' import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' +import { useBranchPushMutation } from '@/data/branches/branch-push-mutation' import { useBranchQuery } from '@/data/branches/branch-query' import { useBranchResetMutation } from '@/data/branches/branch-reset-mutation' import { useBranchRestoreMutation } from '@/data/branches/branch-restore-mutation' @@ -262,6 +264,7 @@ const PreviewBranchActions = ({ setShowPersistentBranchDeleteConfirmationModal, ] = useState(false) const [showEditBranchModal, setShowEditBranchModal] = useState(false) + const [showConfirmRetriggersModal, setShowConfirmRetriggersModal] = useState(false) const { mutate: resetBranch, isPending: isResetting } = useBranchResetMutation({ onSuccess() { @@ -286,6 +289,20 @@ const PreviewBranchActions = ({ }, }) + const { mutate: branchPushMutate, isPending: isRetriggering } = useBranchPushMutation({ + onSuccess() { + toast.success('Success! Please allow a few minutes for the branch to update.') + setShowConfirmRetriggersModal(false) + }, + onError: (data) => { + toast.error(`Failed to trigger workflow: ${data.message}`) + }, + }) + + const onRetriggerBranch = () => { + branchPushMutate({ branchRef, projectRef }) + } + const onRestoreBranch = () => { restoreBranch({ branchRef, projectRef }) } @@ -345,28 +362,54 @@ const PreviewBranchActions = ({ {!branch.deletion_scheduled_at && ( - { - e.stopPropagation() - setShowConfirmResetModal(true) - }} - onClick={(e) => { - e.stopPropagation() - setShowConfirmResetModal(true) - }} - tooltip={{ - content: { - side: 'left', - text: !isBranchActiveHealthy - ? 'Branch is still initializing. Please wait for it to become healthy before resetting.' - : undefined, - }, - }} - > - Reset branch - + <> + { + e.stopPropagation() + setShowConfirmRetriggersModal(true) + }} + onClick={(e) => { + e.stopPropagation() + setShowConfirmRetriggersModal(true) + }} + tooltip={{ + content: { + side: 'left', + text: !canUpdateBranches + ? `You need additional permissions to ${branch.git_branch ? 'resync' : 'rebase'} branches` + : !isBranchActiveHealthy + ? `Branch is still initializing. Please wait for it to become healthy before ${branch.git_branch ? 'resyncing' : 'rebasing'}.` + : undefined, + }, + }} + > + {branch.git_branch ? 'Resync branch' : 'Rebase branch'} + + { + e.stopPropagation() + setShowConfirmResetModal(true) + }} + onClick={(e) => { + e.stopPropagation() + setShowConfirmResetModal(true) + }} + tooltip={{ + content: { + side: 'left', + text: !isBranchActiveHealthy + ? 'Branch is still initializing. Please wait for it to become healthy before resetting.' + : undefined, + }, + }} + > + Reset branch + + )} {!branch.deletion_scheduled_at && ( @@ -497,10 +540,26 @@ const PreviewBranchActions = ({

+ setShowConfirmRetriggersModal(false)} + onConfirm={onRetriggerBranch} + > +

+ {branch.git_branch + ? 'This will re-run all steps of the workflow based on the latest git branch state.' + : 'This will re-run all steps of the workflow based on the latest dashboard state.'} +

+
+ setShowPersistentBranchDeleteConfirmationModal(false)} diff --git a/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx b/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx index f5967e6d7be7f..307289013f2f0 100644 --- a/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx +++ b/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx @@ -111,7 +111,7 @@ export const WorkflowLogs = ({ branch }: WorkflowLogsProps) => { (workflowRuns.length > 0 ? (