saving progress

but not happy with my implementation, I should switch to a table file, save, load style menu
This commit is contained in:
Luke Hagar
2024-05-28 12:16:40 -05:00
parent f035b17c36
commit e0c181872d
12 changed files with 385 additions and 213 deletions

View File

@@ -24,6 +24,7 @@
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"autoprefixer": "10.4.19",
"dexie": "^4.0.7",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",

8
pnpm-lock.yaml generated
View File

@@ -57,6 +57,9 @@ importers:
autoprefixer:
specifier: 10.4.19
version: 10.4.19(postcss@8.4.38)
dexie:
specifier: ^4.0.7
version: 4.0.7
eslint:
specifier: ^8.56.0
version: 8.57.0
@@ -852,6 +855,9 @@ packages:
devalue@5.0.0:
resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==}
dexie@4.0.7:
resolution: {integrity: sha512-M+Lo6rk4pekIfrc2T0o2tvVJwL6EAAM/B78DNfb8aaxFVoI1f8/rz5KTxuAnApkwqTSuxx7T5t0RKH7qprapGg==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -2439,6 +2445,8 @@ snapshots:
devalue@5.0.0: {}
dexie@4.0.7: {}
didyoumean@1.2.2: {}
dir-glob@3.0.1:

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { localStoragePrefix } from '$lib';
</script>
<button
class="btn variant-ghost-success"
on:click={() => {
if (
confirm(
'This operation clears all the current values, unsaved data will be lost, are you sure?'
)
) {
// remove `openApi` from localStorage
localStorage.removeItem(`${localStoragePrefix}openApi`);
}
}}
>
Create New
</button>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { db, type APISpec } from '$lib/db';
export let spec: APISpec;
</script>
<button
class="btn variant-ghost-error"
on:click={async () => {
if (confirm(`Are you sure you want to delete '${spec.name}'?`)) {
await db.apiSpecs.delete(spec.id);
}
}}
>
Delete
</button>

View File

@@ -0,0 +1,80 @@
<script lang="ts">
import { localStoragePrefix, openApiStore } from '$lib';
import filenamify from 'filenamify';
import { stringify } from 'yaml';
$: fileName = filenamify($openApiStore?.info?.title) || 'openapi';
</script>
<label class="flex flex-col text-xs">
<span>Download</span>
<button
class="btn btn-sm grow variant-ghost-tertiary mb-1"
on:click={() => {
const openApiStorage = localStorage.getItem(`${localStoragePrefix}openApi`);
if (!openApiStorage) return;
const openApi = JSON.parse(openApiStorage);
const blob = new Blob(
[stringify(openApi, null, { indent: 2, aliasDuplicateObjects: false })],
{ type: 'application/yaml' }
);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${fileName}.yaml`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}}
>
<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>
YAML
</button>
<button
class="btn btn-sm grow variant-ghost-tertiary"
on:click={() => {
const openApiStorage = localStorage.getItem(`${localStoragePrefix}openApi`);
if (!openApiStorage) return;
const openApi = JSON.parse(openApiStorage);
const blob = new Blob([JSON.stringify(openApi, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${fileName}.json`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}}
>
<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>
JSON
</button>
</label>

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import { openApiStore } from '$lib';
import { db } from '$lib/db';
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
import {
FileButton,
FileDropzone,
getModalStore,
type ModalSettings,
type ModalStore
} from '@skeletonlabs/skeleton';
import { liveQuery } from 'dexie';
import { parse } from 'yaml';
let files: FileList | undefined;
function onFileUpload(e: Event): void {
console.log('onFileUpload');
if (!files) return;
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
const isJson = file.name.endsWith('.json');
let content: OpenAPIV3_1.Document;
try {
if (isJson) {
content = JSON.parse(result);
} else {
content = parse(result);
}
openApiStore.set(content);
} catch (error) {
console.error(`Error parsing ${isJson ? 'json' : 'yaml'} file`, error);
}
};
reader.readAsText(file);
}
</script>
<FileDropzone
bind:files
label="upload"
accept=".yml,.yaml,.json"
on:dragover|once={() => {
files = undefined;
}}
on:change={onFileUpload}
type="file"
name="openapispec"
>
<svelte:fragment slot="lead">
<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.25m6.75 12-3-3m0 0-3 3m3-3v6m-1.5-15H5.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>
</svelte:fragment>
<svelte:fragment slot="message">
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
<span class="font-semibold">Click to upload</span> or drag and drop
</p>
</svelte:fragment>
<svelte:fragment slot="meta">JSON, YAML</svelte:fragment></FileDropzone
>

View File

