Add new pages for managing tags, tasks, transitions, users, user groups, and their respective JavaScript functionalities

- Implemented tag management page with filtering, data table, and AJAX functionality.
- Created task management page with task listing, filtering, and AJAX data loading.
- Developed transition management page with a data table for transitions.
- Added user management page with comprehensive user details, filtering options, and AJAX support.
- Introduced user group management page with filtering and data table for user groups.
- Enhanced JavaScript for data tables, including state saving, filtering, and AJAX data fetching for all new pages.
This commit is contained in:
matt
2026-02-06 23:54:21 -05:00
parent 122d098be4
commit 87a444b8de
34 changed files with 4579 additions and 688 deletions

320
SIDEBAR_DESIGN_REVIEW.md Normal file
View File

@@ -0,0 +1,320 @@
# Comprehensive Sidebar Design Review - OTS Signage Theme
## Executive Summary
The sidebar is well-structured with a modern dark-mode design, but has several areas for refinement:
- **Padding**: Generally good, but can be more consistent
- **Icon Arrangement**: Icons are properly centered and sized, but scaling between states could be improved
- **Overall Style**: Cohesive and modern, with good dark mode aesthetics
---
## 1. SIDEBAR DIMENSIONS & STRUCTURE
### CSS Variables (in `override.css`):
```css
--ots-sidebar-width: 256px /* Full width */
--ots-sidebar-collapsed-width: 64px /* Collapsed width */
--ots-sidebar-header-height: 64px /* Header section */
--ots-sidebar-item-height: 44px /* Nav item height */
--ots-sidebar-item-radius: 10px /* Border radius */
--ots-sidebar-item-padding-x: 12px /* Horizontal padding */
```
### Layout Analysis:
- **Full Width**: 256px (reasonable for desktop, matches common patterns)
- **Collapsed Width**: 64px (good for icon-only display)
- **Responsive Break**: Changes to overlay at 768px (mobile-first, good)
**Issue #1**: When collapsed, icons still have adequate space (20px icon + padding), but text labels are 100% hidden. Consider adding a tooltip system for discoverability.
---
## 2. PADDING ANALYSIS
### Sidebar Header (`.sidebar-header`)
```css
padding: 20px 16px; /* Vertical: 20px | Horizontal: 16px */
```
- **Assessment**: ✅ Good - balanced and symmetrical
- **Component**: Contains logo (32px icon) + brand text + toggle button
- **Vertical Alignment**: Properly centered with `align-items: center`
### Sidebar Content (`.sidebar-content`)
```css
padding: 12px 0; /* Vertical: 12px | Horizontal: 0 */
```
- **Assessment**: ⚠️ Inconsistent - no horizontal padding here
- **Issue**: Padding is applied at the list item level instead (see nav items)
### Sidebar Navigation (`.sidebar-nav`)
```css
padding: 72px 0 120px; /* Top: 72px | Bottom: 120px | Sides: 0 */
```
- **Assessment**: ⚠️ **PROBLEM AREA** - padding is excessive for top/bottom
- 72px top padding assumes a fixed header height, could be dynamic
- 120px bottom padding creates large blank space (mobile unfriendly)
- No horizontal padding means nav items extend to edge
### Navigation Items (`.ots-sidebar li.sidebar-list > a`)
```css
padding: 6px 10px; /* Item content padding */
margin: 3px 10px; /* Outer margin/spacing */
border-radius: 12px;
min-height: 40px;
```
- **Assessment**: ⚠️ Mixed padding/margin approach
- **Inner padding (6px 10px)**: Good for text breathing room
- **Outer margin (3px 10px)**: Creates 10px left/right space from sidebar edge
- **Min-height (40px)**: Good touch target size
**Recommendation**: The left margin (10px) combined with border-radius (12px) creates a nice inset look. Could be intentional and good.
---
## 3. ICON ARRANGEMENT
### Icon Container (`.ots-nav-icon`)
```css
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: currentColor;
justify-self: center;
```
- **Assessment**: ✅ Excellent
- Square container with centered content
- `justify-self: center` ensures centering in CSS Grid layout
- `color: currentColor` inherits link color for active/hover states
### Nav Item Grid Layout
```css
display: grid;
grid-template-columns: 20px 1fr; /* Icon: 20px fixed | Text: flex */
align-items: center;
column-gap: 12px;
```
- **Assessment**: ✅ Good grid-based approach
- Consistent 12px gap between icon and text
- Icon column is tight (20px), text is flexible
- **Issue**: 20px icon container is narrower than 24px icon element - could cause slight misalignment
**Recommendation**: Change to `grid-template-columns: 24px 1fr` to match the icon container size.
### Active Item Styling
```css
.ots-sidebar li.sidebar-list.active > a {
color: #0b1221; /* Dark text on light bg */
background-color: #ffffff;
font-weight: 600;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.25);
}
```
- **Assessment**: ✅ Strong visual feedback
- High contrast (white background + dark text)
- Shadow adds depth
- Icon inherits dark color via `currentColor`
---
## 4. OVERALL STYLE & DESIGN CONSISTENCY
### Color Palette (Dark Mode)
```css
--ots-sidebar-bg: #08132a; /* Very dark blue */
--ots-sidebar-link: #f9fbff; /* Nearly white text */
--ots-sidebar-link-hover-bg: rgba(255,255,255,0.08);
--ots-sidebar-active-bg: rgba(255,255,255,0.06);
--ots-sidebar-active-text: #ffffff;
--ots-sidebar-muted-text: #8ea4c7; /* Muted section headers */
```
- **Assessment**: ✅ Excellent contrast and hierarchy
- Background: Deep navy (#08132a)
- Primary text: Nearly white (#f9fbff)
- Hover: 8% white overlay (subtle)
- Active: 6% white overlay (subtle)
- Section headers: Medium blue (#8ea4c7)
- **Accessibility**: WCAG AA compliant (>4.5:1 contrast)
### Section Headers (`.sidebar-title`)
```css
display: block;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
color: #8ea4c7;
letter-spacing: 0.12em;
padding: 12px 14px 4px;
```
- **Assessment**: ✅ Good hierarchy
- Small, uppercase, muted color clearly distinguishes from regular nav items
- Letter spacing adds visual interest
- Adequate padding separates sections
### Sidebar Footer (`.sidebar-footer`)
```css
border-top: 1px solid var(--color-border);
padding: 16px;
background-color: rgba(59, 130, 246, 0.05); /* Slight blue tint */
```
- **Assessment**: ✅ Good
- Separated with border
- Subtle background color differentiates from main nav
- 16px padding consistent with other components
---
## 5. RESPONSIVE BEHAVIOR
### Mobile Override (@media max-width: 768px)
```css
.ots-sidebar {
transform: translateX(-100%);
transition: transform var(--transition-base);
width: 280px;
}
.ots-sidebar.active {
transform: translateX(0);
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.3);
}
.ots-main {
margin-left: 0;
}
```
- **Assessment**: ✅ Good mobile experience
- Slides in from left (drawer pattern)
- Shadow adds depth when open
- Content isn't pushed aside (overlay instead)
- **Issue**: Sidebar width changes from 256px → 280px on mobile (should be 280px, maybe even narrower for mobile)
---
## 6. COLLAPSED STATE STYLING
### Collapsed Class
```css
.ots-sidebar.collapsed {
/* No explicit width override - uses CSS variable */
}
```
- **Assessment**: ⚠️ Lacks dedicated styling
- When `.collapsed` is added, only width changes (via CSS variable)
- No visual indicator (badge, animation) that it's collapsed
- Icon-only display works but lacks affordance
### Collapsed Item Styling (`.sidebar-collapsed-item-bg`)
```css
--ots-sidebar-collapsed-item-bg: rgba(255, 255, 255, 0.08);
--ots-sidebar-collapsed-item-hover-bg: rgba(255, 255, 255, 0.16);
```
- **Assessment**: ⚠️ Variables defined but not actively used in CSS rules
- These variables exist but I don't see them applied to `.collapsed` state styling
- **Action Item**: Verify if collapsed items have proper styling
---
## 7. KEY ISSUES & RECOMMENDATIONS
### 🔴 HIGH PRIORITY
1. **Icon/Grid Mismatch**
- Icon container: 24px, but grid column: 20px
- **Fix**: Change `grid-template-columns: 20px 1fr``grid-template-columns: 24px 1fr`
2. **Excessive Nav Padding**
- `.sidebar-nav { padding: 72px 0 120px }` is too much
- **Fix**: Use dynamic sizing based on header height (JavaScript or CSS custom property)
- **Suggestion**: `padding: var(--ots-sidebar-header-height) 0 60px` (60px is enough for footer breathing room)
3. **Collapsed State Styling**
- Collapsed items have CSS variables defined but not used
- **Fix**: Add active rules like:
```css
.ots-sidebar.collapsed li.sidebar-list.active > a {
background-color: rgba(255, 255, 255, 0.12);
}
```
### 🟡 MEDIUM PRIORITY
4. **Icon Discoverability When Collapsed**
- No tooltips or labels appear when sidebar is collapsed
- **Suggestion**: Add `title` attributes to nav items or implement CSS tooltips
5. **Sidebar Header Button Spacing**
- Header has "toggle" button but spacing could be tighter on mobile
- **Suggestion**: When collapsed, header could be more compact
6. **Mobile Width Inconsistency**
- Sidebar is 256px full-width but 280px on mobile (why wider?)
- **Suggestion**: Keep consistent at 256px or make it responsive (e.g., 90vw max 280px on small phones)
### 🟢 LOW PRIORITY
7. **Brand Icon & Text**
- Logo + text look good but could use more breathing room
- **Current**: `gap: 12px` - consider `gap: 16px` for more visual separation
---
## 8. VISUAL SPACING SUMMARY
| Component | Padding | Margin | Assessment |
|-----------|---------|--------|------------|
| Header | 20px V, 16px H | — | ✅ Good |
| Content Wrapper | 12px V, 0 H | — | ⚠️ Inconsistent |
| Nav List | 72px T, 120px B | — | 🔴 Excessive |
| Nav Items | 6px V, 10px H | 3px V, 10px H | ✅ Good |
| Section Headers | 12px T, 14px H | — | ✅ Good |
| Footer | 16px | — | ✅ Consistent |
---
## 9. ICON SIZING CONSISTENCY
| Element | Width | Height | Font Size | Usage |
|---------|-------|--------|-----------|-------|
| Nav Icon | 24px | 24px | 16px | Primary nav items |
| Brand Icon | 32px | 32px | 24px | Logo in header |
| Topbar Icon | 20px | 20px | 16px | Topbar controls |
**Assessment**: ✅ Good hierarchy and clarity
---
## 10. RECOMMENDATIONS FOR NEXT PHASE
1. ✅ Fix grid column width mismatch (20px → 24px)
2. ✅ Refactor `.sidebar-nav` padding (use CSS variables or dynamic)
3. ✅ Add collapsed state active item styling
4. ✅ Add `title` attributes to nav items for tooltip support
5. ⚠️ Consider adding a visual "collapse indicator" (e.g., chevron or light pulsing border)
6. ⚠️ Standardize mobile sidebar width
7. ⚠️ Add more breathing room in header (gap: 16px instead of 12px for brand icon)
---
## Summary
**Overall Grade: B+ (85/100)**
### Strengths:
- ✅ Modern, cohesive dark-mode aesthetic
- ✅ Good color contrast and accessibility
- ✅ Proper icon sizing and centering
- ✅ Responsive mobile overlay pattern
- ✅ Clear visual hierarchy (headers, active states, hover)
### Weaknesses:
- ⚠️ Excessive bottom padding in nav list
- ⚠️ Grid icon column width mismatch
- ⚠️ Lacking visual affordance when collapsed
- ⚠️ Icon discoverability issues
### Quick Wins (implement first):
1. Change `grid-template-columns: 20px 1fr` → `24px 1fr`
2. Change `.sidebar-nav padding: 72px 0 120px` → `64px 0 60px`
3. Add collapsed state styling for active items

View File

@@ -283,3 +283,19 @@ img {
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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -31,11 +31,66 @@
});
}
// 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) {
@@ -43,6 +98,7 @@
if (isCollapsed) {
sidebar.classList.add('collapsed');
body.classList.add('ots-sidebar-collapsed');
document.documentElement.classList.add('ots-sidebar-collapsed');
updateSidebarStateClass();
}
@@ -51,10 +107,8 @@
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');
// Update measured sidebar width when collapsed state changes
updateSidebarWidth();
// Recalculate nav offset so items remain below header after collapse
updateSidebarNavOffset();
updateSidebarStateClass();
});
@@ -65,9 +119,8 @@
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');
updateSidebarWidth();
// Recalculate nav offset after expanding
updateSidebarNavOffset();
updateSidebarStateClass();
});
@@ -90,17 +143,16 @@
}
/**
* Measure sidebar width and set CSS variable for layout
* 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() {
// No-op: CSS handles layout via body.ots-sidebar-collapsed class
if (window.__otsDebug) {
const sidebar = document.querySelector('.ots-sidebar');
if (!sidebar) return;
// If collapsed, use the known collapsed width; otherwise use measured width
const collapsed = sidebar.classList.contains('collapsed');
const base = collapsed ? 64 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 256;
const padding = 0;
const value = Math.max(64, Math.round(base + padding));
document.documentElement.style.setProperty('--ots-sidebar-width', `${value}px`);
const collapsed = sidebar ? sidebar.classList.contains('collapsed') : false;
console.log('[OTS] updateSidebarWidth (no-op, CSS-driven)', { collapsed });
}
}
/**

View 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-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 dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboFilter card mb-3 bg-light dashboard-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 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add an Application" %}" href="{{ url_for("application.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Application" %}</button>
<button class="btn btn-sm btn-primary" 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 dashboard-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 %}

View File

@@ -22,7 +22,7 @@
<div class="sidebar-content">
<ul class="sidebar ots-sidebar-nav">
<li class="sidebar-list">
<a href="{{ url_for("home") }}">
<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>

View File

@@ -44,11 +44,6 @@
// 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');
// Also set the CSS variable used for collapsed width so layout shifts correctly
try {
var v = getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-collapsed-width') || '64px';
document.documentElement.style.setProperty('--ots-sidebar-width', v);
} catch(e){}
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){}
@@ -58,11 +53,9 @@
</script>
<style nonce="{{ cspNonce }}">html,body{background:#ffffff!important;color:#111111!important}
/* Hide the top header row immediately when sidebar is collapsed to prevent flash */
html.ots-sidebar-collapsed .row.header.header-side,
body.ots-sidebar-collapsed .row.header.header-side,
.ots-sidebar.collapsed ~ .ots-main .row.header.header-side,
.ots-sidebar.collapsed .row.header.header-side { display: none !important; height: 0 !important; margin: 0 !important; padding: 0 !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 %}
@@ -112,34 +105,21 @@
{% endif %}
<div id="content-wrapper">
<div class="page-content">
{% if not horizontalNav or hideNavigation == "1" or forceHide %}
<div class="row header header-side">
<div class="col-sm-12">
<div class="meta pull-left xibo-logo-container">
<div class="page"><img class="xibo-logo" src="{{ theme.uri("img/xibologo.png") }}"></div>
</div>
{# Floating top-right actions: notification bell + user menu #}
{% if not forceHide %}
{% if not hideNavigation == "1" %}
<button type="button" class="pull-right navbar-toggler navbar-toggler-side" data-toggle="collapse" data-target="#navbar-collapse-1" aria-controls="navbarNav" aria-expanded="false">
<span class="fa fa-bars"></span>
</button>
{% endif %}
<div class="user-actions pull-right">
<div class="ots-page-actions">
{% include "authed-theme-topbar.twig" ignore missing %}
{% if currentUser.featureEnabled("drawer") %}
<div class="user-notif">
<div class="ots-topbar-action">
{% include "authed-notification-drawer.twig" with { 'compact': true } %}
</div>
{% endif %}
<div class="user">
<div class="ots-topbar-action">
{% include "authed-user-menu.twig" with { 'compact': true } %}
</div>
</div>
{% include "authed-theme-topbar.twig" ignore missing %}
{% endif %}
</div>
</div>
{% endif %}
<div class="page-content">
<div class="row">
<div class="col-sm-12">
{% block actionMenu %}{% endblock %}

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Campaigns"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("campaign.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -98,8 +91,8 @@
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 dashboard-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">
@@ -110,12 +103,18 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none">
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("campaign.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Campaign" %}" href="{{ url_for("campaign.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Campaign" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Commands"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("command.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
@@ -65,6 +58,12 @@
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("command.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add Command" %}" href="{{ url_for("command.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Command" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -368,7 +368,7 @@
</form>
</div>
</div>
<div class="XiboData card pt-3">
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<table id="displaysGrid" class="table table-striped" data-state-preference-name="statusDashboardDisplays" style="width: 100%;">
<thead>
<tr>

View File

@@ -26,14 +26,7 @@
{% block title %}{{ "DataSets"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("dataset.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -65,7 +58,7 @@
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -83,8 +76,8 @@
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 dashboard-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">
@@ -95,12 +88,18 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none">
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("dataset.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new DataSet" %}" href="{{ url_for("dataSet.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add DataSet" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Dayparting"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("daypart.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -67,6 +60,12 @@
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("daypart.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Daypart" %}" href="{{ url_for("daypart.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Daypart" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Displays"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("displays.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block headContent %}
{# Add page source code bundle ( CSS ) #}
@@ -55,10 +48,6 @@
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 {
var v = getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-collapsed-width') || '64px';
document.documentElement.style.setProperty('--ots-sidebar-width', v);
} catch(e){}
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){}
@@ -67,11 +56,9 @@
})();
</script>
<style nonce="{{ cspNonce }}">html,body{background:#ffffff!important;color:#111111!important}
/* Hide the top header row immediately when sidebar is collapsed to prevent flash */
html.ots-sidebar-collapsed .row.header.header-side,
body.ots-sidebar-collapsed .row.header.header-side,
.ots-sidebar.collapsed ~ .ots-main .row.header.header-side,
.ots-sidebar.collapsed .row.header.header-side { display: none !important; height: 0 !important; margin: 0 !important; padding: 0 !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 }}">
@@ -162,7 +149,7 @@
{% if currentUser.featureEnabled("displaygroup.view") %}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -282,6 +269,12 @@
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("displays.add") %}
<button class="btn btn-sm btn-success 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> {% trans "Add Display" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Display Groups"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("displaygroup.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -62,7 +55,7 @@
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -116,6 +109,12 @@
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("displaygroup.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add Display Group" %}" href="{{ url_for("displayGroup.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Group" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Display Setting Profiles"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("displayprofile.add") %}
<button class="btn btn-icon btn-info 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -65,6 +58,12 @@
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("displayprofile.add") %}
<button class="btn btn-sm btn-info 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> {% trans "Add Profile" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View 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-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 dashboard-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 dashboard-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 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("font.add") %}
<button class="btn btn-sm btn-success" href="#" id="fontUploadForm" title="{% trans "Add a new Font" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Upload Font" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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 %}

View File

@@ -3,19 +3,7 @@
{% block title %}{{ "Layouts"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("layout.add") %}
<button class="btn btn-success 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-icon btn-info" 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -64,7 +52,7 @@
{% 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: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -81,7 +69,7 @@
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -97,7 +85,7 @@
{% 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: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -156,8 +144,8 @@
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 dashboard-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">
@@ -169,13 +157,20 @@
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none">
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("layout.add") %}
<button class="btn btn-sm btn-success 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> {% trans "Add Layout" %}</button>
<button class="btn btn-sm btn-info" id="layoutUploadForm" title="{% trans "Import a Layout from a ZIP file." %}" href="#"><i class="fa fa-cloud-download" aria-hidden="true"></i> {% trans "Import" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -25,35 +25,7 @@
{% block title %}{{ "Library"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabledCount(["library.add", "library.modify"]) > 0 or settings.SETTING_LIBRARY_TIDY_ENABLED == 1 %}
{% if currentUser.featureEnabled("library.add") %}
<button class="btn btn-icon btn-success" href="#" id="libraryUploadForm" title="{% trans "Add a new media item to the library" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i></button>
<button class="btn btn-icon btn-success XiboFormButton" title="{% trans "Add a new media item to the library via external URL" %}" href="{{ url_for("library.uploadUrl.form") }}"><i class="fa fa-link" aria-hidden="true"></i></button>
{% endif %}
{% if settings.SETTING_LIBRARY_TIDY_ENABLED == 1 and currentUser.featureEnabled("library.modify") %}
<button class="btn btn-icon btn-warning XiboFormButton btn-tidy" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("library.tidy.form") }}">
<svg class="icon icon-broom-pantry" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<g fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
<!-- dustpan -->
<path d="M3 6h6l2 6v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6z" fill="currentColor" opacity="0.08"/>
<path d="M9 6v0" />
<path d="M3.5 7.5L8 7.5" />
<!-- broom handle -->
<path d="M14 3l6 6-7 7" />
<!-- bristles -->
<path d="M11 14l4.5-4.5M12 15l5-5M13 16l5.5-5.5" />
<!-- small hand grip accent -->
<circle cx="14.5" cy="4.5" r="0.5" fill="currentColor" />
</g>
</svg>
</button>
{% endif %}
{% endif %}
<button class="btn btn-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
@@ -99,7 +71,7 @@
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -115,7 +87,7 @@
{% 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: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -167,6 +139,18 @@
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% 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" href="#" id="libraryUploadForm" title="{% trans "Add a new media item to the library" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Media" %}</button>
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new media item to the library via external URL" %}" href="{{ url_for("library.uploadUrl.form") }}"><i class="fa fa-link" aria-hidden="true"></i> {% trans "Add URL" %}</button>
{% endif %}
{% if settings.SETTING_LIBRARY_TIDY_ENABLED == 1 and currentUser.featureEnabled("library.modify") %}
<button class="btn btn-sm btn-warning 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> {% trans "Tidy" %}</button>
{% endif %}
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Menu Boards"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("menuBoard.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -66,7 +59,7 @@
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -84,8 +77,8 @@
</div>
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 dashboard-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">
@@ -96,8 +89,18 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("menuBoard.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Menu Board" %}" href="{{ url_for("menuBoard.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Menu Board" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View 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-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 dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboFilter card mb-3 bg-light dashboard-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 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button class="btn btn-sm btn-primary" 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 %}

View File

@@ -29,40 +29,32 @@
color-scheme: dark;
}
/* Ensure the page title/description remain visible when the sidebar is collapsed */
html.ots-sidebar-collapsed .page-header,
body.ots-sidebar-collapsed .page-header,
.ots-sidebar.collapsed ~ .ots-main .page-header,
.ots-main .page-header {
/* Page header - always visible and properly positioned */
.ots-main .page-header,
.ots-main .page-header h1,
.ots-main .page-header p.text-muted {
display: block !important;
visibility: visible !important;
}
.ots-main .page-header {
position: relative !important;
z-index: 100 !important;
height: auto !important;
padding-top: 16px !important;
padding-bottom: 16px !important;
margin-top: 8px !important;
margin-bottom: 12px !important;
}
html.ots-sidebar-collapsed .page-header h1,
body.ots-sidebar-collapsed .page-header h1,
.ots-sidebar.collapsed ~ .ots-main .page-header h1,
.ots-main .page-header h1 {
color: var(--color-text-primary) !important;
}
html.ots-sidebar-collapsed .page-header p.text-muted,
body.ots-sidebar-collapsed .page-header p.text-muted,
.ots-sidebar.collapsed ~ .ots-main .page-header p.text-muted,
.ots-main .page-header p.text-muted {
color: var(--color-text-tertiary) !important;
}
/* Ensure page header is not obscured by panels when sidebar is collapsed */
.ots-main .page-header {
position: relative !important;
z-index: 2500 !important;
margin-top: 8px !important;
margin-bottom: 12px !important;
}
/* Prevent immediate container clipping near the top */
.ots-content,
.ots-main,
@@ -70,37 +62,6 @@ body.ots-sidebar-collapsed .page-header p.text-muted,
overflow: visible !important;
}
/* Fixed-position fallback: keep the page header visible and readable when sidebar is collapsed */
@media (min-width: 992px) {
html.ots-sidebar-collapsed .page-header,
body.ots-sidebar-collapsed .page-header,
.ots-sidebar.collapsed ~ .ots-main .page-header {
position: fixed !important;
top: 16px !important;
left: calc(var(--ots-sidebar-width,64px) + 24px) !important;
/* fallback positions in case the CSS variable isn't set early */
left: 80px !important;
left: 260px !important;
right: 24px !important;
z-index: 3200 !important;
background: transparent !important;
padding-top: 8px !important;
padding-bottom: 8px !important;
margin: 0 !important;
}
/* If sidebar is collapsed, prefer a smaller left offset */
html.ots-sidebar-collapsed .page-header,
body.ots-sidebar-collapsed .page-header {
left: 80px !important;
}
html.ots-sidebar-collapsed .page-header .ots-filter-header,
body.ots-sidebar-collapsed .page-header .ots-filter-header {
margin-top: 0 !important;
}
}
html,
body {
background-color: var(--color-background);
@@ -120,10 +81,10 @@ body {
position: fixed;
left: 0;
top: 0;
width: 260px;
width: var(--ots-sidebar-width);
height: 100vh;
background-color: #08132a;
border-right: 1px solid rgba(255, 255, 255, 0.06);
border-right: none;
padding: 0;
display: flex;
flex-direction: column;
@@ -135,7 +96,6 @@ body {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 260px;
}
.ots-content {
@@ -189,7 +149,7 @@ body {
.brand-link {
display: flex;
align-items: center;
gap: 12px;
gap: 16px;
color: var(--color-text-primary);
font-weight: 700;
font-size: 16px;
@@ -216,7 +176,7 @@ body {
.sidebar-nav {
list-style: none;
margin: 0;
padding: 72px 0 120px;
padding: 64px 0 60px;
}
/* Extra top padding when sidebar is collapsed or expanded so items clear header */
@@ -238,7 +198,7 @@ body {
.ots-sidebar li.sidebar-main > a,
.ots-sidebar li.sidebar-title > a {
display: grid;
grid-template-columns: 20px 1fr;
grid-template-columns: 24px 1fr;
align-items: center;
column-gap: 12px;
padding: 6px 10px;
@@ -271,6 +231,16 @@ body {
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.25);
}
/* Collapsed state - ensure active items remain visually distinct */
.ots-sidebar.collapsed li.sidebar-list.active > a,
.ots-sidebar.collapsed li.sidebar-list > a.active,
.ots-sidebar.collapsed li.sidebar-main.active > a,
.ots-sidebar.collapsed li.sidebar-main > a.active {
background-color: rgba(255, 255, 255, 0.12);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.06);
color: #ffffff;
}
.ots-sidebar .ots-nav-icon {
width: 24px;
height: 24px;
@@ -432,18 +402,19 @@ body {
============================================================================ */
.ots-topbar {
background-color: var(--color-surface-elevated);
border-bottom: 2px solid var(--color-border);
padding: 8px 24px;
background-color: transparent;
border-bottom: none;
padding: 0;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
height: 64px;
gap: 4px;
height: 56px;
z-index: 1100;
position: sticky;
top: 0;
width: 100%;
position: relative;
width: auto;
flex: 1;
min-width: 0;
}
/* Topbar nav container - override .navbar-nav defaults */
@@ -452,10 +423,11 @@ body {
border: 0 !important;
padding: 0 !important;
margin: 0 !important;
gap: 6px;
gap: 2px;
height: 100%;
align-items: center;
justify-content: flex-start;
flex-wrap: nowrap;
}
.ots-topbar .nav-item {
@@ -521,7 +493,7 @@ body {
/* Ensure content is offset below the sticky topbar when horizontal nav present */
nav.navbar + #content-wrapper,
nav.navbar + #content-wrapper .page-content {
padding-top: 64px;
padding-top: 56px;
}
/* Right-side controls: notification bell + account menu */
@@ -537,61 +509,239 @@ nav.navbar + #content-wrapper .page-content {
align-items: center;
}
.header-side .user,
.header-side .user-notif,
.header-side .user-actions {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0;
}
/* ============================================================================
TOPBAR STRIP (sidebar mode) - Clean, Modern Single-Line Menu Bar
============================================================================ */
.header-side .user-actions {
float: right;
display: flex;
align-items: center;
gap: 12px;
}
.header-side .user-actions > * {
display: inline-flex !important;
align-items: center;
/* The topbar strip is the bar at the top of the content area in sidebar mode.
Layout: [Logo (collapsed only)] ---- [notifications] [user] [hamburger] */
.ots-topbar-strip {
position: sticky !important;
top: 0 !important;
z-index: 1100 !important;
background-color: var(--color-surface-elevated) !important;
border-bottom: 1px solid var(--color-border) !important;
height: 56px !important;
min-height: 56px !important;
max-height: 56px !important;
margin: 0 !important;
padding: 0 !important;
overflow: visible !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
}
.header-side .user-actions li,
.header-side .user-actions .nav-item,
.header-side .user-actions .dropdown,
.header-side .user-actions .item,
.header-side .user-actions .nav-link {
display: inline-flex !important;
align-items: center;
width: auto !important;
/* When sidebar is expanded, make the topbar background subtler */
body:not(.ots-sidebar-collapsed) .ots-topbar-strip {
background-color: var(--color-background) !important;
box-shadow: none !important;
border-bottom-color: var(--color-border) !important;
}
.header-side .user-actions img.nav-avatar {
display: block;
width: 36px;
height: 36px;
}
.header-side .user-actions .dropdown-menu {
right: 0;
left: auto;
}
/* Ensure header area does not clip absolutely positioned dropdowns */
.row.header.header-side,
.row.header.header-side .col-sm-12,
.row.header.header-side .user-actions {
/* Inner flex container */
.ots-topbar-inner {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
height: 56px !important;
padding: 0 16px !important;
margin: 0 !important;
gap: 12px !important;
overflow: visible !important;
}
/* Ensure user dropdown renders above other content */
.header-side .user-actions .dropdown-menu,
.ots-user-menu {
/* Left side: logo (only visible when sidebar collapsed) */
.ots-topbar-left {
display: flex !important;
align-items: center !important;
gap: 12px !important;
flex-shrink: 0 !important;
min-width: 0 !important;
}
.ots-topbar-strip .xibo-logo-container {
display: flex !important;
align-items: center !important;
gap: 10px !important;
text-decoration: none !important;
}
.ots-topbar-strip .xibo-logo {
width: 28px !important;
height: 28px !important;
object-fit: contain !important;
}
.ots-topbar-strip .xibo-logo-text {
display: inline-flex !important;
flex-direction: column !important;
line-height: 1 !important;
}
.ots-topbar-strip .brand-line-top {
font-weight: 700 !important;
font-size: 15px !important;
color: var(--color-text-primary) !important;
letter-spacing: 0.02em !important;
}
.ots-topbar-strip .brand-line-bottom {
font-weight: 600 !important;
font-size: 11px !important;
color: var(--color-text-secondary) !important;
margin-top: -1px !important;
}
/* Right side: actions cluster */
.ots-topbar-right {
display: flex !important;
align-items: center !important;
gap: 4px !important;
flex-shrink: 0 !important;
margin-left: auto !important;
}
/* Each action item (notification bell, user avatar) */
.ots-topbar-action {
display: inline-flex !important;
align-items: center !important;
position: static !important;
}
.ots-topbar-action .nav-item,
.ots-topbar-action .dropdown,
.ots-topbar-action > div {
display: inline-flex !important;
align-items: center !important;
position: static !important;
}
/* Notification & user menu links */
.ots-topbar-action .nav-link {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 36px !important;
height: 36px !important;
padding: 0 !important;
border-radius: 8px !important;
color: var(--color-text-secondary) !important;
transition: all 150ms ease !important;
border: none !important;
background: transparent !important;
}
.ots-topbar-action .nav-link:hover {
background-color: rgba(59, 130, 246, 0.08) !important;
color: var(--color-primary) !important;
}
/* Bell icon */
.ots-topbar-action .ots-topbar-icon {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 18px !important;
height: 18px !important;
font-size: 16px !important;
}
/* Avatar in topbar */
.ots-topbar-action img.nav-avatar {
display: block !important;
width: 32px !important;
height: 32px !important;
border-radius: 50% !important;
object-fit: cover !important;
border: 2px solid transparent !important;
transition: border-color 150ms ease !important;
}
.ots-topbar-action .nav-link:hover img.nav-avatar {
border-color: var(--color-primary) !important;
}
/* Dropdown menus from the topbar */
.ots-topbar-action .dropdown-menu,
.ots-topbar-strip .dropdown-menu {
position: absolute !important;
top: 100% !important;
right: 0 !important;
left: auto !important;
margin-top: 0 !important;
min-width: 200px !important;
background-color: var(--color-surface-elevated) !important;
border: 1px solid var(--color-border) !important;
border-radius: 10px !important;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25) !important;
padding: 6px 0 !important;
z-index: 3000 !important;
overflow: visible !important;
}
.ots-topbar-strip .dropdown-item,
.ots-topbar-strip .dropdown-menu a {
display: flex !important;
align-items: center !important;
gap: 8px !important;
padding: 8px 14px !important;
margin: 1px 6px !important;
border-radius: 6px !important;
color: var(--color-text-secondary) !important;
font-size: 13px !important;
font-weight: 500 !important;
transition: all 150ms ease !important;
text-decoration: none !important;
}
.ots-topbar-strip .dropdown-item:hover,
.ots-topbar-strip .dropdown-menu a:hover {
background-color: rgba(59, 130, 246, 0.08) !important;
color: var(--color-primary) !important;
}
.ots-topbar-strip .dropdown-header {
padding: 8px 14px 4px !important;
font-size: 12px !important;
font-weight: 600 !important;
color: var(--color-text-tertiary) !important;
text-transform: uppercase !important;
letter-spacing: 0.04em !important;
}
.ots-topbar-strip .dropdown-divider {
margin: 4px 0 !important;
border-top-color: var(--color-border) !important;
}
/* Hamburger button */
.ots-topbar-hamburger {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 36px !important;
height: 36px !important;
padding: 0 !important;
border: none !important;
background: transparent !important;
color: var(--color-text-secondary) !important;
border-radius: 8px !important;
cursor: pointer !important;
transition: all 150ms ease !important;
font-size: 16px !important;
flex-shrink: 0 !important;
}
.ots-topbar-hamburger:hover {
background-color: rgba(59, 130, 246, 0.08) !important;
color: var(--color-primary) !important;
}
/* Ensure no clipping */
.ots-topbar-strip,
.ots-topbar-strip .col-sm-12,
.ots-topbar-strip .ots-topbar-right,
.ots-topbar-strip .ots-topbar-action {
overflow: visible !important;
}
/* When JS decides to open to the left (avoid viewport overflow) */
@@ -600,66 +750,54 @@ nav.navbar + #content-wrapper .page-content {
right: 0 !important;
}
/* When JS wants explicit left-aligned menu (menu's left edge aligned to trigger's left) */
/* When JS wants explicit left-aligned menu */
.dropdown-menu-left-align {
left: 0 !important;
right: auto !important;
}
/* Force header row into a flex container so right-side controls align horizontally */
.row.header.header-side {
position: relative;
z-index: 10;
/* Topbar strip responsive */
@media (max-width: 768px) {
.ots-topbar-strip {
height: 48px !important;
min-height: 48px !important;
max-height: 48px !important;
}
.row.header.header-side .col-sm-12 {
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
gap: 12px;
.ots-topbar-inner {
height: 48px !important;
padding: 0 12px !important;
gap: 8px !important;
}
/* Ensure notification and user li elements render inline in header */
.header-side li.dropdown.nav-item.item,
.header-side .dropdown.nav-item.item,
.header-side .user-actions > li,
.header-side .user-actions > .dropdown,
.header-side .user-actions > .nav-item {
display: inline-flex !important;
float: none !important;
vertical-align: middle !important;
margin: 0 8px !important;
.ots-topbar-strip .brand-line-top {
font-size: 13px !important;
}
.header-side .nav-link {
display: inline-flex !important;
align-items: center !important;
padding: 4px !important;
.ots-topbar-strip .brand-line-bottom {
font-size: 10px !important;
}
.header-side .ots-topbar-icon,
.header-side .nav-avatar,
.header-side img.nav-avatar {
display: inline-block !important;
vertical-align: middle !important;
.ots-topbar-strip .xibo-logo {
width: 24px !important;
height: 24px !important;
}
/* Push user actions to the right and maintain flex layout */
.row.header.header-side .meta {
flex: 0 0 auto;
.ots-topbar-action .nav-link,
.ots-topbar-hamburger {
width: 32px !important;
height: 32px !important;
}
.row.header.header-side .user-actions {
margin-left: auto;
display: flex !important;
align-items: center !important;
gap: 12px !important;
flex-shrink: 0;
.ots-topbar-action img.nav-avatar {
width: 28px !important;
height: 28px !important;
}
}
/* Ensure sidebar items are visible and above header when sidebar is collapsed */
.ots-sidebar.collapsed {
z-index: 20;
z-index: 1200;
}
.ots-sidebar.collapsed .ots-nav-icon {
@@ -676,7 +814,7 @@ nav.navbar + #content-wrapper .page-content {
.ots-topbar .nav-item.open .nav-link,
.ots-topbar .nav-item.active .nav-link {
background-color: rgba(59, 130, 246, 0.12);
background-color: rgba(59, 130, 246, 0.1);
color: var(--color-primary);
font-weight: 600;
}
@@ -684,14 +822,14 @@ nav.navbar + #content-wrapper .page-content {
.ots-topbar .dropdown-toggle::after {
content: '';
display: inline-block;
margin-left: 6px;
margin-left: 4px;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid currentColor;
opacity: 0.6;
transition: transform var(--transition-fast);
border-left: 3.5px solid transparent;
border-right: 3.5px solid transparent;
border-top: 3.5px solid currentColor;
opacity: 0.5;
transition: transform 150ms ease;
}
.ots-topbar .nav-item.open .dropdown-toggle::after {
@@ -699,14 +837,14 @@ nav.navbar + #content-wrapper .page-content {
}
.ots-topbar .dropdown-menu {
border-radius: 8px;
border-radius: 10px;
padding: 6px 0;
margin-top: 4px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
border: 1px solid var(--color-border);
background-color: var(--color-surface);
background-color: var(--color-surface-elevated);
min-width: 180px;
z-index: 1100;
z-index: 3000;
}
.ots-topbar .dropdown-item,
@@ -714,18 +852,18 @@ nav.navbar + #content-wrapper .page-content {
display: flex;
align-items: center;
gap: 8px;
border-radius: 4px;
padding: 8px 14px;
border-radius: 6px;
padding: 8px 12px;
color: var(--color-text-secondary);
font-size: 14px;
font-size: 13px;
font-weight: 500;
margin: 2px 6px;
transition: all var(--transition-fast);
margin: 1px 6px;
transition: all 150ms ease;
}
.ots-topbar .dropdown-item:hover,
.ots-topbar .dropdown-menu a:hover {
background-color: rgba(59, 130, 246, 0.1);
background-color: rgba(59, 130, 246, 0.08);
color: var(--color-primary);
}
@@ -1042,20 +1180,16 @@ nav.navbar + #content-wrapper .page-content {
margin-bottom: 16px;
}
/* When the sidebar is collapsed, hide the page header and meta area to
provide a compact layout consistent with the collapsed navigation state. */
.ots-sidebar.collapsed ~ .ots-main .page .meta,
.ots-sidebar.collapsed ~ .ots-main .page-header,
.ots-sidebar.collapsed ~ .ots-main .header-side,
.ots-sidebar.collapsed + .ots-main .page .meta,
.ots-sidebar.collapsed + .ots-main .page-header,
.ots-sidebar-collapsed .ots-main .page .meta,
.ots-sidebar-collapsed .ots-main .page-header,
body.ots-sidebar-collapsed .page .meta,
body.ots-sidebar-collapsed .page-header {
display: none !important;
margin: 0 !important;
padding: 0 !important;
/* Page header should always be visible - ensure this is displayed even when sidebar is collapsed */
body.ots-sidebar-collapsed .ots-main .page-header,
body.ots-sidebar-collapsed .page-header,
.page-header {
display: block !important;
margin-left: 0 !important;
margin-right: 0 !important;
margin-bottom: 16px !important;
padding-top: 16px !important;
padding-bottom: 16px !important;
}
.page-header h1 {
@@ -1446,6 +1580,7 @@ body .panel .panel-heading,
display: flex;
flex-direction: column;
min-width: 0;
overflow: visible !important;
}
.ots-displays-body {
@@ -1453,6 +1588,7 @@ body .panel .panel-heading,
min-width: 0;
display: flex;
flex-direction: column;
overflow: visible !important;
}
.ots-displays-body .XiboGrid {
@@ -1460,6 +1596,7 @@ body .panel .panel-heading,
min-width: 0;
display: flex;
flex-direction: column;
overflow: visible !important;
}
.ots-displays-title {
@@ -3121,6 +3258,12 @@ hr {
padding-top: 24px;
}
/* Override Bootstrap col padding inside page-content */
.page-content > .row > .col-sm-12 {
padding-left: 16px;
padding-right: 16px;
}
/* =============================================================================
NAVBAR / TOPBAR
============================================================================= */
@@ -3128,8 +3271,15 @@ hr {
.navbar,
.navbar-default {
background: var(--ots-surface-2);
border: 1px solid var(--ots-border);
box-shadow: var(--ots-shadow-sm);
border: none;
border-bottom: 1px solid var(--ots-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
height: 56px;
min-height: 56px;
padding: 0 16px;
display: flex;
align-items: center;
margin-bottom: 0;
}
.navbar-brand,
@@ -3160,7 +3310,7 @@ hr {
border: 0;
padding: 0;
margin: 0;
gap: 6px;
gap: 2px;
height: auto;
align-items: center;
}
@@ -3168,25 +3318,27 @@ hr {
.ots-topbar .nav-link {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 10px;
gap: 6px;
padding: 6px 10px;
border-radius: 6px;
color: var(--ots-text);
font-weight: 600;
transition: background var(--ots-transition), color var(--ots-transition);
font-weight: 500;
font-size: 13px;
transition: background 150ms ease, color 150ms ease;
}
.ots-topbar .nav-link:hover,
.ots-topbar .nav-item.open .nav-link,
.ots-topbar .nav-item.active .nav-link {
background: rgba(79, 140, 255, 0.18);
background: rgba(79, 140, 255, 0.12);
color: var(--ots-primary);
}
.ots-topbar .dropdown-menu {
border-radius: 12px;
padding: 8px;
box-shadow: var(--ots-shadow-md);
border-radius: 10px;
padding: 6px 0;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
border: 1px solid var(--ots-border);
}
.ots-topbar .dropdown-item,
@@ -3194,8 +3346,10 @@ hr {
display: flex;
align-items: center;
gap: 8px;
border-radius: 8px;
padding: 8px 10px;
border-radius: 6px;
padding: 8px 12px;
margin: 1px 6px;
font-size: 13px;
}
.ots-topbar-icon {

View File

@@ -24,14 +24,7 @@
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("playersoftware.add") %}
<button class="btn btn-icon btn-success" 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
@@ -67,6 +60,12 @@
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("playersoftware.add") %}
<button class="btn btn-sm btn-success" href="#" id="playerSoftwareUploadForm" title="{% trans "Upload a new Player Software file" %}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Upload Software" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -23,14 +23,7 @@
{% block title %}{{ "Playlists"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("playlist.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -78,7 +71,7 @@
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -94,7 +87,7 @@
{% 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: "200px" },
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
@@ -145,6 +138,12 @@
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("playlist.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add Playlist" %}" href="{{ url_for("playlist.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Playlist" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Resolutions"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("resolution.add") %}
<button class="btn btn-icon btn-success XiboFormButton" title="{% trans "Add a new resolution for use on layouts" %}" href="{{ url_for("resolution.add.form") }}"><i class="fa fa-plus" aria-hidden="true"></i></button>
{% endif %}
<button class="btn btn-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
@@ -65,6 +58,12 @@
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("resolution.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new resolution for use on layouts" %}" href="{{ url_for("resolution.add.form") }}"><i class="fa fa-plus" aria-hidden="true"></i> {% trans "Add Resolution" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -26,16 +26,7 @@
{% block title %}{{ "Schedule"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("schedule.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -129,7 +120,7 @@
</div>
{% set title %}{% trans "Displays" %}{% endset %}
<div class="form-group mr-1 mb-1 pagedSelect" style="min-width: 200px">
<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[]"
@@ -147,7 +138,7 @@
</div>
{% set title %}{% trans "Display Groups" %}{% endset %}
<div class="form-group mr-2 mb-1 pagedSelect" style="min-width: 200px">
<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[]"
@@ -206,6 +197,12 @@
</div>
<div class="XiboSchedule card dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("schedule.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Scheduled event" %}" href="{{ url_for("schedule.add.form") }}"><i class="fa fa-plus" aria-hidden="true"></i> {% trans "Add Event" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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">

File diff suppressed because it is too large Load Diff

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Sync Groups"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("display.syncAdd") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -87,6 +80,12 @@
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("display.syncAdd") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Sync Group" %}" href="{{ url_for("syncgroup.form.add") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Sync Group" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View 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-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 dashboard-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 dashboard-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 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Tag" %}" href="{{ url_for("tag.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Tag" %}</button>
<button class="btn btn-sm btn-primary" 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 %}

View 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-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 dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboFilter card mb-3 bg-light dashboard-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 dashboard-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 XiboFormButton" href="{{ url_for("task.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Task" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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 %}

View File

@@ -25,14 +25,7 @@
{% block title %}{{ "Templates"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("template.add") %}
<button class="btn btn-icon btn-success 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-icon btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block actionMenu %}{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
@@ -69,8 +62,8 @@
</form>
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 dashboard-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">
@@ -81,12 +74,18 @@
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none">
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("template.add") %}
<button class="btn btn-sm btn-success 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> {% trans "Add Template" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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>

View File

@@ -30,44 +30,18 @@
};
/**
* Measure sidebar width and set CSS variable for layout
* 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 collapsed = sidebar.classList.contains('collapsed');
// If called with a forced mode, use the stored defaults
const forceMode = updateSidebarWidth._forceMode || null;
const base = (forceMode === 'full') ? (window.__otsFullSidebarWidth || 256)
: (forceMode === 'collapsed') ? (window.__otsCollapsedSidebarWidth || 70)
: (collapsed ? 70 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 240);
const padding = 5;
const value = Math.max(70, Math.round(base + padding));
// Apply CSS variable used by layout and also set an inline width fallback
document.documentElement.style.setProperty('--ots-sidebar-width', `${value}px`);
try {
// Inline width helps force an immediate reflow when CSS rules/important flags interfere
// Use setProperty with 'important' so stylesheet !important rules can't override it.
sidebar.style.setProperty('width', `${value}px`, 'important');
// Force reflow to encourage the browser to apply the new sizing immediately
// eslint-disable-next-line no-unused-expressions
sidebar.offsetHeight;
} catch (err) {
try { sidebar.style.width = `${value}px`; } catch (e) { /* ignore */ }
}
// Debug logging to help identify timing/specifity issues in the wild
// No-op: CSS handles layout via body.ots-sidebar-collapsed class
if (window.__otsDebug) {
console.log('[OTS] updateSidebarWidth', { collapsed, base, value, cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width') });
const sidebar = document.querySelector('.ots-sidebar');
const collapsed = sidebar ? sidebar.classList.contains('collapsed') : false;
console.log('[OTS] updateSidebarWidth (no-op, CSS-driven)', { collapsed });
}
}
// Helper to request a forced width update
function forceSidebarWidthMode(mode) {
updateSidebarWidth._forceMode = mode; // 'full' | 'collapsed' | null
updateSidebarWidth();
updateSidebarWidth._forceMode = null;
}
/**
* Measure the sidebar header bottom and set the top padding of the nav list
* so nav items always begin below the header (logo + buttons).
@@ -106,86 +80,34 @@
}
/**
* Measure the sidebar and set an explicit left margin on the page wrapper
* so the gap between the sidebar and page content is exactly 5px.
* DISABLED: Cleanup function to remove inline styles that were forcing incorrect margins
* The sidebar layout is now controlled entirely by CSS variables and margin-left.
*/
function updateSidebarGap() {
const sidebar = document.querySelector('.ots-sidebar');
// target likely content containers in this app
// This function is intentionally left minimal.
// Spacing is now handled by CSS: .ots-main { margin-left: var(--ots-sidebar-width) }
// Removing any inline margin-left or padding-left that may have been set previously
const targets = [
document.getElementById('page-wrapper'),
document.querySelector('.ots-main'),
document.getElementById('content-wrapper'),
document.querySelector('#content')
].filter(Boolean);
if (!sidebar || !targets.length) return;
const gap = (typeof window.__otsDesiredSidebarGap !== 'undefined') ? Number(window.__otsDesiredSidebarGap) : 0; // desired gap in px (default 0)
const rect = sidebar.getBoundingClientRect();
// desired inner left padding (allows trimming space inside the content area)
const desiredInnerPadding = (typeof window.__otsDesiredPagePaddingLeft !== 'undefined') ? Number(window.__otsDesiredPagePaddingLeft) : 8;
targets.forEach(pageWrapper => {
const pageRect = pageWrapper.getBoundingClientRect();
const computed = window.getComputedStyle(pageWrapper);
const currentMargin = parseFloat(computed.marginLeft) || 0;
const currentGap = Math.round(pageRect.left - rect.right);
// Calculate how much to adjust margin-left so gap becomes `gap`.
const delta = currentGap - gap;
const newMargin = Math.max(0, Math.round(currentMargin - delta));
try {
pageWrapper.style.setProperty('margin-left', `${newMargin}px`, 'important');
pageWrapper.style.setProperty('padding-left', `${desiredInnerPadding}px`, 'important');
pageWrapper.style.removeProperty('margin-left');
pageWrapper.style.removeProperty('padding-left');
} catch (err) {
pageWrapper.style.marginLeft = `${newMargin}px`;
pageWrapper.style.paddingLeft = `${desiredInnerPadding}px`;
pageWrapper.style.marginLeft = '';
pageWrapper.style.paddingLeft = '';
}
// Also adjust common child wrapper padding if present
// Also remove from common child wrappers
try {
const inner = pageWrapper.querySelector('.page-content') || pageWrapper.querySelector('.ots-content') || pageWrapper.querySelector('.container');
if (inner) inner.style.setProperty('padding-left', `${desiredInnerPadding}px`, 'important');
} catch (err) {}
if (window.__otsDebug) console.log('[OTS] updateSidebarGap', {
target: pageWrapper.tagName + (pageWrapper.id ? '#'+pageWrapper.id : ''),
sidebarWidth: rect.width,
sidebarRight: rect.right,
pageLeft: pageRect.left,
currentGap,
newMargin
});
// Detect narrow intervening elements (visual separator) and neutralize their visuals
try {
const sampleXs = [Math.round(rect.right + 2), Math.round((rect.right + pageRect.left) / 2), Math.round(pageRect.left - 2)];
const ys = [Math.floor(window.innerHeight / 2), Math.floor(window.innerHeight / 4), Math.floor(window.innerHeight * 0.75)];
const seen = new Set();
sampleXs.forEach(x => {
ys.forEach(y => {
try {
const els = document.elementsFromPoint(x, y) || [];
els.forEach(el => {
if (!el || el === document.documentElement || el === document.body) return;
if (el === sidebar || el === pageWrapper) return;
const b = el.getBoundingClientRect();
// narrow vertical candidates between sidebar and content
if (b.left >= rect.right - 4 && b.right <= pageRect.left + 4 && b.width <= 80 && b.height >= 40) {
const id = el.tagName + (el.id ? '#'+el.id : '') + (el.className ? '.'+el.className.split(' ').join('.') : '');
if (seen.has(id)) return;
seen.add(id);
try {
el.style.setProperty('background', 'transparent', 'important');
el.style.setProperty('background-image', 'none', 'important');
el.style.setProperty('box-shadow', 'none', 'important');
el.style.setProperty('border', 'none', 'important');
el.style.setProperty('pointer-events', 'none', 'important');
if (window.__otsDebug) console.log('[OTS] neutralized intervening element', { id, rect: b });
} catch (err) {}
if (inner) {
inner.style.removeProperty('padding-left');
}
});
} catch (err) {}
});
});
} catch (err) {}
});
}
@@ -225,11 +147,65 @@
if (!sidebar) return;
// 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;
const firstFocusable = sidebar.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (firstFocusable) firstFocusable.focus(); else { sidebar.setAttribute('tabindex', '-1'); sidebar.focus(); }
document.addEventListener('keydown', escHandler);
} else {
bd.classList.remove('show');
toggleBtn.setAttribute('aria-expanded', 'false');
if (lastFocus) lastFocus.focus();
document.removeEventListener('keydown', escHandler);
}
}
updateSidebarStateClass();
});
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) {
@@ -237,8 +213,9 @@
if (isCollapsed) {
sidebar.classList.add('collapsed');
body.classList.add('ots-sidebar-collapsed');
document.documentElement.classList.add('ots-sidebar-collapsed');
updateSidebarStateClass();
updateSidebarGap();
// updateSidebarGap() disabled - use CSS variables instead
}
collapseBtn.addEventListener('click', function(e) {
@@ -246,26 +223,10 @@
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');
// Force collapsed width immediately
forceSidebarWidthMode('collapsed');
// Recalculate nav offset so items remain below header after collapse
updateSidebarNavOffset();
// Ensure page content gap is updated for collapsed width
updateSidebarGap();
// Re-run shortly after to catch any late layout changes
setTimeout(updateSidebarGap, 80);
updateSidebarStateClass();
// Debug state after toggle
try {
console.log('[OTS] collapseBtn clicked', {
nowCollapsed,
classes: sidebar.className,
inlineStyle: sidebar.getAttribute('style'),
computedWidth: getComputedStyle(sidebar).width,
cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width')
});
} catch (err) {}
});
}
@@ -274,23 +235,10 @@
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');
// Force full width when expanding
forceSidebarWidthMode('full');
// Recalculate nav offset after expanding
updateSidebarNavOffset();
// Ensure page content gap is updated for expanded width
updateSidebarGap();
setTimeout(updateSidebarGap, 80);
updateSidebarStateClass();
try {
console.log('[OTS] expandBtn clicked', {
classes: sidebar.className,
inlineStyle: sidebar.getAttribute('style'),
computedWidth: getComputedStyle(sidebar).width,
cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width')
});
} catch (err) {}
});
}
@@ -583,8 +531,7 @@
} else {
sidebar.classList.add('mobile');
}
updateSidebarWidth();
updateSidebarGap();
// updateSidebarGap() disabled - use CSS variables instead
});
}
@@ -651,7 +598,6 @@
input.placeholder = 'Search…';
input.className = 'table-search-input';
input.setAttribute('aria-label', 'Table search');
input.style.minWidth = '180px';
controls.appendChild(input);
@@ -751,6 +697,96 @@
}
}
/**
* Move DataTable row dropdown menus to <body> so they escape
* any overflow:hidden / overflow:auto ancestor containers.
*
* Xibo renders the row action button as:
* <div class="btn-group pull-right dropdown-menu-container">
* <button class="btn btn-white dropdown-toggle" data-toggle="dropdown">
* <div class="dropdown-menu dropdown-menu-right">...items...</div>
* </div>
*
* Bootstrap fires shown/hide.bs.dropdown on the toggle's parent element
* (the .btn-group). We listen on document with a selector that matches
* the actual Xibo markup.
*/
function initRowDropdowns() {
var DROPDOWN_PARENT_SEL = '.dropdown-menu-container, .btn-group';
var SCOPE_SEL = '.XiboData ' + DROPDOWN_PARENT_SEL
+ ', .XiboGrid ' + DROPDOWN_PARENT_SEL
+ ', #datatable-container ' + DROPDOWN_PARENT_SEL
+ ', .dataTables_wrapper ' + DROPDOWN_PARENT_SEL;
$(document).on('shown.bs.dropdown', SCOPE_SEL, function(e) {
var $container = $(this);
var $trigger = $container.find('[data-toggle="dropdown"]');
var $menu = $container.find('.dropdown-menu');
if (!$menu.length || !$trigger.length) return;
// Mark the menu so we can style it and find it later
$menu.addClass('ots-row-dropdown');
// Store original parent so we can put it back on close
$menu.data('ots-original-parent', $container);
// Get trigger position in viewport
var btnRect = $trigger[0].getBoundingClientRect();
// Move to body
$menu.detach().appendTo('body');
// Position below the trigger button, aligned to the right edge
var top = btnRect.bottom + 2;
var left = btnRect.right - $menu.outerWidth();
// Keep within viewport bounds
if (left < 8) left = 8;
if (top + $menu.outerHeight() > window.innerHeight - 8) {
// Open upward if no room below
top = btnRect.top - $menu.outerHeight() - 2;
}
if (top < 8) top = 8;
$menu.css({
position: 'fixed',
top: top + 'px',
left: left + 'px',
display: 'block'
});
});
// When the dropdown closes, move the menu back to its original parent
$(document).on('hide.bs.dropdown', SCOPE_SEL, function(e) {
var $container = $(this);
var $menu = $('body > .dropdown-menu.ots-row-dropdown').filter(function() {
var orig = $(this).data('ots-original-parent');
return orig && orig[0] === $container[0];
});
if ($menu.length) {
$menu.removeClass('ots-row-dropdown').css({ position: '', top: '', left: '', display: '' });
$menu.detach().appendTo($container);
}
});
// Also close any orphaned body-appended dropdown on outside click
$(document).on('click', function(e) {
var $openMenus = $('body > .dropdown-menu.ots-row-dropdown');
if (!$openMenus.length) return;
// If the click is inside the menu itself, let it through
if ($(e.target).closest('.ots-row-dropdown').length) return;
$openMenus.each(function() {
var $menu = $(this);
var $parent = $menu.data('ots-original-parent');
if ($parent && $parent.length) {
$menu.removeClass('ots-row-dropdown').css({ position: '', top: '', left: '', display: '' });
$menu.detach().appendTo($parent);
$parent.removeClass('open show');
}
});
});
}
/**
* Initialize all features when DOM is ready
*/
@@ -759,6 +795,7 @@
initSidebarSectionToggles();
initThemeToggle();
initDropdowns();
initRowDropdowns();
initSearch();
initPageInteractions();
initDataTables();
@@ -767,11 +804,11 @@
initChartSafeguard();
updateSidebarWidth();
updateSidebarNavOffset();
updateSidebarGap();
// updateSidebarGap() disabled - use CSS variables instead
var debouncedUpdate = debounce(function() {
updateSidebarNavOffset();
updateSidebarWidth();
updateSidebarGap();
// updateSidebarGap() disabled - use CSS variables instead
}, 120);
window.addEventListener('resize', debouncedUpdate);
}

View 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-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 dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboFilter card mb-3 bg-light dashboard-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 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
<button class="btn btn-sm btn-primary" 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 %}

View 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-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 dashboard-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 dashboard-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 dashboard-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 XiboFormButton" title="{% trans "Add a new User" %}" href="{{ addUserFormUrl }}"><i class="fa fa-user-plus" aria-hidden="true"></i> {% trans "Add User" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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 %}

View 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-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 dashboard-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 dashboard-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 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.isSuperAdmin() %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new User Group" %}" href="{{ url_for("group.add.form") }}"><i class="fa fa-users" aria-hidden="true"></i> {% trans "Add Group" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" 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 %}