% layout 'yancy';
% content_for head => begin
<script src="/yancy/vue.js"></script>
<script src="/yancy/marked.js"></script>
<style>
main {
margin-top: 10px;
}
.clickable {
cursor: pointer;
}
th.clickable:hover {
background: #dee2e6;
}
.sidebar {
position: fixed;
padding: 0;
left: 0;
top: 0;
bottom: 0;
padding-top: 66px;
background: #f8f9fa;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
overflow: scroll;
}
.sidebar .nav-link {
color: #666;
}
.sidebar .nav-link:hover {
color: #212529;
}
.sidebar .nav-link.active {
color: #007bff;
}
.sidebar .sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
.sidebar .sidebar-top {
display: none;
}
.data-table tbody tr:nth-child( even ) > div {
height: 0;
overflow: hidden;
}
.data-table tbody tr:nth-child( odd ).open + tr td {
padding-bottom: 0.75em;
}
.data-table tbody tr:nth-child( odd ).open + tr > div {
height: auto;
}
.data-table tbody tr:nth-child( even ) td {
padding: 0;
border: 0;
}
.data-table textarea, .add-item textarea {
width: 100%;
}
.add-item {
margin-top: 0.75em;
}
.yes-no input {
display: none;
}
.markdown-editor-panels {
width: calc( 100% - 2px );
height: 50vh;
border: 1px solid #ccc;
border-radius: .25em;
}
.markdown-editor-panels > div,
.markdown-editor-panels > textarea {
display: inline-block;
width: 49%;
height: 100%;
vertical-align: top;
box-sizing: border-box;
padding: 0 20px;
overflow: scroll;
}
div.markdown-editor-panels > textarea {
border: none;
border-right: 1px solid #ccc;
resize: none;
outline: none;
background-color: #f6f6f6;
font-size: 14px;
font-family: 'Monaco', courier, monospace;
padding: 20px;
}
.filters .filter-desc {
vertical-align: middle;
}
/* Fix Bootstrap 4 problem: https://github.com/twbs/bootstrap/issues/23454 */
.is-invalid + .invalid-feedback {
display: block;
}
@media (min-width: 575.98px) {
/* Force sidebar to always be open on larger screens */
#sidebar-collapse.collapse {
display: block;
}
}
@media (min-width: 992px) {
.markdown-preview {
display: none;
}
.markdown-editor .collapse {
display: inline-block;
}
}
@media (max-width: 575.98px) {
.sidebar {
position: absolute;
top: 0;
left: 0;
bottom: auto;
right: 0;
padding-top: 0;
z-index: 10;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1);
}
.sidebar-collapse {
height: 0;
}
.sidebar .sidebar-top {
display: flex;
}
main {
margin-top: 66px;
}
}
@media (max-width: 992px) {
div.markdown-editor-panels > div,
div.markdown-editor-panels > textarea {
display: block;
width: 100%;
}
div.markdown-editor-panels > div.collapse,
div.markdown-editor-panels > textarea.collapse {
display: none;
}
}
</style>
% end
<main id="app" class="container-fluid">
<div v-if="hasCollections" class="sidebar col-sm-3 col-md-3 col-lg-2">
<div class="bg-dark p-2 justify-content-between sidebar-top">
<button class="btn btn-outline-light" type="button"
data-toggle="collapse" data-target="#sidebar-collapse"
aria-controls="sidebar-collapse" aria-expanded="false"
aria-label="Toggle navigation">
<i class="fa fa-bars"></i> Menu
</button>
<a class="btn btn-outline-light" href="<%= stash 'return_to' %>">
<i class="fa fa-sign-out"></i> Back
</a>
</div>
<div id="sidebar-collapse" class="collapse">
<h6 class="sidebar-heading text-muted px-3 mt-2">
Collections
</h6>
<ul class="nav flex-column">
<!-- collection list -->
<li v-for="( val, key ) in collections" class="nav-item">
<a href="#" @click.prevent="setCollection( key )"
class="nav-link"
:class="currentCollection == key ? 'active' : ''"
>
{{ val.operations.list.schema.title || key }}
</a>
</li>
</ul>
</div>
</div>
<div v-if="hasCollections !== null && !hasCollections" class="alert alert-danger" role="alert">
<h4 class="alert-heading">No Collections Configured</h4>
<p class="mb-0">Please configure your data collections, or have
Yancy scan your database by setting <code>read_schema =>
1</code>.</p>
</div>
<div v-show="error && error.fetchSpec" class="alert alert-danger" role="alert">
<h4 class="alert-heading">Error Fetching API Spec</h4>
<p>Error from server: {{ error.fetchSpec }}</p>
<p class="mb-0">Please fix the error and reload the page.</p>
</div>
<div class="row">
<div v-show="currentCollection" class="offset-sm-3 col-sm-9 offset-md-3 col-md-9 offset-lg-2 col-lg-10 table-responsive-md">
<!-- current collection -->
<h2>{{ schema.title ? schema.title : currentCollection }}</h2>
<div v-if="schema.description"
v-html="marked( schema.description )"></div>
<div v-show="error && error.fetchPage" class="alert alert-danger" role="alert">
<h4 class="alert-heading">Error Fetching Data</h4>
<p class="mb-0">Error from server: {{ error.fetchPage }}</p>
</div>
<div v-show="info && info.addItem" class="alert alert-success alert-dismissable" role="alert">
<button type="button" @click="info.addItem = false" class="close" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
Item Added
</div>
<div v-show="info && info.saveItem" class="alert alert-success alert-dismissable" role="alert">
<button type="button" @click="info.saveItem = false" class="close" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
Item Saved
</div>
<div class="collection-buttons d-flex">
<button @click="fetchPage()" class="btn btn-primary mr-1">Refresh</button>
<a v-if="schema['x-view-url']" :href="schema['x-view-url']" class="btn btn-secondary mr-1">
<i class="fa fa-eye" aria-hidden="true"></i> View
</a>
<button @click="newFilter = { }" class="btn btn-outline-secondary mr-1">
Add Filter
</button>
<button @click="showAddItem()" class="btn btn-outline-secondary ml-auto"
:class="{active:addingItem}"
>
Add Item
</button>
</div>
<div v-if="addingItem" class="add-item">
<div v-show="error && error.addItem" class="alert alert-danger" role="alert">
<h4 class="alert-heading">Error Adding Item</h4>
<p class="mb-0">Error from server: {{ error.addItem }}</p>
</div>
<item-form v-model="newItem"
:schema="operations['add'].schema"
@close="cancelAddItem" @input="addItem"
:error="formError"
:show-read-only="false"
/>
</div>
<div class="filters mt-1">
<form class="form-inline" v-if="newFilter"
@submit.prevent="addFilter()"
>
<select class="custom-select col-md-4 col-lg-4" v-model="newFilter.field">
<option v-for="col in columns" :value="col.field">
{{ col.title }}
</option>
</select>
<input class="form-control col-sm-12 col-md-4 col-lg-5" v-model="newFilter.value">
<span class="d-inline-flex justify-space-between col-lg-3 col-md-4" style="padding: 0">
<button style="flex: 1 1 50%" class="btn btn-outline-success">Add</button>
<button style="flex: 1 1 50%" @click.prevent="cancelFilter()" class="btn btn-outline-danger">Remove</button>
</span>
</form>
<div v-for="( filter, i ) in filters">
<span class="filter-desc">Filter: <code>{{ filter.field }}</code> matches <code>{{ filter.value }}</code></span>
<button class="btn btn-outline-danger" @click="removeFilter(i)">Remove</button>
</div>
</div>
<table style="margin-top: 0.5em; width: 100%" class="table data-table">
<thead class="thead-light">
<tr>
<th></th>
<th v-for="( col, i ) in columns"
class="clickable"
@click="toggleSort( col )"
>
<i class="fa" :class="sortClass( col )"></i>
{{col.title}}
</th>
</tr>
</thead>
<tbody>
<template v-for="( row, i ) in items">
<tr :class="openedRow == i ? 'open' : ''">
<td style="width: 5em">
<a v-if="schema['x-view-item-url']" :href="rowViewUrl(row)" title="View">
<i class="fa fa-eye" aria-hidden="true"></i>
</a>
<a href="#" @click.prevent="toggleRow(i)" title="Edit">
<i class="fa fa-pencil-square-o" aria-hidden="true"></i>
</a>
<a href="#" @click.prevent="confirmDeleteItem(i)" title="Delete">
<i class="fa fa-trash-o" aria-hidden="true"></i>
</a>
</td>
<td v-for="col in columns">
{{ col.field ? row[col.field] : fillUrl( col.template, row ) }}
</td>
</tr>
<tr>
<td :colspan="2 + columns.length">
<div v-if="openedRow == i" v-show="error && error.saveItem" class="alert alert-danger" role="alert">
<h4 class="alert-heading">Error Saving Item</h4>
<p class="mb-0">Error from server: {{ error.saveItem }}</p>
</div>
<item-form v-if="openedRow == i" v-model="items[i]"
:schema="operations['set'].schema"
@close="toggleRow(i)" @input="saveItem(i)"
:error="formError"
/>
</td>
</tr>
</template>
</tbody>
</table>
<p v-if="items.length == 0">
No items found.
<a href="#" @click.prevent="showAddItem()">Create an item</a>.
</p>
<nav aria-label="List page navigation">
<ul class="pagination">
<li class="page-item" :class="currentPage == 1 ? 'disabled' : ''">
<a class="page-link" href="#" aria-label="First"
@click.prevent="gotoPage( 1 )"
>
<span aria-hidden="true">«</span>
<span class="sr-only">First</span>
</a>
</li>
<li class="page-item" :class="currentPage == 1 ? 'disabled' : ''">
<a class="page-link" href="#" aria-label="Previous"
@click.prevent="gotoPage( currentPage - 1 )"
>
<span aria-hidden="true">‹</span>
<span class="sr-only">Previous</span>
</a>
</li>
<li class="page-item disabled" v-if="pagerPages[0] > 1">
<span class="page-link">…</span>
</li>
<li v-for="page in pagerPages" class="page-item"
:class="page == currentPage ? 'active': ''"
>
<a class="page-link" href="#" @click.prevent="gotoPage( page )">
{{ page }}
</a>
</li>
<li class="page-item disabled" v-if="pagerPages[ pagerPages.length - 1 ] < totalPages">
<span class="page-link">…</span>
</li>
<li class="page-item" :class="currentPage >= totalPages ? 'disabled' : ''">
<a class="page-link" href="#" aria-label="Next"
@click.prevent="gotoPage( currentPage + 1 )"
>
<span aria-hidden="true">›</span>
<span class="sr-only">Next</span>
</a>
</li>
<li class="page-item" :class="currentPage >= totalPages ? 'disabled' : ''">
<a class="page-link" href="#" aria-label="Last"
@click.prevent="gotoPage( totalPages )"
>
<span aria-hidden="true">»</span>
<span class="sr-only">Last</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
<div id="confirmDelete" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this item?</p>
</div>
<div class="modal-footer">
<button @click.prevent="deleteItem()" type="button" class="btn btn-danger">Delete</button>
<button @click.prevent="cancelDeleteItem()" type="button" class="btn btn-secondary">Cancel</button>
</div>
</div>
</main>
<template id="item-form">
<div>
<form @submit.prevent="save()">
<div class="pull-right" style="margin-bottom: 4px">
<label class="btn btn-secondary">Show Raw <input type="checkbox" v-model="showRaw" /></label>
</div>
<button class="btn btn-primary">Save</button>
<button type="button" @click="cancel()" class="btn btn-danger">Cancel</button>
<div v-if="!showRaw">
<div v-for="( conf, i ) in properties" class="form-group" style="clear: both">
<label :for="'field-' + conf.name + '-' + _uid">
{{ conf.title }} {{ isRequired( conf.name ) ? '*' : '' }}
</label>
<edit-field v-model="$data._value[conf.name]"
:example="example[conf.name]" :schema="conf"
:required="isRequired( conf.name )" :valid="!error[conf.name]"
:aria-labelledby="'field-' + conf.name + '-desc-' + _uid"
/></edit-field>
<div v-show="error[conf.name]" class="invalid-feedback">Error: {{ error[ conf.name ] }}</div>
<small v-if="conf.description" :id="'field-' + conf.name + '-desc-' + _uid"
v-html="marked( conf.description )"></small>
</div>
</div>
<div v-if="showRaw && rawError" class="alert alert-danger" role="alert" style="clear: both">
<h4 class="alert-heading">Error Parsing JSON</h4>
<p class="mb-0">{{ rawError.message }}</p>
</div>
<textarea v-if="showRaw" rows="10" v-model="rawValue" @change="updateRaw"></textarea>
</form>
</div>
</template>
<template id="edit-field">
<div :class="!valid ? 'is-invalid' : ''">
<input v-if="fieldType != 'select' && fieldType != 'checkbox' && fieldType != 'markdown' && fieldType != 'textarea'"
:type="fieldType" :pattern="pattern" :required="required"
:inputmode="inputMode"
:minlength="minlength" :maxlength="maxlength"
:min="min" :max="max"
:readonly="readonly"
:placeholder="example"
v-model="$data._value" @change="input" class="form-control"
:class="!valid ? 'is-invalid' : ''"
/>
<textarea v-if="fieldType == 'textarea'" :required="required"
:disabled="readonly" v-model="$data._value" @change="input"
:class="!valid ? 'is-invalid' : ''" rows="5"></textarea>
<select v-if="fieldType == 'select'"
:required="required" :disabled="readonly"
v-model="$data._value" @change="input"
class="custom-select"
:class="!valid ? 'is-invalid' : ''"
>
<option v-if="!required" :value="undefined">- empty -</option>
<option v-for="val in schema.enum">{{val}}</option>
</select>
<div v-if="fieldType == 'checkbox'"
class="btn-group yes-no"
>
<label class="btn btn-xs" :class="value ? 'btn-success active' : 'btn-outline-success'"
@click="readonly || ( $data._value = true )"
>
<input type="radio" :name="_uid"
v-model="$data._value" @change="input" :value="true"
> Yes
</label>
<label class="btn btn-xs" :class="value ? 'btn-outline-danger' : 'btn-danger active'"
@click="readonly || ( $data._value = false )"
>
<input type="radio" :name="_uid"
v-model="$data._value" @change="input" :value="false"
> No
</label>
</div>
<div v-if="fieldType == 'markdown'" class="markdown-editor">
<div class="markdown-editor-button-bar mb-1">
<button type="button" class="btn btn-outline-secondary markdown-preview"
:class="{active:showHtml}" @click.prevent="showHtml = !showHtml"
>
Preview
</button>
</div>
<div class="markdown-editor-panels">
<textarea v-model="$data._value" @change="input" :class="{collapse:showHtml}"></textarea>
<div v-html="html" :class="{collapse:!showHtml}"></div>
</div>
</div>
</div>
</template>
<script>var specUrl = '<%= url_for("yancy.api") %>';</script>
<script src="/yancy/app.js"></script>