Browse Source

Complete Try It Out implementation

shalvah 2 years ago
parent
commit
c1eaafd28b

+ 5 - 5
resources/js/tryitout.js

@@ -81,10 +81,10 @@ function cancelTryOut(endpointId) {
     document.querySelector('#example-responses-' + endpointId).hidden = false;
 }
 
-function makeAPICall(method, path, body, query, headers, endpointId) {
+function makeAPICall(method, path, body = {}, query = {}, headers = {}, endpointId = null) {
     console.log({endpointId, path, body, query, headers});
 
-    if (!(body instanceof FormData)) {
+    if (!(body instanceof FormData) && typeof body !== "string") {
         body = JSON.stringify(body)
     }
 
@@ -122,7 +122,7 @@ function makeAPICall(method, path, body, query, headers, endpointId) {
         mode: 'cors',
         credentials: 'same-origin',
     })
-        .then(response => Promise.all([response.status, response.text(), response.headers]));
+        .then(response => Promise.all([response.status, response.statusText, response.text(), response.headers]));
 }
 
 function hideCodeSamples(endpointId) {
@@ -251,13 +251,13 @@ async function executeTryOut(endpointId, form) {
 
     let preflightPromise = Promise.resolve();
     if (window.useCsrf && window.csrfUrl) {
-        preflightPromise = makeAPICall('GET', window.csrfUrl, {}, {}, {}, null).then(() => {
+        preflightPromise = makeAPICall('GET', window.csrfUrl).then(() => {
             headers['X-XSRF-TOKEN'] = getCookie('XSRF-TOKEN');
         });
     }
 
     return preflightPromise.then(() => makeAPICall(method, path, body, query, headers, endpointId))
-        .then(([responseStatus, responseContent, responseHeaders]) => {
+        .then(([responseStatus, statusText, responseContent, responseHeaders]) => {
             handleResponse(endpointId, responseContent, responseStatus, responseHeaders)
         })
         .catch(err => {

+ 2 - 1
resources/views/themes/default/endpoint.blade.php

@@ -74,7 +74,8 @@
         @if($metadata['try_it_out']['enabled'] ?? false)
             <button type="button"
                     style="background-color: #8fbcd4; padding: 5px 10px; border-radius: 5px; border-width: thin;"
-                    id="btn-tryout-{{ $endpoint->endpointId() }}"
+                    data-endpoint="{{ $endpoint->endpointId() }}"
+                    class="tryItOut-btn"
                     onclick="tryItOut('{{ $endpoint->endpointId() }}');">Try it out ⚡
             </button>
             <button type="button"

+ 10 - 257
resources/views/themes/elements/endpoint.blade.php

@@ -145,259 +145,12 @@
             </div>
         </div>
 
-        @if($metadata['try_it_out']['enabled'] ?? false)
-            <div data-testid="two-column-right" class="sl-relative sl-w-2/5 sl-ml-16" style="max-width: 500px;">
-                <div class="sl-stack sl-stack--vertical sl-stack--6 sl-flex sl-flex-col sl-items-stretch">
-                    <div class="sl-inverted">
-                        <div class="sl-overflow-y-hidden sl-rounded-lg">
-                            <div class="TryItPanel sl-bg-canvas-100 sl-rounded-lg">
+        <div data-testid="two-column-right" class="sl-relative sl-w-2/5 sl-ml-16" style="max-width: 500px;">
+            <div class="sl-stack sl-stack--vertical sl-stack--6 sl-flex sl-flex-col sl-items-stretch">
 
-                                @if($endpoint->isAuthed())
-                                    <div class="sl-panel sl-outline-none sl-w-full expandable">
-                                        <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
-                                             role="button">
-                                            <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
-                                                <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
-                                                    <svg aria-hidden="true" focusable="false" data-prefix="fas"
-                                                         data-icon="caret-down"
-                                                         class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
-                                                         xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
-                                                        <path fill="currentColor"
-                                                              d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
-                                                    </svg>
-                                                </div>
-                                                Auth
-                                            </div>
-                                        </div>
-                                        <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
-                                            <div class="ParameterGrid sl-p-4">
-                                                <label aria-hidden="true" for="auth-{{ $endpoint->endpointId() }}">{{ $metadata['auth']['name'] }}</label>
-                                                <span class="sl-mx-3">:</span>
-                                                <div class="sl-flex sl-flex-1">
-                                                    <div class="sl-input sl-flex-1 sl-relative">
-                                                        <code>{{ $metadata['auth']['prefix'] }}</code>
-                                                        <input aria-label="{{ $metadata['auth']['name'] }}" id="auth-{{ $endpoint->endpointId() }}"
-                                                               data-component="{{ $metadata['auth']['location'] }}"
-                                                               data-prefix="{{ $metadata['auth']['prefix'] }}"
-                                                               name="{{ $metadata['auth']['name'] }}"
-                                                               placeholder="{{ $metadata['auth']['placeholder'] }}"
-                                                               class="auth-value sl-relative {{ $metadata['auth']['prefix'] ? 'sl-w-3/5' : 'sl-w-full sl-pr-2.5 sl-pl-2.5' }} sl-h-md sl-text-base sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border">
-                                                    </div>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                @endif
-
-
-                                @if(count($endpoint->headers))
-                                    <div class="sl-panel sl-outline-none sl-w-full expandable">
-                                        <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
-                                             role="button">
-                                            <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
-                                                <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
-                                                    <svg aria-hidden="true" focusable="false" data-prefix="fas"
-                                                         data-icon="caret-down"
-                                                         class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
-                                                         xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
-                                                        <path fill="currentColor"
-                                                              d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
-                                                    </svg>
-                                                </div>
-                                                Headers
-                                            </div>
-                                        </div>
-                                        <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
-                                            <div class="ParameterGrid sl-p-4">
-                                                @foreach($endpoint->headers as $name => $example)
-                                                    @php
-                                                    if($endpoint->isAuthed() && $metadata['auth']['location'] === 'header' && $name === $metadata['auth']['name']) continue;
-                                                    @endphp
-                                                <label aria-hidden="true" for="header-{{ $endpoint->endpointId() }}-{{ $name }}">{{ $name }}</label>
-                                                <span class="sl-mx-3">:</span>
-                                                <div class="sl-flex sl-flex-1">
-                                                    <div class="sl-input sl-flex-1 sl-relative">
-                                                        <input aria-label="{{ $name }}" id="header-{{ $endpoint->endpointId() }}-{{ $name }}"
-                                                                value="{{ $example }}"
-                                                                class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border">
-                                                    </div>
-                                                </div>
-                                                @endforeach
-                                            </div>
-                                        </div>
-                                    </div>
-                                @endif
-
-                                @if(count($endpoint->urlParameters))
-                                    <div class="sl-panel sl-outline-none sl-w-full expandable">
-                                        <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
-                                             role="button">
-                                            <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
-                                                <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
-                                                    <svg aria-hidden="true" focusable="false" data-prefix="fas"
-                                                         data-icon="caret-down"
-                                                         class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
-                                                         xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
-                                                        <path fill="currentColor"
-                                                              d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
-                                                    </svg>
-                                                </div>
-                                                URL Parameters
-                                            </div>
-                                        </div>
-                                        <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
-                                            <div class="ParameterGrid sl-p-4">
-                                                @foreach($endpoint->urlParameters as $name => $parameter)
-                                                <label aria-hidden="true" for="urlparam-{{ $endpoint->endpointId() }}-{{ $name }}">{{ $name }}</label>
-                                                <span class="sl-mx-3">:</span>
-                                                <div class="sl-flex sl-flex-1">
-                                                    <div class="sl-input sl-flex-1 sl-relative">
-                                                        <input aria-label="{{ $name }}" id="urlparam-{{ $endpoint->endpointId() }}-{{ $name }}"
-                                                                placeholder="{{ $parameter->description }}" value="{{ $parameter->example }}"
-                                                                class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border">
-                                                    </div>
-                                                </div>
-                                                @endforeach
-                                            </div>
-                                        </div>
-                                    </div>
-                                @endif
-
-
-                                @if(count($endpoint->queryParameters))
-                                    <div class="sl-panel sl-outline-none sl-w-full expandable">
-                                        <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
-                                             role="button">
-                                            <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
-                                                <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
-                                                    <svg aria-hidden="true" focusable="false" data-prefix="fas"
-                                                         data-icon="caret-down"
-                                                         class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
-                                                         xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
-                                                        <path fill="currentColor"
-                                                              d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
-                                                    </svg>
-                                                </div>
-                                                Query Parameters
-                                            </div>
-                                        </div>
-                                        <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
-                                            <div class="ParameterGrid sl-p-4">
-                                                @foreach($endpoint->queryParameters as $name => $parameter)
-                                                    @php
-                                                        /** @var \Knuckles\Camel\Output\Parameter $parameter */
-                                                        if ($parameter->type == 'object') // Skip; individual object children are listed
-                                                            continue;
-                                                        if (str_contains($name, "[]"))
-                                                            // This likely belongs to an obj-array (eg objs[].a); we only show the parent (objs[]), so skip
-                                                            continue;
-                                                    @endphp
-                                                <label aria-hidden="true" for="queryparam-{{ $endpoint->endpointId() }}-{{ $name }}">{{ $name }}</label>
-                                                <span class="sl-mx-3">:</span>
-                                                <div class="sl-flex sl-flex-1">
-                                                    <div class="sl-input sl-flex-1 sl-relative">
-                                                        @if(str_ends_with($parameter->type, '[]'))
-                                                            <input aria-label="{{ $name }}" id="queryparam-{{ $endpoint->endpointId() }}-{{ $name }}"
-                                                                   placeholder="{{ $parameter->description }}"
-                                                                   value="{{ json_encode($parameter->example) }}"
-                                                                   class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
-                                                            >
-                                                        @else
-                                                        <input aria-label="{{ $name }}" id="queryparam-{{ $endpoint->endpointId() }}-{{ $name }}"
-                                                                placeholder="{{ $parameter->description }}" value="{{ $parameter->example }}"
-                                                                class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
-                                                        >
-                                                        @endif
-                                                    </div>
-                                                </div>
-                                                @endforeach
-                                            </div>
-                                        </div>
-                                    </div>
-                                @endif
-
-
-                                @if(count($endpoint->bodyParameters))
-                                    <div class="sl-panel sl-outline-none sl-w-full expandable">
-                                        <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
-                                             role="button">
-                                            <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
-                                                <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
-                                                    <svg aria-hidden="true" focusable="false" data-prefix="fas"
-                                                         data-icon="caret-down"
-                                                         class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
-                                                         xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
-                                                        <path fill="currentColor"
-                                                              d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
-                                                    </svg>
-                                                </div>
-                                                Body
-                                            </div>
-                                        </div>
-                                        <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
-                                                @if($endpoint->hasJsonBody())
-                                                    <div class="TextRequestBody sl-p-4">
-                                                        <div class="code-editor language-json" id="json-body-{{ $endpoint->endpointId() }}"
-                                                             style="font-family: var(--font-code); font-size: 12px; line-height: var(--lh-code);"
-                                                        >{!! json_encode($endpoint->getSampleBody(), JSON_PRETTY_PRINT) !!}</div>
-                                                    </div>
-                                                    @else
-                                                <div class="ParameterGrid sl-p-4">
-                                                @foreach($endpoint->bodyParameters as $name => $parameter)
-                                                    @php
-                                                    /** @var \Knuckles\Camel\Output\Parameter $parameter */
-                                                    if ($parameter->type == 'object') // Skip; individual object children are listed
-                                                        continue;
-                                                    if (str_contains($name, "[]"))
-                                                        // This likely belongs to an obj-array (eg objs[].a); we only show the parent (objs[]), so skip
-                                                        continue;
-                                                    @endphp
-                                                <label aria-hidden="true" for="bodyparam-{{ $endpoint->endpointId() }}-{{ $name }}">{{ $name }}</label>
-                                                <span class="sl-mx-3">:</span>
-                                                <div class="sl-flex sl-flex-1">
-                                                    <div class="sl-input sl-flex-1 sl-relative">
-                                                        @if($parameter->type == 'file')
-                                                            <input aria-label="{{ $name }}"
-                                                                   id="bodyparam-{{ $endpoint->endpointId() }}-{{ $name }}"
-                                                                   type="file" placeholder="Upload"
-                                                                   class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
-                                                            >
-                                                        @elseif(str_ends_with($parameter->type, '[]'))
-                                                            <input aria-label="{{ $name }}"
-                                                                   id="bodyparam-{{ $endpoint->endpointId() }}-{{ $name }}"
-                                                                   placeholder="{{ $parameter->description }}"
-                                                                   value="{{ json_encode($parameter->example) }}"
-                                                                   class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
-                                                            >
-                                                        @else
-                                                            <input aria-label="{{ $name }}"
-                                                                   id="bodyparam-{{ $endpoint->endpointId() }}-{{ $name }}"
-                                                                   placeholder="{{ $parameter->description }}"
-                                                                   value="{{ $parameter->example }}"
-                                                                   class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
-                                                            >
-                                                        @endif
-                                                    </div>
-                                                </div>
-                                                @endforeach
-                                                    </div>
-                                                    @endif
-                                        </div>
-                                    </div>
-                                @endif
-
-
-                                <div class="SendButtonHolder sl-mt-4 sl-p-4 sl-pt-0">
-                                    <div class="sl-stack sl-stack--horizontal sl-stack--2 sl-flex sl-flex-row sl-items-center">
-                                        <button type="button"
-                                                class="sl-button sl-h-sm sl-text-base sl-font-medium sl-px-1.5 sl-bg-primary hover:sl-bg-primary-dark active:sl-bg-primary-darker disabled:sl-bg-canvas-100 sl-text-on-primary disabled:sl-text-body sl-rounded sl-border-transparent sl-border disabled:sl-opacity-70">
-                                            Send API Request
-                                        </button>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
+                @if($metadata['try_it_out']['enabled'] ?? false)
+                    @include("scribe::themes.elements.try_it_out")
+                @endif
 
                     @if($metadata['example_languages'])
                         <div class="sl-panel sl-outline-none sl-w-full sl-rounded-lg">
@@ -405,7 +158,8 @@
                                 <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
                                     <div class="sl--ml-2">
                                         Request sample:
-                                        <select class="example-request-lang-toggle sl-text-base" aria-label="Request Sample Language"
+                                        <select class="example-request-lang-toggle sl-text-base"
+                                                aria-label="Request Sample Language"
                                                 onchange="switchExampleLanguage(event.target.value);">
                                             @foreach($metadata['example_languages'] as $language)
                                                 <option>{{ $language }}</option>
@@ -498,10 +252,9 @@
                                     </div>
                                 </div>
                             @endforeach
-                            @endif
                         </div>
-                </div>
-            </div>
-        @endif
+                    @endif
+        </div>
+    </div>
 </div>
 

+ 89 - 1
resources/views/themes/elements/index.blade.php

@@ -33,7 +33,7 @@
         </script>
         <script src="{{ u::getVersionedAsset($assetPathPrefix.'js/tryitout.js') }}"></script>
         <style>
-            .code-editor {
+            .code-editor, .response-content {
                 color: whitesmoke;
                 background-color: transparent;
             }
@@ -62,6 +62,94 @@
             }
 
         </style>
+
+        <script>
+            function tryItOut(btnElement) {
+                btnElement.disabled = true;
+
+                let endpointId = btnElement.dataset.endpoint;
+
+                let errorPanel = document.querySelector(`.tryItOut-error[data-endpoint=${endpointId}]`);
+                errorPanel.hidden = true;
+                let responsePanel = document.querySelector(`.tryItOut-response[data-endpoint=${endpointId}]`);
+                responsePanel.hidden = true;
+
+                let form = btnElement.form;
+                let { method, path, hasjsonbody } = form.dataset;
+                let body = {};
+                if (hasjsonbody === "1") {
+                    body = form.querySelector('.code-editor').textContent;
+                } else if (form.dataset.hasfiles === "1") {
+                    body = new FormData();
+                    form.querySelectorAll('input[data-component=body]')
+                        .forEach(el => {
+                            if (el.type === 'file') {
+                                if (el.files[0]) body.append(el.name, el.files[0])
+                            } else body.append(el.name, el.value);
+                        });
+                } else {
+                    form.querySelectorAll('input[data-component=body]').forEach(el => {
+                        _.set(body, el.name, el.value);
+                    });
+                }
+
+                const urlParameters = form.querySelectorAll('input[data-component=url]');
+                urlParameters.forEach(el => (path = path.replace(new RegExp(`\\{${el.name}\\??}`), el.value)));
+
+                const headers = Object.fromEntries(Array.from(form.querySelectorAll('input[data-component=header]'))
+                    .map(el => [el.name, (el.dataset.prefix || '') + el.value]));
+
+                const query = {}
+                form.querySelectorAll('input[data-component=query]').forEach(el => {
+                    _.set(query, el.name, el.value);
+                });
+
+                let preflightPromise = Promise.resolve();
+                if (window.useCsrf && window.csrfUrl) {
+                    preflightPromise = makeAPICall('GET', window.csrfUrl).then(() => {
+                        headers['X-XSRF-TOKEN'] = getCookie('XSRF-TOKEN');
+                    });
+                }
+
+                return preflightPromise.then(() => makeAPICall(method, path, body, query, headers, endpointId))
+                    .then(([responseStatus, statusText, responseContent, responseHeaders]) => {
+                        responsePanel.hidden = false;
+                        responsePanel.querySelector(`.response-status`).textContent = responseStatus + " " + statusText ;
+
+                        let contentEl = responsePanel.querySelector(`.response-content`);
+                        if (responseContent === '') {
+                            contentEl.textContent = '<Empty response>'
+                            return;
+                        }
+
+                        // Prettify it if it's JSON
+                        let isJson = false;
+                        try {
+                            const jsonParsed = JSON.parse(responseContent);
+                            if (jsonParsed !== null) {
+                                isJson = true;
+                                responseContent = JSON.stringify(jsonParsed, null, 4);
+                            }
+                        } catch (e) {}
+
+                        contentEl.innerHTML = responseContent;
+                        isJson && window.hljs.highlightElement(contentEl);
+                    })
+                    .catch(err => {
+                        console.log(err);
+                        let errorMessage = err.message || err;
+                        errorPanel.hidden = false;
+                        errorPanel.querySelector(`.error-message`).textContent = errorMessage;
+                    })
+                    .finally(() => { btnElement.disabled = false } );
+            }
+
+            window.addEventListener('DOMContentLoaded', () => {
+                document.querySelectorAll('.tryItOut-btn').forEach(el => {
+                    el.addEventListener('click', () => tryItOut(el));
+                });
+            })
+        </script>
     @endif
 
     <script src="{{ u::getVersionedAsset($assetPathPrefix.'js/theme-elements.js') }}"></script>

+ 329 - 0
resources/views/themes/elements/try_it_out.blade.php

@@ -0,0 +1,329 @@
+@php
+/** @var \Knuckles\Camel\Output\OutputEndpointData $endpoint */
+@endphp
+
+<div class="sl-inverted">
+    <div class="sl-overflow-y-hidden sl-rounded-lg">
+        <form class="TryItPanel sl-bg-canvas-100 sl-rounded-lg"
+              data-method="{{ $endpoint->httpMethods[0] }}"
+              data-path="{{ $endpoint->uri }}"
+              data-hasfiles="{{ $endpoint->hasFiles() ? 1 : 0 }}"
+              data-hasjsonbody="{{ $endpoint->hasJsonBody() ? 1 : 0 }}">
+            @if($endpoint->isAuthed() && $metadata['auth']['location'] !== 'body')
+                <div class="sl-panel sl-outline-none sl-w-full expandable">
+                    <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
+                         role="button">
+                        <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
+                            <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
+                                <svg aria-hidden="true" focusable="false" data-prefix="fas"
+                                     data-icon="caret-down"
+                                     class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
+                                     xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
+                                    <path fill="currentColor"
+                                          d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
+                                </svg>
+                            </div>
+                            Auth
+                        </div>
+                    </div>
+                    <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
+                        <div class="ParameterGrid sl-p-4">
+                            <label aria-hidden="true"
+                                   for="auth-{{ $endpoint->endpointId() }}">{{ $metadata['auth']['name'] }}</label>
+                            <span class="sl-mx-3">:</span>
+                            <div class="sl-flex sl-flex-1">
+                                <div class="sl-input sl-flex-1 sl-relative">
+                                    <code>{{ $metadata['auth']['prefix'] }}</code>
+                                    <input aria-label="{{ $metadata['auth']['name'] }}"
+                                           id="auth-{{ $endpoint->endpointId() }}"
+                                           data-component="{{ $metadata['auth']['location'] }}"
+                                           data-prefix="{{ $metadata['auth']['prefix'] }}"
+                                           name="{{ $metadata['auth']['name'] }}"
+                                           placeholder="{{ $metadata['auth']['placeholder'] }}"
+                                           class="auth-value sl-relative {{ $metadata['auth']['prefix'] ? 'sl-w-3/5' : 'sl-w-full sl-pr-2.5 sl-pl-2.5' }} sl-h-md sl-text-base sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border">
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            @endif
+
+            @if(count($endpoint->headers))
+                <div class="sl-panel sl-outline-none sl-w-full expandable">
+                    <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
+                         role="button">
+                        <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
+                            <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
+                                <svg aria-hidden="true" focusable="false" data-prefix="fas"
+                                     data-icon="caret-down"
+                                     class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
+                                     xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
+                                    <path fill="currentColor"
+                                          d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
+                                </svg>
+                            </div>
+                            Headers
+                        </div>
+                    </div>
+                    <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
+                        <div class="ParameterGrid sl-p-4">
+                            @foreach($endpoint->headers as $name => $example)
+                                @php
+                                    if($endpoint->isAuthed() && $metadata['auth']['location'] === 'header' && $name === $metadata['auth']['name']) continue;
+                                @endphp
+                                <label aria-hidden="true"
+                                       for="header-{{ $endpoint->endpointId() }}-{{ $name }}">{{ $name }}</label>
+                                <span class="sl-mx-3">:</span>
+                                <div class="sl-flex sl-flex-1">
+                                    <div class="sl-input sl-flex-1 sl-relative">
+                                        <input aria-label="{{ $name }}" name="{{ $name }}"
+                                               id="header-{{ $endpoint->endpointId() }}-{{ $name }}"
+                                               value="{{ $example }}" data-component="header"
+                                               class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border">
+                                    </div>
+                                </div>
+                            @endforeach
+                        </div>
+                    </div>
+                </div>
+            @endif
+
+            @if(count($endpoint->urlParameters))
+                <div class="sl-panel sl-outline-none sl-w-full expandable">
+                    <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
+                         role="button">
+                        <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
+                            <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
+                                <svg aria-hidden="true" focusable="false" data-prefix="fas"
+                                     data-icon="caret-down"
+                                     class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
+                                     xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
+                                    <path fill="currentColor"
+                                          d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
+                                </svg>
+                            </div>
+                            URL Parameters
+                        </div>
+                    </div>
+                    <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
+                        <div class="ParameterGrid sl-p-4">
+                            @foreach($endpoint->urlParameters as $name => $parameter)
+                                <label aria-hidden="true"
+                                       for="urlparam-{{ $endpoint->endpointId() }}-{{ $name }}">{{ $name }}</label>
+                                <span class="sl-mx-3">:</span>
+                                <div class="sl-flex sl-flex-1">
+                                    <div class="sl-input sl-flex-1 sl-relative">
+                                        <input aria-label="{{ $name }}" name="{{ $name }}"
+                                               id="urlparam-{{ $endpoint->endpointId() }}-{{ $name }}"
+                                               placeholder="{{ $parameter->description }}"
+                                               value="{{ $parameter->example }}" data-component="url"
+                                               class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border">
+                                    </div>
+                                </div>
+                            @endforeach
+                        </div>
+                    </div>
+                </div>
+            @endif
+
+            @if(count($endpoint->queryParameters))
+                <div class="sl-panel sl-outline-none sl-w-full expandable">
+                    <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
+                         role="button">
+                        <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
+                            <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
+                                <svg aria-hidden="true" focusable="false" data-prefix="fas"
+                                     data-icon="caret-down"
+                                     class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
+                                     xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
+                                    <path fill="currentColor"
+                                          d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
+                                </svg>
+                            </div>
+                            Query Parameters
+                        </div>
+                    </div>
+                    <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
+                        <div class="ParameterGrid sl-p-4">
+                            @foreach($endpoint->queryParameters as $name => $parameter)
+                                @php
+                                    /** @var \Knuckles\Camel\Output\Parameter $parameter */
+                                    if ($parameter->type == 'object') // Skip; individual object children are listed
+                                        continue;
+                                    if (str_contains($name, "[]"))
+                                        // This likely belongs to an obj-array (eg objs[].a); we only show the parent (objs[]), so skip
+                                        continue;
+                                    if($endpoint->isAuthed() && $metadata['auth']['location'] === 'query'
+                                    && $name === $metadata['auth']['name']) continue;
+                                @endphp
+                                <label aria-hidden="true"
+                                       for="queryparam-{{ $endpoint->endpointId() }}-{{ $name }}">{{ $name }}</label>
+                                <span class="sl-mx-3">:</span>
+                                <div class="sl-flex sl-flex-1">
+                                    <div class="sl-input sl-flex-1 sl-relative">
+                                        @if(str_ends_with($parameter->type, '[]'))
+                                            <input aria-label="{{ $name }}" name="{{ $name }}"
+                                                   id="queryparam-{{ $endpoint->endpointId() }}-{{ $name }}"
+                                                   placeholder="{{ $parameter->description }}"
+                                                   value="{{ json_encode($parameter->example) }}" data-component="query"
+                                                   class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
+                                            >
+                                        @else
+                                            <input aria-label="{{ $name }}" name="{{ $name }}"
+                                                   id="queryparam-{{ $endpoint->endpointId() }}-{{ $name }}"
+                                                   placeholder="{{ $parameter->description }}"
+                                                   value="{{ $parameter->example }}" data-component="query"
+                                                   class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
+                                            >
+                                        @endif
+                                    </div>
+                                </div>
+                            @endforeach
+                        </div>
+                    </div>
+                </div>
+            @endif
+
+            @if(count($endpoint->bodyParameters))
+                <div class="sl-panel sl-outline-none sl-w-full expandable">
+                    <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
+                         role="button">
+                        <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
+                            <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
+                                <svg aria-hidden="true" focusable="false" data-prefix="fas"
+                                     data-icon="caret-down"
+                                     class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
+                                     xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
+                                    <path fill="currentColor"
+                                          d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
+                                </svg>
+                            </div>
+                            Body
+                        </div>
+                    </div>
+                    <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
+                        @if($endpoint->hasJsonBody())
+                            <div class="TextRequestBody sl-p-4">
+                                <div class="code-editor language-json"
+                                     id="json-body-{{ $endpoint->endpointId() }}"
+                                     style="font-family: var(--font-code); font-size: 12px; line-height: var(--lh-code);"
+                                >{!! json_encode($endpoint->getSampleBody(), JSON_PRETTY_PRINT) !!}</div>
+                            </div>
+                        @else
+                            <div class="ParameterGrid sl-p-4">
+                                @foreach($endpoint->bodyParameters as $name => $parameter)
+                                    @php
+                                        /** @var \Knuckles\Camel\Output\Parameter $parameter */
+                                        if ($parameter->type == 'object') // Skip; individual object children are listed
+                                            continue;
+                                        if (str_contains($name, "[]"))
+                                            // This likely belongs to an obj-array (eg objs[].a); we only show the parent (objs[]), so skip
+                                            continue;
+                                    @endphp
+                                    <label aria-hidden="true"
+                                           for="bodyparam-{{ $endpoint->endpointId() }}-{{ $name }}">{{ $name }}</label>
+                                    <span class="sl-mx-3">:</span>
+                                    <div class="sl-flex sl-flex-1">
+                                        <div class="sl-input sl-flex-1 sl-relative">
+                                            @if($parameter->type == 'file')
+                                                <input aria-label="{{ $name }}" name="{{ $name }}"
+                                                       id="bodyparam-{{ $endpoint->endpointId() }}-{{ $name }}"
+                                                       type="file" data-component="body"
+                                                       class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
+                                                >
+                                            @elseif(str_ends_with($parameter->type, '[]'))
+                                                <input aria-label="{{ $name }}" name="{{ $name }}"
+                                                       id="bodyparam-{{ $endpoint->endpointId() }}-{{ $name }}"
+                                                       placeholder="{{ $parameter->description }}"
+                                                       value="{{ json_encode($parameter->example) }}" data-component="body"
+                                                       class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
+                                                >
+                                            @else
+                                                <input aria-label="{{ $name }}" name="{{ $name }}"
+                                                       id="bodyparam-{{ $endpoint->endpointId() }}-{{ $name }}"
+                                                       placeholder="{{ $parameter->description }}"
+                                                       value="{{ $parameter->example }}" data-component="body"
+                                                       class="sl-relative sl-w-full sl-h-md sl-text-base sl-pr-2.5 sl-pl-2.5 sl-rounded sl-border-transparent hover:sl-border-input focus:sl-border-primary sl-border"
+                                                >
+                                            @endif
+                                        </div>
+                                    </div>
+                                @endforeach
+                            </div>
+                        @endif
+                    </div>
+                </div>
+            @endif
+
+            <div class="SendButtonHolder sl-mt-4 sl-p-4 sl-pt-0">
+                <div class="sl-stack sl-stack--horizontal sl-stack--2 sl-flex sl-flex-row sl-items-center">
+                    <button type="button" data-endpoint="{{ $endpoint->endpointId() }}"
+                            class="tryItOut-btn sl-button sl-h-sm sl-text-base sl-font-medium sl-px-1.5 sl-bg-primary hover:sl-bg-primary-dark active:sl-bg-primary-darker disabled:sl-bg-canvas-100 sl-text-on-primary disabled:sl-text-body sl-rounded sl-border-transparent sl-border disabled:sl-opacity-70"
+                    >
+                        Send API Request
+                    </button>
+                </div>
+            </div>
+
+            <div data-endpoint="{{ $endpoint->endpointId() }}"
+                 class="tryItOut-error expandable sl-panel sl-outline-none sl-w-full" hidden>
+                <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
+                     role="button">
+                    <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
+                        <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
+                            <svg aria-hidden="true" focusable="false" data-prefix="fas"
+                                 data-icon="caret-down"
+                                 class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
+                                 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
+                                <path fill="currentColor"
+                                      d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
+                            </svg>
+                        </div>
+                        Error
+                    </div>
+                </div>
+                <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
+                    <div class="sl-panel__content sl-p-4">
+                        <p class="sl-pb-2"><strong
+                                    class="error-message"></strong></p>
+                        <p class="sl-pb-2">1. Double check that your computer is connected to
+                            the internet.</p>
+                        <p class="sl-pb-2">2. Make sure the API is actually running and
+                            available under the specified URL.</p>
+                        <p>3. If you've checked all of the above and still experiencing issues,
+                            check if the API supports <a target="_blank"
+                                                         rel="noopener noreferrer"
+                                                         href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"
+                                                         class="sl-link sl-font-semibold">CORS</a>.
+                        </p>
+                    </div>
+                </div>
+            </div>
+
+                <div data-endpoint="{{ $endpoint->endpointId() }}"
+                     class="tryItOut-response expandable sl-panel sl-outline-none sl-w-full" hidden>
+                    <div class="sl-panel__titlebar sl-flex sl-items-center sl-relative focus:sl-z-10 sl-text-base sl-leading-none sl-pr-4 sl-pl-3 sl-bg-canvas-200 sl-text-body sl-border-input focus:sl-border-primary sl-cursor-pointer sl-select-none"
+                         role="button">
+                        <div class="sl-flex sl-flex-1 sl-items-center sl-h-lg">
+                            <div class="sl-flex sl-items-center sl-mr-1.5 expansion-chevrons expansion-chevrons-solid expanded">
+                                <svg aria-hidden="true" focusable="false" data-prefix="fas"
+                                     data-icon="caret-down"
+                                     class="svg-inline--fa fa-caret-down fa-fw sl-icon" role="img"
+                                     xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
+                                    <path fill="currentColor"
+                                          d="M310.6 246.6l-127.1 128C176.4 380.9 168.2 384 160 384s-16.38-3.125-22.63-9.375l-127.1-128C.2244 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75S319.8 237.5 310.6 246.6z"></path>
+                                </svg>
+                            </div>
+                            Response
+                        </div>
+                    </div>
+                    <div class="sl-panel__content-wrapper sl-bg-canvas-100 children" role="region">
+                        <div class="sl-panel__content sl-p-4">
+                            <p class="sl-pb-2 response-status"></p>
+                            <pre><code class="sl-pb-2 response-content language-json" style="max-height: 300px;"></code></pre>
+                        </div>
+                    </div>
+                </div>
+        </form>
+    </div>
+</div>