Restructure, add README license and copyright
This commit is contained in:
36
ots-signs/config.php
Normal file
36
ots-signs/config.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
* Copyright (C) 2006-2021 Xibo Signage Ltd
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
defined('XIBO') or die("Sorry, you are not allowed to directly access this page.<br /> Please press the back button in your browser.");
|
||||
|
||||
$config = array(
|
||||
'theme_name' => 'otssignange',
|
||||
'theme_title' => 'OTS Signs',
|
||||
'app_name' => 'OTS Signage',
|
||||
'theme_url' => 'CMS Homepage',
|
||||
'cms_source_url' => 'https://github.com/xibosignage/xibo-cms',
|
||||
'cms_install_url' => 'manual/en/install_cms.html',
|
||||
'cms_release_notes_url' => 'manual/en/release_notes.html',
|
||||
'latest_news_url' => 'http://xibo.org.uk/feed/',
|
||||
'client_sendCurrentLayoutAsStatusUpdate_enabled' => false,
|
||||
'client_screenShotRequestInterval_enabled' => false,
|
||||
'view_path' => 'views',
|
||||
'product_support_url' => 'https://community.xibo.org.uk/c/support'
|
||||
);
|
||||
301
ots-signs/css/client.css
Normal file
301
ots-signs/css/client.css
Normal file
@@ -0,0 +1,301 @@
|
||||
/* ============================================================================
|
||||
XIBO CMS CLIENT CSS - HTML Widget Styling
|
||||
============================================================================
|
||||
This stylesheet applies to HTML/embedded widgets rendered on displays.
|
||||
Use the same design tokens as override.css for visual consistency.
|
||||
============================================================================ */
|
||||
|
||||
:root {
|
||||
/* Color Tokens (mirrored from override.css) */
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-dark: #1d4ed8;
|
||||
--color-success: #10b981;
|
||||
--color-danger: #ef4444;
|
||||
--color-warning: #f59e0b;
|
||||
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
--color-background: #ffffff;
|
||||
--color-text-primary: #1f2937;
|
||||
--color-text-secondary: #6b7280;
|
||||
--color-border: #e5e7eb;
|
||||
|
||||
/* Typography */
|
||||
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-semibold: 600;
|
||||
--line-height-normal: 1.5;
|
||||
|
||||
/* Spacing */
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-6: 1.5rem;
|
||||
|
||||
/* Radius & Shadow */
|
||||
--radius-md: 0.5rem;
|
||||
--shadow-base: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: #0f172a;
|
||||
--color-text-primary: #f1f5f9;
|
||||
--color-text-secondary: #cbd5e1;
|
||||
--color-border: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
/* Global widget styles */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-family-base);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
body {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--color-text-primary);
|
||||
margin-top: var(--space-6);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 var(--space-4) 0;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
transition: color 150ms ease-in-out;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-primary-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Common widget containers */
|
||||
.widget,
|
||||
.card,
|
||||
.panel {
|
||||
background-color: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-6);
|
||||
margin-bottom: var(--space-6);
|
||||
box-shadow: var(--shadow-base);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button,
|
||||
.btn {
|
||||
background-color: var(--color-primary);
|
||||
color: #ffffff;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-family: var(--font-family-base);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease-in-out;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.btn:hover {
|
||||
background-color: var(--color-primary-dark);
|
||||
border-color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
button:focus,
|
||||
.btn:focus {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="search"],
|
||||
input[type="number"],
|
||||
textarea,
|
||||
select {
|
||||
background-color: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-family: var(--font-family-base);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-primary);
|
||||
transition: border-color 150ms ease-in-out;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="search"]:focus,
|
||||
input[type="number"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: var(--color-gray-100);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: var(--space-4);
|
||||
text-align: left;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
/* Alert boxes */
|
||||
.alert {
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4) var(--space-6);
|
||||
margin-bottom: var(--space-6);
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d1fae5;
|
||||
border-color: var(--color-success);
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #fee2e2;
|
||||
border-color: var(--color-danger);
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fef3c7;
|
||||
border-color: var(--color-warning);
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #cffafe;
|
||||
border-color: #0ea5e9;
|
||||
color: #0c4a6e;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul, ol {
|
||||
margin-bottom: var(--space-6);
|
||||
padding-left: var(--space-6);
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
code {
|
||||
background-color: var(--color-gray-100);
|
||||
color: var(--color-gray-900);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: "SF Mono", Monaco, Menlo, Courier, monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--color-gray-100);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4);
|
||||
overflow-x: auto;
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Table search input sizing moved to CSS for responsive control */
|
||||
.table-search-input {
|
||||
min-width: 11.25rem; /* 180px */
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.table-search-input {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
43
ots-signs/css/html-preview.css
Normal file
43
ots-signs/css/html-preview.css
Normal file
@@ -0,0 +1,43 @@
|
||||
/* Preview Splash Screen - Matches Modern Theme */
|
||||
|
||||
div.preview-splash {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
url('../preview/img/xibologo.png') no-repeat center center;
|
||||
background-attachment: fixed;
|
||||
background-size: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
div.preview-splash::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Preview widget container styling */
|
||||
.preview-widget {
|
||||
background-color: #ffffff;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.preview-widget-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
1532
ots-signs/css/override-dark.css
Normal file
1532
ots-signs/css/override-dark.css
Normal file
File diff suppressed because it is too large
Load Diff
7888
ots-signs/css/override.css
Normal file
7888
ots-signs/css/override.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
ots-signs/img/192x192.png
Normal file
BIN
ots-signs/img/192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
ots-signs/img/512x512.png
Normal file
BIN
ots-signs/img/512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
ots-signs/img/favicon.ico
Normal file
BIN
ots-signs/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
ots-signs/img/xibologo.png
Normal file
BIN
ots-signs/img/xibologo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
778
ots-signs/js/theme.js
Normal file
778
ots-signs/js/theme.js
Normal file
@@ -0,0 +1,778 @@
|
||||
/**
|
||||
* OTS Signage Modern Theme - Client-Side Utilities
|
||||
* Sidebar toggle, dropdown menus, and UI interactions
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
sidebarCollapsed: 'otsTheme:sidebarCollapsed'
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize sidebar toggle functionality
|
||||
*/
|
||||
function initSidebarToggle() {
|
||||
const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
const closeBtn = document.querySelector('.ots-sidebar-close');
|
||||
const collapseBtn = document.querySelector('.sidebar-collapse-btn-visible');
|
||||
const expandBtn = document.querySelector('.sidebar-expand-btn');
|
||||
const body = document.body;
|
||||
|
||||
if (!sidebar) return;
|
||||
|
||||
// Handle sidebar close button
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
sidebar.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile-aware toggle: add backdrop, aria-expanded, and focus management
|
||||
if (toggleBtn) {
|
||||
let lastFocus = null;
|
||||
function ensureBackdrop() {
|
||||
let bd = document.querySelector('.ots-backdrop');
|
||||
if (!bd) {
|
||||
bd = document.createElement('div');
|
||||
bd.className = 'ots-backdrop';
|
||||
bd.addEventListener('click', function() {
|
||||
sidebar.classList.remove('active');
|
||||
bd.classList.remove('show');
|
||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
||||
if (lastFocus) lastFocus.focus();
|
||||
});
|
||||
document.body.appendChild(bd);
|
||||
}
|
||||
return bd;
|
||||
}
|
||||
|
||||
toggleBtn.setAttribute('role', 'button');
|
||||
toggleBtn.setAttribute('aria-controls', 'ots-sidebar');
|
||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
||||
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const isNowActive = !sidebar.classList.contains('active');
|
||||
sidebar.classList.toggle('active');
|
||||
// On small screens show backdrop and manage focus
|
||||
if (window.innerWidth <= 768) {
|
||||
const bd = ensureBackdrop();
|
||||
if (isNowActive) {
|
||||
bd.classList.add('show');
|
||||
toggleBtn.setAttribute('aria-expanded', 'true');
|
||||
lastFocus = document.activeElement;
|
||||
// move focus into the sidebar
|
||||
const firstFocusable = sidebar.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (firstFocusable) firstFocusable.focus(); else sidebar.setAttribute('tabindex', '-1'), sidebar.focus();
|
||||
// add escape handler
|
||||
document.addEventListener('keydown', escHandler);
|
||||
} else {
|
||||
bd.classList.remove('show');
|
||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
||||
if (lastFocus) lastFocus.focus();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function escHandler(e) {
|
||||
if (e.key === 'Escape' || e.key === 'Esc') {
|
||||
const bd = document.querySelector('.ots-backdrop');
|
||||
if (sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
if (bd) bd.classList.remove('show');
|
||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
||||
if (lastFocus) lastFocus.focus();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (collapseBtn) {
|
||||
const isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true';
|
||||
if (isCollapsed) {
|
||||
sidebar.classList.add('collapsed');
|
||||
body.classList.add('ots-sidebar-collapsed');
|
||||
document.documentElement.classList.add('ots-sidebar-collapsed');
|
||||
updateSidebarStateClass();
|
||||
}
|
||||
|
||||
collapseBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const nowCollapsed = !sidebar.classList.contains('collapsed');
|
||||
sidebar.classList.toggle('collapsed');
|
||||
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
||||
document.documentElement.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
|
||||
syncSubmenuDisplayForState(nowCollapsed);
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarStateClass();
|
||||
updateSidebarWidth();
|
||||
setTimeout(updateSidebarWidth, 250);
|
||||
});
|
||||
}
|
||||
|
||||
if (expandBtn) {
|
||||
expandBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
sidebar.classList.remove('collapsed');
|
||||
body.classList.remove('ots-sidebar-collapsed');
|
||||
document.documentElement.classList.remove('ots-sidebar-collapsed');
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
|
||||
syncSubmenuDisplayForState(false);
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarStateClass();
|
||||
updateSidebarWidth();
|
||||
setTimeout(updateSidebarWidth, 250);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize sidebar section toggles
|
||||
initSidebarSectionToggles();
|
||||
|
||||
// Inject flyout headers (icon + label) into each submenu for collapsed state
|
||||
buildFlyoutHeaders();
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', function(e) {
|
||||
if (window.innerWidth <= 768) {
|
||||
const isClickInsideSidebar = sidebar.contains(e.target);
|
||||
const isClickOnToggle = toggleBtn && toggleBtn.contains(e.target);
|
||||
|
||||
if (!isClickInsideSidebar && !isClickOnToggle && sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sidebar width is now handled purely by CSS classes (.ots-sidebar-collapsed).
|
||||
* This function is kept as a no-op for backward compatibility.
|
||||
*/
|
||||
function updateSidebarWidth() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const w = sidebar.offsetWidth;
|
||||
document.documentElement.style.setProperty('--ots-sidebar-actual-width', w + 'px');
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the sidebar header bottom and set the top padding of the nav list
|
||||
* so nav items always begin below the header (logo + buttons).
|
||||
*/
|
||||
function updateSidebarNavOffset() {
|
||||
/* No-op: sidebar uses flex-direction:column so the header and
|
||||
nav content are separate flex children that never overlap.
|
||||
Previously this set padding-top:~72px which created a huge gap. */
|
||||
var nav = document.querySelector('.ots-sidebar .sidebar-nav, .ots-sidebar .ots-sidebar-nav');
|
||||
if (nav) {
|
||||
try { nav.style.removeProperty('padding-top'); } catch(e) { nav.style.paddingTop = ''; }
|
||||
}
|
||||
}
|
||||
|
||||
// simple debounce helper
|
||||
function debounce(fn, wait) {
|
||||
let t;
|
||||
return function () {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn.apply(this, arguments), wait);
|
||||
};
|
||||
}
|
||||
|
||||
function updateSidebarStateClass() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const body = document.body;
|
||||
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||
if (!isCollapsed) {
|
||||
body.classList.add('ots-sidebar-open');
|
||||
} else {
|
||||
body.classList.remove('ots-sidebar-open');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build flyout headers for each sidebar-submenu.
|
||||
* Pulls the icon class(es) and label from the parent group toggle
|
||||
* and injects a styled header <li> at the top of the submenu.
|
||||
* Idempotent — skips submenus that already have a header.
|
||||
*/
|
||||
function buildFlyoutHeaders() {
|
||||
var groups = document.querySelectorAll('.sidebar-group');
|
||||
groups.forEach(function(group) {
|
||||
var submenu = group.querySelector('.sidebar-submenu');
|
||||
if (!submenu) return;
|
||||
// Don't inject twice
|
||||
if (submenu.querySelector('.flyout-header')) return;
|
||||
|
||||
var toggle = group.querySelector('.sidebar-group-toggle');
|
||||
if (!toggle) return;
|
||||
|
||||
// Grab the icon element's class list and the label text
|
||||
var iconEl = toggle.querySelector('.ots-nav-icon');
|
||||
var textEl = toggle.querySelector('.ots-nav-text');
|
||||
if (!textEl) return;
|
||||
|
||||
var label = textEl.textContent.trim();
|
||||
|
||||
// Build the header <li>
|
||||
var header = document.createElement('li');
|
||||
header.className = 'flyout-header';
|
||||
header.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Clone the icon
|
||||
if (iconEl) {
|
||||
var icon = document.createElement('span');
|
||||
icon.className = iconEl.className; // copies all fa classes
|
||||
icon.classList.add('flyout-header-icon');
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
header.appendChild(icon);
|
||||
}
|
||||
|
||||
var text = document.createElement('span');
|
||||
text.className = 'flyout-header-text';
|
||||
text.textContent = label;
|
||||
header.appendChild(text);
|
||||
|
||||
submenu.insertBefore(header, submenu.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When toggling between collapsed/expanded, sync all submenu inline
|
||||
* display styles so that:
|
||||
* - Collapsed: no inline display → CSS :hover handles flyouts
|
||||
* - Expanded: inline display block/none based on is-open state
|
||||
*/
|
||||
function syncSubmenuDisplayForState(isCollapsed) {
|
||||
var groups = document.querySelectorAll('.sidebar-group');
|
||||
groups.forEach(function(group) {
|
||||
var submenu = group.querySelector('.sidebar-submenu');
|
||||
if (!submenu) return;
|
||||
if (isCollapsed) {
|
||||
// Remove inline display so CSS visibility/opacity hover rules work
|
||||
submenu.style.removeProperty('display');
|
||||
} else {
|
||||
// Expanded mode: show/hide based on is-open class
|
||||
var isOpen = group.classList.contains('is-open');
|
||||
submenu.style.display = isOpen ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize sidebar section collapse/expand functionality
|
||||
*/
|
||||
function initSidebarSectionToggles() {
|
||||
const groupToggles = document.querySelectorAll('.sidebar-group-toggle');
|
||||
|
||||
syncSidebarActiveStates();
|
||||
|
||||
groupToggles.forEach(toggle => {
|
||||
const group = toggle.closest('.sidebar-group');
|
||||
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
|
||||
const caret = toggle.querySelector('.sidebar-group-caret');
|
||||
if (submenu) {
|
||||
const isOpen = group.classList.contains('is-open');
|
||||
// Only set inline display when sidebar is NOT collapsed;
|
||||
// collapsed state uses CSS :hover to show flyout menus.
|
||||
const sidebarEl = document.querySelector('.ots-sidebar');
|
||||
const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
|
||||
if (!isCollapsed) {
|
||||
submenu.style.display = isOpen ? 'block' : 'none';
|
||||
} else {
|
||||
// Clear any leftover inline display so CSS :hover can work
|
||||
submenu.style.removeProperty('display');
|
||||
}
|
||||
toggle.setAttribute('aria-expanded', isOpen.toString());
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const group = toggle.closest('.sidebar-group');
|
||||
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
|
||||
if (!submenu) return;
|
||||
|
||||
const sidebarEl = document.querySelector('.ots-sidebar');
|
||||
const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
|
||||
|
||||
// When collapsed, don't toggle submenus on click — hover handles it
|
||||
if (isCollapsed) return;
|
||||
|
||||
const isOpen = group.classList.contains('is-open');
|
||||
group.classList.toggle('is-open', !isOpen);
|
||||
toggle.setAttribute('aria-expanded', (!isOpen).toString());
|
||||
submenu.style.display = isOpen ? 'none' : 'block';
|
||||
syncSidebarActiveStates();
|
||||
});
|
||||
|
||||
if (caret) {
|
||||
caret.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggle.click();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function syncSidebarActiveStates() {
|
||||
const groups = document.querySelectorAll('.sidebar-group');
|
||||
groups.forEach(group => {
|
||||
const toggle = group.querySelector('.sidebar-group-toggle');
|
||||
if (!toggle) return;
|
||||
|
||||
const hasActiveChild = Boolean(
|
||||
group.querySelector('.sidebar-list.active') ||
|
||||
group.querySelector('.sidebar-list > a.active')
|
||||
);
|
||||
|
||||
toggle.classList.toggle('active', hasActiveChild);
|
||||
|
||||
if (hasActiveChild) {
|
||||
group.classList.add('is-open');
|
||||
const submenu = group.querySelector('.sidebar-submenu');
|
||||
if (submenu) submenu.style.display = 'block';
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize dropdown menus
|
||||
*/
|
||||
function initDropdowns() {
|
||||
// Only handle OTS-specific dropdowns (notifications, etc.).
|
||||
// The user menu (#navbarUserMenu) is handled by theme-scripts.twig's
|
||||
// initDropdowns() which uses floatMenu() for proper positioning.
|
||||
// Let Bootstrap handle topbar nav dropdowns (Schedule, Design, etc.) natively
|
||||
// so that links like Dayparting can navigate normally.
|
||||
const otsDropdowns = Array.from(
|
||||
document.querySelectorAll('.ots-topbar-action .dropdown, .ots-page-actions .dropdown')
|
||||
).filter(function(el) {
|
||||
// Exclude the user menu — it has its own dedicated handler in theme-scripts.twig
|
||||
return !el.querySelector('#navbarUserMenu');
|
||||
});
|
||||
|
||||
otsDropdowns.forEach(dropdown => {
|
||||
const toggle = dropdown.querySelector('.dropdown-toggle, [data-toggle="dropdown"]');
|
||||
const menu = dropdown.querySelector('.dropdown-menu');
|
||||
|
||||
if (!toggle || !menu) return;
|
||||
|
||||
// Toggle menu on toggle click
|
||||
toggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const isNowActive = dropdown.classList.toggle('active');
|
||||
|
||||
// Close other OTS dropdowns
|
||||
otsDropdowns.forEach(other => {
|
||||
if (other !== dropdown) other.classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!dropdown.contains(e.target)) {
|
||||
dropdown.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
// Support DataTables Buttons collections which are not wrapped by .dropdown
|
||||
document.addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.dt-button');
|
||||
|
||||
if (btn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const wrapper = btn.closest('.dt-buttons') || btn.parentElement;
|
||||
|
||||
// close other open dt-buttons collections
|
||||
document.querySelectorAll('.dt-buttons.active').forEach(w => {
|
||||
if (w !== wrapper) w.classList.remove('active');
|
||||
});
|
||||
|
||||
wrapper.classList.toggle('active');
|
||||
|
||||
// If DataTables placed the collection on the body, find it and position it under the clicked button
|
||||
const allCollections = Array.from(document.querySelectorAll('.dt-button-collection'));
|
||||
let collection = wrapper.querySelector('.dt-button-collection') || allCollections.find(c => !wrapper.contains(c));
|
||||
|
||||
// If DataTables didn't create a collection element, create one as a fallback
|
||||
if (!collection) {
|
||||
collection = document.createElement('div');
|
||||
collection.className = 'dt-button-collection';
|
||||
// prefer to append near wrapper for positioning; fallback to body
|
||||
(wrapper || document.body).appendChild(collection);
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
// hide other collections
|
||||
allCollections.forEach(c => { if (c !== collection) { c.classList.remove('show'); c.style.display = 'none'; } });
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const top = rect.bottom + window.scrollY;
|
||||
const left = rect.left + window.scrollX;
|
||||
|
||||
collection.style.position = 'absolute';
|
||||
collection.style.top = `${top}px`;
|
||||
collection.style.left = `${left}px`;
|
||||
collection.style.display = 'block';
|
||||
collection.classList.add('show');
|
||||
// DEBUG: log collection contents
|
||||
try {
|
||||
console.log('dt-button-collection opened, children:', collection.children.length, collection);
|
||||
} catch (err) {}
|
||||
|
||||
// If the collection is empty or visually empty, build a fallback column list from the nearest table
|
||||
const isEmpty = collection.children.length === 0 || collection.textContent.trim() === '' || collection.offsetHeight < 10;
|
||||
if (isEmpty) {
|
||||
try {
|
||||
let table = btn.closest('table') || wrapper.querySelector('table') || document.querySelector('table');
|
||||
if (table && window.jQuery && jQuery.fn && jQuery.fn.dataTable && jQuery.fn.dataTable.isDataTable(table)) {
|
||||
const dt = jQuery(table).DataTable();
|
||||
// clear existing
|
||||
collection.innerHTML = '';
|
||||
const thead = table.querySelectorAll('thead th');
|
||||
thead.forEach((th, idx) => {
|
||||
const text = (th.textContent || th.innerText || `Column ${idx+1}`).trim();
|
||||
const item = document.createElement('div');
|
||||
item.style.padding = '6px 12px';
|
||||
item.style.display = 'flex';
|
||||
item.style.alignItems = 'center';
|
||||
item.style.gap = '8px';
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.checked = dt.column(idx).visible();
|
||||
checkbox.addEventListener('change', function() {
|
||||
dt.column(idx).visible(this.checked);
|
||||
});
|
||||
const label = document.createElement('span');
|
||||
label.textContent = text;
|
||||
label.style.color = 'var(--color-text-primary)';
|
||||
item.appendChild(checkbox);
|
||||
item.appendChild(label);
|
||||
collection.appendChild(item);
|
||||
});
|
||||
console.log('Fallback: populated collection with', collection.children.length, 'items');
|
||||
} else {
|
||||
console.log('Fallback: no DataTable instance found to populate column visibility');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Error building fallback column list', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// click outside dt-button -> close any open collections
|
||||
document.querySelectorAll('.dt-buttons.active').forEach(w => w.classList.remove('active'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize search functionality
|
||||
*/
|
||||
function initSearch() {
|
||||
const searchForm = document.querySelector('.topbar-search');
|
||||
if (!searchForm) return;
|
||||
|
||||
const input = searchForm.querySelector('.search-input');
|
||||
if (input) {
|
||||
input.addEventListener('focus', function() {
|
||||
searchForm.style.borderColor = 'var(--color-primary)';
|
||||
});
|
||||
input.addEventListener('blur', function() {
|
||||
searchForm.style.borderColor = 'var(--color-border)';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize page specific interactions
|
||||
*/
|
||||
function initPageInteractions() {
|
||||
// Displays page - folder selection
|
||||
const folderItems = document.querySelectorAll('.folder-item');
|
||||
folderItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
folderItems.forEach(f => f.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Media page - item selection
|
||||
const mediaItems = document.querySelectorAll('.media-item');
|
||||
mediaItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
this.style.opacity = '0.7';
|
||||
setTimeout(() => this.style.opacity = '1', 200);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sidebar responsive
|
||||
*/
|
||||
function makeResponsive() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
const main = document.querySelector('.ots-main');
|
||||
|
||||
if (!sidebar) return;
|
||||
|
||||
// Add toggle button for mobile
|
||||
if (window.innerWidth <= 768) {
|
||||
sidebar.classList.add('mobile');
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
if (window.innerWidth > 768) {
|
||||
sidebar.classList.remove('mobile', 'active');
|
||||
} else {
|
||||
sidebar.classList.add('mobile');
|
||||
}
|
||||
// Recompute sidebar width on resize
|
||||
updateSidebarWidth();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent Chart.js errors when chart elements are missing
|
||||
*/
|
||||
function initChartSafeguard() {
|
||||
if (!window.Chart) return;
|
||||
|
||||
if (typeof window.Chart.acquireContext === 'function') {
|
||||
window.Chart.acquireContext = function(item) {
|
||||
if (!item) return null;
|
||||
|
||||
const candidate = item.length ? item[0] : item;
|
||||
if (candidate && typeof candidate.getContext === 'function') {
|
||||
return candidate.getContext('2d');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.Chart.prototype && typeof window.Chart.prototype.acquireContext === 'function') {
|
||||
window.Chart.prototype.acquireContext = function(item) {
|
||||
if (!item) return null;
|
||||
|
||||
const candidate = item.length ? item[0] : item;
|
||||
if (candidate && typeof candidate.getContext === 'function') {
|
||||
return candidate.getContext('2d');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance tables: wrap in card, add per-table search box, client-side filtering
|
||||
* Non-destructive: skips tables already enhanced
|
||||
*/
|
||||
function enhanceTables() {
|
||||
const selector = '.ots-content table, .content table, .container table, .card table, table';
|
||||
const tables = Array.from(document.querySelectorAll(selector));
|
||||
let counter = 0;
|
||||
|
||||
tables.forEach(table => {
|
||||
// only enhance tables that have a thead and tbody
|
||||
if (!table || table.classList.contains('modern-table')) return;
|
||||
if (!table.querySelector('thead') || !table.querySelector('tbody')) return;
|
||||
|
||||
counter += 1;
|
||||
table.classList.add('modern-table');
|
||||
|
||||
// Build wrapper structure
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'modern-table-card';
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'table-controls';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'search';
|
||||
input.placeholder = 'Search…';
|
||||
input.className = 'table-search-input';
|
||||
input.setAttribute('aria-label', 'Table search');
|
||||
input.style.minWidth = '180px';
|
||||
|
||||
controls.appendChild(input);
|
||||
|
||||
const tableWrapper = document.createElement('div');
|
||||
tableWrapper.className = 'table-wrapper';
|
||||
tableWrapper.style.overflow = 'auto';
|
||||
|
||||
// Insert wrapper into DOM in place of the table
|
||||
const parent = table.parentNode;
|
||||
parent.replaceChild(wrapper, table);
|
||||
wrapper.appendChild(controls);
|
||||
wrapper.appendChild(tableWrapper);
|
||||
tableWrapper.appendChild(table);
|
||||
|
||||
// Simple, light-weight search filtering for this table only
|
||||
input.addEventListener('input', function (e) {
|
||||
const term = (e.target.value || '').toLowerCase();
|
||||
table.querySelectorAll('tbody tr').forEach(tr => {
|
||||
const text = tr.textContent.toLowerCase();
|
||||
tr.style.display = term === '' || text.includes(term) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize DataTables for enhanced behavior when available.
|
||||
* Falls back gracefully if DataTables or jQuery are not present.
|
||||
*/
|
||||
function initDataTables() {
|
||||
if (!window.jQuery) return;
|
||||
const $ = window.jQuery;
|
||||
if (!$.fn || !$.fn.dataTable) return;
|
||||
|
||||
$('.modern-table, table').each(function () {
|
||||
try {
|
||||
if (!$.fn.dataTable.isDataTable(this)) {
|
||||
$(this).DataTable({
|
||||
responsive: true,
|
||||
lengthChange: false,
|
||||
pageLength: 10,
|
||||
autoWidth: false,
|
||||
dom: '<"table-controls"f>rt<"table-meta"ip>',
|
||||
language: { search: '' }
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// If initialization fails, ignore and allow fallback enhancer
|
||||
console.warn('DataTables init failed for table', this, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close every open dropdown / popover on the page.
|
||||
* Covers: Bootstrap 4 native dropdowns, OTS custom dropdowns,
|
||||
* the user-menu, notification drawer, and DataTable row menus.
|
||||
*/
|
||||
function closeAllDropdowns() {
|
||||
try {
|
||||
// Row dropdown menus appended to body
|
||||
document.querySelectorAll('.ots-row-dropdown').forEach(function(m) {
|
||||
m.classList.remove('show', 'ots-row-dropdown');
|
||||
m.style.cssText = '';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.dropdown.show, .btn-group.show').forEach(function(el) {
|
||||
el.classList.remove('show');
|
||||
var m = el.querySelector('.dropdown-menu.show');
|
||||
if (m) m.classList.remove('show');
|
||||
});
|
||||
document.querySelectorAll('.dropdown-menu.show').forEach(function(m) {
|
||||
m.classList.remove('show');
|
||||
});
|
||||
document.querySelectorAll('.dropdown.active, .ots-topbar-action .dropdown.active, .ots-page-actions .dropdown.active').forEach(function(el) {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
var userMenu = document.querySelector('.ots-user-menu-body.ots-user-menu-open');
|
||||
if (userMenu) {
|
||||
userMenu.classList.remove('ots-user-menu-open');
|
||||
var userToggle = document.querySelector('#navbarUserMenu');
|
||||
if (userToggle) {
|
||||
var dd = userToggle.closest('.dropdown');
|
||||
if (dd) dd.classList.remove('active', 'show');
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('.dt-buttons.active').forEach(function(w) { w.classList.remove('active'); });
|
||||
document.querySelectorAll('.dt-button-collection.show').forEach(function(c) { c.classList.remove('show'); c.style.display = 'none'; });
|
||||
if (window.jQuery) {
|
||||
window.jQuery('.dropdown-toggle[aria-expanded="true"]').attr('aria-expanded', 'false');
|
||||
window.jQuery('.dropdown-menu.show').removeClass('show');
|
||||
window.jQuery('.dropdown.show, .btn-group.show').removeClass('show');
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire up global listeners that trigger closeAllDropdowns().
|
||||
*/
|
||||
function initGlobalDropdownDismiss() {
|
||||
document.addEventListener('show.bs.modal', closeAllDropdowns, true);
|
||||
try {
|
||||
if (window.jQuery) {
|
||||
window.jQuery(document).on('show.bs.modal', closeAllDropdowns);
|
||||
window.jQuery(document).on('click', '.XiboFormButton, .XiboAjaxSubmit, .XiboFormRender', function() {
|
||||
closeAllDropdowns();
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var link = e.target.closest('.dropdown-menu a, .ots-topbar a.nav-link, .ots-topbar a.dropdown-item, .XiboFormButton');
|
||||
if (link && !e.defaultPrevented) {
|
||||
closeAllDropdowns();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', closeAllDropdowns);
|
||||
try {
|
||||
var origPush = history.pushState;
|
||||
var origReplace = history.replaceState;
|
||||
history.pushState = function() { origPush.apply(this, arguments); closeAllDropdowns(); };
|
||||
history.replaceState = function() { origReplace.apply(this, arguments); closeAllDropdowns(); };
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
var content = document.getElementById('content') || document.querySelector('.ots-content');
|
||||
if (content) {
|
||||
var contentObs = new MutationObserver(debounce(closeAllDropdowns, 80));
|
||||
contentObs.observe(content, { childList: true });
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all features when DOM is ready
|
||||
*/
|
||||
function init() {
|
||||
initSidebarToggle();
|
||||
initDropdowns();
|
||||
initGlobalDropdownDismiss();
|
||||
initSearch();
|
||||
initPageInteractions();
|
||||
initDataTables();
|
||||
enhanceTables();
|
||||
makeResponsive();
|
||||
initChartSafeguard();
|
||||
// Set initial sidebar width variable and keep it updated
|
||||
updateSidebarWidth();
|
||||
// Set initial nav offset and keep it updated on resize
|
||||
updateSidebarNavOffset();
|
||||
const debouncedUpdateNavOffset = debounce(function() {
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarWidth();
|
||||
}, 120);
|
||||
window.addEventListener('resize', debouncedUpdateNavOffset);
|
||||
}
|
||||
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
BIN
ots-signs/layouts/default-layout.zip
Normal file
BIN
ots-signs/layouts/default-layout.zip
Normal file
Binary file not shown.
91
ots-signs/views/about-page.twig
Normal file
91
ots-signs/views/about-page.twig
Normal file
@@ -0,0 +1,91 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2026 OTS Signs
|
||||
*
|
||||
* About page for OTS Signs.
|
||||
*/
|
||||
#}
|
||||
{% extends "non-authed.twig" %}
|
||||
|
||||
{% block title %}{{ "About"|trans }} | {% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style type="text/css">
|
||||
.about-container {
|
||||
padding: 24px 30px 30px;
|
||||
margin: 10px auto 20px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.about-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.about-links a {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.about-meta {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.about-disclaimer {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}{% endblock %}
|
||||
{% block contentClass %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<a class="btn btn-icon btn-info" href="{{ url_for("home") }}" title="{% trans "Home" %}"><i class="fa fa-home"></i></a>
|
||||
<div class="about-container">
|
||||
<h1>
|
||||
{% trans "About" %}
|
||||
<a href="https://ots-signs.com" target="_blank" rel="noopener noreferrer">OTS Signs</a>
|
||||
</h1>
|
||||
<p>
|
||||
{% trans "An" %}
|
||||
<a href="https://oribi-tech.com" target="_blank" rel="noopener noreferrer">Oribi Technology Services</a>
|
||||
{% trans "product." %}
|
||||
</p>
|
||||
<p class="text-muted">{% trans "OTS Signs provides a compact, focused admin UI and proxy for Xibo CMS" %}</p>
|
||||
|
||||
{% set appVersion = version|default("dev") %}
|
||||
{% set appEnvironment = appEnvironment|default(environment|default("local")) %}
|
||||
{% set commitSha = revision|default("") %}
|
||||
|
||||
<div class="about-meta">
|
||||
<div>{% trans "Version" %}: {{ appVersion }}</div>
|
||||
<div>{% trans "Environment" %}: {{ appEnvironment }}</div>
|
||||
{% if commitSha %}
|
||||
<div>{% trans "Commit" %}: {{ commitSha|slice(0, 7) }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="about-links">
|
||||
<a href="/privacy">{% trans "Privacy Policy" %}</a>
|
||||
<a href="/terms">{% trans "Terms of Service" %}</a>
|
||||
<a href="/open-source-licenses">{% trans "Open Source Licenses" %}</a>
|
||||
</div>
|
||||
|
||||
<div class="about-disclaimer">
|
||||
<strong>{% trans "Disclaimer:" %}</strong>
|
||||
{% trans "OTS Signs is an independent product developed by Oribi Technology Services. It is not affiliated with or endorsed by the Xibo project or its maintainers. Xibo is a trademark of Xibo Digital Signage Ltd. Use of Xibo APIs is subject to their terms and conditions." %}
|
||||
<div class="mt-2">
|
||||
<a href="https://github.com/xibosignage/xibo" target="_blank" rel="noopener noreferrer">{% trans "Xibo CMS on GitHub" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
51
ots-signs/views/about-text.twig
Normal file
51
ots-signs/views/about-text.twig
Normal file
@@ -0,0 +1,51 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2026 OTS Signs
|
||||
*
|
||||
* About dialog content for OTS Signs.
|
||||
*/
|
||||
#}
|
||||
{% extends "form-base.twig" %}
|
||||
|
||||
{% block formTitle %}{% trans "About" %}{% endblock %}
|
||||
|
||||
{% block formButtons %}
|
||||
{% trans "Close" %}, XiboDialogClose()
|
||||
{% endblock %}
|
||||
|
||||
{% block formHtml %}
|
||||
<div class="about-container">
|
||||
<h2>
|
||||
{% trans "About" %}
|
||||
<a href="https://ots-signs.com" target="_blank" rel="noopener noreferrer">OTS Signs</a>
|
||||
</h2>
|
||||
|
||||
<p class="text-muted">{% trans "OTS Signs provides a compact, focused interface for your digital signage network" %}</p>
|
||||
|
||||
{% set appVersion = version|default("dev") %}
|
||||
{% set appEnvironment = appEnvironment|default(environment|default("local")) %}
|
||||
{% set commitSha = revision|default("") %}
|
||||
|
||||
<div class="about-meta">
|
||||
<div>{% trans "Version" %}: {{ appVersion }}</div>
|
||||
<div>{% trans "Environment" %}: {{ appEnvironment }}</div>
|
||||
{% if commitSha %}
|
||||
<div>{% trans "Commit" %}: {{ commitSha|slice(0, 7) }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="about-links">
|
||||
<a href="/privacy">{% trans "Privacy Policy" %}</a>
|
||||
<a href="/terms">{% trans "Terms of Service" %}</a>
|
||||
<a href="/open-source-licenses">{% trans "Open Source Licenses" %}</a>
|
||||
</div>
|
||||
|
||||
<div class="about-disclaimer">
|
||||
<strong>{% trans "Disclaimer:" %}</strong>
|
||||
{% trans "OTS Signs is an custom front end developed by Oribi Technology Services for the Xibo CMS. It is not affiliated with the Xibo project or its maintainers. Xibo is a trademark of Xibo Digital Signage Ltd. Use of Xibo is subject to their terms and conditions." %}
|
||||
<div class="mt-2">
|
||||
<a href="https://source.otshosting.app/OTSSigns/CMS-Server" target="_blank" rel="noopener noreferrer">{% trans "View the CMS server source on GitHub" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
268
ots-signs/views/applications-page.twig
Normal file
268
ots-signs/views/applications-page.twig
Normal file
@@ -0,0 +1,268 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Applications Page
|
||||
* Based on Xibo CMS applications-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Applications"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Applications" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage API applications and connectors." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Applications" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('name', title) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add an Application" %}" href="{{ url_for("application.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="applications" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="widget content-card ots-displays-card mt-2">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Connectors" %}</h1>
|
||||
</div>
|
||||
<div id="connectors" class="card-deck">
|
||||
{% if theme.getThemeConfig("app_name") == "Xibo" %}
|
||||
<div class="card p3 mt-2" style="min-width: 250px; max-width: 250px;">
|
||||
<img class="card-img-top" style="max-height: 250px" src="{{ theme.rootUri() }}theme/default/img/connectors/canva_logo.png" alt="Canva">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Canva</h5>
|
||||
<p class="card-text">
|
||||
Publish your designs from Canva to Xibo at the push of a button.
|
||||
<br/>
|
||||
<br/>
|
||||
This connector is configured in Canva using the "Publish menu".
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a class="btn btn-primary" href="https://canva.com" target="_blank">Visit Canva</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
|
||||
{% autoescape "js" %}
|
||||
var copyToClipboardTrans = "{{ "Copy to Clipboard"|trans }}";
|
||||
var couldNotCopyTrans = "{{ "Could not copy"|trans }}";
|
||||
var copiedTrans = "{{ "Copied!"|trans }}";
|
||||
{% endautoescape %}
|
||||
|
||||
var table;
|
||||
$(document).ready(function() {
|
||||
table = $('#applications').DataTable({
|
||||
language: dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
responsive: true,
|
||||
stateDuration: 0,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 0, "asc"]],
|
||||
ajax: {
|
||||
url: "{{ url_for('application.search') }}",
|
||||
data: function (d) {
|
||||
$.extend(d, $('#applications').closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "name", "render": dataTableSpacingPreformatted },
|
||||
{ "data": "owner" },
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#applications_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
|
||||
// Connectors
|
||||
loadConnectors();
|
||||
});
|
||||
|
||||
function loadConnectors() {
|
||||
var connectorTemplate = Handlebars.compile($('#template-connector-cards').html());
|
||||
var $connectorContainer = $('#connectors');
|
||||
$connectorContainer.find('.connector').remove();
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '{{ url_for("connector.search") }}?isVisible=1&showUninstalled=1',
|
||||
cache: false,
|
||||
dataType:"json",
|
||||
success: function(xhr, textStatus, error) {
|
||||
$.each(xhr.data, function(index, element) {
|
||||
if (element.isHidden) {
|
||||
return;
|
||||
}
|
||||
element.configureUrl = '{{ url_for("connector.edit.form", {id: ":id"}) }}'.replace(':id', element.connectorId);
|
||||
element.proxyUrl = '{{ url_for("connector.edit.form.proxy", {id: ":id", method: ":method"}) }}'.replace(':id', element.connectorId);
|
||||
element.thumbnail = element.thumbnail || 'theme/default/img/thumbs/placeholder.png';
|
||||
if (!element.thumbnail.startsWith('http')) {
|
||||
element.thumbnail = '{{ theme.rootUri() }}' + element.thumbnail;
|
||||
}
|
||||
element.enabledIcon = (element.isEnabled) ? 'fa-check' : 'fa-times';
|
||||
element.classNameLast = element.className.substr(element.className.lastIndexOf('\\') + 1);
|
||||
$connectorContainer.append(connectorTemplate(element));
|
||||
});
|
||||
|
||||
$connectorContainer.trigger('connectors.loaded');
|
||||
XiboInitialise('#connectors');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function connectorFormSubmit() {
|
||||
XiboFormSubmit($('#connectorEditForm'), null, function() {
|
||||
loadConnectors();
|
||||
});
|
||||
}
|
||||
|
||||
function copyFromSecretInput(dialog) {
|
||||
$('#copy-button').tooltip();
|
||||
|
||||
$('#copy-button').bind('click', function() {
|
||||
var input = $('#clientSecret');
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
try {
|
||||
var success = document.execCommand('copy');
|
||||
if (success) {
|
||||
$('#copy-button').trigger('copied', [copiedTrans]);
|
||||
} else {
|
||||
$('#copy-button').trigger('copied', [couldNotCopyTrans]);
|
||||
}
|
||||
} catch (err) {
|
||||
$('#copy-button').trigger('copied', [couldNotCopyTrans]);
|
||||
}
|
||||
|
||||
input.blur();
|
||||
});
|
||||
|
||||
$('#copy-button').bind('copied', function(event, message) {
|
||||
const $self = $(this);
|
||||
$(this).tooltip('hide')
|
||||
.attr('data-original-title', message)
|
||||
.tooltip('show');
|
||||
|
||||
setTimeout(function() {
|
||||
$self.tooltip('hide').attr('data-original-title', copyToClipboardTrans);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onAuthCodeChanged(dialog);
|
||||
$(dialog).find('#authCode').on('change', function() {
|
||||
onAuthCodeChanged(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
function onAuthCodeChanged(dialog) {
|
||||
var authCode = $(dialog).find("#authCode").is(":checked");
|
||||
var $authCodeTab = $(dialog).find(".tabForAuthCode");
|
||||
|
||||
if (authCode) {
|
||||
$authCodeTab.removeClass("d-none");
|
||||
} else {
|
||||
$authCodeTab.addClass("d-none");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{% for js in connectorJavaScript %}
|
||||
{% include js ~ ".twig" %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScriptTemplates %}
|
||||
{{ parent() }}
|
||||
|
||||
{% verbatim %}
|
||||
<script type="text/x-handlebars-template" id="template-connector-cards">
|
||||
<div class="connector card p3 mt-2" style="min-width: 250px; max-width: 250px;"
|
||||
data-proxy-url="{{proxyUrl}}"
|
||||
data-connector-class-name="{{className}}"
|
||||
data-connector-class-name-last="{{classNameLast}}"
|
||||
data-connector-id="{{ connectorId }}">
|
||||
{{#if thumbnail}}<img class="card-img-top" style="max-height: 250px" src="{{ thumbnail }}" alt="{{ title }}">{{/if}}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ title }}</h5>
|
||||
<p class="card-text">
|
||||
{{ description }}
|
||||
<br/>
|
||||
<br/>
|
||||
{{#if isInstalled }}
|
||||
{% endverbatim %}{{ "Enabled"|trans }}{% verbatim %}: <span class="fa {{ enabledIcon }}"></span>
|
||||
{{/if}}
|
||||
{{#unless isInstalled }}
|
||||
{% endverbatim %}{{ "Installed"|trans }}{% verbatim %}: <span class="fa fa-times"></span>
|
||||
{{/unless}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="btn btn-primary XiboFormButton" href="{{ configureUrl }}">
|
||||
{% endverbatim %}{{ "Configure"|trans }}{% verbatim %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
{% endverbatim %}
|
||||
{% endblock %}
|
||||
26
ots-signs/views/authed-notification-drawer.twig
Normal file
26
ots-signs/views/authed-notification-drawer.twig
Normal file
@@ -0,0 +1,26 @@
|
||||
{#
|
||||
Compact-aware notification drawer override
|
||||
#}
|
||||
{% if compact is defined and compact %}
|
||||
<div class="dropdown nav-item item ots-notif-compact">
|
||||
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarNotificationDrawer">
|
||||
<span class="ots-topbar-icon fa fa-bell" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right ots-notif-menu" aria-labelledby="navbarNotificationDrawer">
|
||||
<div class="dropdown-header">Notifications</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-item">No new notifications</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<li class="dropdown nav-item item">
|
||||
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarNotificationDrawer">
|
||||
<span class="ots-topbar-icon fa fa-bell" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right ots-notif-menu" aria-labelledby="navbarNotificationDrawer">
|
||||
<div class="dropdown-header">Notifications</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-item">No new notifications</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
441
ots-signs/views/authed-sidebar.twig
Normal file
441
ots-signs/views/authed-sidebar.twig
Normal file
@@ -0,0 +1,441 @@
|
||||
{#
|
||||
OTS Signage Theme override
|
||||
Based on Xibo CMS default authed-sidebar.twig (master branch)
|
||||
Applied OTS sidebar styling
|
||||
#}
|
||||
<div id="sidebar-wrapper" class="ots-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a class="brand-link" href="{{ url_for("home") }}">
|
||||
<span class="brand-icon">
|
||||
<img class="brand-logo" src="{{ theme.uri("img/xibologo.png") }}" alt="{% trans "Logo" %}">
|
||||
</span>
|
||||
<span class="brand-text">OTS Signs</span>
|
||||
</a>
|
||||
<button class="sidebar-expand-btn" type="button" aria-label="{% trans "Expand sidebar" %}">
|
||||
<i class="fa fa-chevron-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="sidebar-collapse-btn sidebar-collapse-btn-visible" type="button" aria-label="{% trans "Collapse sidebar" %}">
|
||||
<i class="fa fa-chevron-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<ul class="sidebar ots-sidebar-nav">
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("home") }}" data-tooltip="Dashboard">
|
||||
<span class="ots-nav-icon fa fa-home" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Dashboard" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% set scheduleCount = currentUser.featureEnabledCount(["schedule.view", "daypart.view"]) %}
|
||||
{% if scheduleCount > 0 %}
|
||||
<li class="sidebar-group">
|
||||
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="scheduling">
|
||||
<span class="ots-nav-icon fa fa-calendar" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Scheduling" %}</span>
|
||||
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
{% if currentUser.featureEnabled("daypart.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("daypart.view") }}">
|
||||
<span class="ots-nav-icon fa fa-clock-o" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Dayparts" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("schedule.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("schedule.view") }}">
|
||||
<span class="ots-nav-icon fa fa-calendar-check-o" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Schedules" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<li class="sidebar-group">
|
||||
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="media">
|
||||
<span class="ots-nav-icon fa fa-picture-o" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Media" %}</span>
|
||||
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
{% if currentUser.featureEnabled("library.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("library.view") }}">
|
||||
<span class="ots-nav-icon fa fa-image" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Library" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("playlist.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("playlist.view") }}">
|
||||
<span class="ots-nav-icon fa fa-list" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Playlists" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("dataset.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("dataset.view") }}">
|
||||
<span class="ots-nav-icon fa fa-database" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "DataSets" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("menuBoard.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("menuBoard.view") }}">
|
||||
<span class="ots-nav-icon fa fa-cutlery" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Menu Boards" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<li class="sidebar-group">
|
||||
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="design">
|
||||
<span class="ots-nav-icon fa fa-paint-brush" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Design" %}</span>
|
||||
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
{% if currentUser.featureEnabled("campaign.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("campaign.view") }}">
|
||||
<span class="ots-nav-icon fa fa-bullhorn" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Campaigns" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("layout.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("layout.view") }}">
|
||||
<span class="ots-nav-icon fa fa-columns" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Layouts" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("template.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("template.view") }}">
|
||||
<span class="ots-nav-icon fa fa-clone" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Templates" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("resolution.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("resolution.view") }}">
|
||||
<span class="ots-nav-icon fa fa-expand" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Resolutions" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view", "playersoftware.view", "command.view", "display.syncView"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<li class="sidebar-group">
|
||||
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="displays">
|
||||
<span class="ots-nav-icon fa fa-desktop" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Displays" %}</span>
|
||||
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
{% if currentUser.featureEnabled("displays.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("display.view") }}">
|
||||
<span class="ots-nav-icon fa fa-desktop" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "All Displays" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("displaygroup.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("displaygroup.view") }}">
|
||||
<span class="ots-nav-icon fa fa-object-group" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Screen Groups" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("display.syncView") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("syncgroup.view") }}">
|
||||
<span class="ots-nav-icon fa fa-link" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Sync Groups" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("displayprofile.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("displayprofile.view") }}">
|
||||
<span class="ots-nav-icon fa fa-cog" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Display Settings" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("playersoftware.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("playersoftware.view") }}">
|
||||
<span class="ots-nav-icon fa fa-download" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Player Versions" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("command.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("command.view") }}">
|
||||
<span class="ots-nav-icon fa fa-terminal" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Commands" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
|
||||
{% set userMenuViewable = true %}
|
||||
{% else %}
|
||||
{% set userMenuViewable = false %}
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["usergroup.view", "module.view", "transition.view", "task.view", "tag.view", "font.view"]) %}
|
||||
{% if countViewable > 0 or userMenuViewable %}
|
||||
<li class="sidebar-group">
|
||||
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="settings">
|
||||
<span class="ots-nav-icon fa fa-cog" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Settings" %}</span>
|
||||
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
{% if userMenuViewable %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("user.view") }}">
|
||||
<span class="ots-nav-icon fa fa-user" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Users" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("usergroup.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("group.view") }}">
|
||||
<span class="ots-nav-icon fa fa-users" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "User Groups" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.isSuperAdmin() %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("admin.view") }}">
|
||||
<span class="ots-nav-icon fa fa-sliders" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Settings" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.isSuperAdmin() %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("application.view") }}">
|
||||
<span class="ots-nav-icon fa fa-puzzle-piece" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Applications" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("module.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("module.view") }}">
|
||||
<span class="ots-nav-icon fa fa-cubes" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Modules" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("transition.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("transition.view") }}">
|
||||
<span class="ots-nav-icon fa fa-random" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Transitions" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("task.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("task.view") }}">
|
||||
<span class="ots-nav-icon fa fa-tasks" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Tasks" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("tag.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("tag.view") }}">
|
||||
<span class="ots-nav-icon fa fa-tags" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Tags" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.isSuperAdmin() %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("folders.view") }}">
|
||||
<span class="ots-nav-icon fa fa-folder-open" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Folders" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("font.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("font.view") }}">
|
||||
<span class="ots-nav-icon fa fa-font" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Fonts" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["report.view", "report.scheduling", "report.saving"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<li class="sidebar-group">
|
||||
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="reporting">
|
||||
<span class="ots-nav-icon fa fa-bar-chart" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Reporting" %}</span>
|
||||
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
{% if currentUser.featureEnabled("report.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("report.view") }}">
|
||||
<span class="ots-nav-icon fa fa-file-text-o" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "All Reports" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("report.scheduling") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("reportschedule.view") }}">
|
||||
<span class="ots-nav-icon fa fa-calendar" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Report Schedules" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("report.saving") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("savedreport.view") }}">
|
||||
<span class="ots-nav-icon fa fa-floppy-o" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Saved Reports" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["log.view", "sessions.view", "auditlog.view", "fault.view"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<li class="sidebar-group">
|
||||
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="advanced">
|
||||
<span class="ots-nav-icon fa fa-shield" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Advanced" %}</span>
|
||||
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
{% if currentUser.featureEnabled("log.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("log.view") }}">
|
||||
<span class="ots-nav-icon fa fa-list-alt" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Log" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("sessions.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("sessions.view") }}">
|
||||
<span class="ots-nav-icon fa fa-user-secret" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Sessions" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("auditlog.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("auditlog.view") }}">
|
||||
<span class="ots-nav-icon fa fa-clipboard" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Audit Trail" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("fault.view") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("fault.view") }}">
|
||||
<span class="ots-nav-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Report Fault" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["developer.edit"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<li class="sidebar-group">
|
||||
<a class="sidebar-group-toggle" href="#" aria-expanded="false" data-group="developer">
|
||||
<span class="ots-nav-icon fa fa-code" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Developer" %}</span>
|
||||
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
{% if currentUser.featureEnabled("developer.edit") %}
|
||||
<li class="sidebar-list">
|
||||
<a href="{{ url_for("developer.templates.view") }}">
|
||||
<span class="ots-nav-icon fa fa-code" aria-hidden="true"></span>
|
||||
<span class="ots-nav-text">{% trans "Module Templates" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
6
ots-signs/views/authed-theme-topbar.twig
Normal file
6
ots-signs/views/authed-theme-topbar.twig
Normal file
@@ -0,0 +1,6 @@
|
||||
{#
|
||||
OTS Signage Theme override
|
||||
Optional include rendered in authed.twig (top right navbar)
|
||||
Minimal, low-risk addition for verification
|
||||
#}
|
||||
{# OTS topbar badge removed #}
|
||||
472
ots-signs/views/authed-topbar.twig
Normal file
472
ots-signs/views/authed-topbar.twig
Normal file
@@ -0,0 +1,472 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
<ul class="nav navbar-nav ots-topbar">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for("home") }}">
|
||||
<span class="ots-topbar-icon fa fa-home" aria-hidden="true"></span>
|
||||
{% trans "Dashboard" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["schedule.view", "daypart.view"]) %}
|
||||
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
|
||||
{% if countViewable > 0 %}
|
||||
{% if countViewable > 1 %}
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="ots-topbar-icon fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Schedule" %} <span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
{% if currentUser.featureEnabled("schedule.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("schedule.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Schedule" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if currentUser.featureEnabled("daypart.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("daypart.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-clock" aria-hidden="true"></span>
|
||||
{% trans "Dayparting" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if countViewable > 1 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
|
||||
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
|
||||
{% if countViewable > 0 %}
|
||||
{% if countViewable > 1 %}
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="ots-topbar-icon fa fa-paint-brush" aria-hidden="true"></span>
|
||||
{% trans "Design" %} <span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
{% if currentUser.featureEnabled("campaign.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("campaign.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-bullhorn" aria-hidden="true"></span>
|
||||
{% trans "Campaigns" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("layout.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("layout.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-columns" aria-hidden="true"></span>
|
||||
{% trans "Layouts" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("template.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("template.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-clone" aria-hidden="true"></span>
|
||||
{% trans "Templates" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("resolution.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("resolution.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-expand" aria-hidden="true"></span>
|
||||
{% trans "Resolutions" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if countViewable > 1 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
|
||||
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
|
||||
{% if countViewable > 0 %}
|
||||
{% if countViewable > 1 %}
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="ots-topbar-icon fa fa-folder-open" aria-hidden="true"></span>
|
||||
{% trans "Library" %} <span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
{% if currentUser.featureEnabled("playlist.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("playlist.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-list" aria-hidden="true"></span>
|
||||
{% trans "Playlists" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("library.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("library.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-photo" aria-hidden="true"></span>
|
||||
{% trans "Media" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("dataset.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("dataset.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-database" aria-hidden="true"></span>
|
||||
{% trans "DataSets" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("menuBoard.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("menuBoard.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-th-large" aria-hidden="true"></span>
|
||||
{% trans "Menu Boards" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if countViewable > 1 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view", "playersoftware.view", "command.view"]) %}
|
||||
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
|
||||
{% if countViewable > 0 %}
|
||||
{% if countViewable > 1 %}
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="ots-topbar-icon fa fa-desktop" aria-hidden="true"></span>
|
||||
{% trans "Displays" %} <span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
{% if currentUser.featureEnabled("displays.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("display.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-desktop" aria-hidden="true"></span>
|
||||
{% trans "Displays" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("displaygroup.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("displaygroup.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-object-group" aria-hidden="true"></span>
|
||||
{% trans "Display Groups" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("display.syncView") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("syncgroup.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-link" aria-hidden="true"></span>
|
||||
{% trans "Sync Groups" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("displayprofile.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("displayprofile.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-sliders" aria-hidden="true"></span>
|
||||
{% trans "Display Settings" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("playersoftware.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("playersoftware.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-download" aria-hidden="true"></span>
|
||||
{% trans "Player Versions" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("command.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("command.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-terminal" aria-hidden="true"></span>
|
||||
{% trans "Commands" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if countViewable > 1 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
|
||||
{% set userMenuViewable = true %}
|
||||
{% else %}
|
||||
{% set userMenuViewable = false %}
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["usergroup.view", "module.view", "transition.view", "task.view"]) %}
|
||||
{% set groupElementClass = (countViewable > 1 or (countViewable == 1 and userMenuViewable)) ? 'dropdown-item' : 'nav-link' %}
|
||||
{% if countViewable > 0 or userMenuViewable %}
|
||||
{% if countViewable > 1 or (countViewable == 1 and userMenuViewable) %}
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="ots-topbar-icon fa fa-cog" aria-hidden="true"></span>
|
||||
{% trans "Administration" %} <span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% endif %}
|
||||
{% if userMenuViewable %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("user.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-users" aria-hidden="true"></span>
|
||||
{% trans "Users" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("usergroup.view") %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("group.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-users-cog" aria-hidden="true"></span>
|
||||
{% trans "User Groups" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.isSuperAdmin() %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("admin.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-wrench" aria-hidden="true"></span>
|
||||
{% trans "Settings" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.isSuperAdmin() %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("application.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-th" aria-hidden="true"></span>
|
||||
{% trans "Applications" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("module.view") %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("module.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-puzzle-piece" aria-hidden="true"></span>
|
||||
{% trans "Modules" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("transition.view") %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("transition.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-exchange" aria-hidden="true"></span>
|
||||
{% trans "Transitions" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("task.view") %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("task.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-tasks" aria-hidden="true"></span>
|
||||
{% trans "Tasks" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("tag.view") %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("tag.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-tags" aria-hidden="true"></span>
|
||||
{% trans "Tags" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.isSuperAdmin() %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("folders.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-folder" aria-hidden="true"></span>
|
||||
{% trans "Folders" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("font.view") %}
|
||||
{% if countViewable == 0 %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("font.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-font" aria-hidden="true"></span>
|
||||
{% trans "Fonts" %}
|
||||
</a>
|
||||
{% if countViewable == 0 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if countViewable > 1 or (countViewable == 1 and userMenuViewable) %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if countViewable > 1 or (countViewable == 1 and userMenuViewable) %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["report.view", "report.scheduling", "report.saving"]) %}
|
||||
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
|
||||
{% if countViewable > 0 %}
|
||||
{% if countViewable > 1 %}
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="ots-topbar-icon fa fa-chart-bar" aria-hidden="true"></span>
|
||||
{% trans "Reporting" %} <span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("report.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("report.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-file-alt" aria-hidden="true"></span>
|
||||
{% trans "All Reports" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("report.scheduling") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("reportschedule.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-calendar-alt" aria-hidden="true"></span>
|
||||
{% trans "Report Schedules" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("report.saving") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("savedreport.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-save" aria-hidden="true"></span>
|
||||
{% trans "Saved Reports" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if countViewable > 1 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["log.view", "sessions.view", "auditlog.view", "fault.view"]) %}
|
||||
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
|
||||
{% if countViewable > 0 %}
|
||||
{% if countViewable > 1 %}
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="ots-topbar-icon fa fa-shield-alt" aria-hidden="true"></span>
|
||||
{% trans "Advanced" %} <span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
{% endif %}
|
||||
{% if currentUser.featureEnabled("log.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("log.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-list-alt" aria-hidden="true"></span>
|
||||
{% trans "Log" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("sessions.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("sessions.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-history" aria-hidden="true"></span>
|
||||
{% trans "Sessions" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("auditlog.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("auditlog.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-clipboard-list" aria-hidden="true"></span>
|
||||
{% trans "Audit Trail" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("fault.view") %}
|
||||
<a class="{{ groupElementClass }}" href="{{ url_for("fault.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
|
||||
{% trans "Report Fault" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if countViewable > 1 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% set countViewable = currentUser.featureEnabledCount(["developer.edit"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="ots-topbar-icon fa fa-code" aria-hidden="true"></span>
|
||||
{% trans "Developer" %} <span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% if currentUser.featureEnabled("developer.edit") %}
|
||||
<a class="dropdown-item" href="{{ url_for("developer.templates.view") }}">
|
||||
<span class="ots-topbar-icon fa fa-code-branch" aria-hidden="true"></span>
|
||||
{% trans "Module Templates" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
51
ots-signs/views/authed-user-menu.twig
Normal file
51
ots-signs/views/authed-user-menu.twig
Normal file
@@ -0,0 +1,51 @@
|
||||
{#
|
||||
OTS Signage Theme override
|
||||
Based on Xibo CMS default authed-user-menu.twig (master branch)
|
||||
Minimal change: add ots-user-menu class for easy verification
|
||||
#}
|
||||
{% if compact is defined and compact %}
|
||||
<div class="dropdown nav-item item ots-user-menu-compact">
|
||||
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarUserMenu">
|
||||
<img class="nav-avatar" src="{{ theme.uri("img/avatar.jpg") }}" />
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right ots-user-menu" aria-labelledby="navbarUserMenu">
|
||||
{% else %}
|
||||
<li class="dropdown nav-item item">
|
||||
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarUserMenu">
|
||||
<img class="nav-avatar" src="{{ theme.uri("img/avatar.jpg") }}" />
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right ots-user-menu" aria-labelledby="navbarUserMenu">
|
||||
{% endif %}
|
||||
<h6 class="dropdown-header">{{ currentUser.userName }}<br/>
|
||||
<div id="XiboClock">{{ clock }}</div>
|
||||
</h6>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item XiboFormButton" href="{{ url_for("user.preferences.form") }}" title="{% trans "Preferences" %}">{% trans "Preferences" %}</a>
|
||||
|
||||
{% if currentUser.featureEnabled("user.profile") %}
|
||||
<a class="dropdown-item XiboFormButton" href="{{ url_for("user.edit.profile.form") }}" title="{% trans "Edit Profile" %}">{% trans "Edit Profile" %}</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="dropdown-item XiboFormButton" href="{{ url_for("user.applications") }}" title="{% trans "View my authenticated applications" %}">{% trans "My Applications" %}</a>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" id="ots-theme-toggle" href="#" title="Toggle light/dark mode">
|
||||
<i class="fa fa-moon-o" id="ots-theme-icon" aria-hidden="true"></i>
|
||||
<span id="ots-theme-label">Dark Mode</span>
|
||||
</a>
|
||||
|
||||
<a class="dropdown-item" href="https://portal.oribi-tech.com" target="_blank" rel="noopener noreferrer" title="{% trans "Client Portal" %}">{% trans "Client Portal" %}</a>
|
||||
|
||||
<a class="dropdown-item XiboFormButton" href="{{ url_for("about") }}" title="{% trans "About the CMS" %}">{% trans "About" %}</a>
|
||||
|
||||
{% if not hideLogout %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" title="{% trans "Logout" %}" href="{{ logoutUrl }}">{% trans "Logout" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if compact is defined and compact %}
|
||||
</div>
|
||||
{% else %}
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
167
ots-signs/views/authed.twig
Normal file
167
ots-signs/views/authed.twig
Normal file
@@ -0,0 +1,167 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020-2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "base.twig" %}
|
||||
|
||||
{% block headContent %}
|
||||
<script nonce="{{ cspNonce }}">
|
||||
(function(){
|
||||
try{
|
||||
var stored = localStorage.getItem('ots-theme-mode');
|
||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
var mode = stored || (prefersLight ? 'light' : 'dark');
|
||||
if(mode === 'light') document.documentElement.classList.add('ots-light-mode');
|
||||
else document.documentElement.classList.remove('ots-light-mode');
|
||||
}catch(e){}
|
||||
})();
|
||||
|
||||
(function(){
|
||||
// Apply collapsed sidebar state early to prevent header flashing
|
||||
try {
|
||||
var collapsed = localStorage.getItem('otsTheme:sidebarCollapsed');
|
||||
if (collapsed === 'true') {
|
||||
// diagnostic
|
||||
try { console.debug && console.debug('otsTheme:sidebarCollapsed early:', collapsed); } catch(e){}
|
||||
// Add on <html> immediately; body may not be parsed yet
|
||||
document.documentElement.classList.add('ots-sidebar-collapsed');
|
||||
if (document.body) document.body.classList.add('ots-sidebar-collapsed');
|
||||
try { console.debug && console.debug('applied ots-sidebar-collapsed early'); } catch(e){}
|
||||
} else {
|
||||
try { console.debug && console.debug('otsTheme:sidebarCollapsed early: not set'); } catch(e){}
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
|
||||
</script>
|
||||
<style nonce="{{ cspNonce }}">
|
||||
/* Let the CSS variable theming (light/dark) control page background */
|
||||
html,body{background-color:var(--color-background,#0f172a)!important;color:var(--color-text-primary,#ffffff)!important}
|
||||
/* Hide the old topbar strip entirely — actions are now in .ots-page-actions */
|
||||
.row.header.header-side,
|
||||
.ots-topbar-strip { display: none !important; height: 0 !important; margin: 0 !important; padding: 0 !important; overflow: hidden !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set horizontalNav = currentUser.getOptionValue("navigationMenuPosition", theme.getSetting("NAVIGATION_MENU_POSITION", "vertical")) == "horizontal" %}
|
||||
|
||||
{% if not hideNavigation %}
|
||||
{% set hideNavigation = currentUser.getOptionValue("hideNavigation", "0") %}
|
||||
{% endif %}
|
||||
|
||||
<div {% if hideNavigation == "0" and not horizontalNav and not forceHide %}id="page-wrapper"{% endif %} class="active">
|
||||
|
||||
{% if hideNavigation == "0" and not forceHide %}
|
||||
{% if horizontalNav %}
|
||||
<nav class="navbar navbar-default navbar-expand-lg">
|
||||
<a class="navbar-brand xibo-logo-container" href="#">
|
||||
<img class="xibo-logo" src="{{ theme.uri("img/xibologo.png") }}">
|
||||
<span class="xibo-logo-text">
|
||||
<span class="brand-line brand-line-top">OTS</span>
|
||||
<span class="brand-line brand-line-bottom">Signs</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbar-collapse-1" aria-controls="navbarNav" aria-expanded="false">
|
||||
<span class="fa fa-bars"></span>
|
||||
</button>
|
||||
|
||||
<!-- Collect the nav links, forms, and other content for toggling -->
|
||||
<div class="navbar-collapse collapse justify-content-between" id="navbar-collapse-1">
|
||||
{% include "authed-topbar.twig" %}
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% include "authed-theme-topbar.twig" ignore missing %}
|
||||
{% if currentUser.featureEnabled("drawer") %}
|
||||
{% include "authed-notification-drawer.twig" %}
|
||||
{% endif %}
|
||||
{% include "authed-user-menu.twig" %}
|
||||
</ul>
|
||||
</div><!-- /.navbar-collapse -->
|
||||
</nav>
|
||||
{% else %}
|
||||
<div class="navbar-collapse navbar-collapse-side collapse" id="navbar-collapse-1">
|
||||
{% include "authed-sidebar.twig" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div id="content-wrapper" class="{% if hideNavigation == "1" %}no-nav{% endif %}{% if horizontalNav %} ots-horizontal-nav{% endif %}">
|
||||
{# Floating top-right actions: notification bell + user menu #}
|
||||
{# Hidden when horizontal nav is active — the navbar already has these controls #}
|
||||
{% if not forceHide and not horizontalNav %}
|
||||
<div class="ots-page-actions"{% if hideNavigation == "1" %} style="display:none!important"{% endif %}>
|
||||
{% include "authed-theme-topbar.twig" ignore missing %}
|
||||
{% if currentUser.featureEnabled("drawer") %}
|
||||
<div class="ots-topbar-action">
|
||||
{% include "authed-notification-drawer.twig" with { 'compact': true } %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="ots-topbar-action">
|
||||
{% include "authed-user-menu.twig" with { 'compact': true } %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="page-content">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% if settings.INSTANCE_SUSPENDED == "partial" %}
|
||||
<div class="alert alert-warning">{{ "CMS suspended. Displays will show cached content. Please contact your administrator."|trans }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% block pageContent %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% block pageFooter %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set helpLinks = helpService.getLinksForPage(route) %}
|
||||
{% set faultViewEnabled = currentUser.featureEnabled("fault.view") %}
|
||||
|
||||
{# Hide in mobile view (sm/<768px) #}
|
||||
<div id="help-pane" class="d-none d-md-flex help-pane"
|
||||
data-help-links="{{ helpLinks|json_encode }}"
|
||||
data-url-help-landing-page={{ helpService.getLandingPage() }}
|
||||
data-fault-view-enabled={{faultViewEnabled}}
|
||||
data-fault-view-url={{ url_for("fault.view") }}
|
||||
>
|
||||
<div class="help-pane-container" style="display: none;">
|
||||
</div>
|
||||
<div class="help-pane-btn">
|
||||
<i class="fas fa-question"></i>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScriptTemplates %}
|
||||
{# File upload templates and scripts #}
|
||||
{% include "include-file-upload.twig" %}
|
||||
{% endblock %}
|
||||
183
ots-signs/views/campaign-page.twig
Normal file
183
ots-signs/views/campaign-page.twig
Normal file
@@ -0,0 +1,183 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Campaigns"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Campaigns" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your campaigns and ad campaigns." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="campaignView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Campaigns" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('name', title) }}
|
||||
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}
|
||||
{% set title %}{% trans "Tags" %}{% endset %}
|
||||
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
|
||||
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
|
||||
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
|
||||
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
|
||||
{% endif %}
|
||||
|
||||
{% set title %}{% trans "Layouts" %}{% endset %}
|
||||
{% set values = [{id: 0, value: ""}, {id: 2, value: "Yes"}, {id: 1, value: "No"}] %}
|
||||
{{ inline.dropdown("hasLayouts", "single", title, 0, values, "id", "value") }}
|
||||
|
||||
{{ inline.hidden("folderId") }}
|
||||
|
||||
{% set title %}{% trans "Layout ID" %}{% endset %}
|
||||
{{ inline.number("layoutId", title, layoutId) }}
|
||||
|
||||
{% if currentUser.featureEnabled('ad.campaign') %}
|
||||
{% set title %}{% trans "Type" %}{% endset %}
|
||||
{% set options = [
|
||||
{ id: null, name: "" },
|
||||
{ id: "list", name: "Layout list"|trans },
|
||||
{ id: "ad", name: "Ad Campaign"|trans }
|
||||
] %}
|
||||
{{ inline.dropdown("type", "single", title, "both", options, "id", "name", helpText) }}
|
||||
{% endif %}
|
||||
|
||||
{% set title %}{% trans "Cycle Based Playback" %}{% endset %}
|
||||
{% set enabled %}{% trans "Enabled" %}{% endset %}
|
||||
{% set disabled %}{% trans "Disabled" %}{% endset %}
|
||||
{% set options = [
|
||||
{ optionid: "", option: "" },
|
||||
{ optionid: 0, option: disabled},
|
||||
{ optionid: 1, option: enabled}
|
||||
] %}
|
||||
{{ inline.dropdown("cyclePlaybackEnabled", "single", title, "", options, "optionid", "option") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabled("campaign.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Campaign" %}" href="{{ url_for("campaign.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="campaigns" class="table table-striped" data-content-type="campaign" data-content-id-name="campaignId" data-state-preference-name="campaignGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
{% if currentUser.featureEnabled('ad.campaign') %}
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Start Date" %}</th>
|
||||
<th>{% trans "End Date" %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "# Layouts" %}</th>
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
|
||||
<th>{% trans "Duration" %}</th>
|
||||
<th>{% trans "Cycle based Playback" %}</th>
|
||||
<th>{% trans "Play Count" %}</th>
|
||||
{% if currentUser.featureEnabled('ad.campaign') %}
|
||||
<th>{% trans "Target Type" %}</th>
|
||||
<th>{% trans "Target" %}</th>
|
||||
<th>{% trans "Plays" %}</th>
|
||||
<th>{% trans "Spend" %}</th>
|
||||
<th>{% trans "Impressions" %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Ref 1" %}</th>
|
||||
<th>{% trans "Ref 2" %}</th>
|
||||
<th>{% trans "Ref 3" %}</th>
|
||||
<th>{% trans "Ref 4" %}</th>
|
||||
<th>{% trans "Ref 5" %}</th>
|
||||
<th>{% trans "Created At" %}</th>
|
||||
<th>{% trans "Modified At" %}</th>
|
||||
<th>{% trans "Modified By" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
{# Initialise JS variables and translations #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}" defer>
|
||||
{# JS variables #}
|
||||
var campaignSearchURL = "{{ url_for('campaign.search') }}";
|
||||
var layoutSearchURL = "{{ url_for('layout.search') }}";
|
||||
|
||||
var folderViewEnabled = "{{ currentUser.featureEnabled('folder.view') }}";
|
||||
var adCampaignEnabled = "{{ currentUser.featureEnabled('ad.campaign') }}";
|
||||
var taggingEnabled = "{{ currentUser.featureEnabled('tag.tagging') }}";
|
||||
|
||||
{# Custom translations #}
|
||||
var campaignPageTrans = {
|
||||
list: "{% trans "List" %}",
|
||||
ad: "{% trans "Ad" %}",
|
||||
plays: "{% trans "Plays" %}",
|
||||
budget: "{% trans "Budget" %}",
|
||||
impressions: "{% trans "Impressions" %}",
|
||||
};
|
||||
</script>
|
||||
|
||||
{# Add page source code bundle #}
|
||||
<script src="{{ theme.rootUri() }}dist/pages/campaign-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
{% endblock %}
|
||||
161
ots-signs/views/command-page.twig
Normal file
161
ots-signs/views/command-page.twig
Normal file
@@ -0,0 +1,161 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020-2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Commands"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Commands" %}</h1>
|
||||
<p class="text-muted">{% trans "Create and manage commands for Displays." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Commands" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('command', title) }}
|
||||
|
||||
{% set title %}{% trans "Code" %}{% endset %}
|
||||
{{ inline.inputNameGrid('code', title, null, 'useRegexForCode', 'logicalOperatorCode') }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if currentUser.featureEnabled("command.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add Command" %}" href="{{ url_for("command.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="commands" class="table table-striped" data-state-preference-name="commandGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Code" %}</th>
|
||||
<th>{% trans "Available On" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Sharing" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table = $("#commands").DataTable({ "language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("command.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#commands").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "command", "render": dataTableSpacingPreformatted , responsivePriority: 2},
|
||||
{ "data": "code" , responsivePriority: 2},
|
||||
{
|
||||
"data": "availableOn",
|
||||
responsivePriority: 3,
|
||||
"render": function(data, type) {
|
||||
|
||||
if (type !== "display")
|
||||
return data;
|
||||
|
||||
var returnData = '';
|
||||
|
||||
if (typeof data !== undefined && data != null) {
|
||||
var arrayOfTags = data.split(',');
|
||||
|
||||
returnData += '<div class="permissionsDiv">';
|
||||
|
||||
for (var i = 0; i < arrayOfTags.length; i++) {
|
||||
var name = arrayOfTags[i];
|
||||
if (name !== '') {
|
||||
returnData += '<li class="badge ' + ((name === 'lg') ? '' : 'capitalize') + '">' + name.replace("lg", "webOS").replace("sssp", "Tizen") + '</span></li>'
|
||||
}
|
||||
}
|
||||
|
||||
returnData += '</div>';
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
},
|
||||
{ "data": "description", responsivePriority: 3 },
|
||||
{
|
||||
"data": "groupsWithPermissions",
|
||||
responsivePriority: 3,
|
||||
"render": dataTableCreatePermissions
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#commands_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
461
ots-signs/views/dashboard-icon-page.twig
Normal file
461
ots-signs/views/dashboard-icon-page.twig
Normal file
@@ -0,0 +1,461 @@
|
||||
{#
|
||||
/**
|
||||
* OTS Signage Theme - Icon Dashboard Override
|
||||
*
|
||||
* Custom stylized icon dashboard that uses card-based buttons
|
||||
* matching the OTS dashboard design system.
|
||||
*
|
||||
* Based on Xibo CMS dashboard-icon-page.twig
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block pageContent %}
|
||||
{% include "theme-dashboard-message.twig" ignore missing %}
|
||||
|
||||
<div class="dashboard-page">
|
||||
<div class="page-header">
|
||||
<h1>{% trans "Dashboard" %}</h1>
|
||||
<p class="text-muted">{% trans "Quick access to all areas of your signage network" %}</p>
|
||||
</div>
|
||||
|
||||
{# ── Scheduling ────────────────────────────────────────────── #}
|
||||
{% set scheduleCount = currentUser.featureEnabledCount(["schedule.view", "daypart.view"]) %}
|
||||
{% if scheduleCount > 0 %}
|
||||
<div class="icon-dash-section">
|
||||
<h3 class="section-title"><i class="fa fa-calendar"></i> {% trans "Scheduling" %}</h3>
|
||||
<div class="icon-dash-grid">
|
||||
{% if currentUser.featureEnabled("schedule.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("schedule.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
|
||||
<i class="fa fa-calendar-check-o"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Schedule" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Manage event schedules" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("daypart.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("daypart.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Dayparting" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Define time slots" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Design ────────────────────────────────────────────────── #}
|
||||
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<div class="icon-dash-section">
|
||||
<h3 class="section-title"><i class="fa fa-paint-brush"></i> {% trans "Design" %}</h3>
|
||||
<div class="icon-dash-grid">
|
||||
{% if currentUser.featureEnabled("campaign.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("campaign.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--green">
|
||||
<i class="fa fa-bullhorn"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Campaigns" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Organise layout playlists" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("layout.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("layout.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
|
||||
<i class="fa fa-columns"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Layouts" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Design screen layouts" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("template.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("template.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--purple">
|
||||
<i class="fa fa-clone"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Templates" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Reusable layout templates" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("resolution.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("resolution.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--teal">
|
||||
<i class="fa fa-expand"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Resolutions" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Screen resolution presets" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Library ───────────────────────────────────────────────── #}
|
||||
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<div class="icon-dash-section">
|
||||
<h3 class="section-title"><i class="fa fa-picture-o"></i> {% trans "Library" %}</h3>
|
||||
<div class="icon-dash-grid">
|
||||
{% if currentUser.featureEnabled("library.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("library.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--orange">
|
||||
<i class="fa fa-image"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Library" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Upload & manage media" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("playlist.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("playlist.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
|
||||
<i class="fa fa-list"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Playlists" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Content playlists" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("dataset.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("dataset.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
|
||||
<i class="fa fa-database"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "DataSets" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Tabular data sources" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("menuBoard.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("menuBoard.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--red">
|
||||
<i class="fa fa-cutlery"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Menu Boards" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Digital menu management" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Displays ──────────────────────────────────────────────── #}
|
||||
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view"]) %}
|
||||
{% if countViewable > 0 %}
|
||||
<div class="icon-dash-section">
|
||||
<h3 class="section-title"><i class="fa fa-desktop"></i> {% trans "Displays" %}</h3>
|
||||
<div class="icon-dash-grid">
|
||||
{% if currentUser.featureEnabled("displays.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("display.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--green">
|
||||
<i class="fa fa-desktop"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Displays" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Manage all screens" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("displaygroup.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("displaygroup.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--blue">
|
||||
<i class="fa fa-object-group"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Display Groups" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Organise screen groups" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("displayprofile.view") %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("displayprofile.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--purple">
|
||||
<i class="fa fa-cog"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Display Settings" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "Player configuration profiles" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Administration ────────────────────────────────────────── #}
|
||||
{% set showAdmin = false %}
|
||||
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
|
||||
{% set showAdmin = true %}
|
||||
{% endif %}
|
||||
{% if currentUser.isSuperUser() %}
|
||||
{% set showAdmin = true %}
|
||||
{% endif %}
|
||||
|
||||
{% if showAdmin %}
|
||||
<div class="icon-dash-section">
|
||||
<h3 class="section-title"><i class="fa fa-cogs"></i> {% trans "Administration" %}</h3>
|
||||
<div class="icon-dash-grid">
|
||||
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("user.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--indigo">
|
||||
<i class="fa fa-users"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Users" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "User accounts & permissions" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.isSuperUser() %}
|
||||
<a class="icon-dash-card dashboard-card" href="{{ url_for("admin.view") }}">
|
||||
<div class="icon-dash-card-icon icon-dash-card-icon--orange">
|
||||
<i class="fa fa-cogs"></i>
|
||||
</div>
|
||||
<div class="icon-dash-card-body">
|
||||
<span class="icon-dash-card-title">{% trans "Settings" %}</span>
|
||||
<span class="icon-dash-card-desc">{% trans "CMS system configuration" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<style>
|
||||
/* ===================================================================
|
||||
ICON DASHBOARD – Card Button Styles
|
||||
Matches the OTS dashboard-card design system
|
||||
=================================================================== */
|
||||
|
||||
/* Section spacing */
|
||||
.icon-dash-section {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.icon-dash-section:first-of-type {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
/* Grid layout – responsive card grid */
|
||||
.icon-dash-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
/* Individual card – inherits .dashboard-card base from override.css */
|
||||
.icon-dash-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 22px 24px;
|
||||
text-decoration: none !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* Override rigid dashboard-card flex-direction:column if set */
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
/* Subtle radial glow matching kpi-card--modern */
|
||||
.icon-dash-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top right, rgba(59, 130, 246, 0.10), transparent 60%);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.icon-dash-card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Icon container */
|
||||
.icon-dash-card-icon {
|
||||
flex-shrink: 0;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
font-size: 22px;
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
.icon-dash-card:hover .icon-dash-card-icon {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* Icon colour variants */
|
||||
.icon-dash-card-icon--blue {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.28), rgba(59, 130, 246, 0.12));
|
||||
color: #60a5fa;
|
||||
}
|
||||
.icon-dash-card-icon--green {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.28), rgba(16, 185, 129, 0.12));
|
||||
color: #34d399;
|
||||
}
|
||||
.icon-dash-card-icon--orange {
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.28), rgba(245, 158, 11, 0.12));
|
||||
color: #fbbf24;
|
||||
}
|
||||
.icon-dash-card-icon--red {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.28), rgba(239, 68, 68, 0.12));
|
||||
color: #f87171;
|
||||
}
|
||||
.icon-dash-card-icon--purple {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.28), rgba(124, 58, 237, 0.12));
|
||||
color: #a78bfa;
|
||||
}
|
||||
.icon-dash-card-icon--indigo {
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.28), rgba(99, 102, 241, 0.12));
|
||||
color: #818cf8;
|
||||
}
|
||||
.icon-dash-card-icon--teal {
|
||||
background: linear-gradient(135deg, rgba(20, 184, 166, 0.28), rgba(20, 184, 166, 0.12));
|
||||
color: #2dd4bf;
|
||||
}
|
||||
|
||||
/* Text area */
|
||||
.icon-dash-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
/* Reset inherited dashboard-card body padding */
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.icon-dash-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon-dash-card-desc {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Hover effects matching action-card--modern */
|
||||
.icon-dash-card:hover {
|
||||
border-color: rgba(59, 130, 246, 0.45) !important;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 20px 40px rgba(8, 15, 30, 0.45) !important;
|
||||
}
|
||||
|
||||
.icon-dash-card:active {
|
||||
transform: translateY(0px);
|
||||
box-shadow: 0 10px 20px rgba(8, 15, 30, 0.35) !important;
|
||||
}
|
||||
|
||||
/* Section title with icon */
|
||||
.icon-dash-section .section-title i {
|
||||
margin-right: 8px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* ── Light mode overrides ─────────────────────────────────────── */
|
||||
body.ots-light-mode .icon-dash-card {
|
||||
background: linear-gradient(180deg, #ffffff, #f8fafc) !important;
|
||||
border-color: rgba(148, 163, 184, 0.25) !important;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode .icon-dash-card:hover {
|
||||
background: linear-gradient(180deg, #ffffff, #f1f5f9) !important;
|
||||
border-color: rgba(59, 130, 246, 0.4) !important;
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode .icon-dash-card-desc {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* ── Responsive adjustments ───────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.icon-dash-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.icon-dash-card {
|
||||
padding: 16px 18px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.icon-dash-card-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 18px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.icon-dash-card-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.icon-dash-card-desc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.icon-dash-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.icon-dash-card-desc {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
1211
ots-signs/views/dashboard-status-page.twig
Normal file
1211
ots-signs/views/dashboard-status-page.twig
Normal file
File diff suppressed because it is too large
Load Diff
596
ots-signs/views/dataset-page.twig
Normal file
596
ots-signs/views/dataset-page.twig
Normal file
@@ -0,0 +1,596 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
{% import "forms.twig" as forms %}
|
||||
|
||||
{% block title %}{{ "DataSets"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "DataSets" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage structured data sources." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="dataSetView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter DataSets" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline" onsubmit="return false">
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('dataSet', title) }}
|
||||
|
||||
{% set title %}{% trans "Code" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show items which match the provided code" %}{% endset %}
|
||||
{{ inline.input("code", title, "", helpText) }}
|
||||
|
||||
{% set title %}{% trans "Owner" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("user.search") },
|
||||
{ name: "data-search-term", value: "userName" },
|
||||
{ name: "data-search-term-tags", value: "tags" },
|
||||
{ name: "data-id-property", value: "userId" },
|
||||
{ name: "data-text-property", value: "userName" },
|
||||
{ name: "data-initial-key", value: "userId" },
|
||||
] %}
|
||||
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
|
||||
{{ inline.hidden("folderId") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabled("dataset.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new DataSet" %}" href="{{ url_for("dataSet.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="datasets" class="table table-striped" data-state-preference-name="dataSetGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Code" %}</th>
|
||||
<th>{% trans "Remote?" %}</th>
|
||||
<th>{% trans "Real time?" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Sharing" %}</th>
|
||||
<th>{% trans "Last Sync" %}</th>
|
||||
<th>{% trans "Data Last Modified" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table;
|
||||
$(document).ready(function() {
|
||||
{% if not currentUser.featureEnabled("folder.view") %}
|
||||
disableFolders();
|
||||
{% endif %}
|
||||
|
||||
table = $("#datasets").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 0, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("dataSet.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#datasets").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "dataSetId", responsivePriority: 2 },
|
||||
{ "data": "dataSet", "render": dataTableSpacingPreformatted, responsivePriority: 2 },
|
||||
{ "data": "description", responsivePriority: 4 },
|
||||
{ "data": "code", responsivePriority: 3 },
|
||||
{
|
||||
"data": "isRemote",
|
||||
responsivePriority: 3,
|
||||
"render": dataTableTickCrossColumn
|
||||
},
|
||||
{
|
||||
data: 'isRealTime',
|
||||
responsivePriority: 3,
|
||||
render: dataTableTickCrossColumn,
|
||||
},
|
||||
{ "data": "owner", responsivePriority: 3 },
|
||||
{
|
||||
"data": "groupsWithPermissions",
|
||||
responsivePriority: 3,
|
||||
"render": dataTableCreatePermissions
|
||||
},
|
||||
{
|
||||
"data": "lastSync",
|
||||
responsivePriority: 4,
|
||||
"render": dataTableDateFromUnix
|
||||
},
|
||||
{
|
||||
"data": "lastDataEdit",
|
||||
responsivePriority: 4,
|
||||
"render": dataTableDateFromUnix
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', function(e, settings) {
|
||||
dataTableDraw(e, settings);
|
||||
|
||||
// Upload form
|
||||
$(".dataSetImportForm").click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var template = Handlebars.compile($("#template-dataset-upload").html());
|
||||
var data = table.row($(this).closest("tr")).data();
|
||||
var columns = [];
|
||||
var i = 1;
|
||||
|
||||
$.each(data.columns, function (index, element) {
|
||||
if (element.dataSetColumnTypeId === 1) {
|
||||
element.index = i;
|
||||
columns.push(element);
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle bars and open a dialog
|
||||
bootbox.dialog({
|
||||
message: template({
|
||||
trans: {
|
||||
addFiles: "{% trans "Add CSV Files" %}",
|
||||
startUpload: "{% trans "Start upload" %}",
|
||||
cancelUpload: "{% trans "Cancel upload" %}",
|
||||
processing: "{% trans "Processing..." %}"
|
||||
},
|
||||
upload: {
|
||||
maxSize: {{ libraryUpload.maxSize }},
|
||||
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
|
||||
validExt: "{{ libraryUpload.validExt }}",
|
||||
utf8Message: "{% trans "If the CSV file contains non-ASCII characters please ensure the file is UTF-8 encoded" %}"
|
||||
},
|
||||
columns: columns
|
||||
}),
|
||||
title: "{% trans "CSV Import" %}",
|
||||
size: 'large',
|
||||
buttons: {
|
||||
main: {
|
||||
label: "{% trans "Done" %}",
|
||||
className: "btn-primary btn-bb-main",
|
||||
callback: function() {
|
||||
table.ajax.reload();
|
||||
XiboDialogClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).on('shown.bs.modal', function() {
|
||||
// Configure the upload form
|
||||
var url = "{{ url_for("dataSet.import", {id: ':id'}) }}".replace(":id", data.dataSetId);
|
||||
var form = $(this).find("form");
|
||||
var refreshSessionInterval;
|
||||
|
||||
// Initialize the jQuery File Upload widget:
|
||||
form.fileupload({
|
||||
url: url,
|
||||
disableImageResize: true
|
||||
});
|
||||
|
||||
// Upload server status check for browsers with CORS support:
|
||||
if ($.support.cors) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'HEAD'
|
||||
}).fail(function () {
|
||||
$('<span class="alert alert-error"/>')
|
||||
.text('Upload server currently unavailable - ' + new Date())
|
||||
.appendTo(form);
|
||||
});
|
||||
}
|
||||
|
||||
// Enable iframe cross-domain access via redirect option:
|
||||
form.fileupload(
|
||||
'option',
|
||||
'redirect',
|
||||
window.location.href.replace(
|
||||
/\/[^\/]*$/,
|
||||
'/cors/result.html?%s'
|
||||
)
|
||||
);
|
||||
|
||||
form.bind('fileuploadsubmit', function (e, data) {
|
||||
var inputs = data.context.find(':input');
|
||||
if (inputs.filter('[required][value=""]').first().focus().length) {
|
||||
return false;
|
||||
}
|
||||
data.formData = inputs.serializeArray().concat(form.serializeArray());
|
||||
|
||||
inputs.filter("input").prop("disabled", true);
|
||||
}).bind('fileuploadstart', function (e, data) {
|
||||
|
||||
// Show progress data
|
||||
form.find('.fileupload-progress .progress-extended').show();
|
||||
form.find('.fileupload-progress .progress-end').hide();
|
||||
|
||||
if (form.fileupload("active") <= 0)
|
||||
refreshSessionInterval = setInterval("XiboPing('" + pingUrl + "?refreshSession=true')", 1000 * 60 * 3);
|
||||
|
||||
return true;
|
||||
}).bind('fileuploaddone', function (e, data) {
|
||||
if (refreshSessionInterval != null && form.fileupload("active") <= 0)
|
||||
clearInterval(refreshSessionInterval);
|
||||
}).bind('fileuploadprogressall', function (e, data) {
|
||||
// Hide progress data and show processing
|
||||
if(data.total > 0 && data.loaded == data.total) {
|
||||
form.find('.fileupload-progress .progress-extended').hide();
|
||||
form.find('.fileupload-progress .progress-end').show();
|
||||
}
|
||||
}).bind('fileuploadadded fileuploadcompleted fileuploadfinished', function (e, data) {
|
||||
// Get uploaded and downloaded files and toggle Done button
|
||||
var filesToUploadCount = form.find('tr.template-upload').length;
|
||||
var $button = form.parents('.modal:first').find('button.btn-bb-main');
|
||||
|
||||
if(filesToUploadCount == 0) {
|
||||
$button.removeAttr('disabled');
|
||||
} else {
|
||||
$button.attr('disabled', 'disabled');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#datasets_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function dataSetFormOpen(dialog) {
|
||||
// Bind the remote dataset test button
|
||||
$(dialog).find("#dataSetRemoteTestButton").on('click', function() {
|
||||
var $form = $(dialog).find("form");
|
||||
XiboRemoteRequest("{{ url_for("dataSet.test.remote") }}", $form.serializeObject(), function(response) {
|
||||
if (!response.success || !$.trim(response.data.entries)) {
|
||||
response.data = response.message;
|
||||
}
|
||||
$("#datasetRemoteTestRequestResult").html('<pre style="height: 300px; overflow: scroll">' + JSON.stringify(response.data, null, 3) + '</pre>');
|
||||
});
|
||||
});
|
||||
|
||||
// Set up some dependencies between the isRemote checkbox and the tabs related to remote datasets
|
||||
onRemoteFieldChanged(dialog);
|
||||
|
||||
// show data source dropdown if real time is checked
|
||||
onIsRealTimeFieldChanged(dialog);
|
||||
|
||||
$(dialog).find("#isRemote").on('change', function() {
|
||||
onRemoteFieldChanged(dialog);
|
||||
});
|
||||
|
||||
$(dialog).find("#isRealTime").on('change', function() {
|
||||
onIsRealTimeFieldChanged(dialog);
|
||||
});
|
||||
|
||||
// Auth field
|
||||
onAuthenticationFieldChanged(dialog);
|
||||
|
||||
$(dialog).find("#authentication").on('change', function() {
|
||||
onAuthenticationFieldChanged(dialog);
|
||||
});
|
||||
|
||||
// remote DataSet source
|
||||
onSourceFieldChanged(dialog);
|
||||
$(dialog).find('#sourceId').on('change', function() {
|
||||
onSourceFieldChanged(dialog);
|
||||
});
|
||||
|
||||
// Validate form manually because
|
||||
// uri field depends on isRemote being checked
|
||||
if (forms != undefined) {
|
||||
const $form = $(dialog).find('form');
|
||||
forms.validateForm(
|
||||
$form, // form
|
||||
$form.parent(), // container
|
||||
{
|
||||
submitHandler: XiboFormSubmit,
|
||||
rules: {
|
||||
uri: {
|
||||
required: function(element) {
|
||||
return $form.find('#isRemote').is(':checked')
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onIsRealTimeFieldChanged(dialog) {
|
||||
var isRealTime = $(dialog).find("#isRealTime").is(":checked");
|
||||
var dataSourceField = $(dialog).find("#dataSourceField");
|
||||
var dataConnectorSource = $(dialog).find("#dataConnectorSource");
|
||||
|
||||
if (isRealTime) {
|
||||
// show and enable data connector source
|
||||
dataSourceField.removeClass("d-none");
|
||||
dataConnectorSource.prop('disabled', false)
|
||||
} else {
|
||||
// hide and disable data connector source
|
||||
dataSourceField.addClass("d-none");
|
||||
dataConnectorSource.prop('disabled', true)
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoteFieldChanged(dialog) {
|
||||
var isRemote = $(dialog).find("#isRemote").is(":checked");
|
||||
var $remoteTabs = $(dialog).find(".tabForRemoteDataSet");
|
||||
|
||||
if (isRemote) {
|
||||
$remoteTabs.removeClass("d-none");
|
||||
} else {
|
||||
$remoteTabs.addClass("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
function onAuthenticationFieldChanged(dialog) {
|
||||
var authentication = $(dialog).find("#authentication").val();
|
||||
var $authFieldUserName = $(dialog).find(".auth-field-username");
|
||||
var $authFieldPassword = $(dialog).find(".auth-field-password");
|
||||
|
||||
if (authentication === "none") {
|
||||
$authFieldUserName.addClass("d-none");
|
||||
$authFieldPassword.addClass("d-none");
|
||||
} else if (authentication === "bearer") {
|
||||
$authFieldUserName.addClass("d-none");
|
||||
$authFieldPassword.removeClass("d-none");
|
||||
} else {
|
||||
$authFieldUserName.removeClass("d-none");
|
||||
$authFieldPassword.removeClass("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
function onSourceFieldChanged(dialog) {
|
||||
var sourceId = $(dialog).find('#sourceId').val();
|
||||
var $jsonSource = $(dialog).find(".json-source-field");
|
||||
var $csvSource = $(dialog).find(".csv-source-field");
|
||||
|
||||
if (sourceId == 1) {
|
||||
$jsonSource.removeClass('d-none');
|
||||
$csvSource.addClass('d-none');
|
||||
} else {
|
||||
$jsonSource.addClass('d-none');
|
||||
$csvSource.removeClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function deleteMultiSelectFormOpen(dialog) {
|
||||
{% set message = 'Delete any associated data?' %}
|
||||
|
||||
var $input = $('<input type=checkbox id="deleteData" name="deleteData"> {{ message|trans|e }} </input>');
|
||||
$input.on('change', function() {
|
||||
dialog.data().commitData = {deleteData: $(this).val()};
|
||||
});
|
||||
$(dialog).find('.modal-body').append($input);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScriptTemplates %}
|
||||
{{ parent() }}
|
||||
|
||||
{% verbatim %}
|
||||
|
||||
<script type="text/x-handlebars-template" id="template-dataset-upload">
|
||||
<form class="form-horizontal" method="post" enctype="multipart/form-data" data-max-file-size="{{ upload.maxSize }}" data-accept-file-types="/(\.|\/)csv/i">
|
||||
<div class="row fileupload-buttonbar">
|
||||
<div class="card p-3 mb-3 bg-light">
|
||||
{{ upload.maxSizeMessage }} <br>
|
||||
{{ upload.utf8Message }}
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<!-- The fileinput-button span is used to style the file input field as button -->
|
||||
<span class="btn btn-success fileinput-button">
|
||||
<i class="fa fa-plus"></i>
|
||||
<span>{{ trans.addFiles }}</span>
|
||||
<input type="file" name="files">
|
||||
</span>
|
||||
<button type="submit" class="btn btn-primary start">
|
||||
<i class="fa fa-upload"></i>
|
||||
<span>{{ trans.startUpload }}</span>
|
||||
</button>
|
||||
<button type="reset" class="btn btn-warning cancel">
|
||||
<i class="fa fa-ban"></i>
|
||||
<span>{{ trans.cancelUpload }}</span>
|
||||
</button>
|
||||
<!-- The loading indicator is shown during file processing -->
|
||||
<span class="fileupload-loading"></span>
|
||||
</div>
|
||||
<!-- The global progress information -->
|
||||
<div class="col-md-4 fileupload-progress fade">
|
||||
<!-- The global progress bar -->
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width:0%;">
|
||||
<div class="sr-only"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The extended global progress information -->
|
||||
<div class="progress-extended"> </div>
|
||||
<!-- Processing info container -->
|
||||
<div class="progress-end" style="display:none;">{{ trans.processing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% endverbatim %}
|
||||
{% set title %}{% trans "Overwrite existing data?" %}{% endset %}
|
||||
{% set helpText %}{% trans "Erase all content in this DataSet and overwrite it with the new content in this import." %}{% endset %}
|
||||
{{ forms.checkbox("overwrite", title, "", helpText) }}
|
||||
|
||||
{% set title %}{% trans "Ignore first row?" %}{% endset %}
|
||||
{% set helpText %}{% trans "Ignore the first row? Useful if the CSV has headings." %}{% endset %}
|
||||
{{ forms.checkbox("ignorefirstrow", title, "", helpText) }}
|
||||
|
||||
{% set message %}{% trans "In the fields below please enter the column number in the CSV file that corresponds to the Column Heading listed. This should be done before Adding the file." %}{% endset %}
|
||||
{{ forms.message(message) }}
|
||||
|
||||
{% verbatim %}
|
||||
{{#each columns}}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 control-label" for="csvImport_{{dataSetColumnId}}">{{heading}}</label>
|
||||
<div class="col-sm-10">
|
||||
<input class="form-control" name="csvImport_{{dataSetColumnId}}" type="number" id="csvImport_{{dataSetColumnId}}" value="{{ index }}" />
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The table listing the files available for upload/download -->
|
||||
<table role="presentation" class="table table-striped"><tbody class="files"></tbody></table>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<!-- The template to display files available for upload -->
|
||||
<script id="template-dataset-upload" type="text/x-tmpl">
|
||||
{% for (var i=0, file; file=o.files[i]; i++) { %}
|
||||
<tr class="template-upload">
|
||||
<td>
|
||||
<span class="fileupload-preview"></span>
|
||||
</td>
|
||||
<td class="title">
|
||||
{% if (file.error) { %}
|
||||
<div><span class="label label-danger">{%=file.error%}</span></div>
|
||||
{% } %}
|
||||
{% if (!file.error) { %}
|
||||
<label for="name[]"><input name="name[]" type="text" id="name" value="" /></label>
|
||||
{% } %}
|
||||
</td>
|
||||
<td>
|
||||
<p class="size">{%=o.formatFileSize(file.size)%}</p>
|
||||
{% if (!o.files.error) { %}
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width:0%;">
|
||||
<div class="sr-only"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
</td>
|
||||
<td class="btn-group">
|
||||
{% if (!o.files.error && !i && !o.options.autoUpload) { %}
|
||||
<button class="btn btn-primary start">
|
||||
<i class="fa fa-upload"></i>
|
||||
</button>
|
||||
{% } %}
|
||||
{% if (!i) { %}
|
||||
<button class="btn btn-warning cancel">
|
||||
<i class="fa fa-ban"></i>
|
||||
</button>
|
||||
{% } %}
|
||||
</td>
|
||||
</tr>
|
||||
{% } %}
|
||||
</script>
|
||||
<!-- The template to display files available for download -->
|
||||
<script id="template-dataset-download" type="text/x-tmpl">
|
||||
{% for (var i=0, file; file=o.files[i]; i++) { %}
|
||||
<tr class="template-download">
|
||||
<td>
|
||||
<p class="name" id="{%=file.storedas%}" status="{% if (file.error) { %}error{% } %}">
|
||||
{%=file.name%}
|
||||
</p>
|
||||
{% if (file.error) { %}
|
||||
<div><span class="label label-danger">{%=file.error%}</span></div>
|
||||
{% } %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="size">{%=o.formatFileSize(file.size)%}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% } %}
|
||||
</script>
|
||||
|
||||
{% endverbatim %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
138
ots-signs/views/datatable-contrast.twig
Normal file
138
ots-signs/views/datatable-contrast.twig
Normal file
@@ -0,0 +1,138 @@
|
||||
/* High-specificity DataTables contrast overrides
|
||||
Ensures table body text is readable against dark theme backgrounds.
|
||||
Light text on dark backgrounds (dark mode).
|
||||
Dark text on light backgrounds (light mode).
|
||||
*/
|
||||
|
||||
/* FIRST: Light mode rules that check actual background colors (not dependent on body class) */
|
||||
#datatable-container table.dataTable tbody td,
|
||||
#datatable-container .dataTables_wrapper table.dataTable tbody td,
|
||||
.ots-table-card table.dataTable tbody td,
|
||||
.ots-table-card table.dataTable tbody td * {
|
||||
color: var(--color-text-primary) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable thead th,
|
||||
.ots-table-card table.dataTable thead th,
|
||||
#datatable-container table.dataTable thead th * {
|
||||
color: var(--color-text-secondary) !important;
|
||||
opacity: 1 !important;
|
||||
background-color: var(--color-surface) !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody tr.table-success td,
|
||||
#datatable-container table.dataTable tbody tr.success td,
|
||||
#datatable-container table.dataTable tbody tr.selected td,
|
||||
#datatable-container table.dataTable tbody tr.highlight td {
|
||||
background-color: rgba(16, 185, 129, 0.1) !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody td .btn,
|
||||
#datatable-container table.dataTable tbody td .badge,
|
||||
#datatable-container table.dataTable tbody td .dropdown-toggle {
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody tr {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody tr:nth-child(even) {
|
||||
background-color: var(--color-surface-elevated) !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody tr:hover {
|
||||
background-color: rgba(37, 99, 235, 0.06) !important;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input,
|
||||
.dataTables_wrapper .dataTables_length select,
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
color: var(--color-text-primary) !important;
|
||||
background: var(--color-surface) !important;
|
||||
border-color: var(--color-border) !important;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_info,
|
||||
.dataTables_wrapper .dataTables_filter,
|
||||
.dataTables_wrapper .dataTables_length,
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
/* SECOND: Explicit light mode class overrides for when .ots-light-mode is present */
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody td,
|
||||
body.ots-light-mode #datatable-container .dataTables_wrapper table.dataTable tbody td,
|
||||
body.ots-light-mode .ots-table-card table.dataTable tbody td,
|
||||
body.ots-light-mode .ots-table-card table.dataTable tbody td * {
|
||||
color: #0f172a !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable thead th,
|
||||
body.ots-light-mode .ots-table-card table.dataTable thead th,
|
||||
body.ots-light-mode #datatable-container table.dataTable thead th * {
|
||||
color: #334155 !important;
|
||||
opacity: 1 !important;
|
||||
background-color: #f1f5f9 !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr.table-success td,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr.success td,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr.selected td,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr.highlight td {
|
||||
background-color: rgba(16, 185, 129, 0.1) !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody td .btn,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody td .badge,
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody td .dropdown-toggle {
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr:nth-child(even) {
|
||||
background-color: #f1f5f9 !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode #datatable-container table.dataTable tbody tr:hover {
|
||||
background-color: rgba(37, 99, 235, 0.06) !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_filter input,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_length select,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
color: #0f172a !important;
|
||||
background: #ffffff !important;
|
||||
border-color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_info,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_filter,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_length,
|
||||
body.ots-light-mode .dataTables_wrapper .dataTables_paginate {
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable tbody td img,
|
||||
#datatable-container table.dataTable tbody td svg {
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
#datatable-container table.dataTable thead th.sorting:after,
|
||||
#datatable-container table.dataTable thead th.sorting_asc:after,
|
||||
#datatable-container table.dataTable thead th.sorting_desc:after {
|
||||
color: rgba(255,255,255,0.7) !important;
|
||||
}
|
||||
|
||||
.ots-table-card table.dataTable tbody tr td,
|
||||
.ots-table-card table.dataTable tbody tr td * {
|
||||
-webkit-text-fill-color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
261
ots-signs/views/daypart-page.twig
Normal file
261
ots-signs/views/daypart-page.twig
Normal file
@@ -0,0 +1,261 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Dayparting"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Dayparting" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage time-based scheduling rules." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Dayparts" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('name', title) }}
|
||||
|
||||
{% set title %}{% trans "Retired" %}{% endset %}
|
||||
{% set option1 = "Yes"|trans %}
|
||||
{% set option2 = "No"|trans %}
|
||||
{% set values = [{id: 1, value: option1}, {id: 0, value: option2}] %}
|
||||
{{ inline.dropdown("isRetired", "single", title, 0, values, "id", "value") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if currentUser.featureEnabled("daypart.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Daypart" %}" href="{{ url_for("daypart.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="dayparts" class="table table-striped" data-state-preference-name="daypartGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Start Time" %}</th>
|
||||
<th>{% trans "End Time" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
|
||||
var table = $("#dayparts").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("daypart.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#dayparts").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "name", "render": dataTableSpacingPreformatted , responsivePriority: 2},
|
||||
{ "data": "description" },
|
||||
{ "data": "startTime" },
|
||||
{ "data": "endTime" },
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#dayparts_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
|
||||
function dayPartFormOpen(dialog) {
|
||||
// Render a set of exceptions
|
||||
$exceptions = $(dialog).find("#dayPartExceptions");
|
||||
|
||||
// Days of the week translations
|
||||
var daysOfTheWeek = [
|
||||
{ day: "Mon", title: "{% trans "Monday" %}" },
|
||||
{ day: "Tue", title: "{% trans "Tuesday" %}" },
|
||||
{ day: "Wed", title: "{% trans "Wednesday" %}" },
|
||||
{ day: "Thu", title: "{% trans "Thursday" %}" },
|
||||
{ day: "Fri", title: "{% trans "Friday" %}" },
|
||||
{ day: "Sat", title: "{% trans "Saturday" %}" },
|
||||
{ day: "Sun", title: "{% trans "Sunday" %}" }
|
||||
];
|
||||
|
||||
// Compile the handlebars template
|
||||
var exceptionsTemplate = Handlebars.compile($("#dayPartExceptionsTemplate").html());
|
||||
|
||||
if (dialog.data().extra.exceptions.length == 0) {
|
||||
// Contexts for template
|
||||
var context = {
|
||||
daysOfWeek: daysOfTheWeek,
|
||||
buttonGlyph: "fa-plus",
|
||||
exceptionDay: "",
|
||||
exceptionStart: "",
|
||||
exceptionEnd: "",
|
||||
fieldId: 0
|
||||
};
|
||||
|
||||
// Append
|
||||
$exceptions.append(exceptionsTemplate(context));
|
||||
|
||||
XiboInitialise("#" + $exceptions.prop("id"));
|
||||
} else {
|
||||
// For each of the existing exceptions, create form components
|
||||
var i = 0;
|
||||
$.each(dialog.data().extra.exceptions, function (index, field) {
|
||||
i++;
|
||||
// call the template
|
||||
var context = {
|
||||
daysOfWeek: daysOfTheWeek,
|
||||
buttonGlyph: ((i == 1) ? "fa-plus" : "fa-minus"),
|
||||
exceptionDay: field.day,
|
||||
exceptionStart: field.start,
|
||||
exceptionEnd: field.end,
|
||||
fieldId: i
|
||||
};
|
||||
|
||||
$exceptions.append(exceptionsTemplate(context));
|
||||
|
||||
XiboInitialise("#" + $exceptions.prop("id"));
|
||||
});
|
||||
}
|
||||
|
||||
// Nabble the resulting buttons
|
||||
$exceptions.on("click", "button", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// find the gylph
|
||||
if ($(this).find("i").hasClass("fa-plus")) {
|
||||
var context = {
|
||||
daysOfWeek: daysOfTheWeek,
|
||||
buttonGlyph: "fa-minus",
|
||||
exceptionDay: "",
|
||||
exceptionStart: "",
|
||||
exceptionEnd: "",
|
||||
fieldId: $exceptions.find('.form-group').length + 1
|
||||
};
|
||||
|
||||
$exceptions.append(exceptionsTemplate(context));
|
||||
|
||||
XiboInitialise("#" + $exceptions.prop("id"));
|
||||
} else {
|
||||
// Remove this row
|
||||
$(this).closest(".form-group").remove();
|
||||
}
|
||||
});
|
||||
|
||||
// check if we already have this day in exceptions array, if so remove the row with a message.
|
||||
$exceptions.on("change", "select", function() {
|
||||
var selectedDays = [];
|
||||
$('select').not('#' + $(this).attr('id')).each(function(i) {
|
||||
selectedDays.push($(this).val());
|
||||
});
|
||||
|
||||
if (selectedDays.includes(this.value)) {
|
||||
toastr.error(translations.dayPartExceptionErrorMessage);
|
||||
// Remove this row
|
||||
$(this).closest(".form-group").remove();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Equals helper for the templates below
|
||||
Handlebars.registerHelper('eq', function(v1, v2, opts) {
|
||||
if (v1 === v2) {
|
||||
return opts.fn(this);
|
||||
} else {
|
||||
return opts.inverse(this);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% verbatim %}
|
||||
<script type="text/x-handlebars-template" id="dayPartExceptionsTemplate">
|
||||
<div class="form-group row">
|
||||
<div class="col-3">
|
||||
<select class="form-control" name="exceptionDays[]" id="exceptionDays_{{fieldId}}">
|
||||
<option value=""></option>
|
||||
{{#each daysOfWeek}}
|
||||
<option value="{{ day }}" {{#eq day ../exceptionDay}}selected{{/eq}}>{{ title }}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
{% endverbatim %}
|
||||
{{ inline.time("exceptionStartTimes[]", "", "{{ exceptionStart }}" ) }}
|
||||
{% verbatim %}
|
||||
</div>
|
||||
<div class="col-3">
|
||||
{% endverbatim %}
|
||||
{{ inline.time("exceptionEndTimes[]", "", "{{ exceptionEnd }}" ) }}
|
||||
{% verbatim %}
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<button class="btn btn-white"><i class="fa {{ buttonGlyph }}"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
{% endverbatim %}
|
||||
{% endblock %}
|
||||
499
ots-signs/views/display-page.twig
Normal file
499
ots-signs/views/display-page.twig
Normal file
@@ -0,0 +1,499 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Displays"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block headContent %}
|
||||
{# Add page source code bundle ( CSS ) #}
|
||||
<script nonce="{{ cspNonce }}">
|
||||
(function(){
|
||||
try{
|
||||
var stored = localStorage.getItem('ots-theme-mode');
|
||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
var mode = stored || (prefersLight ? 'light' : 'light');
|
||||
if(mode === 'light') document.documentElement.classList.add('ots-light-mode');
|
||||
else document.documentElement.classList.remove('ots-light-mode');
|
||||
}catch(e){}
|
||||
})();
|
||||
|
||||
(function(){
|
||||
// Apply collapsed sidebar state early to prevent header flashing
|
||||
try {
|
||||
var collapsed = localStorage.getItem('otsTheme:sidebarCollapsed');
|
||||
if (collapsed === 'true') {
|
||||
try { console.debug && console.debug('otsTheme:sidebarCollapsed early (page):', collapsed); } catch(e){}
|
||||
document.documentElement.classList.add('ots-sidebar-collapsed');
|
||||
if (document.body) document.body.classList.add('ots-sidebar-collapsed');
|
||||
try { console.debug && console.debug('applied ots-sidebar-collapsed early (page)'); } catch(e){}
|
||||
} else {
|
||||
try { console.debug && console.debug('otsTheme:sidebarCollapsed early (page): not set'); } catch(e){}
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<style nonce="{{ cspNonce }}">html,body{background:#ffffff!important;color:#111111!important}
|
||||
/* Hide the topbar strip entirely — actions are now in .ots-page-actions */
|
||||
.row.header.header-side,
|
||||
.ots-topbar-strip { display: none !important; height: 0 !important; margin: 0 !important; padding: 0 !important; overflow: hidden !important; }
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.css?v={{ version }}&rev={{revision }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Displays" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your player fleet and status." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="displayView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Displays" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item"><a class="nav-link active" href="#filter-general" role="tab" data-toggle="tab">{% trans "General" %}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#filter-advanced" role="tab" data-toggle="tab">{% trans "Advanced" %}</a></li>
|
||||
</ul>
|
||||
<form class="form-inline">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="filter-general">
|
||||
{% set title %}{% trans "ID" %}{% endset %}
|
||||
{{ inline.number("displayId", title) }}
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('display', title) }}
|
||||
|
||||
{% set title %}{% trans "Status" %}{% endset %}
|
||||
{% set check %}{% trans "Up to date" %}{% endset %}
|
||||
{% set cross %}{% trans "Downloading" %}{% endset %}
|
||||
{% set cloud %}{% trans "Out of date" %}{% endset %}
|
||||
{% set options = [
|
||||
{ optionid: "", option: "" },
|
||||
{ optionid: "1", option: check},
|
||||
{ optionid: "2", option: cross},
|
||||
{ optionid: "3", option: cloud}
|
||||
] %}
|
||||
{{ inline.dropdown("mediaInventoryStatus", "single", title, "", options, "optionid", "option") }}
|
||||
|
||||
{% set title %}{% trans "Logged In?" %}{% endset %}
|
||||
{% set yesOption %}{% trans "Yes" %}{% endset %}
|
||||
{% set noOption %}{% trans "No" %}{% endset %}
|
||||
{% set options = [
|
||||
{ optionid: "", option: "" },
|
||||
{ optionid: "1", option: yesOption},
|
||||
{ optionid: "0", option: noOption}
|
||||
] %}
|
||||
{{ inline.dropdown("loggedIn", "single", title, "", options, "optionid", "option") }}
|
||||
|
||||
{% set title %}{% trans "Authorised?" %}{% endset %}
|
||||
{% set yesOption %}{% trans "Yes" %}{% endset %}
|
||||
{% set noOption %}{% trans "No" %}{% endset %}
|
||||
{% set options = [
|
||||
{ optionid: "", option: "" },
|
||||
{ optionid: "1", option: yesOption },
|
||||
{ optionid: "0", option: noOption},
|
||||
] %}
|
||||
{{ inline.dropdown("authorised", "single", title, "", options, "optionid", "option") }}
|
||||
|
||||
{% set title %}{% trans "XMR Registered?" %}{% endset %}
|
||||
{% set yesOption %}{% trans "Yes" %}{% endset %}
|
||||
{% set noOption %}{% trans "No" %}{% endset %}
|
||||
{% set options = [
|
||||
{ optionid: "", option: "" },
|
||||
{ optionid: 1, option: yesOption},
|
||||
{ optionid: 0, option: noOption},
|
||||
] %}
|
||||
{{ inline.dropdown("xmrRegistered", "single", title, "", options, "optionid", "option") }}
|
||||
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}
|
||||
{% set title %}{% trans "Tags" %}{% endset %}
|
||||
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
|
||||
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
|
||||
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
|
||||
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("displaygroup.view") %}
|
||||
{% set title %}{% trans "Display Group" %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("displayGroup.search") },
|
||||
{ name: "data-filter-options", value: '{"isDisplaySpecific":0}' },
|
||||
{ name: "data-search-term", value: "displayGroup" },
|
||||
{ name: "data-id-property", value: "displayGroupId" },
|
||||
{ name: "data-text-property", value: "displayGroup" },
|
||||
{ name: "data-initial-key", value: "displayGroupId" },
|
||||
] %}
|
||||
{{ inline.dropdown("displayGroupId", "single", title, "", null, "displayGroupId", "displayGroup", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
{% endif %}
|
||||
|
||||
{% if currentUser.featureEnabled("displayprofile.view") %}
|
||||
{% set title %}{% trans "Display Profile" %}{% endset %}
|
||||
{{ inline.dropdown("displayProfileId", "single", title, "", [{displayProfileId:null, name:""}]|merge(displayProfiles), "displayProfileId", "name") }}
|
||||
{% endif %}
|
||||
|
||||
{{ inline.hidden("folderId") }}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="filter-advanced">
|
||||
{% set title %}{% trans "Last Accessed" %}{% endset %}
|
||||
{{ inline.date("lastAccessed", title) }}
|
||||
|
||||
{% set title %}{% trans "Player Type" %}{% endset %}
|
||||
{% set android %}{% trans "Android" %}{% endset %}
|
||||
{% set chromeos %}{% trans "ChromeOS" %}{% endset %}
|
||||
{% set windows %}{% trans "Windows" %}{% endset %}
|
||||
{% set webos %}{% trans "webOS" %}{% endset %}
|
||||
{% set sssp %}{% trans "Tizen" %}{% endset %}
|
||||
{% set linux %}{% trans "Linux" %}{% endset %}
|
||||
{% set options = [
|
||||
{ optionid: "", option: "" },
|
||||
{ optionid: "android", option: android},
|
||||
{ optionid: "chromeos", option: chromeos},
|
||||
{ optionid: "windows", option: windows},
|
||||
{ optionid: "lg", option: webos},
|
||||
{ optionid: "sssp", option: sssp},
|
||||
{ optionid: "linux", option: linux},
|
||||
] %}
|
||||
{{ inline.dropdown("clientType", "single", title, "", options, "optionid", "option") }}
|
||||
|
||||
{% set title %}{% trans "Player Code" %}{% endset %}
|
||||
{{ inline.input("clientCode", title) }}
|
||||
|
||||
{% set title %}{% trans "Custom ID" %}{% endset %}
|
||||
{{ inline.input("customId", title) }}
|
||||
|
||||
{% set title %}{% trans "Mac Address" %}{% endset %}
|
||||
{{ inline.input("macAddress", title) }}
|
||||
|
||||
{% set title %}{% trans "IP Address" %}{% endset %}
|
||||
{{ inline.input("clientAddress", title) }}
|
||||
|
||||
{% set title %}{% trans "Orientation" %}{% endset %}
|
||||
{% set landscape %}{% trans "Landscape" %}{% endset %}
|
||||
{% set portrait %}{% trans "Portrait" %}{% endset %}
|
||||
{% set options = [
|
||||
{ optionid: "", option: "" },
|
||||
{ optionid: "landscape", option: landscape},
|
||||
{ optionid: "portrait", option: portrait}
|
||||
] %}
|
||||
{{ inline.dropdown("orientation", "single", title, "", options, "optionid", "option") }}
|
||||
|
||||
{% set title %}{% trans "Commercial Licence" %}{% endset %}
|
||||
{% set licensed %}{% trans "Licensed fully" %}{% endset %}
|
||||
{% set trial %}{% trans "Trial" %}{% endset %}
|
||||
{% set notLinceced %}{% trans "Not licenced" %}{% endset %}
|
||||
{% set notApplicable %}{% trans "Not applicable" %}{% endset %}
|
||||
{% set options = [
|
||||
{ optionid: "", option: "" },
|
||||
{ optionid: "1", option: licensed},
|
||||
{ optionid: "2", option: trial},
|
||||
{ optionid: "0", option: notLinceced},
|
||||
{ optionid: "3", option: notApplicable}
|
||||
] %}
|
||||
{{ inline.dropdown("commercialLicence", "single", title, "", options, "optionid", "option") }}
|
||||
|
||||
{% set title %}{% trans "Player supported?" %}{% endset %}
|
||||
{% set yesOption %}{% trans "Yes" %}{% endset %}
|
||||
{% set noOption %}{% trans "No" %}{% endset %}
|
||||
{% set options = [
|
||||
{ optionid: "", option: "" },
|
||||
{ optionid: 1, option: yesOption},
|
||||
{ optionid: 0, option: noOption},
|
||||
] %}
|
||||
{{ inline.dropdown("isPlayerSupported", "single", title, "", options, "optionid", "option") }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{{ "Open / Close Folder Search options"|trans }}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
<button type="button" id="map_button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{{ "Map"|trans }}"><i class="fa fa-map"></i></button>
|
||||
<button type="button" id="list_button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{{ "List"|trans }}"><i class="fa fa-list"></i></button>
|
||||
{% if currentUser.featureEnabled("displays.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a Display via user_code displayed on the Player screen" %}" href="{{ url_for("display.addViaCode.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="displays" class="table table-striped" data-content-type="display" data-content-id-name="displayId" data-state-preference-name="displayGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Display" %}</th>
|
||||
<th>{% trans "Display Type" %}</th>
|
||||
<th>{% trans "Address" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Authorised?" %}</th>
|
||||
<th>{% trans "Current Layout" %}</th>
|
||||
<th>{% trans "Storage Available" %}</th>
|
||||
<th>{% trans "Storage Total" %}</th>
|
||||
<th>{% trans "Storage Free %" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Orientation" %}</th>
|
||||
<th>{% trans "Resolution" %}</th>
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
|
||||
<th>{% trans "Default Layout" %}</th>
|
||||
<th>{% trans "Interleave Default" %}</th>
|
||||
<th>{% trans "Email Alert" %}</th>
|
||||
<th>{% trans "Logged In" %}</th>
|
||||
<th>{% trans "Last Accessed" %}</th>
|
||||
<th>{% trans "Display Profile" %}</th>
|
||||
<th>{% trans "Version" %}</th>
|
||||
<th>{% trans "Supported?" %}</th>
|
||||
<th>{% trans "Device Name" %}</th>
|
||||
<th>{% trans "IP Address" %}</th>
|
||||
<th>{% trans "Mac Address" %}</th>
|
||||
<th>{% trans "Timezone" %}</th>
|
||||
<th>{% trans "Languages" %}</th>
|
||||
<th>{% trans "Latitude" %}</th>
|
||||
<th>{% trans "Longitude" %}</th>
|
||||
<th>{% trans "Screen shot?" %}</th>
|
||||
<th>{% trans "Thumbnail" %}</th>
|
||||
<th>{% trans "CMS Transfer?" %}</th>
|
||||
<th>{% trans "Bandwidth Limit" %}</th>
|
||||
<th>{% trans "Last Command" %}</th>
|
||||
<th>{% trans "XMR Registered" %}</th>
|
||||
<th>{% trans "Commercial Licence" %}</th>
|
||||
<th>{% trans "Remote" %}</th>
|
||||
<th>{% trans "Sharing" %}</th>
|
||||
<th>{% trans "Screen Size" %}</th>
|
||||
<th>{% trans "Is Mobile?" %}</th>
|
||||
<th>{% trans "Outdoor?" %}</th>
|
||||
<th>{% trans "Reference 1" %}</th>
|
||||
<th>{% trans "Reference 2" %}</th>
|
||||
<th>{% trans "Reference 3" %}</th>
|
||||
<th>{% trans "Reference 4" %}</th>
|
||||
<th>{% trans "Reference 5" %}</th>
|
||||
<th>{% trans "Custom ID" %}</th>
|
||||
<th>{% trans "Cost Per Play" %}</th>
|
||||
<th>{% trans "Impressions Per Play" %}</th>
|
||||
<th>{% trans "Created Date" %}</th>
|
||||
<th>{% trans "Modified Date" %}</th>
|
||||
<th>{% trans "Faults?" %}</th>
|
||||
<th>{% trans "OS Version" %}</th>
|
||||
<th>{% trans "OS SDK" %}</th>
|
||||
<th>{% trans "Manufacturer" %}</th>
|
||||
<th>{% trans "Brand" %}</th>
|
||||
<th>{% trans "Model" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Map -->
|
||||
<div class="row" id="map-view-container" style="display:none;">
|
||||
<div class="col-sm-12">
|
||||
<div class="map-legend" style="display:none; position: absolute; z-index: 500; right: 20px; top: 10px;">
|
||||
<div class="display-map-legend" style="font-size: 12px;">
|
||||
<div>Logged in</div>
|
||||
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-green-check.png'/> - Up to date</div>
|
||||
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-yellow-check.png'/> - Out of date</div>
|
||||
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-red-check.png'/> - Downloading/Unknown</div>
|
||||
</br>
|
||||
<div>Logged out</div>
|
||||
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-green-cross.png'/> - Up to date</div>
|
||||
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-yellow-cross.png'/> - Out of date</div>
|
||||
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-red-cross.png'/> - Downloading/Unknown</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="display-map" class="content-card ots-map-card" data-displays-url="{{ url_for("display.map") }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
{# Initialise JS variables and translations #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}" defer>
|
||||
{# JS variables #}
|
||||
var publicPath = "{{ theme.rootUri() }}";
|
||||
var displaySearchURL = "{{ url_for('display.search') }}";
|
||||
var layoutSearchURL = "{{ url_for('layout.search') }}";
|
||||
var mapConfig = {{ mapConfig| json_encode | raw }};
|
||||
var playerVersionSupport = "{{playerVersion}}";
|
||||
var folderViewEnabled = "{{ currentUser.featureEnabled('folder.view') }}";
|
||||
var taggingEnabled = "{{ currentUser.featureEnabled('tag.tagging') }}";
|
||||
var showThumbnailColumn = "{{ currentUser.getOptionValue('showThumbnailColumn', 1) }}";
|
||||
var SHOW_DISPLAY_AS_VNCLINK = "{{ settings.SHOW_DISPLAY_AS_VNCLINK }}";
|
||||
var SHOW_DISPLAY_AS_VNC_TGT = "{{ settings.SHOW_DISPLAY_AS_VNC_TGT }}";
|
||||
|
||||
{# Custom translations #}
|
||||
var displayPageTrans = {
|
||||
back: "{% trans "Back" %}",
|
||||
yes: "{% trans "Yes" %}",
|
||||
no: "{% trans "No" %}",
|
||||
daysOfTheWeek: {
|
||||
monday: "{% trans "Monday" %}",
|
||||
tuesday: "{% trans "Tuesday" %}",
|
||||
wednesday: "{% trans "Wednesday" %}",
|
||||
thursday: "{% trans "Thursday" %}",
|
||||
friday: "{% trans "Friday" %}",
|
||||
saturday: "{% trans "Saturday" %}",
|
||||
sunday: "{% trans "Sunday" %}",
|
||||
},
|
||||
playerStatusWindow: "{% trans "Player Status Window" %}",
|
||||
VNCtoThisDisplay: "{% trans "VNC to this Display" %}",
|
||||
TeamViewertoThisDisplay: "{% trans "TeamViewer to this Display" %}",
|
||||
WebkeytoThisDisplay: "{% trans "Webkey to this Display" %}",
|
||||
};
|
||||
</script>
|
||||
|
||||
{# Add page source code bundle ( JS ) #}
|
||||
<script src="{{ theme.rootUri() }}dist/leaflet.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
|
||||
{# Initialize map/list view toggle AFTER all other scripts load #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
function initMapListToggle() {
|
||||
var mapBtn = document.getElementById('map_button');
|
||||
var listBtn = document.getElementById('list_button');
|
||||
var mapViewContainer = document.getElementById('map-view-container');
|
||||
// DataTables wraps the <table> in a div with id "displays_wrapper"
|
||||
var tableWrapper = document.getElementById('displays_wrapper');
|
||||
// Fallback: if DataTables hasn't wrapped it yet, target the table itself
|
||||
if (!tableWrapper) {
|
||||
tableWrapper = document.getElementById('displays');
|
||||
}
|
||||
|
||||
if (!mapBtn || !listBtn || !mapViewContainer || !tableWrapper) {
|
||||
console.warn('Map/list toggle: required elements not found:', {
|
||||
mapBtn: !!mapBtn,
|
||||
listBtn: !!listBtn,
|
||||
mapViewContainer: !!mapViewContainer,
|
||||
tableWrapper: !!tableWrapper
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Map/list toggle initialized');
|
||||
|
||||
// Show list view by default
|
||||
tableWrapper.style.display = '';
|
||||
mapViewContainer.style.display = 'none';
|
||||
listBtn.classList.add('active');
|
||||
mapBtn.classList.remove('active');
|
||||
|
||||
// Map button click handler
|
||||
mapBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
console.log('Map button clicked');
|
||||
mapViewContainer.style.display = 'block';
|
||||
tableWrapper.style.display = 'none';
|
||||
mapBtn.classList.add('active');
|
||||
listBtn.classList.remove('active');
|
||||
|
||||
// Leaflet can't size itself in a hidden container.
|
||||
// After making the map visible, tell every Leaflet map
|
||||
// instance inside it to recalculate its dimensions.
|
||||
setTimeout(function() {
|
||||
var mapEl = document.getElementById('display-map');
|
||||
if (mapEl) {
|
||||
// Leaflet stores its instance on the DOM element as _leaflet_map
|
||||
var leafletKeys = Object.keys(mapEl).filter(function(k) {
|
||||
return k.indexOf('_leaflet_map') === 0 || k === '_leaflet';
|
||||
});
|
||||
// Try the standard _leaflet_map key
|
||||
if (mapEl._leaflet_map) {
|
||||
mapEl._leaflet_map.invalidateSize();
|
||||
console.log('Leaflet invalidateSize called via _leaflet_map');
|
||||
}
|
||||
// Also try iterating over Leaflet-stamped keys
|
||||
for (var i = 0; i < leafletKeys.length; i++) {
|
||||
var inst = mapEl[leafletKeys[i]];
|
||||
if (inst && typeof inst.invalidateSize === 'function') {
|
||||
inst.invalidateSize();
|
||||
console.log('Leaflet invalidateSize called via', leafletKeys[i]);
|
||||
}
|
||||
}
|
||||
// Fallback: dispatch a resize event so Leaflet picks it up
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// List button click handler
|
||||
listBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
console.log('List button clicked');
|
||||
tableWrapper.style.display = '';
|
||||
mapViewContainer.style.display = 'none';
|
||||
listBtn.classList.add('active');
|
||||
mapBtn.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Give the page bundle time to initialize DataTables
|
||||
setTimeout(initMapListToggle, 500);
|
||||
});
|
||||
} else {
|
||||
// Give the page bundle time to initialize DataTables
|
||||
setTimeout(initMapListToggle, 500);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
377
ots-signs/views/displaygroup-page.twig
Normal file
377
ots-signs/views/displaygroup-page.twig
Normal file
@@ -0,0 +1,377 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020-2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Display Groups"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Display Groups" %}</h1>
|
||||
<p class="text-muted">{% trans "Organize Displays into logical groups." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="displayGroupGridView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Display Groups" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "ID" %}{% endset %}
|
||||
{{ inline.input("displayGroupId", title) }}
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('displayGroup', title) }}
|
||||
|
||||
{% set title %}{% trans "Display" %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("display.search") },
|
||||
{ name: "data-search-term", value: "display" },
|
||||
{ name: "data-search-term-tags", value: "tags" },
|
||||
{ name: "data-id-property", value: "displayId" },
|
||||
{ name: "data-text-property", value: "display" },
|
||||
{ name: "data-initial-key", value: "displayId" },
|
||||
] %}
|
||||
{% set helpText %}{% trans "Return Display Groups that directly contain the selected Display." %}{% endset %}
|
||||
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
|
||||
{% set title %}{% trans "Nested Display" %}{% endset %}
|
||||
{% set helpText %}{% trans "Return Display Groups that contain the selected Display somewhere in the nested Display Group relationship tree." %}{% endset %}
|
||||
{{ inline.dropdown("nestedDisplayId", "single", title, "", null, "displayId", "display", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
|
||||
{% set title %}{% trans "Dynamic Criteria" %}{% endset %}
|
||||
{{ inline.input("dynamicCriteria", title) }}
|
||||
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}
|
||||
{% set title %}{% trans "Tags" %}{% endset %}
|
||||
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
|
||||
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
|
||||
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
|
||||
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
|
||||
{% endif %}
|
||||
|
||||
{{ inline.hidden("folderId") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabled("displaygroup.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add Display Group" %}" href="{{ url_for("displayGroup.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="displaygroups" class="table table-striped" data-content-type="displayGroup" data-content-id-name="displayGroupId" data-state-preference-name="displayGroupGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Is Dynamic?" %}</th>
|
||||
<th>{% trans "Criteria" %}</th>
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}
|
||||
<th>{% trans "Criteria Tags" %}</th>
|
||||
<th>{% trans "Tags" %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Sharing" %}</th>
|
||||
<th>{% trans "Reference 1" %}</th>
|
||||
<th>{% trans "Reference 2" %}</th>
|
||||
<th>{% trans "Reference 3" %}</th>
|
||||
<th>{% trans "Reference 4" %}</th>
|
||||
<th>{% trans "Reference 5" %}</th>
|
||||
<th>{% trans "Created Date" %}</th>
|
||||
<th>{% trans "Modified Date" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var displayGroupTable;
|
||||
var displayTable;
|
||||
var criteria;
|
||||
var criteriaTag;
|
||||
var useRegexForName;
|
||||
var exactTags;
|
||||
var logicalOperator;
|
||||
var logicalOperatorName;
|
||||
|
||||
$(document).ready(function() {
|
||||
{% if not currentUser.featureEnabled("folder.view") %}
|
||||
disableFolders();
|
||||
{% endif %}
|
||||
|
||||
displayGroupTable = $("#displaygroups").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
"filter": false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("displayGroup.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#displaygroups").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "displayGroupId", responsivePriority: 2},
|
||||
{ "data": "displayGroup", "render": dataTableSpacingPreformatted, responsivePriority: 2 },
|
||||
{ "data": "description", responsivePriority: 3 },
|
||||
{ "data": "isDynamic", "render": dataTableTickCrossColumn, responsivePriority: 3 },
|
||||
{ "data": "dynamicCriteria", responsivePriority: 4 },
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}
|
||||
{ "data": "dynamicCriteriaTags", responsivePriority: 4},
|
||||
{
|
||||
"name": "tags",
|
||||
"sortable": false,
|
||||
responsivePriority: 3,
|
||||
"data": dataTableCreateTags
|
||||
},
|
||||
{% endif %}
|
||||
{
|
||||
"data": "groupsWithPermissions",
|
||||
visible: false,
|
||||
responsivePriority: 10,
|
||||
"render": dataTableCreatePermissions
|
||||
},
|
||||
{ "data": "ref1", "visible": false, responsivePriority: 5},
|
||||
{ "data": "ref2", "visible": false, responsivePriority: 5},
|
||||
{ "data": "ref3", "visible": false, responsivePriority: 5},
|
||||
{ "data": "ref4", "visible": false, responsivePriority: 5},
|
||||
{ "data": "ref5", "visible": false, responsivePriority: 5},
|
||||
{ "data": "createdDt", "visible": false, responsivePriority: 5 },
|
||||
{ "data": "modifiedDt", "visible": false, responsivePriority: 5 },
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
displayGroupTable.ajax.reload();
|
||||
});
|
||||
|
||||
displayGroupTable.on('draw', dataTableDraw);
|
||||
displayGroupTable.on('draw', { form: $("#displaygroups").closest(".XiboGrid").find(".FilterDiv form") }, dataTableCreateTagEvents);
|
||||
displayGroupTable.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(displayGroupTable, $('#displaygroups_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
displayGroupTable.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
function setDeleteMultiSelectFormOpen(dialog) {
|
||||
$(dialog).find('.save-button').prop('disabled', false);
|
||||
var template = Handlebars.compile($('#template-display-group-multi-delete-checkbox').html());
|
||||
var $input = $(template());
|
||||
$input.find('input').on('change', function() {
|
||||
$(dialog).find('.save-button').prop('disabled', !$(this).is(':checked'));
|
||||
});
|
||||
$(dialog).find('.modal-body').append($input);
|
||||
}
|
||||
|
||||
function displayGroupAddFormNext() {
|
||||
// Get form
|
||||
var $form = $("#displayGroupAddForm");
|
||||
|
||||
// Set apply and apply reset data
|
||||
$form.data("apply", true);
|
||||
$form.data("applyCallback", 'applyResetCallback');
|
||||
|
||||
// Submit form
|
||||
$form.submit();
|
||||
}
|
||||
|
||||
function applyResetCallback(form) {
|
||||
// Reset form fields
|
||||
$(form).find('#displayGroup').val("");
|
||||
}
|
||||
|
||||
function displayGroupFormOpen(dialog) {
|
||||
displayTable = null;
|
||||
|
||||
$(dialog).find("input[name=dynamicCriteria]").on("keyup", _.debounce(function() {
|
||||
displayGroupQueryDynamicMembers(dialog);
|
||||
}, 500));
|
||||
|
||||
$(dialog).find("input[name=dynamicCriteriaTags], input[name=exactTags], select[name=logicalOperator], select[name=logicalOperatorName]").change(function() {
|
||||
displayGroupQueryDynamicMembers(dialog);
|
||||
});
|
||||
|
||||
var $form = $('#displayGroupAddForm');
|
||||
|
||||
// First time in there
|
||||
displayGroupQueryDynamicMembers(dialog);
|
||||
}
|
||||
|
||||
function displayGroupQueryDynamicMembers(dialog) {
|
||||
|
||||
if ($(dialog).find("input[name=isDynamic]")[0].checked) {
|
||||
|
||||
criteria = $(dialog).find("input[name=dynamicCriteria]").val();
|
||||
criteriaTag = $(dialog).find("input[name=dynamicCriteriaTags]").val();
|
||||
useRegexForName = $(dialog).find("input[name=useRegexForName]").val();
|
||||
exactTags = $(dialog).find("input[name=exactTags]").is(':checked');
|
||||
logicalOperator = $(dialog).find("select[name=logicalOperator]").val();
|
||||
logicalOperatorName = $(dialog).find("select[name=logicalOperatorName]").val();
|
||||
|
||||
if (criteria === "" && criteriaTag === "") {
|
||||
if (displayTable != null) {
|
||||
displayTable.destroy();
|
||||
displayTable = null;
|
||||
$("#displayGroupDisplays tbody").empty();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (displayTable != null) {
|
||||
displayTable.ajax.reload();
|
||||
} else {
|
||||
displayTable = $("#displayGroupDisplays").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
serverSide: true,
|
||||
stateSave: true, stateDuration: 0,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("display.search") }}",
|
||||
"data": function (d) {
|
||||
$.extend(
|
||||
d,
|
||||
{
|
||||
display: criteria,
|
||||
tags: criteriaTag,
|
||||
useRegexForName: useRegexForName,
|
||||
exactTags: exactTags,
|
||||
logicalOperator: logicalOperator,
|
||||
logicalOperatorName: logicalOperatorName
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "displayId"},
|
||||
{"data": "display"},
|
||||
{"data": dataTableCreateTags},
|
||||
{
|
||||
"data": "mediaInventoryStatus",
|
||||
"render": function (data, type, row) {
|
||||
if (type != "display")
|
||||
return data;
|
||||
|
||||
var icon = "";
|
||||
if (data == 1)
|
||||
icon = "fa-check";
|
||||
else if (data == 0)
|
||||
icon = "fa-times";
|
||||
else
|
||||
icon = "fa-cloud-download";
|
||||
|
||||
return "<span class='fa " + icon + "'></span>";
|
||||
}
|
||||
},
|
||||
{"data": "licensed", "render": dataTableTickCrossColumn}
|
||||
]
|
||||
});
|
||||
|
||||
displayTable.on('processing.dt', dataTableProcessing);
|
||||
displayTable.on('draw', { form: $(".displayGroupForm") }, dataTableCreateTagEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScriptTemplates %}
|
||||
{{ parent() }}
|
||||
|
||||
{% verbatim %}
|
||||
<script type="text/x-handlebars-template" id="template-display-group-multi-delete-checkbox">
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-2 col-sm-10 mt-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="checkbox-confirmDelete" name="confirmDelete">
|
||||
<label class="form-check-label" for="checkbox-confirmDelete">
|
||||
{% endverbatim %}{{ "Are you sure you want to delete?"|trans }}{% verbatim %}
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">{% endverbatim %}{{ "Check to confirm deletion of the selected records."|trans }}{% verbatim %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
{% endverbatim %}
|
||||
{% endblock %}
|
||||
167
ots-signs/views/displayprofile-page.twig
Normal file
167
ots-signs/views/displayprofile-page.twig
Normal file
@@ -0,0 +1,167 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Display Setting Profiles"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Display Settings" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage Display settings profiles." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Display Settings" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('displayProfile', title) }}
|
||||
|
||||
{% set title %}{% trans "Type" %}{% endset %}
|
||||
{{ inline.dropdown("type", "single", title, "", [{typeId:null, type:""}]|merge(types), "typeId","type") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if currentUser.featureEnabled("displayprofile.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Display Settings Profile" %}" href="{{ url_for("displayProfile.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="displayProfiles" class="table table-striped" data-state-preference-name="displayProfileGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Default" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table = $("#displayProfiles").DataTable({ "language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("displayProfile.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#displayProfiles").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "name", "render": dataTableSpacingPreformatted , responsivePriority: 2},
|
||||
{ "data": "type" },
|
||||
{ "data": "isDefault", "render": dataTableTickCrossColumn },
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#displayProfiles_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
|
||||
// Custom submit for display profile form
|
||||
function displayProfileEditFormSubmit() {
|
||||
var $form = $("#displayProfileForm");
|
||||
|
||||
// Remove temp fields and enable checkbox after submit
|
||||
$form.submit(function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Re-enable checkboxes
|
||||
$form.find('input[type="checkbox"]').each(function () {
|
||||
// Enable checkbox
|
||||
$(this).attr('disabled', false);
|
||||
});
|
||||
|
||||
// Remove temp input fields
|
||||
$form.find('input.temp-input').each(function () {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Replace all checkboxes with hidden input fields
|
||||
$form.find('input[type="checkbox"]').each(function () {
|
||||
// Get checkbox values
|
||||
var value = $(this).is(':checked') ? 'on' : 'off';
|
||||
var id = $(this).attr('id');
|
||||
|
||||
// Create hidden input
|
||||
$('<input type="hidden" class="temp-input">')
|
||||
.attr('id', id)
|
||||
.attr('name', id)
|
||||
.val(value)
|
||||
.appendTo($(this).parent());
|
||||
|
||||
// Disable checkbox so it won't be submitted
|
||||
$(this).attr('disabled', true);
|
||||
});
|
||||
|
||||
// Submit form
|
||||
$form.submit();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
159
ots-signs/views/fonts-page.twig
Normal file
159
ots-signs/views/fonts-page.twig
Normal file
@@ -0,0 +1,159 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Fonts Page
|
||||
* Based on Xibo CMS fonts-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Fonts"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Fonts" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage fonts for your signage content." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="fontView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Fonts" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "ID" %}{% endset %}
|
||||
{{ inline.number("id", title) }}
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('name', title) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if currentUser.featureEnabled("font.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn" href="#" id="fontUploadForm" title="{% trans "Add a new Font" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="fonts" class="table table-striped" data-state-preference-name="fontGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "name" %}</th>
|
||||
<th>{% trans "File Name" %}</th>
|
||||
<th>{% trans "Created" %}</th>
|
||||
<th>{% trans "Modified" %}</th>
|
||||
<th>{% trans "Modified By" %}</th>
|
||||
<th>{% trans "Size" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var fontsTable;
|
||||
$(document).ready(function() {
|
||||
fontsTable = $("#fonts").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
responsive: true,
|
||||
stateDuration: 0,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
url: "{{ url_for("font.search") }}",
|
||||
data: function (d) {
|
||||
$.extend(d, $("#fonts").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "id", responsivePriority: 2},
|
||||
{"data": "name", responsivePriority: 2},
|
||||
{"data": "fileName", responsivePriority: 4},
|
||||
{"data": "createdAt", responsivePriority: 3},
|
||||
{"data": "modifiedAt", responsivePriority: 3},
|
||||
{"data": "modifiedBy", responsivePriority: 3},
|
||||
{
|
||||
"name": "size",
|
||||
responsivePriority: 3,
|
||||
"data": null,
|
||||
"render": {"_": "size", "display": "fileSizeFormatted", "sort": "size"}
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fontsTable.on('draw', dataTableDraw);
|
||||
fontsTable.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(fontsTable, $('#fonts_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
fontsTable.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
$("#fontUploadForm").click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
openUploadForm({
|
||||
url: "{{ url_for("font.add") }}",
|
||||
title: "{% trans "Add Font" %}",
|
||||
initialisedBy: "font-upload",
|
||||
buttons: {
|
||||
main: {
|
||||
label: "{% trans "Done" %}",
|
||||
className: "btn-primary btn-bb-main",
|
||||
callback: function () {
|
||||
fontsTable.ajax.reload();
|
||||
XiboDialogClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
templateOptions: {
|
||||
includeTagsInput: false,
|
||||
trans: {
|
||||
addFiles: "{% trans "Add files" %}",
|
||||
startUpload: "{% trans "Start upload" %}",
|
||||
cancelUpload: "{% trans "Cancel upload" %}"
|
||||
},
|
||||
upload: {
|
||||
maxSize: {{ libraryUpload.maxSize }},
|
||||
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
|
||||
validExt: "{{ validExt }}"
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
800
ots-signs/views/include-file-upload.twig
Normal file
800
ots-signs/views/include-file-upload.twig
Normal file
@@ -0,0 +1,800 @@
|
||||
{#
|
||||
/**
|
||||
* OTS Signage — Modern Upload Media Modal
|
||||
* Replaces the core Xibo include-file-upload.twig with a redesigned,
|
||||
* drag-and-drop, multi-file upload experience.
|
||||
*
|
||||
* Reuses the existing openUploadForm(options) API so every page
|
||||
* (library, layout, fonts, player software, dataset, etc.) keeps working
|
||||
* without any caller changes.
|
||||
*
|
||||
* Dependencies already present in Xibo: jQuery, jQuery UI, jQuery File Upload,
|
||||
* Bootstrap 4 modal, moment.js.
|
||||
*/
|
||||
#}
|
||||
|
||||
{# ── Upload Modal Markup ────────────────────────────────────────────────── #}
|
||||
<div class="modal fade ots-upload-modal" id="ots-upload-modal" tabindex="-1"
|
||||
role="dialog" aria-labelledby="ots-upload-modal-title" aria-modal="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content ots-upload-content">
|
||||
|
||||
{# Header #}
|
||||
<div class="modal-header ots-upload-header">
|
||||
<h5 class="modal-title ots-upload-title" id="ots-upload-modal-title"></h5>
|
||||
<button type="button" class="ots-upload-close" data-dismiss="modal" aria-label="Close">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Body #}
|
||||
<div class="modal-body ots-upload-body">
|
||||
|
||||
{# Tab switcher: File / URL #}
|
||||
<div class="ots-upload-tabs" id="ots-upload-tabs">
|
||||
<button type="button" class="ots-upload-tab active" data-tab="file" id="ots-tab-file">
|
||||
<i class="fas fa-file-upload"></i> File
|
||||
</button>
|
||||
<button type="button" class="ots-upload-tab" data-tab="url" id="ots-tab-url">
|
||||
<i class="fas fa-link"></i> URL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# ── FILE TAB ──────────────────────────────────────────────── #}
|
||||
<div class="ots-upload-tab-content" id="ots-upload-tab-file">
|
||||
|
||||
{# Folder selector row – shown only when options.folderSelector is true #}
|
||||
<div class="ots-upload-folder-row d-none" id="ots-upload-folder-row">
|
||||
<span class="ots-upload-folder-label" id="ots-upload-folder-label"></span>
|
||||
<button type="button" class="btn btn-sm ots-upload-folder-btn" id="ots-upload-folder-btn" title="">
|
||||
<i class="fas fa-folder-open"></i> <span id="ots-upload-folder-text"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Max file size notice #}
|
||||
<div class="ots-upload-notice d-none" id="ots-upload-size-notice"></div>
|
||||
|
||||
{# Drop-zone #}
|
||||
<form id="ots-upload-form" enctype="multipart/form-data" method="POST">
|
||||
<div class="ots-upload-dropzone" id="ots-upload-dropzone" role="button" tabindex="0"
|
||||
aria-label="Drag and drop files here or click to browse">
|
||||
<div class="ots-upload-dropzone-inner">
|
||||
<div class="ots-upload-dropzone-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<rect x="4" y="8" width="40" height="32" rx="6" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<path d="M24 30V18m0 0l-6 6m6-6l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="ots-upload-dropzone-text">
|
||||
<strong id="ots-upload-drop-label">Drop files here</strong><br>
|
||||
<span class="ots-upload-dropzone-sub">or <span class="ots-upload-browse-link">browse your computer</span></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{# File input lives outside the dropzone to avoid click-event loops #}
|
||||
<input type="file" id="ots-upload-input" name="files[]" multiple class="ots-upload-input-hidden" />
|
||||
</form>
|
||||
|
||||
{# Valid extensions badge #}
|
||||
<div class="ots-upload-ext-info d-none" id="ots-upload-ext-info"></div>
|
||||
|
||||
{# Options row (update in layouts / delete old revisions) #}
|
||||
<div class="ots-upload-options d-none" id="ots-upload-options"></div>
|
||||
|
||||
{# File list / queue #}
|
||||
<div class="ots-upload-queue d-none" id="ots-upload-queue">
|
||||
<div class="ots-upload-queue-header">
|
||||
<span class="ots-upload-queue-title">Files</span>
|
||||
<span class="ots-upload-queue-count" id="ots-upload-queue-count"></span>
|
||||
</div>
|
||||
<ul class="ots-upload-file-list" id="ots-upload-file-list"></ul>
|
||||
</div>
|
||||
|
||||
</div>{# /ots-upload-tab-file #}
|
||||
|
||||
{# ── URL TAB ───────────────────────────────────────────────── #}
|
||||
<div class="ots-upload-tab-content d-none" id="ots-upload-tab-url">
|
||||
<div class="ots-upload-url-section">
|
||||
<div class="ots-upload-url-icon">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M17 23l6-6m-3.5.5a5 5 0 017.07 0l1.42 1.42a5 5 0 010 7.07l-2.83 2.83a5 5 0 01-7.07 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M23 17l-6 6m3.5-.5a5 5 0 00-7.07 0l-1.42-1.42a5 5 0 010-7.07l2.83-2.83a5 5 0 017.07 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="ots-upload-url-desc">Add media from an external URL</p>
|
||||
<div class="ots-upload-url-fields">
|
||||
<div class="ots-upload-url-field">
|
||||
<label for="ots-upload-url-input">URL</label>
|
||||
<input type="url" id="ots-upload-url-input" class="form-control ots-upload-url-input"
|
||||
placeholder="https://example.com/image.jpg" autocomplete="off" />
|
||||
</div>
|
||||
<button type="button" class="btn ots-upload-btn-start ots-upload-url-add" id="ots-upload-url-add">
|
||||
<i class="fas fa-plus"></i> Add to queue
|
||||
</button>
|
||||
</div>
|
||||
{# URL queue list #}
|
||||
<div class="ots-upload-queue d-none" id="ots-upload-url-queue">
|
||||
<div class="ots-upload-queue-header">
|
||||
<span class="ots-upload-queue-title">URLs</span>
|
||||
<span class="ots-upload-queue-count" id="ots-upload-url-queue-count"></span>
|
||||
</div>
|
||||
<ul class="ots-upload-file-list" id="ots-upload-url-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>{# /ots-upload-tab-url #}
|
||||
|
||||
</div>
|
||||
|
||||
{# Footer #}
|
||||
<div class="modal-footer ots-upload-footer">
|
||||
<button type="button" class="btn ots-upload-btn-cancel" data-dismiss="modal" id="ots-upload-btn-cancel">Cancel</button>
|
||||
<button type="button" class="btn ots-upload-btn-start d-none" id="ots-upload-btn-start">
|
||||
<i class="fas fa-cloud-upload-alt"></i> <span id="ots-upload-btn-start-label">Start upload</span>
|
||||
</button>
|
||||
<button type="button" class="btn ots-upload-btn-done d-none" id="ots-upload-btn-done">Done</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Upload JavaScript ──────────────────────────────────────────────────── #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
/**
|
||||
* openUploadForm(options)
|
||||
* Drop-in replacement for the core Xibo openUploadForm.
|
||||
* Keeps the same options API so existing page callers (library, layout, fonts,
|
||||
* player-software, dataset) work without modification.
|
||||
*
|
||||
* Options shape (all optional except url):
|
||||
* {
|
||||
* url: String – POST endpoint
|
||||
* title: String – modal title
|
||||
* initialisedBy: String – an identifier for the caller
|
||||
* buttons: {
|
||||
* main: { label, className, callback }
|
||||
* },
|
||||
* templateOptions: {
|
||||
* multi: Boolean – allow multiple files (default true)
|
||||
* trans: { addFiles, startUpload, cancelUpload, selectFolder, ... },
|
||||
* upload: { maxSize, maxSizeMessage, validExt, validExtensionsMessage },
|
||||
* folderSelector: Boolean,
|
||||
* currentWorkingFolderId: Number,
|
||||
* oldMediaId: Number – when replacing a media item
|
||||
* oldFolderId: Number,
|
||||
* updateInAllChecked: Boolean,
|
||||
* deleteOldRevisionsChecked: Boolean,
|
||||
* },
|
||||
* uploadDoneEvent: Function – called when all uploads finish
|
||||
* }
|
||||
*/
|
||||
window.openUploadForm = function openUploadForm(options) {
|
||||
'use strict';
|
||||
|
||||
options = options || {};
|
||||
var tOpts = options.templateOptions || {};
|
||||
var trans = tOpts.trans || {};
|
||||
var upload = tOpts.upload || {};
|
||||
var multi = tOpts.multi !== false;
|
||||
|
||||
// ── References ──
|
||||
var $modal = $('#ots-upload-modal');
|
||||
var $title = $('#ots-upload-modal-title');
|
||||
var $dropzone = $('#ots-upload-dropzone');
|
||||
var $form = $('#ots-upload-form');
|
||||
var $input = $('#ots-upload-input');
|
||||
var $queue = $('#ots-upload-queue');
|
||||
var $fileList = $('#ots-upload-file-list');
|
||||
var $queueCount= $('#ots-upload-queue-count');
|
||||
var $btnStart = $('#ots-upload-btn-start');
|
||||
var $btnDone = $('#ots-upload-btn-done');
|
||||
var $btnCancel = $('#ots-upload-btn-cancel');
|
||||
var $startLabel= $('#ots-upload-btn-start-label');
|
||||
var $folderRow = $('#ots-upload-folder-row');
|
||||
var $sizeNotice= $('#ots-upload-size-notice');
|
||||
var $extInfo = $('#ots-upload-ext-info');
|
||||
var $optionsRow= $('#ots-upload-options');
|
||||
var $dropLabel = $('#ots-upload-drop-label');
|
||||
|
||||
// ── Extra references for URL tab ──
|
||||
var $tabFile = $('#ots-tab-file');
|
||||
var $tabUrl = $('#ots-tab-url');
|
||||
var $panelFile = $('#ots-upload-tab-file');
|
||||
var $panelUrl = $('#ots-upload-tab-url');
|
||||
var $urlInput = $('#ots-upload-url-input');
|
||||
var $urlAddBtn = $('#ots-upload-url-add');
|
||||
var $urlQueue = $('#ots-upload-url-queue');
|
||||
var $urlList = $('#ots-upload-url-list');
|
||||
var $urlCount = $('#ots-upload-url-queue-count');
|
||||
var urlQueue = []; // { url, id, status, $el, xhr }
|
||||
|
||||
// ── Reset state ──
|
||||
$fileList.empty();
|
||||
$urlList.empty();
|
||||
$queue.addClass('d-none');
|
||||
$urlQueue.addClass('d-none');
|
||||
$btnStart.addClass('d-none');
|
||||
$btnDone.addClass('d-none');
|
||||
$folderRow.addClass('d-none');
|
||||
$sizeNotice.addClass('d-none');
|
||||
$extInfo.addClass('d-none');
|
||||
$optionsRow.addClass('d-none').empty();
|
||||
$input.val('');
|
||||
$urlInput.val('');
|
||||
$dropzone.removeClass('ots-upload-dropzone--over ots-upload-dropzone--has-files');
|
||||
|
||||
// Reset to file tab
|
||||
$tabFile.addClass('active');
|
||||
$tabUrl.removeClass('active');
|
||||
$panelFile.removeClass('d-none');
|
||||
$panelUrl.addClass('d-none');
|
||||
|
||||
// ── Populate UI from options ──
|
||||
$title.text(options.title || 'Upload');
|
||||
$startLabel.text(trans.startUpload || 'Start upload');
|
||||
$btnCancel.text(trans.cancelUpload || 'Cancel');
|
||||
$dropLabel.text(trans.addFiles || 'Drop files here');
|
||||
|
||||
if (!multi) {
|
||||
$input.removeAttr('multiple');
|
||||
} else {
|
||||
$input.attr('multiple', 'multiple');
|
||||
}
|
||||
|
||||
// Max file size notice
|
||||
if (upload.maxSizeMessage) {
|
||||
$sizeNotice.text(upload.maxSizeMessage).removeClass('d-none');
|
||||
}
|
||||
|
||||
// Valid extensions
|
||||
if (upload.validExt) {
|
||||
var extList = upload.validExt.replace(/\|/g, ', ');
|
||||
var extMsg = upload.validExtensionsMessage || ('Allowed: ' + extList);
|
||||
$extInfo.text(extMsg).removeClass('d-none');
|
||||
}
|
||||
|
||||
// Folder selector
|
||||
if (tOpts.folderSelector) {
|
||||
$folderRow.removeClass('d-none');
|
||||
$('#ots-upload-folder-label').text((trans.selectedFolder || 'Current Folder:'));
|
||||
$('#ots-upload-folder-text').text(trans.selectFolder || 'Select Folder');
|
||||
$('#ots-upload-folder-btn').attr('title', trans.selectFolderTitle || 'Change folder');
|
||||
|
||||
// Wire folder-selector button using the CMS's built-in folder-tree modal
|
||||
// (templates['folder-tree'], initJsTreeAjax — provided by the Xibo core)
|
||||
$('#ots-upload-folder-btn').off('click').on('click', function() {
|
||||
var modalId = 'ots-upload-folder-tree-modal';
|
||||
var containerId = 'ots-upload-folder-form-tree';
|
||||
var $ftModal = $('#' + modalId);
|
||||
|
||||
// ── First open: build the modal from the Handlebars template ──
|
||||
if ($ftModal.length === 0 && typeof templates !== 'undefined' && templates['folder-tree']) {
|
||||
var folderTreeTpl = templates['folder-tree'];
|
||||
var treeConfig = {
|
||||
container: containerId,
|
||||
modal: modalId
|
||||
};
|
||||
if (typeof translations !== 'undefined' && translations.folderTree) {
|
||||
treeConfig.trans = translations.folderTree;
|
||||
}
|
||||
$('body').append(folderTreeTpl(treeConfig));
|
||||
$ftModal = $('#' + modalId);
|
||||
|
||||
// Inject OK / Cancel footer
|
||||
var $footer = $ftModal.find('.modal-footer');
|
||||
if ($footer.length === 0) {
|
||||
$footer = $('<div class="modal-footer"></div>');
|
||||
$ftModal.find('.modal-content').append($footer);
|
||||
}
|
||||
$footer.empty().append(
|
||||
'<button type="button" class="btn btn-sm ots-upload-btn-cancel" data-dismiss="modal">Cancel</button>' +
|
||||
'<button type="button" class="btn btn-sm ots-upload-btn-start" id="ots-folder-confirm-btn">' +
|
||||
'<i class="fas fa-check"></i> OK' +
|
||||
'</button>'
|
||||
);
|
||||
|
||||
// Configure as static backdrop once
|
||||
$ftModal.modal({ backdrop: 'static', keyboard: true, show: false });
|
||||
|
||||
// Fix stacked-modal body class when this modal closes
|
||||
$ftModal.on('hidden.bs.modal', function() {
|
||||
if ($('.modal:visible').length) {
|
||||
$(document.body).addClass('modal-open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($ftModal.length === 0) {
|
||||
console.warn('Folder tree template not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Every open: reset pending selection and re-init jstree ──
|
||||
var pendingFolderId = tOpts.currentWorkingFolderId || null;
|
||||
var pendingFolderName = null;
|
||||
|
||||
// Destroy previous jstree instance so it re-initialises cleanly
|
||||
var $treeContainer = $ftModal.find('#' + containerId);
|
||||
if ($treeContainer.jstree && $treeContainer.jstree(true)) {
|
||||
try { $treeContainer.jstree('destroy'); } catch(e) {}
|
||||
}
|
||||
|
||||
// Initialise jstree
|
||||
if (typeof initJsTreeAjax === 'function') {
|
||||
initJsTreeAjax($treeContainer, 'ots-upload-form', true, 600);
|
||||
}
|
||||
|
||||
// Show the modal (works on first and subsequent opens)
|
||||
$ftModal.modal('show');
|
||||
|
||||
// Bind selection handler after the modal is visible + jstree auto-select settles
|
||||
$ftModal.off('shown.bs.modal.otsUpload').on('shown.bs.modal.otsUpload', function() {
|
||||
setTimeout(function() {
|
||||
$treeContainer.off('select_node.jstree.otsUpload')
|
||||
.on('select_node.jstree.otsUpload', function(e, data) {
|
||||
if (data && data.node) {
|
||||
pendingFolderId = data.node.id;
|
||||
pendingFolderName = data.node.text || data.node.id;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// OK button — apply selection and close
|
||||
$ftModal.find('#ots-folder-confirm-btn').off('click').on('click', function() {
|
||||
if (pendingFolderId) {
|
||||
tOpts.currentWorkingFolderId = pendingFolderId;
|
||||
$('#ots-upload-folder-text').text(pendingFolderName || pendingFolderId);
|
||||
}
|
||||
$ftModal.modal('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Done button
|
||||
var mainBtn = (options.buttons && options.buttons.main) || {};
|
||||
$btnDone.text(mainBtn.label || 'Done');
|
||||
if (mainBtn.className) {
|
||||
$btnDone.attr('class', 'btn ots-upload-btn-done d-none ' + mainBtn.className);
|
||||
}
|
||||
|
||||
// ── Internal state ──
|
||||
var fileQueue = []; // { file, id, status, $el, xhr }
|
||||
var nextId = 0;
|
||||
var uploading = false;
|
||||
var uploadCount = 0;
|
||||
var successCount= 0;
|
||||
|
||||
// ── Helper: human-readable size ──
|
||||
function humanSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// ── Helper: valid extension check ──
|
||||
function isExtAllowed(filename) {
|
||||
if (!upload.validExt) return true;
|
||||
var ext = filename.split('.').pop().toLowerCase();
|
||||
var allowed = upload.validExt.toLowerCase().split('|');
|
||||
return allowed.indexOf(ext) !== -1;
|
||||
}
|
||||
|
||||
// ── Helper: generate preview (images only) ──
|
||||
function generatePreview(file, $thumb) {
|
||||
if (file.type && file.type.indexOf('image/') === 0 && file.size < 10 * 1048576) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
$thumb.css('background-image', 'url(' + e.target.result + ')').addClass('has-preview');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
// Icon based on type
|
||||
var icon = 'fa-file';
|
||||
if (file.type && file.type.indexOf('video/') === 0) icon = 'fa-file-video';
|
||||
else if (file.type && file.type.indexOf('audio/') === 0) icon = 'fa-file-audio';
|
||||
else if (file.type && file.type.indexOf('application/pdf') === 0) icon = 'fa-file-pdf';
|
||||
else if (file.name && /\.(xlsx?|csv)$/i.test(file.name)) icon = 'fa-file-excel';
|
||||
$thumb.html('<i class="fas ' + icon + '"></i>');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add files to queue ──
|
||||
function addFiles(files) {
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var file = files[i];
|
||||
|
||||
// Multi check
|
||||
if (!multi && fileQueue.length >= 1) {
|
||||
// Replace existing file
|
||||
fileQueue = [];
|
||||
$fileList.empty();
|
||||
}
|
||||
|
||||
var id = nextId++;
|
||||
var extOk = isExtAllowed(file.name);
|
||||
var sizeOk = !upload.maxSize || file.size <= upload.maxSize;
|
||||
|
||||
var statusClass = '';
|
||||
var statusText = humanSize(file.size);
|
||||
if (!extOk) { statusClass = 'ots-upload-file--error'; statusText = 'Invalid file type'; }
|
||||
else if (!sizeOk) { statusClass = 'ots-upload-file--error'; statusText = 'File too large'; }
|
||||
|
||||
var $el = $(
|
||||
'<li class="ots-upload-file-item ' + statusClass + '" data-id="' + id + '">' +
|
||||
'<div class="ots-upload-file-thumb"></div>' +
|
||||
'<div class="ots-upload-file-info">' +
|
||||
'<span class="ots-upload-file-name">' + $('<span>').text(file.name).html() + '</span>' +
|
||||
'<span class="ots-upload-file-meta">' + statusText + '</span>' +
|
||||
'<div class="ots-upload-file-progress"><div class="ots-upload-file-progress-bar"></div></div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="ots-upload-file-remove" aria-label="Remove" title="Remove">' +
|
||||
'<i class="fas fa-times"></i>' +
|
||||
'</button>' +
|
||||
'</li>'
|
||||
);
|
||||
|
||||
generatePreview(file, $el.find('.ots-upload-file-thumb'));
|
||||
|
||||
$el.find('.ots-upload-file-remove').on('click', (function(fileId) {
|
||||
return function() { removeFile(fileId); };
|
||||
})(id));
|
||||
|
||||
$fileList.append($el);
|
||||
|
||||
fileQueue.push({
|
||||
file: file,
|
||||
id: id,
|
||||
status: (extOk && sizeOk) ? 'pending' : 'error',
|
||||
$el: $el,
|
||||
xhr: null
|
||||
});
|
||||
}
|
||||
|
||||
updateQueueUI();
|
||||
}
|
||||
|
||||
// ── Remove file ──
|
||||
function removeFile(id) {
|
||||
fileQueue = fileQueue.filter(function(f) {
|
||||
if (f.id === id) {
|
||||
f.$el.slideUp(200, function() { $(this).remove(); });
|
||||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
updateQueueUI();
|
||||
}
|
||||
|
||||
// ── Queue UI update ──
|
||||
function updateQueueUI() {
|
||||
var validFiles = fileQueue.filter(function(f) { return f.status === 'pending'; });
|
||||
var total = fileQueue.length;
|
||||
$queueCount.text(total + ' file' + (total !== 1 ? 's' : ''));
|
||||
if (total > 0) {
|
||||
$queue.removeClass('d-none');
|
||||
$dropzone.addClass('ots-upload-dropzone--has-files');
|
||||
} else {
|
||||
$queue.addClass('d-none');
|
||||
$dropzone.removeClass('ots-upload-dropzone--has-files');
|
||||
}
|
||||
// Show start button only when there are valid pending files and not already uploading
|
||||
if (validFiles.length > 0 && !uploading) {
|
||||
$btnStart.removeClass('d-none');
|
||||
} else if (!uploading) {
|
||||
$btnStart.addClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Upload all pending items (files + URLs) ──
|
||||
function startUpload() {
|
||||
var filePending = fileQueue.filter(function(f) { return f.status === 'pending'; });
|
||||
var urlPending = urlQueue.filter(function(f) { return f.status === 'pending'; });
|
||||
var allPending = filePending.concat(urlPending);
|
||||
if (allPending.length === 0) return;
|
||||
uploading = true;
|
||||
uploadCount = allPending.length;
|
||||
successCount = 0;
|
||||
|
||||
$btnStart.addClass('d-none');
|
||||
$btnCancel.text(trans.cancelUpload || 'Cancel');
|
||||
|
||||
// Upload sequentially
|
||||
var idx = 0;
|
||||
function uploadNext() {
|
||||
if (idx >= allPending.length) {
|
||||
uploading = false;
|
||||
onAllDone();
|
||||
return;
|
||||
}
|
||||
var item = allPending[idx++];
|
||||
if (item.file) {
|
||||
uploadSingle(item, uploadNext);
|
||||
} else if (item.url) {
|
||||
uploadUrlItem(item, uploadNext);
|
||||
} else {
|
||||
uploadNext();
|
||||
}
|
||||
}
|
||||
uploadNext();
|
||||
}
|
||||
|
||||
// ── Upload a single file ──
|
||||
function uploadSingle(item, callback) {
|
||||
item.status = 'uploading';
|
||||
item.$el.addClass('ots-upload-file--uploading');
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('files[]', item.file, item.file.name);
|
||||
|
||||
// Standard Xibo hidden fields
|
||||
if (tOpts.currentWorkingFolderId) formData.append('folderId', tOpts.currentWorkingFolderId);
|
||||
if (tOpts.oldMediaId) formData.append('oldMediaId', tOpts.oldMediaId);
|
||||
if (tOpts.oldFolderId) formData.append('oldFolderId', tOpts.oldFolderId);
|
||||
|
||||
// Checkboxes
|
||||
$optionsRow.find('input[type="checkbox"]').each(function() {
|
||||
formData.append($(this).attr('name'), $(this).is(':checked') ? '1' : '0');
|
||||
});
|
||||
|
||||
var $bar = item.$el.find('.ots-upload-file-progress-bar');
|
||||
var $meta = item.$el.find('.ots-upload-file-meta');
|
||||
|
||||
item.xhr = $.ajax({
|
||||
url: options.url,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
xhr: function() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable) {
|
||||
var pct = Math.round((e.loaded / e.total) * 100);
|
||||
$bar.css('width', pct + '%');
|
||||
$meta.text(pct + '%');
|
||||
}
|
||||
});
|
||||
return xhr;
|
||||
},
|
||||
success: function(response) {
|
||||
item.status = 'done';
|
||||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--done');
|
||||
$bar.css('width', '100%');
|
||||
$meta.text('Complete');
|
||||
successCount++;
|
||||
if (typeof options.uploadDoneEvent === 'function') {
|
||||
options.uploadDoneEvent(item.file, response);
|
||||
}
|
||||
callback();
|
||||
},
|
||||
error: function(xhr) {
|
||||
item.status = 'error';
|
||||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--error');
|
||||
var msg = 'Upload failed';
|
||||
try { msg = JSON.parse(xhr.responseText).message || msg; } catch(e){}
|
||||
$meta.text(msg);
|
||||
$bar.css('width', '0%');
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── All uploads finished ──
|
||||
function onAllDone() {
|
||||
$btnDone.removeClass('d-none');
|
||||
$btnStart.addClass('d-none');
|
||||
$queueCount.text(successCount + '/' + uploadCount + ' uploaded');
|
||||
}
|
||||
|
||||
// ── Drag & drop ──
|
||||
$dropzone.off('.otsUpload').on({
|
||||
'dragenter.otsUpload dragover.otsUpload': function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$dropzone.addClass('ots-upload-dropzone--over');
|
||||
},
|
||||
'dragleave.otsUpload': function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$dropzone.removeClass('ots-upload-dropzone--over');
|
||||
},
|
||||
'drop.otsUpload': function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$dropzone.removeClass('ots-upload-dropzone--over');
|
||||
var dt = e.originalEvent.dataTransfer;
|
||||
if (dt && dt.files && dt.files.length) {
|
||||
addFiles(dt.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Click to browse — use native .click() on the raw DOM element;
|
||||
// jQuery's .trigger('click') does NOT open the file picker in most browsers.
|
||||
$dropzone.off('click.otsUpload').on('click.otsUpload', function(e) {
|
||||
// Don't trigger if clicking on the remove button inside the queue
|
||||
if ($(e.target).closest('.ots-upload-file-remove').length) return;
|
||||
$input[0].click();
|
||||
});
|
||||
|
||||
// Keyboard accessibility on dropzone
|
||||
$dropzone.off('keydown.otsUpload').on('keydown.otsUpload', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
$input[0].click();
|
||||
}
|
||||
});
|
||||
|
||||
// File input change
|
||||
$input.off('change.otsUpload').on('change.otsUpload', function() {
|
||||
if (this.files && this.files.length) {
|
||||
addFiles(this.files);
|
||||
// Reset so the same file can be re-selected
|
||||
this.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Start upload button
|
||||
$btnStart.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
startUpload();
|
||||
});
|
||||
|
||||
// Done button
|
||||
$btnDone.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
if (mainBtn.callback) {
|
||||
mainBtn.callback();
|
||||
}
|
||||
$modal.modal('hide');
|
||||
});
|
||||
|
||||
// Clean up on modal close
|
||||
$modal.off('hidden.bs.modal.otsUpload').on('hidden.bs.modal.otsUpload', function() {
|
||||
// Abort any in-progress uploads
|
||||
fileQueue.forEach(function(f) {
|
||||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||||
});
|
||||
fileQueue = [];
|
||||
$fileList.empty();
|
||||
uploading = false;
|
||||
});
|
||||
|
||||
// ── Tab switching ──
|
||||
$tabFile.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
$tabFile.addClass('active');
|
||||
$tabUrl.removeClass('active');
|
||||
$panelFile.removeClass('d-none');
|
||||
$panelUrl.addClass('d-none');
|
||||
});
|
||||
$tabUrl.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
$tabUrl.addClass('active');
|
||||
$tabFile.removeClass('active');
|
||||
$panelUrl.removeClass('d-none');
|
||||
$panelFile.addClass('d-none');
|
||||
});
|
||||
|
||||
// ── URL: add to queue ──
|
||||
function addUrlToQueue(url) {
|
||||
if (!url || !url.trim()) return;
|
||||
url = url.trim();
|
||||
var id = nextId++;
|
||||
var displayName = url.length > 60 ? url.substring(0, 57) + '...' : url;
|
||||
|
||||
var $el = $(
|
||||
'<li class="ots-upload-file-item" data-id="' + id + '">' +
|
||||
'<div class="ots-upload-file-thumb"><i class="fas fa-link"></i></div>' +
|
||||
'<div class="ots-upload-file-info">' +
|
||||
'<span class="ots-upload-file-name">' + $('<span>').text(displayName).html() + '</span>' +
|
||||
'<span class="ots-upload-file-meta">Ready</span>' +
|
||||
'<div class="ots-upload-file-progress"><div class="ots-upload-file-progress-bar"></div></div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="ots-upload-file-remove" aria-label="Remove" title="Remove">' +
|
||||
'<i class="fas fa-times"></i>' +
|
||||
'</button>' +
|
||||
'</li>'
|
||||
);
|
||||
|
||||
$el.find('.ots-upload-file-remove').on('click', (function(fileId) {
|
||||
return function() { removeUrlItem(fileId); };
|
||||
})(id));
|
||||
|
||||
$urlList.append($el);
|
||||
urlQueue.push({ url: url, id: id, status: 'pending', $el: $el, xhr: null });
|
||||
updateUrlQueueUI();
|
||||
}
|
||||
|
||||
function removeUrlItem(id) {
|
||||
urlQueue = urlQueue.filter(function(f) {
|
||||
if (f.id === id) {
|
||||
f.$el.slideUp(200, function() { $(this).remove(); });
|
||||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
updateUrlQueueUI();
|
||||
}
|
||||
|
||||
function updateUrlQueueUI() {
|
||||
var pending = urlQueue.filter(function(f) { return f.status === 'pending'; });
|
||||
var total = urlQueue.length;
|
||||
$urlCount.text(total + ' URL' + (total !== 1 ? 's' : ''));
|
||||
if (total > 0) {
|
||||
$urlQueue.removeClass('d-none');
|
||||
} else {
|
||||
$urlQueue.addClass('d-none');
|
||||
}
|
||||
if (pending.length > 0 && !uploading) {
|
||||
$btnStart.removeClass('d-none');
|
||||
} else if (!uploading && fileQueue.filter(function(f) { return f.status === 'pending'; }).length === 0) {
|
||||
$btnStart.addClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
$urlAddBtn.off('click.otsUpload').on('click.otsUpload', function() {
|
||||
addUrlToQueue($urlInput.val());
|
||||
$urlInput.val('').focus();
|
||||
});
|
||||
|
||||
// Allow Enter key in URL input to add
|
||||
$urlInput.off('keydown.otsUpload').on('keydown.otsUpload', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addUrlToQueue($urlInput.val());
|
||||
$urlInput.val('').focus();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Upload a single URL item ──
|
||||
function uploadUrlItem(item, callback) {
|
||||
item.status = 'uploading';
|
||||
item.$el.addClass('ots-upload-file--uploading');
|
||||
var $bar = item.$el.find('.ots-upload-file-progress-bar');
|
||||
var $meta = item.$el.find('.ots-upload-file-meta');
|
||||
$bar.css('width', '50%');
|
||||
$meta.text('Downloading...');
|
||||
|
||||
var postData = { url: item.url };
|
||||
if (tOpts.currentWorkingFolderId) postData.folderId = tOpts.currentWorkingFolderId;
|
||||
|
||||
item.xhr = $.ajax({
|
||||
url: options.url,
|
||||
type: 'POST',
|
||||
data: postData,
|
||||
success: function(response) {
|
||||
item.status = 'done';
|
||||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--done');
|
||||
$bar.css('width', '100%');
|
||||
$meta.text('Complete');
|
||||
successCount++;
|
||||
if (typeof options.uploadDoneEvent === 'function') {
|
||||
options.uploadDoneEvent(null, response);
|
||||
}
|
||||
callback();
|
||||
},
|
||||
error: function(xhr) {
|
||||
item.status = 'error';
|
||||
item.$el.removeClass('ots-upload-file--uploading').addClass('ots-upload-file--error');
|
||||
var msg = 'Upload failed';
|
||||
try { msg = JSON.parse(xhr.responseText).message || msg; } catch(e){}
|
||||
$meta.text(msg);
|
||||
$bar.css('width', '0%');
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Clean up URL queue on modal close ──
|
||||
$modal.off('hidden.bs.modal.otsUploadUrl').on('hidden.bs.modal.otsUploadUrl', function() {
|
||||
urlQueue.forEach(function(f) {
|
||||
if (f.xhr) { try { f.xhr.abort(); } catch(e){} }
|
||||
});
|
||||
urlQueue = [];
|
||||
$urlList.empty();
|
||||
});
|
||||
|
||||
// ── Show modal ──
|
||||
$modal.modal({ backdrop: 'static', keyboard: true });
|
||||
};
|
||||
</script>
|
||||
0
ots-signs/views/index.html
Normal file
0
ots-signs/views/index.html
Normal file
327
ots-signs/views/inline.twig
Normal file
327
ots-signs/views/inline.twig
Normal file
@@ -0,0 +1,327 @@
|
||||
|
||||
{% macro disabled(name, title, value, helpText, groupClass) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}">{{ title }}</label>
|
||||
<input readonly class="form-control" value="{{ value }}"></input>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro hidden(name, value) %}
|
||||
<input name="{{ name }}" type="hidden" id="{{ name }}" value="{{ value }}" />
|
||||
{% endmacro %}
|
||||
|
||||
{% macro raw(text, groupClass) %}
|
||||
<div class="{{ groupClass }}">
|
||||
{{ text|raw }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro message(message, groupClass, messageStyleClass) %}
|
||||
<div class="{% if messageStyleClass %}{{messageStyleClass}}{% endif %} mr-1 {{ groupClass }}">
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro alert(message, alertType, groupClass) %}
|
||||
<div class="row">
|
||||
<div class="mr-3 alert alert-{% if alertType %}{{alertType}}{% else %}primary{% endif %} {{ groupClass }}" role="alert">{{ message }}</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro button(title, type, link, groupClass) %}
|
||||
<div class="form-group {{ groupClass }}">
|
||||
{% if type == "link" %}
|
||||
<a class="btn btn-white xibo-inline-btn mr-1 ml-0" href="{{ link }}">{{ title }}</a>
|
||||
{% else %}
|
||||
<button class="btn btn-white xibo-inline-btn mr-1 ml-0" type="{{ type }}">{{ title }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro input(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro inputWithTags(name, title, value, helpText, groupClass, validation, accessKey, exactTag, exactTagTitle, logicalOperatorTitle, autoCompleteEnabled = 1) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
{% if exactTag %}
|
||||
<div class="input-group input-group-tags-exact">
|
||||
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" data-role="tagsInputInline" {% if autoCompleteEnabled == 1 %} data-auto-complete-url="{{ url_for('tag.search') }}?allTags=1" {% endif %} {{ validation }} />
|
||||
<div class="input-group-append input-group-addon">
|
||||
<div class="input-group-text">
|
||||
<input title="{{ exactTagTitle }}" type="checkbox" id="{{ exactTag }}" name="{{ exactTag }}">
|
||||
</div>
|
||||
<select class="custom-select" id="logicalOperator" name="logicalOperator" title="{{ logicalOperatorTitle }}" style="min-width:auto!important">
|
||||
<option value="OR" selected>OR</option>
|
||||
<option value="AND">AND</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" data-role="tagsInputInline" {% if autoCompleteEnabled == 1 %} data-auto-complete-url="{{ url_for('tag.search') }}?allTags=1" {% endif %} {{ validation }} />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro number(name, title, value, helpText, groupClass, validation, accessKey, maxNumber, minNumber) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control" name="{{ name }}" {% if maxNumber %}max="{{maxNumber}}" {% endif %}{% if minNumber %}min="{{minNumber}}" {% endif %}type="number" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro email(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control" name="{{ name }}" type="email" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro password(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" for="{{ name }}" title="{{ helpText }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control" name="{{ name }}" type="password" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro checkbox(name, title, value, groupClass, accessKey) %}
|
||||
<div class="form-group ml-2 mr-3 mb-1 {{ groupClass }}">
|
||||
<div class="form-check">
|
||||
<input title="{{ title }}" class="form-check-input" type="checkbox" id="{{ name }}" name="{{ name }}" {% if value == 1 %}checked{% endif %}>
|
||||
<label class="form-check-label" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro radio(name, id, title, value, helpText, groupClass, accessKey, setValue) %}
|
||||
<div class="form-group ml-2 mr-3 mb-1 {{ groupClass }}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="{{ id }}" name="{{ name }}" value="{{ setValue }}" {% if value == setValue %}checked{% endif %}>
|
||||
<label class="form-check-label" for="{{ name }}" title="{{ helpText }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dropdown(name, type, title, value, options, optionId, optionValue, helpText, groupClass, validation, accessKey, callBack, dataAttributes, optionGroups) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" for="{{ name }}" title="{{ helpText }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<select class="form-control" {% if type == "dropdownmulti" %}multiple{% endif %} name="{{ name }}" id="{{ name }}" {{ callBack }}
|
||||
{% if type == "dropdownmulti" %}
|
||||
data-allow-clear="true"
|
||||
data-placeholder--id=null
|
||||
data-placeholder--value=""
|
||||
{% endif %}
|
||||
{% if dataAttributes|length > 0 %}
|
||||
{% for attribute in dataAttributes %}
|
||||
{{ attribute.name }}="{{ attribute.value }}"
|
||||
{% endfor %}
|
||||
{% endif %}>
|
||||
|
||||
{% set hasGroups = optionGroups|length > 0 %}
|
||||
{% if not hasGroups %}
|
||||
{% set optionGroups = {label: "General"} %}
|
||||
{% endif %}
|
||||
|
||||
{% for group in optionGroups %}
|
||||
{% if hasGroups %}
|
||||
<optgroup label="{{ group.label }}">
|
||||
{% set tempOptions = attribute(options, group.id) %}
|
||||
{% else %}
|
||||
{% set tempOptions = options %}
|
||||
{% endif %}
|
||||
|
||||
{% for option in tempOptions %}
|
||||
|
||||
{% set itemOptionId = attribute(option, optionId) %}
|
||||
{% set itemOptionValue = attribute(option, optionValue) %}
|
||||
|
||||
{% if type == "dropdownmulti" %}
|
||||
{% set selected = (itemOptionId in value) %}
|
||||
{% else %}
|
||||
{% set selected = (itemOptionId == value) %}
|
||||
{% endif %}
|
||||
|
||||
<option value="{{ itemOptionId }}" {% if selected %}selected{% endif %}>{{ itemOptionValue }}</option>
|
||||
{% endfor %}
|
||||
|
||||
{% if hasGroups %}
|
||||
</optgroup>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro permissions(name, options) %}
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>{% trans "Group" %}</th>
|
||||
<th>{% trans "View" %}</th>
|
||||
<th>{% trans "Edit" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
{% for item in options %}
|
||||
<tr>
|
||||
<td>{{ name }}</td>
|
||||
<td><input type="checkbox" name="{{ name }}" value="{{ value_view }}" {{ value_view_checked }}></td>
|
||||
<td><input type="checkbox" name="{{ name }}" value="{{ value_edit }}" {{ value_edit_checked }}></td>
|
||||
<td><input type="checkbox" name="{{ name }}" value="{{ value_del }}" {{ value_del_checked }}></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro date(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></div>
|
||||
<input class="form-control dateControl date" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dateMonth(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></span>
|
||||
<input class="form-control dateControl month" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dateTime(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ linkedName }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></span>
|
||||
<input class="form-control dateControl dateTime" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro time(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1 {% if title == '' %}d-none{% endif %}" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></span>
|
||||
<input class="form-control dateControl time" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro switch(name, title, value, labelWidth, switchSize, onText, offText, groupClass, accessKey, disabled) %}
|
||||
<div class="form-group {{ groupClass }}">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="bootstrap-switch-target" id="{{ name }}" name="{{ name }}" accesskey="{{ accessKey }}"
|
||||
{% if value == 1 %}checked{% endif %}
|
||||
{% if disabled == 1 %}disabled{% endif %}
|
||||
data-label-text="{{ title }}"
|
||||
{% if onText not in [null, undefined, ""] %} data-on-text="{{ onText }}"{% endif %}
|
||||
{% if offText not in [null, undefined, ""] %} data-off-text="{{ offText }}"{% endif %}
|
||||
{% if switchSize not in [null, undefined, ""] %}data-size="{{ switchSize }}"{% else %}data-size="small"{% endif %}
|
||||
{% if labelWidth not in [null, undefined, ""] %} data-label-width="{{ labelWidth }}"{% endif %}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro color(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control XiboColorPicker" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro inputNameGrid(name, title, groupClass, useRegexName, logicalOperatorName) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="" for="{{ name }}" accesskey="">{{ title }}</label>
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="">
|
||||
<div class="input-group-append input-group-addon">
|
||||
<div class="input-group-text">
|
||||
<input title="{% trans "Use Regex?" %}" type="checkbox" {% if useRegexName %} id="{{ useRegexName }}" name="{{ useRegexName }}" {% else %} id="useRegexForName" name="useRegexForName"{% endif %}>
|
||||
</div>
|
||||
<select class="custom-select" {% if logicalOperatorName %} id="{{ logicalOperatorName }}" name="{{ logicalOperatorName }}" {% else %} id="logicalOperatorName" name="logicalOperatorName"{% endif %}
|
||||
title="{% trans "When filtering by multiple names, which logical operator should be used?" %}" style="min-width:auto!important">
|
||||
<option value="OR" selected>OR</option>
|
||||
<option value="AND">AND</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dateRangeFilter(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 d-flex flex-row {{ groupClass }}">
|
||||
{% set today = now | date_modify('today') | date("Y-m-d H:i:s") %}
|
||||
<div class="form-group mr-1">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">
|
||||
{{ title }}
|
||||
</label>
|
||||
<div class="d-inline-flex">
|
||||
<select class="form-control XiboDateRangeFilter" name="{{ name }}" id="{{ name }}">
|
||||
<option value="" >{% trans "Select a range" %}</option>
|
||||
<option value="today" selected>{% trans "Today" %}</option>
|
||||
<option value="yesterday">{% trans "Yesterday" %}</option>
|
||||
<option value="thisweek">{% trans "This Week" %}</option>
|
||||
<option value="thismonth">{% trans "This Month" %}</option>
|
||||
<option value="thisyear">{% trans "This Year" %}</option>
|
||||
<option value="lastweek">{% trans "Last Week" %}</option>
|
||||
<option value="lastmonth">{% trans "Last Month" %}</option>
|
||||
<option value="lastyear">{% trans "Last Year" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group hidden mr-1 {{ 'rangeFilterInput_' ~ name }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">
|
||||
{% trans "From Date" %}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend input-group-text date-open-button" role="button">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</div>
|
||||
<input class="form-control dateControl date rangeInput"
|
||||
type="text" name="fromDt" id="{{ 'fromDt_' ~ name }}"
|
||||
value="{{ today }}"
|
||||
/>
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none"
|
||||
role="button"
|
||||
>
|
||||
<i class="fa fa-times"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group hidden {{ 'rangeFilterInput_' ~ name }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">
|
||||
{% trans "To Date" %}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend input-group-text date-open-button" role="button">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</div>
|
||||
<input class="form-control dateControl date rangeInput"
|
||||
type="text" name="toDt" id="{{ 'toDt_' ~ name }}"
|
||||
value="{{ today | date_modify('+1 day -1 second') | date("Y-m-d H:i:s") }}"
|
||||
/>
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none"
|
||||
role="button"
|
||||
>
|
||||
<i class="fa fa-times"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
528
ots-signs/views/layout-page.twig
Normal file
528
ots-signs/views/layout-page.twig
Normal file
@@ -0,0 +1,528 @@
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Layouts"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Layouts" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage and design your layouts." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-type="layout" data-grid-name="layoutView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Layouts" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item"><a class="nav-link active" href="#general-filter" role="tab" data-toggle="tab" aria-selected="true"><span>{% trans "General" %}</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#advanced-filter" role="tab" data-toggle="tab" aria-selected="false"><span>{% trans "Advanced" %}</span></a></li>
|
||||
</ul>
|
||||
<form class="form-inline d-block">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="general-filter" role="tabpanel">
|
||||
{% set title %}{% trans "ID" %}{% endset %}
|
||||
{{ inline.number("campaignId", title) }}
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('layout', title) }}
|
||||
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}
|
||||
{% set title %}{% trans "Tags" %}{% endset %}
|
||||
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
|
||||
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
|
||||
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
|
||||
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
|
||||
{% endif %}
|
||||
|
||||
{% set title %}{% trans "Code" %}{% endset %}
|
||||
{{ inline.input('codeLike', title) }}
|
||||
|
||||
{% if currentUser.featureEnabled("displaygroup.view") %}
|
||||
{% set title %}{% trans "Display Group" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show Layouts active on the selected Display / Display Group" %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("displayGroup.search") },
|
||||
{ name: "data-filter-options", value: '{"isDisplaySpecific":-1}' },
|
||||
{ name: "data-search-term", value: "displayGroup" },
|
||||
{ name: "data-id-property", value: "displayGroupId" },
|
||||
{ name: "data-text-property", value: "displayGroup" },
|
||||
{ name: "data-initial-key", value: "displayGroupId" },
|
||||
] %}
|
||||
{{ inline.dropdown("activeDisplayGroupId", "single", title, "", null, "displayGroupId", "displayGroup", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
{% endif %}
|
||||
|
||||
{% set title %}{% trans "Owner" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("user.search") },
|
||||
{ name: "data-search-term", value: "userName" },
|
||||
{ name: "data-search-term-tags", value: "tags" },
|
||||
{ name: "data-id-property", value: "userId" },
|
||||
{ name: "data-text-property", value: "userName" },
|
||||
{ name: "data-initial-key", value: "userId" },
|
||||
] %}
|
||||
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
|
||||
{% set title %}{% trans "Owner User Group" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show items owned by users in the selected User Group." %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("group.search") },
|
||||
{ name: "data-search-term", value: "group" },
|
||||
{ name: "data-id-property", value: "groupId" },
|
||||
{ name: "data-text-property", value: "group" },
|
||||
{ name: "data-initial-key", value: "userGroupId" },
|
||||
] %}
|
||||
{{ inline.dropdown("ownerUserGroupId", "single", title, "", null, "groupId", "group", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
|
||||
{% set title %}{% trans "Orientation" %}{% endset %}
|
||||
{% set option1 = "All"|trans %}
|
||||
{% set option2 = "Landscape"|trans %}
|
||||
{% set option3 = "Portrait"|trans %}
|
||||
{% set values = [{id: '', value: option1}, {id: 'landscape', value: option2}, {id: 'portrait', value: option3}] %}
|
||||
{{ inline.dropdown("orientation", "single", title, '', values, "id", "value") }}
|
||||
|
||||
{{ inline.hidden("folderId") }}
|
||||
</div>
|
||||
<div class="tab-pane" id="advanced-filter" role="tabpanel">
|
||||
{% set title %}{% trans "Retired" %}{% endset %}
|
||||
{% set option1 = "No"|trans %}
|
||||
{% set option2 = "Yes"|trans %}
|
||||
{% set values = [{id: 0, value: option1}, {id: 1, value: option2}] %}
|
||||
{{ inline.dropdown("retired", "single", title, 0, values, "id", "value") }}
|
||||
|
||||
{% set title %}{% trans "Show" %}{% endset %}
|
||||
{% set option1 = "All"|trans %}
|
||||
{% set option2 = "Only Used"|trans %}
|
||||
{% set option3 = "Only Unused"|trans %}
|
||||
{% set values = [{id: 1, value: option1}, {id: 2, value: option2}, {id: 3, value: option3}] %}
|
||||
{{ inline.dropdown("layoutStatusId", "single", title, 1, values, "id", "value") }}
|
||||
|
||||
{% set title %}{% trans "Description" %}{% endset %}
|
||||
{% set option1 = "All"|trans %}
|
||||
{% set option2 = "1st line"|trans %}
|
||||
{% set option3 = "Widget List"|trans %}
|
||||
{% set values = [{id: 1, value: option1}, {id: 2, value: option2}, {id: 3, value: option3}] %}
|
||||
{{ inline.dropdown("showDescriptionId", "single", title, 2, values, "id", "value") }}
|
||||
|
||||
{% if currentUser.featureEnabled("library.view") %}
|
||||
{% set title %}{% trans "Media" %}{% endset %}
|
||||
{{ inline.input("mediaLike", title) }}
|
||||
{% endif %}
|
||||
|
||||
{% set title %}{% trans "Layout ID" %}{% endset %}
|
||||
{{ inline.number("layoutId", title) }}
|
||||
|
||||
{% set title %}{% trans "Modified Since" %}{% endset %}
|
||||
{{ inline.date("modifiedSinceDt", title) }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabled("layout.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn layout-add-button" title="{% trans "Add a new Layout and jump to the layout editor." %}" href="{{ url_for("layout.add") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-sm btn-info ots-toolbar-btn" id="layoutUploadForm" title="{% trans "Import a Layout from a ZIP file." %}" href="#"><i class="fa fa-cloud-download" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="layouts" class="table table-striped responsive nowrap" data-content-type="layout" data-content-id-name="layoutId" data-state-preference-name="layoutGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Duration" %}</th>
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
|
||||
<th>{% trans "Orientation" %}</th>
|
||||
<th>{% trans "Thumbnail" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Sharing" %}</th>
|
||||
<th>{% trans "Valid?" %}</th>
|
||||
<th>{% trans "Stats?" %}</th>
|
||||
<th>{% trans "Created" %}</th>
|
||||
<th>{% trans "Modified" %}</th>
|
||||
<th>{% trans "Layout ID" %}</th>
|
||||
<th>{% trans "Code" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table;
|
||||
$(document).ready(function() {
|
||||
{% if not currentUser.featureEnabled("folder.view") %}
|
||||
disableFolders();
|
||||
{% endif %}
|
||||
|
||||
table = $("#layouts").DataTable({
|
||||
language: dataTablesLanguage,
|
||||
lengthMenu: [10, 25, 50, 100, 250, 500],
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
dataType: 'json',
|
||||
order: [[1, "asc"]],
|
||||
ajax: {
|
||||
url: "{{ url_for("layout.search") }}",
|
||||
data: function (d) {
|
||||
$.extend(d, $("#layouts").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{"data": "campaignId", responsivePriority: 1},
|
||||
{
|
||||
"data": "layout",
|
||||
responsivePriority: 2,
|
||||
"render": dataTableSpacingPreformatted
|
||||
},
|
||||
{
|
||||
"name": "publishedStatus",
|
||||
responsivePriority: 2,
|
||||
"data": function (data, type) {
|
||||
if (data.publishedDate != null) {
|
||||
var now = moment();
|
||||
var published = moment(data.publishedDate);
|
||||
var differenceMinutes = published.diff(now, 'minutes');
|
||||
var momentDifference = moment(now).to(published);
|
||||
|
||||
if (differenceMinutes < -5) {
|
||||
return data.publishedStatus.concat(" - ", translations.publishedStatusFailed);
|
||||
} else {
|
||||
return data.publishedStatus.concat(" - ", translations.publishedStatusFuture + " " + momentDifference);
|
||||
}
|
||||
} else {
|
||||
return data.publishedStatus;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"data": null,
|
||||
responsivePriority: 10,
|
||||
"render": {"_": "description", "display": "descriptionFormatted", "sort": "description"}
|
||||
},
|
||||
{
|
||||
"name": "duration",
|
||||
responsivePriority: 3,
|
||||
"data": function (data, type) {
|
||||
if (type != "display")
|
||||
return data.duration;
|
||||
|
||||
return dataTableTimeFromSeconds(data.duration, type);
|
||||
}
|
||||
},
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}{
|
||||
"sortable": false,
|
||||
"visible": false,
|
||||
responsivePriority: 3,
|
||||
"data": dataTableCreateTags
|
||||
},{% endif %}
|
||||
{ data: 'orientation', responsivePriority: 10, visible: false},
|
||||
{
|
||||
responsivePriority: 5,
|
||||
data: 'thumbnail',
|
||||
render: function(data, type, row) {
|
||||
if (type !== 'display') {
|
||||
return row.layoutId;
|
||||
}
|
||||
if (data) {
|
||||
return '<a class="img-replace" data-toggle="lightbox" data-type="image" href="' + data + '">' +
|
||||
'<img class="img-fluid" src="' + data + '" alt="{{ "Thumbnail"|trans }}" />' +
|
||||
'</a>';
|
||||
} else {
|
||||
var addUrl = '{{ url_for("layout.thumbnail.add", {id: ":id"}) }}'.replace(':id', row.layoutId);
|
||||
return '<a class="img-replace generate-layout-thumbnail" data-type="image" href="' + addUrl + '">' +
|
||||
'<img class="img-fluid" src="{{ theme.uri("img/thumbs/placeholder.png") }}" alt="{{ "Add Thumbnail"|trans }}" />' +
|
||||
'</a>';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
sortable: false
|
||||
},
|
||||
{"data": "owner", responsivePriority: 4},
|
||||
{
|
||||
"data": "groupsWithPermissions",
|
||||
responsivePriority: 4,
|
||||
"render": dataTableCreatePermissions
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
responsivePriority: 3,
|
||||
"data": function (data, type) {
|
||||
if (type != "display")
|
||||
return data.status;
|
||||
|
||||
var icon = "";
|
||||
if (data.status == 1)
|
||||
icon = "fa-check";
|
||||
else if (data.status == 2)
|
||||
icon = "fa-exclamation";
|
||||
else if (data.status == 3)
|
||||
icon = "fa-cogs";
|
||||
else
|
||||
icon = "fa-times";
|
||||
|
||||
return '<span class="fa ' + icon + '" title="' + (data.statusDescription) + ((data.statusMessage == null) ? "" : " - " + (data.statusMessage)) + '"></span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "enableStat",
|
||||
responsivePriority: 4,
|
||||
"data": function (data) {
|
||||
|
||||
var icon = "";
|
||||
if (data.enableStat == 1)
|
||||
icon = "fa-check";
|
||||
else
|
||||
icon = "fa-times";
|
||||
|
||||
return '<span class="fa ' + icon + '" title="' + (data.enableStatDescription) + '"></span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": "createdDt",
|
||||
responsivePriority: 6,
|
||||
"render": dataTableDateFromIso,
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
data: "modifiedDt",
|
||||
responsivePriority: 6,
|
||||
render: dataTableDateFromIso,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
data: "layoutId",
|
||||
visible: false,
|
||||
responsivePriority: 4
|
||||
},
|
||||
{"data": "code", "visible":false, responsivePriority: 4},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('draw', { form: $("#layouts").closest(".XiboGrid").find(".FilterDiv form") }, dataTableCreateTagEvents);
|
||||
table.on('draw', function(e, settings) {
|
||||
$('#' + e.target.id + ' .generate-layout-thumbnail').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var $anchor = $(this);
|
||||
$.ajax({
|
||||
url: $anchor.attr('href'),
|
||||
method: 'POST',
|
||||
success: function() {
|
||||
$anchor.find('img').attr('src', $anchor.attr('href'));
|
||||
$anchor.removeClass('generate-layout-thumbnail').attr('data-toggle', 'lightbox');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#layouts_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function() {
|
||||
table.ajax.reload();
|
||||
});
|
||||
|
||||
// Bind to the layout add button
|
||||
$('button.layout-add-button').on('click', function() {
|
||||
let currentWorkingFolderId =
|
||||
$("#layouts")
|
||||
.closest(".XiboGrid")
|
||||
.find(".FilterDiv form")
|
||||
.find('#folderId').val()
|
||||
// Submit the URL provided as a POST request.
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: $(this).attr('href'),
|
||||
cache: false,
|
||||
data : {folderId : currentWorkingFolderId},
|
||||
dataType: 'json',
|
||||
success: function(response, textStatus, error) {
|
||||
if (response.success && response.id) {
|
||||
XiboRedirect('{{ url_for("layout.designer", {id: ':id'}) }}'.replace(':id', response.id));
|
||||
} else {
|
||||
if (response.login) {
|
||||
LoginBox(response.message);
|
||||
} else {
|
||||
SystemMessage(response.message ?? '{{ "Unknown Error"|trans }}', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(xhr, textStatus, errorThrown) {
|
||||
SystemMessage(xhr.responseText, false);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$("#layoutUploadForm").click(function(e) {
|
||||
e.preventDefault();
|
||||
var currentWorkingFolderId = $('#folderId').val();
|
||||
|
||||
// Open the upload dialog with our options.
|
||||
openUploadForm({
|
||||
url: "{{ url_for("layout.import") }}",
|
||||
title: "{{ "Upload Layout"|trans }}",
|
||||
videoImageCovers: false,
|
||||
buttons: {
|
||||
main: {
|
||||
label: "{{ "Done"|trans }}",
|
||||
className: "btn-primary btn-bb-main",
|
||||
callback: function () {
|
||||
table.ajax.reload();
|
||||
XiboDialogClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
templateOptions: {
|
||||
layoutImport: true,
|
||||
updateInAllChecked: {% if settings.LIBRARY_MEDIA_UPDATEINALL_CHECKB == 1 %}true{% else %}false{% endif %},
|
||||
deleteOldRevisionsChecked: {% if settings.LIBRARY_MEDIA_DELETEOLDVER_CHECKB == 1 %}true{% else %}false{% endif %},
|
||||
trans: {
|
||||
addFiles: "{{ "Add Layout Export ZIP Files"|trans }}",
|
||||
startUpload: "{{ "Start Import"|trans }}",
|
||||
cancelUpload: "{{ "Cancel Import"|trans }}",
|
||||
replaceExistingMediaMessage: "{{ "Replace Existing Media?"|trans }}",
|
||||
importTagsMessage: "{{ "Import Tags?"|trans }}",
|
||||
useExistingDataSetsMessage: "{{ "Use existing DataSets matched by name?"|trans }}",
|
||||
dataSetDataMessage: "{{ "Import DataSet Data?"|trans }}",
|
||||
fallbackMessage: "{{ "Import Widget Fallback Data?"|trans }}",
|
||||
selectFolder: "{{ "Select Folder"|trans }}",
|
||||
selectFolderTitle: "{{ "Change Current Folder location"|trans }}",
|
||||
selectedFolder: "{{ "Current Folder"|trans }}:",
|
||||
selectedFolderTitle: "{{ "Upload files to this Folder"|trans }}"
|
||||
},
|
||||
upload: {
|
||||
maxSize: {{ libraryUpload.maxSize }},
|
||||
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
|
||||
validExt: "zip"
|
||||
},
|
||||
currentWorkingFolderId: currentWorkingFolderId,
|
||||
folderSelector: true
|
||||
},
|
||||
formOpenedEvent: function () {
|
||||
// Configure the active behaviour of the checkboxes
|
||||
$("#useExistingDataSets").on("click", function () {
|
||||
$("#importDataSetData").prop("disabled", ($(this).is(":checked")));
|
||||
});
|
||||
},
|
||||
uploadDoneEvent: function (data) {
|
||||
XiboDialogClose();
|
||||
table.ajax.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function layoutExportFormSubmit() {
|
||||
var $form = $("#layoutExportForm");
|
||||
window.location = $form.attr("action") + "?" + $form.serialize();
|
||||
|
||||
setTimeout(function() {
|
||||
XiboDialogClose();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function assignLayoutToCampaignFormSubmit() {
|
||||
var form = $("#layoutAssignCampaignForm");
|
||||
|
||||
var url = form.prop("action").replace(":id", form.find("#campaignId").val());
|
||||
|
||||
$.ajax({
|
||||
type: form.attr("method"),
|
||||
url: url,
|
||||
data: {layoutId: form.data().layoutId},
|
||||
cache: false,
|
||||
dataType:"json",
|
||||
success: XiboSubmitResponse
|
||||
});
|
||||
}
|
||||
|
||||
function setEnableStatMultiSelectFormOpen(dialog) {
|
||||
var $input = $('<input type=checkbox id="enableStat" name="enableStat"> {{ "Enable Stats Collection?"|trans }} </input>');
|
||||
var $helpText = $('<span class="help-block">{{ "Check to enable the collection of Proof of Play statistics for the selected items."|trans }}</span>');
|
||||
|
||||
$input.on('change', function() {
|
||||
dialog.data().commitData = {enableStat: $(this).val()};
|
||||
});
|
||||
|
||||
$(dialog).find('.modal-body').append($input);
|
||||
$(dialog).find('.modal-body').append($helpText);
|
||||
}
|
||||
|
||||
function layoutPublishFormOpen() {
|
||||
// Nothing to do here, but we use the same form on the layout designer and have a callback registered there
|
||||
}
|
||||
|
||||
function layoutEditFormSaved() {
|
||||
// Nothing to do here.
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
580
ots-signs/views/library-page.twig
Normal file
580
ots-signs/views/library-page.twig
Normal file
@@ -0,0 +1,580 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2022 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Library"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Media" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your media library." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="libraryView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Media" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "ID" %}{% endset %}
|
||||
{{ inline.number("mediaId", title) }}
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('media', title) }}
|
||||
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}
|
||||
{% set title %}{% trans "Tags" %}{% endset %}
|
||||
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
|
||||
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
|
||||
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
|
||||
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
|
||||
{% endif %}
|
||||
|
||||
{% set attributes = [
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" }
|
||||
] %}
|
||||
|
||||
{% set title %}{% trans "Owner" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("user.search") },
|
||||
{ name: "data-search-term", value: "userName" },
|
||||
{ name: "data-search-term-tags", value: "tags" },
|
||||
{ name: "data-id-property", value: "userId" },
|
||||
{ name: "data-text-property", value: "userName" },
|
||||
{ name: "data-initial-key", value: "userId" },
|
||||
] %}
|
||||
{{ inline.dropdown("ownerId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
|
||||
{% set title %}{% trans "Owner User Group" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show items owned by users in the selected User Group." %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("group.search") },
|
||||
{ name: "data-search-term", value: "group" },
|
||||
{ name: "data-id-property", value: "groupId" },
|
||||
{ name: "data-text-property", value: "group" },
|
||||
{ name: "data-initial-key", value: "userGroupId" },
|
||||
] %}
|
||||
{{ inline.dropdown("ownerUserGroupId", "single", title, "", null, "groupId", "group", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
|
||||
{% set title %}{% trans "Type" %}{% endset %}
|
||||
{{ inline.dropdown("type", "single", title, "", [{"type": none, "name": ""}]|merge(modules), "type", "name") }}
|
||||
|
||||
{% set title %}{% trans "Retired" %}{% endset %}
|
||||
{% set values = [{id: 0, value: "No"}, {id: 1, value: "Yes"}] %}
|
||||
{{ inline.dropdown("retired", "single", title, 0, values, "id", "value") }}
|
||||
|
||||
{{ inline.hidden("folderId") }}
|
||||
|
||||
{% set title %}{% trans "Layout ID" %}{% endset %}
|
||||
{{ inline.number("layoutId", title, layoutId) }}
|
||||
|
||||
{% set title %}{% trans "Orientation" %}{% endset %}
|
||||
{% set option1 = "All"|trans %}
|
||||
{% set option2 = "Landscape"|trans %}
|
||||
{% set option3 = "Portrait"|trans %}
|
||||
{% set values = [{id: '', value: option1}, {id: 'landscape', value: option2}, {id: 'portrait', value: option3}] %}
|
||||
{{ inline.dropdown("orientation", "single", title, '', values, "id", "value") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabledCount(["library.add", "library.modify"]) > 0 or settings.SETTING_LIBRARY_TIDY_ENABLED == 1 %}
|
||||
{% if currentUser.featureEnabled("library.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn" href="#" id="libraryUploadForm" title="{% trans "Add a new media item to the library" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button> {% endif %}
|
||||
{% if settings.SETTING_LIBRARY_TIDY_ENABLED == 1 and currentUser.featureEnabled("library.modify") %}
|
||||
<button class="btn btn-sm btn-warning ots-toolbar-btn XiboFormButton btn-tidy" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("library.tidy.form") }}"><i class="fa fa-broom" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="libraryItems" class="table table-striped responsive nowrap" data-content-type="media" data-content-id-name="mediaId" data-state-preference-name="libraryGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tag" %}</th>{% endif %}
|
||||
<th>{% trans "Thumbnail" %}</th>
|
||||
<th>{% trans "Duration" %}</th>
|
||||
<th>{% trans "Duration (seconds)" %}</th>
|
||||
<th>{% trans "Size" %}</th>
|
||||
<th>{% trans "Size (bytes)" %}</th>
|
||||
<th>{% trans "Resolution" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Sharing" %}</th>
|
||||
<th>{% trans "Revised" %}</th>
|
||||
<th>{% trans "Released" %}</th>
|
||||
<th>{% trans "File Name" %}</th>
|
||||
<th>{% trans "Stats?" %}</th>
|
||||
<th>{% trans "Created" %}</th>
|
||||
<th>{% trans "Modified" %}</th>
|
||||
<th>{% trans "Expires" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table;
|
||||
$(document).ready(function() {
|
||||
|
||||
{% if not currentUser.featureEnabled("folder.view") %}
|
||||
disableFolders();
|
||||
{% endif %}
|
||||
|
||||
table = $("#libraryItems").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
responsive: true,
|
||||
stateDuration: 0,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("library.search") }}",
|
||||
"data": function (d) {
|
||||
$.extend(d, $("#libraryItems").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "mediaId", responsivePriority: 2},
|
||||
{"data": "name", "render": dataTableSpacingPreformatted, responsivePriority: 3 },
|
||||
{"data": "mediaType", responsivePriority: 2},
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}{
|
||||
"sortable": false,
|
||||
responsivePriority: 2,
|
||||
"visible": false,
|
||||
"data": dataTableCreateTags
|
||||
},{% endif %}
|
||||
{
|
||||
responsivePriority: 5,
|
||||
data: 'thumbnail',
|
||||
render: function(data, type, row) {
|
||||
if (type !== 'display') {
|
||||
return row.mediaId;
|
||||
}
|
||||
if (data) {
|
||||
return '<a class="img-replace" data-toggle="lightbox" data-type="image" href="' + data + '">' +
|
||||
'<img class="img-fluid" src="' + data.replace('download', 'thumbnail') + '" alt="{{ "Thumbnail"|trans }}" />' +
|
||||
'</a>';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
"name": "duration",
|
||||
responsivePriority: 3,
|
||||
"data": function (data, type) {
|
||||
if (type != "display")
|
||||
return data.duration;
|
||||
|
||||
return dataTableTimeFromSeconds(data.duration, type);
|
||||
}
|
||||
},
|
||||
{"data": "duration", "visible": false, responsivePriority: 10},
|
||||
{
|
||||
"name": "fileSize",
|
||||
responsivePriority: 3,
|
||||
"data": null,
|
||||
"render": {"_": "fileSize", "display": "fileSizeFormatted", "sort": "fileSize"}
|
||||
},
|
||||
{"data": "fileSize", "visible": false, responsivePriority: 10},
|
||||
{
|
||||
name: 'width',
|
||||
data: function(data, type, row, meta) {
|
||||
if (type !== 'display' || data.width === 0 || data.height === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return data.width + 'x' + data.height;
|
||||
},
|
||||
visible: false,
|
||||
responsivePriority: 10
|
||||
},
|
||||
{"data": "owner", responsivePriority: 5},
|
||||
{
|
||||
"data": "groupsWithPermissions",
|
||||
responsivePriority: 5,
|
||||
"render": dataTableCreatePermissions
|
||||
},
|
||||
{"data": "revised", "render": dataTableTickCrossColumn, "visible": false, responsivePriority: 6},
|
||||
{
|
||||
"name": "released",
|
||||
responsivePriority: 6,
|
||||
"data": function (data, type) {
|
||||
if (type != "display")
|
||||
return data.released;
|
||||
|
||||
var icon = "";
|
||||
if (data.released == 1)
|
||||
icon = "fa-check";
|
||||
else if (data.released == 0)
|
||||
icon = "fa-cogs";
|
||||
else if (data.released == 2)
|
||||
icon = "fa-times";
|
||||
|
||||
|
||||
return '<span class="fa ' + icon + '" title="' + (data.releasedDescription) + '"></span>';
|
||||
},
|
||||
"visible": false
|
||||
},
|
||||
{"data": "fileName", responsivePriority: 500},
|
||||
{
|
||||
"name": "enableStat",
|
||||
responsivePriority: 6,
|
||||
"data": function (data) {
|
||||
|
||||
var icon = "";
|
||||
if (data.enableStat == 'On')
|
||||
icon = "fa-check";
|
||||
else if (data.enableStat == 'Off')
|
||||
icon = "fa-times";
|
||||
else
|
||||
icon = "fa-level-down";
|
||||
|
||||
return '<span class="fa ' + icon + '" title="' + (data.enableStatDescription) + '"></span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": "createdDt",
|
||||
responsivePriority: 6,
|
||||
"render": dataTableDateFromIso,
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"data": "modifiedDt",
|
||||
responsivePriority: 6,
|
||||
"render": dataTableDateFromIso,
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"name": "expires",
|
||||
responsivePriority: 6,
|
||||
"data": function (data, type) {
|
||||
if (data.expires != null && data.expires != 0) {
|
||||
var now = moment();
|
||||
var expiresIn = moment.unix(data.expires);
|
||||
var differenceMinutes = expiresIn.diff(now, 'minutes');
|
||||
var momentDifference = moment(now).to(expiresIn);
|
||||
|
||||
if (differenceMinutes < -10 ) {
|
||||
return data.mediaExpiryFailed;
|
||||
} else {
|
||||
return data.mediaExpiresIn.replace('%s', momentDifference);
|
||||
}
|
||||
} else {
|
||||
return data.mediaNoExpiryDate;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('draw', { form: $("#libraryItems").closest(".XiboGrid").find(".FilterDiv form") } ,dataTableCreateTagEvents);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#libraryItems_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
$("#libraryUploadForm").click(function(e) {
|
||||
e.preventDefault();
|
||||
var currentWorkingFolderId = $('#folderId').val();
|
||||
|
||||
openUploadForm({
|
||||
url: "{{ url_for("library.add") }}",
|
||||
title: "{% trans "Add Media" %}",
|
||||
initialisedBy: "library-upload",
|
||||
buttons: {
|
||||
main: {
|
||||
label: "{% trans "Done" %}",
|
||||
className: "btn-primary btn-bb-main",
|
||||
callback: function () {
|
||||
table.ajax.reload();
|
||||
XiboDialogClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
templateOptions: {
|
||||
trans: {
|
||||
addFiles: "{% trans "Add files" %}",
|
||||
startUpload: "{% trans "Start upload" %}",
|
||||
cancelUpload: "{% trans "Cancel upload" %}",
|
||||
selectFolder: "{% trans "Select Folder" %}",
|
||||
selectFolderTitle: "{% trans "Change Current Folder location" %}",
|
||||
selectedFolder: "{% trans "Current Folder" %}:",
|
||||
selectedFolderTitle: "{% trans "Upload files to this Folder" %}",
|
||||
},
|
||||
upload: {
|
||||
maxSize: {{ libraryUpload.maxSize }},
|
||||
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
|
||||
validExt: "{{ validExt }}"
|
||||
},
|
||||
updateInAllChecked: {% if settings.LIBRARY_MEDIA_UPDATEINALL_CHECKB == 1 %}true{% else %}false{% endif %},
|
||||
deleteOldRevisionsChecked: {% if settings.LIBRARY_MEDIA_DELETEOLDVER_CHECKB == 1 %}true{% else %}false{% endif %},
|
||||
currentWorkingFolderId: currentWorkingFolderId,
|
||||
folderSelector: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Media Edit form
|
||||
*/
|
||||
function mediaEditFormOpen(dialog) {
|
||||
// ── OTS: Style the edit-media modal to match the upload modal ──
|
||||
// dialog IS the .modal element (returned by bootbox.dialog())
|
||||
dialog.addClass('ots-edit-media-modal');
|
||||
|
||||
// Also apply via the global enhancer in case the class wasn't added
|
||||
if (typeof window.otsEnhanceModal === 'function') {
|
||||
window.otsEnhanceModal(dialog);
|
||||
}
|
||||
|
||||
// Create a new button
|
||||
var footer = dialog.find(".modal-footer");
|
||||
var mediaId = dialog.find("#mediaEditForm").data().mediaId;
|
||||
var validExtensions = dialog.find("#mediaEditForm").data().validExtensions;
|
||||
var folderId = dialog.find("#mediaEditForm").data().folderId;
|
||||
|
||||
// Append
|
||||
var replaceButton = $('<button class="btn btn-warning">{% trans "Replace" %}</button>');
|
||||
replaceButton.click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Open the upload dialog with our options.
|
||||
openUploadForm({
|
||||
url: "{{ url_for("library.add") }}",
|
||||
title: "{% trans "Upload media" %}",
|
||||
buttons: {
|
||||
main: {
|
||||
label: "{% trans "Done" %}",
|
||||
className: "btn-primary btn-bb-main",
|
||||
callback: function () {
|
||||
table.ajax.reload();
|
||||
XiboDialogClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
templateOptions: {
|
||||
multi: false,
|
||||
oldMediaId: mediaId,
|
||||
oldFolderId: folderId,
|
||||
updateInAllChecked: {% if settings.LIBRARY_MEDIA_UPDATEINALL_CHECKB == 1 %}true{% else %}false{% endif %},
|
||||
deleteOldRevisionsChecked: {% if settings.LIBRARY_MEDIA_DELETEOLDVER_CHECKB == 1 %}true{% else %}false{% endif %},
|
||||
trans: {
|
||||
addFiles: "{% trans "Add Replacement" %}",
|
||||
startUpload: "{% trans "Start Replace" %}",
|
||||
cancelUpload: "{% trans "Cancel Replace" %}",
|
||||
updateInLayouts: {
|
||||
title: "{% trans "Update this media in all layouts it is assigned to?" %}",
|
||||
helpText: "{% trans "Note: It will only be updated in layouts you have permission to edit." %}"
|
||||
},
|
||||
deleteOldRevisions: {
|
||||
title: "{% trans "Delete the old version?" %}",
|
||||
helpText: "{% trans "Completely remove the old version of this media item if a new file is being uploaded." %}"
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
maxSize: {{ libraryUpload.maxSize }},
|
||||
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
|
||||
validExt: validExtensions,
|
||||
validExtensionsMessage: "{{ "Valid extensions are %s" }}".replace("%s", validExtensions).replace(/\|/g, ", ")
|
||||
}
|
||||
},
|
||||
uploadDoneEvent: function () {
|
||||
XiboDialogClose();
|
||||
table.ajax.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
footer.find(".btn-primary").before(replaceButton);
|
||||
}
|
||||
|
||||
///
|
||||
/// Library Usage Form
|
||||
///
|
||||
function usageFormOpen(dialog) {
|
||||
// Displays tab
|
||||
var usageTable = $("#usageReportTable").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
serverSide: true,
|
||||
stateSave: true, stateDuration: 0,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
responsive: true,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("library.usage", {id: ':id'}) }}".replace(":id", $("#usageReportTable").data().mediaId),
|
||||
"data": function(dataDisplay) {
|
||||
$.extend(dataDisplay, $(dialog).find("#usageReportForm").serializeObject());
|
||||
return dataDisplay;
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "displayId"},
|
||||
{ "data": "display" },
|
||||
{ "data": "description" }
|
||||
]
|
||||
});
|
||||
|
||||
usageTable.on('draw', dataTableDraw);
|
||||
usageTable.on('processing.dt', dataTableProcessing);
|
||||
|
||||
// Layouts tab
|
||||
var usageTableLayouts = $("#usageReportLayoutsTable").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
serverSide: true,
|
||||
stateSave: true, stateDuration: 0,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
responsive: true,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("library.usage.layouts", {id: ':id'}) }}".replace(":id", $("#usageReportLayoutsTable").data().mediaId)
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "layoutId"},
|
||||
{ "data": "layout" },
|
||||
{ "data": "description" },
|
||||
{
|
||||
"orderable": false,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
usageTableLayouts.on('draw', dataTableDraw);
|
||||
usageTableLayouts.on('processing.dt', dataTableProcessing);
|
||||
}
|
||||
|
||||
function setDefaultMultiSelectFormOpen(dialog) {
|
||||
{% set message = 'Force delete from any existing layouts, assignments, etc' %}
|
||||
{% set message2 = 'Notify each Display that has this Media in its local storage to remove it immediately?' %}
|
||||
|
||||
var $input = $(
|
||||
'<div class="form-group">' +
|
||||
'<input type=checkbox id="forceDelete" name="forceDelete"> {{ message|trans|e }} </input>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
var $input2 = $(
|
||||
'<div class="form-group">' +
|
||||
'<input type=checkbox id="purge" name="purge"> {{ message2|trans|e }} </input>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
$(dialog).find('.modal-body').append($input, $input2);
|
||||
|
||||
$('#forceDelete, #purge').on('change', function() {
|
||||
dialog.data().commitData = {
|
||||
forceDelete: $('#forceDelete').val(),
|
||||
purge: $('#purge').val()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setEnableStatMultiSelectFormOpen(dialog) {
|
||||
|
||||
var $select = $('<select id="enableStat" name="enableStat" class="form-control">' +
|
||||
'<option value="Off">{% trans %} Off {% endtrans %}</option>' +
|
||||
'<option value="On">{% trans %} On {% endtrans %}</option>' +
|
||||
'<option value="Inherit">{% trans %} Inherit {% endtrans %}</option>' +
|
||||
'</select>');
|
||||
|
||||
$select.on('change', function() {
|
||||
dialog.data().commitData = {enableStat: $(this).val()};
|
||||
}).trigger('change');
|
||||
|
||||
$(dialog).find('.modal-body').append($select);
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
120
ots-signs/views/login.twig
Normal file
120
ots-signs/views/login.twig
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ theme.getThemeConfig("theme_title") }}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="token" content="{{ csrfToken }}"/>
|
||||
<meta name="public-path" content="{{ theme.rootUri() }}"/>
|
||||
<link rel="shortcut icon" href="{{ theme.uri("img/favicon.ico") }}" />
|
||||
<!-- Import CSS bundle from dist -->
|
||||
<script src="{{ theme.rootUri() }}dist/style.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
|
||||
<!-- Minimal inline adjustments (layout only) -->
|
||||
<style type="text/css">
|
||||
html { font-size: 14px; }
|
||||
body { padding-top: 40px !important; padding-bottom: 40px !important; font-size: 1rem; }
|
||||
</style>
|
||||
<!-- Import user made CSS from theme -->
|
||||
<link href="{{ theme.uri("css/override.css") }}?{{ version }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ theme.uri("css/override-dark.css") }}?{{ version }}" rel="stylesheet" media="screen">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<!-- Fallback animated background element (inline styles ensure it appears even if external CSS is cached) -->
|
||||
<div class="ots-login-bg" aria-hidden="true"></div>
|
||||
<!-- Animated blurred color blobs -->
|
||||
<div class="ots-login-blob ots-login-blob--1" aria-hidden="true"></div>
|
||||
<div class="ots-login-blob ots-login-blob--2" aria-hidden="true"></div>
|
||||
<div class="ots-login-blob ots-login-blob--3" aria-hidden="true"></div>
|
||||
<style>
|
||||
.ots-login-bg{position:fixed;inset:0;z-index:0;pointer-events:none;filter:blur(20px);opacity:0.95;
|
||||
background: linear-gradient(120deg, rgba(14,28,45,0.6), rgba(6,16,30,0.55)),
|
||||
radial-gradient(circle at 10% 20%, rgba(79,140,255,0.06), transparent 10%),
|
||||
radial-gradient(circle at 85% 80%, rgba(255,138,0,0.04), transparent 12%);
|
||||
background-size:200% 200%,100% 100%,100% 100%;
|
||||
animation: ots-login-bg-shift-inline 14s linear infinite;}
|
||||
@keyframes ots-login-bg-shift-inline{0%{background-position:0% 50%,0 0,0 0}50%{background-position:100% 50%,0 0,0 0}100%{background-position:0% 50%,0 0,0 0}}
|
||||
/* Ensure login card sits above fallback background */
|
||||
.login-card{position:relative;z-index:2}
|
||||
</style>
|
||||
<div class="container">
|
||||
{% if authCASEnabled %}
|
||||
<form id="cas-login-form" class="login-card text-center" action="{{ url_for("cas.login") }}" method="post">
|
||||
{% for priorRoute in flash('priorRoute') %}
|
||||
<input name="priorRoute" type="hidden" value="{{ priorRoute }}" />
|
||||
{% endfor %}
|
||||
<p class="login-brand"><img alt="Logo" class="login-logo" src="{{ theme.uri("img/xibologo.png") }}"><span class="login-brand-text">OTS Signs</span></p>
|
||||
|
||||
<p>{% trans %}Connect with the Central Authentication Server{% endtrans %}</p>
|
||||
|
||||
{% for loginMessage in flash('cas_login_message') %}
|
||||
<div class="alert alert-danger">{{ loginMessage }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<p><button class="btn btn-signin" type="submit" name="logincas">{% trans %} CAS Login{% endtrans %}</button></p>
|
||||
</form>
|
||||
{% else %}
|
||||
<form id="login-form" class="login-card text-center" action="{{ url_for("login") }}" method="post">
|
||||
{% for priorRoute in flash('priorRoute') %}
|
||||
<input name="priorRoute" type="hidden" value="{{ priorRoute }}" />
|
||||
{% endfor %}
|
||||
<input type="hidden" name="{{ csrfKey }}" value="{{ csrfToken }}" />
|
||||
<p class="login-brand"><a href="{{ theme.getThemeConfig("theme_url") }}"><img class="login-logo" src="{{ theme.uri("img/xibologo.png") }}"></a><span class="login-brand-text">OTS Signs</span></p>
|
||||
|
||||
<p class="lead">{% trans "Please provide your credentials" %}</p>
|
||||
|
||||
<input id="username" class="form-control input-block-level" name="username" type="text" placeholder="{% trans "User" %}" autofocus autocomplete="username">
|
||||
<input id="password" class="form-control input-block-level" name="password" type="password" placeholder="{% trans "Password" %}" autocomplete="current-password">
|
||||
|
||||
{% for loginMessage in flash('login_message') %}
|
||||
<div class="alert alert-danger">{{ loginMessage }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<p><button class="btn btn-signin" type="submit">{% trans "Login" %}</button></p>
|
||||
|
||||
{% if passwordReminderEnabled %}<p><a href="#" id="reminder-form-toggle">{% trans "Forgotten your password?" %}</a></p>{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if passwordReminderEnabled %}
|
||||
<form id="reminder-form" class="login-card text-center d-none" action="{{ url_for("login.forgotten") }}" method="post">
|
||||
<input type="hidden" name="{{ csrfKey }}" value="{{ csrfToken }}" />
|
||||
<p class="login-brand"><a href="{{ theme.getThemeConfig("theme_url") }}"><img class="login-logo" src="{{ theme.uri("img/xibologo.png") }}"></a><span class="login-brand-text">OTS Signs</span></p>
|
||||
|
||||
<p>{% trans "Please provide your user name" %}</p>
|
||||
<input id="username" class="form-control input-block-level" name="username" type="text" placeholder="{% trans "User" %}">
|
||||
|
||||
{% for loginMessage in flash('login_message') %}
|
||||
<div class="alert alert-danger">{{ loginMessage }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<p><button class="btn btn-signin" type="submit">{% trans "Send Reset" %}</button></p>
|
||||
|
||||
<p><a href="#" id="login-form-toggle">{% trans "Login instead?" %}</a></p>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div> <!-- /container -->
|
||||
<!-- Import JS bundle from dist -->
|
||||
<script src="{{ theme.rootUri() }}dist/vendor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
$(function() {
|
||||
$("#reminder-form-toggle").on("click", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
$("#login-form").addClass("d-none");
|
||||
$("#reminder-form").removeClass("d-none");
|
||||
});
|
||||
|
||||
$("#login-form-toggle").on("click", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
$("#login-form").removeClass("d-none");
|
||||
$("#reminder-form").addClass("d-none");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
200
ots-signs/views/menuboard-page.twig
Normal file
200
ots-signs/views/menuboard-page.twig
Normal file
@@ -0,0 +1,200 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2022 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Menu Boards"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Menu Boards" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your menu boards and content." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-type="menuBoard" data-grid-name="menuBoardView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Menu Boards" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "ID" %}{% endset %}
|
||||
{{ inline.number("menuId", title) }}
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('name', title) }}
|
||||
|
||||
{% set title %}{% trans "Code" %}{% endset %}
|
||||
{{ inline.input('code', title) }}
|
||||
|
||||
{% set title %}{% trans "Owner" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("user.search") },
|
||||
{ name: "data-search-term", value: "userName" },
|
||||
{ name: "data-search-term-tags", value: "tags" },
|
||||
{ name: "data-id-property", value: "userId" },
|
||||
{ name: "data-text-property", value: "userName" },
|
||||
{ name: "data-initial-key", value: "userId" },
|
||||
] %}
|
||||
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
|
||||
{{ inline.hidden("folderId") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabled("menuBoard.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Menu Board" %}" href="{{ url_for("menuBoard.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="menuBoards" class="table table-striped responsive nowrap" data-content-type="menuBoard" data-content-id-name="menuId" data-state-preference-name="menuBoardGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Code" %}</th>
|
||||
<th>{% trans "Modified Date" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Permissions" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table;
|
||||
$(document).ready(function() {
|
||||
{% if not currentUser.featureEnabled("folder.view") %}
|
||||
disableFolders();
|
||||
{% endif %}
|
||||
|
||||
table = $("#menuBoards").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
"lengthMenu": [10, 25, 50, 100, 250, 500],
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
dataType: 'json',
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
url: "{{ url_for("menuBoard.search") }}",
|
||||
"data": function (d) {
|
||||
$.extend(d, $("#menuBoards").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "menuId", responsivePriority: 2},
|
||||
{
|
||||
"data": "name",
|
||||
responsivePriority: 2,
|
||||
"render": dataTableSpacingPreformatted
|
||||
},
|
||||
{
|
||||
"data": "description",
|
||||
responsivePriority: 2,
|
||||
"render": dataTableSpacingPreformatted
|
||||
},
|
||||
{
|
||||
"data": "code", responsivePriority: 3
|
||||
},
|
||||
{
|
||||
"name": "modifiedDt",
|
||||
"data": function (data) {
|
||||
return moment.unix(data.modifiedDt).format(jsDateFormat);
|
||||
}
|
||||
},
|
||||
{"data": "owner", responsivePriority: 4},
|
||||
{
|
||||
"data": "groupsWithPermissions",
|
||||
responsivePriority: 4,
|
||||
"render": dataTableCreatePermissions
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#menuBoards_wrapper').find('.col-md-6').eq(1));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
121
ots-signs/views/module-page.twig
Normal file
121
ots-signs/views/module-page.twig
Normal file
@@ -0,0 +1,121 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Module Page
|
||||
* Based on Xibo CMS module-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Modules"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Modules" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage installed modules." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Modules" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.input('name', title) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="modules" class="table table-striped" data-state-preference-name="moduleGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Library Media" %}</th>
|
||||
<th>{% trans "Default Duration" %}</th>
|
||||
<th>{% trans "Preview Enabled" %}</th>
|
||||
<th title="{% trans "Can this module be assigned to a Layout?" %}">{% trans "Assignable" %}</th>
|
||||
<th>{% trans "Enabled" %}</th>
|
||||
<th>{% trans "Errors" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table = $('#modules').DataTable({
|
||||
language: dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: false,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
order: [[ 0, 'asc']],
|
||||
ajax: {
|
||||
url: '{{ url_for("module.search") }}',
|
||||
data: function (d) {
|
||||
$.extend(d, $('#modules').closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ "data": "name" , responsivePriority: 2},
|
||||
{ "data": "description" },
|
||||
{ "data": "regionSpecific", "render": dataTableTickCrossInverseColumn },
|
||||
{ "data": "defaultDuration" },
|
||||
{ "data": "previewEnabled", "render": dataTableTickCrossColumn },
|
||||
{ "data": "assignable", "render": dataTableTickCrossColumn },
|
||||
{ "data": "enabled", "render": dataTableTickCrossColumn },
|
||||
{ "data": "errors", "render": dataTableTickCrossColumn },
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#modules_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
|
||||
function moduleEditFormOpen(dialog) {
|
||||
var moduleSettings = $(dialog).data('extra')['settings'];
|
||||
var $targetContainer = $(dialog).find('.form-module-configure-fields')
|
||||
|
||||
forms.createFields(moduleSettings, $targetContainer);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
7131
ots-signs/views/override-styles.twig
Normal file
7131
ots-signs/views/override-styles.twig
Normal file
File diff suppressed because it is too large
Load Diff
20
ots-signs/views/partials/_dashboard-card.twig
Normal file
20
ots-signs/views/partials/_dashboard-card.twig
Normal file
@@ -0,0 +1,20 @@
|
||||
{#
|
||||
Reusable dashboard card partial.
|
||||
Usage (embed to allow overriding the `body` block):
|
||||
{% embed 'custom/otssignange/views/partials/_dashboard-card.twig' with {'classes':'ots-displays-card'} %}
|
||||
{% block body %}
|
||||
... inner content ...
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
#}
|
||||
<div class="dashboard-card {{ classes|default('') }}" {% if id is defined %}id="{{ id }}"{% endif %}>
|
||||
{% if title is defined and title %}
|
||||
<div class="dashboard-card-header">
|
||||
{{ title|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="dashboard-card-body">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
197
ots-signs/views/playersoftware-page.twig
Normal file
197
ots-signs/views/playersoftware-page.twig
Normal file
@@ -0,0 +1,197 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Player Versions" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage player software versions and downloads." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="playerSoftwareView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Player Versions" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Type" %}{% endset %}
|
||||
{{ inline.dropdown("playerType", "single", title, "", [{"type": none, "typeShow": none}]|merge(types), "type", "typeShow") }}
|
||||
|
||||
{% set title %}{% trans "Version" %}{% endset %}
|
||||
{{ inline.dropdown("playerVersion", "single", title, "", [{"version": none, "version": none}]|merge(versions), "version", "version") }}
|
||||
|
||||
{% set title %}{% trans "Code" %}{% endset %}
|
||||
{{ inline.input("playerCode", title) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if currentUser.featureEnabled("playersoftware.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn" href="#" id="playerSoftwareUploadForm" title="{% trans "Upload a new Player Software file" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="playerSoftwareItems" class="table table-striped" data-state-preference-name="playerSoftwareGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Version ID" %}</th>
|
||||
<th>{% trans "Player Version Name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Version" %}</th>
|
||||
<th>{% trans "Code" %}</th>
|
||||
<th>{% trans "File Name" %}</th>
|
||||
<th>{% trans "Size" %}</th>
|
||||
<th>{% trans "Created At" %}</th>
|
||||
<th>{% trans "Modified At" %}</th>
|
||||
<th>{% trans "Modified By" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table;
|
||||
$(document).ready(function() {
|
||||
table = $("#playerSoftwareItems").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[2, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("playersoftware.search") }}",
|
||||
"data": function (d) {
|
||||
$.extend(d, $("#playerSoftwareItems").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "versionId", responsivePriority: 2},
|
||||
{"data": "playerShowVersion", responsivePriority: 2},
|
||||
{"data": "type", responsivePriority: 2},
|
||||
{"data": "version", responsivePriority: 2},
|
||||
{"data": "code", responsivePriority: 2},
|
||||
{"data": "fileName", responsivePriority: 4},
|
||||
{
|
||||
"name": "size",
|
||||
responsivePriority: 3,
|
||||
"data": null,
|
||||
"render": {"_": "size", "display": "fileSizeFormatted", "sort": "size"}
|
||||
},
|
||||
{"data": "createdAt", responsivePriority: 6, visible: false},
|
||||
{"data": "modifiedAt", responsivePriority: 6, visible: false},
|
||||
{"data": "modifiedBy", responsivePriority: 6, visible: false},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
],
|
||||
|
||||
createdRow: function (row, data, index) {
|
||||
if (data.version === "" || data.version === null || data.code === 0) {
|
||||
$(row).addClass('table-danger');
|
||||
$(row).attr('Title', "{{ "Please set Player Software Version"|trans }}");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#playerSoftwareItems_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
$("#playerSoftwareUploadForm").click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
openUploadForm({
|
||||
url: "{{ url_for("playersoftware.add") }}",
|
||||
title: "{% trans "Upload Version" %}",
|
||||
videoImageCovers: false,
|
||||
buttons: {
|
||||
main: {
|
||||
label: "{% trans "Done" %}",
|
||||
className: "btn-primary btn-bb-main",
|
||||
callback: function () {
|
||||
table.ajax.reload();
|
||||
XiboDialogClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
templateOptions: {
|
||||
includeTagsInput: false,
|
||||
multi: false,
|
||||
trans: {
|
||||
addFiles: "{% trans "Add files" %}",
|
||||
startUpload: "{% trans "Start upload" %}",
|
||||
cancelUpload: "{% trans "Cancel upload" %}",
|
||||
processing: "{% trans "Processing..." %}"
|
||||
},
|
||||
upload: {
|
||||
maxSize: {{ libraryUpload.maxSize }},
|
||||
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
|
||||
validExt: "{{ validExt }}"
|
||||
},
|
||||
updateInAllChecked: false,
|
||||
deleteOldRevisionsChecked: false,
|
||||
folderSelector: false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
551
ots-signs/views/playlist-page.twig
Normal file
551
ots-signs/views/playlist-page.twig
Normal file
@@ -0,0 +1,551 @@
|
||||
{#
|
||||
* Copyright (C) 2021 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Playlists"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Playlists" %}</h1>
|
||||
<p class="text-muted">{% trans "Create and manage content playlists." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="playlistView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Playlists" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item"><a class="nav-link active" href="#general-filter" role="tab" data-toggle="tab"><span>{% trans "General" %}</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#advanced-filter" role="tab" data-toggle="tab"><span>{% trans "Advanced" %}</span></a></li>
|
||||
</ul>
|
||||
<form class="form-inline">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="general-filter">
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('name', title) }}
|
||||
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}
|
||||
{% set title %}{% trans "Tags" %}{% endset %}
|
||||
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
|
||||
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
|
||||
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
|
||||
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
|
||||
{% endif %}
|
||||
|
||||
{% set attributes = [
|
||||
{ name: "data-live-search", value: "true" },
|
||||
{ name: "data-selected-text-format", value: "count > 4" }
|
||||
] %}
|
||||
|
||||
{% set title %}{% trans "Owner" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("user.search") },
|
||||
{ name: "data-search-term", value: "userName" },
|
||||
{ name: "data-search-term-tags", value: "tags" },
|
||||
{ name: "data-id-property", value: "userId" },
|
||||
{ name: "data-text-property", value: "userName" },
|
||||
{ name: "data-initial-key", value: "userId" },
|
||||
] %}
|
||||
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
|
||||
{% set title %}{% trans "Owner User Group" %}{% endset %}
|
||||
{% set helpText %}{% trans "Show items owned by users in the selected User Group." %}{% endset %}
|
||||
{% set attributes = [
|
||||
{ name: "data-width", value: "100%" },
|
||||
{ name: "data-allow-clear", value: "true" },
|
||||
{ name: "data-placeholder--id", value: null },
|
||||
{ name: "data-placeholder--value", value: "" },
|
||||
{ name: "data-search-url", value: url_for("group.search") },
|
||||
{ name: "data-search-term", value: "group" },
|
||||
{ name: "data-id-property", value: "groupId" },
|
||||
{ name: "data-text-property", value: "group" },
|
||||
{ name: "data-initial-key", value: "userGroupId" },
|
||||
] %}
|
||||
{{ inline.dropdown("ownerUserGroupId", "single", title, "", null, "groupId", "group", helpText, "pagedSelect", "", "", "", attributes) }}
|
||||
{{ inline.hidden("folderId") }}
|
||||
|
||||
{% set title %}{% trans "Layout ID" %}{% endset %}
|
||||
{{ inline.number("layoutId", title, layoutId) }}
|
||||
</div>
|
||||
<div class="tab-pane" id="advanced-filter">
|
||||
|
||||
{% set title %}{% trans "Show" %}{% endset %}
|
||||
{% set values = [{id: 1, value: "All"}, {id: 2, value: "Only Used"}, {id: 3, value: "Only Unused"}] %}
|
||||
{{ inline.dropdown("playlistStatusId", "single", title, 1, values, "id", "value") }}
|
||||
|
||||
{% if currentUser.featureEnabled("library.view") %}
|
||||
{% set title %}{% trans "Media" %}{% endset %}
|
||||
{{ inline.input("mediaLike", title) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabled("playlist.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add Playlist" %}" href="{{ url_for("playlist.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="playlists" class="table table-striped" data-content-type="playlist"
|
||||
data-content-id-name="playlistId" data-state-preference-name="playlistGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Duration" %}</th>
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
|
||||
<th>{% trans "Dynamic?" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Sharing" %}</th>
|
||||
<th>{% trans "Created" %}</th>
|
||||
<th>{% trans "Modified" %}</th>
|
||||
<th>{% trans "Stats?" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dummyLayout" style="display:none"></div>
|
||||
|
||||
<div id="editor-container"></div>
|
||||
|
||||
<div class="loading-overlay">
|
||||
<i class="fa fa-spinner fa-spin loading-icon"></i>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
{# Add common files #}
|
||||
{% include "editorTranslations.twig" %}
|
||||
{% include "editorVars.twig" %}
|
||||
|
||||
<script src="{{ theme.rootUri() }}dist/playlistEditor.bundle.min.js?v={{ version }}&rev={{ revision }}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/codeEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/wysiwygEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/editorCommon.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
|
||||
{# Custom translations #}
|
||||
{% autoescape "js" %}
|
||||
{# Insert custom translations here #}
|
||||
{% endautoescape %}
|
||||
|
||||
var table;
|
||||
$(document).ready(function () {
|
||||
|
||||
{% if not currentUser.featureEnabled("folder.view") %}
|
||||
disableFolders();
|
||||
{% endif %}
|
||||
|
||||
// Create ourselves a little hidden layout for preview sizing, etc
|
||||
$("#dummyLayout").html('<div id="layout" data-background-color="#000000" style="background-color: #000000" designer_scale="1"><div id="region_-1" zindex="1" tip_scale="1" designer_scale="1" width="800" height="450"></div></div>');
|
||||
|
||||
// Configure the DataTable
|
||||
table = $("#playlists").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
"lengthMenu": [10, 25, 50, 100, 250, 500],
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
url: "{{ url_for("playlist.search") }}",
|
||||
"data": function (d) {
|
||||
$.extend(d, $("#playlists").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "playlistId", responsivePriority: 2},
|
||||
{
|
||||
"data": "name",
|
||||
responsivePriority: 3,
|
||||
"render": dataTableSpacingPreformatted
|
||||
},
|
||||
{
|
||||
"data": "duration",
|
||||
responsivePriority: 3,
|
||||
"render": function (data, type, row) {
|
||||
if (type !== "display" && type !== "export")
|
||||
return data;
|
||||
|
||||
if (row.requiresDurationUpdate === 1) {
|
||||
return '<span class="fa fa-clock-o" title="{{ "Changes have been made and we are recalculating this Playlists duration" }}"></span>';
|
||||
} else if (row.requiresDurationUpdate !== 0) {
|
||||
return moment().startOf("day").seconds(data).format("H:mm:ss") + ' <span class="fa fa-clock-o" title="{{ "This duration will be updated at " }}' + moment(row.requiresDurationUpdate, "X").format(jsDateFormat) + '"></span>';
|
||||
}
|
||||
|
||||
return dataTableTimeFromSeconds(data, type, row);
|
||||
}
|
||||
},
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}{
|
||||
"sortable": false,
|
||||
"visible": false,
|
||||
responsivePriority: 4,
|
||||
"data": dataTableCreateTags
|
||||
},{% endif %}
|
||||
{"data": "isDynamic", "render": dataTableTickCrossColumn, responsivePriority: 4},
|
||||
{"data": "owner", responsivePriority: 4},
|
||||
{
|
||||
"data": "groupsWithPermissions",
|
||||
responsivePriority: 5,
|
||||
"render": dataTableCreatePermissions
|
||||
},
|
||||
{
|
||||
"data": "createdDt",
|
||||
responsivePriority: 6,
|
||||
"render": dataTableDateFromIso,
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"data": "modifiedDt",
|
||||
responsivePriority: 6,
|
||||
"render": dataTableDateFromIso,
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"name": "enableStat",
|
||||
responsivePriority: 6,
|
||||
"data": function (data) {
|
||||
|
||||
var icon = "";
|
||||
if (data.enableStat == 'On')
|
||||
icon = "fa-check";
|
||||
else if (data.enableStat == 'Off')
|
||||
icon = "fa-times";
|
||||
else
|
||||
icon = "fa-level-down";
|
||||
|
||||
return '<span class="fa ' + icon + '" title="' + (data.enableStatDescription) + '"></span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('draw', {form: $("#playlists").closest(".XiboGrid").find(".FilterDiv form")}, dataTableCreateTagEvents);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#playlists_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
// Playlist Add Form
|
||||
// contains a grid on the populate tab
|
||||
// hook up the grid
|
||||
var mediaTable;
|
||||
var nameFilter;
|
||||
var tagFilter;
|
||||
var exactTags;
|
||||
var logicalOperator;
|
||||
var logicalOperatorName;
|
||||
var filterFolderId;
|
||||
|
||||
function playlistEditorFormOpen(formData) {
|
||||
|
||||
// Clear container
|
||||
$('#editor-container').empty();
|
||||
|
||||
// Append form
|
||||
$('#editor-container').append(formData.message);
|
||||
}
|
||||
|
||||
function playlistFormOpen(dialog) {
|
||||
mediaTable = null;
|
||||
|
||||
$(dialog).find("input[name=filterMediaName]").on("keyup", _.debounce(function () {
|
||||
playlistFormPopulateMediaTable(dialog);
|
||||
}, 500));
|
||||
|
||||
$(dialog).find("input[name=filterMediaTag], input[name=exactTags], select[name=logicalOperator], select[name=logicalOperatorName], select[name=filterFolderId]").on("change", function () {
|
||||
playlistFormPopulateMediaTable(dialog);
|
||||
});
|
||||
|
||||
// First time in there
|
||||
playlistFormPopulateMediaTable(dialog);
|
||||
|
||||
// Run function to set the form submit behaviour
|
||||
playlistAddFormOpen();
|
||||
}
|
||||
|
||||
///
|
||||
/// Playlist Usage Form
|
||||
///
|
||||
function usageFormOpen(dialog) {
|
||||
// Displays tab
|
||||
var usageTable = $("#usageReportTable").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
serverSide: true,
|
||||
stateSave: true, stateDuration: 0,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
responsive: true,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("playlist.usage", {id:':id'}) }}".replace(":id", $("#usageReportTable").data().playlistId),
|
||||
"data": function (dataDisplay) {
|
||||
$.extend(dataDisplay, $(dialog).find("#usageReportForm").serializeObject());
|
||||
return dataDisplay;
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "displayId"},
|
||||
{"data": "display"},
|
||||
{"data": "description"}
|
||||
]
|
||||
});
|
||||
|
||||
usageTable.on('draw', dataTableDraw);
|
||||
usageTable.on('processing.dt', dataTableProcessing);
|
||||
|
||||
// Layouts tab
|
||||
var usageTableLayouts = $("#usageReportLayoutsTable").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
serverSide: true,
|
||||
stateSave: true, stateDuration: 0,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
responsive: true,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("playlist.usage.layouts", {id:':id'}) }}".replace(":id", $("#usageReportLayoutsTable").data().playlistId)
|
||||
},
|
||||
"columns": [
|
||||
{"data": "layoutId"},
|
||||
{"data": "layout"},
|
||||
{"data": "description"},
|
||||
{
|
||||
"orderable": false,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
usageTableLayouts.on('draw', dataTableDraw);
|
||||
usageTableLayouts.on('processing.dt', dataTableProcessing);
|
||||
}
|
||||
|
||||
function playlistFormPopulateMediaTable(dialog) {
|
||||
nameFilter = $(dialog).find("input[name=filterMediaName]").val();
|
||||
tagFilter = $(dialog).find("input[name=filterMediaTag]").val();
|
||||
exactTags = $(dialog).find("input[name=exactTags]").is(':checked')
|
||||
logicalOperator = $(dialog).find("select[name=logicalOperator]").val();
|
||||
logicalOperatorName = $(dialog).find("select[name=logicalOperatorName]").val();
|
||||
filterFolderId = $(dialog).find("select[name=filterFolderId]").val() ?? "";
|
||||
|
||||
if (nameFilter === "" && tagFilter === "" && filterFolderId === "") {
|
||||
if (mediaTable != null) {
|
||||
mediaTable.destroy();
|
||||
mediaTable = null;
|
||||
$("#playlistLibraryMedia tbody").empty();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (mediaTable != null) {
|
||||
mediaTable.ajax.reload();
|
||||
} else {
|
||||
mediaTable = $("#playlistLibraryMedia").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
filter: false,
|
||||
responsive: true,
|
||||
searchDelay: 3000,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("library.search") }}",
|
||||
"data": function (d) {
|
||||
$.extend(
|
||||
d,
|
||||
{
|
||||
media: nameFilter,
|
||||
tags: tagFilter,
|
||||
folderId: filterFolderId,
|
||||
assignable: 1,
|
||||
exactTags: exactTags,
|
||||
logicalOperator: logicalOperator,
|
||||
logicalOperatorName: logicalOperatorName
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "mediaId"},
|
||||
{"data": "name"},
|
||||
{"data": "mediaType"},
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}{"data": dataTableCreateTags},{% endif %}
|
||||
{
|
||||
"name": "duration",
|
||||
"data": function (data, type) {
|
||||
if (type !== "display")
|
||||
return data.duration;
|
||||
|
||||
return moment().startOf("day").seconds(data.duration).format("H:mm:ss");
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
mediaTable.on('processing.dt', dataTableProcessing);
|
||||
mediaTable.on('draw', {form: $(".playlistForm")}, dataTableCreateTagEvents);
|
||||
}
|
||||
}
|
||||
|
||||
function setEnableStatMultiSelectFormOpen(dialog) {
|
||||
|
||||
var $select = $('<select id="enableStat" name="enableStat" class="form-control">' +
|
||||
'<option value="Off">{% trans %} Off {% endtrans %}</option>' +
|
||||
'<option value="On">{% trans %} On {% endtrans %}</option>' +
|
||||
'<option value="Inherit">{% trans %} Inherit {% endtrans %}</option>' +
|
||||
'</select>');
|
||||
|
||||
$select.on('change', function () {
|
||||
dialog.data().commitData = {enableStat: $(this).val()};
|
||||
}).trigger('change');
|
||||
|
||||
$(dialog).find('.modal-body').append($select);
|
||||
}
|
||||
|
||||
function playlistAddFormOpen() {
|
||||
$("#playlistAddForm").off("submit").submit(function (e) {
|
||||
e.preventDefault();
|
||||
var form = $(this);
|
||||
|
||||
$.ajax({
|
||||
type: $(this).attr("method"),
|
||||
url: $(this).attr("action"),
|
||||
data: $(this).serialize(),
|
||||
cache: false,
|
||||
dataType: "json",
|
||||
success: function (xhr, textStatus, error) {
|
||||
|
||||
XiboSubmitResponse(xhr, form);
|
||||
|
||||
if (xhr.success && xhr.data.isDynamic == 0) {
|
||||
|
||||
// Open the editor
|
||||
openPlaylistEditorForm(xhr.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openPlaylistEditorForm(playlistId) {
|
||||
var requestPath = playlistEditorUrl;
|
||||
|
||||
// replace id if necessary/exists
|
||||
requestPath = requestPath.replace(':id', playlistId);
|
||||
|
||||
$.ajax({
|
||||
url: requestPath,
|
||||
type: 'GET'
|
||||
}).done(function (res) {
|
||||
|
||||
if (!res.success) {
|
||||
// Login Form needed?
|
||||
if (res.login) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Just an error we dont know about
|
||||
if (res.message == undefined) {
|
||||
console.error(res);
|
||||
} else {
|
||||
console.error(res.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Clear container
|
||||
$('#editor-container').empty();
|
||||
|
||||
// Append form
|
||||
$('#editor-container').append(res.html);
|
||||
}
|
||||
}).fail(function (jqXHR, textStatus, errorThrown) {
|
||||
// Output error to console
|
||||
console.error(jqXHR, textStatus, errorThrown);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
132
ots-signs/views/resolution-page.twig
Normal file
132
ots-signs/views/resolution-page.twig
Normal file
@@ -0,0 +1,132 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Resolutions"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Resolutions" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage display resolutions." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="resolutionView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Resolutions" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Enabled" %}{% endset %}
|
||||
{% set option1 %}{% trans "Yes" %}{% endset %}
|
||||
{% set option2 %}{% trans "No" %}{% endset %}
|
||||
{% set values = [{id: 1, value: option1}, {id: 0, value: option2}] %}
|
||||
{{ inline.dropdown("enabled", "single", title, 1, values, "id", "value") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if currentUser.featureEnabled("resolution.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new resolution for use on layouts" %}" href="{{ url_for("resolution.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="resolutions" class="table table-striped" data-state-preference-name="resolutionGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Resolution" %}</th>
|
||||
<th>{% trans "Width" %}</th>
|
||||
<th>{% trans "Height" %}</th>
|
||||
<th>{% trans "Enabled?" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
$(document).ready(function() {
|
||||
var table = $("#resolutions").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
responsive: true,
|
||||
stateDuration: 0,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
url: "{{ url_for("resolution.search") }}",
|
||||
data: function (d) {
|
||||
$.extend(d, $("#resolutions").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "resolutionId", responsivePriority: 2},
|
||||
{"data": "resolution"},
|
||||
{"data": "width"},
|
||||
{"data": "height"},
|
||||
{"data": "enabled"},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#resolutions_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
369
ots-signs/views/schedule-page.twig
Normal file
369
ots-signs/views/schedule-page.twig
Normal file
@@ -0,0 +1,369 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
{% import "forms.twig" as forms %}
|
||||
|
||||
{% block title %}{{ "Schedule"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Schedule" %}</h1>
|
||||
<p class="text-muted">{% trans "Schedule content to your displays." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="scheduleGridView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Schedule" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="schedule-filter">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item"><a class="nav-link active" href="#general-filter" role="tab" data-toggle="tab" aria-selected="true"><span>{% trans "General" %}</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#advanced-filter" role="tab" data-toggle="tab" aria-selected="false"><span>{% trans "Advanced" %}</span></a></li>
|
||||
</ul>
|
||||
<form class="form-inline">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="general-filter" role="tabpanel">
|
||||
{% set title %}{% trans "Range" %}{% endset %}
|
||||
{% set range %}{% trans "Custom" %}{% endset %}
|
||||
{% set day %}{% trans "Day" %}{% endset %}
|
||||
{% set week %}{% trans "Week" %}{% endset %}
|
||||
{% set month %}{% trans "Month" %}{% endset %}
|
||||
{% set year %}{% trans "Year" %}{% endset %}
|
||||
{% set options = [
|
||||
{ name: "custom", range: range },
|
||||
{ name: "day", range: day },
|
||||
{ name: "week", range: week },
|
||||
{ name: "month", range: month },
|
||||
{ name: "year", range: year },
|
||||
] %}
|
||||
{{ inline.dropdown("range", "single", title, "month", options, "name", "range", "", "date-range-input") }}
|
||||
|
||||
{% set title %}{% trans 'From Date' %}{% endset %}
|
||||
{{ inline.dateTime("fromDt", title, "", "", "custom-date-range d-none", "", "") }}
|
||||
|
||||
{% set title %}{% trans 'To Date' %}{% endset %}
|
||||
{{ inline.dateTime("toDt", title, "", "", "custom-date-range d-none", "", "") }}
|
||||
|
||||
{% set title %}{% trans "Date Controls" %}{% endset %}
|
||||
<div class="form-group mr-1 mb-1 controls-date-range">
|
||||
<div class="control-label mr-1" title=""
|
||||
accesskey="">{{ title }}</div>
|
||||
<div class="controls-date-inputs">
|
||||
<div class="inputgroup date" id="dateInput">
|
||||
<span class="btn btn-outline-primary date-open-button" role="button">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control" id="dateInputLink" data-input style="display:none;"/>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary" data-calendar-nav="prev"><span class="fa fa-caret-left"></span> {% trans "Prev" %}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-calendar-nav="today">{% trans "Today" %}</button>
|
||||
<button type="button" class="btn btn-secondary" data-calendar-nav="next">{% trans "Next" %} <span class="fa fa-caret-right"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('name', title) }}
|
||||
|
||||
{% set title %}{% trans 'Event Type' %}{% endset %}
|
||||
{{ inline.dropdown("eventTypeId", "single", title, "", [{eventTypeId: null, eventTypeName: "All"}]|merge(eventTypes), "eventTypeId", "eventTypeName") }}
|
||||
|
||||
{% set title %}{% trans "Layout / Campaign" %}{% endset %}
|
||||
{% set helpText %}{% trans "Please select a Layout or Campaign for this Event to show" %}{% endset %}
|
||||
|
||||
<div class="form-group mr-1 mb-1">
|
||||
<label class="control-label mr-1" for="campaignId" title=""
|
||||
accesskey="">{{ title }}</label>
|
||||
<select name="campaignId" id="campaignIdFilter" class="form-control"
|
||||
data-search-url="{{ url_for("campaign.search") }}"
|
||||
data-trans-campaigns="{% trans "Campaigns" %}"
|
||||
data-trans-layouts="{% trans "Layouts" %}"
|
||||
data-allow-clear="true"
|
||||
data-width="100%"
|
||||
title="{% trans "Layout / Campaign" %}"
|
||||
data-placeholder="{% trans "Layout / Campaign" %}"
|
||||
data-dropdownAutoWidth
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% set title %}{% trans "Displays" %}{% endset %}
|
||||
<div class="form-group mr-1 mb-1 pagedSelect">
|
||||
<label class="control-label mr-1" for="DisplayList" title=""
|
||||
accesskey="">{{ title }}</label>
|
||||
<select id="DisplayList" class="form-control" name="displaySpecificGroupIds[]"
|
||||
data-width="100%"
|
||||
data-placeholder="{% trans "Displays" %}"
|
||||
data-search-url="{{ url_for("display.search") }}"
|
||||
data-search-term="display"
|
||||
data-id-property="displayGroupId"
|
||||
data-text-property="display"
|
||||
data-additional-property="displayGroupId"
|
||||
data-allow-clear="true"
|
||||
data-initial-key="displayGroupIds[]"
|
||||
multiple>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% set title %}{% trans "Display Groups" %}{% endset %}
|
||||
<div class="form-group mr-2 mb-1 pagedSelect">
|
||||
<label class="control-label mr-1" for="DisplayGroupList" title=""
|
||||
accesskey="">{{ title }}</label>
|
||||
<select id="DisplayGroupList" class="form-control" name="displayGroupIds[]"
|
||||
data-width="100%"
|
||||
data-placeholder="{% trans "Display Groups" %}"
|
||||
data-search-url="{{ url_for("displayGroup.search") }}"
|
||||
data-search-term="displayGroup"
|
||||
data-id-property="displayGroupId"
|
||||
data-text-property="displayGroup"
|
||||
data-allow-clear="true"
|
||||
data-initial-key="displayGroupIds[]"
|
||||
multiple>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="advanced-filter" role="tabpanel">
|
||||
{% set label %}{% trans "Direct Schedule?" %}{% endset %}
|
||||
{% set title %}{% trans "Show only events scheduled directly on selected Displays/Groups" %}{% endset %}
|
||||
<div class="form-group ml-2 mr-3 mb-1">
|
||||
<div class="form-check">
|
||||
<input title="{{ title }}" class="form-check-input" type="checkbox" id="directSchedule" name="directSchedule">
|
||||
<label class="form-check-label" title="{{ title }}" for="directSchedule" accesskey="">{{ label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set title %}{% trans "Only show schedules which appear on all filtered displays/groups?" %}{% endset %}
|
||||
{% set label %}{% trans "Shared Schedule?" %}{% endset %}
|
||||
<div class="form-group ml-2 mr-3 mb-1">
|
||||
<div class="form-check">
|
||||
<input title="{{ title }}" class="form-check-input" type="checkbox" id="sharedSchedule" name="sharedSchedule">
|
||||
<label class="form-check-label" title="{{ title }}" for="sharedSchedule" accesskey="">{{ label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set title %}{% trans 'Geo Aware?' %}{% endset %}
|
||||
{% set options = [
|
||||
{ id: null, name: "Both"|trans },
|
||||
{ id: 0, name: "No"|trans },
|
||||
{ id: 1, name: "Yes"|trans }
|
||||
] %}
|
||||
{{ inline.dropdown("geoAware", "single", title, "both", options, "id", "name") }}
|
||||
|
||||
{% set title %}{% trans 'Recurring?' %}{% endset %}
|
||||
{% set options = [
|
||||
{ id: null, name: "Both" },
|
||||
{ id: 0, name: "No"|trans },
|
||||
{ id: 1, name: "Yes"|trans }
|
||||
] %}
|
||||
{{ inline.dropdown("recurring", "single", title, "both", options, "id", "name") }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="XiboSchedule card content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if currentUser.featureEnabled("schedule.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Scheduled event" %}" href="{{ url_for("schedule.add.form") }}"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="schedule-nav grid-nav nav-link active" id="grid-tab" href="#grid-view"
|
||||
data-schedule-view="grid"
|
||||
role="tab"
|
||||
data-toggle="tab"><span>{% trans "Grid" %}</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="schedule-nav calendar-nav nav-link" id="calendar-tab" href="#calendar-view"
|
||||
data-schedule-view="calendar"
|
||||
data-calendar-view="month"
|
||||
role="tab"
|
||||
data-toggle="tab"><span>{% trans "Calendar" %}</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="xibo-calendar-header-container col-xl-12">
|
||||
<div class="ots-calendar-nav">
|
||||
<button type="button" class="ots-cal-arrow ots-cal-arrow-prev" id="ots-cal-prev" title="{% trans 'Previous' %}">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
<div class="xibo-calendar-header text-center">
|
||||
<h1 class="page-header"></h1>
|
||||
<div class="calendar-loading">
|
||||
<span id="calendar-progress-table" class="fa fa-spin fa-cog"></span>
|
||||
<span id="calendar-progress" class="fa fa-spin fa-cog"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="ots-cal-arrow ots-cal-arrow-next" id="ots-cal-next" title="{% trans 'Next' %}">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="grid-view">
|
||||
<div class="XiboData pt-3">
|
||||
<table id="schedule-grid" class="table table-striped w-100"
|
||||
data-state-preference-name="scheduleGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'ID' %}</th>
|
||||
<th></th>
|
||||
<th>{% trans 'Event Type' %}</th>
|
||||
<th>{% trans 'Name' %}</th>
|
||||
<th>{% trans 'Start' %}</th>
|
||||
<th>{% trans 'End' %}</th>
|
||||
<th>{% trans 'Event' %}</th>
|
||||
<th>{% trans 'Campaign ID' %}</th>
|
||||
<th>{% trans 'Display Groups' %}</th>
|
||||
<th>{% trans 'SoV' %}</th>
|
||||
<th>{% trans 'Max Plays per Hour' %}</th>
|
||||
<th>{% trans 'Geo Aware?' %}</th>
|
||||
<th>{% trans 'Recurring?' %}</th>
|
||||
<th>{% trans 'Recurrence Description' %}</th>
|
||||
<th>{% trans 'Recurrence Type' %}</th>
|
||||
<th>{% trans 'Recurrence Interval' %}</th>
|
||||
<th>{% trans 'Recurrence Repeats On' %}</th>
|
||||
<th>{% trans 'Recurrence End' %}</th>
|
||||
<th>{% trans 'Priority?' %}</th>
|
||||
<th>{% trans 'Criteria?' %}</th>
|
||||
<th>{% trans 'Created On' %}</th>
|
||||
<th>{% trans 'Updated On' %}</th>
|
||||
<th>{% trans 'Modified By' %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="calendar-view">
|
||||
<div class="row">
|
||||
<div id="CalendarContainer"
|
||||
data-agenda-link="{{ url_for("schedule.events", {id: ':id'}) }}"
|
||||
data-calendar-type="{{ settings.CALENDAR_TYPE }}" class="col-sm-12"
|
||||
data-default-lat="{{ defaultLat }}"
|
||||
data-default-long="{{ defaultLong }}">
|
||||
<div class="calendar-view" id="Calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="cal-legend">
|
||||
<ul>
|
||||
<li class="event-always"><span
|
||||
class="fa fa-retweet"></span> {% trans "Always showing" %}</li>
|
||||
<li class="event-info"><span
|
||||
class="fa fa-desktop"></span> {% trans "Single Display" %}</li>
|
||||
<li class="event-success"><span
|
||||
class="fa fa-desktop"></span> {% trans "Multi Display" %}</li>
|
||||
<li class="event-important"><span
|
||||
class="fa fa-bullseye"></span> {% trans "Priority" %}</li>
|
||||
<li class="event-special"><span
|
||||
class="fa fa-repeat"></span> {% trans "Recurring" %}</li>
|
||||
<li class="event-inverse"><span
|
||||
class="fa fa-lock"></span> {% trans "View Only" %}</li>
|
||||
<li class="event-command"><span
|
||||
class="fa fa-wrench"></span> {% trans "Command" %}</li>
|
||||
<li class="event-interrupt"><span
|
||||
class="fa fa-hand-paper"></span> {% trans "Interrupt" %}</li>
|
||||
<li class="event-geo-location"><span
|
||||
class="fa fa-map-marker"></span> {% trans "Geo Location" %}</li>
|
||||
<li class="event-action"><span
|
||||
class="fa fa-paper-plane "></span> {% trans "Interactive Action" %}
|
||||
</li>
|
||||
<li class="event-sync"><span
|
||||
class="fa fa-refresh"></span> {% trans "Synchronised" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
{# Initialise JS variables #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
{# JS variables #}
|
||||
var scheduleRecurrenceDeleteUrl = "{{ url_for("schedule.recurrence.delete.form", {id:':id'}) }}";
|
||||
var layoutPreviewUrl = "{{ theme.rootUri() }}preview/layout/preview/:id";
|
||||
var scheduleSearchUrl = "{{ url_for("schedule.search") }}";
|
||||
var userAgendaViewEnabled = "{{ currentUser.featureEnabled('schedule.agenda') }}";
|
||||
|
||||
{# Custom translations #}
|
||||
var schedulePageTrans = {
|
||||
always: "{% trans "Always" %}",
|
||||
adjustTimesofTimer: "{% trans "Adjust the times of this timer. To add or remove a day, use the Display Profile." %}",
|
||||
daysOfTheWeek: {
|
||||
monday: "{% trans "Monday" %}",
|
||||
tuesday: "{% trans "Tuesday" %}",
|
||||
wednesday: "{% trans "Wednesday" %}",
|
||||
thursday: "{% trans "Thursday" %}",
|
||||
friday: "{% trans "Friday" %}",
|
||||
saturday: "{% trans "Saturday" %}",
|
||||
sunday: "{% trans "Sunday" %}",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
{# Add page source code bundle ( JS ) #}
|
||||
<script src="{{ theme.rootUri() }}dist/leaflet.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/pages/schedule-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script nonce="{{ cspNonce }}">
|
||||
$(function() {
|
||||
$('#ots-cal-prev').on('click', function() {
|
||||
$('button[data-calendar-nav="prev"]').trigger('click');
|
||||
});
|
||||
$('#ots-cal-next').on('click', function() {
|
||||
$('button[data-calendar-nav="next"]').trigger('click');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
1253
ots-signs/views/settings-page.twig
Normal file
1253
ots-signs/views/settings-page.twig
Normal file
File diff suppressed because it is too large
Load Diff
187
ots-signs/views/syncgroup-page.twig
Normal file
187
ots-signs/views/syncgroup-page.twig
Normal file
@@ -0,0 +1,187 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Sync Groups"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Sync Groups" %}</h1>
|
||||
<p class="text-muted">{% trans "Create and manage synchronized Display groups." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="syncGroupGridView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Sync Groups" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "ID" %}{% endset %}
|
||||
{{ inline.input("syncGroupId", title) }}
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('name', title) }}
|
||||
|
||||
{% set title %}{% trans "Lead Display ID" %}{% endset %}
|
||||
{{ inline.input("leadDisplayId", title) }}
|
||||
|
||||
{{ inline.hidden("folderId") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabled("display.syncAdd") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Sync Group" %}" href="{{ url_for("syncgroup.form.add") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="syncgroups" class="table table-striped" data-content-type="syncGroup" data-content-id-name="syncGroupId" data-state-preference-name="syncGroupGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Created Date" %}</th>
|
||||
<th>{% trans "Modified Date" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Modified By" %}</th>
|
||||
<th>{% trans "Publisher Port" %}</th>
|
||||
<th>{% trans "Switch Delay" %}</th>
|
||||
<th>{% trans "Video Pause Delay" %}</th>
|
||||
<th>{% trans "Lead Display" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
let syncGroupTable;
|
||||
|
||||
$(document).ready(function() {
|
||||
syncGroupTable = $("#syncgroups").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
"filter": false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("syncgroup.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#syncgroups").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "syncGroupId", responsivePriority: 2 },
|
||||
{ "data": "name", responsivePriority: 1 },
|
||||
{ "data": "createdDt", responsivePriority: 2 },
|
||||
{ "data": "modifiedDt", responsivePriority: 2 },
|
||||
{ "data": "owner", responsivePriority: 3 },
|
||||
{ "data": "modifiedByName", responsivePriority: 4 },
|
||||
{ "data": "syncPublisherPort", responsivePriority: 3 },
|
||||
{ "data": "syncSwitchDelay", responsivePriority: 3 },
|
||||
{ "data": "syncVideoPauseDelay", responsivePriority: 3 },
|
||||
{ "data": "leadDisplay", responsivePriority: 3 },
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
syncGroupTable.on('draw', dataTableDraw);
|
||||
syncGroupTable.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(syncGroupTable, $('#syncgroups_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
syncGroupTable.ajax.reload();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScriptTemplates %}
|
||||
{{ parent() }}
|
||||
|
||||
{% verbatim %}
|
||||
<script type="text/x-handlebars-template" id="template-display-group-multi-delete-checkbox">
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-2 col-sm-10 mt-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="checkbox-confirmDelete" name="confirmDelete">
|
||||
<label class="form-check-label" for="checkbox-confirmDelete">
|
||||
{% endverbatim %}{{ "Are you sure you want to delete?"|trans }}{% verbatim %}
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">{% endverbatim %}{{ "Check to confirm deletion of the selected records."|trans }}{% verbatim %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
{% endverbatim %}
|
||||
{% endblock %}
|
||||
171
ots-signs/views/tag-page.twig
Normal file
171
ots-signs/views/tag-page.twig
Normal file
@@ -0,0 +1,171 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Tag Page
|
||||
* Based on Xibo CMS tag-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Tags"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Tags" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage content tags." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="tagView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Tags" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "ID" %}{% endset %}
|
||||
{{ inline.number("tagId", title) }}
|
||||
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('tag', title) }}
|
||||
|
||||
{% set title %}{% trans "Show System tags?" %}{% endset %}
|
||||
{{ inline.checkbox("isSystem", title, 0) }}
|
||||
|
||||
{% set title %}{% trans "Show only tags with values?" %}{% endset %}
|
||||
{{ inline.checkbox("haveOptions", title, 0) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Tag" %}" href="{{ url_for("tag.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="tags" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "isRequired" %}</th>
|
||||
<th>{% trans "Values" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table = $("#tags").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 1, "desc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("tag.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#tags").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "tagId", responsivePriority: 2 },
|
||||
{ "data": "tag", responsivePriority: 2 },
|
||||
{
|
||||
"data": "isRequired",
|
||||
responsivePriority: 3,
|
||||
"render": function (data, type, row) {
|
||||
if (type != "display") {
|
||||
return data;
|
||||
}
|
||||
|
||||
var icon = "";
|
||||
if (data == 1)
|
||||
icon = "fa-check";
|
||||
else if (data == 0)
|
||||
icon = "fa-times";
|
||||
|
||||
return "<span class='fa " + icon + "'></span>";
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": "options",
|
||||
responsivePriority: 3,
|
||||
"render": function (data, type, row) {
|
||||
if (type != "display") {
|
||||
return data;
|
||||
}
|
||||
|
||||
return JSON.parse(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#tags_wrapper').find('.dataTables_buttons'), false);
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
|
||||
function usageFormOpen(dialog) {
|
||||
const $tagUsageTable = $("#tagUsageTable");
|
||||
var usageTable = $tagUsageTable.DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true, stateDuration: 0,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
responsive: true,
|
||||
"order": [[1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("tag.usage", {id: ':id'}) }}".replace(":id", $tagUsageTable.data().tagId),
|
||||
"data": function(data) {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "entityId"},
|
||||
{ "data": "type"},
|
||||
{ "data": "name" },
|
||||
{ "data": "value" }
|
||||
]
|
||||
});
|
||||
|
||||
usageTable.on('draw', dataTableDraw);
|
||||
usageTable.on('processing.dt', dataTableProcessing);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
177
ots-signs/views/task-page.twig
Normal file
177
ots-signs/views/task-page.twig
Normal file
@@ -0,0 +1,177 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Task Page
|
||||
* Based on Xibo CMS task-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Tasks"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Tasks" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage scheduled system tasks." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card" style="display:none;">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if settings.TASK_CONFIG_LOCKED_CHECKB == 0 or settings.TASK_CONFIG_LOCKED_CHECKB == "Unchecked" %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" href="{{ url_for("task.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="tasks" class="table table-striped" data-state-preference-name="taskGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Active" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Next Run" %}</th>
|
||||
<th>{% trans "Run Now" %}</th>
|
||||
<th>{% trans "Last Run" %}</th>
|
||||
<th>{% trans "Last Status" %}</th>
|
||||
<th>{% trans "Last Duration" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table = $("#tasks").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
responsive: true,
|
||||
stateDuration: 0,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("task.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#tasks").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "taskId" , responsivePriority: 2},
|
||||
{ "data": "name" , responsivePriority: 2},
|
||||
{
|
||||
"data": "isActive",
|
||||
responsivePriority: 2,
|
||||
"render": dataTableTickCrossColumn
|
||||
},
|
||||
{
|
||||
"data": "status",
|
||||
"render": function (data, type, row) {
|
||||
if (type !== "display")
|
||||
return data;
|
||||
|
||||
var icon = "";
|
||||
var title = "";
|
||||
if (data === 1) {
|
||||
if (moment(row.lastRunStartDt, "X").tz) {
|
||||
title = "PID: " + row.pid + " (" + moment(row.lastRunStartDt, "X").tz(timezone).format(jsDateFormat) + ")";
|
||||
} else {
|
||||
title = "PID: " + row.pid + " (" + moment(row.lastRunStartDt, "X").format(jsDateFormat) + ")";
|
||||
}
|
||||
icon = "fa-cogs";
|
||||
}
|
||||
else if (data === 3) {
|
||||
title = "Exit: " + row.lastRunExitCode;
|
||||
icon = "fa-bug";
|
||||
}
|
||||
else if (data === 5) {
|
||||
title = "Time out";
|
||||
icon = "fa-hourglass-o";
|
||||
}
|
||||
else {
|
||||
title = "";
|
||||
icon = "fa-clock-o";
|
||||
}
|
||||
|
||||
return '<span class="fa ' + icon + '" title="' + title + '"></span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": "nextRunDt",
|
||||
"orderable": false,
|
||||
"render": dataTableDateFromUnix
|
||||
},
|
||||
{
|
||||
"data": "runNow",
|
||||
"render": dataTableTickCrossColumn
|
||||
},
|
||||
{
|
||||
"data": "lastRunDt",
|
||||
"render": dataTableDateFromUnix
|
||||
},
|
||||
{
|
||||
"data": "lastRunStatus",
|
||||
"render": function (data, type, row) {
|
||||
if (type !== "display")
|
||||
return data;
|
||||
|
||||
var icon = "";
|
||||
if (data === 4)
|
||||
icon = "fa-check";
|
||||
else
|
||||
icon = "fa-times";
|
||||
|
||||
return '<span class="fa ' + icon + '" title="' +
|
||||
((row.lastRunMessage === null) ? "" : row.lastRunMessage) + '"></span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": "lastRunDuration",
|
||||
"render": function (data, type, row) {
|
||||
if (type !== "display")
|
||||
return data;
|
||||
|
||||
return (data === null) ? 0 : moment().startOf("day").seconds(data).format("H:mm:ss");
|
||||
}
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#tasks_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
290
ots-signs/views/template-page.twig
Normal file
290
ots-signs/views/template-page.twig
Normal file
@@ -0,0 +1,290 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2022 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Templates"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Templates" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage your reusable templates." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="templateView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Templates" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content collapsed" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('template', title) }}
|
||||
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}
|
||||
{% set title %}{% trans "Tags" %}{% endset %}
|
||||
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
|
||||
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
|
||||
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
|
||||
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
|
||||
{% endif %}
|
||||
|
||||
{{ inline.hidden("folderId") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-with-folders-container ots-grid-with-folders">
|
||||
<div class="grid-folder-tree-container p-3 content-card ots-folder-tree" id="grid-folder-filter">
|
||||
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
|
||||
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
|
||||
</div>
|
||||
<div class="folder-search-no-results d-none">
|
||||
<p>{% trans 'No Folders matching the search term' %}</p>
|
||||
</div>
|
||||
<div id="container-folder-tree"></div>
|
||||
</div>
|
||||
<div id="datatable-container">
|
||||
<div class="XiboData card py-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-sm btn-outline-secondary ots-toolbar-btn" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder"></i></button>
|
||||
<div id="breadcrumbs"></div>
|
||||
{% if currentUser.featureEnabled("template.add") %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new Template and jump to the layout editor." %}" href="{{ url_for("template.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="templates" class="table table-striped" data-content-type="layout" data-content-id-name="layoutId" data-state-preference-name="templateGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
|
||||
<th>{% trans "Orientation" %}</th>
|
||||
<th>{% trans "Thumbnail" %}</th>
|
||||
<th>{% trans "Sharing" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
{% if not currentUser.featureEnabled("folder.view") %}
|
||||
disableFolders();
|
||||
{% endif %}
|
||||
var table = $("#templates").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 1, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("template.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#templates").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "layout", responsivePriority: 2},
|
||||
{
|
||||
"name": "publishedStatus",
|
||||
responsivePriority: 2,
|
||||
"data": function (data, type) {
|
||||
if (data.publishedDate != null) {
|
||||
var now = moment();
|
||||
var published = moment(data.publishedDate);
|
||||
var differenceMinutes = published.diff(now, 'minutes');
|
||||
var momentDifference = moment(now).to(published);
|
||||
|
||||
if (differenceMinutes < -5) {
|
||||
return data.publishedStatus.concat(" - ", translations.publishedStatusFailed);
|
||||
} else {
|
||||
return data.publishedStatus.concat(" - ", translations.publishedStatusFuture + " " + momentDifference);
|
||||
}
|
||||
} else {
|
||||
return data.publishedStatus;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
{ "data": "owner", responsivePriority: 3},
|
||||
{
|
||||
"name": "description",
|
||||
"data": null,
|
||||
responsivePriority: 3,
|
||||
"render": {"_": "description", "display": "descriptionWithMarkup", "sort": "description"}
|
||||
},
|
||||
{% if currentUser.featureEnabled("tag.tagging") %}{
|
||||
"sortable": false,
|
||||
"visible": false,
|
||||
"data": dataTableCreateTags,
|
||||
responsivePriority: 3
|
||||
},{% endif %}
|
||||
{ data: 'orientation', responsivePriority: 10, visible: false},
|
||||
{
|
||||
responsivePriority: 3,
|
||||
data: 'thumbnail',
|
||||
render: function (data, type, row) {
|
||||
if (type !== 'display') {
|
||||
return row.layoutId;
|
||||
}
|
||||
if (data) {
|
||||
return '<a class="img-replace" data-toggle="lightbox" data-type="image" href="' + data + '">' +
|
||||
'<img class="img-fluid" src="' + data + '" alt="{{ "Thumbnail"|trans }}" />' +
|
||||
'</a>';
|
||||
} else {
|
||||
var addUrl = '{{ url_for("layout.thumbnail.add", {id: ":id"}) }}'.replace(':id', row.layoutId);
|
||||
return '<a class="img-replace generate-layout-thumbnail" href="' + addUrl + '">' +
|
||||
'<img class="img-fluid" src="{{ theme.uri("img/thumbs/placeholder.png") }}" alt="{{ "Add Thumbnail"|trans }}" />' +
|
||||
'</a>';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
"data": "groupsWithPermissions",
|
||||
responsivePriority: 4,
|
||||
"render": dataTableCreatePermissions
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('draw', { form: $("#templates").closest(".XiboGrid").find(".FilterDiv form") } ,dataTableCreateTagEvents);
|
||||
table.on('draw', function(e, settings) {
|
||||
$('#' + e.target.id + ' .generate-layout-thumbnail').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var $anchor = $(this);
|
||||
$.ajax({
|
||||
url: $anchor.attr('href'),
|
||||
method: 'POST',
|
||||
success: function() {
|
||||
$anchor.find('img').attr('src', $anchor.attr('href'));
|
||||
$anchor.removeClass('generate-layout-thumbnail').attr('data-toggle', 'lightbox');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#templates_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
|
||||
function templateFormOpen() {
|
||||
if ($('#folder-tree-form-modal').length === 0) {
|
||||
// compile tree folder modal and append it to Form
|
||||
var folderTreeModal = templates['folder-tree'];
|
||||
var treeConfig = {"container": "container-folder-form-tree", "modal": "folder-tree-form-modal"};
|
||||
treeConfig.trans = translations.folderTree;
|
||||
$("body").append(folderTreeModal(treeConfig));
|
||||
|
||||
$("#folder-tree-form-modal").on('hidden.bs.modal', function () {
|
||||
// Fix for 2nd/overlay modal
|
||||
$('.modal:visible').length && $(document.body).addClass('modal-open');
|
||||
|
||||
$(this).data('bs.modal', null);
|
||||
});
|
||||
}
|
||||
|
||||
// select current working folder if one is selected in the grid
|
||||
if ($('#container-folder-tree').jstree("get_selected", true)[0] !== undefined) {
|
||||
$('#templateAddForm' + ' #folderId').val($('#container-folder-tree').jstree("get_selected", true)[0].id);
|
||||
}
|
||||
|
||||
initJsTreeAjax($('#folder-tree-form-modal').find('#container-folder-form-tree'), 'templateAddForm', true, 600);
|
||||
|
||||
$("#templateAddForm").submit(function(e) {
|
||||
e.preventDefault();
|
||||
var form = $(this);
|
||||
|
||||
var url = $(this).data().redirect;
|
||||
|
||||
$.ajax({
|
||||
type: $(this).attr("method"),
|
||||
url: $(this).attr("action"),
|
||||
data: $(this).serialize(),
|
||||
cache: false,
|
||||
dataType:"json",
|
||||
success: function(xhr, textStatus, error) {
|
||||
|
||||
XiboSubmitResponse(xhr, form);
|
||||
|
||||
if (xhr.success) {
|
||||
// Reload the designer
|
||||
XiboRedirect(url.replace(":id", xhr.id));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function layoutPublishFormOpen() {
|
||||
// Nothing to do here, but we use the same form on the layout designer and have a callback registered there
|
||||
}
|
||||
|
||||
function layoutEditFormSaved() {
|
||||
// Nothing to do here.
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
5
ots-signs/views/theme-dashboard-message.twig
Normal file
5
ots-signs/views/theme-dashboard-message.twig
Normal file
@@ -0,0 +1,5 @@
|
||||
{#
|
||||
OTS Signage Theme override
|
||||
Optional dashboard message block included with ignore missing
|
||||
#}
|
||||
|
||||
22
ots-signs/views/theme-javascript.twig
Normal file
22
ots-signs/views/theme-javascript.twig
Normal file
@@ -0,0 +1,22 @@
|
||||
{#
|
||||
OTS Signage Theme - JavaScript and CSS injection
|
||||
This file is auto-included by Xibo's base.twig at the end of the document
|
||||
|
||||
NOTE: CSS and JS are INLINED to bypass web server MIME type issues with /custom/ paths
|
||||
This ensures all styles and scripts load regardless of web server routing configuration
|
||||
#}
|
||||
|
||||
<!-- Theme CSS overrides - INLINED to bypass MIME type issues -->
|
||||
<style nonce="{{ cspNonce }}">
|
||||
{% include "override-styles.twig" %}
|
||||
</style>
|
||||
|
||||
<!-- DataTables contrast fixes - INLINED to override core DataTables defaults -->
|
||||
<style nonce="{{ cspNonce }}">
|
||||
{% include "datatable-contrast.twig" %}
|
||||
</style>
|
||||
|
||||
<!-- Theme JavaScript - INLINED to bypass MIME type issues -->
|
||||
<script nonce="{{ cspNonce }}">
|
||||
{% include "theme-scripts.twig" %}
|
||||
</script>
|
||||
1554
ots-signs/views/theme-scripts.twig
Normal file
1554
ots-signs/views/theme-scripts.twig
Normal file
File diff suppressed because it is too large
Load Diff
98
ots-signs/views/transition-page.twig
Normal file
98
ots-signs/views/transition-page.twig
Normal file
@@ -0,0 +1,98 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Transition Page
|
||||
* Based on Xibo CMS transition-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Transitions"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Transitions" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage display transitions." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card" style="display:none;">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="transitions" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Has Duration" %}</th>
|
||||
<th>{% trans "Has Direction" %}</th>
|
||||
<th>{% trans "Enabled for In" %}</th>
|
||||
<th>{% trans "Enabled for Out" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table = $("#transitions").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 0, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("transition.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#transitions").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "transition", responsivePriority: 2 },
|
||||
{ "data": "hasDuration", "render": dataTableTickCrossColumn, responsivePriority: 3 },
|
||||
{ "data": "hasDirection", "render": dataTableTickCrossColumn, responsivePriority: 3 },
|
||||
{ "data": "availableAsIn", "render": dataTableTickCrossColumn, responsivePriority: 3 },
|
||||
{ "data": "availableAsOut", "render": dataTableTickCrossColumn, responsivePriority: 3 },
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#transitions_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
444
ots-signs/views/user-page.twig
Normal file
444
ots-signs/views/user-page.twig
Normal file
@@ -0,0 +1,444 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - User Page
|
||||
* Based on Xibo CMS user-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Users"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "Users" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage system users and permissions." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="usersView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter Users" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Username" %}{% endset %}
|
||||
{{ inline.inputNameGrid('userName', title) }}
|
||||
|
||||
{% set title %}{% trans "User Type" %}{% endset %}
|
||||
{{ inline.dropdown("userTypeId", "single", title, "", [{userTypeId:null, userType:""}]|merge(userTypes), "userTypeId", "userType") }}
|
||||
|
||||
{% set title %}{% trans "Retired" %}{% endset %}
|
||||
{% set values = [{id: 1, value: "Yes"}, {id: 0, value: "No"}] %}
|
||||
{{ inline.dropdown("retired", "single", title, 0, values, "id", "value") }}
|
||||
|
||||
{% set title %}{% trans "First Name" %}{% endset %}
|
||||
{{ inline.input('firstName', title) }}
|
||||
|
||||
{% set title %}{% trans "Last Name" %}{% endset %}
|
||||
{{ inline.input('lastName', title) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if currentUser.isSuperAdmin() or (currentUser.isGroupAdmin() and currentUser.featureEnabled("users.add")) %}
|
||||
{% if currentUser.getOptionValue("isAlwaysUseManualAddUserForm", 0) %}
|
||||
{% set addUserFormUrl = url_for("user.add.form") %}
|
||||
{% else %}
|
||||
{% set addUserFormUrl = url_for("user.onboarding.form") %}
|
||||
{% endif %}
|
||||
<button id="user-add-button" class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new User" %}" href="{{ addUserFormUrl }}"><i class="fa fa-user-plus" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="users" class="table table-striped" data-state-preference-name="userGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Username" %}</th>
|
||||
<th>{% trans "Homepage" %}</th>
|
||||
<th>{% trans "Home folder" %}</th>
|
||||
<th>{% trans "Email" %}</th>
|
||||
<th>{% trans "Library Quota" %}</th>
|
||||
<th>{% trans "Last Login" %}</th>
|
||||
<th>{% trans "Logged In?" %}</th>
|
||||
<th>{% trans "Retired?" %}</th>
|
||||
<th>{% trans "Two Factor" %}</th>
|
||||
<th>{% trans "First Name" %}</th>
|
||||
<th>{% trans "Last Name" %}</th>
|
||||
<th>{% trans "Phone" %}</th>
|
||||
<th>{% trans "Ref 1" %}</th>
|
||||
<th>{% trans "Ref 2" %}</th>
|
||||
<th>{% trans "Ref 3" %}</th>
|
||||
<th>{% trans "Ref 4" %}</th>
|
||||
<th>{% trans "Ref 5" %}</th>
|
||||
<th class="rowMenu">{% trans "Row Menu" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
|
||||
$(document).ready(function() {
|
||||
var table = $("#users").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
responsive: true,
|
||||
stateDuration: 0,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
searchDelay: 3000,
|
||||
"order": [[0, "asc"]],
|
||||
"filter": false,
|
||||
ajax: {
|
||||
url: "{{ url_for("user.search") }}",
|
||||
"data": function (d) {
|
||||
$.extend(d, $("#users").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{"data": "userName", responsivePriority: 2},
|
||||
{
|
||||
"data": "homePage",
|
||||
"sortable": false,
|
||||
responsivePriority: 3
|
||||
},
|
||||
{
|
||||
data: 'homeFolder',
|
||||
responsivePriority: 4
|
||||
},
|
||||
{"data": "email", responsivePriority: 3},
|
||||
{
|
||||
"name": "libraryQuota",
|
||||
responsivePriority: 3,
|
||||
"data": null,
|
||||
"render": {"_": "libraryQuota", "display": "libraryQuotaFormatted", "sort": "libraryQuota"}
|
||||
},
|
||||
{"data": "lastAccessed", "visible": false, responsivePriority: 4},
|
||||
{
|
||||
"data": "loggedIn",
|
||||
responsivePriority: 3,
|
||||
"render": dataTableTickCrossColumn,
|
||||
"visible": false,
|
||||
"sortable": false
|
||||
},
|
||||
{
|
||||
"data": "retired",
|
||||
responsivePriority: 3,
|
||||
"render": dataTableTickCrossColumn
|
||||
},
|
||||
{
|
||||
"data": "twoFactorTypeId",
|
||||
responsivePriority: 5,
|
||||
"visible": false,
|
||||
"render": function (data, type, row) {
|
||||
if (type != "display")
|
||||
return data;
|
||||
|
||||
var icon = "";
|
||||
if (data == 1)
|
||||
icon = "fa-envelope";
|
||||
else if (data == 2)
|
||||
icon = "fa-google";
|
||||
else
|
||||
icon = "fa-times";
|
||||
|
||||
return '<span class="fa ' + icon + '" title="' + (row.twoFactorDescription) + '"></span>';
|
||||
}
|
||||
},
|
||||
{"data": "firstName", "visible": false, responsivePriority: 5},
|
||||
{"data": "lastName", "visible": false, responsivePriority: 5},
|
||||
{"data": "phone", "visible": false, responsivePriority: 5},
|
||||
{"data": "ref1", "visible": false, responsivePriority: 5},
|
||||
{"data": "ref2", "visible": false, responsivePriority: 5},
|
||||
{"data": "ref3", "visible": false, responsivePriority: 5},
|
||||
{"data": "ref4", "visible": false, responsivePriority: 5},
|
||||
{"data": "ref5", "visible": false, responsivePriority: 5},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing)
|
||||
dataTableAddButtons(table, $('#users_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
function userFormOpen(dialog) {
|
||||
// Make a select2 from the home page select
|
||||
var $userForm = $(dialog).find("form.UserForm");
|
||||
var $groupId = $(dialog).find("select[name=groupId]");
|
||||
var $userTypeId = $(dialog).find("select[name=userTypeId]");
|
||||
var $select = $(dialog).find(".homepage-select");
|
||||
$select.select2({
|
||||
minimumResultsForSearch: Infinity,
|
||||
ajax: {
|
||||
url: $select.data("searchUrl"),
|
||||
dataType: "json",
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
return {
|
||||
q: params.term,
|
||||
page: params.page,
|
||||
userId: $userForm.data().userId,
|
||||
groupId: $groupId.val(),
|
||||
userTypeId: $userTypeId.val(),
|
||||
};
|
||||
},
|
||||
processResults: function (data) {
|
||||
var results = [];
|
||||
$.each(data.data, function(index, el) {
|
||||
results.push({
|
||||
"id": el.homepage,
|
||||
"text": el.title,
|
||||
"content": el.description
|
||||
});
|
||||
});
|
||||
return {
|
||||
results: results
|
||||
};
|
||||
}
|
||||
},
|
||||
templateResult: function(state) {
|
||||
if (!state.content)
|
||||
return state.text;
|
||||
|
||||
return $("<span>" + state.content + "</span>");
|
||||
}
|
||||
});
|
||||
|
||||
initFolderPanel(dialog, true);
|
||||
|
||||
// Validate form
|
||||
var $userForm = $('.UserForm');
|
||||
forms.validateForm(
|
||||
$userForm,
|
||||
$userForm.parents('.modal-body'),
|
||||
{
|
||||
submitHandler: function (form) {
|
||||
var libraryQuotaField = $(form).find('input[name=libraryQuota]');
|
||||
var libraryQuotaUnitsField = $(form).find('select[name=libraryQuotaUnits]');
|
||||
var libraryQuota = libraryQuotaField.val();
|
||||
|
||||
if (libraryQuotaUnitsField.val() === 'mb') {
|
||||
libraryQuota = libraryQuota * 1024;
|
||||
} else if (libraryQuotaUnitsField.val() === 'gb') {
|
||||
libraryQuota = libraryQuota * 1024 * 1024;
|
||||
}
|
||||
|
||||
libraryQuotaField.prop('value', libraryQuota);
|
||||
XiboFormSubmit(form);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function onboardingFormOpen(dialog) {
|
||||
$(dialog).find('[data-toggle="popover"]').popover();
|
||||
|
||||
{% if currentUser.featureEnabled("folder.view") %}
|
||||
initFolderPanel(dialog, false, true);
|
||||
{% endif %}
|
||||
|
||||
var navListItems = $(dialog).find('div.setup-panel div a'),
|
||||
allWells = $(dialog).find('.setup-content'),
|
||||
stepWizard = $(dialog).find('.stepwizard');
|
||||
|
||||
navListItems.click(function (e) {
|
||||
e.preventDefault();
|
||||
var $target = $($(this).attr('href')),
|
||||
$item = $(this);
|
||||
|
||||
if (!$item.attr('disabled')) {
|
||||
navListItems
|
||||
.removeClass('btn-success')
|
||||
.addClass('btn-default');
|
||||
|
||||
$item.addClass('btn-success');
|
||||
|
||||
allWells.hide();
|
||||
$target.show();
|
||||
$target.find('input:eq(0)').focus();
|
||||
|
||||
stepWizard.data("active", $target.prop("id"))
|
||||
|
||||
if ($target.data("next") === "finished") {
|
||||
$(dialog).find("#onboarding-steper-next-button").html("{{ "Save"|trans }}");
|
||||
} else {
|
||||
$(dialog).find("#onboarding-steper-next-button").html("{{ "Next"|trans }}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(dialog).find(".modal-footer")
|
||||
.append($('<a class="btn btn-default">').html("{{ "Close"|trans }}")
|
||||
.click(function(e) {
|
||||
e.preventDefault();
|
||||
XiboDialogClose();
|
||||
}))
|
||||
.append($('<a id="onboarding-steper-next-button" class="btn">').html("{{ "Next"|trans }}")
|
||||
.addClass("btn-primary")
|
||||
.click(function(e) {
|
||||
e.preventDefault();
|
||||
var steps = $(dialog).find(".stepwizard"),
|
||||
curStep = $(dialog).find("#" + steps.data("active")),
|
||||
curInputs = curStep.find("input[type='text'],input[type='url']"),
|
||||
isValid = true;
|
||||
|
||||
if (curStep.data("next") === "finished") {
|
||||
var $form = $(dialog).find("#userOnboardingForm");
|
||||
$form.data("apply", true);
|
||||
XiboFormSubmit($form, e, function(xhr) {
|
||||
if (xhr.success && xhr.id) {
|
||||
{% if currentUser.featureEnabled("folder.view") %}
|
||||
var selected = $(dialog).find("#container-form-folder-tree").jstree("get_selected");
|
||||
|
||||
var rootIndex = selected.indexOf('1');
|
||||
if (rootIndex > -1) {
|
||||
selected.splice(rootIndex, 1);
|
||||
}
|
||||
|
||||
var groupIds = {};
|
||||
groupIds[xhr.data.groupId] = {
|
||||
"view": 1,
|
||||
"edit": 1
|
||||
};
|
||||
var permissionsUrl = "{{ url_for("user.permissions.multi", {entity: ":entity"}) }}";
|
||||
$.ajax(permissionsUrl.replace(":entity", "Folder"), {
|
||||
"method": "POST",
|
||||
"data": {
|
||||
"ids": selected.join(","),
|
||||
"groupIds": groupIds
|
||||
},
|
||||
"error": function() {
|
||||
toastr.error("{{ "Problem saving folder sharing, please check the User created." }}");
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
XiboDialogClose();
|
||||
}
|
||||
});
|
||||
} else if (curStep.data("next") === "onboarding-step-2" && $("input[name='groupId']:checked").val() === "manual") {
|
||||
XiboDialogClose();
|
||||
XiboFormRender("{{ url_for("user.add.form") }}");
|
||||
} else {
|
||||
var nextStepWizard = steps.find("a[href='#" + curStep.data("next") + "']");
|
||||
|
||||
$(dialog).find(".form-group").removeClass("has-error");
|
||||
for (var i = 0; i < curInputs.length; i++) {
|
||||
if (!curInputs[i].validity.valid) {
|
||||
isValid = false;
|
||||
$(curInputs[i]).closest(".form-group").addClass("has-error");
|
||||
}
|
||||
}
|
||||
|
||||
if (curStep.data("next") === "onboarding-step-2") {
|
||||
var $userGroupSelected = $("input[name='groupId']:checked");
|
||||
$(dialog).find("input[name=homePageId]").val($userGroupSelected.data("defaultHomepageId"));
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
nextStepWizard.removeAttr('disabled').trigger('click');
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function userHomeFolderFormOpen(dialog) {
|
||||
initFolderPanel(dialog, true);
|
||||
}
|
||||
|
||||
function userHomeFolderMultiselectFormOpen(dialog) {
|
||||
var $input = $('<div id="container-form-folder-tree" class="card card-body bg-light"></div>');
|
||||
var $helpText = $('<span class="help-block">{{ "Set a home folder to use as the default folder for new content."|trans }}</span>');
|
||||
|
||||
$(dialog).find('.modal-body').append($input);
|
||||
$(dialog).find('.modal-body').append($helpText);
|
||||
|
||||
initFolderPanel(dialog, true);
|
||||
}
|
||||
|
||||
function initFolderPanel(dialog, isHomeOnSelect = false, isHomeContext = false) {
|
||||
var plugins = [];
|
||||
|
||||
if (!isHomeOnSelect) {
|
||||
plugins.push('checkbox');
|
||||
}
|
||||
|
||||
initJsTreeAjax(
|
||||
'#container-form-folder-tree',
|
||||
'user-add_edit-form',
|
||||
true,
|
||||
600,
|
||||
function(tree, $container) {
|
||||
if (!isHomeOnSelect) {
|
||||
tree.disable_checkbox(1);
|
||||
tree.disable_node(1);
|
||||
}
|
||||
$container.jstree('open_all');
|
||||
},
|
||||
function(data) {
|
||||
if (isHomeOnSelect && data.action === 'select_node') {
|
||||
$(dialog).find('input[name=homeFolderId]').val(data.node.id);
|
||||
|
||||
dialog.data().commitData = {homeFolderId: data.node.id};
|
||||
}
|
||||
},
|
||||
function($node, items) {
|
||||
if (isHomeContext) {
|
||||
items['home'] = {
|
||||
separator_before: false,
|
||||
separator_after: false,
|
||||
label: translations.folderTreeSetAsHome,
|
||||
action: function () {
|
||||
$(dialog).find('input[name=homeFolderId]').val($node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
},
|
||||
plugins,
|
||||
$(dialog).find('input[name=homeFolderId]').val()
|
||||
);
|
||||
|
||||
$('.folder-tree-buttons').on('click', 'button', function(ev) {
|
||||
const jsTree = $(dialog).find('#container-form-folder-tree').jstree(true);
|
||||
if ($(ev.target).attr('id') === 'selectAllBtn') {
|
||||
jsTree.select_all();
|
||||
} else if ($(ev.target).attr('id') === 'selectNoneBtn') {
|
||||
jsTree.deselect_all();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
194
ots-signs/views/usergroup-page.twig
Normal file
194
ots-signs/views/usergroup-page.twig
Normal file
@@ -0,0 +1,194 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - User Group Page
|
||||
* Based on Xibo CMS usergroup-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "User Groups"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-static-page ots-displays-page">
|
||||
<div class="page-header ots-page-header">
|
||||
<h1>{% trans "User Groups" %}</h1>
|
||||
<p class="text-muted">{% trans "Manage user groups and permissions." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="widget content-card ots-displays-card">
|
||||
<div class="widget-body ots-displays-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="userGroupView">
|
||||
<div class="XiboFilter card mb-3 bg-light content-card ots-filter-card">
|
||||
<div class="ots-filter-header">
|
||||
<h3 class="ots-filter-title">{% trans "Filter User Groups" %}</h3>
|
||||
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ots-filter-content" id="ots-filter-content">
|
||||
<div class="FilterDiv card-body" id="Filter">
|
||||
<form class="form-inline">
|
||||
{% set title %}{% trans "Name" %}{% endset %}
|
||||
{{ inline.inputNameGrid('userGroup', title) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="XiboData card pt-3 content-card ots-table-card">
|
||||
<div class="ots-table-toolbar">
|
||||
{% if currentUser.isSuperAdmin() %}
|
||||
<button class="btn btn-sm btn-success ots-toolbar-btn XiboFormButton" title="{% trans "Add a new User Group" %}" href="{{ url_for("group.add.form") }}"><i class="fa fa-users" aria-hidden="true"></i></button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-primary ots-toolbar-btn" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<table id="userGroups" class="table table-striped" data-state-preference-name="userGroupGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "User Group" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Library Quota" %}</th>
|
||||
<th>{% trans "Receive System Notifications?" %}</th>
|
||||
<th>{% trans "Receive Display Notifications?" %}</th>
|
||||
<th>{% trans "Receive Custom Notifications?" %}</th>
|
||||
<th>{% trans "Receive DataSet Notifications?" %}</th>
|
||||
<th>{% trans "Receive Layout Notifications?" %}</th>
|
||||
<th>{% trans "Receive Library Notifications?" %}</th>
|
||||
<th>{% trans "Receive Report Notifications?" %}</th>
|
||||
<th>{% trans "Receive Schedule Notifications?" %}</th>
|
||||
<th>{% trans "Is shown for Add User?" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
$(document).ready(function() {
|
||||
var table = $("#userGroups").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
searchDelay: 3000,
|
||||
filter: false,
|
||||
order: [[0, 'asc']],
|
||||
ajax: {
|
||||
url: "{{ url_for('group.search') }}",
|
||||
data: function (d) {
|
||||
$.extend(d, $('#userGroups').closest('.XiboGrid').find('.FilterDiv form').serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{data: 'group', render: dataTableSpacingPreformatted, responsivePriority: 2 },
|
||||
{data: 'description', visible: false },
|
||||
{
|
||||
name: 'libraryQuota',
|
||||
data: null,
|
||||
render: {'_': 'libraryQuota', 'display': 'libraryQuotaFormatted', 'sort': 'libraryQuota'}
|
||||
},
|
||||
{
|
||||
data: 'isSystemNotification',
|
||||
render: dataTableTickCrossColumn
|
||||
},
|
||||
{
|
||||
data: 'isDisplayNotification',
|
||||
render: dataTableTickCrossColumn
|
||||
},
|
||||
{
|
||||
data: 'isDataSetNotification',
|
||||
render: dataTableTickCrossColumn,
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
data: 'isLayoutNotification',
|
||||
render: dataTableTickCrossColumn,
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
data: 'isLibraryNotification',
|
||||
render: dataTableTickCrossColumn,
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
data: 'isReportNotification',
|
||||
render: dataTableTickCrossColumn,
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
data: 'isScheduleNotification',
|
||||
render: dataTableTickCrossColumn,
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
data: 'isCustomNotification',
|
||||
render: dataTableTickCrossColumn,
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
data: "isShownForAddUser",
|
||||
render: dataTableTickCrossColumn
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', dataTableDraw);
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#userGroups_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
function handleLibraryQuotaField(libraryQuotaField, libraryQuotaUnitsField) {
|
||||
var libraryQuota = libraryQuotaField.val();
|
||||
|
||||
if (libraryQuotaUnitsField.val() === 'mb') {
|
||||
libraryQuota = libraryQuota * 1024;
|
||||
} else if (libraryQuotaUnitsField.val() === 'gb') {
|
||||
libraryQuota = libraryQuota * 1024 * 1024;
|
||||
}
|
||||
|
||||
libraryQuotaField.prop('value', libraryQuota);
|
||||
}
|
||||
|
||||
function userGroupFormOpen() {
|
||||
var $userGroupForm = $('.UserGroupForm');
|
||||
forms.validateForm(
|
||||
$userGroupForm,
|
||||
$userGroupForm.parents('.modal-body'),
|
||||
{
|
||||
submitHandler: function (form) {
|
||||
handleLibraryQuotaField(
|
||||
$(form).find('input[name=libraryQuota]'),
|
||||
$(form).find('select[name=libraryQuotaUnits]')
|
||||
);
|
||||
|
||||
XiboFormSubmit(form);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user