diff --git a/.changeset/humble-bananas-rush.md b/.changeset/humble-bananas-rush.md deleted file mode 100644 index b8d338c..0000000 --- a/.changeset/humble-bananas-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": patch ---- - -Fix incorrect Dark Mode Tooltip Behavior diff --git a/.changeset/late-sloths-film.md b/.changeset/late-sloths-film.md deleted file mode 100644 index 2374d17..0000000 --- a/.changeset/late-sloths-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": patch ---- - -Fix incorrect header label on $ref target nodes in the graph view diff --git a/.changeset/proud-seals-watch.md b/.changeset/proud-seals-watch.md deleted file mode 100644 index f401a07..0000000 --- a/.changeset/proud-seals-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": patch ---- - -Update CI workflows for better version handling diff --git a/.changeset/silver-teeth-notice.md b/.changeset/silver-teeth-notice.md deleted file mode 100644 index 810416e..0000000 --- a/.changeset/silver-teeth-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": patch ---- - -Fix graph zoom reset when editor panel is resized diff --git a/.changeset/unified-search-bar.md b/.changeset/unified-search-bar.md new file mode 100644 index 0000000..4f0b43c --- /dev/null +++ b/.changeset/unified-search-bar.md @@ -0,0 +1,5 @@ +--- +"json-schema-studio": minor +--- + +Add unified search bar that highlights matching graph nodes and corresponding editor code diff --git a/.changeset/wacky-cities-share.md b/.changeset/wacky-cities-share.md deleted file mode 100644 index 7f64e1c..0000000 --- a/.changeset/wacky-cities-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"json-schema-studio": minor ---- - -Added changeset bot diff --git a/.github/workflows/build-preview.yml b/.github/workflows/build-preview.yml index cb3b969..380083c 100644 --- a/.github/workflows/build-preview.yml +++ b/.github/workflows/build-preview.yml @@ -35,6 +35,9 @@ jobs: - name: Save PR number run: echo "${{ github.event.pull_request.number }}" > pr_number.txt + - name: Save PR head SHA + run: echo "${{ github.event.pull_request.head.sha }}" > pr_head_sha.txt + - name: Upload build artifact uses: actions/upload-artifact@v4 with: @@ -42,3 +45,4 @@ jobs: path: | dist pr_number.txt + pr_head_sha.txt diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 840ef89..075c9ab 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -32,6 +32,36 @@ jobs: PR_NUMBER=$(cat artifact/pr_number.txt) echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + - name: Read PR head SHA + id: sha + run: | + PR_HEAD_SHA=$(cat artifact/pr_head_sha.txt) + echo "pr_head_sha=$PR_HEAD_SHA" >> $GITHUB_OUTPUT + + - name: Create GitHub Deployment + id: deployment + uses: actions/github-script@v7 + with: + script: | + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: '${{ steps.sha.outputs.pr_head_sha }}', + environment: 'pr-preview-${{ steps.pr.outputs.pr_number }}', + auto_merge: false, + required_contexts: [], + transient_environment: true, + description: 'PR Preview Deployment' + }); + core.setOutput('deployment_id', deployment.data.id); + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.data.id, + state: 'in_progress', + description: 'Deploying preview to Cloudflare Pages...' + }); + - name: Publish to Cloudflare Pages id: cloudflare_deploy uses: cloudflare/wrangler-action@v3 @@ -43,12 +73,41 @@ jobs: --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=pr-${{ steps.pr.outputs.pr_number }} + - name: Set deployment status to success + if: success() + uses: actions/github-script@v7 + with: + script: | + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ steps.deployment.outputs.deployment_id }}, + state: 'success', + environment_url: '${{ steps.cloudflare_deploy.outputs.pages-deployment-alias-url }}', + description: 'Preview deployed successfully' + }); + + - name: Set deployment status to failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ steps.deployment.outputs.deployment_id }}, + state: 'failure', + log_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}', + description: 'Preview deployment failed' + }); + - name: Comment PR with Preview Link + if: success() uses: thollander/actions-comment-pull-request@v3 with: pr-number: ${{ steps.pr.outputs.pr_number }} message: | - ### Preview Deployed! + ### ✅ Preview Deployed! | Item | Status | | :--- | :--- | @@ -57,4 +116,21 @@ jobs: | **Action** | [View Logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | _Last updated at ${{ github.event.workflow_run.updated_at }}_ - comment-tag: preview_deploy_status # This ensures it updates the same comment instead of spamming + comment-tag: preview_deploy_status + + - name: Comment PR with failure notice + if: failure() + uses: thollander/actions-comment-pull-request@v3 + with: + pr-number: ${{ steps.pr.outputs.pr_number }} + message: | + ### ⚠️ Preview Deploy Failed! + + | Item | Status | + | :--- | :--- | + | **Latest Deploy** | ❌ Failed | + | **Environment** | `Preview (PR-${{ steps.pr.outputs.pr_number }})` | + | **Action** | [View Logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | + + _Last updated at ${{ github.event.workflow_run.updated_at }}_ + comment-tag: preview_deploy_status diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ace27be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# json-schema-studio + +## 0.7.0 + +### Minor Changes + +- 1f46606: Added changeset bot + +### Patch Changes + +- 6207986: fix: prevent keyboard auto-opening on node selection in mobile +- 0e8a67a: Fix incorrect Dark Mode Tooltip Behavior +- 900e962: Fix incorrect header label on $ref target nodes in the graph view +- 2253d53: Update CI workflows for better version handling +- 24e6b85: Fix graph zoom reset when editor panel is resized diff --git a/package.json b/package.json index 033f58b..cf76d9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-schema-studio", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "homepage": "studio.ioflux.org", "repository": "https://github.com/ioflux-org/studio-json-schema", diff --git a/src/App.css b/src/App.css index fecd830..5a38dbf 100644 --- a/src/App.css +++ b/src/App.css @@ -1,11 +1,14 @@ -body{ - background-color: var(--bg-color) +body { + background-color: var(--bg-color); } .visualize { background-color: var(--visualize-bg-color); - background-image: - linear-gradient(to right, var(--visualize-bg-image) 0.1px, transparent 1px), + background-image: linear-gradient( + to right, + var(--visualize-bg-image) 0.1px, + transparent 1px + ), linear-gradient(to bottom, var(--visualize-bg-image) 0.1px, transparent 1px); background-size: 30px 30px; } diff --git a/src/components/GraphView.tsx b/src/components/GraphView.tsx index 7e970a3..2cba710 100644 --- a/src/components/GraphView.tsx +++ b/src/components/GraphView.tsx @@ -49,7 +49,8 @@ const GraphView = ({ compiledSchema: CompiledSchema | null; }) => { const { setCenter, getZoom, fitView } = useReactFlow(); - const { selectedNode, setSelectedNode } = useContext(AppContext); + const { selectedNode, setSelectedNode, searchString, registerNavigateMatch } = + useContext(AppContext); const containerRef = useRef(null); const [nodes, setNodes, onNodeChange] = useNodesState([]); @@ -58,7 +59,6 @@ const GraphView = ({ const [hoveredEdgeId, setHoveredEdgeId] = useState(null); const [matchedNodes, setMatchedNodes] = useState([]); const [currentMatchIndex, setCurrentMatchIndex] = useState(0); - const [searchString, setSearchString] = useState(""); const [errorMessage, setErrorMessage] = useState(""); const [showErrorPopup, setShowErrorPopup] = useState(true); const matchCount = matchedNodes.length; @@ -78,6 +78,9 @@ const GraphView = ({ const x = foundNode.position.x + NODE_WIDTH / 2; const y = foundNode.position.y + NODE_HEIGHT / 2; + setSelectedNode({ + id: foundNode.id, + }); setCenter(x, y, { zoom: Math.max(getZoom(), 1), duration: 500 }); setNodes((nds) => @@ -90,12 +93,12 @@ const GraphView = ({ return newIndex; }); }, - [matchedNodes] + [matchedNodes, matchCount, setCenter, getZoom, setNodes] ); - const handleChange = useCallback((e: React.ChangeEvent) => { - setSearchString(e.target.value); - }, []); + useEffect(() => { + registerNavigateMatch(navigateMatch); + }, [navigateMatch, registerNavigateMatch]); const onNodeClick: NodeMouseHandler = useCallback((_event, node) => { setSelectedNode({ @@ -275,8 +278,13 @@ const GraphView = ({ const observer = new ResizeObserver(() => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { - const currentZoom = getZoom(); - fitView({ duration: 800, minZoom: currentZoom, maxZoom: currentZoom, padding: 0.05 }); + const currentZoom = getZoom(); + fitView({ + duration: 800, + minZoom: currentZoom, + maxZoom: currentZoom, + padding: 0.05, + }); }, 100); }); @@ -292,7 +300,7 @@ const GraphView = ({ const trimmed = searchString.trim(); const timeout = setTimeout(() => { - if (!trimmed) { + if (!trimmed || trimmed.length < 3) { setMatchedNodes([]); setCurrentMatchIndex(0); setErrorMessage(""); @@ -307,17 +315,22 @@ const GraphView = ({ searchWords.length === 0 ? [] : nodes.filter((node) => { - const labelWords = extractKeywords(node.data.nodeLabel); - return searchWords.every((word) => labelWords.includes(word)); + const titleKeyWords = extractKeywords(node.data.nodeLabel); + return searchWords.every((word) => titleKeyWords.includes(word)); }); setMatchedNodes(foundNodes); if (foundNodes.length > 0) { - const firstNode = foundNodes[currentMatchIndex % foundNodes.length]; + setCurrentMatchIndex(0); + const firstNode = foundNodes[0]; const x = firstNode.position.x + NODE_WIDTH / 2; const y = firstNode.position.y + NODE_HEIGHT / 2; + setSelectedNode({ + id: firstNode.id, + }); + setCenter(x, y, { zoom: Math.max(getZoom(), 1), duration: 500 }); setNodes((nds) => { let changed = false; @@ -331,33 +344,19 @@ const GraphView = ({ setErrorMessage(""); } else { + setSelectedNode(null); + fitView({ duration: 800, padding: 0.05 }); setErrorMessage(`${trimmed} is not in schema`); } }, 300); return () => clearTimeout(timeout); }, [searchString]); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (matchCount <= 1) return; - - if (e.key === "ArrowRight" || e.key === "Enter") { - e.preventDefault(); - navigateMatch("next"); - } else if (e.key === "ArrowLeft") { - e.preventDefault(); - navigateMatch("prev"); - } - }, - [matchCount, navigateMatch] - ); - return (
- {selectedNode && ( + {selectedNode?.data && (
)} -
-
- - - {searchString && ( - - )} + {matchCount > 1 && ( +
+ + + {currentMatchIndex + 1}/{matchCount} + +
- {matchCount > 1 && ( -
- - - - {currentMatchIndex + 1}/{matchCount} - - - -
- )} -
+ )}
); }; diff --git a/src/components/MonacoEditor.tsx b/src/components/MonacoEditor.tsx index 87478bf..9891127 100644 --- a/src/components/MonacoEditor.tsx +++ b/src/components/MonacoEditor.tsx @@ -89,8 +89,14 @@ const saveSchemaJSON = (key: string, schema: JSONSchema) => { }; const MonacoEditor = () => { - const { theme, isFullScreen, containerRef, schemaFormat, changeSchemaFormat, selectedNode } = - useContext(AppContext); + const { + theme, + isFullScreen, + containerRef, + schemaFormat, + changeSchemaFormat, + selectedNode, + } = useContext(AppContext); const editorRef = useRef(null); const editorPanelRef = useRef(null); @@ -177,7 +183,6 @@ const MonacoEditor = () => { editorRef.current.revealPositionInCenter(startPos); editorRef.current.setPosition(startPos); - editorRef.current.focus(); const decoration = { range: new (window as any).monaco.Range( @@ -285,9 +290,7 @@ const MonacoEditor = () => { isAnimating ? "panel-animating" : "" }`} > - {isFullScreen && ( - - )} + {isFullScreen && } {
setSearchString(e.target.value)} + onKeyDown={handleKeyDown} + /> +
+ {searchString ? ( + + ) : ( + + ⌘K + + )} +
+
+ + +
  • + +
  • +
  • - {theme === "light" && ( - - )} - - + {theme === "light" && ( + + )}
  • {
  • + + {mobileSearchOpen && ( +
    + + + { + setSearchString(e.target.value); + }} + onKeyDown={handleKeyDown} + /> + +
    + )} ); }; diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index 9ecd0e7..410d286 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -4,9 +4,11 @@ export type SchemaFormat = "json" | "yaml"; export type SelectedNode = { id: string; - data: Record; + data?: Record; }; +export type NavigationDirection = "next" | "prev"; + type AppContextType = { containerRef: Ref; isFullScreen: boolean; @@ -19,8 +21,13 @@ type AppContextType = { changeSchemaFormat: (format: SchemaFormat) => void; selectedNode: SelectedNode | null; - setSelectedNode: (selectedNode: SelectedNode | null) => void; + + searchString: string; + setSearchString: (search: string) => void; + + registerNavigateMatch: (fn: (dir: NavigationDirection) => void) => void; + triggerNavigateMatch: (dir: NavigationDirection) => void; }; export const AppContext = createContext({} as AppContextType); diff --git a/src/contexts/AppProvider.tsx b/src/contexts/AppProvider.tsx index be7df06..f1969ca 100644 --- a/src/contexts/AppProvider.tsx +++ b/src/contexts/AppProvider.tsx @@ -5,7 +5,12 @@ import { useRef, type ReactNode, } from "react"; -import { AppContext, type SchemaFormat, type SelectedNode } from "./AppContext"; +import { + AppContext, + type NavigationDirection, + type SchemaFormat, + type SelectedNode, +} from "./AppContext"; export const AppProvider = ({ children }: { children: ReactNode }) => { const containerRef = useRef(null); @@ -38,6 +43,19 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { }; const [selectedNode, setSelectedNode] = useState(null); + const [searchString, setSearchString] = useState(""); + + const navigateMatchRef = useRef<((dir: NavigationDirection) => void) | null>( + null + ); + + const registerNavigateMatch = (fn: (dir: NavigationDirection) => void) => { + navigateMatchRef.current = fn; + }; + + const triggerNavigateMatch = (dir: NavigationDirection) => { + navigateMatchRef.current?.(dir); + }; const toggleFullScreen = useCallback(() => { const el = containerRef.current; @@ -81,6 +99,10 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { changeSchemaFormat, selectedNode, setSelectedNode, + searchString, + setSearchString, + registerNavigateMatch, + triggerNavigateMatch, }; return {children};