Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
429f042
feat: enhance "Import from URL" with remote $refs and URL tracking
ivangsa Feb 16, 2026
dbbaab4
wicg-file-system-access
ivangsa Mar 2, 2026
e45d614
grants folder access
ivangsa Mar 3, 2026
0d9189c
url navigation removal
ivangsa Mar 3, 2026
2c15772
fix: buttons nested inside buttons
ivangsa Mar 3, 2026
fadc475
filetree frist working draft
ivangsa Mar 3, 2026
eb22384
tree view
ivangsa Mar 3, 2026
53bac5c
fix: linting errors
ivangsa Mar 6, 2026
ee295ab
(keep an eye) authomatic fixes for Sonnar warnings
ivangsa Mar 6, 2026
ca01b8c
fix for netlify build error
ivangsa Mar 6, 2026
f42f32e
fix for netlify build error
ivangsa Mar 6, 2026
0420839
fix for netlify build error
ivangsa Mar 6, 2026
fdb9a60
debug grantFolderAccess
ivangsa Mar 6, 2026
633d793
rename import buttons
ivangsa Mar 9, 2026
d0a393b
reorder buttons
ivangsa Mar 9, 2026
40b1abe
write functionality
ivangsa Mar 9, 2026
3bfc88a
add markdown and avsc preview
ivangsa Mar 11, 2026
f1d1355
docs reflect new functionality
ivangsa Mar 11, 2026
7659363
fix side bar and "unsaved changes" label
ivangsa Mar 11, 2026
3b12869
fixes for netlify linter
ivangsa Mar 11, 2026
27bff3c
fixes for netlify linter
ivangsa Mar 11, 2026
eb7fd79
BrowserNotSupportedModal
ivangsa Mar 11, 2026
8bc24e0
Merge branch 'master' into master
ivangsa Mar 11, 2026
923cc73
wrap console.log to disable in prod
ivangsa Mar 11, 2026
1ec964c
Merge branch 'master' into master
ivangsa Mar 22, 2026
53974ed
update FileTreeView to use a Splitpanel
ivangsa Mar 30, 2026
551f358
measures and light optimization of treeview rendering
ivangsa Mar 30, 2026
5e77023
fix: sonar
ivangsa Mar 31, 2026
6300f31
fix: mermaid first rendering
ivangsa Mar 31, 2026
469fda0
fix netlify linting errors
ivangsa Mar 31, 2026
1d05938
fix(studio): narrow save file picker accept types
ivangsa Mar 31, 2026
6b2daf1
`fix(studio): resolve file picker and monaco build typings`
ivangsa Mar 31, 2026
9d1aa49
fix sonnar issues
ivangsa Mar 31, 2026
40850f7
fixes sonnar issue
ivangsa Apr 1, 2026
2a2c76c
chore: UI change
Shurtu-gal Apr 2, 2026
713bb95
fix: don't show file tree in normal view
Shurtu-gal Apr 2, 2026
9941459
chore: add adr and changelog
Shurtu-gal Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/lovely-keys-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@asyncapi/studio": minor
---

Add local-folder editing with relative reference resolution and direct file saving.

- Added support for opening and editing local folders via the File System Access API.
- Added automatic resolution of relative references (for example `$ref`, `./`, and `../`) inside opened folders.
- Added direct writes to local files, with format inference on Save and Save As based on file extension.
- Added preview support for OpenAPI (RapiDoc), Markdown (including Mermaid), and Avro schemas, alongside existing AsyncAPI preview.
- Changed save/export behavior: Save writes to the current local file when available, while Save As prompts for a destination and infers output format from the extension.
- Removed auto-save and draft restoration via `localStorage`, and removed separate YAML/JSON export actions.

Notes:
- Local-folder access currently requires File System Access API support (Chromium-based browsers such as Chrome, Edge, and Brave).
- Firefox and Safari do not currently support this local-folder workflow.
- Folder access is permission-based per session and must be re-authorized on each visit.
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,58 @@ pnpm run build:ds
pnpm run build
```

## Features

### Remote URL Import with Relative References

Studio supports importing AsyncAPI files from remote URLs with automatic resolution of relative `$ref` references:

- Import files from any URL (e.g., GitHub raw URLs, public APIs)
- The parser automatically resolves relative references using the remote URL as base path
- Example: A file at `https://example.com/specs/api.yaml` referencing `../schemas/user.json` resolves to `https://example.com/schemas/user.json`

### Local Folder Access for Reference Resolution

Studio can resolve local file references (e.g., `$ref: './schema.avsc'`) by requesting folder access:

**Workflow:**
1. Click **Import** → **Open Folder**
2. Select the root folder containing your AsyncAPI files and schemas
3. Select the main AsyncAPI file within that folder
4. The parser automatically resolves all relative file references

