feature/add route parameters (#18)

* add package manager field to package json

* Remove TODO

* design rework and path basics

* update packagemanager

* remove package manager
This commit is contained in:
Malte Teichert
2024-05-24 19:31:38 +02:00
committed by GitHub
parent 55eeb50276
commit 9dad6d9b32
19 changed files with 2821 additions and 1933 deletions

77
TODO.md
View File

@@ -1,77 +0,0 @@
# TODO
## Functionality
- [x] Add path renaming
- [x] Add path sub-page for editing a specific route
- [x] Check that a path variable can only be included once
- [ ] Add schema editor component
- [ ] Add route-parameters
- [ ] path
- [ ] query
- [ ] headers
- [ ] cookies
- [ ] Add route operations
- [ ] request template
- [ ] response template
- [ ] get
- [ ] put
- [ ] post
- [ ] delete
- [ ] options
- [ ] head
- [ ] patch
- [ ] trace
---
- [ ] Add Component Builder
- [ ] Strings
- [ ] Numbers
- [ ] Booleans
- [ ] Arrays
- [ ] Objects
- [ ] OneOf
- [ ] AllOf
- [ ] AnyOf
- [ ] Enums
- [ ] null (really easy :))
- [ ] Retrofit $ref components into the following:
- [ ] schema
- [ ] securitySchemas
- [ ] pathItems
- [ ] parameters
- [ ] requestBodies
- [ ] responses
- [ ] headers
- [ ] examples
- [ ] callbacks
- [ ] links
---
- [ ] Add Support for webhooks
---
- [ ] Add external documentation field
---
- [ ] Replace prompt, alert and confirms with skeleton-modals
- [x] Create path prompt
- [x] Edit path prompt
- [x] Rename path prompt
- [x] Delete path confirm
- [ ] Add extensions
---
- [ ] Add integration test(s) to check functionality
## UI/UX
- [x] Lightswitch
- [ ] Theme switcher
- [ ] Breadcrumbs
- [ ] Document import/export

View File

@@ -42,7 +42,11 @@
"type": "module",
"dependencies": {
"@floating-ui/dom": "1.6.5",
"@sveltejs/enhanced-img": "^0.2.0",
"openapi-types": "^12.1.3",
"svelte-persisted-store": "^0.9.2"
},
"engines": {
"node": ">20.0.0"
}
}
}

