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:
320
SIDEBAR_DESIGN_REVIEW.md
Normal file
320
SIDEBAR_DESIGN_REVIEW.md
Normal 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
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
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`);
|
||||
// No-op: CSS handles layout via body.ots-sidebar-collapsed class
|
||||
if (window.__otsDebug) {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
const collapsed = sidebar ? sidebar.classList.contains('collapsed') : false;
|
||||
console.log('[OTS] updateSidebarWidth (no-op, CSS-driven)', { collapsed });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
268
custom/otssignange/views/applications-page.twig
Normal file
268
custom/otssignange/views/applications-page.twig
Normal file
@@ -0,0 +1,268 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Applications Page
|
||||
* Based on Xibo CMS applications-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Applications"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
{% 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">
|
||||
{% if currentUser.featureEnabled("drawer") %}
|
||||
<div class="user-notif">
|
||||
{% include "authed-notification-drawer.twig" with { 'compact': true } %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="user">
|
||||
{% include "authed-user-menu.twig" with { 'compact': true } %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "authed-theme-topbar.twig" ignore missing %}
|
||||
{% endif %}
|
||||
{# Floating top-right actions: notification bell + user menu #}
|
||||
{% if not forceHide %}
|
||||
<div class="ots-page-actions">
|
||||
{% include "authed-theme-topbar.twig" ignore missing %}
|
||||
{% if currentUser.featureEnabled("drawer") %}
|
||||
<div class="ots-topbar-action">
|
||||
{% include "authed-notification-drawer.twig" with { 'compact': true } %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="ots-topbar-action">
|
||||
{% include "authed-user-menu.twig" with { 'compact': true } %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="page-content">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
159
custom/otssignange/views/fonts-page.twig
Normal file
159
custom/otssignange/views/fonts-page.twig
Normal file
@@ -0,0 +1,159 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Fonts Page
|
||||
* Based on Xibo CMS fonts-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Fonts"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
121
custom/otssignange/views/module-page.twig
Normal file
121
custom/otssignange/views/module-page.twig
Normal file
@@ -0,0 +1,121 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Module Page
|
||||
* Based on Xibo CMS module-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Modules"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-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 %}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
1253
custom/otssignange/views/settings-page.twig
Normal file
1253
custom/otssignange/views/settings-page.twig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
171
custom/otssignange/views/tag-page.twig
Normal file
171
custom/otssignange/views/tag-page.twig
Normal file
@@ -0,0 +1,171 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Tag Page
|
||||
* Based on Xibo CMS tag-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Tags"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-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 %}
|
||||
177
custom/otssignange/views/task-page.twig
Normal file
177
custom/otssignange/views/task-page.twig
Normal file
@@ -0,0 +1,177 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Task Page
|
||||
* Based on Xibo CMS task-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Tasks"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
});
|
||||
if (inner) {
|
||||
inner.style.removeProperty('padding-left');
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
98
custom/otssignange/views/transition-page.twig
Normal file
98
custom/otssignange/views/transition-page.twig
Normal file
@@ -0,0 +1,98 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - Transition Page
|
||||
* Based on Xibo CMS transition-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Transitions"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-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 %}
|
||||
444
custom/otssignange/views/user-page.twig
Normal file
444
custom/otssignange/views/user-page.twig
Normal file
@@ -0,0 +1,444 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - User Page
|
||||
* Based on Xibo CMS user-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Users"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-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 %}
|
||||
194
custom/otssignange/views/usergroup-page.twig
Normal file
194
custom/otssignange/views/usergroup-page.twig
Normal file
@@ -0,0 +1,194 @@
|
||||
{#
|
||||
/*
|
||||
* OTS Signs Theme - User Group Page
|
||||
* Based on Xibo CMS usergroup-page.twig with OTS styling
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "User Groups"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="ots-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 %}
|
||||
Reference in New Issue
Block a user