@@ -8,6 +8,7 @@
<div class="border-token rounded-container-token space-y-1 p-4">
<div class="flex flex-row justify-between">
<h4 class="h4">License</h4>
{#if $openApiStore.info.license}
<label class="text-sm space-x-2">
<span>Pick a license</span>
<select
@@ -30,8 +31,10 @@
</optgroup>
</select>
</label>
{/if}
</div>
{#if $openApiStore.info.license}
<label class="text-sm space-y-1">
<span>Name (required)</span>
<input
@@ -62,4 +65,19 @@
bind:value={$openApiStore.info.license.url}
/>
</label>
{:else}
<button
type="button"
class="btn variant-filled-primary"
on:click={() => {
$openApiStore.info.license = {
name: '',
identifier: '',
url: ''
};
}}
>
Add License
</button>
{/if}
</div>

View File

@@ -50,6 +50,7 @@
</div>
<div class="border-token rounded-container-token bg-surface-backdrop-token space-y-1 p-4">
<h4 class="h4">Contact Information</h4>
{#if $openApiStore.info.contact}
<label class="space-y-1">
<span class="text-sm">Name (optional)</span>
<input
@@ -80,6 +81,22 @@
bind:value={$openApiStore.info.contact.url}
/>
</label>
{:else}
<button
type="button"
class="btn variant-filled-primary"
on:click={() => {
$openApiStore.info.contact = {
name: '',
email: '',
url: ''
};
}}
>
Add Contact
</button>
{/if}
</div>
<LicenseAtom />
</form>

24
src/lib/db.ts Normal file
View File

@@ -0,0 +1,24 @@
import type { OpenAPIV3_1 } from "$lib/openAPITypes";
import Dexie, { type Table } from 'dexie';
import { persisted } from "svelte-persisted-store";
export const selectedSpec = persisted<APISpec | undefined>(`selectedSpec`, undefined)
export interface APISpec {
id?: string;
name: string;
spec: OpenAPIV3_1.Document;
}
export class MySubClassedDexie extends Dexie {
apiSpecs!: Table<APISpec>;
constructor() {
super('oasDesigner');
this.version(1).stores({
apiSpecs: '++id, name, spec', // Primary key and indexed props
});
}
}
export const db = new MySubClassedDexie();

View File

@@ -16,12 +16,14 @@ export const operationCount = (openApiDoc: OpenAPIV3_1.Document) => {
export const pathCount = (openApiDoc: OpenAPIV3_1.Document) => {
let count = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const path in openApiDoc.paths) {
count++;
}
return count;
}
export const openApiStore = persisted<OpenAPIV3_1.Document>(`${localStoragePrefix}openApi`, {
openapi: '3.1.0', // OpenAPI version
info: {

View File

@@ -1,4 +1,30 @@
<div class="w-full h-full flex flex-col items-center justify-center grow">
<script lang="ts">
import CreateNewButton from '$lib/components/FileManagement/CreateNewButton.svelte';
import DeleteButton from '$lib/components/FileManagement/DeleteButton.svelte';
import Upload from '$lib/components/FileManagement/Upload.svelte';
import { db, selectedSpec } from '$lib/db';
import { liveQuery } from 'dexie';
let apiSpecs = liveQuery(() => db.apiSpecs.toArray());
$: console.log($apiSpecs);
</script>
<div class="grid place-content-center h-full gap-2 px-1">
<label class="flex flex-col text-xs">
<span>Select an API</span>
<select bind:value={$selectedSpec} class="select w-64">
{#if $apiSpecs}
{#each $apiSpecs as spec (spec.id)}
<option value={spec}>{spec.name}</option>
{/each}
{/if}
</select>
</label>
<CreateNewButton />
<Upload />
<DeleteButton />
</div>
<!-- <div class="w-full h-full flex flex-col items-center justify-center grow">
<h1 class="h1">
<span
class="bg-gradient-to-br from-blue-500 to-cyan-300 bg-clip-text text-transparent box-decoration-clone"
@@ -20,4 +46,4 @@
Deploy.
</span>
</h1>
</div>
</div> -->

View File

@@ -12,31 +12,21 @@
import { parse, stringify } from 'yaml';
import { openApiStore } from '$lib';
import filenamify from 'filenamify';
import { liveQuery } from 'dexie';
import { db, type APISpec } from '$lib/db';
import Upload from '$lib/components/FileManagement/Upload.svelte';
import { onMount } from 'svelte';
import DownloadButtons from '$lib/components/FileManagement/DownloadButtons.svelte';
let files: FileList | undefined;
let currentSpec: APISpec | undefined;
$: if (currentSpec) {
$openApiStore = currentSpec.spec;
}
let apiSpecs = liveQuery(() => db.apiSpecs.toArray());
$: console.log($apiSpecs);
$: fileName = filenamify($openApiStore.info.title) || 'openapi';
function onFileUpload(e: Event): void {
if (!files) return;
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
const isJson = file.name.endsWith('.json');
try {
if (isJson) {
openApiStore.set(JSON.parse(result));
} else {
openApiStore.set(parse(result));
}
} catch (error) {
console.error(`Error parsing ${isJson ? 'json' : 'yaml'} file`, error);
}
};
reader.readAsText(file);
}
</script>
<AppRail width="w-28" aspectRatio="aspect-[3/2]" background="variant-ghost-surface" border="ring-0">
@@ -143,113 +133,10 @@
</AppRailAnchor>
<svelte:fragment slot="trail">
<FileButton
bind:files
accept=".yml,.yaml,.json"
button="btn text-sm rounded-none text-wrap variant-soft-primary flex flex-col justify-center items-center h-20 w-full"
on:change={onFileUpload}
type="file"
name="openapispec"
>
<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.25m6.75 12-3-3m0 0-3 3m3-3v6m-1.5-15H5.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>
Upload
</FileButton>
<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"
on:click={() => {
const openApiStorage = localStorage.getItem(`${localStoragePrefix}openApi`);
if (!openApiStorage) return;
const openApi = JSON.parse(openApiStorage);
const blob = new Blob([JSON.stringify(openApi, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${fileName}.json`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}}
>
<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-soft-primary flex flex-col justify-center items-center h-20 w-full"
on:click={() => {
const openApiStorage = localStorage.getItem(`${localStoragePrefix}openApi`);
if (!openApiStorage) return;
const openApi = JSON.parse(openApiStorage);
const blob = new Blob(
[stringify(openApi, null, { indent: 2, aliasDuplicateObjects: false })],
{ type: 'application/yaml' }
);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${fileName}.yaml`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}}
>
<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 YAML
</button>
<button
type="button"
class="btn text-sm rounded-none text-wrap variant-soft-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">
<div class="p-2">
<DownloadButtons />
</div>
<div class="flex justify-center my-4">
<LightSwitch />
</div>
<AppRailAnchor href="https://www.speakeasyapi.dev/openapi" target="_blank">