**Supported reference formats:**
- `./schema.avsc` - Same directory as the AsyncAPI file
- `../common/types.yaml` - Parent directory
- `apis/avro/schema.avsc` - Subdirectory path

**Supported schema formats:**
- Avro `.avsc` files
- JSON Schema `.json` files
- YAML schema `.yaml` files

**Browser compatibility:**
- ✅ Chrome, Edge, Brave (File System Access API supported)
- ❌ Firefox, Safari (not supported)

**Security note:** Folder access is granted per session only and is not persisted. You must grant access each time you open the application.

### Schema Editing

- Edit both the main AsyncAPI document and referenced schema files
- Changes to referenced schemas are automatically reflected when the parser re-validates
- Real-time validation across all files

### File Saving

- Files opened from a folder (using **Open Folder**) can be saved to their original location with the **Save** button
- For other files, the **Save** button behaves as **Save As**, allowing you to export the current editor content to a selected local file

### Additional Viewers

- **Markdown Preview**: View documentation files with full Markdown rendering, including Mermaid diagrams
- **Avro Schema Viewer**: Visualize Avro schemas with automatically generated Mermaid diagrams

## Architecture decision records

### Create a new architecture decision record
Expand All @@ -68,4 +120,4 @@ pnpm run build

### List existing architecture decision records

See [docs/adr](docs/adr)
See [doc/adr](doc/adr)
2 changes: 2 additions & 0 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"js-yaml": "^4.1.1",
"monaco-editor": "0.34.1",
"monaco-yaml": "4.0.2",
"mermaid": "^11.13.0",
"next": "14.2.35",
"postcss": "8.4.31",
"react": "18.2.0",
Expand Down Expand Up @@ -120,6 +121,7 @@
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@types/wicg-file-system-access": "^2023.10.7",
"assert": "^2.0.0",
"autoprefixer": "^10.4.13",
"browserify-zlib": "^0.2.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/components/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const Content: FunctionComponent<ContentProps> = () => { // eslint-disabl
const navigationEnabled = show.primarySidebar;
const editorEnabled = show.primaryPanel;
const viewEnabled = show.secondaryPanel;
const viewType = secondaryPanelType;
const viewType = secondaryPanelType === 'avro' ? 'template' : secondaryPanelType;

const splitPosLeft = 'splitPos:left';
const splitPosRight = 'splitPos:right';
Expand Down
6 changes: 3 additions & 3 deletions apps/studio/src/components/Editor/ConvertDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ import { useDocumentsState, useFilesState } from '../../state';
export const ConvertDropdown: React.FC = () => {
const { editorSvc } = useServices();
const isInvalidDocument = !useDocumentsState(state =>
state.documents['asyncapi'].valid
state.documents['asyncapi']?.valid
);
const language = useFilesState(state => state.files['asyncapi'].language);

return (
<Dropdown
opener={
<Tooltip content="Convert" placement="top" hideOnClick={true}>
<button className="bg-inherit">
<div className="bg-inherit">
<FaFileExport />
</button>
</div>
</Tooltip>
}
buttonHoverClassName="text-gray-500 hover:text-white"
Expand Down
78 changes: 29 additions & 49 deletions apps/studio/src/components/Editor/EditorDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
GeneratorModal,
ConvertModal,
ImportUUIDModal,
OpenFolderModal,
} from '../Modals';
import { Dropdown } from '../common';

Expand All @@ -20,9 +21,10 @@ interface EditorDropdownProps {}
export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () => {
const { editorSvc } = useServices();
const isInvalidDocument = !useDocumentsState(state => {
return state.documents['asyncapi'].valid
return state.documents['asyncapi']?.valid
});
const language = useFilesState(state => state.files['asyncapi'].language);
const file = useFilesState(state => state.files['asyncapi']);
const language = file.language;

const importUrlButton = (
<button
Expand All @@ -46,14 +48,17 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
</button>
);

const fileInputRef = React.useRef<HTMLInputElement>(null);

const importFileButton = (
<label
className="block px-4 py-1 w-full text-left text-sm rounded-md focus:outline-none transition ease-in-out duration-150 cursor-pointer"
title="Import File"
>
<input
ref={fileInputRef}
type="file"
accept='.yaml, .yml, .json'
accept='.yaml, .yml, .json, .avsc'
style={{ position: 'fixed', top: '-100em' }}
onChange={event => {
toast.promise(editorSvc.importFile(event.target.files), {
Expand All @@ -73,12 +78,25 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
</div>
),
});
// Reset so the same file can be re-imported
if (fileInputRef.current) fileInputRef.current.value = '';
}}
/>
Import File
</label>
);

const openFolderButton = (
<button
type="button"
className="px-4 py-1 w-full text-left text-sm rounded-md focus:outline-none transition ease-in-out duration-150"
title="Open Folder"
onClick={() => show(OpenFolderModal)}
>
Open Folder
</button>
);

const importBase64Button = (
<button
type="button"
Expand Down Expand Up @@ -106,12 +124,10 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
<button
type="button"
className="px-4 py-1 w-full text-left text-sm rounded-md focus:outline-none transition ease-in-out duration-150 disabled:cursor-not-allowed"
title={`Save as ${language === 'yaml' ? 'YAML' : 'JSON'}`}
title="Save"
onClick={() => {
toast.promise(
language === 'yaml'
? editorSvc.saveAsYaml()
: editorSvc.saveAsJSON(),
editorSvc.saveCurrentFile(),
{
loading: 'Saving...',
success: (
Expand All @@ -131,46 +147,9 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
},
);
}}
disabled={isInvalidDocument}
disabled={!file.modified}
>
Save as {language === 'yaml' ? 'YAML' : 'JSON'}
</button>
);

const convertLangAndSaveButton = (
<button
type="button"
className="px-4 py-1 w-full text-left text-sm rounded-md focus:outline-none transition ease-in-out duration-150 disabled:cursor-not-allowed"
title={`Convert and save as ${
language === 'yaml' ? 'JSON' : 'YAML'
}`}
onClick={() => {
toast.promise(
language === 'yaml'
? editorSvc.saveAsJSON()
: editorSvc.saveAsYaml(),
{
loading: 'Saving...',
success: (
<div>
<span className="block text-bold">
Document succesfully converted and saved!
</span>
</div>
),
error: (
<div>
<span className="block text-bold text-red-400">
Failed to convert and save document.
</span>
</div>
),
},
);
}}
disabled={isInvalidDocument}
>
Convert and save as {language === 'yaml' ? 'JSON' : 'YAML'}
Save
</button>
);

Expand Down Expand Up @@ -255,6 +234,9 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
<li className="hover:bg-gray-900">
{importUrlButton}
</li>
<li className="hover:bg-gray-900">
{openFolderButton}
</li>
<li className="hover:bg-gray-900">
{importFileButton}
</li>
Expand All @@ -279,9 +261,6 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
<li className="hover:bg-gray-900">
{saveFileButton}
</li>
<li className="hover:bg-gray-900">
{convertLangAndSaveButton}
</li>
</div>
<div>
<li className="hover:bg-gray-900">
Expand All @@ -295,3 +274,4 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
</Dropdown>
);
};

