diff --git a/apps/studio/server/config.ts b/apps/studio/server/config.ts index 33ef7a1..905aab7 100644 --- a/apps/studio/server/config.ts +++ b/apps/studio/server/config.ts @@ -361,6 +361,52 @@ export function loadPublishEnv(): PublishEnvResult { export type PublicStudioConfig = Omit; +function publicPublisherPaths( + publisher: SupportedPublisher, + defaultBranch: string, +): Pick { + if (publisher === "gitlab") { + return { + owner: + process.env.GITLAB_PROJECT_PATH?.trim() || + process.env.GITLAB_PROJECT_ID?.trim() || + "", + repo: "", + branch: process.env.GITLAB_BRANCH?.trim() || defaultBranch, + }; + } + + if (publisher === "bitbucket") { + return { + owner: process.env.BITBUCKET_WORKSPACE?.trim() || "", + repo: process.env.BITBUCKET_REPO_SLUG?.trim() || "", + branch: process.env.BITBUCKET_BRANCH?.trim() || defaultBranch, + }; + } + + if (publisher === "wordpress") { + return { + owner: process.env.WORDPRESS_API_URL?.trim() || "", + repo: "", + branch: defaultBranch, + }; + } + + if (publisher === "ghost") { + return { + owner: process.env.GHOST_ADMIN_URL?.trim() || "", + repo: "", + branch: defaultBranch, + }; + } + + return { + owner: process.env.GITHUB_OWNER?.trim() || "", + repo: process.env.GITHUB_REPO?.trim() || "", + branch: process.env.GITHUB_BRANCH?.trim() || defaultBranch, + }; +} + export function loadPublicConfig(): PublicStudioConfig { const project = loadProjectConfig(); const rawAdapter = process.env.CMS_ADAPTER?.trim() || project.adapter; @@ -368,32 +414,10 @@ export function loadPublicConfig(): PublicStudioConfig { const adapter = resolveAdapter(rawAdapter) ?? "astro-mdx"; const publisher = resolvePublisher(rawPublisher) ?? "github"; const mediaDir = process.env.CMS_MEDIA_DIR?.trim() || project.mediaDir; - - let owner = ""; - let repo = ""; - let branch = project.defaultBranch; - - if (publisher === "gitlab") { - owner = - process.env.GITLAB_PROJECT_PATH?.trim() || - process.env.GITLAB_PROJECT_ID?.trim() || - ""; - branch = process.env.GITLAB_BRANCH?.trim() || project.defaultBranch; - } else if (publisher === "bitbucket") { - owner = process.env.BITBUCKET_WORKSPACE?.trim() || ""; - repo = process.env.BITBUCKET_REPO_SLUG?.trim() || ""; - branch = process.env.BITBUCKET_BRANCH?.trim() || project.defaultBranch; - } else if (publisher === "wordpress") { - owner = process.env.WORDPRESS_API_URL?.trim() || ""; - branch = project.defaultBranch; - } else if (publisher === "ghost") { - owner = process.env.GHOST_ADMIN_URL?.trim() || ""; - branch = project.defaultBranch; - } else { - owner = process.env.GITHUB_OWNER?.trim() || ""; - repo = process.env.GITHUB_REPO?.trim() || ""; - branch = process.env.GITHUB_BRANCH?.trim() || project.defaultBranch; - } + const { owner, repo, branch } = publicPublisherPaths( + publisher, + project.defaultBranch, + ); return { owner, diff --git a/apps/studio/server/setupHealth.ts b/apps/studio/server/setupHealth.ts index 0c64790..86f8d25 100644 --- a/apps/studio/server/setupHealth.ts +++ b/apps/studio/server/setupHealth.ts @@ -4,17 +4,14 @@ import { validateConfig } from "@sourcedraft/setup"; import { isAuthConfigured } from "./auth.js"; import { loadProjectConfig, loadPublicConfig } from "./config.js"; import { - isBitbucketConfigured, isBitbucketRepoConfigured, isBitbucketTokenConfigured, isBitbucketWorkspaceConfigured, isDemoModeAvailable, isDemoModeForced, - isGitHubConfigured, isGitHubOwnerConfigured, isGitHubRepoConfigured, isGitHubTokenConfigured, - isGitLabConfigured, isGitLabProjectConfigured, isGitLabTokenConfigured, isGhostAdminApiKeyConfigured, @@ -309,21 +306,15 @@ export function getSetupHealth(): SetupHealthReport { }, ]; - let nextAction: string | null = null; - - if (demoModeForced) { - nextAction = - "Demo mode is active. Explore Studio locally or configure your publisher and disable SOURCEDRAFT_DEMO_MODE for real publishing."; - } else if (!adminPasswordConfigured && demoModeAvailable) { - nextAction = - "Enter demo mode from the sign-in screen or set SOURCEDRAFT_ADMIN_PASSWORD for password sign-in."; - } else if (!adminPasswordConfigured) { - nextAction = "Set SOURCEDRAFT_ADMIN_PASSWORD in .env and restart the API server."; - } else if (!publisherReady) { - nextAction = publisherSetupMessage(activePublisher); - } else { - nextAction = null; - } + const nextAction = demoModeForced + ? "Demo mode is active. Explore Studio locally or configure your publisher and disable SOURCEDRAFT_DEMO_MODE for real publishing." + : !adminPasswordConfigured && demoModeAvailable + ? "Enter demo mode from the sign-in screen or set SOURCEDRAFT_ADMIN_PASSWORD for password sign-in." + : !adminPasswordConfigured + ? "Set SOURCEDRAFT_ADMIN_PASSWORD in .env and restart the API server." + : !publisherReady + ? publisherSetupMessage(activePublisher) + : null; const validation = validateConfig(); const compatibility: SetupCompatibilityReport = { diff --git a/apps/studio/src/App.tsx b/apps/studio/src/App.tsx index 959b744..226a9ca 100644 --- a/apps/studio/src/App.tsx +++ b/apps/studio/src/App.tsx @@ -113,7 +113,13 @@ function App() { return; } - fetchStudioConfig().then((config) => { + let cancelled = false; + + void fetchStudioConfig().then((config) => { + if (cancelled) { + return; + } + setStudioConfig(config); setDemoMode(config.demoMode === true); setForm((current) => { @@ -128,7 +134,14 @@ function App() { }); }); - void refreshPosts(); + const postsTimer = window.setTimeout(() => { + void refreshPosts(); + }, 0); + + return () => { + cancelled = true; + window.clearTimeout(postsTimer); + }; }, [authenticated, refreshPosts]); const githubReady = useMemo( diff --git a/apps/studio/src/components/MarkdownToolbar.tsx b/apps/studio/src/components/MarkdownToolbar.tsx index b6d685d..7524833 100644 --- a/apps/studio/src/components/MarkdownToolbar.tsx +++ b/apps/studio/src/components/MarkdownToolbar.tsx @@ -1,7 +1,6 @@ -import { useState, type KeyboardEvent, type RefObject } from "react"; +import { useState, type RefObject } from "react"; import type { PostSummary } from "../lib/posts.js"; import { - actionForShortcut, applyMarkdownAction, applyResultToTextarea, selectionFromTextarea, @@ -185,28 +184,3 @@ export function MarkdownToolbar({ ); } - -export function handleMarkdownShortcut( - event: KeyboardEvent, - body: string, - onBodyChange: (body: string) => void, -): boolean { - if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) { - return false; - } - - const action = actionForShortcut(event.key); - if (!action) { - return false; - } - - event.preventDefault(); - const textarea = event.currentTarget; - const selection = selectionFromTextarea(textarea); - const result = applyMarkdownAction(body, selection, action); - onBodyChange(result.value); - requestAnimationFrame(() => { - applyResultToTextarea(textarea, result); - }); - return true; -} diff --git a/apps/studio/src/components/MediaLibrary.tsx b/apps/studio/src/components/MediaLibrary.tsx index 297adb0..4828cbe 100644 --- a/apps/studio/src/components/MediaLibrary.tsx +++ b/apps/studio/src/components/MediaLibrary.tsx @@ -48,7 +48,13 @@ export function MediaLibrary({ }, [githubReady]); useEffect(() => { - void loadLibrary(); + const timer = window.setTimeout(() => { + void loadLibrary(); + }, 0); + + return () => { + window.clearTimeout(timer); + }; }, [loadLibrary, refreshKey]); async function handleCopy(publicPath: string) { diff --git a/apps/studio/src/components/WritingCanvas.tsx b/apps/studio/src/components/WritingCanvas.tsx index c014d32..41b9375 100644 --- a/apps/studio/src/components/WritingCanvas.tsx +++ b/apps/studio/src/components/WritingCanvas.tsx @@ -1,10 +1,8 @@ import { useId, useRef } from "react"; import type { PostSummary } from "../lib/posts"; import { DocumentOutline } from "./DocumentOutline"; -import { - handleMarkdownShortcut, - MarkdownToolbar, -} from "./MarkdownToolbar"; +import { handleMarkdownShortcut } from "../lib/markdownShortcuts"; +import { MarkdownToolbar } from "./MarkdownToolbar"; type WritingCanvasProps = { title: string; diff --git a/apps/studio/src/hooks/useDocumentAutosave.ts b/apps/studio/src/hooks/useDocumentAutosave.ts index ece4a36..1373472 100644 --- a/apps/studio/src/hooks/useDocumentAutosave.ts +++ b/apps/studio/src/hooks/useDocumentAutosave.ts @@ -49,7 +49,7 @@ export function useDocumentAutosave({ publishing, enabled, }: UseDocumentAutosaveOptions): UseDocumentAutosaveResult { - const baselineRef = useRef(snapshot); + const [baseline, setBaseline] = useState(snapshot); const [localSavedAt, setLocalSavedAt] = useState(null); const [syncedWithRemote, setSyncedWithRemote] = useState(false); const [restorePrompt, setRestorePrompt] = useState( @@ -60,8 +60,8 @@ export function useDocumentAutosave({ const initialCheckDoneRef = useRef(false); const isDirty = useMemo( - () => !snapshotsEqual(snapshot, baselineRef.current), - [snapshot], + () => !snapshotsEqual(snapshot, baseline), + [snapshot, baseline], ); const checkRestorePrompt = useCallback((current: DocumentSnapshot) => { @@ -91,11 +91,11 @@ export function useDocumentAutosave({ const commitBaseline = useCallback( (baseline: DocumentSnapshot, options: CommitBaselineOptions) => { - baselineRef.current = { + setBaseline({ form: { ...baseline.form }, editingPath: baseline.editingPath, slugAuto: baseline.slugAuto, - }; + }); setSyncedWithRemote(options.remoteSync); setLocalSavedAt(null); setRestorePrompt(null); diff --git a/apps/studio/src/lib/markdownShortcuts.ts b/apps/studio/src/lib/markdownShortcuts.ts new file mode 100644 index 0000000..aa7a39e --- /dev/null +++ b/apps/studio/src/lib/markdownShortcuts.ts @@ -0,0 +1,32 @@ +import type { KeyboardEvent } from "react"; +import { + actionForShortcut, + applyMarkdownAction, + applyResultToTextarea, + selectionFromTextarea, +} from "./markdownEditor.js"; + +export function handleMarkdownShortcut( + event: KeyboardEvent, + body: string, + onBodyChange: (body: string) => void, +): boolean { + if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) { + return false; + } + + const action = actionForShortcut(event.key); + if (!action) { + return false; + } + + event.preventDefault(); + const textarea = event.currentTarget; + const selection = selectionFromTextarea(textarea); + const result = applyMarkdownAction(body, selection, action); + onBodyChange(result.value); + requestAnimationFrame(() => { + applyResultToTextarea(textarea, result); + }); + return true; +}