Release v2.4.2
This commit is contained in:
@@ -11,8 +11,7 @@
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Display name</h3>
|
||||
</div>
|
||||
<form id="account-name-form" method="post" action="/account/name" data-async-save data-async-save-close-url="{{returnUrl}}">
|
||||
<input type="hidden" name="return_url" value="{{returnUrl}}" />
|
||||
<form id="account-name-form" method="post" action="/account/name" data-async-save>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<input id="account-name" type="text" name="name" class="form-control" value="{{name}}" autocomplete="name" required />
|
||||
@@ -20,7 +19,7 @@
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Display name actions">
|
||||
{{{saveActionButtons formId="account-name-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
<button type="submit" class="btn btn-success" form="account-name-form" name="save_action" value="save">Save</button>
|
||||
<a class="btn btn-warning" href="/dashboard" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,8 +32,7 @@
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Password</h3>
|
||||
</div>
|
||||
<form id="account-password-form" method="post" action="/account/password" data-async-save data-async-save-close-url="{{returnUrl}}">
|
||||
<input type="hidden" name="return_url" value="{{returnUrl}}" />
|
||||
<form id="account-password-form" method="post" action="/account/password" data-async-save>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="account-current-password" class="form-label">Current password</label>
|
||||
@@ -51,7 +49,7 @@
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Password actions">
|
||||
{{{saveActionButtons formId="account-password-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
<button type="submit" class="btn btn-success" form="account-password-form" name="save_action" value="save">Save</button>
|
||||
<a class="btn btn-warning" href="/dashboard" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<div class="login-logo text-center mb-3">
|
||||
<strong>Pulse</strong> Signage
|
||||
</div>
|
||||
<div class="card mx-auto" style="max-width: 26rem;">
|
||||
<div class="card-body p-4">
|
||||
<h1 class="h4 mb-3">Sign in</h1>
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Add API source</h2>
|
||||
<p>Adjust the endpoint or refresh cadence for this API source.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">API source details</h3>
|
||||
</div>
|
||||
<form id="api-source-form" method="post" action="/data-sources/api-sources" data-async-save data-async-save-close-url="/data-sources/api-sources" data-async-save-new-url="/data-sources/api-sources/new">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-lg-6">
|
||||
<label for="api-source-name" class="form-label">Name</label>
|
||||
<input id="api-source-name" name="name" class="form-control" value="{{apiSource.name}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<label for="api-source-url" class="form-label">API URL</label>
|
||||
<input id="api-source-url" name="api_url" type="url" class="form-control" value="{{apiSource.apiUrl}}" placeholder="https://example.com/api.json" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h4 class="api-source-section-heading">Access and parsing</h4>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="api-source-auth-method" class="form-label">Auth method</label>
|
||||
<select id="api-source-auth-method" name="auth_method" class="form-select" data-api-source-auth-method>
|
||||
<option value="none" {{#if (eq apiSource.authMethod 'none')}}selected{{/if}}>None</option>
|
||||
<option value="basic" {{#if (eq apiSource.authMethod 'basic')}}selected{{/if}}>Basic</option>
|
||||
<option value="bearer" {{#if (eq apiSource.authMethod 'bearer')}}selected{{/if}}>Bearer token</option>
|
||||
<option value="api_key_header" {{#if (eq apiSource.authMethod 'api_key_header')}}selected{{/if}}>API key header</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<label for="api-source-items-path" class="form-label">Items path</label>
|
||||
<input id="api-source-items-path" name="items_path" class="form-control" value="{{apiSource.itemsPath}}" placeholder="items, results.data, or another array path" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-12 col-md-8 offset-md-4">
|
||||
<div class="form-text">Optional. Use dot notation to point at the array this source should iterate through.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4" data-api-source-auth-details-section>
|
||||
<h4 class="api-source-section-heading">Authentication details</h4>
|
||||
<div class="row g-3 mb-3" data-api-source-auth-panel="basic" hidden>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="api-source-auth-username" class="form-label">Username</label>
|
||||
<input id="api-source-auth-username" name="auth_username" class="form-control" value="{{apiSource.authUsername}}" autocomplete="username" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="api-source-auth-password" class="form-label">Password</label>
|
||||
<input id="api-source-auth-password" name="auth_password" type="password" class="form-control" value="{{apiSource.authPassword}}" autocomplete="current-password" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-3" data-api-source-auth-panel="bearer" hidden>
|
||||
<div class="col-12">
|
||||
<label for="api-source-auth-bearer-token" class="form-label">Bearer token</label>
|
||||
<input id="api-source-auth-bearer-token" name="auth_bearer_token" type="password" class="form-control" value="{{apiSource.authBearerToken}}" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-3" data-api-source-auth-panel="api_key_header" hidden>
|
||||
<div class="col-12 col-md-5">
|
||||
<label for="api-source-auth-header-name" class="form-label">Header name</label>
|
||||
<input id="api-source-auth-header-name" name="auth_header_name" class="form-control" value="{{apiSource.authHeaderName}}" placeholder="X-API-Key" />
|
||||
</div>
|
||||
<div class="col-12 col-md-7">
|
||||
<label for="api-source-auth-header-value" class="form-label">Header value</label>
|
||||
<input id="api-source-auth-header-value" name="auth_header_value" class="form-control" value="{{apiSource.authHeaderValue}}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="api-source-section-heading">Refresh</h4>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="api-source-interval" class="form-label">Update interval</label>
|
||||
<input id="api-source-interval" name="update_interval_value" type="number" min="1" class="form-control" value="{{apiSource.updateIntervalValue}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="api-source-interval-unit" class="form-label">Unit</label>
|
||||
<select id="api-source-interval-unit" name="update_interval_unit" class="form-select">
|
||||
<option value="seconds" {{#if (eq apiSource.updateIntervalUnit 'seconds')}}selected{{/if}}>Seconds</option>
|
||||
<option value="minutes" {{#if (eq apiSource.updateIntervalUnit 'minutes')}}selected{{/if}}>Minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="API source actions">
|
||||
{{{saveActionButtons formId="api-source-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/data-sources/api-sources" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/assets/js/data-sources/api-source-form.js?v={{appVersion}}"></script>
|
||||
</form>
|
||||
</div>
|
||||
+26
-38
@@ -1,26 +1,24 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit API source - {{apiSource.name}}</h2>
|
||||
<p>Adjust the endpoint or refresh cadence and review the latest JSON response.</p>
|
||||
<h2>{{#if isEdit}}Edit API source - {{apiSource.name}}{{else}}Add API source{{/if}}</h2>
|
||||
<p>{{#if isEdit}}Adjust the endpoint or refresh cadence and review the latest JSON response.{{else}}Adjust the endpoint or refresh cadence for this API source.{{/if}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">API source details</h3>
|
||||
</div>
|
||||
<form id="api-source-form" method="post" action="/data-sources/api-sources/{{apiSource.id}}" data-async-save data-async-save-refresh-target="#api-source-response-panel" data-async-save-refresh-state-url="/data-sources/api-sources/{{apiSource.id}}/state" data-async-save-close-url="/data-sources/api-sources" data-async-save-new-url="/data-sources/api-sources/new">
|
||||
<form id="api-source-form" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">API source details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<label for="api-source-name" class="form-label">Name</label>
|
||||
<input id="api-source-name" name="name" class="form-control" value="{{apiSource.name}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<label for="api-source-url" class="form-label">API URL</label>
|
||||
<input id="api-source-url" name="api_url" type="url" class="form-control" value="{{apiSource.apiUrl}}" placeholder="https://example.com/api.json" required />
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-lg-6">
|
||||
<label for="api-source-name" class="form-label">Name</label>
|
||||
<input id="api-source-name" name="name" class="form-control" value="{{apiSource.name}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<label for="api-source-url" class="form-label">API URL</label>
|
||||
<input id="api-source-url" name="api_url" type="url" class="form-control" value="{{apiSource.apiUrl}}" placeholder="https://example.com/api.json" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
@@ -93,24 +91,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="API source actions">
|
||||
{{{saveActionButtons formId="api-source-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/data-sources/api-sources" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if inUse}}
|
||||
<button type="submit" class="btn btn-outline-danger" form="delete-api-source-form" disabled>Delete</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-danger" form="delete-api-source-form">Delete</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{{saveActionButtons formId="api-source-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card card-outline card-secondary mt-4" id="api-source-response-panel">
|
||||
<div class="card card-outline card-secondary mt-3" id="api-source-response-panel">
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<h3 class="card-title mb-0">Latest response</h3>
|
||||
{{#if apiSource.lastResponseJson}}
|
||||
{{#if lastResponseJson}}
|
||||
<div class="btn-group btn-group-sm ms-auto" role="group" aria-label="JSON tree actions">
|
||||
<button type="button" class="btn btn-outline-secondary" data-json-toggle-collapse-all>
|
||||
<i class="bi bi-arrows-collapse me-1"></i>
|
||||
@@ -124,8 +113,8 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{#if apiSource.lastPullError}}
|
||||
<div class="alert alert-warning">{{apiSource.lastPullError}}</div>
|
||||
{{#if lastPullError}}
|
||||
<div class="alert alert-warning">{{lastPullError}}</div>
|
||||
{{/if}}
|
||||
<dl class="row">
|
||||
<dt class="col-12 col-md-3">Last pulled</dt>
|
||||
@@ -143,10 +132,10 @@
|
||||
<dt class="col-12 col-md-3">Content type</dt>
|
||||
<dd class="col-12 col-md-9">{{#if apiSource.lastResponseContentType}}{{apiSource.lastResponseContentType}}{{else}}-{{/if}}</dd>
|
||||
</dl>
|
||||
{{#if apiSource.lastResponseJson}}
|
||||
{{#if lastResponseJson}}
|
||||
<div data-json-toggle-panel>
|
||||
<script type="application/json" data-json-toggle-source>{{json apiSource.lastResponseJson}}</script>
|
||||
<div class="mb-0 small bg-body-tertiary border rounded p-3 font-monospace" style="white-space: pre-wrap; word-break: break-word;" data-json-toggle-output>{{apiSource.lastResponseJson}}</div>
|
||||
<script type="application/json" data-json-toggle-source>{{json lastResponseJson}}</script>
|
||||
<div class="mb-0 small bg-body-tertiary border rounded p-3 font-monospace" style="white-space: pre-wrap; word-break: break-word;" data-json-toggle-output>{{lastResponseJson}}</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="alert alert-secondary mb-0">No JSON response is stored yet.</div>
|
||||
@@ -154,5 +143,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="delete-api-source-form" method="post" action="/data-sources/api-sources/{{apiSource.id}}/delete" data-confirm-message="Delete this API source?"></form>
|
||||
<script type="module" src="/assets/js/data-sources/api-source-form.js?v={{appVersion}}"></script>
|
||||
<script src="/assets/js/data-sources/api-source-form.js?v={{appVersion}}"></script>
|
||||
@@ -1,48 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Add RSS feed</h2>
|
||||
<p>Capture a feed URL, refresh interval, and the number of items to store.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">RSS feed details</h3>
|
||||
</div>
|
||||
<form id="rss-feed-form" method="post" action="/data-sources/rss-feeds" data-async-save data-async-save-close-url="/data-sources/rss-feeds" data-async-save-new-url="/data-sources/rss-feeds/new">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="rss-feed-name" class="form-label">Name</label>
|
||||
<input id="rss-feed-name" name="name" class="form-control" value="{{rssFeed.name}}" required />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="rss-feed-url" class="form-label">Feed URL</label>
|
||||
<input id="rss-feed-url" name="feed_url" type="url" class="form-control" value="{{rssFeed.feedUrl}}" placeholder="https://example.com/feed.xml" required />
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="rss-feed-interval" class="form-label">Update interval</label>
|
||||
<input id="rss-feed-interval" name="update_interval_value" type="number" min="1" class="form-control" value="{{rssFeed.updateIntervalValue}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="rss-feed-interval-unit" class="form-label">Unit</label>
|
||||
<select id="rss-feed-interval-unit" name="update_interval_unit" class="form-select">
|
||||
<option value="seconds" {{#if (eq rssFeed.updateIntervalUnit 'seconds')}}selected{{/if}}>Seconds</option>
|
||||
<option value="minutes" {{#if (eq rssFeed.updateIntervalUnit 'minutes')}}selected{{/if}}>Minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="rss-feed-item-limit" class="form-label">Items pulled</label>
|
||||
<input id="rss-feed-item-limit" name="item_limit" type="number" min="1" class="form-control" value="{{rssFeed.itemLimit}}" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="RSS feed actions">
|
||||
{{{saveActionButtons formId="rss-feed-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/data-sources/rss-feeds" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,189 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit RSS feed - {{rssFeed.name}}</h2>
|
||||
<p>Adjust the source URL, refresh interval, or item count.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">RSS feed details</h3>
|
||||
</div>
|
||||
<form id="rss-feed-form" method="post" action="/data-sources/rss-feeds/{{rssFeed.id}}" data-async-save data-async-save-refresh-target="#rss-feed-items-panel" data-async-save-refresh-state-url="/data-sources/rss-feeds/{{rssFeed.id}}/state" data-async-save-close-url="/data-sources/rss-feeds" data-async-save-new-url="/data-sources/rss-feeds/new">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="rss-feed-name" class="form-label">Name</label>
|
||||
<input id="rss-feed-name" name="name" class="form-control" value="{{rssFeed.name}}" required />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="rss-feed-url" class="form-label">Feed URL</label>
|
||||
<input id="rss-feed-url" name="feed_url" type="url" class="form-control" value="{{rssFeed.feedUrl}}" placeholder="https://example.com/feed.xml" required />
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="rss-feed-interval" class="form-label">Update interval</label>
|
||||
<input id="rss-feed-interval" name="update_interval_value" type="number" min="1" class="form-control" value="{{rssFeed.updateIntervalValue}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="rss-feed-interval-unit" class="form-label">Unit</label>
|
||||
<select id="rss-feed-interval-unit" name="update_interval_unit" class="form-select">
|
||||
<option value="seconds" {{#if (eq rssFeed.updateIntervalUnit 'seconds')}}selected{{/if}}>Seconds</option>
|
||||
<option value="minutes" {{#if (eq rssFeed.updateIntervalUnit 'minutes')}}selected{{/if}}>Minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="rss-feed-item-limit" class="form-label">Items pulled</label>
|
||||
<input id="rss-feed-item-limit" name="item_limit" type="number" min="1" class="form-control" value="{{rssFeed.itemLimit}}" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="RSS feed actions">
|
||||
{{{saveActionButtons formId="rss-feed-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/data-sources/rss-feeds" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if inUse}}
|
||||
<button type="submit" class="btn btn-outline-danger" form="delete-rss-feed-form" disabled>Delete</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-danger" form="delete-rss-feed-form">Delete</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-secondary mt-4" id="rss-feed-items-panel">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Pulled items</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{#if pullError}}
|
||||
<div class="alert alert-warning">{{pullError}}</div>
|
||||
{{/if}}
|
||||
{{#if pulledItems.length}}
|
||||
<div class="accordion rss-feed-items-accordion" id="rss-feed-items-accordion">
|
||||
{{#each pulledItems}}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="rss-feed-item-heading-{{id}}-{{position}}">
|
||||
<button class="accordion-button {{#unless @first}}collapsed{{/unless}}" type="button" data-bs-toggle="collapse" data-bs-target="#rss-feed-item-collapse-{{id}}-{{position}}" aria-expanded="{{#if @first}}true{{else}}false{{/if}}" aria-controls="rss-feed-item-collapse-{{id}}-{{position}}">
|
||||
<span class="me-2">{{title}}</span>
|
||||
<span class="text-muted small">{{#if pubDate}}• {{pubDate}}{{/if}}</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="rss-feed-item-collapse-{{id}}-{{position}}" class="accordion-collapse collapse {{#if @first}}show{{/if}}" aria-labelledby="rss-feed-item-heading-{{id}}-{{position}}" data-bs-parent="#rss-feed-items-accordion">
|
||||
<div class="accordion-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-12 col-md-3">Title</dt>
|
||||
<dd class="col-12 col-md-9">{{title}}</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">Link</dt>
|
||||
<dd class="col-12 col-md-9">
|
||||
{{#if link}}
|
||||
<a href="{{link}}" target="_blank" rel="noopener noreferrer">{{link}}</a>
|
||||
{{else}}
|
||||
<span class="empty">-</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">Published</dt>
|
||||
<dd class="col-12 col-md-9">{{#if pubDate}}{{pubDate}}{{else}}-{{/if}}</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">Author</dt>
|
||||
<dd class="col-12 col-md-9">{{#if itemJson.author}}{{itemJson.author}}{{else}}-{{/if}}</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">GUID</dt>
|
||||
<dd class="col-12 col-md-9">{{#if itemJson.guid}}{{itemJson.guid}}{{else}}-{{/if}}</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">Comments</dt>
|
||||
<dd class="col-12 col-md-9">
|
||||
{{#if itemJson.comments}}
|
||||
<a href="{{itemJson.comments}}" target="_blank" rel="noopener noreferrer">{{itemJson.comments}}</a>
|
||||
{{else}}
|
||||
<span class="empty">-</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">Description</dt>
|
||||
<dd class="col-12 col-md-9">{{#if description}}{{description}}{{else}}<span class="empty">-</span>{{/if}}</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">Categories</dt>
|
||||
<dd class="col-12 col-md-9">
|
||||
{{#if itemJson.categories.length}}
|
||||
<ul class="list-group list-group-flush border rounded">
|
||||
{{#each itemJson.categories}}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start">
|
||||
<span>{{value}}</span>
|
||||
{{#if domain}}<span class="text-muted small ms-3">{{domain}}</span>{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<span class="empty">-</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">Enclosure</dt>
|
||||
<dd class="col-12 col-md-9">
|
||||
{{#if itemJson.enclosure}}
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-12 col-lg-4">URL</dt>
|
||||
<dd class="col-12 col-lg-8">
|
||||
{{#if itemJson.enclosure.url}}
|
||||
<a href="{{itemJson.enclosure.url}}" target="_blank" rel="noopener noreferrer">{{itemJson.enclosure.url}}</a>
|
||||
{{else}}
|
||||
<span class="empty">-</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
<dt class="col-12 col-lg-4">Length</dt>
|
||||
<dd class="col-12 col-lg-8">{{#if itemJson.enclosure.length}}{{itemJson.enclosure.length}}{{else}}-{{/if}}</dd>
|
||||
<dt class="col-12 col-lg-4">Type</dt>
|
||||
<dd class="col-12 col-lg-8">{{#if itemJson.enclosure.type}}{{itemJson.enclosure.type}}{{else}}-{{/if}}</dd>
|
||||
</dl>
|
||||
{{else}}
|
||||
<span class="empty">-</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">Source</dt>
|
||||
<dd class="col-12 col-md-9 mb-0">
|
||||
{{#if itemJson.source}}
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-12 col-lg-4">Title</dt>
|
||||
<dd class="col-12 col-lg-8">{{#if itemJson.source.title}}{{itemJson.source.title}}{{else}}-{{/if}}</dd>
|
||||
<dt class="col-12 col-lg-4">URL</dt>
|
||||
<dd class="col-12 col-lg-8">
|
||||
{{#if itemJson.source.url}}
|
||||
<a href="{{itemJson.source.url}}" target="_blank" rel="noopener noreferrer">{{itemJson.source.url}}</a>
|
||||
{{else}}
|
||||
<span class="empty">-</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
</dl>
|
||||
{{else}}
|
||||
<span class="empty">-</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
|
||||
<dt class="col-12 col-md-3">Raw item XML</dt>
|
||||
<dd class="col-12 col-md-9 mb-0">
|
||||
{{#if itemJson.rawXml}}
|
||||
<details>
|
||||
<summary class="small text-muted">Show raw XML</summary>
|
||||
<pre class="mb-0 mt-2 small bg-body-tertiary border rounded p-3 text-wrap">{{itemJson.rawXml}}</pre>
|
||||
</details>
|
||||
{{else}}
|
||||
<span class="empty">-</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="alert alert-secondary mb-0">No feed items could be loaded.</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="delete-rss-feed-form" method="post" action="/data-sources/rss-feeds/{{rssFeed.id}}/delete" data-confirm-message="Delete this RSS feed?"></form>
|
||||
@@ -0,0 +1,141 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{#if isEdit}}Edit RSS feed - {{rssFeed.name}}{{else}}Add RSS feed{{/if}}</h2>
|
||||
<p>{{#if isEdit}}Adjust the source URL, refresh interval, or item count.{{else}}Capture a feed URL, refresh interval, and the number of items to store.{{/if}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="rss-feed-form" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">RSS feed details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="rss-feed-name" class="form-label">Name</label>
|
||||
<input id="rss-feed-name" name="name" class="form-control" value="{{rssFeed.name}}" required />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="rss-feed-url" class="form-label">Feed URL</label>
|
||||
<input id="rss-feed-url" name="feed_url" type="url" class="form-control" value="{{rssFeed.feedUrl}}" placeholder="https://example.com/feed.xml" required />
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="rss-feed-interval" class="form-label">Update interval</label>
|
||||
<input id="rss-feed-interval" name="update_interval_value" type="number" min="1" class="form-control" value="{{rssFeed.updateIntervalValue}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="rss-feed-interval-unit" class="form-label">Unit</label>
|
||||
<select id="rss-feed-interval-unit" name="update_interval_unit" class="form-select">
|
||||
<option value="seconds" {{#if (eq rssFeed.updateIntervalUnit 'seconds')}}selected{{/if}}>Seconds</option>
|
||||
<option value="minutes" {{#if (eq rssFeed.updateIntervalUnit 'minutes')}}selected{{/if}}>Minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="rss-feed-item-limit" class="form-label">Items pulled</label>
|
||||
<input id="rss-feed-item-limit" name="item_limit" type="number" min="1" class="form-control" value="{{rssFeed.itemLimit}}" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
{{{saveActionButtons formId="rss-feed-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card card-outline card-secondary admin-form-card mt-4">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Latest items</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="accordion" id="rss-feed-items-accordion">
|
||||
{{#if pullError}}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="rss-feed-items-heading-error">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#rss-feed-items-collapse-error" aria-expanded="true" aria-controls="rss-feed-items-collapse-error">
|
||||
Preview pulled feed items
|
||||
</button>
|
||||
</h2>
|
||||
<div id="rss-feed-items-collapse-error" class="accordion-collapse collapse show" aria-labelledby="rss-feed-items-heading-error" data-bs-parent="#rss-feed-items-accordion">
|
||||
<div class="accordion-body">
|
||||
<div class="alert alert-danger mb-0" role="alert">{{pullError}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else if pulledItems.length}}
|
||||
{{#each pulledItems}}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="rss-feed-item-heading-{{@index}}">
|
||||
<button class="accordion-button {{#unless @first}}collapsed{{/unless}}" type="button" data-bs-toggle="collapse" data-bs-target="#rss-feed-item-collapse-{{@index}}" aria-expanded="{{#if @first}}true{{else}}false{{/if}}" aria-controls="rss-feed-item-collapse-{{@index}}">
|
||||
{{#if title}}{{title}}{{else}}Item {{@index}}{{/if}}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="rss-feed-item-collapse-{{@index}}" class="accordion-collapse collapse {{#if @first}}show{{/if}}" aria-labelledby="rss-feed-item-heading-{{@index}}" data-bs-parent="#rss-feed-items-accordion">
|
||||
<div class="accordion-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped align-middle mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap" style="width: 10rem;">Title</th>
|
||||
<td class="text-break">{{#if title}}{{title}}{{else}}-{{/if}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">Link</th>
|
||||
<td class="text-break">
|
||||
{{#if link}}
|
||||
<a href="{{link}}" target="_blank" rel="noreferrer">{{link}}</a>
|
||||
{{else}}
|
||||
-
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">Published</th>
|
||||
<td class="text-break">{{#if pubDate}}{{pubDate}}{{else}}-{{/if}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">Description</th>
|
||||
<td class="text-break">{{#if description}}{{description}}{{else}}-{{/if}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">Author</th>
|
||||
<td class="text-break">{{#if author}}{{author}}{{else}}-{{/if}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">Guid</th>
|
||||
<td class="text-break">{{#if guid}}{{guid}}{{else}}-{{/if}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">Comments</th>
|
||||
<td class="text-break">
|
||||
{{#if comments}}
|
||||
<a href="{{comments}}" target="_blank" rel="noreferrer">{{comments}}</a>
|
||||
{{else}}
|
||||
-
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="rss-feed-items-heading-empty">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#rss-feed-items-collapse-empty" aria-expanded="false" aria-controls="rss-feed-items-collapse-empty">
|
||||
Preview pulled feed items
|
||||
</button>
|
||||
</h2>
|
||||
<div id="rss-feed-items-collapse-empty" class="accordion-collapse collapse" aria-labelledby="rss-feed-items-heading-empty" data-bs-parent="#rss-feed-items-accordion">
|
||||
<div class="accordion-body">
|
||||
<div class="text-muted">No feed items have been pulled yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+19
-31
@@ -1,31 +1,31 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{#if isEdit}}Edit schedule group{{else}}Add schedule group{{/if}}</h2>
|
||||
<h2>{{#if isEdit}}Edit timetable group{{else}}Add timetable group{{/if}}</h2>
|
||||
<p>Define a group of events with their start and end times.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Schedule group details</h3>
|
||||
<h3 class="card-title">Timetable group details</h3>
|
||||
</div>
|
||||
<form id="schedule-group-form" method="post" action="{{#if isEdit}}/data-sources/schedules/{{scheduleGroup.id}}{{else}}/data-sources/schedules{{/if}}" data-async-save data-async-save-close-url="/data-sources/schedules" data-async-save-new-url="/data-sources/schedules/new">
|
||||
<form id="timetable-group-form" method="post" action="{{#if isEdit}}/data-sources/timetables/{{timetableGroup.id}}{{else}}/data-sources/timetables{{/if}}" data-async-save data-async-save-close-url="/data-sources/timetables"{{#unless isEdit}} data-async-save-new-redirect="response-url"{{/unless}} data-async-save-new-url="/data-sources/timetables/new">
|
||||
<div class="card-body d-grid gap-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-4">
|
||||
<label for="schedule-group-name" class="form-label">Name</label>
|
||||
<input id="schedule-group-name" name="name" class="form-control" value="{{scheduleGroup.name}}" required />
|
||||
<label for="timetable-group-name" class="form-label">Name</label>
|
||||
<input id="timetable-group-name" name="name" class="form-control" value="{{timetableGroup.name}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-lg-8">
|
||||
<label for="schedule-group-short-description" class="form-label">Short description</label>
|
||||
<input id="schedule-group-short-description" name="short_description" class="form-control" value="{{scheduleGroup.shortDescription}}" placeholder="Morning events" />
|
||||
<label for="timetable-group-short-description" class="form-label">Short description</label>
|
||||
<input id="timetable-group-short-description" name="short_description" class="form-control" value="{{timetableGroup.shortDescription}}" placeholder="Morning events" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-2">
|
||||
<h4 class="schedule-section-heading mb-0">Entries</h4>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-add-schedule-entry>Add entry</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-add-timetable-entry>Add entry</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle schedule-entries-table mb-0">
|
||||
@@ -38,9 +38,9 @@
|
||||
<th style="width: 6rem;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-schedule-entries-body>
|
||||
{{#each scheduleEntries}}
|
||||
<tr data-schedule-entry-row>
|
||||
<tbody data-timetable-entries-body>
|
||||
{{#each timetableEntries}}
|
||||
<tr data-timetable-entry-row>
|
||||
<td>
|
||||
<input type="hidden" name="entry_id[]" value="{{id}}" />
|
||||
<input type="text" name="entry_title[]" class="form-control" value="{{title}}" required />
|
||||
@@ -55,36 +55,24 @@
|
||||
<input type="datetime-local" name="entry_end_datetime[]" class="form-control" value="{{endValue}}" />
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-schedule-entry>Remove</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-timetable-entry>Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="form-text mb-0">Add one or more rows. A schedule region can render upcoming or current entries from this group.</p>
|
||||
<p class="form-text mb-0">Add one or more rows. A timetable region can render upcoming or current entries from this group.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Schedule group actions">
|
||||
{{{saveActionButtons formId="schedule-group-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/data-sources/schedules" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if isEdit}}
|
||||
{{#if inUse}}
|
||||
<button type="submit" class="btn btn-outline-danger" form="delete-schedule-group-form" disabled>Delete</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-danger" form="delete-schedule-group-form">Delete</button>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{{saveActionButtons formId="timetable-group-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<template id="schedule-entry-row-template">
|
||||
<tr data-schedule-entry-row>
|
||||
<template id="timetable-entry-row-template">
|
||||
<tr data-timetable-entry-row>
|
||||
<td>
|
||||
<input type="hidden" name="entry_id[]" value="" />
|
||||
<input type="text" name="entry_title[]" class="form-control" value="" required />
|
||||
@@ -99,13 +87,13 @@
|
||||
<input type="datetime-local" name="entry_end_datetime[]" class="form-control" value="" />
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-schedule-entry>Remove</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-timetable-entry>Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
{{#if isEdit}}
|
||||
<form id="delete-schedule-group-form" method="post" action="/data-sources/schedules/{{scheduleGroup.id}}/delete" data-confirm-message="Delete this schedule group?"></form>
|
||||
<form id="delete-timetable-group-form" method="post" action="/data-sources/timetables/{{timetableGroup.id}}/delete" data-confirm-message="Delete this timetable group?"></form>
|
||||
{{/if}}
|
||||
|
||||
<script type="module" src="/assets/js/data-sources/schedule-group-form.js?v={{assetVersion}}"></script>
|
||||
<script type="module" src="/assets/js/data-sources/timetable-group-form.js?v={{assetVersion}}"></script>
|
||||
+14
-14
@@ -1,7 +1,7 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Schedules</h2>
|
||||
<p>Store grouped events with start and end times so a schedule region can show what is coming up next.</p>
|
||||
<h2>Timetables</h2>
|
||||
<p>Store grouped events with start and end times so a timetable region can show what is coming up next.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
|
||||
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
|
||||
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
|
||||
<input type="search" class="form-control" placeholder="Search schedules" aria-label="Search schedules" data-table-search />
|
||||
<input type="search" class="form-control" placeholder="Search timetables" aria-label="Search timetables" data-table-search />
|
||||
</div>
|
||||
{{#if (hasPermission currentUser 'schedules.create')}}
|
||||
<a class="btn btn-primary btn-sm" href="/data-sources/schedules/new">Add schedule group</a>
|
||||
{{#if (hasPermission currentUser 'timetables.create')}}
|
||||
<a class="btn btn-primary btn-sm" href="/data-sources/timetables/new">Add timetable group</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,8 +30,8 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if scheduleGroups.length}}
|
||||
{{#each scheduleGroups}}
|
||||
{{#if timetableGroups.length}}
|
||||
{{#each timetableGroups}}
|
||||
<tr data-table-search-row>
|
||||
<td data-label="Name">{{name}}</td>
|
||||
<td data-label="Description" class="text-break">{{short_description}}</td>
|
||||
@@ -44,13 +44,13 @@
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
{{#if (anyPermission ../currentUser 'schedules.update' 'schedules.delete')}}
|
||||
{{#if (anyPermission ../currentUser 'timetables.update' 'timetables.delete')}}
|
||||
<div class="actions">
|
||||
{{#if (hasPermission ../currentUser 'schedules.update')}}
|
||||
<a class="btn btn-sm btn-primary" href="/data-sources/schedules/{{id}}/edit">Edit</a>
|
||||
{{#if (hasPermission ../currentUser 'timetables.update')}}
|
||||
<a class="btn btn-sm btn-primary" href="/data-sources/timetables/{{id}}/edit">Edit</a>
|
||||
{{/if}}
|
||||
{{#if (hasPermission ../currentUser 'schedules.delete')}}
|
||||
<form class="inline-form" method="post" action="/data-sources/schedules/{{id}}/delete" data-confirm-message="Delete this schedule group?">
|
||||
{{#if (hasPermission ../currentUser 'timetables.delete')}}
|
||||
<form class="inline-form" method="post" action="/data-sources/timetables/{{id}}/delete" data-confirm-message="Delete this timetable group?">
|
||||
{{#if inUse}}
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit" disabled>Delete</button>
|
||||
{{else}}
|
||||
@@ -66,10 +66,10 @@
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr data-table-search-empty-default><td colspan="5" class="empty">No schedule groups yet.</td></tr>
|
||||
<tr data-table-search-empty-default><td colspan="5" class="empty">No timetable groups yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{> table-pagination pagination=pagination basePath="/data-sources/schedules" alwaysShow=true}}
|
||||
{{> table-pagination pagination=pagination basePath="/data-sources/timetables" alwaysShow=true}}
|
||||
</div>
|
||||
@@ -1,82 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Create role</h2>
|
||||
<p>Define a new role and choose its permissions before saving.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/rbac" id="role-create-form" data-async-save data-async-save-close-url="/rbac">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-4 d-flex flex-column gap-4">
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Role details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="role-name" class="form-label">Role name</label>
|
||||
<input id="role-name" name="name" class="form-control" value="{{formValues.name}}" required />
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="role-description" class="form-label">Description</label>
|
||||
<textarea id="role-description" name="description" class="form-control" rows="5">{{formValues.description}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-8 d-flex flex-column gap-4">
|
||||
<div class="card card-outline card-secondary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Permissions</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-body-secondary mb-3">Pick the permissions this role should have now.</p>
|
||||
{{#if permissionGroups.length}}
|
||||
<div class="accordion" id="role-create-permissions-accordion">
|
||||
{{#each permissionGroups}}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="create-heading-{{id}}">
|
||||
<button class="accordion-button {{#unless @first}}collapsed{{/unless}}" type="button" data-bs-toggle="collapse" data-bs-target="#create-collapse-{{id}}" aria-expanded="{{#if @first}}true{{else}}false{{/if}}" aria-controls="create-collapse-{{id}}">
|
||||
<span>{{title}}</span>
|
||||
{{#if categoryName}}
|
||||
<span class="badge text-bg-secondary ms-2">{{categoryName}}</span>
|
||||
{{/if}}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="create-collapse-{{id}}" class="accordion-collapse collapse {{#if @first}}show{{/if}}" aria-labelledby="create-heading-{{id}}" data-bs-parent="#role-create-permissions-accordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-3">
|
||||
{{#each permissions}}
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-check card card-outline card-secondary position-relative p-3 h-100 mb-0">
|
||||
<input class="form-check-input position-absolute top-0 end-0 m-3" type="checkbox" name="permission_keys[]" value="{{permission_key}}" data-permission-key="{{permission_key}}" {{#if isSelected}}checked{{/if}} />
|
||||
<span class="form-check-label pe-4">
|
||||
<strong>{{actionLabel}}</strong>
|
||||
<span class="d-block text-body-secondary small">{{#if description}}{{description}}{{else}}No description provided.{{/if}}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="alert alert-warning mb-0">No permissions are available.</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end mt-4">
|
||||
<div class="btn-group" role="group" aria-label="Role actions">
|
||||
{{{saveActionButtons formId="role-create-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/rbac" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,15 +1,14 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit role - {{role.name}}</h2>
|
||||
<p>Choose which permissions and users this role receives.</p>
|
||||
<h2>{{#if isEdit}}Edit role - {{role.name}}{{else}}Create role{{/if}}</h2>
|
||||
<p>{{#if isEdit}}Choose which permissions and users this role receives.{{else}}Define a new role and choose its permissions and users before saving.{{/if}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/rbac/{{role.id}}" id="role-edit-form" data-async-save data-async-save-close-url="/rbac">
|
||||
<input type="hidden" name="permissions_present" value="1" />
|
||||
<form id="{{formId}}" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
||||
<input type="hidden" name="users_present" value="1" />
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-xl-4 d-flex flex-column gap-4">
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
@@ -17,23 +16,17 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="role-name-edit" class="form-label">Role Name</label>
|
||||
<input id="role-name-edit" name="name" class="form-control" value="{{role.name}}" required />
|
||||
<label for="{{#if isEdit}}role-name-edit{{else}}role-name{{/if}}" class="form-label">Role name</label>
|
||||
<input id="{{#if isEdit}}role-name-edit{{else}}role-name{{/if}}" name="name" class="form-control" value="{{#if isEdit}}{{role.name}}{{else}}{{formValues.name}}{{/if}}" required />
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="role-description-edit" class="form-label">Description</label>
|
||||
<textarea id="role-description-edit" name="description" class="form-control" rows="5">{{role.description}}</textarea>
|
||||
<label for="{{#if isEdit}}role-description-edit{{else}}role-description{{/if}}" class="form-label">Description</label>
|
||||
<textarea id="{{#if isEdit}}role-description-edit{{else}}role-description{{/if}}" name="description" class="form-control" rows="5">{{#if isEdit}}{{role.description}}{{else}}{{formValues.description}}{{/if}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end flex-wrap">
|
||||
<div class="btn-group" role="group" aria-label="Role actions">
|
||||
{{{saveActionButtons formId="role-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/rbac" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if inUse}}
|
||||
<button type="submit" class="btn btn-outline-danger" form="role-delete-form" disabled>Delete</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-danger" form="role-delete-form">Delete</button>
|
||||
{{/if}}
|
||||
{{{saveActionButtons formId=formId saveUrl=footerSaveUrl cancelUrl=footerCancelUrl deleteUrl=footerDeleteUrl deleteDisabled=footerDeleteDisabled deleteTitle=footerDeleteTitle showSaveAndClose=false showSaveAndNew=false showDelete=footerShowDelete}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,7 +49,7 @@
|
||||
<th style="width: 1%" scope="col" data-sortable="false">Select</th>
|
||||
<th scope="col" data-table-sort-key="user">Username</th>
|
||||
<th scope="col" data-table-sort-key="name">Name</th>
|
||||
<th scope="col" data-table-sort-key="roles">Roles</th>
|
||||
{{!-- <th scope="col" data-table-sort-key="roles">Roles</th> --}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -72,7 +65,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Name">{{#if name}}{{name}}{{else}}-{{/if}}</td>
|
||||
<td data-label="Roles" class="text-body-secondary">{{#if roleNames}}{{roleNames}}{{else}}No roles assigned{{/if}}</td>
|
||||
{{!-- <td data-label="Roles" class="text-body-secondary">{{#if roleNames}}{{roleNames}}{{else}}No roles assigned{{/if}}</td> --}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
@@ -91,20 +84,20 @@
|
||||
<h3 class="card-title">Permissions</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-body-secondary mb-3">Each section below is a resource. Pick the actions this role should have for that resource.</p>
|
||||
<p class="text-body-secondary mb-3">{{#if isEdit}}Each section below is a resource. Pick the actions this role should have for that resource.{{else}}Each section below is a resource. Pick the actions this role should have for that resource.{{/if}}</p>
|
||||
{{#if permissionGroups.length}}
|
||||
<div class="accordion" id="role-permissions-accordion">
|
||||
<div class="accordion" id="{{#if isEdit}}role-permissions-accordion{{else}}role-create-permissions-accordion{{/if}}">
|
||||
{{#each permissionGroups}}
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-{{id}}">
|
||||
<button class="accordion-button {{#unless @first}}collapsed{{/unless}}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-{{id}}" aria-expanded="{{#if @first}}true{{else}}false{{/if}}" aria-controls="collapse-{{id}}">
|
||||
<h2 class="accordion-header" id="{{#if ../isEdit}}heading-{{id}}{{else}}create-heading-{{id}}{{/if}}">
|
||||
<button class="accordion-button {{#unless @first}}collapsed{{/unless}}" type="button" data-bs-toggle="collapse" data-bs-target="#{{#if ../isEdit}}collapse-{{id}}{{else}}create-collapse-{{id}}{{/if}}" aria-expanded="{{#if @first}}true{{else}}false{{/if}}" aria-controls="{{#if ../isEdit}}collapse-{{id}}{{else}}create-collapse-{{id}}{{/if}}">
|
||||
<span>{{title}}</span>
|
||||
{{#if categoryName}}
|
||||
<span class="badge text-bg-secondary ms-2">{{categoryName}}</span>
|
||||
{{/if}}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-{{id}}" class="accordion-collapse collapse {{#if @first}}show{{/if}}" aria-labelledby="heading-{{id}}" data-bs-parent="#role-permissions-accordion">
|
||||
<div id="{{#if ../isEdit}}collapse-{{id}}{{else}}create-collapse-{{id}}{{/if}}" class="accordion-collapse collapse {{#if @first}}show{{/if}}" aria-labelledby="{{#if ../isEdit}}heading-{{id}}{{else}}create-heading-{{id}}{{/if}}" data-bs-parent="#{{#if ../isEdit}}role-permissions-accordion{{else}}role-create-permissions-accordion{{/if}}">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-3">
|
||||
{{#each permissions}}
|
||||
@@ -128,14 +121,7 @@
|
||||
<div class="alert alert-warning mb-0">No permissions are available.</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Permission actions">
|
||||
{{{saveActionButtons formId="role-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/rbac/{{role.id}}/delete" id="role-delete-form" class="d-none" data-confirm-message="Delete {{role.name}}?"></form>
|
||||
</form>
|
||||
@@ -1,75 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Add user</h2>
|
||||
<p>Create a new login for the web UI.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="user-form" method="post" action="/users">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">User details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="user-username" class="form-label">Username</label>
|
||||
<input id="user-username" type="text" name="username" class="form-control" autocomplete="username" value="{{formValues.username}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="user-name" class="form-label">Name</label>
|
||||
<input id="user-name" type="text" name="name" class="form-control" autocomplete="name" value="{{formValues.name}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="user-password" class="form-label">Password</label>
|
||||
<input id="user-password" type="password" name="password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="user-confirm-password" class="form-label">Confirm password</label>
|
||||
<input id="user-confirm-password" type="password" name="confirm_password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="User actions">
|
||||
{{{saveActionButtons formId="user-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/users" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card card-outline card-secondary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Roles</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
{{#if roles.length}}
|
||||
{{#each roles}}
|
||||
<div class="col-4">
|
||||
<label class="form-check card card-outline card-secondary position-relative p-3 h-100 mb-0">
|
||||
<input class="form-check-input position-absolute top-0 end-0 m-3" type="checkbox" name="role_ids[]" value="{{id}}" {{#if isSelected}}checked{{/if}} />
|
||||
<span class="form-check-label pe-4">
|
||||
<strong>{{name}}</strong>
|
||||
<span class="d-block text-body-secondary small">{{#if description}}{{description}}{{else}}No description provided.{{/if}}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning mb-0">Create at least one role before adding users.</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-text mt-2">Select at least one role so the new user can sign in with access.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,102 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit user - {{user.name}}</h2>
|
||||
<p>Update the name or password for {{user.username}}.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card card-outline card-secondary admin-form-card mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Profile</h3>
|
||||
</div>
|
||||
<form id="user-profile-form" method="post" action="/users/{{user.id}}/username" data-async-save data-async-save-close-url="/users">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="edit-user-username" class="form-label">Username</label>
|
||||
<input id="edit-user-username" type="text" name="username" value="{{user.username}}" class="form-control" autocomplete="username" required disabled />
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="edit-user-name" class="form-label">Name</label>
|
||||
<input id="edit-user-name" type="text" name="name" value="{{user.name}}" class="form-control" autocomplete="name" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Profile actions">
|
||||
{{{saveActionButtons formId="user-profile-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/users" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if inUse}}
|
||||
<button type="submit" class="btn btn-outline-danger" form="delete-user-form" disabled>Delete</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-danger" form="delete-user-form">Delete</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card card-outline card-warning admin-form-card h-100 mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Reset password</h3>
|
||||
</div>
|
||||
<form id="user-password-form" method="post" action="/users/{{user.id}}/password" data-async-save data-async-save-close-url="/users">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="edit-user-password" class="form-label">New password</label>
|
||||
<input id="edit-user-password" type="password" name="password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="edit-user-confirm-password" class="form-label">Confirm password</label>
|
||||
<input id="edit-user-confirm-password" type="password" name="confirm_password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Password actions">
|
||||
{{{saveActionButtons formId="user-password-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card card-outline card-secondary admin-form-card h-100 mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Roles</h3>
|
||||
</div>
|
||||
<form id="user-roles-form" method="post" action="/users/{{user.id}}/roles" data-async-save data-async-save-close-url="/users">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
{{#if roles.length}}
|
||||
{{#each roles}}
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-check card card-outline card-secondary p-3 h-100 mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="role_ids[]" value="{{id}}" {{#if isSelected}}checked{{/if}} />
|
||||
<span class="form-check-label ms-2">
|
||||
<strong>{{name}}</strong>
|
||||
<span class="d-block text-body-secondary small">{{#if description}}{{description}}{{else}}No description provided.{{/if}}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning mb-0">Create at least one user before assigning this role.</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Role actions">
|
||||
{{{saveActionButtons formId="user-roles-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="delete-user-form" method="post" action="/users/{{user.id}}/delete" class="d-none" data-confirm-message="Delete {{user.username}}?"></form>
|
||||
@@ -0,0 +1,87 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{#if isEdit}}Edit user - {{user.name}}{{else}}Add user{{/if}}</h2>
|
||||
<p>{{#if isEdit}}Update the user's login details and roles.{{else}}Create a new login for the web UI.{{/if}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="{{formId}}" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{#if isEdit}}Profile{{else}}User details{{/if}}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="{{#if isEdit}}edit-user-username{{else}}user-username{{/if}}" class="form-label">Username</label>
|
||||
<input id="{{#if isEdit}}edit-user-username{{else}}user-username{{/if}}" type="text" name="username" class="form-control" autocomplete="username" value="{{#if isEdit}}{{user.username}}{{else}}{{formValues.username}}{{/if}}" {{#if isEdit}}disabled{{else}}required{{/if}} />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="{{#if isEdit}}edit-user-name{{else}}user-name{{/if}}" class="form-label">Name</label>
|
||||
<input id="{{#if isEdit}}edit-user-name{{else}}user-name{{/if}}" type="text" name="name" class="form-control" autocomplete="name" value="{{#if isEdit}}{{user.name}}{{else}}{{formValues.name}}{{/if}}" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="{{#if isEdit}}Profile actions{{else}}User actions{{/if}}">
|
||||
{{{saveActionButtons formId=formId saveUrl=footerSaveUrl cancelUrl=footerCancelUrl deleteUrl=footerDeleteUrl deleteDisabled=footerDeleteDisabled deleteTitle=footerDeleteTitle showSaveAndClose=false showSaveAndNew=false showDelete=footerShowDelete}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-warning admin-form-card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{#if isEdit}}Reset password{{else}}Password{{/if}}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="{{#if isEdit}}edit-user-password{{else}}user-password{{/if}}" class="form-label">Password</label>
|
||||
<input id="{{#if isEdit}}edit-user-password{{else}}user-password{{/if}}" type="password" name="password" class="form-control" autocomplete="new-password" minlength="8" {{#unless isEdit}}required{{/unless}} />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="{{#if isEdit}}edit-user-confirm-password{{else}}user-confirm-password{{/if}}" class="form-label">Confirm password</label>
|
||||
<input id="{{#if isEdit}}edit-user-confirm-password{{else}}user-confirm-password{{/if}}" type="password" name="confirm_password" class="form-control" autocomplete="new-password" minlength="8" {{#unless isEdit}}required{{/unless}} />
|
||||
</div>
|
||||
</div>
|
||||
{{#if isEdit}}
|
||||
<div class="form-text mt-2">Leave both fields blank to keep the current password.</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card card-outline card-secondary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Roles</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
{{#if roles.length}}
|
||||
{{#each roles}}
|
||||
<div class="col-12 col-md-6 col-xl-4">
|
||||
<label class="form-check card card-outline card-secondary position-relative p-3 h-100 mb-0">
|
||||
<input class="form-check-input position-absolute top-0 end-0 m-3" type="checkbox" name="role_ids[]" value="{{id}}" {{#if isSelected}}checked{{/if}} />
|
||||
<span class="form-check-label pe-4">
|
||||
<strong>{{name}}</strong>
|
||||
<span class="d-block text-body-secondary small">{{#if description}}{{description}}{{else}}No description provided.{{/if}}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning mb-0">{{#if isEdit}}Create at least one role before assigning this role.{{else}}Create at least one role before adding users.{{/if}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="form-text mt-2">{{#if isEdit}}Select at least one role so the user can sign in with access.{{else}}Select at least one role so the new user can sign in with access.{{/if}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<script src="/assets/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/assets/adminlte/bootstrap-icons/css/bootstrap-icons.min.css" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/css/adminlte.min.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-custom.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-custom.css?v={{appVersion}}" />
|
||||
{{#if stylesheets.length}}
|
||||
{{#each stylesheets}}
|
||||
<link rel="stylesheet" href="/assets/{{this}}" />
|
||||
|
||||
+110
-86
@@ -10,7 +10,7 @@
|
||||
<link rel="preload" href="/assets/adminlte/bootstrap-icons/fonts/bootstrap-icons.woff" as="font" type="font/woff" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/bootstrap-icons/css/bootstrap-icons.min.css" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/css/adminlte.min.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-custom.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-custom.css?v={{appVersion}}" />
|
||||
{{#if stylesheets.length}}
|
||||
{{#each stylesheets}}
|
||||
<link rel="stylesheet" href="{{assetHref this}}" />
|
||||
@@ -25,11 +25,35 @@
|
||||
<script src="/assets/js/admin/admin-page.js"></script>
|
||||
<script src="/assets/js/system-status.js"></script>
|
||||
{{else if authShell}}
|
||||
<div id="app-toast-container" class="toast-container app-toast-container position-fixed end-0 p-3">
|
||||
{{#if message}}
|
||||
<div class="toast shadow-lg border-0" id="app-toast" role="status" aria-live="polite" aria-atomic="true" data-bs-autohide="true" data-bs-delay="4000" data-toast-variant="{{#if messageVariant}}{{messageVariant}}{{else}}warning{{/if}}" data-toast-created-at="">
|
||||
<div class="toast-header border-0 text-bg-{{#if messageVariant}}{{messageVariant}}{{else}}warning{{/if}}">
|
||||
{{#if (eq messageVariant 'success')}}
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
{{else if (eq messageVariant 'warning')}}
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
{{else if (eq messageVariant 'danger')}}
|
||||
<i class="bi bi-x-circle-fill me-2"></i>
|
||||
{{else if (eq messageVariant 'primary')}}
|
||||
<i class="bi bi-bell-fill me-2"></i>
|
||||
{{else}}
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
{{/if}}
|
||||
<strong class="me-auto">Pulse</strong>
|
||||
<small class="toast-time text-nowrap">just now</small>
|
||||
<button type="button" class="btn-close btn-close-white ms-2" data-bs-dismiss="toast" aria-label="Dismiss notification"></button>
|
||||
</div>
|
||||
<div class="toast-body">{{message}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<main class="login-page d-flex align-items-center justify-content-center min-vh-100 p-3">
|
||||
<div class="login-box">
|
||||
{{{body}}}
|
||||
</div>
|
||||
</main>
|
||||
<script src="/assets/js/toast.js"></script>
|
||||
<script src="/assets/js/admin/admin-page.js"></script>
|
||||
<script src="/assets/js/system-status.js"></script>
|
||||
{{else}}
|
||||
@@ -167,14 +191,6 @@
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'announcements.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'announcements')}}active{{/if}}" href="/announcements">
|
||||
<i class="nav-icon bi bi-megaphone"></i>
|
||||
<p>Announcements</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'screens.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'screens')}}active{{/if}}" href="/screens">
|
||||
@@ -215,88 +231,96 @@
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'announcements.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'announcements')}}active{{/if}}" href="/announcements">
|
||||
<i class="nav-icon bi bi-megaphone"></i>
|
||||
<p>Announcements</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (anyPermission currentUser 'rss-feeds.read' 'schedules.read' 'api-sources.read')}}
|
||||
<li class="nav-header">DATA SOURCES</li>
|
||||
{{#if (hasPermission currentUser 'rss-feeds.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'rss-feeds')}}active{{/if}}" href="/data-sources/rss-feeds">
|
||||
<i class="nav-icon bi bi-rss"></i>
|
||||
<p>RSS feeds</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'schedules.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'schedules')}}active{{/if}}" href="/data-sources/schedules">
|
||||
<i class="nav-icon bi bi-calendar-event"></i>
|
||||
<p>Schedules</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'api-sources.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'api-sources')}}active{{/if}}" href="/data-sources/api-sources">
|
||||
<i class="nav-icon bi bi-braces"></i>
|
||||
<p>API sources</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'rss-feeds.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'rss-feeds')}}active{{/if}}" href="/data-sources/rss-feeds">
|
||||
<i class="nav-icon bi bi-rss"></i>
|
||||
<p>RSS feeds</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'api-sources.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'api-sources')}}active{{/if}}" href="/data-sources/api-sources">
|
||||
<i class="nav-icon bi bi-braces"></i>
|
||||
<p>API sources</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'timetables.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'timetables')}}active{{/if}}" href="/data-sources/timetables">
|
||||
<i class="nav-icon bi bi-calendar-event"></i>
|
||||
<p>Timetables</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (anyPermission currentUser 'background-tasks.read' 'scheduled-tasks.read' 'users.read' 'rbac.read' 'fonts.read')}}
|
||||
<li class="nav-header">SETTINGS</li>
|
||||
{{#if (hasPermission currentUser 'users.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'users')}}active{{/if}}" href="/users">
|
||||
<i class="nav-icon bi bi-people"></i>
|
||||
<p>Users</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'rbac.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'rbac')}}active{{/if}}" href="/rbac">
|
||||
<i class="nav-icon bi bi-shield-lock"></i>
|
||||
<p>Roles and permissions</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'fonts.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'fonts')}}active{{/if}}" href="/settings/fonts">
|
||||
<i class="nav-icon bi bi-type"></i>
|
||||
<p>Fonts</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (anyPermission currentUser 'background-tasks.read' 'scheduled-tasks.read')}}
|
||||
<li class="nav-item has-treeview{{#if backgroundTasksMenuOpen}} menu-open{{/if}}">
|
||||
<a class="nav-link{{#if backgroundTasksMenuOpen}} active{{/if}}" href="#">
|
||||
<i class="nav-icon bi bi-pen"></i>
|
||||
<p>
|
||||
Tasks
|
||||
<i class="nav-arrow bi bi-chevron-right"></i>
|
||||
</p>
|
||||
</a>
|
||||
{{#if (anyPermission currentUser 'background-tasks.read')}}
|
||||
<ul class="nav nav-treeview">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'background-tasks')}}active{{/if}}" href="/settings/tasks-background">
|
||||
<i class="nav-icon bi bi-list-ol"></i>
|
||||
<p>Task queue</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'scheduled-tasks.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'background-tasks-scheduled')}}active{{/if}}" href="/settings/tasks-scheduled">
|
||||
<i class="nav-icon bi bi-calendar-event"></i>
|
||||
<p>Scheduled tasks</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'users.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'users')}}active{{/if}}" href="/users">
|
||||
<i class="nav-icon bi bi-people"></i>
|
||||
<p>Users</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'rbac.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'rbac')}}active{{/if}}" href="/rbac">
|
||||
<i class="nav-icon bi bi-shield-lock"></i>
|
||||
<p>Roles and permissions</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'fonts.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'fonts')}}active{{/if}}" href="/settings/fonts">
|
||||
<i class="nav-icon bi bi-type"></i>
|
||||
<p>Fonts</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (anyPermission currentUser 'background-tasks.read' 'scheduled-tasks.read')}}
|
||||
<li class="nav-item has-treeview{{#if backgroundTasksMenuOpen}} menu-open{{/if}}">
|
||||
<a class="nav-link{{#if backgroundTasksMenuOpen}} active{{/if}}" href="#">
|
||||
<i class="nav-icon bi bi-pen"></i>
|
||||
<p>
|
||||
Tasks
|
||||
<i class="nav-arrow bi bi-chevron-right"></i>
|
||||
</p>
|
||||
</a>
|
||||
{{#if (anyPermission currentUser 'background-tasks.read')}}
|
||||
<ul class="nav nav-treeview">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'background-tasks')}}active{{/if}}" href="/settings/tasks-background">
|
||||
<i class="nav-icon bi bi-list-ol"></i>
|
||||
<p>Task queue</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'scheduled-tasks.read')}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'background-tasks-scheduled')}}active{{/if}}" href="/settings/tasks-scheduled">
|
||||
<i class="nav-icon bi bi-calendar-event"></i>
|
||||
<p>Scheduled tasks</p>
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit announcement{{#if announcement.short_label}} - {{announcement.short_label}}{{/if}}</h2>
|
||||
<p>Update the message, screens, color, or expiry behavior.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="announcement-form" method="post" action="/announcements/{{announcement.id}}" data-async-save data-async-save-close-url="/announcements" data-async-save-new-url="/announcements/new">
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card card-outline card-primary admin-form-card h-100 mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Content</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-4">
|
||||
<label for="announcement-short-label" class="form-label">Descriptor</label>
|
||||
<input id="announcement-short-label" name="short_label" class="form-control" value="{{announcement.short_label}}" maxlength="255" placeholder="Power warning" />
|
||||
<div class="form-text">A short name for admin lists and page titles.</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="announcement-message" class="form-label">Message</label>
|
||||
<textarea id="announcement-message" name="message" class="form-control" rows="6" required>{{announcement.message}}</textarea>
|
||||
<div class="form-text">Use the full text that should appear on screens.</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-6 col-xl-12">
|
||||
<label for="announcement-type" class="form-label">Type</label>
|
||||
<select id="announcement-type" name="announcement_type" class="form-select">
|
||||
{{#each announcementTypes}}
|
||||
<option value="{{value}}" {{#if (eq ../announcement.announcement_type value)}}selected{{/if}}>{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-12">
|
||||
<label for="announcement-color" class="form-label">Color</label>
|
||||
<div class="row g-2 align-items-stretch">
|
||||
<div class="col">
|
||||
<select id="announcement-color" name="color_key" class="form-select">
|
||||
{{#each announcementColors}}
|
||||
<option value="{{value}}" {{#if (eq ../announcement.color_key value)}}selected{{/if}}>{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="announcement-color-preview" data-announcement-color-preview aria-hidden="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-12">
|
||||
<div class="form-label">Icon</div>
|
||||
<div class="announcement-icon-picker" data-announcement-icon-picker-shell>
|
||||
<select id="announcement-icon" name="icon_key" class="d-none" tabindex="-1" aria-hidden="true">
|
||||
{{#each announcementIcons}}
|
||||
<option value="{{value}}" {{#if (eq ../announcement.icon_key value)}}selected{{/if}}>{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<button type="button" class="announcement-icon-picker__toggle" data-announcement-icon-picker-toggle aria-expanded="false" aria-controls="announcement-icon-picker-menu">
|
||||
<span class="announcement-icon-picker__toggle-icon" data-announcement-icon-picker-icon aria-hidden="true"></span>
|
||||
<span class="announcement-icon-picker__toggle-label" data-announcement-icon-picker-label>Select an icon</span>
|
||||
<i class="bi bi-chevron-down announcement-icon-picker__caret" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="announcement-icon-picker-menu" class="announcement-icon-picker__menu" data-announcement-icon-picker hidden>
|
||||
<div class="announcement-icon-picker__header">
|
||||
<span>Choose an icon</span>
|
||||
<button type="button" class="btn btn-sm btn-link text-decoration-none p-0" data-announcement-icon-picker-close>Close</button>
|
||||
</div>
|
||||
<div class="announcement-icon-picker__grid">
|
||||
{{#each announcementIcons}}
|
||||
<button type="button" class="announcement-icon-picker__option" data-announcement-icon-option data-icon-key="{{value}}" aria-pressed="false" title="{{label}}">
|
||||
<i class="bi bi-{{value}}" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{label}}</span>
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="card card-outline card-primary admin-form-card mb-3">
|
||||
<div class="card-footer d-flex flex-wrap align-items-center gap-2">
|
||||
<div class="btn-group" role="group" aria-label="Announcement play actions">
|
||||
<button type="submit" class="btn {{announcement.actionClassName}}" form="announcement-action-form"><i class="bi {{announcement.actionIcon}} me-1" aria-hidden="true"></i>{{announcement.actionLabel}}</button>
|
||||
</div>
|
||||
<div class="btn-group ms-auto" role="group" aria-label="Announcement save actions">
|
||||
{{{saveActionButtons formId="announcement-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/announcements" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if (hasPermission currentUser 'announcements.delete')}}
|
||||
{{#if announcement.id}}
|
||||
<button type="submit" class="btn {{#if announcement.canDelete}}btn-danger{{else}}btn-outline-danger{{/if}}" form="delete-announcement-form" {{#unless announcement.canDelete}}disabled title="{{announcement.deleteBlockedMessage}}"{{/unless}}>Delete</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-outline card-secondary admin-form-card mb-4">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Screens</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-0">
|
||||
<div class="col-12 col-md-4 col-xl-12">
|
||||
<label for="announcement-duration-mode" class="form-label">Display mode</label>
|
||||
<select id="announcement-duration-mode" name="duration_mode" class="form-select">
|
||||
<option value="duration" {{#if (eq announcement.durationMode 'duration')}}selected{{/if}}>For a duration</option>
|
||||
<option value="until_disabled" {{#if (eq announcement.durationMode 'until_disabled')}}selected{{/if}}>Until disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12" data-announcement-duration-value-fields {{#unless (eq announcement.durationMode 'duration')}}hidden{{/unless}}>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4 col-xl-6">
|
||||
<label for="announcement-duration-value" class="form-label">Duration value</label>
|
||||
<input id="announcement-duration-value" name="duration_value" type="number" min="1" class="form-control" value="{{announcement.durationValue}}" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-6">
|
||||
<label for="announcement-duration-unit" class="form-label">Unit</label>
|
||||
<select id="announcement-duration-unit" name="duration_unit" class="form-select">
|
||||
<option value="seconds" {{#if (eq announcement.durationUnit 'seconds')}}selected{{/if}}>Seconds</option>
|
||||
<option value="minutes" {{#if (eq announcement.durationUnit 'minutes')}}selected{{/if}}>Minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label for="announcement-screens" class="form-label">Target screens</label>
|
||||
<select id="announcement-screens" name="screen_ids[]" class="form-select" multiple size="8" required>
|
||||
{{#each announcementScreens}}
|
||||
<option value="{{id}}" {{#if isSelected}}selected{{/if}}>{{name}}{{#if slug}} ({{slug}}){{/if}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<div class="form-text">Select one or more screens for this announcement.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/assets/js/announcements/announcement-form.js?v={{appVersion}}"></script>
|
||||
</form>
|
||||
|
||||
<form id="announcement-action-form" method="post" action="{{announcement.actionPath}}" class="d-none" data-confirm-message="{{announcement.actionConfirmMessage}}" data-async-command></form>
|
||||
<form id="delete-announcement-form" method="post" action="/announcements/{{announcement.id}}/delete" class="d-none" data-confirm-message="Delete this announcement?"></form>
|
||||
+18
-12
@@ -1,11 +1,11 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Add announcement</h2>
|
||||
<p>Give the announcement a short description, then define the full message and target screens.</p>
|
||||
<h2>{{#if isEdit}}Edit announcement{{#if announcement.short_label}} - {{announcement.short_label}}{{/if}}{{else}}Add announcement{{/if}}</h2>
|
||||
<p>{{#if isEdit}}Update the message, screens, color, or expiry behavior.{{else}}Give the announcement a short description, then define the full message and target screens.{{/if}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="announcement-form" method="post" action="/announcements" data-async-save data-async-save-close-url="/announcements" data-async-save-new-url="/announcements/new">
|
||||
<form id="announcement-form" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card card-outline card-primary admin-form-card h-100 mb-0">
|
||||
@@ -14,13 +14,13 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-4">
|
||||
<label for="announcement-short-label" class="form-label">Short Description</label>
|
||||
<input id="announcement-short-label" name="short_label" class="form-control" value="{{announcement.short_label}}" maxlength="255" placeholder="Announcement Short Name" />
|
||||
<label for="announcement-short-label" class="form-label">{{#if isEdit}}Descriptor{{else}}Short Description{{/if}}</label>
|
||||
<input id="announcement-short-label" name="short_label" class="form-control" value="{{announcement.short_label}}" maxlength="255" placeholder="{{#if isEdit}}Power warning{{else}}Announcement Short Name{{/if}}" />
|
||||
<div class="form-text">A short name for admin lists and page titles.</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="announcement-message" class="form-label">Message</label>
|
||||
<textarea id="announcement-message" name="message" class="form-control" rows="6" required placeholder="Maintenance starts in 10 minutes.">{{announcement.message}}</textarea>
|
||||
<textarea id="announcement-message" name="message" class="form-control" rows="6" required placeholder="{{#if isEdit}}{{else}}Maintenance starts in 10 minutes.{{/if}}">{{announcement.message}}</textarea>
|
||||
<div class="form-text">Use the full text that should appear on screens.</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-0">
|
||||
@@ -82,11 +82,12 @@
|
||||
</div>
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="card card-outline card-primary admin-form-card mb-3">
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Announcement actions">
|
||||
{{{saveActionButtons formId="announcement-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/announcements" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
<div class="card-footer d-flex flex-wrap align-items-center">
|
||||
{{#if isEdit}}
|
||||
<button type="submit" class="btn {{announcement.actionClassName}}" form="{{actionFormId}}"><i class="bi {{announcement.actionIcon}} me-1" aria-hidden="true"></i>{{announcement.actionLabel}}</button>
|
||||
{{/if}}
|
||||
<div class="ms-auto d-flex flex-wrap align-items-center gap-2">
|
||||
{{{saveActionButtons formId="announcement-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled deleteTitle=announcement.deleteBlockedMessage showDelete=showDeleteAction showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +96,7 @@
|
||||
<h3 class="card-title">Screens</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="row g-3 mb-0">
|
||||
<div class="col-12 col-md-4 col-xl-12">
|
||||
<label for="announcement-duration-mode" class="form-label">Display mode</label>
|
||||
<select id="announcement-duration-mode" name="duration_mode" class="form-select">
|
||||
@@ -132,3 +133,8 @@
|
||||
</div>
|
||||
<script type="module" src="/assets/js/announcements/announcement-form.js?v={{appVersion}}"></script>
|
||||
</form>
|
||||
|
||||
{{#if isEdit}}
|
||||
<form id="announcement-action-form" method="post" action="{{announcement.actionPath}}" class="d-none" data-confirm-message="{{announcement.actionConfirmMessage}}" data-async-command></form>
|
||||
<form id="delete-announcement-form" method="post" action="/announcements/{{announcement.id}}/delete" class="d-none" data-confirm-message="Delete this announcement?"></form>
|
||||
{{/if}}
|
||||
@@ -1,44 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit canvas - {{canvasSize.name}}</h2>
|
||||
<p>Update the stored canvas dimensions.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Canvas size details</h3>
|
||||
</div>
|
||||
<form id="canvas-size-form" method="post" action="/canvas-sizes/{{canvasSize.id}}" data-async-save data-async-save-close-url="/canvas-sizes" data-async-save-new-url="/canvas-sizes/new">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="canvas-size-name" class="form-label">Name</label>
|
||||
<input id="canvas-size-name" name="name" class="form-control" value="{{canvasSize.name}}" required />
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="canvas-size-width" class="form-label">Width</label>
|
||||
<input id="canvas-size-width" name="width" type="number" min="1" class="form-control" value="{{canvasSize.width}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="canvas-size-height" class="form-label">Height</label>
|
||||
<input id="canvas-size-height" name="height" type="number" min="1" class="form-control" value="{{canvasSize.height}}" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Canvas size actions">
|
||||
{{{saveActionButtons formId="canvas-size-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/canvas-sizes" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if inUse}}
|
||||
<button type="submit" class="btn btn-outline-danger" form="delete-canvas-size-form" disabled>Delete</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-danger" form="delete-canvas-size-form">Delete</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form id="delete-canvas-size-form" method="post" action="/canvas-sizes/{{canvasSize.id}}/delete" data-confirm-message="Delete this canvas size?"></form>
|
||||
|
||||
+10
-15
@@ -1,15 +1,15 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Create canvas size</h2>
|
||||
<p>Add a reusable canvas dimension for templates.</p>
|
||||
<h2>{{#if isEdit}}Edit canvas - {{canvasSize.name}}{{else}}Create canvas size{{/if}}</h2>
|
||||
<p>{{#if isEdit}}Update the stored canvas dimensions.{{else}}Add a reusable canvas dimension for templates.{{/if}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Canvas size details</h3>
|
||||
</div>
|
||||
<form id="canvas-size-form" method="post" action="/canvas-sizes">
|
||||
<form id="canvas-size-form" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Canvas size details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="canvas-size-name" class="form-label">Name</label>
|
||||
@@ -27,12 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Canvas size actions">
|
||||
{{{saveActionButtons formId="canvas-size-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/canvas-sizes" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
</div>
|
||||
{{{saveActionButtons formId="canvas-size-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteConfirmMessage=deleteConfirmMessage deleteDisabled=deleteDisabled}}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
@@ -5,6 +5,70 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (hasPermission currentUser 'clients.allow')}}
|
||||
<div class="card card-outline card-secondary mb-4 screen-command-card">
|
||||
<div class="card-header">
|
||||
<div class="dashboard-card-heading">
|
||||
<h3 class="card-title">Screen-level controls</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{#if screens.length}}
|
||||
<div class="screen-command-panel">
|
||||
<div class="screen-command-panel-left">
|
||||
<div class="screen-command-selector-field">
|
||||
<label class="screen-command-selector-label" for="screen-command-select">Target screen</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text" aria-hidden="true"><i class="bi bi-broadcast"></i></span>
|
||||
<select
|
||||
id="screen-command-select"
|
||||
class="form-select"
|
||||
aria-label="Target screen"
|
||||
data-screen-command-select
|
||||
>
|
||||
{{#each screens}}
|
||||
<option
|
||||
value="{{slug}}"
|
||||
data-screen-name="{{name}}"
|
||||
data-player-connection-count="{{player_connection_count}}"
|
||||
data-playlist-name="{{playlist_name}}"
|
||||
>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="screen-command-actions">
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Reload selected screen?" data-async-command data-screen-command-form data-screen-command-action="reload">
|
||||
<input type="hidden" name="command" value="reload" />
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen</button>
|
||||
</form>
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Pause selected screen?" data-async-command data-screen-command-form data-screen-command-action="pause">
|
||||
<input type="hidden" name="command" value="pause" />
|
||||
<input type="hidden" name="paused" value="true" />
|
||||
<button type="submit" class="btn btn-sm btn-info"><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>Pause screen</button>
|
||||
</form>
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Blackout selected screen?" data-async-command data-screen-command-form data-screen-command-action="blackout">
|
||||
<input type="hidden" name="command" value="blackout" />
|
||||
<input type="hidden" name="blackout" value="true" />
|
||||
<button type="submit" class="btn btn-sm btn-secondary"><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout screen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="screen-command-panel-right">
|
||||
<div class="screen-command-summary">
|
||||
<span class="screen-command-pill is-live" data-screen-command-pill>Choose a screen</span>
|
||||
<strong data-screen-command-name>Select a target screen</strong>
|
||||
<span class="screen-command-summary-meta" data-screen-command-meta>Commands update automatically for the selected screen.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty">No screens are available to target yet.</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="card card-outline card-primary" data-table-search-container data-table-pagination-card>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Active connections</h3>
|
||||
@@ -69,31 +133,28 @@
|
||||
<form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-confirm-message="Reloading will restart the player page. Continue?" data-async-command>
|
||||
<input type="hidden" name="command" value="reload" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm btn-danger">Reload</button>
|
||||
<button type="submit" class="btn btn-sm btn-danger" aria-label="Reload client" title="Reload client"><i class="bi bi-arrow-repeat" aria-hidden="true"></i></button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-sm btn-danger" data-action="move-screen" data-bs-toggle="modal" data-bs-target="#client-move-screen-modal" aria-label="Move client to another screen" title="Move client to another screen"><i class="bi bi-display" aria-hidden="true"></i></button>
|
||||
<form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="previous" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm btn-warning" aria-label="Previous slide">◀</button>
|
||||
<button type="submit" class="btn btn-sm btn-warning" aria-label="Previous slide" title="Previous slide"><i class="bi bi-skip-backward-fill" aria-hidden="true"></i></button>
|
||||
</form>
|
||||
<form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="next" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm btn-warning" aria-label="Next slide">▶</button>
|
||||
<button type="submit" class="btn btn-sm btn-warning" aria-label="Next slide" title="Next slide"><i class="bi bi-skip-forward-fill" aria-hidden="true"></i></button>
|
||||
</form>
|
||||
<form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="pause" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm btn-info">
|
||||
<i class="bi bi-pause-fill me-1" aria-hidden="false"></i>Pause
|
||||
</button>
|
||||
<button type="submit" class="btn btn-sm btn-info" aria-label="Pause client" title="Pause client"><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>Pause</button>
|
||||
</form>
|
||||
<form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="blackout" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm {{#if blackout}}btn-success{{else}}btn-secondary{{/if}}">
|
||||
<i class="bi bi-eye-slash me-1" aria-hidden="false"></i>Blackout
|
||||
</button>
|
||||
<button type="submit" class="btn btn-sm {{#if blackout}}btn-success{{else}}btn-secondary{{/if}}" aria-label="{{#if blackout}}Restore client{{else}}Blackout client{{/if}}" title="{{#if blackout}}Restore client{{else}}Blackout client{{/if}}"><i class="bi {{#if blackout}}bi-eye{{else}}bi-eye-slash{{/if}} me-1" aria-hidden="true"></i>{{#if blackout}}Restore{{else}}Blackout{{/if}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
@@ -108,3 +169,35 @@
|
||||
</div>
|
||||
{{> table-pagination pagination=pagination basePath="/clients" alwaysShow=true}}
|
||||
</div>
|
||||
|
||||
{{#> modal-shell modalId="client-move-screen-modal" modalLabelId="client-move-screen-modal-label" modalDialogClass="modal-dialog-centered modal-dialog-scrollable modal-lg" modalBackdropStatic=true modalKeyboardDisabled=true}}
|
||||
<form id="client-move-screen-form" method="post" action="#" class="d-flex flex-column">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 class="modal-title fs-5" id="client-move-screen-modal-label">Move client to another screen</h2>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="command" value="moveclient" />
|
||||
<input type="hidden" name="connectionId" value="" data-client-move-connection-id />
|
||||
<input type="hidden" name="deviceId" value="" data-client-move-device-id />
|
||||
<input type="hidden" name="clientName" value="" data-client-move-client-name />
|
||||
<label class="form-label" for="client-move-screen-target">Target screen</label>
|
||||
<select class="form-select" id="client-move-screen-target" name="targetScreenSlug" data-client-move-target required>
|
||||
{{#if screens.length}}
|
||||
<option value="">Select a screen</option>
|
||||
{{#each screens}}
|
||||
<option value="{{slug}}">{{name}}</option>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<option value="">No screens available</option>
|
||||
{{/if}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-primary">Move client</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
{{/modal-shell}}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Create playlist</h2>
|
||||
<p>Name the playlist and start adding slides.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Playlist details</h3>
|
||||
</div>
|
||||
<form id="playlist-edit-form" method="post" action="/playlists" data-async-save data-async-save-new-url="/playlists/new">
|
||||
<div class="card-body">
|
||||
<div class="mb-0">
|
||||
<label for="playlist-name" class="form-label">Name</label>
|
||||
<input id="playlist-name" name="name" class="form-control" placeholder="Lobby loop" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Playlist actions">
|
||||
{{{saveActionButtons formId="playlist-edit-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/playlists" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit playlist - {{playlist.name}}</h2>
|
||||
<p>Adjust the playlist details, slide order, and scheduling.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card card-outline card-primary mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Playlist details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="playlist-edit-form" method="post" action="/playlists/{{playlist.id}}" data-async-save data-async-save-reload-on-success data-async-save-close-url="/playlists" data-async-save-new-url="/playlists/new">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-12 col-lg-8">
|
||||
<label for="playlist-name" class="form-label">Name</label>
|
||||
<input id="playlist-name" name="name" class="form-control" value="{{playlist.name}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 d-flex align-items-lg-end justify-content-lg-start">
|
||||
<div class="d-flex flex-column align-items-lg-start gap-1 w-100">
|
||||
<label class="form-label d-none d-lg-block invisible">Add slide</label>
|
||||
{{#if hasSlides}}
|
||||
{{#if hasSelectableSlides}}
|
||||
<button type="button" class="btn btn-primary w-100" id="playlist-open-slide-modal" data-bs-toggle="modal" data-bs-target="#playlist-add-slide-modal">Add slide</button>
|
||||
{{else}}
|
||||
<div class="text-body-secondary small text-lg-start">
|
||||
{{#if playlistCanvasMismatch}}
|
||||
Remove mismatched slides before adding more.
|
||||
{{else if playlistCanvasSignature}}
|
||||
No slides match this playlist canvas size.
|
||||
{{else}}
|
||||
No slides are currently available for this playlist.
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="text-body-secondary small text-lg-start">Create slides first.</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group playlist-actions-group" role="group" aria-label="Playlist actions">
|
||||
{{{saveActionButtons formId="playlist-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/playlists" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if inUse}}
|
||||
<button type="submit" class="btn btn-outline-danger" form="delete-playlist-form" disabled>Delete</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-danger" form="delete-playlist-form">Delete</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card card-outline card-light h-100 mb-0">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">Options</h4>
|
||||
</div>
|
||||
<div class="card-body py-3">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="playlist-fade-between-slides" name="fade_between_slides" value="1" form="playlist-edit-form" {{#if playlistFadeBetweenSlides}}checked{{/if}} />
|
||||
<label class="form-check-label" for="playlist-fade-between-slides">Fade between slides</label>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-3 mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="playlist-skip-unavailable-rtmp" name="skip_unavailable_rtmp" value="1" form="playlist-edit-form" {{#if playlistSkipUnavailableRtmp}}checked{{/if}} />
|
||||
<label class="form-check-label" for="playlist-skip-unavailable-rtmp">Skip RTMP slides when stream is unavailable</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card card-outline card-secondary mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Playlist items</h3>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table class="table table-striped w-100 mb-0">
|
||||
<thead><tr><th>Order</th><th>Slide</th><th>Schedule</th><th>Duration</th><th>Actions</th></tr></thead>
|
||||
<tbody id="playlist-items-body" data-playlist-id="{{playlist.id}}">
|
||||
{{#if playlistSlides.length}}
|
||||
{{#each playlistSlides}}
|
||||
<tr data-playlist-slide-row data-row-key="existing-{{id}}" data-slide-id="{{slide_id}}" data-canvas-signature="{{canvasSignature}}" data-media-type="{{media_type}}" data-media-path="{{media_path}}" data-video-source-path="{{videoSourcePath}}" data-video-duration-seconds="{{videoDurationSeconds}}" data-use-video-duration-state="{{#if useVideoDuration}}true{{else}}false{{/if}}">
|
||||
<td class="playlist-order-cell" data-label="Order">
|
||||
<div class="playlist-order-cell-inner">
|
||||
<button type="button" class="playlist-drag-handle btn btn-link p-0 text-body-secondary" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder">
|
||||
<span class="playlist-drag-handle-icon" aria-hidden="true">
|
||||
<svg class="playlist-drag-handle-svg" viewBox="0 0 24 32" focusable="false" aria-hidden="true">
|
||||
<polygon points="12,2 20,9 4,9"></polygon>
|
||||
<rect x="4" y="14" width="16" height="4" rx="2"></rect>
|
||||
<polygon points="4,23 20,23 12,30"></polygon>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<span class="playlist-order-number">{{displayOrder}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Slide">
|
||||
<div class="playlist-slide-cell">
|
||||
<div class="playlist-slide-thumb" aria-hidden="true" {{#if canvas_width}}style="--playlist-slide-thumb-aspect-ratio: {{canvas_width}} / {{canvas_height}};"{{/if}}>
|
||||
{{#if thumbnail_path}}
|
||||
<img class="playlist-slide-thumb-image" src="{{thumbnail_path}}" alt="" loading="lazy" />
|
||||
{{else}}
|
||||
<span class="playlist-slide-thumb-placeholder"><i class="bi bi-image" aria-hidden="true"></i></span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="playlist-slide-cell-content">
|
||||
<span class="playlist-slide-title">{{title}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="slide_id[]" value="{{slide_id}}" form="playlist-edit-form" />
|
||||
</td>
|
||||
<td data-label="Schedule">
|
||||
<div class="playlist-schedule-summary">{{scheduleSummary}}</div>
|
||||
<input type="hidden" name="schedule_mode[]" value="{{schedule_mode}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_start_datetime[]" value="{{schedule_start_datetime}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_end_datetime[]" value="{{schedule_end_datetime}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_start_time[]" value="{{schedule_start_time}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_end_time[]" value="{{schedule_end_time}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_days_json[]" value="{{schedule_days_json}}" form="playlist-edit-form" />
|
||||
</td>
|
||||
<td data-label="Duration">
|
||||
<div class="playlist-duration-field">
|
||||
<input name="duration_seconds[]" type="text" inputmode="decimal" value="{{durationSeconds}}" required form="playlist-edit-form" class="form-control form-control-sm playlist-duration-input text-end" {{#if useVideoDuration}}disabled{{/if}} />
|
||||
<input type="hidden" name="use_video_duration[]" value="{{#if useVideoDuration}}1{{else}}0{{/if}}" form="playlist-edit-form" />
|
||||
{{#if useVideoDuration}}
|
||||
<input type="hidden" name="duration_seconds[]" value="{{durationSeconds}}" form="playlist-edit-form" data-video-duration-mirror />
|
||||
{{/if}}
|
||||
{{#if showVideoDurationButton}}
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm playlist-use-video-duration{{#if useVideoDuration}} active{{/if}}" data-use-video-duration-button aria-pressed="{{#if useVideoDuration}}true{{else}}false{{/if}}">{{#if useVideoDuration}}Use Video Duration{{else}}Use video duration{{/if}}</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions playlist-item-actions">
|
||||
<button type="button" class="btn btn-sm btn-primary" data-schedule-config="/playlists/{{../playlist.id}}/slides/{{id}}/config" data-schedule-config-row="existing-{{id}}">Schedule</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" data-playlist-remove-row>Remove</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr class="playlist-empty-row"><td colspan="5" class="empty">No slides assigned yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="playlist-add-slide-data">{{json selectableSlides}}</script>
|
||||
|
||||
{{#> modal-shell modalId="playlist-add-slide-modal" modalLabelId="playlist-add-slide-modal-label" modalDialogClass="modal-dialog-centered modal-dialog-scrollable modal-xl" modalBackdropStatic=true modalKeyboardDisabled=true}}
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 class="modal-title fs-5" id="playlist-add-slide-modal-label">Add slides to playlist</h2>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body playlist-slide-picker-modal-body">
|
||||
<div class="playlist-slide-picker-toolbar d-flex flex-column flex-lg-row gap-3 align-items-stretch align-items-lg-end">
|
||||
<div class="flex-grow-1">
|
||||
<label class="form-label mb-1" for="playlist-slide-picker-search">Search</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
|
||||
<input type="search" class="form-control" id="playlist-slide-picker-search" placeholder="Search slide titles" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist-slide-picker-filter mb-0">
|
||||
<button type="button" class="btn btn-outline-secondary playlist-slide-picker-filter-button" id="playlist-slide-picker-show-assigned" aria-pressed="false">Show already added slides</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist-slide-picker-empty empty is-hidden" id="playlist-slide-picker-empty">No slides match your search.</div>
|
||||
<div class="playlist-slide-picker-grid" id="playlist-slide-picker-grid" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-end">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-end gap-2 w-100">
|
||||
<div class="text-body-secondary small text-end" id="playlist-slide-picker-selected-count-wrap"><span id="playlist-slide-picker-selected-count">0</span> selected</div>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="playlist-confirm-add-slides" disabled>Add selected slides</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/modal-shell}}
|
||||
|
||||
<form id="delete-playlist-form" method="post" action="/playlists/{{playlist.id}}/delete" data-confirm-message="Delete this playlist?"></form>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{#if isEdit}}Edit playlist - {{playlist.name}}{{else}}Add playlist{{/if}}</h2>
|
||||
<p>Set playlist details, items, and schedules.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4 align-items-stretch">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card card-outline card-primary admin-form-card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Playlist details</h3>
|
||||
</div>
|
||||
<form id="playlist-edit-form" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<label for="playlist-name" class="form-label">Name</label>
|
||||
<input id="playlist-name" name="name" class="form-control" value="{{playlist.name}}" placeholder="Lobby loop" required />
|
||||
</div>
|
||||
<div class="col-12 col-lg-5">
|
||||
<label for="playlist-canvas-size" class="form-label">Canvas size</label>
|
||||
<select id="playlist-canvas-size" name="canvas_size_id" class="form-select" form="playlist-edit-form" data-playlist-canvas-editable="{{#if playlistCanvasEditable}}true{{else}}false{{/if}}" {{#unless playlistCanvasEditable}}disabled{{/unless}} {{#unless playlistCanvasId}}required{{/unless}}>
|
||||
<option value="">Select a canvas size</option>
|
||||
{{#each canvasSizes}}
|
||||
<option value="{{id}}" {{#if selected}}selected{{/if}}>{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<input type="hidden" id="playlist-canvas-size-lock" name="canvas_size_id" value="{{playlistCanvasSizeId}}" form="playlist-edit-form" {{#if playlistCanvasEditable}}disabled{{/if}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
{{{saveActionButtons formId="playlist-edit-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteConfirmMessage="Delete this playlist?" deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card card-outline card-light h-100 mb-0">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">Options</h4>
|
||||
</div>
|
||||
<div class="card-body py-3">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="playlist-fade-between-slides" name="fade_between_slides" value="1" form="playlist-edit-form" {{#if playlistFadeBetweenSlides}}checked{{/if}} />
|
||||
<label class="form-check-label" for="playlist-fade-between-slides">Fade between slides</label>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-3 mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="playlist-skip-unavailable-rtmp" name="skip_unavailable_rtmp" value="1" form="playlist-edit-form" {{#if playlistSkipUnavailableRtmp}}checked{{/if}} />
|
||||
<label class="form-check-label" for="playlist-skip-unavailable-rtmp">Skip RTMP slides when unavailable</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card card-outline card-secondary mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Playlist items</h3>
|
||||
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="playlist-open-slide-modal" data-bs-toggle="modal" data-bs-target="#playlist-add-slide-modal" {{#unless playlistCanvasId}}disabled{{/unless}}>Add slide</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0 playlist-items-table-responsive">
|
||||
<table class="table table-striped w-100 mb-0">
|
||||
<thead><tr><th>Order</th><th>Slide</th><th>Schedule</th><th>Config</th><th>Actions</th></tr></thead>
|
||||
<tbody id="playlist-items-body" data-playlist-id="{{playlist.id}}" data-playlist-canvas-id="{{playlistCanvasId}}">
|
||||
{{#if playlistSlides.length}}
|
||||
{{#each playlistSlides}}
|
||||
{{> signage/playlists/slide-row slide=this playlistScheduleConfigUrl=../playlistScheduleConfigUrl}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr class="playlist-empty-row"><td colspan="5" class="empty">No slides assigned yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="playlist-slide-row-template">
|
||||
{{> signage/playlists/slide-row slide=playlistSlideTemplate playlistScheduleConfigUrl=playlistScheduleConfigUrl}}
|
||||
</template>
|
||||
|
||||
<script type="application/json" id="playlist-add-slide-data">{{json selectableSlides}}</script>
|
||||
|
||||
{{#> modal-shell modalId="playlist-add-slide-modal" modalLabelId="playlist-add-slide-modal-label" modalDialogClass="modal-dialog-centered modal-dialog-scrollable modal-xl" modalBackdropStatic=true modalKeyboardDisabled=true}}
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 class="modal-title fs-5" id="playlist-add-slide-modal-label">Add slides to playlist</h2>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body playlist-slide-picker-modal-body">
|
||||
<div class="playlist-slide-picker-toolbar d-flex flex-column flex-lg-row gap-3 align-items-stretch align-items-lg-end">
|
||||
<div class="flex-grow-1">
|
||||
<label class="form-label mb-1" for="playlist-slide-picker-search">Search</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
|
||||
<input type="search" class="form-control" id="playlist-slide-picker-search" placeholder="Search slide titles" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist-slide-picker-filter mb-0">
|
||||
<button type="button" class="btn btn-outline-secondary playlist-slide-picker-filter-button" id="playlist-slide-picker-show-assigned" aria-pressed="false">Show already added slides</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist-slide-picker-empty empty is-hidden" id="playlist-slide-picker-empty">No slides match your search.</div>
|
||||
<div class="playlist-slide-picker-grid" id="playlist-slide-picker-grid" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-end">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-end gap-2 w-100">
|
||||
<div class="text-body-secondary small text-end" id="playlist-slide-picker-selected-count-wrap"><span id="playlist-slide-picker-selected-count">0</span> selected</div>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="playlist-confirm-add-slides" disabled>Add selected slides</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/modal-shell}}
|
||||
@@ -0,0 +1,46 @@
|
||||
<div class="card card-widget card-outline card-secondary mb-3 schedule-rule-card{{#if isCollapsed}} collapsed-card{{/if}}" data-schedule-rule-card>
|
||||
<div class="card-header py-2 d-flex align-items-center">
|
||||
<div class="flex-grow-1 min-w-0">
|
||||
<h4 class="h6 mb-0">Rule <span data-schedule-rule-number>{{ruleIndex}}</span></h4>
|
||||
<div class="text-body-secondary small" data-schedule-rule-summary>{{ruleSummary}}</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-4 ms-auto flex-shrink-0">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" data-remove-schedule-rule data-confirm-message="Remove this rule?">Remove</button>
|
||||
<button type="button" class="btn btn-tool p-0 text-body-secondary" data-lte-toggle="card-collapse" aria-label="Collapse rule {{ruleIndex}}" title="Collapse rule {{ruleIndex}}">
|
||||
<i class="bi bi-dash-lg" aria-hidden="true" data-lte-icon="collapse"></i>
|
||||
<i class="bi bi-plus-lg" aria-hidden="true" data-lte-icon="expand"></i>
|
||||
<span class="visually-hidden">Collapse rule {{ruleIndex}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="hidden" name="schedule_rule_row_key[]" value="" data-schedule-rule-row-key />
|
||||
<input type="hidden" name="schedule_rule_position[]" value="0" data-schedule-rule-position />
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Start datetime</label>
|
||||
<input type="datetime-local" class="form-control" name="schedule_rule_start_datetime[]" value="{{startDateTimeValue}}" data-schedule-rule-start-datetime />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">End datetime</label>
|
||||
<input type="datetime-local" class="form-control" name="schedule_rule_end_datetime[]" value="{{endDateTimeValue}}" data-schedule-rule-end-datetime />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Start time</label>
|
||||
<input type="time" class="form-control" name="schedule_rule_start_time[]" value="{{startTimeValue}}" data-schedule-rule-start-time />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">End time</label>
|
||||
<input type="time" class="form-control" name="schedule_rule_end_time[]" value="{{endTimeValue}}" data-schedule-rule-end-time />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="btn-group btn-group-sm w-100 schedule-day-grid" role="group" aria-label="Schedule days">
|
||||
{{#each dayOptions}}
|
||||
<input class="btn-check" type="checkbox" id="{{inputId}}" value="{{value}}" autocomplete="off" {{#if checked}}checked{{/if}} data-schedule-rule-day />
|
||||
<label class="btn btn-outline-primary flex-fill schedule-day-button" for="{{inputId}}">{{label}}</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,67 +1,35 @@
|
||||
<div class="card card-outline card-primary mb-0 slide-schedule-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Slide schedule</h3>
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<h3 class="card-title mb-0">Slide schedule</h3>
|
||||
<div class="card-tools ms-auto">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="schedule-add-rule">Add another rule</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/playlists/{{playlist.id}}/slides/{{playlistSlide.id}}/config">
|
||||
<form method="post" action="{{#if playlist.id}}/playlists/{{playlist.id}}/slides/{{playlistSlide.id}}/config{{else}}#{{/if}}" id="playlist-slide-schedule-form" {{#unless playlist.id}}data-schedule-draft="true"{{/unless}}>
|
||||
<div class="card-body">
|
||||
<input type="hidden" name="playlist_slide_id" value="{{playlistSlide.id}}" />
|
||||
<input type="hidden" name="row_key" value="{{rowKey}}" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="schedule-mode-select" class="form-label">Schedule mode</label>
|
||||
<select name="schedule_mode" id="schedule-mode-select" class="form-select">
|
||||
<option value="always" {{#if isAlwaysMode}}selected{{/if}}>Always visible</option>
|
||||
<option value="dates" {{#if isDatesMode}}selected{{/if}}>Between two datetimes</option>
|
||||
<option value="times" {{#if isTimesMode}}selected{{/if}}>Between times on selected days</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary mb-3 schedule-panel {{#unless isDatesMode}}is-hidden{{/unless}}" id="schedule-dates-panel">
|
||||
<div class="card-header py-2">
|
||||
<h4 class="h6 mb-0">Date range</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="schedule-start-datetime" class="form-label">Start datetime</label>
|
||||
<input id="schedule-start-datetime" type="datetime-local" name="schedule_start_datetime" class="form-control" value="{{startDateTimeValue}}" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="schedule-end-datetime" class="form-label">End datetime</label>
|
||||
<input id="schedule-end-datetime" type="datetime-local" name="schedule_end_datetime" class="form-control" value="{{endDateTimeValue}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-outline card-info mb-3">
|
||||
<div class="card-body py-3">
|
||||
<p class="text-body-secondary mb-0">These rules work separately - the playlist plays whenever any of them match.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-secondary mb-0 schedule-panel {{#unless isTimesMode}}is-hidden{{/unless}}" id="schedule-times-panel">
|
||||
<div class="card-header py-2">
|
||||
<h4 class="h6 mb-0">Time range</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="schedule-start-time" class="form-label">Start time</label>
|
||||
<input id="schedule-start-time" type="time" name="schedule_start_time" class="form-control" value="{{startTimeValue}}" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="schedule-end-time" class="form-label">End time</label>
|
||||
<input id="schedule-end-time" type="time" name="schedule_end_time" class="form-control" value="{{endTimeValue}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm w-100 schedule-day-grid mt-3" role="group" aria-label="Schedule days">
|
||||
{{#each dayOptions}}
|
||||
<input class="btn-check" type="checkbox" id="schedule-day-{{value}}" name="schedule_days" value="{{value}}" autocomplete="off" {{#if checked}}checked{{/if}} />
|
||||
<label class="btn btn-outline-primary flex-fill schedule-day-button" for="schedule-day-{{value}}">{{label}}</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="schedule-rule-list">
|
||||
{{#each scheduleRules}}
|
||||
{{> signage/playlists/schedule-rule-card isCollapsed=true}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<template id="schedule-rule-card-template">
|
||||
{{> signage/playlists/schedule-rule-card ruleIndex=1 ruleSummary="" startDateTimeValue="" endDateTimeValue="" startTimeValue="" endTimeValue="" dayOptions=templateDayOptions isCollapsed=true}}
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Slide schedule actions">
|
||||
<button type="submit" class="btn btn-primary">Save schedule</button>
|
||||
<button type="button" class="btn btn-warning" data-confirm-unsaved="You have unsaved changes. Leave this page?" onclick="window.closeScheduleModal && window.closeScheduleModal();">Cancel</button>
|
||||
<button type="button" class="btn btn-warning" data-schedule-cancel data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<tr data-playlist-slide-row data-row-key="{{slide.row_key}}" data-slide-id="{{slide.slide_id}}" data-canvas-signature="{{slide.canvasSignature}}" data-media-type="{{slide.media_type}}" data-media-path="{{slide.media_path}}" data-video-source-path="{{slide.videoSourcePath}}" data-video-duration-seconds="{{slide.videoDurationSeconds}}" data-use-video-duration-state="{{#if slide.useVideoDuration}}true{{else}}false{{/if}}" data-disable-audio-state="{{#if slide.disableAudio}}true{{else}}false{{/if}}">
|
||||
<td class="playlist-order-cell" data-label="Order">
|
||||
<div class="playlist-order-cell-inner">
|
||||
<button type="button" class="playlist-drag-handle btn btn-link p-0 text-body-secondary" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder">
|
||||
<span class="playlist-drag-handle-icon" aria-hidden="true">
|
||||
<svg class="playlist-drag-handle-svg" viewBox="0 0 24 32" focusable="false" aria-hidden="true">
|
||||
<polygon points="12,2 20,9 4,9"></polygon>
|
||||
<rect x="4" y="14" width="16" height="4" rx="2"></rect>
|
||||
<polygon points="4,23 20,23 12,30"></polygon>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<span class="playlist-order-number">{{slide.displayOrder}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Slide">
|
||||
<div class="playlist-slide-cell">
|
||||
<div class="playlist-slide-thumb" aria-hidden="true" {{#if slide.canvas_width}}style="--playlist-slide-thumb-aspect-ratio: {{slide.canvas_width}} / {{slide.canvas_height}};"{{/if}} data-playlist-slide-thumb>
|
||||
{{#if slide.thumbnail_path}}
|
||||
<img class="playlist-slide-thumb-image" src="{{slide.thumbnail_path}}" alt="" loading="lazy" data-playlist-slide-thumb-image />
|
||||
{{else}}
|
||||
<span class="playlist-slide-thumb-placeholder" data-playlist-slide-thumb-placeholder><i class="bi bi-image" aria-hidden="true"></i></span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="playlist-slide-cell-content">
|
||||
<span class="playlist-slide-title" data-playlist-slide-title>{{slide.title}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="slide_id[]" value="{{slide.slide_id}}" form="playlist-edit-form" data-playlist-slide-id-input />
|
||||
<input type="hidden" name="row_key[]" value="{{slide.row_key}}" form="playlist-edit-form" data-playlist-row-key-input />
|
||||
</td>
|
||||
<td data-label="Rules">
|
||||
<div class="playlist-schedule-summary"><span class="chip" data-schedule-summary-chip>{{slide.scheduleSummary}}</span></div>
|
||||
<div class="d-none" data-schedule-rules-container data-schedule-rules='{{json slide.scheduleRules}}'></div>
|
||||
</td>
|
||||
<td data-label="Config">
|
||||
<div class="playlist-duration-field">
|
||||
<input name="duration_seconds[]" type="text" inputmode="decimal" value="{{slide.durationSeconds}}" required form="playlist-edit-form" class="form-control form-control-sm playlist-duration-input text-end" {{#if slide.useVideoDuration}}disabled{{/if}} data-playlist-duration-input />
|
||||
<input type="hidden" name="use_video_duration[]" value="{{#if slide.useVideoDuration}}1{{else}}0{{/if}}" form="playlist-edit-form" data-playlist-use-video-duration-input />
|
||||
<input type="hidden" name="disable_audio[]" value="{{#if slide.disableAudio}}1{{else}}0{{/if}}" form="playlist-edit-form" data-playlist-disable-audio-input />
|
||||
{{#if slide.useVideoDuration}}
|
||||
<input type="hidden" name="duration_seconds[]" value="{{slide.durationSeconds}}" form="playlist-edit-form" data-video-duration-mirror data-playlist-duration-mirror />
|
||||
{{/if}}
|
||||
<button type="button" class="btn btn-outline-info btn-sm playlist-use-video-duration{{#if slide.useVideoDuration}} active{{/if}}" data-use-video-duration-button aria-pressed="{{#if slide.useVideoDuration}}true{{else}}false{{/if}}" aria-label="{{#if slide.useVideoDuration}}Disable use video duration{{else}}Use video duration{{/if}}" title="{{#if slide.useVideoDuration}}Disable use video duration{{else}}Use video duration{{/if}}" {{#unless slide.showVideoDurationButton}}hidden{{/unless}}><i class="bi {{#if slide.useVideoDuration}}bi-camera-video{{else}}bi-camera-video-off{{/if}}" aria-hidden="true" data-use-video-duration-icon></i><span class="visually-hidden">{{#if slide.useVideoDuration}}Disable use video duration{{else}}Use video duration{{/if}}</span></button>
|
||||
<button type="button" class="btn {{#if slide.disableAudio}}btn-outline-secondary{{else}}btn-secondary active{{/if}} btn-sm playlist-disable-audio" data-disable-audio-button aria-pressed="{{#if slide.disableAudio}}true{{else}}false{{/if}}" aria-label="{{#if slide.disableAudio}}Enable audio{{else}}Mute slide{{/if}}" title="{{#if slide.disableAudio}}Enable audio{{else}}Mute slide{{/if}}" {{#unless slide.showMuteButton}}hidden{{/unless}}><i class="bi {{#if slide.disableAudio}}bi-volume-mute{{else}}bi-volume-up{{/if}}" aria-hidden="true" data-disable-audio-icon></i><span class="visually-hidden">{{#if slide.disableAudio}}Enable audio{{else}}Mute slide{{/if}}</span></button>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions playlist-item-actions">
|
||||
<button type="button" class="btn btn-sm btn-primary" data-schedule-config="{{playlistScheduleConfigUrl}}" data-schedule-config-row="{{slide.row_key}}">Schedule</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" data-playlist-remove-row>Remove</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1,43 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Add screen</h2>
|
||||
<p>Register a player endpoint and connect it to a playlist.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Screen details</h3>
|
||||
</div>
|
||||
<form id="screen-edit-form" method="post" action="/screens" data-async-save data-async-save-new-url="/screens/new">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-name" class="form-label">Name</label>
|
||||
<input id="screen-name" name="name" class="form-control" placeholder="Front desk TV" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-slug" class="form-label">Slug</label>
|
||||
<input id="screen-slug" name="slug" class="form-control" placeholder="front-desk-tv" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="screen-playlist" class="form-label">Playlist</label>
|
||||
<select id="screen-playlist" name="playlist_id" class="form-select">
|
||||
<option value="">-- not assigned --</option>
|
||||
{{#each playlists}}
|
||||
<option value="{{id}}">{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Screen actions">
|
||||
{{{saveActionButtons formId="screen-edit-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/screens" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit screen - {{screen.name}}</h2>
|
||||
<p>Update the display name and assigned playlist.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card card-outline card-primary admin-form-card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Screen details</h3>
|
||||
</div>
|
||||
<form id="screen-edit-form" method="post" action="/screens/{{screen.id}}" data-async-save data-async-save-close-url="/screens" data-async-save-new-url="/screens/new">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-name" class="form-label">Name</label>
|
||||
<input id="screen-name" name="name" class="form-control" value="{{screen.name}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-slug" class="form-label">Slug</label>
|
||||
<input id="screen-slug" name="slug" class="form-control" value="{{screen.slug}}" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="screen-playlist" class="form-label">Playlist</label>
|
||||
<select id="screen-playlist" name="playlist_id" class="form-select">
|
||||
<option value="">-- not assigned --</option>
|
||||
{{#each playlists}}
|
||||
<option value="{{id}}" {{#if (eq ../screen.playlist_id id)}}selected{{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group screen-actions-group" role="group" aria-label="Screen actions">
|
||||
{{{saveActionButtons formId="screen-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/screens" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if inUse}}
|
||||
<button type="submit" class="btn btn-outline-danger" form="screen-delete-form" disabled>Delete</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-danger" form="screen-delete-form">Delete</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card card-outline card-secondary admin-form-card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Player URL</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{#if screen.player_url}}
|
||||
<p class="mb-0"><a href="{{screen.player_url}}" target="_blank">{{screen.player_url}}</a></p>
|
||||
{{else}}
|
||||
<p class="mb-0 text-muted">No player URL is available yet.</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{#if isEdit}}Edit screen - {{screen.name}}{{else}}Add screen{{/if}}</h2>
|
||||
<p>{{#if isEdit}}Update the display name and assigned playlist.{{else}}Register a player endpoint and connect it to a playlist.{{/if}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12{{#if isEdit}} col-xl-8{{/if}}">
|
||||
<div class="card card-outline card-primary admin-form-card{{#if isEdit}} h-100{{/if}}">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Screen details</h3>
|
||||
</div>
|
||||
<form id="screen-form" method="post" action="{{formAction}}" {{{formAttrs}}}>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-name" class="form-label">Name</label>
|
||||
<input id="screen-name" name="name" class="form-control" value="{{screen.name}}" placeholder="Front desk TV" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-slug" class="form-label">Slug</label>
|
||||
<input id="screen-slug" name="slug" class="form-control" value="{{screen.slug}}" placeholder="front-desk-tv" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="screen-playlist" class="form-label">Playlist</label>
|
||||
<select id="screen-playlist" name="playlist_id" class="form-select">
|
||||
<option value="">-- not assigned --</option>
|
||||
{{#each playlists}}
|
||||
<option value="{{id}}" {{#if (eq ../screen.playlist_id id)}}selected{{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
{{{saveActionButtons formId="screen-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card card-outline card-secondary admin-form-card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Player URL</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{#if screen.player_url}}
|
||||
<p class="mb-0"><a href="{{screen.player_url}}" target="_blank">{{screen.player_url}}</a></p>
|
||||
{{else}}
|
||||
<p class="mb-0 text-muted">No player URL is available yet.</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
<td data-label="Playlist">{{playlist_name}}</td>
|
||||
<td data-label="Connected clients">
|
||||
{{#if (gt player_connection_count 0)}}
|
||||
<div class="connection-count"><span class="chip">{{player_connection_count}} clients{{#if (gt player_connection_count 1)}}s{{else}}{{/if}}{{#if (lt player_connection_count 1)}}s{{/if}}</span></div>
|
||||
<div class="connection-count"><span class="chip">{{player_connection_count}} {{#if (eq player_connection_count 1)}}client{{else}}clients{{/if}}</span></div>
|
||||
{{else}}
|
||||
<span class="empty">No clients connected.</span>
|
||||
{{/if}}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<div class="slide-editor-shell">
|
||||
<link rel="stylesheet" href="/assets/vendor/cropperjs/cropper.min.css" />
|
||||
<form method="post" action="{{action}}" enctype="multipart/form-data" id="slide-form" class="slide-form-stack" data-async-save-close-url="/slides" data-async-save-new-url="/slides/new">
|
||||
<form method="post" action="{{action}}" enctype="multipart/form-data" id="slide-form" class="slide-form-stack" {{{formAttrs}}}>
|
||||
<div class="card card-outline card-primary admin-form-card slide-template-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Slide details</h3>
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="card card-outline card-primary admin-form-card mb-3">
|
||||
<div class="card-body d-grid">
|
||||
<div class="btn-group template-actions-group slide-actions-group" role="group" aria-label="Slide actions">
|
||||
{{{saveActionButtons formId="slide-form" saveLabel=saveLabel saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
{{{saveActionButtons formId="slide-form"}}}
|
||||
<a class="btn btn-warning" href="/slides" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if isEdit}}
|
||||
{{#if inUse}}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Create template</h2>
|
||||
<p>Define a canvas, upload a background, and place content regions.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/templates" enctype="multipart/form-data" id="template-form" data-template-has-slides="{{#if template.slide_count}}true{{else}}false{{/if}}" data-async-save data-async-save-new-url="/templates/new">
|
||||
<input type="hidden" name="canvas_size_id" id="canvas-size-id" value="{{template.canvas_size_id}}" />
|
||||
<input type="hidden" name="canvas_width" id="canvas-width" value="{{template.canvas_size_width}}" />
|
||||
<input type="hidden" name="canvas_height" id="canvas-height" value="{{template.canvas_size_height}}" />
|
||||
<input type="hidden" id="canvas-size-summary" />
|
||||
<input type="hidden" name="existing_background_image_path" value="{{template.background_image_path}}" />
|
||||
<input type="hidden" name="regions_json" id="regions-json" value="" />
|
||||
<textarea id="template-region-usage" hidden>{{json template.region_usage}}</textarea>
|
||||
|
||||
<div class="template-designer-layout">
|
||||
<div class="card card-outline card-secondary admin-form-card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Designer</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="template-designer-main">
|
||||
<div class="designer-stage" id="designer-stage" style="aspect-ratio: {{template.canvas_size_width}} / {{template.canvas_size_height}};">
|
||||
<img id="background-preview" src="{{template.background_image_path}}" alt="" style="{{#if template.background_image_path}}{{else}}display:none;{{/if}}" />
|
||||
<div class="empty-bg" id="background-empty" style="{{#if template.background_image_path}}display:none;{{/if}}">No background image selected</div>
|
||||
<div class="designer-overlay" id="designer-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-designer-sidebar">
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-body d-grid">
|
||||
<div class="btn-group template-actions-group" role="group" aria-label="Template actions">
|
||||
{{{saveActionButtons formId="template-form" saveLabel="Save" saveButtonClass="btn btn-success template-save-button" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/templates" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-outline card-secondary admin-form-card region-info-card mb-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Region information</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-outline-secondary w-100" id="add-region-button" data-bs-toggle="modal" data-bs-target="#region-add-modal">Add region</button>
|
||||
<div class="region-selector-block mt-4">
|
||||
<label for="region-select" class="form-label">Select region</label>
|
||||
<select id="region-select" class="form-select region-select"></select>
|
||||
</div>
|
||||
<div id="region-list" class="region-list mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4 align-items-stretch">
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card card-outline card-primary admin-form-card h-100 template-details-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Template details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 template-meta-row">
|
||||
<div class="col-12">
|
||||
<label for="template-name" class="form-label">Name</label>
|
||||
<input name="name" id="template-name" class="form-control" value="{{template.name}}" required />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="canvas-size-select" class="form-label">Canvas size</label>
|
||||
<select id="canvas-size-select" class="form-select" data-lock-on-existing-template="false">
|
||||
{{#unless template.canvas_size_id}}
|
||||
<option value="" data-width="{{template.canvas_size_width}}" data-height="{{template.canvas_size_height}}" selected>{{template.canvas_size_label}}</option>
|
||||
{{/unless}}
|
||||
{{#if canvasSizes.length}}
|
||||
{{#each canvasSizes}}
|
||||
<option value="{{id}}" data-width="{{width}}" data-height="{{height}}" {{#if (eq ../template.canvas_size_id id)}}selected{{/if}}>{{name}} ({{width}}x{{height}})</option>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card card-outline card-primary admin-form-card h-100 template-options-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Options</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="background-image" class="form-label">Background image</label>
|
||||
<input type="file" name="background_image" id="background-image" class="form-control" accept="image/*" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="background-color" class="form-label">Background colour</label>
|
||||
<input type="color" name="background_color" id="background-color" class="form-control form-control-color w-100" value="{{#if template.background_color}}{{template.background_color}}{{else}}#111111{{/if}}" title="Choose a background colour" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{#> modal-shell modalId="region-add-modal" modalLabelId="region-add-modal-label" modalDialogClass="modal-dialog-centered"}}
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="region-add-modal-label">Add region</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-body-secondary mb-3">Choose the type of region you want to add to this template.</p>
|
||||
<div class="d-grid gap-2" id="region-add-options"></div>
|
||||
</div>
|
||||
{{/modal-shell}}
|
||||
|
||||
<template id="region-card-template">
|
||||
<div class="card card-outline card-secondary admin-form-card region-item">
|
||||
<div class="card-header template-field-head"><strong data-region-title>Region</strong><span class="chip" data-region-chip>Text</span></div>
|
||||
<div class="card-body p-3 d-grid gap-3 pb-0">
|
||||
<div class="region-field-grid region-field-grid--identity">
|
||||
<label>Region name<input class="form-control" name="region_name[]" value="" placeholder="region_1" required /></label>
|
||||
</div>
|
||||
<input type="hidden" name="region_type[]" value="text" />
|
||||
<input type="hidden" name="region_key[]" value="" />
|
||||
<input type="hidden" name="region_label[]" value="" />
|
||||
<div class="region-field-grid region-field-grid--position">
|
||||
<label>X<input class="form-control" type="number" name="region_x[]" value="80" required /></label>
|
||||
<label>Y<input class="form-control" type="number" name="region_y[]" value="80" required /></label>
|
||||
<label>Z-Index<input class="form-control" type="number" name="region_z[]" value="1" required /></label>
|
||||
</div>
|
||||
<div class="region-field-grid region-field-grid--size">
|
||||
<label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label>
|
||||
<label>Height<input class="form-control" type="number" name="region_height[]" value="120" required /></label>
|
||||
<label>Lock ratio<input class="form-control" type="text" name="region_lock_ratio[]" value="" placeholder="16:9 or 5:4" /></label>
|
||||
<label>Remove<button type="button" class="btn btn-sm btn-outline-danger w-100" data-region-remove-button>Remove</button></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
|
||||
|
||||
+39
-40
@@ -1,11 +1,11 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit template - {{template.name}}</h2>
|
||||
<p>Adjust the canvas and region layout for this template.</p>
|
||||
<h2>{{#if isEdit}}Edit template - {{template.name}}{{else}}Create template{{/if}}</h2>
|
||||
<p>{{#if isEdit}}Adjust the canvas and region layout for this template.{{else}}Define a canvas, upload a background, and place content regions.{{/if}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/templates/{{template.id}}" enctype="multipart/form-data" id="template-form" data-template-has-slides="{{#if template.slide_count}}true{{else}}false{{/if}}" data-async-save data-async-save-close-url="/templates" data-async-save-new-url="/templates/new">
|
||||
<form method="post" action="{{formAction}}" enctype="multipart/form-data" id="template-form" {{{formAttrs}}}>
|
||||
<input type="hidden" name="canvas_size_id" id="canvas-size-id" value="{{template.canvas_size_id}}" />
|
||||
<input type="hidden" name="canvas_width" id="canvas-width" value="{{template.canvas_size_width}}" />
|
||||
<input type="hidden" name="canvas_height" id="canvas-height" value="{{template.canvas_size_height}}" />
|
||||
@@ -32,14 +32,8 @@
|
||||
<div class="template-designer-sidebar">
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-body d-grid">
|
||||
<div class="btn-group template-actions-group" role="group" aria-label="Template actions">
|
||||
{{{saveActionButtons formId="template-form" saveLabel="Save" saveButtonClass="btn btn-success template-save-button" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/templates" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if inUse}}
|
||||
<button type="submit" class="btn btn-outline-danger" form="delete-template-form" disabled>Delete</button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-danger" form="delete-template-form">Delete</button>
|
||||
{{/if}}
|
||||
<div class="btn-group" role="group" aria-label="Template actions">
|
||||
{{{saveActionButtons formId="template-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +69,7 @@
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="canvas-size-select" class="form-label">Canvas size</label>
|
||||
<select id="canvas-size-select" class="form-select" data-lock-on-existing-template="true">
|
||||
<select id="canvas-size-select" class="form-select" data-lock-on-existing-template="{{#if isEdit}}true{{else}}false{{/if}}">
|
||||
{{#unless template.canvas_size_id}}
|
||||
<option value="" data-width="{{template.canvas_size_width}}" data-height="{{template.canvas_size_height}}" selected>{{template.canvas_size_label}}</option>
|
||||
{{/unless}}
|
||||
@@ -99,11 +93,15 @@
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="background-image" class="form-label">Background image</label>
|
||||
<div class="template-background-row d-flex gap-2">
|
||||
{{#if isEdit}}
|
||||
<div class="template-background-row d-flex gap-2">
|
||||
<input type="file" name="background_image" id="background-image" class="form-control" accept="image/*" />
|
||||
<button type="button" class="btn btn-outline-secondary" id="remove-background-image">Remove</button>
|
||||
</div>
|
||||
<input type="checkbox" name="remove_background_image" id="remove-background-image-flag" value="1" hidden />
|
||||
{{else}}
|
||||
<input type="file" name="background_image" id="background-image" class="form-control" accept="image/*" />
|
||||
<button type="button" class="btn btn-outline-secondary" id="remove-background-image">Remove</button>
|
||||
</div>
|
||||
<input type="checkbox" name="remove_background_image" id="remove-background-image-flag" value="1" hidden />
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="background-color" class="form-label">Background colour</label>
|
||||
@@ -116,7 +114,9 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{#if isEdit}}
|
||||
<form id="delete-template-form" method="post" action="/templates/{{template.id}}/delete" data-confirm-message="Delete this template?"></form>
|
||||
{{/if}}
|
||||
|
||||
{{#> modal-shell modalId="region-add-modal" modalLabelId="region-add-modal-label" modalDialogClass="modal-dialog-centered"}}
|
||||
<div class="modal-header">
|
||||
@@ -129,30 +129,29 @@
|
||||
</div>
|
||||
{{/modal-shell}}
|
||||
|
||||
<template id="region-card-template">
|
||||
<div class="card card-outline card-secondary admin-form-card region-item">
|
||||
<div class="card-header template-field-head"><strong data-region-title>Region</strong><span class="chip" data-region-chip>Text</span></div>
|
||||
<div class="card-body p-3 d-grid gap-3 pb-0">
|
||||
<div class="region-field-grid region-field-grid--identity">
|
||||
<label>Region name<input class="form-control" name="region_name[]" value="" placeholder="region_1" required /></label>
|
||||
</div>
|
||||
<input type="hidden" name="region_type[]" value="text" />
|
||||
<input type="hidden" name="region_key[]" value="" />
|
||||
<input type="hidden" name="region_label[]" value="" />
|
||||
<div class="region-field-grid region-field-grid--position">
|
||||
<label>X<input class="form-control" type="number" name="region_x[]" value="80" required /></label>
|
||||
<label>Y<input class="form-control" type="number" name="region_y[]" value="80" required /></label>
|
||||
<label>Z-Index<input class="form-control" type="number" name="region_z[]" value="1" required /></label>
|
||||
</div>
|
||||
<div class="region-field-grid region-field-grid--size">
|
||||
<label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label>
|
||||
<label>Height<input class="form-control" type="number" name="region_height[]" value="120" required /></label>
|
||||
<label>Lock ratio<input class="form-control" type="text" name="region_lock_ratio[]" value="" placeholder="16:9 or 5:4" /></label>
|
||||
<label>Remove<button type="button" class="btn btn-sm btn-outline-danger w-100" data-region-remove-button disabled>Remove</button></label>
|
||||
</div>
|
||||
<template id="region-card-template">
|
||||
<div class="card card-outline card-secondary admin-form-card region-item">
|
||||
<div class="card-header template-field-head"><strong data-region-title>Region</strong><span class="chip" data-region-chip>Text</span></div>
|
||||
<div class="card-body p-3 d-grid gap-3 pb-0">
|
||||
<div class="region-field-grid region-field-grid--identity">
|
||||
<label>Region name<input class="form-control" name="region_name[]" value="" placeholder="region_1" required /></label>
|
||||
</div>
|
||||
<input type="hidden" name="region_type[]" value="text" />
|
||||
<input type="hidden" name="region_key[]" value="" />
|
||||
<input type="hidden" name="region_label[]" value="" />
|
||||
<div class="region-field-grid region-field-grid--position">
|
||||
<label>X<input class="form-control" type="number" name="region_x[]" value="80" required /></label>
|
||||
<label>Y<input class="form-control" type="number" name="region_y[]" value="80" required /></label>
|
||||
<label>Z-Index<input class="form-control" type="number" name="region_z[]" value="1" required /></label>
|
||||
</div>
|
||||
<div class="region-field-grid region-field-grid--size">
|
||||
<label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label>
|
||||
<label>Height<input class="form-control" type="number" name="region_height[]" value="120" required /></label>
|
||||
<label>Lock ratio<input class="form-control" type="text" name="region_lock_ratio[]" value="" placeholder="16:9 or 5:4" /></label>
|
||||
<label>Remove<button type="button" class="btn btn-sm btn-outline-danger w-100" data-region-remove-button{{#if isEdit}} disabled{{/if}}>Remove</button></label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
|
||||
Reference in New Issue
Block a user