13 changes: 9 additions & 4 deletions apps/studio/src/components/Editor/EditorSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,30 @@ import { useFilesState } from '../../state';
import { ShareButton } from './ShareButton';
import { ImportDropdown } from './ImportDropdown';
import { GenerateDropdown } from './GenerateDropdown';
import { SaveDropdown } from './SaveDropdown';
import { SaveButton } from './SaveDropdown';
import { ConvertDropdown } from './ConvertDropdown';

interface EditorSidebarProps {}

export const EditorSidebar: React.FunctionComponent<
EditorSidebarProps
> = () => {
const { source, from } = useFilesState((state) => state.files['asyncapi']);
const { source, from, localPath, uri } = useFilesState((state) => state.files['asyncapi']);

let documentFromText = '';
if (from === 'storage') {
documentFromText = 'From localStorage';
} else if (from === 'file') {
const path = source || localPath || uri;
documentFromText = path ? `From local folder: ${path}` : 'From local folder';
} else if (from === 'url') {
documentFromText = `From URL ${source || uri || ''}`.trim();
} else if (from === 'base64') {
documentFromText = 'From Base64';
} else if (from === 'share') {
documentFromText = 'From Shared';
} else {
documentFromText = `From URL ${source}`;
documentFromText = source || uri ? `From ${source || uri}` : 'From unknown source';
}

return (
Expand All @@ -49,7 +54,7 @@ export const EditorSidebar: React.FunctionComponent<
<GenerateDropdown />
</li>
<li>
<SaveDropdown />
<SaveButton />
</li>
<li>
<ConvertDropdown />
Expand Down
9 changes: 5 additions & 4 deletions apps/studio/src/components/Editor/GenerateDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import { useServices } from '@/services';

export const GenerateDropdown: React.FC = () => {
const isInvalidDocument = !useDocumentsState(state =>
state.documents['asyncapi'].valid
state.documents['asyncapi']?.valid
);
const { editorSvc } = useServices();

return (
<Dropdown
opener={
<Tooltip content="Generate" placement="top" hideOnClick={true}>
<button className="bg-inherit">
<div className="bg-inherit">
<FaCode />
</button>
</div>
</Tooltip>
}
buttonHoverClassName="text-gray-500 hover:text-white"
Expand Down Expand Up @@ -65,4 +65,5 @@ export const GenerateDropdown: React.FC = () => {
</ul>
</Dropdown>
);
};
};

Loading
Loading