3790
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en" class="dark">
<html lang="en" class="dark h-full">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" data-theme="skeleton">
<div style="display: contents">%sveltekit.body%</div>
<body data-sveltekit-preload-data="hover" class="h-full" data-theme="skeleton">
%sveltekit.body%
</body>
</html>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import type { OpenAPIV3 } from '$lib/openAPITypes';
import { SlideToggle } from '@skeletonlabs/skeleton';
export let example: OpenAPIV3.ExampleObject;
export let name: string;
let schema = false;
</script>
<div class="ml-6 ring-1 p-4 rounded-container-token ring-surface-500">
<SlideToggle name="schema" bind:checked={schema}>Schema</SlideToggle>
<p class="-ml-2 font-bold">{name}</p>
{#if schema}
<select class="select" name="schemaSelect">
<option value="schema">Schema not yet implemented</option>
</select>
{:else}
<label>
<p>Summary</p>
<input type="text" class="input" name="summary" bind:value={example.summary} />
</label>
<label>
<p>Description</p>
<textarea class="textarea" name="description" bind:value={example.description} />
</label>
<label>
<p>Value</p>
<input type="text" class="input" name="value" bind:value={example.value} />
</label>
<label>
<p>External Value</p>
<input type="text" class="input" name="externalValue" bind:value={example.externalValue} />
</label>
{/if}
</div>

View File

@@ -0,0 +1,115 @@
<script lang="ts">
import type { OpenAPIV3 } from '$lib/openAPITypes';
import { SlideToggle } from '@skeletonlabs/skeleton';
import ExampleInput from '$lib/components/atoms/ExampleInput.svelte';
export let variableName: string;
export let value: OpenAPIV3.ParameterObject;
export let location: 'path' | 'query' | 'header' | 'cookie';
value.name = variableName;
value.in = location;
if (location === 'path') value.required = true;
let multipleExamples = value.examples && Object.keys(value.examples).length > 1;
const addParameterExample = () => {
const exampleName = prompt('Enter a name for the example');
if (!exampleName) return;
// @ts-expect-error - working we are setting a new key
value.examples[exampleName] = {
$ref: '',
summary: '',
description: '',
value: '',
externalValue: ''
};
};
</script>
<div class="card py-6 px-4 flex flex-col gap-4">
<h4 class="h4">&lbrace;{variableName}&rbrace;</h4>
<span class="flex items-center gap-2">
<p>Location:</p>
<select
name="location"
class="select w-min"
disabled={location === 'path'}
bind:value={location}
>
<option value="path">Path</option>
<option value="query">Query</option>
<option value="header">Header</option>
<option value="cookie">Cookie</option>
</select>
</span>
<label class="space-y-2">
<p>Description</p>
<textarea
class="textarea"
bind:value={value.description}
placeholder="Description of the parameter. Supports markdown."
/>
</label>
<SlideToggle name="required" disabled={location === 'path'} bind:checked={value.required}>
required
</SlideToggle>
<SlideToggle name="deprecated" bind:checked={value.deprecated} disabled={location === 'path'}>
deprecated
</SlideToggle>
<SlideToggle name="allowEmptyValue" bind:value={value.allowEmptyValue}>
Allow Empty Value
</SlideToggle>
<label class="space-y-2">
<p>Style</p>
<select name="style" class="select" bind:value={value.style}>
<option value={undefined} selected>none</option>
<option value="matrix">Matrix</option>
<option value="label">Label</option>
<option value="form">Form</option>
<option value="simple">Simple</option>
<option value="spaceDelimited">Space Delimited</option>
<option value="pipeDelimited">Pipe Delimited</option>
<option value="deepObject">Deep Object</option>
</select>
</label>
<SlideToggle name="explode" bind:checked={value.explode}>Explode</SlideToggle>
<SlideToggle name="allowReserved" bind:checked={value.allowReserved}>Allow Reserved</SlideToggle>
<label class="space-y-2">
<p>Schema</p>
<!-- Subject to change -->
<select class="select" name="schema" bind:value={value.schema}>
<option value="schema">Schema not yet implemented</option>
</select>
</label>
<SlideToggle
name="multiExample"
bind:checked={multipleExamples}
on:click={() => {
if (!value.examples) value.examples = {};
}}
>
Multiple Examples?
</SlideToggle>
{#if multipleExamples}
<div class="space-y-2">
<p>Examples</p>
{#each Object.keys(value.examples) as example}
<ExampleInput bind:example={value.examples[example]} name={example} />
{/each}
<button
type="button"
class="btn btn-sm variant-filled-primary"
on:click={addParameterExample}
>
Add Example
</button>
</div>
{:else}
<label class="space-y-2">
<p>Example</p>
<input type="text" class="input" name="example" placeholder="An example of the value." />
</label>
{/if}
</div>

View File

@@ -2,15 +2,15 @@
import { InputChip } from '@skeletonlabs/skeleton';
import Info from '../icons/Info.svelte';
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
import { pathVariables } from '$lib';
export let id: number;
export let server: OpenAPIV3_1.ServerObject;
const variableRegex = /\{([^}]+)\}/gm;
const addVariables = () => {
if (!server.url) return;
const matches = server.url.match(variableRegex);
const matches = server.url.match(pathVariables);
if (!matches) {
server.variables = undefined;
@@ -73,9 +73,9 @@
<div>
<h4 class="h4">Variables</h4>
<p class="text-sm">
Define variables by adding them to the server URL using curly-braces like so: <code
>&lbrace;id&rbrace;</code
>.
Define variables by adding them to the server URL using curly-braces like so: <code>
&lbrace;id&rbrace;
</code>.
</p>
</div>
{#if server.variables}

View File

@@ -1,59 +1,41 @@
<script lang="ts">
import { addPath } from '$lib';
import { openApiStore, pathRegex, sortPathsAlphabetically } from '$lib';
import { pathTemplate } from '$lib/pathTemplate';
import { openApiStore, sortPathsAlphabetically } from '$lib';
import type { OpenAPIV3 } from '$lib/openAPITypes';
import PathListItem from '../atoms/PathListItem.svelte';
import PathButtons from '../atoms/PathButtons.svelte';
import { getModalStore } from '@skeletonlabs/skeleton';
let paths: OpenAPIV3.PathsObject = {};
// @ts-expect-error - working with a potentially empty object
openApiStore.subscribe((store) => (paths = store.paths));
const modalStore = getModalStore();
// match path with parameters
// add path
// const addPath = () => {
// // prompt user to enter path
// const path = prompt(
// 'Enter path. Wrap path parameters in curly braces. Example: /users/{userId}'
// );
// if (!path) return;
// // check if path is valid
// if (!pathRegex.test(path)) {
// alert('Invalid path');
// return;
// }
// // check if path already exists
// // @ts-expect-error - we are working with an initially empty object
// if ($openApiStore.paths[path]) {
// alert('Path already exists');
// return;
// }
// // create a temporary object to store paths
// // add path to paths object
// $openApiStore.paths = {
// ...$openApiStore.paths,
// [path]: pathTemplate
// };
// // sort paths alphabetically
// sortPathsAlphabetically();
// };
</script>
<div
class="container mx-auto border-token rounded-container-token bg-surface-backdrop-token px-6 py-4 min-h-20 space-y-3"
class="container mx-auto border-token rounded-container-token bg-surface-backdrop-token px-6 py-4 space-y-4"
>
{#if Object.keys($openApiStore.paths).length > 0}
<PathButtons />
<hr />
{#each Object.keys($openApiStore.paths) as pathName, index}
<PathListItem {pathName} id={index} />
{/each}
<PathButtons />
{:else}
<PathButtons justify="justify-center" sort={false} />
{/if}
{#each Object.keys(paths) as pathName, index}
<PathListItem {pathName} id={index} />
{/each}
<span class="w-full flex justify-center">
<button
type="button"
class="btn variant-filled-primary"
on:click={() => {
addPath(modalStore);
}}
>
Add Path
</button>
</span>
<span class="w-full flex justify-center">
<button
type="button"
class="btn btn-sm variant-filled-secondary"
on:click={sortPathsAlphabetically}
>
Sort paths
</button>
</span>
</div>

View File

@@ -39,5 +39,16 @@ export const openApiStore = persisted<OpenAPIV3_1.Document>(`${localStoragePrefi
}
});
export enum HttpMethods {
GET = 'get',
PUT = 'put',
POST = 'post',
DELETE = 'delete',
OPTIONS = 'options',
HEAD = 'head',
PATCH = 'patch',
TRACE = 'trace'
}
// export path handling functions
export * from './pathHandling';

View File

@@ -2,6 +2,9 @@ import { get } from 'svelte/store';
import type { ModalSettings, ModalStore } from '@skeletonlabs/skeleton';
import { pathTemplate } from './pathTemplate';
import { openApiStore } from '$lib';
import type { OpenAPIV3 } from './openAPITypes';
export const pathVariables = /\{([^}]+)\}/gm;
export const pathRegex = /\/([/]*[{]?[a-zA-Z]+[}]?)*/gm;
@@ -39,8 +42,6 @@ export const addPath = (modalStore: ModalStore, startingPoint: string = '/') =>
const store = get(openApiStore);
if (!store.paths) store.paths = {};
store.paths[userPath] = pathTemplate;
// add path to store
openApiStore.set(store);
// sort paths alphabetically
sortPathsAlphabetically();
@@ -136,11 +137,12 @@ export const isValidPath = (path: string) => {
}
// check if path is a valid path
const pathWithoutVariables = path.replace('{', '').replace('}', '');
const pathWithoutVariables = path.replaceAll('{', '').replaceAll('}', '');
console.log(pathWithoutVariables);
const pathRegex = /(\/[a-zA-Z-]+)+|\//gm;
const pathParts = pathWithoutVariables.match(pathRegex);
// the fallback is to return false if the pathParts array is null
return pathParts?.length === 1 ? true : false;
return pathParts?.length === 1;
};
/// sorts the paths in the OpenAPI document alphabetically
@@ -161,3 +163,20 @@ export const sortPathsAlphabetically = () => {
return data;
});
};
export const getPathVariables = (path: string) => {
const variables = path.match(pathVariables);
if (!variables) return [];
return variables.map((variable) => variable.replace('{', '').replace('}', ''));
};
export const sortPathParameters = (parameters: OpenAPIV3.ParameterObject[]) => {
const tempParameters = parameters;
tempParameters.sort((a, b) => {
if (a.in < b.in) return -1;
if (a.in > b.in) return 1;
return 0;
});
return tempParameters;
};

View File

@@ -2,10 +2,10 @@ import type { OpenAPIV3_1 } from './openAPITypes';
export const pathTemplate: OpenAPIV3_1.PathItemObject = {
$ref: undefined,
summary: undefined,
description: undefined,
servers: undefined,
parameters: undefined,
summary: "",
description: "",
servers: [],
parameters: [],
get: undefined,
put: undefined,
post: undefined,

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import FancyAppRail from './FancyAppRail.svelte';
import '../app.postcss';
import { AppBar } from '@skeletonlabs/skeleton';
import { AppBar, AppShell } from '@skeletonlabs/skeleton';
// Floating UI for Popups
import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom';
@@ -15,72 +17,9 @@
<Modal />
<div class="w-full h-full">
<AppBar padding="p-2">
<svelte:fragment slot="lead">
<h1>
OpenAPI generator
<code class="text-xs ml-2 variant-filled-success p-1 px-2 rounded-container-token">
3.1.0
</code>
</h1>
</svelte:fragment>
<svelte:fragment slot="trail">
<LightSwitch />
<a
href="https://www.speakeasyapi.dev/openapi"
target="_blank"
class="btn variant-filled-surface btn-sm hover:variant-filled-primary"
>
Schema Reference
</a>
<button
type="button"
class="btn variant-ringed-error btn-sm hover:variant-filled-error"
on:click={() => {
if (confirm('Are you sure you want to reset ALL current inputs?')) {
// remove `openApi` from localStorage
localStorage.removeItem(`${localStoragePrefix}openApi`);
window.location.reload();
}
}}
>
Clear
</button>
</svelte:fragment>
</AppBar>
<div class="flex flex-row w-full">
<nav class="list-nav">
<!-- (optionally you can provide a label here) -->
<ul>
<li class="">
<a href="/info">
<!-- <span class="badge bg-primary-500">(icon)</span> -->
<span class="flex-auto">Info</span>
</a>
</li>
<li class="">
<a href="/servers">
<!-- <span class="badge bg-primary-500">(icon)</span> -->
<span class="flex-auto">Servers</span>
</a>
</li>
<li class="">
<a href="/authentication">
<!-- <span class="badge bg-primary-500">(icon)</span> -->
<span class="flex-auto">Security</span>
</a>
</li>
<li class="">
<a href="/paths">
<!-- <span class="badge bg-primary-500">(icon)</span> -->
<span class="flex-auto">Paths</span>
</a>
</li>
</ul>
</nav>
<div class="mx-8 my-4 grow">
<slot />
</div>
</div>
</div>
<AppShell slotPageContent="px-6 py-4">
<svelte:fragment slot="sidebarLeft">
<FancyAppRail />
</svelte:fragment>
<slot />
</AppShell>

View File

@@ -1,42 +1,26 @@
<script lang="ts">
import Servers from '$lib/components/sections/Servers.svelte';
import Info from '$lib/components/sections/Info.svelte';
import { TabGroup, Tab } from '@skeletonlabs/skeleton';
import { persisted } from 'svelte-persisted-store';
import Authentication from '$lib/components/sections/Authentication.svelte';
import Paths from '$lib/components/sections/Paths.svelte';
// let tabSet: number = 0;
const tabSet = persisted<number>('tabSet', 0);
</script>
<TabGroup justify="justify-center">
<Tab bind:group={$tabSet} name="info" value={0}>
<h4 class="h4">Info</h4>
</Tab>
<Tab bind:group={$tabSet} name="authentication" value={1}>
<h4 class="h4">Authentication Schemas</h4>
</Tab>
<Tab bind:group={$tabSet} name="servers" value={2}>
<h4 class="h4">Servers</h4>
</Tab>
<Tab bind:group={$tabSet} name="paths" value={3}>
<h4 class="h4">Paths</h4>
</Tab>
<Tab bind:group={$tabSet} name="components" value={4}>
<h4 class="h4">Components</h4>
</Tab>
<svelte:fragment slot="panel">
{#if $tabSet === 0}
<Info />
{:else if $tabSet === 1}
<Authentication />
{:else if $tabSet === 2}
<Servers />
{:else if $tabSet === 3}
<Paths />
{:else if $tabSet === 4}
<p>Components</p>
{/if}
</svelte:fragment>
</TabGroup>
<div class="w-full h-full flex flex-col items-center justify-center">
<h1 class="h1">
<span
class="bg-gradient-to-br from-blue-500 to-cyan-300 bg-clip-text text-transparent box-decoration-clone"
>
Design.
</span>
</h1>
<h1 class="h1">
<span
class="bg-gradient-to-br from-red-500 to-yellow-500 bg-clip-text text-transparent box-decoration-clone"
>
Build.
</span>
</h1>
<h1 class="h1">
<span
class="bg-gradient-to-br from-pink-500 to-violet-500 bg-clip-text text-transparent box-decoration-clone"
>
Deploy.
</span>
</h1>
</div>

View File

@@ -0,0 +1,144 @@
<script lang="ts">
import { AppRail, AppRailAnchor, AppRailTile, LightSwitch } from '@skeletonlabs/skeleton';
import { page } from '$app/stores';
import { localStoragePrefix } from '$lib';
import { goto } from '$app/navigation';
</script>
<AppRail width="w-28" aspectRatio="aspect-[3/2]" background="variant-ghost-surface" border="ring-0">
<svelte:fragment slot="lead">
<div class="my-4">
<AppRailAnchor href="/">
<p class="font-bold mb-1">OpenAPI</p>
<p class=" mb-2">Generator</p>
<code class="text-xs ml-2 variant-filled-success p-1 px-2 rounded-container-token">
3.1.0
</code>
</AppRailAnchor>
</div>
<hr />
</svelte:fragment>
<AppRailAnchor href="/info" selected={$page.url.pathname === '/info'}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mx-auto"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
Info
</AppRailAnchor>
<AppRailAnchor href="/servers" selected={$page.url.pathname === '/servers'}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mx-auto"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"
/>
</svg>
Servers
</AppRailAnchor>
<AppRailAnchor href="/authentication" selected={$page.url.pathname === '/authentication'}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mx-auto"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
/>
</svg>
Authentication
</AppRailAnchor>
<AppRailAnchor href="/paths" selected={$page.url.pathname === '/paths'}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mx-auto"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
/>
</svg>
Paths
</AppRailAnchor>
<svelte:fragment slot="trail">
<button
type="button"
class="btn text-sm rounded-none text-wrap variant-soft-primary flex flex-col justify-center items-center h-20 w-full"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
/>
</svg>
Download JSON
</button>
<button
type="button"
class="btn text-sm rounded-none text-wrap variant-ringed-error hover:variant-soft-error flex justify-center items-center h-16 w-full"
on:click={() => {
if (confirm('Are you sure you want to reset ALL current inputs?')) {
// remove `openApi` from localStorage
localStorage.removeItem(`${localStoragePrefix}openApi`);
window.location.pathname = '/';
}
}}
>
Clear all inputs
</button>
<div class="flex justify-center items-center h-10 w-full my-4">
<LightSwitch />
</div>
<AppRailAnchor href="https://www.speakeasyapi.dev/openapi" target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mx-auto"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
/>
</svg>
API Reference
</AppRailAnchor>
</svelte:fragment>
</AppRail>

View File

@@ -1 +1,17 @@
Route does not exist.
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="w-full h-full flex justify-center items-center">
<div class="card max-w-lg">
<header class="card-header">
<enhanced:img src="./error.jpg" alt="stormy, grayscale seaside" />
</header>
<section class="p-4">
<h3 class="h3">An unfortunate error occured.</h3>
</section>
<footer class="card-footer">
{$page.status} - {$page.error?.message}
</footer>
</div>
</div>

View File

@@ -1,107 +1,160 @@
<script lang="ts">
import ServerInput from '$lib/components/atoms/ServerInput.svelte';
import { openApiStore } from '$lib';
import { SlideToggle } from '@skeletonlabs/skeleton';
import { pathTemplate } from '$lib/pathTemplate';
import { HttpMethods, openApiStore } from '$lib';
import ParameterInput from '$lib/components/atoms/ParameterInput.svelte';
import { getPathVariables } from '$lib/pathHandling';
import type { OpenAPIV3 } from '$lib/openAPITypes';
import type { PageData } from './$types';
import { Accordion, AccordionItem } from '@skeletonlabs/skeleton';
export let data: PageData;
// create local path object to work with
const localPathObject = (() => {
// @ts-expect-error - damn those stolen types for their inaccuracy
return $openApiStore.paths[data.pathName] ?? pathTemplate;
})();
let customServersEnabled: boolean = false;
if (localPathObject.servers?.length > 0) {
customServersEnabled = true;
}
const addCustomServer = () => {
if (!Array.isArray(localPathObject.servers)) {
localPathObject.servers = [];
}
localPathObject.servers.push({ url: '', description: '' });
// yay svelte reactivity
localPathObject.servers = localPathObject.servers;
let newParam: 'query' | 'header' | 'cookie' = 'query';
let tempPath: OpenAPIV3.PathItemObject = {
parameters: []
};
openApiStore.subscribe((store) => {
if (!data.pathName) return;
if (store.paths == undefined) tempPath = {};
if (!store.paths!.hasOwnProperty(data.pathName)) tempPath = {};
// @ts-expect-error - working with a known not empty object
tempPath = store.paths[data.pathName] ?? {};
const removeCustomServer = (index: number) => {
if (!Array.isArray(localPathObject.servers)) {
return;
}
localPathObject.servers.splice(index, 1);
// yay svelte reactivity
localPathObject.servers = localPathObject.servers;
};
if (!tempPath.hasOwnProperty('parameters')) tempPath.parameters = [];
});
const pathVariables = getPathVariables(data.pathName ?? '');
pathVariables.forEach((variable) => {
// push path variables to the parameters array
// @ts-expect-error - working with a array thats loosely defined
tempPath.parameters.push({
name: variable,
in: 'path',
required: true
});
});
</script>
<div class="border-token rounded-container-token bg-surface-backdrop-token p-4 space-y-4">
<h1>{data.pathName}</h1>
<label class="space-y-1">
<h5 class="h5">Summary</h5>
<p class="text-sm">A short summary of what the path item represents.</p>
<input type="text" class="input" placeholder="Summary" bind:value={localPathObject.summary} />
</label>
<label class="space-y-1">
<h5 class="h5">Description</h5>
<p class="text-sm">A description of the path item. Supports Markdown.</p>
<textarea class="textarea" placeholder="Description" bind:value={localPathObject.description} />
</label>
<div>
<span class="flex flex-col gap-4">
<h5 class="h5">Servers</h5>
<SlideToggle name="slide" size="sm" bind:checked={customServersEnabled} />
{#if customServersEnabled && Array.isArray(localPathObject.servers)}
<ul class="list space-y-6">
{#each localPathObject.servers as server, index}
<li class="!block">
<span class="flex w-full justify-end">
<button
type="button"
class="btn btn-sm variant-ringed-error hover:variant-filled-error"
on:click={() => removeCustomServer(index)}
>
Remove Server
</button>
</span>
<ServerInput id={1} bind:server />
</li>
{#if index < localPathObject.servers.length - 1}
<hr />
{/if}
{/each}
</ul>
{/if}
{#if customServersEnabled}
<span class="flex w-full justify-center">
<button type="button" class="btn variant-filled-primary" on:click={addCustomServer}>
Add Server
<div
class="border-token border-surface-500 space-y-4 px-6 py-4 rounded-container-token variant-glass-surface"
>
<h3 class="h3">
{data.pathName}
</h3>
<hr />
<Accordion>
<AccordionItem>
<svelte:fragment slot="summary">
<h4 class="h4">General</h4>
</svelte:fragment>
<svelte:fragment slot="content">
<label class="space-y-2">
<p>Summary</p>
<input
type="text"
class="input"
bind:value={tempPath.summary}
placeholder="Summary of the path"
/>
</label>
<label class="space-y-2">
<p>Description</p>
<textarea
class="textarea"
bind:value={tempPath.description}
placeholder="Description of the path. Supports markdown."
/>
</label>
</svelte:fragment>
</AccordionItem>
<AccordionItem>
<svelte:fragment slot="summary">
<h4 class="h4">Custom Servers</h4>
</svelte:fragment>
<svelte:fragment slot="content">
<p>Here you can add custom servers for this specific call.</p>
{#each tempPath.servers as server, index}
<label class="space-y-2">
<p>Server {index + 1}</p>
<input
type="text"
class="input"
bind:value={server.url}
placeholder="URL of the server"
/>
</label>
{/each}
<button type="button" class="btn variant-filled-primary"> Add Server </button>
</svelte:fragment>
</AccordionItem>
<AccordionItem>
<svelte:fragment slot="summary">
<h4 class="h4">Parameters</h4>
</svelte:fragment>
<svelte:fragment slot="content">
{#each tempPath.parameters as param}
<ParameterInput variableName={param.name} bind:value={param} location="path" />
{/each}
<span class="flex items-center gap-2">
<select name="newParameter" bind:value={newParam} class="select w-min">
<option value="query">Query</option>
<option value="header">Header</option>
<option value="cookie">Cookie</option>
</select>
<button type="button" class="btn variant-filled-primary">
Add {newParam} Parameter
</button>
</span>
{/if}
</span>
</div>
<div>
<h5 class="h5">Parameters</h5>
<div class="ml-2">
<div>
<h6 class="h6">Path Parameters</h6>
</div>
<div>
<h6 class="h6">Query Parameters</h6>
</div>
<div>
<h6 class="h6">Header Parameters</h6>
</div>
<div>
<h6 class="h6">Cookie Parameters</h6>
</div>
</div>
</div>
<div>
<h5 class="h5">Operations</h5>
</div>
</svelte:fragment>
</AccordionItem>
<AccordionItem>
<svelte:fragment slot="summary">
<h4 class="h4">Operations</h4>
</svelte:fragment>
<svelte:fragment slot="content">
<p>
Here you can add operations for this path. Select only the operations you want to support
</p>
<div class="flex gap-4">
{#each Object.values(HttpMethods) as method}
<label class="flex items-center gap-2">
<input
type="checkbox"
class="checkbox"
on:input={(event) => {
if (event.target.checked) {
tempPath[method] = {
tags: [],
summary: '',
description: '',
externalDocs: {
description: '',
url: ''
},
operationId: '',
parameters: [],
requestBody: {
content: {},
description: '',
required: false
},
responses: {},
callbacks: {},
deprecated: false,
security: [],
servers: []
};
}
}}
/>
{method.toUpperCase()}
</label>
{/each}
</div>
</svelte:fragment>
</AccordionItem>
</Accordion>
</div>

View File

@@ -1,27 +1,30 @@
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { openApiStore } from '$lib';
import { get } from 'svelte/store';
import { error } from '@sveltejs/kit';
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
export const load = (async (event) => {
const index = Number(event.params.index);
if (isNaN(index)) {
console.error('Index is not a number', index);
error(404, 'Not found');
}
const paths = get(openApiStore).paths;
if (!paths) {
console.error('No paths found in OpenAPI document');
error(404, 'Not found');
}
// check if path param is an integer
if (!/^\d+$/.test(event.params.index)) error(404, 'Invalid path index');
const index = parseInt(event.params.index);
let apiObject: OpenAPIV3_1.Document;
openApiStore.subscribe((value) => (apiObject = value));
let pathName: string | undefined;
// @ts-expect-error - svelte stores populate the value
if (apiObject && !apiObject.paths) error(404, 'No paths found in the OpenAPI document');
const pathObjectName = Object.keys(paths)[index];
if (!pathObjectName) {
console.error('No path found in OpenAPI document');
error(404, 'Not found');
}
const path = (() => {
// @ts-expect-error - svelte stores populate the value
const paths = Object.keys(apiObject.paths) || [];
if (paths.length === 0) return;
if (paths.length <= index) return;
pathName = paths[index];
// @ts-expect-error - svelte stores populate the value
if (!apiObject || !apiObject.paths) return;
return apiObject.paths[paths[index]];
})();
return {
pathName: pathObjectName
path,
pathName
};
}) satisfies PageLoad;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,7 +1,8 @@
import { purgeCss } from 'vite-plugin-tailwind-purgecss';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { enhancedImages } from '@sveltejs/enhanced-img';
export default defineConfig({
plugins: [sveltekit(), purgeCss()]
plugins: [enhancedImages(), sveltekit(), purgeCss()]
});