almost functional

This commit is contained in:
Matt Batchelder
2026-02-04 15:26:44 -05:00
parent 2153d3c725
commit f392e5d016
39 changed files with 10115 additions and 602 deletions

View File

@@ -0,0 +1,91 @@
{#
/**
* Copyright (C) 2026 OTS Signs
*
* About page for OTS Signs.
*/
#}
{% extends "non-authed.twig" %}
{% block title %}{{ "About"|trans }} | {% endblock %}
{% block style %}
<style type="text/css">
.about-container {
padding: 24px 30px 30px;
margin: 10px auto 20px;
background-color: #fff;
border: 1px solid #e5e5e5;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0,0,0,.05);
max-width: 720px;
}
.about-links {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 12px;
}
.about-links a {
font-size: 14px;
}
.about-meta {
margin-top: 16px;
font-size: 14px;
color: #6c757d;
}
.about-disclaimer {
margin-top: 16px;
font-size: 14px;
}
</style>
{% endblock %}
{% block header %}{% endblock %}
{% block contentClass %}{% endblock %}
{% block content %}
<a class="btn btn-icon btn-info" href="{{ url_for("home") }}" title="{% trans "Home" %}"><i class="fa fa-home"></i></a>
<div class="about-container">
<h1>
{% trans "About" %}
<a href="https://ots-signs.com" target="_blank" rel="noopener noreferrer">OTS Signs</a>
</h1>
<p>
{% trans "An" %}
<a href="https://oribi-tech.com" target="_blank" rel="noopener noreferrer">Oribi Technology Services</a>
{% trans "product." %}
</p>
<p class="text-muted">{% trans "OTS Signs provides a compact, focused admin UI and proxy for Xibo CMS" %}</p>
{% set appVersion = version|default("dev") %}
{% set appEnvironment = appEnvironment|default(environment|default("local")) %}
{% set commitSha = revision|default("") %}
<div class="about-meta">
<div>{% trans "Version" %}: {{ appVersion }}</div>
<div>{% trans "Environment" %}: {{ appEnvironment }}</div>
{% if commitSha %}
<div>{% trans "Commit" %}: {{ commitSha|slice(0, 7) }}</div>
{% endif %}
</div>
<div class="about-links">
<a href="/privacy">{% trans "Privacy Policy" %}</a>
<a href="/terms">{% trans "Terms of Service" %}</a>
<a href="/open-source-licenses">{% trans "Open Source Licenses" %}</a>
</div>
<div class="about-disclaimer">
<strong>{% trans "Disclaimer:" %}</strong>
{% trans "OTS Signs is an independent product developed by Oribi Technology Services. It is not affiliated with or endorsed by the Xibo project or its maintainers. Xibo is a trademark of Xibo Digital Signage Ltd. Use of Xibo APIs is subject to their terms and conditions." %}
<div class="mt-2">
<a href="https://github.com/xibosignage/xibo" target="_blank" rel="noopener noreferrer">{% trans "Xibo CMS on GitHub" %}</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,55 @@
{#
/**
* Copyright (C) 2026 OTS Signs
*
* About dialog content for OTS Signs.
*/
#}
{% extends "form-base.twig" %}
{% block formTitle %}{% trans "About" %}{% endblock %}
{% block formButtons %}
{% trans "Close" %}, XiboDialogClose()
{% endblock %}
{% block formHtml %}
<div class="about-container">
<h2>
{% trans "About" %}
<a href="https://ots-signs.com" target="_blank" rel="noopener noreferrer">OTS Signs</a>
</h2>
<p>
{% trans "An" %}
<a href="https://oribi-tech.com" target="_blank" rel="noopener noreferrer">Oribi Technology Services</a>
{% trans "product." %}
</p>
<p class="text-muted">{% trans "OTS Signs provides a compact, focused interface and hosting for Xibo CMS" %}</p>
{% set appVersion = version|default("dev") %}
{% set appEnvironment = appEnvironment|default(environment|default("local")) %}
{% set commitSha = revision|default("") %}
<div class="about-meta">
<div>{% trans "Version" %}: {{ appVersion }}</div>
<div>{% trans "Environment" %}: {{ appEnvironment }}</div>
{% if commitSha %}
<div>{% trans "Commit" %}: {{ commitSha|slice(0, 7) }}</div>
{% endif %}
</div>
<div class="about-links">
<a href="/privacy">{% trans "Privacy Policy" %}</a>
<a href="/terms">{% trans "Terms of Service" %}</a>
<a href="/open-source-licenses">{% trans "Open Source Licenses" %}</a>
</div>
<div class="about-disclaimer">
<strong>{% trans "Disclaimer:" %}</strong>
{% trans "OTS Signs is an independent product developed by Oribi Technology Services. It is not affiliated with or endorsed by the Xibo project or its maintainers. Xibo is a trademark of Xibo Digital Signage Ltd. Use of Xibo APIs is subject to their terms and conditions." %}
<div class="mt-2">
<a href="https://github.com/xibosignage/xibo" target="_blank" rel="noopener noreferrer">{% trans "Xibo CMS on GitHub" %}</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,166 +1,220 @@
<div id="sidebar-wrapper" class="ots-sidebar-wrapper">
<ul class="sidebar ots-sidebar">
<li class="sidebar-main">
{#
OTS Signage Theme override
Based on Xibo CMS default authed-sidebar.twig (master branch)
Applied OTS sidebar styling
#}
<div id="sidebar-wrapper" class="ots-sidebar">
<div class="sidebar-header">
<a class="brand-link" href="{{ url_for("home") }}">
<span class="brand-icon">
<img class="brand-logo" src="{{ theme.uri("img/xibologo.png") }}" alt="{% trans "Logo" %}">
</span>
<span class="brand-text">OTS Signs</span>
</a>
<button class="sidebar-collapse-btn" type="button" aria-label="{% trans "Collapse sidebar" %}">
<i class="fa fa-chevron-left" aria-hidden="true"></i>
</button>
</div>
<div class="sidebar-content">
<ul class="sidebar ots-sidebar-nav">
<li class="sidebar-list">
<a href="{{ url_for("home") }}">
<span class="ots-nav-icon fa fa-home" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Dashboard" %}</span>
</a>
</li>
{% if currentUser.featureEnabled("schedule.view") %}
<li class="sidebar-list">
<a href="{{ url_for("schedule.view") }}">
{% set scheduleCount = currentUser.featureEnabledCount(["schedule.view", "daypart.view"]) %}
{% if scheduleCount > 0 %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="true">
<span class="ots-nav-icon fa fa-calendar" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Schedule" %}</span>
<span class="ots-nav-text">{% trans "Scheduling" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
{% if currentUser.featureEnabled("daypart.view") %}
<li class="sidebar-list">
<a href="{{ url_for("daypart.view") }}">
<span class="ots-nav-icon fa fa-clock-o" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Dayparts" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("schedule.view") %}
<li class="sidebar-list">
<a href="{{ url_for("schedule.view") }}">
<span class="ots-nav-icon fa fa-calendar-check-o" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Schedules" %}</span>
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
{% if currentUser.featureEnabled("daypart.view") %}
<li class="sidebar-list">
<a href="{{ url_for("daypart.view") }}">
<span class="ots-nav-icon fa fa-clock" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Dayparting" %}</span>
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="true">
<span class="ots-nav-icon fa fa-picture-o" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Media" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
{% if currentUser.featureEnabled("library.view") %}
<li class="sidebar-list">
<a href="{{ url_for("library.view") }}">
<span class="ots-nav-icon fa fa-image" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Library" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("playlist.view") %}
<li class="sidebar-list">
<a href="{{ url_for("playlist.view") }}">
<span class="ots-nav-icon fa fa-list" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Playlists" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("dataset.view") %}
<li class="sidebar-list">
<a href="{{ url_for("dataset.view") }}">
<span class="ots-nav-icon fa fa-database" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "DataSets" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("menuBoard.view") %}
<li class="sidebar-list">
<a href="{{ url_for("menuBoard.view") }}">
<span class="ots-nav-icon fa fa-cutlery" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Menu Boards" %}</span>
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Design" %}</a></li>
{% if currentUser.featureEnabled("campaign.view") %}
<li class="sidebar-list">
<a href="{{ url_for("campaign.view") }}">
<span class="ots-nav-icon fa fa-bullhorn" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Campaigns" %}</span>
</a>
</li>
{% endif %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="true">
<span class="ots-nav-icon fa fa-paint-brush" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Design" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
{% if currentUser.featureEnabled("campaign.view") %}
<li class="sidebar-list">
<a href="{{ url_for("campaign.view") }}">
<span class="ots-nav-icon fa fa-bullhorn" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Campaigns" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("layout.view") %}
<li class="sidebar-list">
<a href="{{ url_for("layout.view") }}">
<span class="ots-nav-icon fa fa-columns" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Layouts" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("layout.view") %}
<li class="sidebar-list">
<a href="{{ url_for("layout.view") }}">
<span class="ots-nav-icon fa fa-columns" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Layouts" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("template.view") %}
<li class="sidebar-list">
<a href="{{ url_for("template.view") }}">
<span class="ots-nav-icon fa fa-clone" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Templates" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("template.view") %}
<li class="sidebar-list">
<a href="{{ url_for("template.view") }}">
<span class="ots-nav-icon fa fa-clone" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Templates" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("resolution.view") %}
<li class="sidebar-list">
<a href="{{ url_for("resolution.view") }}">
<span class="ots-nav-icon fa fa-expand" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Resolutions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("resolution.view") %}
<li class="sidebar-list">
<a href="{{ url_for("resolution.view") }}">
<span class="ots-nav-icon fa fa-expand" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Resolutions" %}</span>
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view", "playersoftware.view", "command.view", "display.syncView"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Library" %}</a></li>
{% if currentUser.featureEnabled("playlist.view") %}
<li class="sidebar-list">
<a href="{{ url_for("playlist.view") }}">
<span class="ots-nav-icon fa fa-list" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Playlists" %}</span>
</a>
</li>
{% endif %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="true">
<span class="ots-nav-icon fa fa-desktop" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Displays" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
{% if currentUser.featureEnabled("displays.view") %}
<li class="sidebar-list">
<a href="{{ url_for("display.view") }}">
<span class="ots-nav-icon fa fa-desktop" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "All Displays" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("library.view") %}
<li class="sidebar-list">
<a href="{{ url_for("library.view") }}">
<span class="ots-nav-icon fa fa-photo" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Media" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("displaygroup.view") %}
<li class="sidebar-list">
<a href="{{ url_for("displaygroup.view") }}">
<span class="ots-nav-icon fa fa-object-group" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Screen Groups" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("dataset.view") %}
<li class="sidebar-list">
<a href="{{ url_for("dataset.view") }}">
<span class="ots-nav-icon fa fa-database" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "DataSets" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("display.syncView") %}
<li class="sidebar-list">
<a href="{{ url_for("syncgroup.view") }}">
<span class="ots-nav-icon fa fa-link" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Sync Groups" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("menuBoard.view") %}
<li class="sidebar-list">
<a href="{{ url_for("menuBoard.view") }}">
<span class="ots-nav-icon fa fa-th-large" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Menu Boards" %}</span>
</a>
</li>
{% endif %}
{% endif %}
{% if currentUser.featureEnabled("displayprofile.view") %}
<li class="sidebar-list">
<a href="{{ url_for("displayprofile.view") }}">
<span class="ots-nav-icon fa fa-cog" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Display Settings" %}</span>
</a>
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view", "playersoftware.view", "command.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Displays" %}</a></li>
{% if currentUser.featureEnabled("displays.view") %}
<li class="sidebar-list">
<a href="{{ url_for("display.view") }}">
<span class="ots-nav-icon fa fa-desktop" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Displays" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("playersoftware.view") %}
<li class="sidebar-list">
<a href="{{ url_for("playersoftware.view") }}">
<span class="ots-nav-icon fa fa-download" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Player Versions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("displaygroup.view") %}
<li class="sidebar-list">
<a href="{{ url_for("displaygroup.view") }}">
<span class="ots-nav-icon fa fa-object-group" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Display Groups" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("display.syncView") %}
<li class="sidebar-list">
<a href="{{ url_for("syncgroup.view") }}">
<span class="ots-nav-icon fa fa-link" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Sync Groups" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("displayprofile.view") %}
<li class="sidebar-list">
<a href="{{ url_for("displayprofile.view") }}">
<span class="ots-nav-icon fa fa-sliders" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Display Settings" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("playersoftware.view") %}
<li class="sidebar-list">
<a href="{{ url_for("playersoftware.view") }}">
<span class="ots-nav-icon fa fa-download" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Player Versions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("command.view") %}
<li class="sidebar-list">
<a href="{{ url_for("command.view") }}">
<span class="ots-nav-icon fa fa-terminal" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Commands" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("command.view") %}
<li class="sidebar-list">
<a href="{{ url_for("command.view") }}">
<span class="ots-nav-icon fa fa-terminal" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Commands" %}</span>
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
@@ -169,188 +223,237 @@
{% set userMenuViewable = false %}
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["usergroup.view", "module.view", "transition.view", "task.view"]) %}
{% set countViewable = currentUser.featureEnabledCount(["usergroup.view", "module.view", "transition.view", "task.view", "tag.view", "font.view"]) %}
{% if countViewable > 0 or userMenuViewable %}
<li class="sidebar-title"><a>{% trans "Administration" %}</a></li>
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="true">
<span class="ots-nav-icon fa fa-cog" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Settings" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
{% if userMenuViewable %}
<li class="sidebar-list">
<a href="{{ url_for("user.view") }}">
<span class="ots-nav-icon fa fa-user" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Users" %}</span>
</a>
</li>
{% endif %}
{% if userMenuViewable %}
<li class="sidebar-list">
<a href="{{ url_for("user.view") }}">
<span class="ots-nav-icon fa fa-users" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Users" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("usergroup.view") %}
<li class="sidebar-list">
<a href="{{ url_for("group.view") }}">
<span class="ots-nav-icon fa fa-users" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "User Groups" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("usergroup.view") %}
<li class="sidebar-list">
<a href="{{ url_for("group.view") }}">
<span class="ots-nav-icon fa fa-users-cog" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "User Groups" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.isSuperAdmin() %}
<li class="sidebar-list">
<a href="{{ url_for("admin.view") }}">
<span class="ots-nav-icon fa fa-sliders" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Settings" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.isSuperAdmin() %}
<li class="sidebar-list">
<a href="{{ url_for("admin.view") }}">
<span class="ots-nav-icon fa fa-wrench" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Settings" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.isSuperAdmin() %}
<li class="sidebar-list">
<a href="{{ url_for("application.view") }}">
<span class="ots-nav-icon fa fa-puzzle-piece" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Applications" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.isSuperAdmin() %}
<li class="sidebar-list">
<a href="{{ url_for("application.view") }}">
<span class="ots-nav-icon fa fa-th" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Applications" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("module.view") %}
<li class="sidebar-list">
<a href="{{ url_for("module.view") }}">
<span class="ots-nav-icon fa fa-cubes" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Modules" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("module.view") %}
<li class="sidebar-list">
<a href="{{ url_for("module.view") }}">
<span class="ots-nav-icon fa fa-puzzle-piece" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Modules" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("transition.view") %}
<li class="sidebar-list">
<a href="{{ url_for("transition.view") }}">
<span class="ots-nav-icon fa fa-random" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Transitions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("transition.view") %}
<li class="sidebar-list">
<a href="{{ url_for("transition.view") }}">
<span class="ots-nav-icon fa fa-exchange" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Transitions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("task.view") %}
<li class="sidebar-list">
<a href="{{ url_for("task.view") }}">
<span class="ots-nav-icon fa fa-tasks" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Tasks" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("task.view") %}
<li class="sidebar-list">
<a href="{{ url_for("task.view") }}">
<span class="ots-nav-icon fa fa-tasks" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Tasks" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("tag.view") %}
<li class="sidebar-list">
<a href="{{ url_for("tag.view") }}">
<span class="ots-nav-icon fa fa-tags" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Tags" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("tag.view") %}
<li class="sidebar-list">
<a href="{{ url_for("tag.view") }}">
<span class="ots-nav-icon fa fa-tags" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Tags" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.isSuperAdmin() %}
<li class="sidebar-list">
<a href="{{ url_for("folders.view") }}">
<span class="ots-nav-icon fa fa-folder-open" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Folders" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.isSuperAdmin() %}
<li class="sidebar-list">
<a href="{{ url_for("folders.view") }}">
<span class="ots-nav-icon fa fa-folder" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Folders" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("font.view") %}
<li class="sidebar-list">
<a href="{{ url_for("font.view") }}">
<span class="ots-nav-icon fa fa-font" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Fonts" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("font.view") %}
<li class="sidebar-list">
<a href="{{ url_for("font.view") }}">
<span class="ots-nav-icon fa fa-font" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Fonts" %}</span>
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["report.view", "report.scheduling", "report.saving"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Reporting" %}</a></li>
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="true">
<span class="ots-nav-icon fa fa-bar-chart" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Reporting" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
{% if currentUser.featureEnabled("report.view") %}
<li class="sidebar-list">
<a href="{{ url_for("report.view") }}">
<span class="ots-nav-icon fa fa-file-text-o" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "All Reports" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("report.view") %}
<li class="sidebar-list">
<a href="{{ url_for("report.view") }}">
<span class="ots-nav-icon fa fa-file-alt" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "All Reports" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("report.scheduling") %}
<li class="sidebar-list">
<a href="{{ url_for("reportschedule.view") }}">
<span class="ots-nav-icon fa fa-calendar-alt" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Report Schedules" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("report.saving") %}
<li class="sidebar-list">
<a href="{{ url_for("savedreport.view") }}">
<span class="ots-nav-icon fa fa-save" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Saved Reports" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("report.scheduling") %}
<li class="sidebar-list">
<a href="{{ url_for("reportschedule.view") }}">
<span class="ots-nav-icon fa fa-calendar" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Report Schedules" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("report.saving") %}
<li class="sidebar-list">
<a href="{{ url_for("savedreport.view") }}">
<span class="ots-nav-icon fa fa-floppy-o" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Saved Reports" %}</span>
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["log.view", "sessions.view", "auditlog.view", "fault.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Advanced" %}</a></li>
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="true">
<span class="ots-nav-icon fa fa-shield" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Advanced" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
{% if currentUser.featureEnabled("log.view") %}
<li class="sidebar-list">
<a href="{{ url_for("log.view") }}">
<span class="ots-nav-icon fa fa-list-alt" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Log" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("log.view") %}
<li class="sidebar-list">
<a href="{{ url_for("log.view") }}">
<span class="ots-nav-icon fa fa-list-alt" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Log" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("sessions.view") %}
<li class="sidebar-list">
<a href="{{ url_for("sessions.view") }}">
<span class="ots-nav-icon fa fa-user-secret" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Sessions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("sessions.view") %}
<li class="sidebar-list">
<a href="{{ url_for("sessions.view") }}">
<span class="ots-nav-icon fa fa-history" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Sessions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("auditlog.view") %}
<li class="sidebar-list">
<a href="{{ url_for("auditlog.view") }}">
<span class="ots-nav-icon fa fa-clipboard" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Audit Trail" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("auditlog.view") %}
<li class="sidebar-list">
<a href="{{ url_for("auditlog.view") }}">
<span class="ots-nav-icon fa fa-clipboard-list" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Audit Trail" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("fault.view") %}
<li class="sidebar-list">
<a href="{{ url_for("fault.view") }}">
<span class="ots-nav-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Report Fault" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("fault.view") %}
<li class="sidebar-list">
<a href="{{ url_for("fault.view") }}">
<span class="ots-nav-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Report Fault" %}</span>
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["developer.edit"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Developer" %}</a></li>
{% if currentUser.featureEnabled("developer.edit") %}
<li class="sidebar-list">
<a href="{{ url_for("developer.templates.view") }}">
<span class="ots-nav-icon fa fa-code" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Module Templates" %}</span>
</a>
</li>
{% endif %}
<li class="sidebar-group">
<a class="sidebar-group-toggle" href="#" aria-expanded="true">
<span class="ots-nav-icon fa fa-code" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Developer" %}</span>
<span class="sidebar-group-caret fa fa-chevron-down" aria-hidden="true"></span>
</a>
<ul class="sidebar-submenu">
{% if currentUser.featureEnabled("developer.edit") %}
<li class="sidebar-list">
<a href="{{ url_for("developer.templates.view") }}">
<span class="ots-nav-icon fa fa-code" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Module Templates" %}</span>
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
</ul>
</ul>
</div>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="user-avatar-lg" aria-hidden="true">
{{ currentUser.userName|slice(0, 1)|upper }}
</div>
<div class="user-details">
<div class="user-role">
<i class="fa fa-shield" aria-hidden="true"></i>
{% if currentUser.isSuperAdmin() %}
{% trans "Super Admin" %}
{% else %}
{% trans "User" %}
{% endif %}
</div>
<div class="user-name">{{ currentUser.userName }}</div>
</div>
</div>
<button class="sidebar-theme-toggle" type="button" aria-label="{% trans "Toggle theme" %}">
<i class="fa fa-sun-o" aria-hidden="true"></i>
</button>
</div>
</div>

View File

@@ -0,0 +1,135 @@
{#
/**
* Copyright (C) 2020-2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base.twig" %}
{% block content %}
{% set horizontalNav = currentUser.getOptionValue("navigationMenuPosition", theme.getSetting("NAVIGATION_MENU_POSITION", "vertical")) == "horizontal" %}
{% if not hideNavigation %}
{% set hideNavigation = currentUser.getOptionValue("hideNavigation", "0") %}
{% endif %}
<div {% if hideNavigation == "0" and not horizontalNav and not forceHide %}id="page-wrapper"{% endif %} class="active">
{% if hideNavigation == "0" and not forceHide %}
{% if horizontalNav %}
<nav class="navbar navbar-default navbar-expand-lg">
<a class="navbar-brand xibo-logo-container" href="#">
<img class="xibo-logo" src="{{ theme.uri("img/xibologo.png") }}">
<span class="xibo-logo-text">Xibo</span>
</a>
<!-- Brand and toggle get grouped for better mobile display -->
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbar-collapse-1" aria-controls="navbarNav" aria-expanded="false">
<span class="fa fa-bars"></span>
</button>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="navbar-collapse collapse justify-content-between" id="navbar-collapse-1">
{% include "authed-topbar.twig" %}
<ul class="nav navbar-nav navbar-right">
{% include "authed-theme-topbar.twig" ignore missing %}
{% if currentUser.featureEnabled("drawer") %}
{% include "authed-notification-drawer.twig" %}
{% endif %}
{% include "authed-user-menu.twig" %}
</ul>
</div><!-- /.navbar-collapse -->
</nav>
{% else %}
<div class="navbar-collapse navbar-collapse-side collapse" id="navbar-collapse-1">
{% include "authed-sidebar.twig" %}
</div>
{% endif %}
{% endif %}
<div id="content-wrapper">
<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 pull-right">
{% include "authed-user-menu.twig" %}
</div>
{% if currentUser.featureEnabled("drawer") %}
<div class="user user-notif pull-right">
{% include "authed-notification-drawer.twig" %}
</div>
{% endif %}
{% include "authed-theme-topbar.twig" ignore missing %}
{% endif %}
</div>
</div>
{% endif %}
<div class="row">
<div class="col-sm-12">
{% block actionMenu %}{% endblock %}
{% if settings.INSTANCE_SUSPENDED == "partial" %}
<div class="alert alert-warning">{{ "CMS suspended. Displays will show cached content. Please contact your administrator."|trans }}</div>
{% endif %}
{% block pageContent %}{% endblock %}
</div>
</div>
<div class="row">
<div class="col-sm-12">
{% block pageFooter %}{% endblock %}
</div>
</div>
</div>
</div>
</div>
{% set helpLinks = helpService.getLinksForPage(route) %}
{% set faultViewEnabled = currentUser.featureEnabled("fault.view") %}
{# Hide in mobile view (sm/<768px) #}
<div id="help-pane" class="d-none d-md-flex help-pane"
data-help-links="{{ helpLinks|json_encode }}"
data-url-help-landing-page={{ helpService.getLandingPage() }}
data-fault-view-enabled={{faultViewEnabled}}
data-fault-view-url={{ url_for("fault.view") }}
>
<div class="help-pane-container" style="display: none;">
</div>
<div class="help-pane-btn">
<i class="fas fa-question"></i>
</div>
</div>
{% endblock %}
{% block javaScriptTemplates %}
{# File upload templates and scripts #}
{% include "include-file-upload.twig" %}
{% endblock %}

View File

@@ -0,0 +1,186 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Campaigns"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Campaigns" %}</h1>
<p class="text-muted">{% trans "Manage your campaigns and ad campaigns." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="campaignView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Campaigns" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-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('name', title) }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{% set title %}{% trans "Layouts" %}{% endset %}
{% set values = [{id: 0, value: ""}, {id: 2, value: "Yes"}, {id: 1, value: "No"}] %}
{{ inline.dropdown("hasLayouts", "single", title, 0, values, "id", "value") }}
{{ inline.hidden("folderId") }}
{% set title %}{% trans "Layout ID" %}{% endset %}
{{ inline.number("layoutId", title, layoutId) }}
{% if currentUser.featureEnabled('ad.campaign') %}
{% set title %}{% trans "Type" %}{% endset %}
{% set options = [
{ id: null, name: "" },
{ id: "list", name: "Layout list"|trans },
{ id: "ad", name: "Ad Campaign"|trans }
] %}
{{ inline.dropdown("type", "single", title, "both", options, "id", "name", helpText) }}
{% endif %}
{% set title %}{% trans "Cycle Based Playback" %}{% endset %}
{% set enabled %}{% trans "Enabled" %}{% endset %}
{% set disabled %}{% trans "Disabled" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: 0, option: disabled},
{ optionid: 1, option: enabled}
] %}
{{ inline.dropdown("cyclePlaybackEnabled", "single", title, "", options, "optionid", "option") }}
</form>
</div>
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none">
<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">
<table id="campaigns" class="table table-striped" data-content-type="campaign" data-content-id-name="campaignId" data-state-preference-name="campaignGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "Name" %}</th>
{% if currentUser.featureEnabled('ad.campaign') %}
<th>{% trans "Type" %}</th>
<th>{% trans "Start Date" %}</th>
<th>{% trans "End Date" %}</th>
{% endif %}
<th>{% trans "# Layouts" %}</th>
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
<th>{% trans "Duration" %}</th>
<th>{% trans "Cycle based Playback" %}</th>
<th>{% trans "Play Count" %}</th>
{% if currentUser.featureEnabled('ad.campaign') %}
<th>{% trans "Target Type" %}</th>
<th>{% trans "Target" %}</th>
<th>{% trans "Plays" %}</th>
<th>{% trans "Spend" %}</th>
<th>{% trans "Impressions" %}</th>
{% endif %}
<th>{% trans "Ref 1" %}</th>
<th>{% trans "Ref 2" %}</th>
<th>{% trans "Ref 3" %}</th>
<th>{% trans "Ref 4" %}</th>
<th>{% trans "Ref 5" %}</th>
<th>{% trans "Created At" %}</th>
<th>{% trans "Modified At" %}</th>
<th>{% trans "Modified By" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
{# Initialise JS variables and translations #}
<script type="text/javascript" nonce="{{ cspNonce }}" defer>
{# JS variables #}
var campaignSearchURL = "{{ url_for('campaign.search') }}";
var layoutSearchURL = "{{ url_for('layout.search') }}";
var folderViewEnabled = "{{ currentUser.featureEnabled('folder.view') }}";
var adCampaignEnabled = "{{ currentUser.featureEnabled('ad.campaign') }}";
var taggingEnabled = "{{ currentUser.featureEnabled('tag.tagging') }}";
{# Custom translations #}
var campaignPageTrans = {
list: "{% trans "List" %}",
ad: "{% trans "Ad" %}",
plays: "{% trans "Plays" %}",
budget: "{% trans "Budget" %}",
impressions: "{% trans "Impressions" %}",
};
</script>
{# Add page source code bundle #}
<script src="{{ theme.rootUri() }}dist/pages/campaign-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
{% endblock %}

View File

@@ -0,0 +1,162 @@
{#
/**
* Copyright (C) 2020-2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Commands"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Commands" %}</h1>
<p class="text-muted">{% trans "Create and manage commands for Displays." %}</p>
</div>
<div class="widget 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 Commands" %}</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('command', title) }}
{% set title %}{% trans "Code" %}{% endset %}
{{ inline.inputNameGrid('code', title, null, 'useRegexForCode', 'logicalOperatorCode') }}
</form>
</div>
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<table id="commands" class="table table-striped" data-state-preference-name="commandGrid">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Code" %}</th>
<th>{% trans "Available On" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Sharing" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var table = $("#commands").DataTable({ "language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[ 1, "asc"]],
ajax: {
"url": "{{ url_for("command.search") }}",
"data": function(d) {
$.extend(d, $("#commands").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{ "data": "command", "render": dataTableSpacingPreformatted , responsivePriority: 2},
{ "data": "code" , responsivePriority: 2},
{
"data": "availableOn",
responsivePriority: 3,
"render": function(data, type) {
if (type !== "display")
return data;
var returnData = '';
if (typeof data !== undefined && data != null) {
var arrayOfTags = data.split(',');
returnData += '<div class="permissionsDiv">';
for (var i = 0; i < arrayOfTags.length; i++) {
var name = arrayOfTags[i];
if (name !== '') {
returnData += '<li class="badge ' + ((name === 'lg') ? '' : 'capitalize') + '">' + name.replace("lg", "webOS").replace("sssp", "Tizen") + '</span></li>'
}
}
returnData += '</div>';
}
return returnData;
}
},
{ "data": "description", responsivePriority: 3 },
{
"data": "groupsWithPermissions",
responsivePriority: 3,
"render": dataTableCreatePermissions
},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#commands_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
</script>
{% endblock %}

View File

@@ -32,6 +32,32 @@
<p class="text-muted">{% trans "Overview of your digital signage network" %}</p>
</div>
<div class="quick-actions-grid">
<h3 class="section-title">{% trans "Quick Actions" %}</h3>
<div class="action-cards">
{% if currentUser.featureEnabled("schedule.view") %}
<a class="action-card action-card--modern dashboard-card" href="{{ url_for("schedule.view") }}">
<div class="action-icon"><i class="fa fa-calendar"></i></div>
<div class="action-label">{% trans "Create Schedule" %}</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("displays.view") %}
<a class="action-card action-card--modern dashboard-card" href="{{ url_for("display.view") }}">
<div class="action-icon"><i class="fa fa-desktop"></i></div>
<div class="action-label">{% trans "Manage Displays" %}</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
<a class="action-card action-card--modern dashboard-card" href="{{ url_for("user.view") }}">
<div class="action-icon"><i class="fa fa-user-plus"></i></div>
<div class="action-label">{% trans "Add User" %}</div>
</a>
{% endif %}
</div>
</div>
<div class="kpi-section">
<div class="kpi-card dashboard-card kpi-card--modern">
<div class="kpi-header">
@@ -100,10 +126,10 @@
<div class="dashboard-panels">
<div class="widget dashboard-chart-card dashboard-chart-card--bandwidth dashboard-card">
<div class="widget-title dashboard-chart-header">
<div class="dashboard-chart-title">
<span class="dashboard-chart-icon"><i class="fa fa-cloud-download"></i></span>
<div>
<div class="dashboard-chart-header">
<div class="dashboard-chart-info">
<div class="dashboard-chart-icon"><i class="fa fa-arrow-down" aria-hidden="true"></i></div>
<div class="dashboard-chart-meta">
<div class="dashboard-chart-heading">
{% if xmdsLimit != "" %}
{% trans %}Bandwidth Usage{% endtrans %}
@@ -120,10 +146,15 @@
</div>
</div>
</div>
{% if currentUser.featureEnabled("displays.reporting") %}
<a class="dashboard-chart-link" href="/report/form/bandwidth">{% trans "More Statistics" %}</a>
{% endif %}
<div class="clearfix"></div>
<div class="dashboard-chart-actions">
{% if currentUser.featureEnabled("displays.reporting") %}
<a class="dashboard-chart-link" href="/report/form/bandwidth">{% trans "More Statistics" %}</a>
{% endif %}
<div class="dashboard-chart-toggle" data-chart="bandwidthChart">
<button type="button" class="dashboard-chart-toggle-button is-active" data-chart-type="line" aria-label="{% trans 'Line chart' %}"><i class="fa fa-chart-line" aria-hidden="true"></i></button>
<button type="button" class="dashboard-chart-toggle-button" data-chart-type="bar" aria-label="{% trans 'Bar chart' %}"><i class="fa fa-chart-bar" aria-hidden="true"></i></button>
</div>
</div>
</div>
<div class="widget-body dashboard-chart-body">
<div class="dashboard-chart-canvas">
@@ -133,10 +164,10 @@
</div>
<div class="widget dashboard-chart-card dashboard-chart-card--library dashboard-card">
<div class="widget-title dashboard-chart-header">
<div class="dashboard-chart-title">
<span class="dashboard-chart-icon"><i class="fa fa-tasks"></i></span>
<div>
<div class="dashboard-chart-header">
<div class="dashboard-chart-info">
<div class="dashboard-chart-icon"><i class="fa fa-hdd-o" aria-hidden="true"></i></div>
<div class="dashboard-chart-meta">
<div class="dashboard-chart-heading">
{% trans "Library Usage" %}
</div>
@@ -149,7 +180,13 @@
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="dashboard-chart-actions">
<div class="dashboard-chart-toggle" data-chart="libraryChart">
<button type="button" class="dashboard-chart-toggle-button is-active" data-chart-type="pie" aria-label="{% trans 'Pie chart' %}"><i class="fa fa-pie-chart" aria-hidden="true"></i></button>
<button type="button" class="dashboard-chart-toggle-button" data-chart-type="doughnut" aria-label="{% trans 'Doughnut chart' %}"><i class="fa fa-circle-o" aria-hidden="true"></i></button>
<button type="button" class="dashboard-chart-toggle-button" data-chart-type="bar" aria-label="{% trans 'Bar chart' %}"><i class="fa fa-chart-bar" aria-hidden="true"></i></button>
</div>
</div>
</div>
<div class="widget-body dashboard-chart-body">
<div class="dashboard-chart-canvas">
@@ -161,8 +198,13 @@
<div class="dashboard-panels">
<div class="panel dashboard-card">
<div class="panel-header">
<h3>{% trans "Display Activity" %}</h3>
<div class="dashboard-chart-header">
<div class="dashboard-chart-info">
<div class="dashboard-chart-icon"><i class="fa fa-desktop" aria-hidden="true"></i></div>
<div class="dashboard-chart-meta">
<div class="dashboard-chart-heading">{% trans "Display Activity" %}</div>
</div>
</div>
</div>
<div class="panel-body">
<div class="table-responsive">
@@ -181,8 +223,13 @@
</div>
<div class="panel dashboard-card">
<div class="panel-header">
<h3>{% trans "Latest News" %}</h3>
<div class="dashboard-chart-header">
<div class="dashboard-chart-info">
<div class="dashboard-chart-icon"><i class="fa fa-newspaper-o" aria-hidden="true"></i></div>
<div class="dashboard-chart-meta">
<div class="dashboard-chart-heading">{% trans "Latest News" %}</div>
</div>
</div>
</div>
<div class="panel-body">
{% if latestNews|length > 0 %}
@@ -202,51 +249,97 @@
</div>
<div class="dashboard-panels">
<div class="panel dashboard-card">
<div class="panel-header">
<h3>{% trans "Display Status" %}</h3>
<div class="widget dashboard-chart-card dashboard-card">
<div class="dashboard-chart-header">
<div class="dashboard-chart-info">
<div class="dashboard-chart-icon"><i class="fa fa-circle-o" aria-hidden="true"></i></div>
<div class="dashboard-chart-meta">
<div class="dashboard-chart-heading">{% trans "Display Status" %}</div>
<div class="dashboard-chart-subtitle">{% trans "Click on the chart for a breakdown" %}</div>
</div>
</div>
<div class="dashboard-chart-actions">
<div class="dashboard-chart-toggle" data-chart="displayStatusChart">
<button type="button" class="dashboard-chart-toggle-button is-active" data-chart-type="doughnut" aria-label="{% trans 'Doughnut chart' %}"><i class="fa fa-circle-o" aria-hidden="true"></i></button>
<button type="button" class="dashboard-chart-toggle-button" data-chart-type="pie" aria-label="{% trans 'Pie chart' %}"><i class="fa fa-pie-chart" aria-hidden="true"></i></button>
</div>
</div>
</div>
<div class="panel-body" style="overflow: hidden;">
<div style="text-align: center; height: 10px; margin-bottom: 5px"><span>{% trans "Click on the chart for a breakdown" %}</span></div>
<div style="position: relative; height: 235px">
<canvas id="displayStatusChart" style="clear:both;"></canvas>
<div class="widget-body dashboard-chart-body">
<div class="dashboard-chart-canvas">
<canvas id="displayStatusChart" style="clear:both;" aria-label="{% trans "Display Status" %}" role="img"></canvas>
</div>
</div>
</div>
<div class="panel dashboard-card">
<div class="panel-header">
<h3>{% trans "Display Content Status" %}</h3>
<div class="widget dashboard-chart-card dashboard-card">
<div class="dashboard-chart-header">
<div class="dashboard-chart-info">
<div class="dashboard-chart-icon"><i class="fa fa-list-alt" aria-hidden="true"></i></div>
<div class="dashboard-chart-meta">
<div class="dashboard-chart-heading">{% trans "Display Content Status" %}</div>
<div class="dashboard-chart-subtitle">{% trans "Click on the chart for a breakdown" %}</div>
</div>
</div>
<div class="dashboard-chart-actions">
<div class="dashboard-chart-toggle" data-chart="displayContentChart">
<button type="button" class="dashboard-chart-toggle-button is-active" data-chart-type="doughnut" aria-label="{% trans 'Doughnut chart' %}"><i class="fa fa-circle-o" aria-hidden="true"></i></button>
<button type="button" class="dashboard-chart-toggle-button" data-chart-type="pie" aria-label="{% trans 'Pie chart' %}"><i class="fa fa-pie-chart" aria-hidden="true"></i></button>
</div>
</div>
</div>
<div class="panel-body" style="overflow: hidden;">
<div style="text-align: center; height: 10px; margin-bottom: 5px"><span>{% trans "Click on the chart for a breakdown" %}</span></div>
<div style="position: relative; height: 235px">
<canvas id="displayContentChart" style="clear:both;"></canvas>
<div class="widget-body dashboard-chart-body">
<div class="dashboard-chart-canvas">
<canvas id="displayContentChart" style="clear:both;" aria-label="{% trans "Display Content Status" %}" role="img"></canvas>
</div>
</div>
</div>
</div>
<div class="dashboard-panels d-none" id="displayGroupStatusChartRow">
<div class="panel dashboard-card">
<div class="panel-header">
<h3 id="dGStatusTitle">{% trans "Display Groups Status" %}</h3>
<div class="widget dashboard-chart-card dashboard-card">
<div class="dashboard-chart-header">
<div class="dashboard-chart-info">
<div class="dashboard-chart-icon"><i class="fa fa-sitemap" aria-hidden="true"></i></div>
<div class="dashboard-chart-meta">
<div class="dashboard-chart-heading" id="dGStatusTitle">{% trans "Display Groups Status" %}</div>
<div class="dashboard-chart-subtitle">{% trans "Grouped by status" %}</div>
</div>
</div>
<div class="dashboard-chart-actions">
<div class="dashboard-chart-toggle" data-chart="displayGroupStatusChart">
<button type="button" class="dashboard-chart-toggle-button is-active" data-chart-type="doughnut" aria-label="{% trans 'Doughnut chart' %}"><i class="fa fa-circle-o" aria-hidden="true"></i></button>
<button type="button" class="dashboard-chart-toggle-button" data-chart-type="pie" aria-label="{% trans 'Pie chart' %}"><i class="fa fa-pie-chart" aria-hidden="true"></i></button>
<button type="button" class="dashboard-chart-toggle-button" data-chart-type="bar" aria-label="{% trans 'Bar chart' %}"><i class="fa fa-chart-bar" aria-hidden="true"></i></button>
</div>
</div>
</div>
<div class="panel-body" style="overflow: hidden;">
<div style="text-align: center; height: 10px; margin-bottom: 5px"><span>{% trans "Click on the chart to view Display information" %}</span></div>
<div style="position: relative; height: 235px;">
<div class="widget-body dashboard-chart-body">
<div class="dashboard-chart-canvas">
<canvas id="displayGroupStatusChart" style="clear:both;"></canvas>
</div>
</div>
</div>
<div class="panel dashboard-card">
<div class="panel-header">
<h3 id="dGContentTitle">{% trans "Display Groups Content Status" %}</h3>
<div class="widget dashboard-chart-card dashboard-card">
<div class="dashboard-chart-header">
<div class="dashboard-chart-info">
<div class="dashboard-chart-icon"><i class="fa fa-folder-o" aria-hidden="true"></i></div>
<div class="dashboard-chart-meta">
<div class="dashboard-chart-heading" id="dGContentTitle">{% trans "Display Groups Content Status" %}</div>
<div class="dashboard-chart-subtitle">{% trans "Grouped by content status" %}</div>
</div>
</div>
<div class="dashboard-chart-actions">
<div class="dashboard-chart-toggle" data-chart="displayGroupContentStatusChart">
<button type="button" class="dashboard-chart-toggle-button is-active" data-chart-type="doughnut" aria-label="{% trans 'Doughnut chart' %}"><i class="fa fa-circle-o" aria-hidden="true"></i></button>
<button type="button" class="dashboard-chart-toggle-button" data-chart-type="pie" aria-label="{% trans 'Pie chart' %}"><i class="fa fa-pie-chart" aria-hidden="true"></i></button>
<button type="button" class="dashboard-chart-toggle-button" data-chart-type="bar" aria-label="{% trans 'Bar chart' %}"><i class="fa fa-chart-bar" aria-hidden="true"></i></button>
</div>
</div>
</div>
<div class="panel-body" style="overflow: hidden;">
<div style="text-align: center; height: 10px; margin-bottom: 5px"><span>{% trans "Click on the chart to view Display information" %}</span></div>
<div style="position: relative; height: 235px">
<div class="widget-body dashboard-chart-body">
<div class="dashboard-chart-canvas">
<canvas id="displayGroupContentStatusChart" style="clear:both;"></canvas>
</div>
</div>
@@ -322,32 +415,6 @@
</div>
</div>
</div>
<div class="quick-actions-grid">
<h3 class="section-title">{% trans "Quick Actions" %}</h3>
<div class="action-cards">
{% if currentUser.featureEnabled("schedule.view") %}
<a class="action-card action-card--modern dashboard-card" href="{{ url_for("schedule.view") }}">
<div class="action-icon"><i class="fa fa-calendar"></i></div>
<div class="action-label">{% trans "Create Schedule" %}</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("displays.view") %}
<a class="action-card action-card--modern dashboard-card" href="{{ url_for("display.view") }}">
<div class="action-icon"><i class="fa fa-desktop"></i></div>
<div class="action-label">{% trans "Manage Displays" %}</div>
</a>
{% endif %}
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
<a class="action-card action-card--modern dashboard-card" href="{{ url_for("user.view") }}">
<div class="action-icon"><i class="fa fa-user-plus"></i></div>
<div class="action-label">{% trans "Add User" %}</div>
</a>
{% endif %}
</div>
</div>
</div>
{% endblock %}
@@ -377,50 +444,173 @@
var displayGroupIdsStatus = [];
var displayGridTable = null
// Create our chart
var bandwidthChart = new Chart($("#bandwidthChart"), {
type: "bar",
data: {{ bandwidthWidget|raw }},
options: {
scales: {
xAxes: [{
stacked: {% if xmdsLimit %}true{% else %}false{% endif %}
}],
yAxes: [{
scaleLabel: {
display: true,
labelString: "{{ bandwidthSuffix }}",
},
stacked: {% if xmdsLimit %}true{% else %}false{% endif %}
}]
},
legend: {
display: false
},
maintainAspectRatio: false,
}
});
// Create our charts
const bandwidthStacked = {% if xmdsLimit %}true{% else %}false{% endif %};
var libraryData = {{ libraryWidgetData|raw }};
const libraryLabels = {{ libraryWidgetLabels|raw }};
var colours = new Array();
for (var i = 0; i < libraryData.length; i++) {
colours.push(stringToColour(libraryLabels[i]));
}
var libraryChart = new Chart($("#libraryChart"), {
type: 'pie',
data: {
datasets: [{
data: libraryData,
backgroundColor: colours
}],
function pickColor(value, fallback) {
if (Array.isArray(value)) return value[0] || fallback;
return value || fallback;
}
labels: {{ libraryWidgetLabels|raw }}
},
options: {
maintainAspectRatio: false
}
});
function cacheDatasetStyles(dataset) {
if (!dataset._ots) {
dataset._ots = {
backgroundColor: dataset.backgroundColor,
borderColor: dataset.borderColor,
fill: dataset.fill
};
}
}
function applyDatasetType(dataset, type, fallbackColor) {
cacheDatasetStyles(dataset);
dataset.type = type;
if (type === 'line') {
const color = pickColor(dataset._ots.borderColor || dataset._ots.backgroundColor, fallbackColor);
dataset.borderColor = color;
dataset.backgroundColor = 'rgba(0, 0, 0, 0)';
dataset.fill = false;
dataset.tension = 0.35;
dataset.pointRadius = 2;
dataset.pointHoverRadius = 3;
dataset.pointBackgroundColor = color;
} else {
dataset.backgroundColor = dataset._ots.backgroundColor || dataset.backgroundColor || fallbackColor;
dataset.borderColor = dataset._ots.borderColor || dataset.borderColor;
dataset.fill = dataset._ots.fill;
dataset.tension = 0;
dataset.pointRadius = 0;
}
}
function setBandwidthScaleOptions(chart, type) {
if (!chart.options || !chart.options.scales) return;
const stacked = type === 'bar' ? bandwidthStacked : false;
chart.options.scales.xAxes[0].stacked = stacked;
chart.options.scales.yAxes[0].stacked = stacked;
}
function setChartType(chart, type, isBandwidth) {
chart.config.type = type;
chart.data.datasets.forEach(function(dataset) {
applyDatasetType(dataset, type, 'rgba(96, 165, 250, 0.9)');
});
if (isBandwidth) {
setBandwidthScaleOptions(chart, type);
}
chart.update();
}
function setLibraryChartType(chart, type) {
if (type === 'pie' || type === 'doughnut') {
chart.config.type = type;
chart.update();
} else if (type === 'bar') {
// Convert pie data to bar format
const labels = chart.data.labels;
const data = chart.data.datasets[0].data;
const colors = chart.data.datasets[0].backgroundColor;
chart.config.type = 'bar';
chart.data.datasets[0].type = 'bar';
chart.data.datasets[0].backgroundColor = colors;
chart.data.datasets[0].borderColor = colors;
chart.options.scales = {
xAxes: [{ stacked: false }],
yAxes: [{ stacked: false }]
};
chart.update();
}
}
var bandwidthChart = new Chart($("#bandwidthChart"), {
type: "line",
data: {{ bandwidthWidget|raw }},
options: {
scales: {
xAxes: [{
stacked: false
}],
yAxes: [{
scaleLabel: {
display: true,
labelString: "{{ bandwidthSuffix }}",
},
stacked: false
}]
},
legend: {
display: false
},
maintainAspectRatio: false,
}
});
var libraryData = {{ libraryWidgetData|raw }};
const libraryLabels = {{ libraryWidgetLabels|raw }};
var colours = new Array();
for (var i = 0; i < libraryData.length; i++) {
colours.push(stringToColour(libraryLabels[i]));
}
var libraryChart = new Chart($("#libraryChart"), {
type: 'pie',
data: {
datasets: [{
data: libraryData,
backgroundColor: colours
}],
labels: {{ libraryWidgetLabels|raw }}
},
options: {
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
setChartType(libraryChart, 'pie', false);
$('.dashboard-chart-toggle').each(function() {
const toggle = $(this);
const chartId = toggle.data('chart');
let chart = null;
if (chartId === 'bandwidthChart') {
chart = bandwidthChart;
} else if (chartId === 'libraryChart') {
chart = libraryChart;
} else if (chartId === 'displayStatusChart') {
chart = displayStatusChart;
} else if (chartId === 'displayContentChart') {
chart = displayContentChart;
} else if (chartId === 'displayGroupStatusChart') {
chart = displayGroupStatusChart;
} else if (chartId === 'displayGroupContentStatusChart') {
chart = displayGroupContentStatusChart;
}
if (!chart) return;
toggle.find('.dashboard-chart-toggle-button').on('click', function(e) {
e.preventDefault();
const type = $(this).data('chart-type');
// Update active state
toggle.find('.dashboard-chart-toggle-button').removeClass('is-active');
$(this).addClass('is-active');
// Update chart type
if (chartId === 'libraryChart') {
setLibraryChartType(chart, type);
} else {
setChartType(chart, type, chartId === 'bandwidthChart');
}
});
});
$('.article_date').each(function(index, element) {
// Replace the ISO date with a nice formatted date "for humans"

View File

@@ -0,0 +1,599 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% import "forms.twig" as forms %}
{% block title %}{{ "DataSets"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "DataSets" %}</h1>
<p class="text-muted">{% trans "Manage structured data sources." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="dataSetView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter DataSets" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline" onsubmit="return false">
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('dataSet', title) }}
{% set title %}{% trans "Code" %}{% endset %}
{% set helpText %}{% trans "Show items which match the provided code" %}{% endset %}
{{ inline.input("code", title, "", helpText) }}
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
{ name: "data-initial-key", value: "userId" },
] %}
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
{{ inline.hidden("folderId") }}
</form>
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none">
<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">
<table id="datasets" class="table table-striped" data-state-preference-name="dataSetGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Code" %}</th>
<th>{% trans "Remote?" %}</th>
<th>{% trans "Real time?" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Sharing" %}</th>
<th>{% trans "Last Sync" %}</th>
<th>{% trans "Data Last Modified" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var table;
$(document).ready(function() {
{% if not currentUser.featureEnabled("folder.view") %}
disableFolders();
{% endif %}
table = $("#datasets").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[ 0, "asc"]],
ajax: {
"url": "{{ url_for("dataSet.search") }}",
"data": function(d) {
$.extend(d, $("#datasets").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{ "data": "dataSetId", responsivePriority: 2 },
{ "data": "dataSet", "render": dataTableSpacingPreformatted, responsivePriority: 2 },
{ "data": "description", responsivePriority: 4 },
{ "data": "code", responsivePriority: 3 },
{
"data": "isRemote",
responsivePriority: 3,
"render": dataTableTickCrossColumn
},
{
data: 'isRealTime',
responsivePriority: 3,
render: dataTableTickCrossColumn,
},
{ "data": "owner", responsivePriority: 3 },
{
"data": "groupsWithPermissions",
responsivePriority: 3,
"render": dataTableCreatePermissions
},
{
"data": "lastSync",
responsivePriority: 4,
"render": dataTableDateFromUnix
},
{
"data": "lastDataEdit",
responsivePriority: 4,
"render": dataTableDateFromUnix
},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', function(e, settings) {
dataTableDraw(e, settings);
// Upload form
$(".dataSetImportForm").click(function(e) {
e.preventDefault();
var template = Handlebars.compile($("#template-dataset-upload").html());
var data = table.row($(this).closest("tr")).data();
var columns = [];
var i = 1;
$.each(data.columns, function (index, element) {
if (element.dataSetColumnTypeId === 1) {
element.index = i;
columns.push(element);
i++;
}
});
// Handle bars and open a dialog
bootbox.dialog({
message: template({
trans: {
addFiles: "{% trans "Add CSV Files" %}",
startUpload: "{% trans "Start upload" %}",
cancelUpload: "{% trans "Cancel upload" %}",
processing: "{% trans "Processing..." %}"
},
upload: {
maxSize: {{ libraryUpload.maxSize }},
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
validExt: "{{ libraryUpload.validExt }}",
utf8Message: "{% trans "If the CSV file contains non-ASCII characters please ensure the file is UTF-8 encoded" %}"
},
columns: columns
}),
title: "{% trans "CSV Import" %}",
size: 'large',
buttons: {
main: {
label: "{% trans "Done" %}",
className: "btn-primary btn-bb-main",
callback: function() {
table.ajax.reload();
XiboDialogClose();
}
}
}
}).on('shown.bs.modal', function() {
// Configure the upload form
var url = "{{ url_for("dataSet.import", {id: ':id'}) }}".replace(":id", data.dataSetId);
var form = $(this).find("form");
var refreshSessionInterval;
// Initialize the jQuery File Upload widget:
form.fileupload({
url: url,
disableImageResize: true
});
// Upload server status check for browsers with CORS support:
if ($.support.cors) {
$.ajax({
url: url,
type: 'HEAD'
}).fail(function () {
$('<span class="alert alert-error"/>')
.text('Upload server currently unavailable - ' + new Date())
.appendTo(form);
});
}
// Enable iframe cross-domain access via redirect option:
form.fileupload(
'option',
'redirect',
window.location.href.replace(
/\/[^\/]*$/,
'/cors/result.html?%s'
)
);
form.bind('fileuploadsubmit', function (e, data) {
var inputs = data.context.find(':input');
if (inputs.filter('[required][value=""]').first().focus().length) {
return false;
}
data.formData = inputs.serializeArray().concat(form.serializeArray());
inputs.filter("input").prop("disabled", true);
}).bind('fileuploadstart', function (e, data) {
// Show progress data
form.find('.fileupload-progress .progress-extended').show();
form.find('.fileupload-progress .progress-end').hide();
if (form.fileupload("active") <= 0)
refreshSessionInterval = setInterval("XiboPing('" + pingUrl + "?refreshSession=true')", 1000 * 60 * 3);
return true;
}).bind('fileuploaddone', function (e, data) {
if (refreshSessionInterval != null && form.fileupload("active") <= 0)
clearInterval(refreshSessionInterval);
}).bind('fileuploadprogressall', function (e, data) {
// Hide progress data and show processing
if(data.total > 0 && data.loaded == data.total) {
form.find('.fileupload-progress .progress-extended').hide();
form.find('.fileupload-progress .progress-end').show();
}
}).bind('fileuploadadded fileuploadcompleted fileuploadfinished', function (e, data) {
// Get uploaded and downloaded files and toggle Done button
var filesToUploadCount = form.find('tr.template-upload').length;
var $button = form.parents('.modal:first').find('button.btn-bb-main');
if(filesToUploadCount == 0) {
$button.removeAttr('disabled');
} else {
$button.attr('disabled', 'disabled');
}
});
});
});
});
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#datasets_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
});
function dataSetFormOpen(dialog) {
// Bind the remote dataset test button
$(dialog).find("#dataSetRemoteTestButton").on('click', function() {
var $form = $(dialog).find("form");
XiboRemoteRequest("{{ url_for("dataSet.test.remote") }}", $form.serializeObject(), function(response) {
if (!response.success || !$.trim(response.data.entries)) {
response.data = response.message;
}
$("#datasetRemoteTestRequestResult").html('<pre style="height: 300px; overflow: scroll">' + JSON.stringify(response.data, null, 3) + '</pre>');
});
});
// Set up some dependencies between the isRemote checkbox and the tabs related to remote datasets
onRemoteFieldChanged(dialog);
// show data source dropdown if real time is checked
onIsRealTimeFieldChanged(dialog);
$(dialog).find("#isRemote").on('change', function() {
onRemoteFieldChanged(dialog);
});
$(dialog).find("#isRealTime").on('change', function() {
onIsRealTimeFieldChanged(dialog);
});
// Auth field
onAuthenticationFieldChanged(dialog);
$(dialog).find("#authentication").on('change', function() {
onAuthenticationFieldChanged(dialog);
});
// remote DataSet source
onSourceFieldChanged(dialog);
$(dialog).find('#sourceId').on('change', function() {
onSourceFieldChanged(dialog);
});
// Validate form manually because
// uri field depends on isRemote being checked
if (forms != undefined) {
const $form = $(dialog).find('form');
forms.validateForm(
$form, // form
$form.parent(), // container
{
submitHandler: XiboFormSubmit,
rules: {
uri: {
required: function(element) {
return $form.find('#isRemote').is(':checked')
},
},
},
},
);
}
}
function onIsRealTimeFieldChanged(dialog) {
var isRealTime = $(dialog).find("#isRealTime").is(":checked");
var dataSourceField = $(dialog).find("#dataSourceField");
var dataConnectorSource = $(dialog).find("#dataConnectorSource");
if (isRealTime) {
// show and enable data connector source
dataSourceField.removeClass("d-none");
dataConnectorSource.prop('disabled', false)
} else {
// hide and disable data connector source
dataSourceField.addClass("d-none");
dataConnectorSource.prop('disabled', true)
}
}
function onRemoteFieldChanged(dialog) {
var isRemote = $(dialog).find("#isRemote").is(":checked");
var $remoteTabs = $(dialog).find(".tabForRemoteDataSet");
if (isRemote) {
$remoteTabs.removeClass("d-none");
} else {
$remoteTabs.addClass("d-none");
}
}
function onAuthenticationFieldChanged(dialog) {
var authentication = $(dialog).find("#authentication").val();
var $authFieldUserName = $(dialog).find(".auth-field-username");
var $authFieldPassword = $(dialog).find(".auth-field-password");
if (authentication === "none") {
$authFieldUserName.addClass("d-none");
$authFieldPassword.addClass("d-none");
} else if (authentication === "bearer") {
$authFieldUserName.addClass("d-none");
$authFieldPassword.removeClass("d-none");
} else {
$authFieldUserName.removeClass("d-none");
$authFieldPassword.removeClass("d-none");
}
}
function onSourceFieldChanged(dialog) {
var sourceId = $(dialog).find('#sourceId').val();
var $jsonSource = $(dialog).find(".json-source-field");
var $csvSource = $(dialog).find(".csv-source-field");
if (sourceId == 1) {
$jsonSource.removeClass('d-none');
$csvSource.addClass('d-none');
} else {
$jsonSource.addClass('d-none');
$csvSource.removeClass('d-none');
}
}
function deleteMultiSelectFormOpen(dialog) {
{% set message = 'Delete any associated data?' %}
var $input = $('<input type=checkbox id="deleteData" name="deleteData"> {{ message|trans|e }} </input>');
$input.on('change', function() {
dialog.data().commitData = {deleteData: $(this).val()};
});
$(dialog).find('.modal-body').append($input);
}
</script>
{% endblock %}
{% block javaScriptTemplates %}
{{ parent() }}
{% verbatim %}
<script type="text/x-handlebars-template" id="template-dataset-upload">
<form class="form-horizontal" method="post" enctype="multipart/form-data" data-max-file-size="{{ upload.maxSize }}" data-accept-file-types="/(\.|\/)csv/i">
<div class="row fileupload-buttonbar">
<div class="card p-3 mb-3 bg-light">
{{ upload.maxSizeMessage }} <br>
{{ upload.utf8Message }}
</div>
<div class="col-md-7">
<!-- The fileinput-button span is used to style the file input field as button -->
<span class="btn btn-success fileinput-button">
<i class="fa fa-plus"></i>
<span>{{ trans.addFiles }}</span>
<input type="file" name="files">
</span>
<button type="submit" class="btn btn-primary start">
<i class="fa fa-upload"></i>
<span>{{ trans.startUpload }}</span>
</button>
<button type="reset" class="btn btn-warning cancel">
<i class="fa fa-ban"></i>
<span>{{ trans.cancelUpload }}</span>
</button>
<!-- The loading indicator is shown during file processing -->
<span class="fileupload-loading"></span>
</div>
<!-- The global progress information -->
<div class="col-md-4 fileupload-progress fade">
<!-- The global progress bar -->
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width:0%;">
<div class="sr-only"></div>
</div>
</div>
<!-- The extended global progress information -->
<div class="progress-extended">&nbsp;</div>
<!-- Processing info container -->
<div class="progress-end" style="display:none;">{{ trans.processing }}</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
{% endverbatim %}
{% set title %}{% trans "Overwrite existing data?" %}{% endset %}
{% set helpText %}{% trans "Erase all content in this DataSet and overwrite it with the new content in this import." %}{% endset %}
{{ forms.checkbox("overwrite", title, "", helpText) }}
{% set title %}{% trans "Ignore first row?" %}{% endset %}
{% set helpText %}{% trans "Ignore the first row? Useful if the CSV has headings." %}{% endset %}
{{ forms.checkbox("ignorefirstrow", title, "", helpText) }}
{% set message %}{% trans "In the fields below please enter the column number in the CSV file that corresponds to the Column Heading listed. This should be done before Adding the file." %}{% endset %}
{{ forms.message(message) }}
{% verbatim %}
{{#each columns}}
<div class="form-group row">
<label class="col-sm-2 control-label" for="csvImport_{{dataSetColumnId}}">{{heading}}</label>
<div class="col-sm-10">
<input class="form-control" name="csvImport_{{dataSetColumnId}}" type="number" id="csvImport_{{dataSetColumnId}}" value="{{ index }}" />
</div>
</div>
{{/each}}
</div>
</div>
<!-- The table listing the files available for upload/download -->
<table role="presentation" class="table table-striped"><tbody class="files"></tbody></table>
</form>
</script>
<!-- The template to display files available for upload -->
<script id="template-dataset-upload" type="text/x-tmpl">
{% for (var i=0, file; file=o.files[i]; i++) { %}
<tr class="template-upload">
<td>
<span class="fileupload-preview"></span>
</td>
<td class="title">
{% if (file.error) { %}
<div><span class="label label-danger">{%=file.error%}</span></div>
{% } %}
{% if (!file.error) { %}
<label for="name[]"><input name="name[]" type="text" id="name" value="" /></label>
{% } %}
</td>
<td>
<p class="size">{%=o.formatFileSize(file.size)%}</p>
{% if (!o.files.error) { %}
<div class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width:0%;">
<div class="sr-only"></div>
</div>
</div>
</div>
{% } %}
</td>
<td class="btn-group">
{% if (!o.files.error && !i && !o.options.autoUpload) { %}
<button class="btn btn-primary start">
<i class="fa fa-upload"></i>
</button>
{% } %}
{% if (!i) { %}
<button class="btn btn-warning cancel">
<i class="fa fa-ban"></i>
</button>
{% } %}
</td>
</tr>
{% } %}
</script>
<!-- The template to display files available for download -->
<script id="template-dataset-download" type="text/x-tmpl">
{% for (var i=0, file; file=o.files[i]; i++) { %}
<tr class="template-download">
<td>
<p class="name" id="{%=file.storedas%}" status="{% if (file.error) { %}error{% } %}">
{%=file.name%}
</p>
{% if (file.error) { %}
<div><span class="label label-danger">{%=file.error%}</span></div>
{% } %}
</td>
<td>
<span class="size">{%=o.formatFileSize(file.size)%}</span>
</td>
</tr>
{% } %}
</script>
{% endverbatim %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,261 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Dayparting"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Dayparting" %}</h1>
<p class="text-muted">{% trans "Manage time-based scheduling rules." %}</p>
</div>
<div class="widget 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 Dayparts" %}</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('name', title) }}
{% set title %}{% trans "Retired" %}{% endset %}
{% set option1 = "Yes"|trans %}
{% set option2 = "No"|trans %}
{% set values = [{id: 1, value: option1}, {id: 0, value: option2}] %}
{{ inline.dropdown("isRetired", "single", title, 0, values, "id", "value") }}
</form>
</div>
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<table id="dayparts" class="table table-striped" data-state-preference-name="daypartGrid">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Start Time" %}</th>
<th>{% trans "End Time" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var table = $("#dayparts").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[ 1, "asc"]],
ajax: {
"url": "{{ url_for("daypart.search") }}",
"data": function(d) {
$.extend(d, $("#dayparts").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{ "data": "name", "render": dataTableSpacingPreformatted , responsivePriority: 2},
{ "data": "description" },
{ "data": "startTime" },
{ "data": "endTime" },
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#dayparts_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
function dayPartFormOpen(dialog) {
// Render a set of exceptions
$exceptions = $(dialog).find("#dayPartExceptions");
// Days of the week translations
var daysOfTheWeek = [
{ day: "Mon", title: "{% trans "Monday" %}" },
{ day: "Tue", title: "{% trans "Tuesday" %}" },
{ day: "Wed", title: "{% trans "Wednesday" %}" },
{ day: "Thu", title: "{% trans "Thursday" %}" },
{ day: "Fri", title: "{% trans "Friday" %}" },
{ day: "Sat", title: "{% trans "Saturday" %}" },
{ day: "Sun", title: "{% trans "Sunday" %}" }
];
// Compile the handlebars template
var exceptionsTemplate = Handlebars.compile($("#dayPartExceptionsTemplate").html());
if (dialog.data().extra.exceptions.length == 0) {
// Contexts for template
var context = {
daysOfWeek: daysOfTheWeek,
buttonGlyph: "fa-plus",
exceptionDay: "",
exceptionStart: "",
exceptionEnd: "",
fieldId: 0
};
// Append
$exceptions.append(exceptionsTemplate(context));
XiboInitialise("#" + $exceptions.prop("id"));
} else {
// For each of the existing exceptions, create form components
var i = 0;
$.each(dialog.data().extra.exceptions, function (index, field) {
i++;
// call the template
var context = {
daysOfWeek: daysOfTheWeek,
buttonGlyph: ((i == 1) ? "fa-plus" : "fa-minus"),
exceptionDay: field.day,
exceptionStart: field.start,
exceptionEnd: field.end,
fieldId: i
};
$exceptions.append(exceptionsTemplate(context));
XiboInitialise("#" + $exceptions.prop("id"));
});
}
// Nabble the resulting buttons
$exceptions.on("click", "button", function (e) {
e.preventDefault();
// find the gylph
if ($(this).find("i").hasClass("fa-plus")) {
var context = {
daysOfWeek: daysOfTheWeek,
buttonGlyph: "fa-minus",
exceptionDay: "",
exceptionStart: "",
exceptionEnd: "",
fieldId: $exceptions.find('.form-group').length + 1
};
$exceptions.append(exceptionsTemplate(context));
XiboInitialise("#" + $exceptions.prop("id"));
} else {
// Remove this row
$(this).closest(".form-group").remove();
}
});
// check if we already have this day in exceptions array, if so remove the row with a message.
$exceptions.on("change", "select", function() {
var selectedDays = [];
$('select').not('#' + $(this).attr('id')).each(function(i) {
selectedDays.push($(this).val());
});
if (selectedDays.includes(this.value)) {
toastr.error(translations.dayPartExceptionErrorMessage);
// Remove this row
$(this).closest(".form-group").remove();
}
})
}
// Equals helper for the templates below
Handlebars.registerHelper('eq', function(v1, v2, opts) {
if (v1 === v2) {
return opts.fn(this);
} else {
return opts.inverse(this);
}
});
</script>
{% verbatim %}
<script type="text/x-handlebars-template" id="dayPartExceptionsTemplate">
<div class="form-group row">
<div class="col-3">
<select class="form-control" name="exceptionDays[]" id="exceptionDays_{{fieldId}}">
<option value=""></option>
{{#each daysOfWeek}}
<option value="{{ day }}" {{#eq day ../exceptionDay}}selected{{/eq}}>{{ title }}</option>
{{/each}}
</select>
</div>
<div class="col-3">
{% endverbatim %}
{{ inline.time("exceptionStartTimes[]", "", "{{ exceptionStart }}" ) }}
{% verbatim %}
</div>
<div class="col-3">
{% endverbatim %}
{{ inline.time("exceptionEndTimes[]", "", "{{ exceptionEnd }}" ) }}
{% verbatim %}
</div>
<div class="col-1">
<button class="btn btn-white"><i class="fa {{ buttonGlyph }}"></i></button>
</div>
</div>
</script>
{% endverbatim %}
{% endblock %}

View File

@@ -28,9 +28,9 @@
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("displays.add") %}
<button class="btn 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 (Code)" %}</button>
<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-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<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 %}
@@ -47,7 +47,6 @@
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-title ots-displays-title">{% trans "Displays" %}</div>
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="displayView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
@@ -237,10 +236,10 @@
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div class="map-controller d-none pl-1 ots-grid-controller">
<button type="button" id="map_button" class="btn btn-primary" title="{{ "Map"|trans }}"><i class="fa fa-map"></i></button>
<button type="button" id="map_button" class="btn btn-icon btn-primary" title="{{ "Map"|trans }}"><i class="fa fa-map"></i></button>
</div>
<div class="list-controller d-none pl-1 ots-grid-controller">
<button type="button" id="list_button" class="btn btn-primary" title="{{ "List"|trans }}"><i class="fa fa-list"></i></button>
<button type="button" id="list_button" class="btn btn-icon btn-primary" title="{{ "List"|trans }}"><i class="fa fa-list"></i></button>
</div>
<div id="datatable-container">

View File

@@ -0,0 +1,381 @@
{#
/**
* Copyright (C) 2020-2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Display Groups"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Display Groups" %}</h1>
<p class="text-muted">{% trans "Organize Displays into logical groups." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="displayGroupGridView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Display Groups" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "ID" %}{% endset %}
{{ inline.input("displayGroupId", title) }}
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('displayGroup', title) }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" },
{ name: "data-initial-key", value: "displayId" },
] %}
{% set helpText %}{% trans "Return Display Groups that directly contain the selected Display." %}{% endset %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", helpText, "pagedSelect", "", "", "", attributes) }}
{% set title %}{% trans "Nested Display" %}{% endset %}
{% set helpText %}{% trans "Return Display Groups that contain the selected Display somewhere in the nested Display Group relationship tree." %}{% endset %}
{{ inline.dropdown("nestedDisplayId", "single", title, "", null, "displayId", "display", helpText, "pagedSelect", "", "", "", attributes) }}
{% set title %}{% trans "Dynamic Criteria" %}{% endset %}
{{ inline.input("dynamicCriteria", title) }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{{ inline.hidden("folderId") }}
</form>
</div>
</div>
</div>
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 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">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div 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">
<table id="displaygroups" class="table table-striped" data-content-type="displayGroup" data-content-id-name="displayGroupId" data-state-preference-name="displayGroupGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Is Dynamic?" %}</th>
<th>{% trans "Criteria" %}</th>
{% if currentUser.featureEnabled("tag.tagging") %}
<th>{% trans "Criteria Tags" %}</th>
<th>{% trans "Tags" %}</th>
{% endif %}
<th>{% trans "Sharing" %}</th>
<th>{% trans "Reference 1" %}</th>
<th>{% trans "Reference 2" %}</th>
<th>{% trans "Reference 3" %}</th>
<th>{% trans "Reference 4" %}</th>
<th>{% trans "Reference 5" %}</th>
<th>{% trans "Created Date" %}</th>
<th>{% trans "Modified Date" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var displayGroupTable;
var displayTable;
var criteria;
var criteriaTag;
var useRegexForName;
var exactTags;
var logicalOperator;
var logicalOperatorName;
$(document).ready(function() {
{% if not currentUser.featureEnabled("folder.view") %}
disableFolders();
{% endif %}
displayGroupTable = $("#displaygroups").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
"filter": false,
searchDelay: 3000,
"order": [[ 1, "asc"]],
ajax: {
"url": "{{ url_for("displayGroup.search") }}",
"data": function(d) {
$.extend(d, $("#displaygroups").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{ "data": "displayGroupId", responsivePriority: 2},
{ "data": "displayGroup", "render": dataTableSpacingPreformatted, responsivePriority: 2 },
{ "data": "description", responsivePriority: 3 },
{ "data": "isDynamic", "render": dataTableTickCrossColumn, responsivePriority: 3 },
{ "data": "dynamicCriteria", responsivePriority: 4 },
{% if currentUser.featureEnabled("tag.tagging") %}
{ "data": "dynamicCriteriaTags", responsivePriority: 4},
{
"name": "tags",
"sortable": false,
responsivePriority: 3,
"data": dataTableCreateTags
},
{% endif %}
{
"data": "groupsWithPermissions",
visible: false,
responsivePriority: 10,
"render": dataTableCreatePermissions
},
{ "data": "ref1", "visible": false, responsivePriority: 5},
{ "data": "ref2", "visible": false, responsivePriority: 5},
{ "data": "ref3", "visible": false, responsivePriority: 5},
{ "data": "ref4", "visible": false, responsivePriority: 5},
{ "data": "ref5", "visible": false, responsivePriority: 5},
{ "data": "createdDt", "visible": false, responsivePriority: 5 },
{ "data": "modifiedDt", "visible": false, responsivePriority: 5 },
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
$("#refreshGrid").click(function () {
displayGroupTable.ajax.reload();
});
displayGroupTable.on('draw', dataTableDraw);
displayGroupTable.on('draw', { form: $("#displaygroups").closest(".XiboGrid").find(".FilterDiv form") }, dataTableCreateTagEvents);
displayGroupTable.on('processing.dt', dataTableProcessing);
dataTableAddButtons(displayGroupTable, $('#displaygroups_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
displayGroupTable.ajax.reload();
});
});
function setDeleteMultiSelectFormOpen(dialog) {
$(dialog).find('.save-button').prop('disabled', false);
var template = Handlebars.compile($('#template-display-group-multi-delete-checkbox').html());
var $input = $(template());
$input.find('input').on('change', function() {
$(dialog).find('.save-button').prop('disabled', !$(this).is(':checked'));
});
$(dialog).find('.modal-body').append($input);
}
function displayGroupAddFormNext() {
// Get form
var $form = $("#displayGroupAddForm");
// Set apply and apply reset data
$form.data("apply", true);
$form.data("applyCallback", 'applyResetCallback');
// Submit form
$form.submit();
}
function applyResetCallback(form) {
// Reset form fields
$(form).find('#displayGroup').val("");
}
function displayGroupFormOpen(dialog) {
displayTable = null;
$(dialog).find("input[name=dynamicCriteria]").on("keyup", _.debounce(function() {
displayGroupQueryDynamicMembers(dialog);
}, 500));
$(dialog).find("input[name=dynamicCriteriaTags], input[name=exactTags], select[name=logicalOperator], select[name=logicalOperatorName]").change(function() {
displayGroupQueryDynamicMembers(dialog);
});
var $form = $('#displayGroupAddForm');
// First time in there
displayGroupQueryDynamicMembers(dialog);
}
function displayGroupQueryDynamicMembers(dialog) {
if ($(dialog).find("input[name=isDynamic]")[0].checked) {
criteria = $(dialog).find("input[name=dynamicCriteria]").val();
criteriaTag = $(dialog).find("input[name=dynamicCriteriaTags]").val();
useRegexForName = $(dialog).find("input[name=useRegexForName]").val();
exactTags = $(dialog).find("input[name=exactTags]").is(':checked');
logicalOperator = $(dialog).find("select[name=logicalOperator]").val();
logicalOperatorName = $(dialog).find("select[name=logicalOperatorName]").val();
if (criteria === "" && criteriaTag === "") {
if (displayTable != null) {
displayTable.destroy();
displayTable = null;
$("#displayGroupDisplays tbody").empty();
}
return;
}
if (displayTable != null) {
displayTable.ajax.reload();
} else {
displayTable = $("#displayGroupDisplays").DataTable({
"language": dataTablesLanguage,
serverSide: true,
stateSave: true, stateDuration: 0,
filter: false,
searchDelay: 3000,
"order": [[1, "asc"]],
ajax: {
"url": "{{ url_for("display.search") }}",
"data": function (d) {
$.extend(
d,
{
display: criteria,
tags: criteriaTag,
useRegexForName: useRegexForName,
exactTags: exactTags,
logicalOperator: logicalOperator,
logicalOperatorName: logicalOperatorName
}
);
}
},
"columns": [
{"data": "displayId"},
{"data": "display"},
{"data": dataTableCreateTags},
{
"data": "mediaInventoryStatus",
"render": function (data, type, row) {
if (type != "display")
return data;
var icon = "";
if (data == 1)
icon = "fa-check";
else if (data == 0)
icon = "fa-times";
else
icon = "fa-cloud-download";
return "<span class='fa " + icon + "'></span>";
}
},
{"data": "licensed", "render": dataTableTickCrossColumn}
]
});
displayTable.on('processing.dt', dataTableProcessing);
displayTable.on('draw', { form: $(".displayGroupForm") }, dataTableCreateTagEvents);
}
}
}
</script>
{% endblock %}
{% block javaScriptTemplates %}
{{ parent() }}
{% verbatim %}
<script type="text/x-handlebars-template" id="template-display-group-multi-delete-checkbox">
<div class="form-group row">
<div class="offset-sm-2 col-sm-10 mt-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="checkbox-confirmDelete" name="confirmDelete">
<label class="form-check-label" for="checkbox-confirmDelete">
{% endverbatim %}{{ "Are you sure you want to delete?"|trans }}{% verbatim %}
</label>
</div>
<small class="form-text text-muted">{% endverbatim %}{{ "Check to confirm deletion of the selected records."|trans }}{% verbatim %}</small>
</div>
</div>
</script>
{% endverbatim %}
{% endblock %}

View File

@@ -0,0 +1,168 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Display Setting Profiles"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Display Settings" %}</h1>
<p class="text-muted">{% trans "Manage Display settings profiles." %}</p>
</div>
<div class="widget 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 Display Settings" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-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('displayProfile', title) }}
{% set title %}{% trans "Type" %}{% endset %}
{{ inline.dropdown("type", "single", title, "", [{typeId:null, type:""}]|merge(types), "typeId","type") }}
</form>
</div>
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<table id="displayProfiles" class="table table-striped" data-state-preference-name="displayProfileGrid">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Default" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var table = $("#displayProfiles").DataTable({ "language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[ 1, "asc"]],
ajax: {
"url": "{{ url_for("displayProfile.search") }}",
"data": function(d) {
$.extend(d, $("#displayProfiles").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{ "data": "name", "render": dataTableSpacingPreformatted , responsivePriority: 2},
{ "data": "type" },
{ "data": "isDefault", "render": dataTableTickCrossColumn },
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#displayProfiles_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
// Custom submit for display profile form
function displayProfileEditFormSubmit() {
var $form = $("#displayProfileForm");
// Remove temp fields and enable checkbox after submit
$form.submit(function(event) {
event.preventDefault();
// Re-enable checkboxes
$form.find('input[type="checkbox"]').each(function () {
// Enable checkbox
$(this).attr('disabled', false);
});
// Remove temp input fields
$form.find('input.temp-input').each(function () {
$(this).remove();
});
});
// Replace all checkboxes with hidden input fields
$form.find('input[type="checkbox"]').each(function () {
// Get checkbox values
var value = $(this).is(':checked') ? 'on' : 'off';
var id = $(this).attr('id');
// Create hidden input
$('<input type="hidden" class="temp-input">')
.attr('id', id)
.attr('name', id)
.val(value)
.appendTo($(this).parent());
// Disable checkbox so it won't be submitted
$(this).attr('disabled', true);
});
// Submit form
$form.submit();
}
</script>
{% endblock %}

View File

@@ -0,0 +1,327 @@
{% macro disabled(name, title, value, helpText, groupClass) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="{{ helpText }}">{{ title }}</label>
<input readonly class="form-control" value="{{ value }}"></input>
</div>
{% endmacro %}
{% macro hidden(name, value) %}
<input name="{{ name }}" type="hidden" id="{{ name }}" value="{{ value }}" />
{% endmacro %}
{% macro raw(text, groupClass) %}
<div class="{{ groupClass }}">
{{ text|raw }}
</div>
{% endmacro %}
{% macro message(message, groupClass, messageStyleClass) %}
<div class="{% if messageStyleClass %}{{messageStyleClass}}{% endif %} mr-1 {{ groupClass }}">
<span>{{ message }}</span>
</div>
{% endmacro %}
{% macro alert(message, alertType, groupClass) %}
<div class="row">
<div class="mr-3 alert alert-{% if alertType %}{{alertType}}{% else %}primary{% endif %} {{ groupClass }}" role="alert">{{ message }}</div>
</div>
{% endmacro %}
{% macro button(title, type, link, groupClass) %}
<div class="form-group {{ groupClass }}">
{% if type == "link" %}
<a class="btn btn-white xibo-inline-btn mr-1 ml-0" href="{{ link }}">{{ title }}</a>
{% else %}
<button class="btn btn-white xibo-inline-btn mr-1 ml-0" type="{{ type }}">{{ title }}</button>
{% endif %}
</div>
{% endmacro %}
{% macro input(name, title, value, helpText, groupClass, validation, accessKey) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" {{ validation }} />
</div>
{% endmacro %}
{% macro inputWithTags(name, title, value, helpText, groupClass, validation, accessKey, exactTag, exactTagTitle, logicalOperatorTitle, autoCompleteEnabled = 1) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
{% if exactTag %}
<div class="input-group input-group-tags-exact">
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" data-role="tagsInputInline" {% if autoCompleteEnabled == 1 %} data-auto-complete-url="{{ url_for('tag.search') }}?allTags=1" {% endif %} {{ validation }} />
<div class="input-group-append input-group-addon">
<div class="input-group-text">
<input title="{{ exactTagTitle }}" type="checkbox" id="{{ exactTag }}" name="{{ exactTag }}">
</div>
<select class="custom-select" id="logicalOperator" name="logicalOperator" title="{{ logicalOperatorTitle }}" style="min-width:auto!important">
<option value="OR" selected>OR</option>
<option value="AND">AND</option>
</select>
</div>
</div>
{% else %}
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" data-role="tagsInputInline" {% if autoCompleteEnabled == 1 %} data-auto-complete-url="{{ url_for('tag.search') }}?allTags=1" {% endif %} {{ validation }} />
{% endif %}
</div>
{% endmacro %}
{% macro number(name, title, value, helpText, groupClass, validation, accessKey, maxNumber, minNumber) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
<input class="form-control" name="{{ name }}" {% if maxNumber %}max="{{maxNumber}}" {% endif %}{% if minNumber %}min="{{minNumber}}" {% endif %}type="number" id="{{ name }}" value="{{ value }}" {{ validation }} />
</div>
{% endmacro %}
{% macro email(name, title, value, helpText, groupClass, validation, accessKey) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
<input class="form-control" name="{{ name }}" type="email" id="{{ name }}" value="{{ value }}" {{ validation }} />
</div>
{% endmacro %}
{% macro password(name, title, value, helpText, groupClass, validation, accessKey) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" for="{{ name }}" title="{{ helpText }}" accesskey="{{ accessKey }}">{{ title }}</label>
<input class="form-control" name="{{ name }}" type="password" id="{{ name }}" value="{{ value }}" {{ validation }} />
</div>
{% endmacro %}
{% macro checkbox(name, title, value, groupClass, accessKey) %}
<div class="form-group ml-2 mr-3 mb-1 {{ groupClass }}">
<div class="form-check">
<input title="{{ title }}" class="form-check-input" type="checkbox" id="{{ name }}" name="{{ name }}" {% if value == 1 %}checked{% endif %}>
<label class="form-check-label" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
</div>
</div>
{% endmacro %}
{% macro radio(name, id, title, value, helpText, groupClass, accessKey, setValue) %}
<div class="form-group ml-2 mr-3 mb-1 {{ groupClass }}">
<div class="form-check">
<input class="form-check-input" type="radio" id="{{ id }}" name="{{ name }}" value="{{ setValue }}" {% if value == setValue %}checked{% endif %}>
<label class="form-check-label" for="{{ name }}" title="{{ helpText }}" accesskey="{{ accessKey }}">{{ title }}</label>
</div>
</div>
{% endmacro %}
{% macro dropdown(name, type, title, value, options, optionId, optionValue, helpText, groupClass, validation, accessKey, callBack, dataAttributes, optionGroups) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" for="{{ name }}" title="{{ helpText }}" accesskey="{{ accessKey }}">{{ title }}</label>
<select class="form-control" {% if type == "dropdownmulti" %}multiple{% endif %} name="{{ name }}" id="{{ name }}" {{ callBack }}
{% if type == "dropdownmulti" %}
data-allow-clear="true"
data-placeholder--id=null
data-placeholder--value=""
{% endif %}
{% if dataAttributes|length > 0 %}
{% for attribute in dataAttributes %}
{{ attribute.name }}="{{ attribute.value }}"
{% endfor %}
{% endif %}>
{% set hasGroups = optionGroups|length > 0 %}
{% if not hasGroups %}
{% set optionGroups = {label: "General"} %}
{% endif %}
{% for group in optionGroups %}
{% if hasGroups %}
<optgroup label="{{ group.label }}">
{% set tempOptions = attribute(options, group.id) %}
{% else %}
{% set tempOptions = options %}
{% endif %}
{% for option in tempOptions %}
{% set itemOptionId = attribute(option, optionId) %}
{% set itemOptionValue = attribute(option, optionValue) %}
{% if type == "dropdownmulti" %}
{% set selected = (itemOptionId in value) %}
{% else %}
{% set selected = (itemOptionId == value) %}
{% endif %}
<option value="{{ itemOptionId }}" {% if selected %}selected{% endif %}>{{ itemOptionValue }}</option>
{% endfor %}
{% if hasGroups %}
</optgroup>
{% endif %}
{% endfor %}
</select>
</div>
{% endmacro %}
{% macro permissions(name, options) %}
<table class="table table-bordered">
<tr>
<th>{% trans "Group" %}</th>
<th>{% trans "View" %}</th>
<th>{% trans "Edit" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
{% for item in options %}
<tr>
<td>{{ name }}</td>
<td><input type="checkbox" name="{{ name }}" value="{{ value_view }}" {{ value_view_checked }}></td>
<td><input type="checkbox" name="{{ name }}" value="{{ value_edit }}" {{ value_edit_checked }}></td>
<td><input type="checkbox" name="{{ name }}" value="{{ value_del }}" {{ value_del_checked }}></td>
</tr>
{% endfor %}
</table>
{% endmacro %}
{% macro date(name, title, value, helpText, groupClass, validation, accessKey) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
<div class="input-group">
<div class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></div>
<input class="form-control dateControl date" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
</div>
</div>
{% endmacro %}
{% macro dateMonth(name, title, value, helpText, groupClass, validation, accessKey) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
<div class="input-group">
<span class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></span>
<input class="form-control dateControl month" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
</div>
</div>
{% endmacro %}
{% macro dateTime(name, title, value, helpText, groupClass, validation, accessKey) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ linkedName }}" accesskey="{{ accessKey }}">{{ title }}</label>
<div class="input-group">
<span class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></span>
<input class="form-control dateControl dateTime" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
</div>
</div>
{% endmacro %}
{% macro time(name, title, value, helpText, groupClass, validation, accessKey) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1 {% if title == '' %}d-none{% endif %}" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
<div class="input-group">
<span class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></span>
<input class="form-control dateControl time" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
</div>
</div>
{% endmacro %}
{% macro switch(name, title, value, labelWidth, switchSize, onText, offText, groupClass, accessKey, disabled) %}
<div class="form-group {{ groupClass }}">
<div class="checkbox">
<input type="checkbox" class="bootstrap-switch-target" id="{{ name }}" name="{{ name }}" accesskey="{{ accessKey }}"
{% if value == 1 %}checked{% endif %}
{% if disabled == 1 %}disabled{% endif %}
data-label-text="{{ title }}"
{% if onText not in [null, undefined, ""] %} data-on-text="{{ onText }}"{% endif %}
{% if offText not in [null, undefined, ""] %} data-off-text="{{ offText }}"{% endif %}
{% if switchSize not in [null, undefined, ""] %}data-size="{{ switchSize }}"{% else %}data-size="small"{% endif %}
{% if labelWidth not in [null, undefined, ""] %} data-label-width="{{ labelWidth }}"{% endif %}
>
</div>
</div>
{% endmacro %}
{% macro color(name, title, value, helpText, groupClass, validation, accessKey) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
<input class="form-control XiboColorPicker" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" {{ validation }} />
</div>
{% endmacro %}
{% macro inputNameGrid(name, title, groupClass, useRegexName, logicalOperatorName) %}
<div class="form-group mr-1 mb-1 {{ groupClass }}">
<label class="control-label mr-1" title="" for="{{ name }}" accesskey="">{{ title }}</label>
<div>
<div class="input-group">
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="">
<div class="input-group-append input-group-addon">
<div class="input-group-text">
<input title="{% trans "Use Regex?" %}" type="checkbox" {% if useRegexName %} id="{{ useRegexName }}" name="{{ useRegexName }}" {% else %} id="useRegexForName" name="useRegexForName"{% endif %}>
</div>
<select class="custom-select" {% if logicalOperatorName %} id="{{ logicalOperatorName }}" name="{{ logicalOperatorName }}" {% else %} id="logicalOperatorName" name="logicalOperatorName"{% endif %}
title="{% trans "When filtering by multiple names, which logical operator should be used?" %}" style="min-width:auto!important">
<option value="OR" selected>OR</option>
<option value="AND">AND</option>
</select>
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro dateRangeFilter(name, title, value, helpText, groupClass, validation, accessKey) %}
<div class="form-group mr-1 mb-1 d-flex flex-row {{ groupClass }}">
{% set today = now | date_modify('today') | date("Y-m-d H:i:s") %}
<div class="form-group mr-1">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">
{{ title }}
</label>
<div class="d-inline-flex">
<select class="form-control XiboDateRangeFilter" name="{{ name }}" id="{{ name }}">
<option value="" >{% trans "Select a range" %}</option>
<option value="today" selected>{% trans "Today" %}</option>
<option value="yesterday">{% trans "Yesterday" %}</option>
<option value="thisweek">{% trans "This Week" %}</option>
<option value="thismonth">{% trans "This Month" %}</option>
<option value="thisyear">{% trans "This Year" %}</option>
<option value="lastweek">{% trans "Last Week" %}</option>
<option value="lastmonth">{% trans "Last Month" %}</option>
<option value="lastyear">{% trans "Last Year" %}</option>
</select>
</div>
</div>
<div class="form-group hidden mr-1 {{ 'rangeFilterInput_' ~ name }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">
{% trans "From Date" %}
</label>
<div class="input-group">
<div class="input-group-prepend input-group-text date-open-button" role="button">
<i class="fa fa-calendar"></i>
</div>
<input class="form-control dateControl date rangeInput"
type="text" name="fromDt" id="{{ 'fromDt_' ~ name }}"
value="{{ today }}"
/>
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none"
role="button"
>
<i class="fa fa-times"></i>
</span>
</div>
</div>
<div class="form-group hidden {{ 'rangeFilterInput_' ~ name }}">
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">
{% trans "To Date" %}
</label>
<div class="input-group">
<div class="input-group-prepend input-group-text date-open-button" role="button">
<i class="fa fa-calendar"></i>
</div>
<input class="form-control dateControl date rangeInput"
type="text" name="toDt" id="{{ 'toDt_' ~ name }}"
value="{{ today | date_modify('+1 day -1 second') | date("Y-m-d H:i:s") }}"
/>
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none"
role="button"
>
<i class="fa fa-times"></i>
</span>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -0,0 +1,536 @@
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% 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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Layouts" %}</h1>
<p class="text-muted">{% trans "Manage and design your layouts." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-type="layout" data-grid-name="layoutView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Layouts" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item"><a class="nav-link active" href="#general-filter" role="tab" data-toggle="tab" aria-selected="true"><span>{% trans "General" %}</span></a></li>
<li class="nav-item"><a class="nav-link" href="#advanced-filter" role="tab" data-toggle="tab" aria-selected="false"><span>{% trans "Advanced" %}</span></a></li>
</ul>
<form class="form-inline d-block">
<div class="tab-content">
<div class="tab-pane active" id="general-filter" role="tabpanel">
{% set title %}{% trans "ID" %}{% endset %}
{{ inline.number("campaignId", title) }}
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('layout', title) }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{% set title %}{% trans "Code" %}{% endset %}
{{ inline.input('codeLike', title) }}
{% if currentUser.featureEnabled("displaygroup.view") %}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set helpText %}{% trans "Show Layouts active on the selected Display / Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-filter-options", value: '{"isDisplaySpecific":-1}' },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" },
{ name: "data-initial-key", value: "displayGroupId" },
] %}
{{ inline.dropdown("activeDisplayGroupId", "single", title, "", null, "displayGroupId", "displayGroup", helpText, "pagedSelect", "", "", "", attributes) }}
{% endif %}
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
{ name: "data-initial-key", value: "userId" },
] %}
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
{% set title %}{% trans "Owner User Group" %}{% endset %}
{% set helpText %}{% trans "Show items owned by users in the selected User Group." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("group.search") },
{ name: "data-search-term", value: "group" },
{ name: "data-id-property", value: "groupId" },
{ name: "data-text-property", value: "group" },
{ name: "data-initial-key", value: "userGroupId" },
] %}
{{ inline.dropdown("ownerUserGroupId", "single", title, "", null, "groupId", "group", helpText, "pagedSelect", "", "", "", attributes) }}
{% set title %}{% trans "Orientation" %}{% endset %}
{% set option1 = "All"|trans %}
{% set option2 = "Landscape"|trans %}
{% set option3 = "Portrait"|trans %}
{% set values = [{id: '', value: option1}, {id: 'landscape', value: option2}, {id: 'portrait', value: option3}] %}
{{ inline.dropdown("orientation", "single", title, '', values, "id", "value") }}
{{ inline.hidden("folderId") }}
</div>
<div class="tab-pane" id="advanced-filter" role="tabpanel">
{% set title %}{% trans "Retired" %}{% endset %}
{% set option1 = "No"|trans %}
{% set option2 = "Yes"|trans %}
{% set values = [{id: 0, value: option1}, {id: 1, value: option2}] %}
{{ inline.dropdown("retired", "single", title, 0, values, "id", "value") }}
{% set title %}{% trans "Show" %}{% endset %}
{% set option1 = "All"|trans %}
{% set option2 = "Only Used"|trans %}
{% set option3 = "Only Unused"|trans %}
{% set values = [{id: 1, value: option1}, {id: 2, value: option2}, {id: 3, value: option3}] %}
{{ inline.dropdown("layoutStatusId", "single", title, 1, values, "id", "value") }}
{% set title %}{% trans "Description" %}{% endset %}
{% set option1 = "All"|trans %}
{% set option2 = "1st line"|trans %}
{% set option3 = "Widget List"|trans %}
{% set values = [{id: 1, value: option1}, {id: 2, value: option2}, {id: 3, value: option3}] %}
{{ inline.dropdown("showDescriptionId", "single", title, 2, values, "id", "value") }}
{% if currentUser.featureEnabled("library.view") %}
{% set title %}{% trans "Media" %}{% endset %}
{{ inline.input("mediaLike", title) }}
{% endif %}
{% set title %}{% trans "Layout ID" %}{% endset %}
{{ inline.number("layoutId", title) }}
{% set title %}{% trans "Modified Since" %}{% endset %}
{{ inline.date("modifiedSinceDt", title) }}
</div>
</div>
</form>
</div>
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none">
<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">
<table id="layouts" class="table table-striped responsive nowrap" data-content-type="layout" data-content-id-name="layoutId" data-state-preference-name="layoutGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Duration" %}</th>
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
<th>{% trans "Orientation" %}</th>
<th>{% trans "Thumbnail" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Sharing" %}</th>
<th>{% trans "Valid?" %}</th>
<th>{% trans "Stats?" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Modified" %}</th>
<th>{% trans "Layout ID" %}</th>
<th>{% trans "Code" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var table;
$(document).ready(function() {
{% if not currentUser.featureEnabled("folder.view") %}
disableFolders();
{% endif %}
table = $("#layouts").DataTable({
language: dataTablesLanguage,
lengthMenu: [10, 25, 50, 100, 250, 500],
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
dataType: 'json',
order: [[1, "asc"]],
ajax: {
url: "{{ url_for("layout.search") }}",
data: function (d) {
$.extend(d, $("#layouts").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
columns: [
{"data": "campaignId", responsivePriority: 1},
{
"data": "layout",
responsivePriority: 2,
"render": dataTableSpacingPreformatted
},
{
"name": "publishedStatus",
responsivePriority: 2,
"data": function (data, type) {
if (data.publishedDate != null) {
var now = moment();
var published = moment(data.publishedDate);
var differenceMinutes = published.diff(now, 'minutes');
var momentDifference = moment(now).to(published);
if (differenceMinutes < -5) {
return data.publishedStatus.concat(" - ", translations.publishedStatusFailed);
} else {
return data.publishedStatus.concat(" - ", translations.publishedStatusFuture + " " + momentDifference);
}
} else {
return data.publishedStatus;
}
}
},
{
"name": "description",
"data": null,
responsivePriority: 10,
"render": {"_": "description", "display": "descriptionFormatted", "sort": "description"}
},
{
"name": "duration",
responsivePriority: 3,
"data": function (data, type) {
if (type != "display")
return data.duration;
return dataTableTimeFromSeconds(data.duration, type);
}
},
{% if currentUser.featureEnabled("tag.tagging") %}{
"sortable": false,
"visible": false,
responsivePriority: 3,
"data": dataTableCreateTags
},{% endif %}
{ data: 'orientation', responsivePriority: 10, visible: false},
{
responsivePriority: 5,
data: 'thumbnail',
render: function(data, type, row) {
if (type !== 'display') {
return row.layoutId;
}
if (data) {
return '<a class="img-replace" data-toggle="lightbox" data-type="image" href="' + data + '">' +
'<img class="img-fluid" src="' + data + '" alt="{{ "Thumbnail"|trans }}" />' +
'</a>';
} else {
var addUrl = '{{ url_for("layout.thumbnail.add", {id: ":id"}) }}'.replace(':id', row.layoutId);
return '<a class="img-replace generate-layout-thumbnail" data-type="image" href="' + addUrl + '">' +
'<img class="img-fluid" src="{{ theme.uri("img/thumbs/placeholder.png") }}" alt="{{ "Add Thumbnail"|trans }}" />' +
'</a>';
}
return '';
},
sortable: false
},
{"data": "owner", responsivePriority: 4},
{
"data": "groupsWithPermissions",
responsivePriority: 4,
"render": dataTableCreatePermissions
},
{
"name": "status",
responsivePriority: 3,
"data": function (data, type) {
if (type != "display")
return data.status;
var icon = "";
if (data.status == 1)
icon = "fa-check";
else if (data.status == 2)
icon = "fa-exclamation";
else if (data.status == 3)
icon = "fa-cogs";
else
icon = "fa-times";
return '<span class="fa ' + icon + '" title="' + (data.statusDescription) + ((data.statusMessage == null) ? "" : " - " + (data.statusMessage)) + '"></span>';
}
},
{
"name": "enableStat",
responsivePriority: 4,
"data": function (data) {
var icon = "";
if (data.enableStat == 1)
icon = "fa-check";
else
icon = "fa-times";
return '<span class="fa ' + icon + '" title="' + (data.enableStatDescription) + '"></span>';
}
},
{
"data": "createdDt",
responsivePriority: 6,
"render": dataTableDateFromIso,
"visible": false
},
{
data: "modifiedDt",
responsivePriority: 6,
render: dataTableDateFromIso,
visible: true
},
{
data: "layoutId",
visible: false,
responsivePriority: 4
},
{"data": "code", "visible":false, responsivePriority: 4},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', dataTableDraw);
table.on('draw', { form: $("#layouts").closest(".XiboGrid").find(".FilterDiv form") }, dataTableCreateTagEvents);
table.on('draw', function(e, settings) {
$('#' + e.target.id + ' .generate-layout-thumbnail').on('click', function(e) {
e.preventDefault();
var $anchor = $(this);
$.ajax({
url: $anchor.attr('href'),
method: 'POST',
success: function() {
$anchor.find('img').attr('src', $anchor.attr('href'));
$anchor.removeClass('generate-layout-thumbnail').attr('data-toggle', 'lightbox');
}
});
});
});
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#layouts_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function() {
table.ajax.reload();
});
// Bind to the layout add button
$('button.layout-add-button').on('click', function() {
let currentWorkingFolderId =
$("#layouts")
.closest(".XiboGrid")
.find(".FilterDiv form")
.find('#folderId').val()
// Submit the URL provided as a POST request.
$.ajax({
type: 'POST',
url: $(this).attr('href'),
cache: false,
data : {folderId : currentWorkingFolderId},
dataType: 'json',
success: function(response, textStatus, error) {
if (response.success && response.id) {
XiboRedirect('{{ url_for("layout.designer", {id: ':id'}) }}'.replace(':id', response.id));
} else {
if (response.login) {
LoginBox(response.message);
} else {
SystemMessage(response.message ?? '{{ "Unknown Error"|trans }}', false);
}
}
},
error: function(xhr, textStatus, errorThrown) {
SystemMessage(xhr.responseText, false);
},
});
});
});
$("#layoutUploadForm").click(function(e) {
e.preventDefault();
var currentWorkingFolderId = $('#folderId').val();
// Open the upload dialog with our options.
openUploadForm({
url: "{{ url_for("layout.import") }}",
title: "{{ "Upload Layout"|trans }}",
videoImageCovers: false,
buttons: {
main: {
label: "{{ "Done"|trans }}",
className: "btn-primary btn-bb-main",
callback: function () {
table.ajax.reload();
XiboDialogClose();
}
}
},
templateOptions: {
layoutImport: true,
updateInAllChecked: {% if settings.LIBRARY_MEDIA_UPDATEINALL_CHECKB == 1 %}true{% else %}false{% endif %},
deleteOldRevisionsChecked: {% if settings.LIBRARY_MEDIA_DELETEOLDVER_CHECKB == 1 %}true{% else %}false{% endif %},
trans: {
addFiles: "{{ "Add Layout Export ZIP Files"|trans }}",
startUpload: "{{ "Start Import"|trans }}",
cancelUpload: "{{ "Cancel Import"|trans }}",
replaceExistingMediaMessage: "{{ "Replace Existing Media?"|trans }}",
importTagsMessage: "{{ "Import Tags?"|trans }}",
useExistingDataSetsMessage: "{{ "Use existing DataSets matched by name?"|trans }}",
dataSetDataMessage: "{{ "Import DataSet Data?"|trans }}",
fallbackMessage: "{{ "Import Widget Fallback Data?"|trans }}",
selectFolder: "{{ "Select Folder"|trans }}",
selectFolderTitle: "{{ "Change Current Folder location"|trans }}",
selectedFolder: "{{ "Current Folder"|trans }}:",
selectedFolderTitle: "{{ "Upload files to this Folder"|trans }}"
},
upload: {
maxSize: {{ libraryUpload.maxSize }},
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
validExt: "zip"
},
currentWorkingFolderId: currentWorkingFolderId,
folderSelector: true
},
formOpenedEvent: function () {
// Configure the active behaviour of the checkboxes
$("#useExistingDataSets").on("click", function () {
$("#importDataSetData").prop("disabled", ($(this).is(":checked")));
});
},
uploadDoneEvent: function (data) {
XiboDialogClose();
table.ajax.reload();
}
});
});
function layoutExportFormSubmit() {
var $form = $("#layoutExportForm");
window.location = $form.attr("action") + "?" + $form.serialize();
setTimeout(function() {
XiboDialogClose();
}, 1000);
}
function assignLayoutToCampaignFormSubmit() {
var form = $("#layoutAssignCampaignForm");
var url = form.prop("action").replace(":id", form.find("#campaignId").val());
$.ajax({
type: form.attr("method"),
url: url,
data: {layoutId: form.data().layoutId},
cache: false,
dataType:"json",
success: XiboSubmitResponse
});
}
function setEnableStatMultiSelectFormOpen(dialog) {
var $input = $('<input type=checkbox id="enableStat" name="enableStat"> {{ "Enable Stats Collection?"|trans }} </input>');
var $helpText = $('<span class="help-block">{{ "Check to enable the collection of Proof of Play statistics for the selected items."|trans }}</span>');
$input.on('change', function() {
dialog.data().commitData = {enableStat: $(this).val()};
});
$(dialog).find('.modal-body').append($input);
$(dialog).find('.modal-body').append($helpText);
}
function layoutPublishFormOpen() {
// Nothing to do here, but we use the same form on the layout designer and have a callback registered there
}
function layoutEditFormSaved() {
// Nothing to do here.
}
</script>
{% endblock %}

View File

@@ -0,0 +1,576 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Library"|trans }} | {% endblock %}
{% block actionMenu %}
<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-danger XiboFormButton" title="{% trans "Run through the library and remove unused and unnecessary files" %}" href="{{ url_for("library.tidy.form") }}"><i class="fa fa-trash" aria-hidden="true"></i></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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Media" %}</h1>
<p class="text-muted">{% trans "Manage your media library." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="libraryView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Media" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "ID" %}{% endset %}
{{ inline.number("mediaId", title) }}
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('media', title) }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{% set attributes = [
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" }
] %}
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
{ name: "data-initial-key", value: "userId" },
] %}
{{ inline.dropdown("ownerId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
{% set title %}{% trans "Owner User Group" %}{% endset %}
{% set helpText %}{% trans "Show items owned by users in the selected User Group." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("group.search") },
{ name: "data-search-term", value: "group" },
{ name: "data-id-property", value: "groupId" },
{ name: "data-text-property", value: "group" },
{ name: "data-initial-key", value: "userGroupId" },
] %}
{{ inline.dropdown("ownerUserGroupId", "single", title, "", null, "groupId", "group", helpText, "pagedSelect", "", "", "", attributes) }}
{% set title %}{% trans "Type" %}{% endset %}
{{ inline.dropdown("type", "single", title, "", [{"type": none, "name": ""}]|merge(modules), "type", "name") }}
{% set title %}{% trans "Retired" %}{% endset %}
{% set values = [{id: 0, value: "No"}, {id: 1, value: "Yes"}] %}
{{ inline.dropdown("retired", "single", title, 0, values, "id", "value") }}
{{ inline.hidden("folderId") }}
{% set title %}{% trans "Layout ID" %}{% endset %}
{{ inline.number("layoutId", title, layoutId) }}
{% set title %}{% trans "Orientation" %}{% endset %}
{% set option1 = "All"|trans %}
{% set option2 = "Landscape"|trans %}
{% set option3 = "Portrait"|trans %}
{% set values = [{id: '', value: option1}, {id: 'landscape', value: option2}, {id: 'portrait', value: option3}] %}
{{ inline.dropdown("orientation", "single", title, '', values, "id", "value") }}
</form>
</div>
</div>
</div>
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 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">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div 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">
<table id="libraryItems" class="table table-striped responsive nowrap" data-content-type="media" data-content-id-name="mediaId" data-state-preference-name="libraryGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Type" %}</th>
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tag" %}</th>{% endif %}
<th>{% trans "Thumbnail" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Duration (seconds)" %}</th>
<th>{% trans "Size" %}</th>
<th>{% trans "Size (bytes)" %}</th>
<th>{% trans "Resolution" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Sharing" %}</th>
<th>{% trans "Revised" %}</th>
<th>{% trans "Released" %}</th>
<th>{% trans "File Name" %}</th>
<th>{% trans "Stats?" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Modified" %}</th>
<th>{% trans "Expires" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var table;
$(document).ready(function() {
{% if not currentUser.featureEnabled("folder.view") %}
disableFolders();
{% endif %}
table = $("#libraryItems").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
responsive: true,
stateDuration: 0,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[1, "asc"]],
ajax: {
"url": "{{ url_for("library.search") }}",
"data": function (d) {
$.extend(d, $("#libraryItems").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{"data": "mediaId", responsivePriority: 2},
{"data": "name", "render": dataTableSpacingPreformatted, responsivePriority: 3 },
{"data": "mediaType", responsivePriority: 2},
{% if currentUser.featureEnabled("tag.tagging") %}{
"sortable": false,
responsivePriority: 2,
"visible": false,
"data": dataTableCreateTags
},{% endif %}
{
responsivePriority: 5,
data: 'thumbnail',
render: function(data, type, row) {
if (type !== 'display') {
return row.mediaId;
}
if (data) {
return '<a class="img-replace" data-toggle="lightbox" data-type="image" href="' + data + '">' +
'<img class="img-fluid" src="' + data.replace('download', 'thumbnail') + '" alt="{{ "Thumbnail"|trans }}" />' +
'</a>';
}
return '';
},
sortable: false
},
{
"name": "duration",
responsivePriority: 3,
"data": function (data, type) {
if (type != "display")
return data.duration;
return dataTableTimeFromSeconds(data.duration, type);
}
},
{"data": "duration", "visible": false, responsivePriority: 10},
{
"name": "fileSize",
responsivePriority: 3,
"data": null,
"render": {"_": "fileSize", "display": "fileSizeFormatted", "sort": "fileSize"}
},
{"data": "fileSize", "visible": false, responsivePriority: 10},
{
name: 'width',
data: function(data, type, row, meta) {
if (type !== 'display' || data.width === 0 || data.height === 0) {
return '';
}
return data.width + 'x' + data.height;
},
visible: false,
responsivePriority: 10
},
{"data": "owner", responsivePriority: 5},
{
"data": "groupsWithPermissions",
responsivePriority: 5,
"render": dataTableCreatePermissions
},
{"data": "revised", "render": dataTableTickCrossColumn, "visible": false, responsivePriority: 6},
{
"name": "released",
responsivePriority: 6,
"data": function (data, type) {
if (type != "display")
return data.released;
var icon = "";
if (data.released == 1)
icon = "fa-check";
else if (data.released == 0)
icon = "fa-cogs";
else if (data.released == 2)
icon = "fa-times";
return '<span class="fa ' + icon + '" title="' + (data.releasedDescription) + '"></span>';
},
"visible": false
},
{"data": "fileName", responsivePriority: 500},
{
"name": "enableStat",
responsivePriority: 6,
"data": function (data) {
var icon = "";
if (data.enableStat == 'On')
icon = "fa-check";
else if (data.enableStat == 'Off')
icon = "fa-times";
else
icon = "fa-level-down";
return '<span class="fa ' + icon + '" title="' + (data.enableStatDescription) + '"></span>';
}
},
{
"data": "createdDt",
responsivePriority: 6,
"render": dataTableDateFromIso,
"visible": false
},
{
"data": "modifiedDt",
responsivePriority: 6,
"render": dataTableDateFromIso,
"visible": false
},
{
"name": "expires",
responsivePriority: 6,
"data": function (data, type) {
if (data.expires != null && data.expires != 0) {
var now = moment();
var expiresIn = moment.unix(data.expires);
var differenceMinutes = expiresIn.diff(now, 'minutes');
var momentDifference = moment(now).to(expiresIn);
if (differenceMinutes < -10 ) {
return data.mediaExpiryFailed;
} else {
return data.mediaExpiresIn.replace('%s', momentDifference);
}
} else {
return data.mediaNoExpiryDate;
}
}
},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', dataTableDraw);
table.on('draw', { form: $("#libraryItems").closest(".XiboGrid").find(".FilterDiv form") } ,dataTableCreateTagEvents);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#libraryItems_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
});
$("#libraryUploadForm").click(function(e) {
e.preventDefault();
var currentWorkingFolderId = $('#folderId').val();
openUploadForm({
url: "{{ url_for("library.add") }}",
title: "{% trans "Add Media" %}",
initialisedBy: "library-upload",
buttons: {
main: {
label: "{% trans "Done" %}",
className: "btn-primary btn-bb-main",
callback: function () {
table.ajax.reload();
XiboDialogClose();
}
}
},
templateOptions: {
trans: {
addFiles: "{% trans "Add files" %}",
startUpload: "{% trans "Start upload" %}",
cancelUpload: "{% trans "Cancel upload" %}",
selectFolder: "{% trans "Select Folder" %}",
selectFolderTitle: "{% trans "Change Current Folder location" %}",
selectedFolder: "{% trans "Current Folder" %}:",
selectedFolderTitle: "{% trans "Upload files to this Folder" %}",
},
upload: {
maxSize: {{ libraryUpload.maxSize }},
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
validExt: "{{ validExt }}"
},
updateInAllChecked: {% if settings.LIBRARY_MEDIA_UPDATEINALL_CHECKB == 1 %}true{% else %}false{% endif %},
deleteOldRevisionsChecked: {% if settings.LIBRARY_MEDIA_DELETEOLDVER_CHECKB == 1 %}true{% else %}false{% endif %},
currentWorkingFolderId: currentWorkingFolderId,
folderSelector: true
}
});
});
/**
* Media Edit form
*/
function mediaEditFormOpen(dialog) {
// Create a new button
var footer = dialog.find(".modal-footer");
var mediaId = dialog.find("#mediaEditForm").data().mediaId;
var validExtensions = dialog.find("#mediaEditForm").data().validExtensions;
var folderId = dialog.find("#mediaEditForm").data().folderId;
// Append
var replaceButton = $('<button class="btn btn-warning">{% trans "Replace" %}</button>');
replaceButton.click(function(e) {
e.preventDefault();
// Open the upload dialog with our options.
openUploadForm({
url: "{{ url_for("library.add") }}",
title: "{% trans "Upload media" %}",
buttons: {
main: {
label: "{% trans "Done" %}",
className: "btn-primary btn-bb-main",
callback: function () {
table.ajax.reload();
XiboDialogClose();
}
}
},
templateOptions: {
multi: false,
oldMediaId: mediaId,
oldFolderId: folderId,
updateInAllChecked: {% if settings.LIBRARY_MEDIA_UPDATEINALL_CHECKB == 1 %}true{% else %}false{% endif %},
deleteOldRevisionsChecked: {% if settings.LIBRARY_MEDIA_DELETEOLDVER_CHECKB == 1 %}true{% else %}false{% endif %},
trans: {
addFiles: "{% trans "Add Replacement" %}",
startUpload: "{% trans "Start Replace" %}",
cancelUpload: "{% trans "Cancel Replace" %}",
updateInLayouts: {
title: "{% trans "Update this media in all layouts it is assigned to?" %}",
helpText: "{% trans "Note: It will only be updated in layouts you have permission to edit." %}"
},
deleteOldRevisions: {
title: "{% trans "Delete the old version?" %}",
helpText: "{% trans "Completely remove the old version of this media item if a new file is being uploaded." %}"
}
},
upload: {
maxSize: {{ libraryUpload.maxSize }},
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
validExt: validExtensions,
validExtensionsMessage: "{{ "Valid extensions are %s" }}".replace("%s", validExtensions).replace(/\|/g, ", ")
}
},
uploadDoneEvent: function () {
XiboDialogClose();
table.ajax.reload();
}
});
});
footer.find(".btn-primary").before(replaceButton);
}
///
/// Library Usage Form
///
function usageFormOpen(dialog) {
// Displays tab
var usageTable = $("#usageReportTable").DataTable({
"language": dataTablesLanguage,
serverSide: true,
stateSave: true, stateDuration: 0,
filter: false,
searchDelay: 3000,
responsive: true,
"order": [[1, "asc"]],
ajax: {
"url": "{{ url_for("library.usage", {id: ':id'}) }}".replace(":id", $("#usageReportTable").data().mediaId),
"data": function(dataDisplay) {
$.extend(dataDisplay, $(dialog).find("#usageReportForm").serializeObject());
return dataDisplay;
}
},
"columns": [
{ "data": "displayId"},
{ "data": "display" },
{ "data": "description" }
]
});
usageTable.on('draw', dataTableDraw);
usageTable.on('processing.dt', dataTableProcessing);
// Layouts tab
var usageTableLayouts = $("#usageReportLayoutsTable").DataTable({
"language": dataTablesLanguage,
serverSide: true,
stateSave: true, stateDuration: 0,
filter: false,
searchDelay: 3000,
responsive: true,
"order": [[1, "asc"]],
ajax: {
"url": "{{ url_for("library.usage.layouts", {id: ':id'}) }}".replace(":id", $("#usageReportLayoutsTable").data().mediaId)
},
"columns": [
{ "data": "layoutId"},
{ "data": "layout" },
{ "data": "description" },
{
"orderable": false,
"data": dataTableButtonsColumn
}
]
});
usageTableLayouts.on('draw', dataTableDraw);
usageTableLayouts.on('processing.dt', dataTableProcessing);
}
function setDefaultMultiSelectFormOpen(dialog) {
{% set message = 'Force delete from any existing layouts, assignments, etc' %}
{% set message2 = 'Notify each Display that has this Media in its local storage to remove it immediately?' %}
var $input = $(
'<div class="form-group">' +
'<input type=checkbox id="forceDelete" name="forceDelete"> {{ message|trans|e }} </input>' +
'</div>'
);
var $input2 = $(
'<div class="form-group">' +
'<input type=checkbox id="purge" name="purge"> {{ message2|trans|e }} </input>' +
'</div>'
);
$(dialog).find('.modal-body').append($input, $input2);
$('#forceDelete, #purge').on('change', function() {
dialog.data().commitData = {
forceDelete: $('#forceDelete').val(),
purge: $('#purge').val()
};
});
}
function setEnableStatMultiSelectFormOpen(dialog) {
var $select = $('<select id="enableStat" name="enableStat" class="form-control">' +
'<option value="Off">{% trans %} Off {% endtrans %}</option>' +
'<option value="On">{% trans %} On {% endtrans %}</option>' +
'<option value="Inherit">{% trans %} Inherit {% endtrans %}</option>' +
'</select>');
$select.on('change', function() {
dialog.data().commitData = {enableStat: $(this).val()};
}).trigger('change');
$(dialog).find('.modal-body').append($select);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,198 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Menu Boards"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Menu Boards" %}</h1>
<p class="text-muted">{% trans "Manage your menu boards and content." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-type="menuBoard" data-grid-name="menuBoardView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Menu Boards" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-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("menuId", title) }}
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('name', title) }}
{% set title %}{% trans "Code" %}{% endset %}
{{ inline.input('code', title) }}
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
{ name: "data-initial-key", value: "userId" },
] %}
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
{{ inline.hidden("folderId") }}
</form>
</div>
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<table id="menuBoards" class="table table-striped responsive nowrap" data-content-type="menuBoard" data-content-id-name="menuId" data-state-preference-name="menuBoardGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Code" %}</th>
<th>{% trans "Modified Date" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Permissions" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var table;
$(document).ready(function() {
{% if not currentUser.featureEnabled("folder.view") %}
disableFolders();
{% endif %}
table = $("#menuBoards").DataTable({
"language": dataTablesLanguage,
"lengthMenu": [10, 25, 50, 100, 250, 500],
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
dataType: 'json',
"order": [[1, "asc"]],
ajax: {
url: "{{ url_for("menuBoard.search") }}",
"data": function (d) {
$.extend(d, $("#menuBoards").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{"data": "menuId", responsivePriority: 2},
{
"data": "name",
responsivePriority: 2,
"render": dataTableSpacingPreformatted
},
{
"data": "description",
responsivePriority: 2,
"render": dataTableSpacingPreformatted
},
{
"data": "code", responsivePriority: 3
},
{
"name": "modifiedDt",
"data": function (data) {
return moment.unix(data.modifiedDt).format(jsDateFormat);
}
},
{"data": "owner", responsivePriority: 4},
{
"data": "groupsWithPermissions",
responsivePriority: 4,
"render": dataTableCreatePermissions
},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#menuBoards_wrapper').find('.col-md-6').eq(1));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
});
</script>
{% endblock %}

View File

@@ -161,7 +161,7 @@ body {
grid-template-columns: 20px 1fr;
align-items: center;
column-gap: 12px;
padding: 8px 12px;
padding: 6px 10px;
color: #c8d5ee;
text-decoration: none;
transition: all var(--transition-fast);
@@ -1191,12 +1191,13 @@ body .panel .panel-heading,
letter-spacing: 0.02em;
}
/* Filter card - modern container */
.ots-filter-card {
background: linear-gradient(180deg, rgba(30, 41, 59, 0.95), rgba(15, 23, 42, 0.92));
border: 1px solid rgba(148, 163, 184, 0.22);
box-shadow: 0 18px 34px rgba(8, 15, 30, 0.32);
background: linear-gradient(135deg, rgba(15, 23, 42, 0.4), rgba(15, 23, 42, 0.2));
border: none;
box-shadow: 0 4px 20px rgba(6, 10, 20, 0.15);
margin-bottom: 0;
border-radius: 12px;
border-radius: 16px;
overflow: hidden;
}
@@ -1205,109 +1206,260 @@ body .panel .panel-heading,
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
background: rgba(15, 23, 42, 0.3);
gap: 12px;
padding: 12px 16px;
border: none;
background: transparent;
}
.ots-filter-title {
font-weight: 600;
font-weight: 700;
color: var(--color-text-primary);
font-size: 14px;
letter-spacing: 0.03em;
text-transform: uppercase;
margin: 0;
}
.ots-filter-toggle {
width: 32px;
height: 32px;
width: 36px;
height: 36px;
padding: 0;
border: none;
background: transparent;
background: rgba(59, 130, 246, 0.08);
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all var(--transition-fast);
border-radius: 10px;
transition: all 200ms ease;
}
.ots-filter-toggle:hover {
background: rgba(59, 130, 246, 0.1);
background: rgba(59, 130, 246, 0.16);
color: var(--color-primary);
}
.ots-filter-content {
padding: 16px;
max-height: 600px;
overflow: hidden;
padding: 0 16px 12px 16px;
max-height: none;
min-height: auto;
overflow: visible;
transition: max-height 300ms ease-out, padding 300ms ease-out;
display: block;
}
.ots-filter-content.collapsed {
max-height: 0;
min-height: 0;
padding: 0 16px;
overflow: hidden;
display: none;
}
.ots-filter-card .nav-tabs {
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
gap: 8px;
margin-bottom: 14px;
display: none;
}
.ots-filter-card .nav-tabs .nav-link {
color: var(--color-text-secondary);
border: 0;
border-radius: 8px;
padding: 10px 14px;
background: transparent;
font-size: 13px;
font-weight: 500;
transition: all var(--transition-fast);
.ots-filter-card .tab-content {
display: block;
}
.ots-filter-card .nav-tabs .nav-link.active,
.ots-filter-card .nav-tabs .nav-link:hover {
color: var(--color-text-primary);
background: rgba(59, 130, 246, 0.12);
.ots-filter-card .tab-pane {
display: none;
}
.ots-filter-card .tab-pane.active,
.ots-filter-card .tab-pane.show {
display: block;
}
.ots-filter-card .form-inline {
display: flex;
flex-wrap: nowrap;
gap: 12px;
align-items: flex-end;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
padding-bottom: 4px;
}
.ots-filter-card .form-inline .form-group,
.ots-filter-card .form-inline .input-group {
margin-right: 16px;
margin-bottom: 12px;
margin-right: 0;
margin-bottom: 0;
flex: 0 0 auto;
min-width: 180px;
}
.ots-filter-card .form-control,
.ots-filter-card select,
.ots-filter-card .select2-selection,
.ots-filter-card .input-group-addon {
background: var(--color-surface) !important;
border: 1px solid var(--color-border) !important;
.ots-filter-card .input-group-addon,
.ots-filter-card .input-group-text {
background: linear-gradient(180deg, rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.65)) !important;
border: 1px solid rgba(148, 163, 184, 0.25) !important;
color: var(--color-text-primary) !important;
border-radius: 6px !important;
padding: 8px 12px !important;
font-size: 13px !important;
transition: all var(--transition-fast) !important;
height: 36px !important;
border-radius: 10px !important;
padding: 12px 14px !important;
font-size: 15px !important;
font-family: inherit !important;
transition: border 150ms ease, box-shadow 150ms ease !important;
height: 48px !important;
line-height: 1.4 !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), 0 6px 16px rgba(6, 10, 20, 0.18) !important;
box-sizing: border-box !important;
}
.ots-filter-card .input-group {
display: flex;
align-items: center;
gap: 6px;
}
.ots-filter-card .input-group > .form-control,
.ots-filter-card .input-group > .custom-select,
.ots-filter-card .input-group > .select2-container {
min-width: 0;
flex: 1 1 auto;
}
.ots-filter-card .form-control:focus,
.ots-filter-card select:focus {
border-color: var(--color-primary) !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1) !important;
background: var(--color-surface) !important;
.ots-filter-card select:focus,
.ots-filter-card .select2-selection:focus {
border-color: rgba(96, 165, 250, 0.7) !important;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2), 0 10px 24px rgba(6, 10, 20, 0.25) !important;
background: rgba(15, 23, 42, 0.9) !important;
}
.ots-filter-card label {
color: var(--color-text-secondary);
font-size: 12px;
font-weight: 500;
margin-bottom: 4px;
color: var(--color-text-tertiary);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 2px;
display: block;
}
.ots-filter-card .select2-selection__rendered {
color: var(--color-text-primary) !important;
line-height: 20px !important;
}
.ots-filter-card .select2-container--default .select2-selection--single,
.ots-filter-card .select2-container--default .select2-selection--multiple {
background: linear-gradient(180deg, rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.65)) !important;
border: 1px solid rgba(148, 163, 184, 0.25) !important;
border-radius: 10px !important;
min-height: 44px !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), 0 6px 16px rgba(6, 10, 20, 0.18) !important;
}
.ots-filter-card .select2-container--default .select2-selection--multiple .select2-search__field {
color: var(--color-text-primary) !important;
background: transparent !important;
}
.ots-filter-card .select2-container--default .select2-selection--multiple .select2-selection__choice {
background: rgba(59, 130, 246, 0.2) !important;
border: 1px solid rgba(59, 130, 246, 0.4) !important;
color: var(--color-text-primary) !important;
border-radius: 999px !important;
}
.ots-filter-card .bootstrap-tagsinput,
.ots-filter-card .tagsinput {
background: linear-gradient(180deg, rgba(15, 23, 42, 0.85), rgba(15, 23, 42, 0.65)) !important;
border: 1px solid rgba(148, 163, 184, 0.25) !important;
border-radius: 10px !important;
color: var(--color-text-primary) !important;
min-height: 44px !important;
padding: 6px 10px !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), 0 6px 16px rgba(6, 10, 20, 0.18) !important;
}
.ots-filter-card .bootstrap-tagsinput input,
.ots-filter-card .tagsinput input {
background: transparent !important;
color: var(--color-text-primary) !important;
}
.ots-filter-card .select2-selection__arrow b {
border-color: var(--color-text-secondary) transparent transparent transparent !important;
}
.ots-filter-card .input-group {
background: transparent;
}
.ots-filter-card .input-group .form-control {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.ots-filter-card .input-group-append .input-group-text,
.ots-filter-card .input-group-addon {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
font-weight: 600;
height: 44px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 12px !important;
}
.ots-filter-card .input-group .input-group-prepend .input-group-text,
.ots-filter-card .input-group .input-group-append .input-group-text {
border-radius: 10px !important;
}
.ots-filter-card .input-group .btn,
.ots-filter-card .input-group .btn.btn-secondary {
height: 44px;
padding: 0 14px;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.25);
background: rgba(30, 41, 59, 0.55);
color: var(--color-text-primary);
font-weight: 600;
letter-spacing: 0.02em;
}
.ots-filter-card input::placeholder,
.ots-filter-card textarea::placeholder {
color: rgba(148, 163, 184, 0.7) !important;
}
.ots-filter-card select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: linear-gradient(45deg, transparent 50%, rgba(148, 163, 184, 0.7) 50%),
linear-gradient(135deg, rgba(148, 163, 184, 0.7) 50%, transparent 50%);
background-position: calc(100% - 16px) 17px, calc(100% - 12px) 17px;
background-size: 4px 4px, 4px 4px;
background-repeat: no-repeat;
padding-right: 28px !important;
}
.ots-filter-card .form-check-input {
width: 18px;
height: 18px;
border-radius: 6px;
border: 1px solid rgba(148, 163, 184, 0.35);
background: rgba(15, 23, 42, 0.65);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
}
.ots-filter-card .form-check-input:checked {
background: var(--color-primary);
border-color: rgba(96, 165, 250, 0.8);
}
.ots-grid-with-folders {
display: grid;
grid-template-columns: 260px 1fr;
@@ -2251,17 +2403,34 @@ legend {
/* Dropdowns, modals, and popovers */
.dropdown-menu,
.dropdown-toggle,
.popover,
.modal,
.modal-content,
.modal-header,
.modal-body,
.modal-footer {
.popover {
background-color: var(--color-surface) !important;
color: var(--color-text-primary) !important;
border: 1px solid var(--color-border) !important;
}
.modal,
.modal-body,
.modal-footer {
background-color: transparent !important;
color: var(--color-text-primary) !important;
border: 1px solid var(--color-border) !important;
}
.modal-content {
background-color: var(--color-surface) !important;
color: var(--color-text-primary) !important;
border: 1px solid var(--color-border) !important;
}
.modal-backdrop,
.modal-backdrop.show,
.modal-backdrop.in {
background-color: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(4px) !important;
opacity: 1 !important;
}
.modal-header {
background-color: var(--color-surface-elevated) !important;
border-bottom: 1px solid var(--color-border) !important;
@@ -3070,6 +3239,22 @@ textarea:focus {
.modal-content {
border-radius: var(--ots-radius-lg);
background-color: var(--ots-surface-2) !important;
}
.modal,
.modal-header,
.modal-body,
.modal-footer {
background-color: transparent !important;
}
.modal-backdrop,
.modal-backdrop.show,
.modal-backdrop.in {
background-color: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(4px) !important;
opacity: 1 !important;
}
.modal-footer {

View File

@@ -0,0 +1,198 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Player Versions" %}</h1>
<p class="text-muted">{% trans "Manage player software versions and downloads." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="playerSoftwareView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Player Versions" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "Type" %}{% endset %}
{{ inline.dropdown("playerType", "single", title, "", [{"type": none, "typeShow": none}]|merge(types), "type", "typeShow") }}
{% set title %}{% trans "Version" %}{% endset %}
{{ inline.dropdown("playerVersion", "single", title, "", [{"version": none, "version": none}]|merge(versions), "version", "version") }}
{% set title %}{% trans "Code" %}{% endset %}
{{ inline.input("playerCode", title) }}
</form>
</div>
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<table id="playerSoftwareItems" class="table table-striped" data-state-preference-name="playerSoftwareGrid">
<thead>
<tr>
<th>{% trans "Version ID" %}</th>
<th>{% trans "Player Version Name" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Version" %}</th>
<th>{% trans "Code" %}</th>
<th>{% trans "File Name" %}</th>
<th>{% trans "Size" %}</th>
<th>{% trans "Created At" %}</th>
<th>{% trans "Modified At" %}</th>
<th>{% trans "Modified By" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var table;
$(document).ready(function() {
table = $("#playerSoftwareItems").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[2, "asc"]],
ajax: {
"url": "{{ url_for("playersoftware.search") }}",
"data": function (d) {
$.extend(d, $("#playerSoftwareItems").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{"data": "versionId", responsivePriority: 2},
{"data": "playerShowVersion", responsivePriority: 2},
{"data": "type", responsivePriority: 2},
{"data": "version", responsivePriority: 2},
{"data": "code", responsivePriority: 2},
{"data": "fileName", responsivePriority: 4},
{
"name": "size",
responsivePriority: 3,
"data": null,
"render": {"_": "size", "display": "fileSizeFormatted", "sort": "size"}
},
{"data": "createdAt", responsivePriority: 6, visible: false},
{"data": "modifiedAt", responsivePriority: 6, visible: false},
{"data": "modifiedBy", responsivePriority: 6, visible: false},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
],
createdRow: function (row, data, index) {
if (data.version === "" || data.version === null || data.code === 0) {
$(row).addClass('table-danger');
$(row).attr('Title', "{{ "Please set Player Software Version"|trans }}");
}
},
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#playerSoftwareItems_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
});
$("#playerSoftwareUploadForm").click(function(e) {
e.preventDefault();
openUploadForm({
url: "{{ url_for("playersoftware.add") }}",
title: "{% trans "Upload Version" %}",
videoImageCovers: false,
buttons: {
main: {
label: "{% trans "Done" %}",
className: "btn-primary btn-bb-main",
callback: function () {
table.ajax.reload();
XiboDialogClose();
}
}
},
templateOptions: {
includeTagsInput: false,
multi: false,
trans: {
addFiles: "{% trans "Add files" %}",
startUpload: "{% trans "Start upload" %}",
cancelUpload: "{% trans "Cancel upload" %}",
processing: "{% trans "Processing..." %}"
},
upload: {
maxSize: {{ libraryUpload.maxSize }},
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
validExt: "{{ validExt }}"
},
updateInAllChecked: false,
deleteOldRevisionsChecked: false,
folderSelector: false
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,554 @@
{#
* Copyright (C) 2021 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Playlists"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Playlists" %}</h1>
<p class="text-muted">{% trans "Create and manage content playlists." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="playlistView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Playlists" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item"><a class="nav-link active" href="#general-filter" role="tab" data-toggle="tab"><span>{% trans "General" %}</span></a></li>
<li class="nav-item"><a class="nav-link" href="#advanced-filter" role="tab" data-toggle="tab"><span>{% trans "Advanced" %}</span></a></li>
</ul>
<form class="form-inline">
<div class="tab-content">
<div class="tab-pane active" id="general-filter">
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('name', title) }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{% set attributes = [
{ name: "data-live-search", value: "true" },
{ name: "data-selected-text-format", value: "count > 4" }
] %}
{% set title %}{% trans "Owner" %}{% endset %}
{% set helpText %}{% trans "Show items owned by the selected User." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
{ name: "data-initial-key", value: "userId" },
] %}
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", helpText, "pagedSelect", "", "", "", attributes) }}
{% set title %}{% trans "Owner User Group" %}{% endset %}
{% set helpText %}{% trans "Show items owned by users in the selected User Group." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("group.search") },
{ name: "data-search-term", value: "group" },
{ name: "data-id-property", value: "groupId" },
{ name: "data-text-property", value: "group" },
{ name: "data-initial-key", value: "userGroupId" },
] %}
{{ inline.dropdown("ownerUserGroupId", "single", title, "", null, "groupId", "group", helpText, "pagedSelect", "", "", "", attributes) }}
{{ inline.hidden("folderId") }}
{% set title %}{% trans "Layout ID" %}{% endset %}
{{ inline.number("layoutId", title, layoutId) }}
</div>
<div class="tab-pane" id="advanced-filter">
{% set title %}{% trans "Show" %}{% endset %}
{% set values = [{id: 1, value: "All"}, {id: 2, value: "Only Used"}, {id: 3, value: "Only Unused"}] %}
{{ inline.dropdown("playlistStatusId", "single", title, 1, values, "id", "value") }}
{% if currentUser.featureEnabled("library.view") %}
{% set title %}{% trans "Media" %}{% endset %}
{{ inline.input("mediaLike", title) }}
{% endif %}
</div>
</div>
</form>
</div>
</div>
</div>
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 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">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div 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">
<table id="playlists" class="table table-striped" data-content-type="playlist"
data-content-id-name="playlistId" data-state-preference-name="playlistGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Duration" %}</th>
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
<th>{% trans "Dynamic?" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Sharing" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Modified" %}</th>
<th>{% trans "Stats?" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="dummyLayout" style="display:none"></div>
<div id="editor-container"></div>
<div class="loading-overlay">
<i class="fa fa-spinner fa-spin loading-icon"></i>
</div>
{% endblock %}
{% block javaScript %}
{# Add common files #}
{% include "editorTranslations.twig" %}
{% include "editorVars.twig" %}
<script src="{{ theme.rootUri() }}dist/playlistEditor.bundle.min.js?v={{ version }}&rev={{ revision }}" nonce="{{ cspNonce }}"></script>
<script src="{{ theme.rootUri() }}dist/codeEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
<script src="{{ theme.rootUri() }}dist/wysiwygEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
<script src="{{ theme.rootUri() }}dist/editorCommon.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
<script type="text/javascript" nonce="{{ cspNonce }}">
{# Custom translations #}
{% autoescape "js" %}
{# Insert custom translations here #}
{% endautoescape %}
var table;
$(document).ready(function () {
{% if not currentUser.featureEnabled("folder.view") %}
disableFolders();
{% endif %}
// Create ourselves a little hidden layout for preview sizing, etc
$("#dummyLayout").html('<div id="layout" data-background-color="#000000" style="background-color: #000000" designer_scale="1"><div id="region_-1" zindex="1" tip_scale="1" designer_scale="1" width="800" height="450"></div></div>');
// Configure the DataTable
table = $("#playlists").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
"lengthMenu": [10, 25, 50, 100, 250, 500],
serverSide: true,
stateSave: true,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[1, "asc"]],
ajax: {
url: "{{ url_for("playlist.search") }}",
"data": function (d) {
$.extend(d, $("#playlists").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{"data": "playlistId", responsivePriority: 2},
{
"data": "name",
responsivePriority: 3,
"render": dataTableSpacingPreformatted
},
{
"data": "duration",
responsivePriority: 3,
"render": function (data, type, row) {
if (type !== "display" && type !== "export")
return data;
if (row.requiresDurationUpdate === 1) {
return '<span class="fa fa-clock-o" title="{{ "Changes have been made and we are recalculating this Playlists duration" }}"></span>';
} else if (row.requiresDurationUpdate !== 0) {
return moment().startOf("day").seconds(data).format("H:mm:ss") + ' <span class="fa fa-clock-o" title="{{ "This duration will be updated at " }}' + moment(row.requiresDurationUpdate, "X").format(jsDateFormat) + '"></span>';
}
return dataTableTimeFromSeconds(data, type, row);
}
},
{% if currentUser.featureEnabled("tag.tagging") %}{
"sortable": false,
"visible": false,
responsivePriority: 4,
"data": dataTableCreateTags
},{% endif %}
{"data": "isDynamic", "render": dataTableTickCrossColumn, responsivePriority: 4},
{"data": "owner", responsivePriority: 4},
{
"data": "groupsWithPermissions",
responsivePriority: 5,
"render": dataTableCreatePermissions
},
{
"data": "createdDt",
responsivePriority: 6,
"render": dataTableDateFromIso,
"visible": false
},
{
"data": "modifiedDt",
responsivePriority: 6,
"render": dataTableDateFromIso,
"visible": false
},
{
"name": "enableStat",
responsivePriority: 6,
"data": function (data) {
var icon = "";
if (data.enableStat == 'On')
icon = "fa-check";
else if (data.enableStat == 'Off')
icon = "fa-times";
else
icon = "fa-level-down";
return '<span class="fa ' + icon + '" title="' + (data.enableStatDescription) + '"></span>';
}
},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', dataTableDraw);
table.on('draw', {form: $("#playlists").closest(".XiboGrid").find(".FilterDiv form")}, dataTableCreateTagEvents);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#playlists_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
});
// Playlist Add Form
// contains a grid on the populate tab
// hook up the grid
var mediaTable;
var nameFilter;
var tagFilter;
var exactTags;
var logicalOperator;
var logicalOperatorName;
var filterFolderId;
function playlistEditorFormOpen(formData) {
// Clear container
$('#editor-container').empty();
// Append form
$('#editor-container').append(formData.message);
}
function playlistFormOpen(dialog) {
mediaTable = null;
$(dialog).find("input[name=filterMediaName]").on("keyup", _.debounce(function () {
playlistFormPopulateMediaTable(dialog);
}, 500));
$(dialog).find("input[name=filterMediaTag], input[name=exactTags], select[name=logicalOperator], select[name=logicalOperatorName], select[name=filterFolderId]").on("change", function () {
playlistFormPopulateMediaTable(dialog);
});
// First time in there
playlistFormPopulateMediaTable(dialog);
// Run function to set the form submit behaviour
playlistAddFormOpen();
}
///
/// Playlist Usage Form
///
function usageFormOpen(dialog) {
// Displays tab
var usageTable = $("#usageReportTable").DataTable({
"language": dataTablesLanguage,
serverSide: true,
stateSave: true, stateDuration: 0,
filter: false,
searchDelay: 3000,
responsive: true,
"order": [[1, "asc"]],
ajax: {
"url": "{{ url_for("playlist.usage", {id:':id'}) }}".replace(":id", $("#usageReportTable").data().playlistId),
"data": function (dataDisplay) {
$.extend(dataDisplay, $(dialog).find("#usageReportForm").serializeObject());
return dataDisplay;
}
},
"columns": [
{"data": "displayId"},
{"data": "display"},
{"data": "description"}
]
});
usageTable.on('draw', dataTableDraw);
usageTable.on('processing.dt', dataTableProcessing);
// Layouts tab
var usageTableLayouts = $("#usageReportLayoutsTable").DataTable({
"language": dataTablesLanguage,
serverSide: true,
stateSave: true, stateDuration: 0,
filter: false,
searchDelay: 3000,
responsive: true,
"order": [[1, "asc"]],
ajax: {
"url": "{{ url_for("playlist.usage.layouts", {id:':id'}) }}".replace(":id", $("#usageReportLayoutsTable").data().playlistId)
},
"columns": [
{"data": "layoutId"},
{"data": "layout"},
{"data": "description"},
{
"orderable": false,
"data": dataTableButtonsColumn
}
]
});
usageTableLayouts.on('draw', dataTableDraw);
usageTableLayouts.on('processing.dt', dataTableProcessing);
}
function playlistFormPopulateMediaTable(dialog) {
nameFilter = $(dialog).find("input[name=filterMediaName]").val();
tagFilter = $(dialog).find("input[name=filterMediaTag]").val();
exactTags = $(dialog).find("input[name=exactTags]").is(':checked')
logicalOperator = $(dialog).find("select[name=logicalOperator]").val();
logicalOperatorName = $(dialog).find("select[name=logicalOperatorName]").val();
filterFolderId = $(dialog).find("select[name=filterFolderId]").val() ?? "";
if (nameFilter === "" && tagFilter === "" && filterFolderId === "") {
if (mediaTable != null) {
mediaTable.destroy();
mediaTable = null;
$("#playlistLibraryMedia tbody").empty();
}
return;
}
if (mediaTable != null) {
mediaTable.ajax.reload();
} else {
mediaTable = $("#playlistLibraryMedia").DataTable({
"language": dataTablesLanguage,
serverSide: true,
stateSave: true,
stateDuration: 0,
filter: false,
responsive: true,
searchDelay: 3000,
"order": [[1, "asc"]],
ajax: {
"url": "{{ url_for("library.search") }}",
"data": function (d) {
$.extend(
d,
{
media: nameFilter,
tags: tagFilter,
folderId: filterFolderId,
assignable: 1,
exactTags: exactTags,
logicalOperator: logicalOperator,
logicalOperatorName: logicalOperatorName
}
);
}
},
"columns": [
{"data": "mediaId"},
{"data": "name"},
{"data": "mediaType"},
{% if currentUser.featureEnabled("tag.tagging") %}{"data": dataTableCreateTags},{% endif %}
{
"name": "duration",
"data": function (data, type) {
if (type !== "display")
return data.duration;
return moment().startOf("day").seconds(data.duration).format("H:mm:ss");
}
}
]
});
mediaTable.on('processing.dt', dataTableProcessing);
mediaTable.on('draw', {form: $(".playlistForm")}, dataTableCreateTagEvents);
}
}
function setEnableStatMultiSelectFormOpen(dialog) {
var $select = $('<select id="enableStat" name="enableStat" class="form-control">' +
'<option value="Off">{% trans %} Off {% endtrans %}</option>' +
'<option value="On">{% trans %} On {% endtrans %}</option>' +
'<option value="Inherit">{% trans %} Inherit {% endtrans %}</option>' +
'</select>');
$select.on('change', function () {
dialog.data().commitData = {enableStat: $(this).val()};
}).trigger('change');
$(dialog).find('.modal-body').append($select);
}
function playlistAddFormOpen() {
$("#playlistAddForm").off("submit").submit(function (e) {
e.preventDefault();
var form = $(this);
$.ajax({
type: $(this).attr("method"),
url: $(this).attr("action"),
data: $(this).serialize(),
cache: false,
dataType: "json",
success: function (xhr, textStatus, error) {
XiboSubmitResponse(xhr, form);
if (xhr.success && xhr.data.isDynamic == 0) {
// Open the editor
openPlaylistEditorForm(xhr.id);
}
}
});
});
}
function openPlaylistEditorForm(playlistId) {
var requestPath = playlistEditorUrl;
// replace id if necessary/exists
requestPath = requestPath.replace(':id', playlistId);
$.ajax({
url: requestPath,
type: 'GET'
}).done(function (res) {
if (!res.success) {
// Login Form needed?
if (res.login) {
window.location.reload();
} else {
// Just an error we dont know about
if (res.message == undefined) {
console.error(res);
} else {
console.error(res.message);
}
}
} else {
// Clear container
$('#editor-container').empty();
// Append form
$('#editor-container').append(res.html);
}
}).fail(function (jqXHR, textStatus, errorThrown) {
// Output error to console
console.error(jqXHR, textStatus, errorThrown);
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,133 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Resolutions"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Resolutions" %}</h1>
<p class="text-muted">{% trans "Manage display resolutions." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="resolutionView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Resolutions" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-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 "Enabled" %}{% endset %}
{% set option1 %}{% trans "Yes" %}{% endset %}
{% set option2 %}{% trans "No" %}{% endset %}
{% set values = [{id: 1, value: option1}, {id: 0, value: option2}] %}
{{ inline.dropdown("enabled", "single", title, 1, values, "id", "value") }}
</form>
</div>
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<table id="resolutions" class="table table-striped" data-state-preference-name="resolutionGrid">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Resolution" %}</th>
<th>{% trans "Width" %}</th>
<th>{% trans "Height" %}</th>
<th>{% trans "Enabled?" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
var table = $("#resolutions").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
responsive: true,
stateDuration: 0,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[1, "asc"]],
ajax: {
url: "{{ url_for("resolution.search") }}",
data: function (d) {
$.extend(d, $("#resolutions").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{"data": "resolutionId", responsivePriority: 2},
{"data": "resolution"},
{"data": "width"},
{"data": "height"},
{"data": "enabled"},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#resolutions_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,355 @@
{#
/**
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% import "forms.twig" as forms %}
{% block title %}{{ "Schedule"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Schedule" %}</h1>
<p class="text-muted">{% trans "Schedule content to your displays." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="scheduleGridView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Schedule" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="schedule-filter">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item"><a class="nav-link active" href="#general-filter" role="tab" data-toggle="tab" aria-selected="true"><span>{% trans "General" %}</span></a></li>
<li class="nav-item"><a class="nav-link" href="#advanced-filter" role="tab" data-toggle="tab" aria-selected="false"><span>{% trans "Advanced" %}</span></a></li>
</ul>
<form class="form-inline">
<div class="tab-content">
<div class="tab-pane active" id="general-filter" role="tabpanel">
{% set title %}{% trans "Range" %}{% endset %}
{% set range %}{% trans "Custom" %}{% endset %}
{% set day %}{% trans "Day" %}{% endset %}
{% set week %}{% trans "Week" %}{% endset %}
{% set month %}{% trans "Month" %}{% endset %}
{% set year %}{% trans "Year" %}{% endset %}
{% set options = [
{ name: "custom", range: range },
{ name: "day", range: day },
{ name: "week", range: week },
{ name: "month", range: month },
{ name: "year", range: year },
] %}
{{ inline.dropdown("range", "single", title, "month", options, "name", "range", "", "date-range-input") }}
{% set title %}{% trans 'From Date' %}{% endset %}
{{ inline.dateTime("fromDt", title, "", "", "custom-date-range d-none", "", "") }}
{% set title %}{% trans 'To Date' %}{% endset %}
{{ inline.dateTime("toDt", title, "", "", "custom-date-range d-none", "", "") }}
{% set title %}{% trans "Date Controls" %}{% endset %}
<div class="form-group mr-1 mb-1 controls-date-range">
<div class="control-label mr-1" title=""
accesskey="">{{ title }}</div>
<div class="controls-date-inputs">
<div class="inputgroup date" id="dateInput">
<span class="btn btn-outline-primary date-open-button" role="button">
<i class="fa fa-calendar"></i>
</span>
<input type="text" class="form-control" id="dateInputLink" data-input style="display:none;"/>
</div>
<div class="btn-group">
<button type="button" class="btn btn-secondary" data-calendar-nav="prev"><span class="fa fa-caret-left"></span> {% trans "Prev" %}</button>
<button type="button" class="btn btn-outline-secondary" data-calendar-nav="today">{% trans "Today" %}</button>
<button type="button" class="btn btn-secondary" data-calendar-nav="next">{% trans "Next" %} <span class="fa fa-caret-right"></span></button>
</div>
</div>
</div>
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('name', title) }}
{% set title %}{% trans 'Event Type' %}{% endset %}
{{ inline.dropdown("eventTypeId", "single", title, "", [{eventTypeId: null, eventTypeName: "All"}]|merge(eventTypes), "eventTypeId", "eventTypeName") }}
{% set title %}{% trans "Layout / Campaign" %}{% endset %}
{% set helpText %}{% trans "Please select a Layout or Campaign for this Event to show" %}{% endset %}
<div class="form-group mr-1 mb-1">
<label class="control-label mr-1" for="campaignId" title=""
accesskey="">{{ title }}</label>
<select name="campaignId" id="campaignIdFilter" class="form-control"
data-search-url="{{ url_for("campaign.search") }}"
data-trans-campaigns="{% trans "Campaigns" %}"
data-trans-layouts="{% trans "Layouts" %}"
data-allow-clear="true"
data-width="100%"
title="{% trans "Layout / Campaign" %}"
data-placeholder="{% trans "Layout / Campaign" %}"
data-dropdownAutoWidth
>
</select>
</div>
{% set title %}{% trans "Displays" %}{% endset %}
<div class="form-group mr-1 mb-1 pagedSelect" style="min-width: 200px">
<label class="control-label mr-1" for="DisplayList" title=""
accesskey="">{{ title }}</label>
<select id="DisplayList" class="form-control" name="displaySpecificGroupIds[]"
data-width="100%"
data-placeholder="{% trans "Displays" %}"
data-search-url="{{ url_for("display.search") }}"
data-search-term="display"
data-id-property="displayGroupId"
data-text-property="display"
data-additional-property="displayGroupId"
data-allow-clear="true"
data-initial-key="displayGroupIds[]"
multiple>
</select>
</div>
{% set title %}{% trans "Display Groups" %}{% endset %}
<div class="form-group mr-2 mb-1 pagedSelect" style="min-width: 200px">
<label class="control-label mr-1" for="DisplayGroupList" title=""
accesskey="">{{ title }}</label>
<select id="DisplayGroupList" class="form-control" name="displayGroupIds[]"
data-width="100%"
data-placeholder="{% trans "Display Groups" %}"
data-search-url="{{ url_for("displayGroup.search") }}"
data-search-term="displayGroup"
data-id-property="displayGroupId"
data-text-property="displayGroup"
data-allow-clear="true"
data-initial-key="displayGroupIds[]"
multiple>
</select>
</div>
</div>
<div class="tab-pane" id="advanced-filter" role="tabpanel">
{% set label %}{% trans "Direct Schedule?" %}{% endset %}
{% set title %}{% trans "Show only events scheduled directly on selected Displays/Groups" %}{% endset %}
<div class="form-group ml-2 mr-3 mb-1">
<div class="form-check">
<input title="{{ title }}" class="form-check-input" type="checkbox" id="directSchedule" name="directSchedule">
<label class="form-check-label" title="{{ title }}" for="directSchedule" accesskey="">{{ label }}</label>
</div>
</div>
{% set title %}{% trans "Only show schedules which appear on all filtered displays/groups?" %}{% endset %}
{% set label %}{% trans "Shared Schedule?" %}{% endset %}
<div class="form-group ml-2 mr-3 mb-1">
<div class="form-check">
<input title="{{ title }}" class="form-check-input" type="checkbox" id="sharedSchedule" name="sharedSchedule">
<label class="form-check-label" title="{{ title }}" for="sharedSchedule" accesskey="">{{ label }}</label>
</div>
</div>
{% set title %}{% trans 'Geo Aware?' %}{% endset %}
{% set options = [
{ id: null, name: "Both"|trans },
{ id: 0, name: "No"|trans },
{ id: 1, name: "Yes"|trans }
] %}
{{ inline.dropdown("geoAware", "single", title, "both", options, "id", "name") }}
{% set title %}{% trans 'Recurring?' %}{% endset %}
{% set options = [
{ id: null, name: "Both" },
{ id: 0, name: "No"|trans },
{ id: 1, name: "Yes"|trans }
] %}
{{ inline.dropdown("recurring", "single", title, "both", options, "id", "name") }}
</div>
</div>
</form>
</div>
</div>
</div>
<div class="XiboSchedule card dashboard-card ots-table-card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="schedule-nav grid-nav nav-link active" id="grid-tab" href="#grid-view"
data-schedule-view="grid"
role="tab"
data-toggle="tab"><span>{% trans "Grid" %}</span></a>
</li>
<li class="nav-item">
<a class="schedule-nav calendar-nav nav-link" id="calendar-tab" href="#calendar-view"
data-schedule-view="calendar"
data-calendar-view="month"
role="tab"
data-toggle="tab"><span>{% trans "Calendar" %}</span></a>
</li>
</ul>
</div>
<div class="card-body">
<div class="xibo-calendar-header-container col-xl-12 d-inline-flex justify-content-between">
<div class="xibo-calendar-header text-center d-inline-flex">
<h1 class="page-header"></h1>
</div>
<div class="calendar-loading">
<span id="calendar-progress-table" class="fa fa-spin fa-cog"></span>
<span id="calendar-progress" class="fa fa-spin fa-cog"></span>
</div>
</div>
<div class="tab-content">
<div class="tab-pane active" id="grid-view">
<div class="XiboData pt-3">
<table id="schedule-grid" class="table table-striped w-100"
data-state-preference-name="scheduleGrid">
<thead>
<tr>
<th>{% trans 'ID' %}</th>
<th></th>
<th>{% trans 'Event Type' %}</th>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Start' %}</th>
<th>{% trans 'End' %}</th>
<th>{% trans 'Event' %}</th>
<th>{% trans 'Campaign ID' %}</th>
<th>{% trans 'Display Groups' %}</th>
<th>{% trans 'SoV' %}</th>
<th>{% trans 'Max Plays per Hour' %}</th>
<th>{% trans 'Geo Aware?' %}</th>
<th>{% trans 'Recurring?' %}</th>
<th>{% trans 'Recurrence Description' %}</th>
<th>{% trans 'Recurrence Type' %}</th>
<th>{% trans 'Recurrence Interval' %}</th>
<th>{% trans 'Recurrence Repeats On' %}</th>
<th>{% trans 'Recurrence End' %}</th>
<th>{% trans 'Priority?' %}</th>
<th>{% trans 'Criteria?' %}</th>
<th>{% trans 'Created On' %}</th>
<th>{% trans 'Updated On' %}</th>
<th>{% trans 'Modified By' %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="tab-pane" id="calendar-view">
<div class="row">
<div id="CalendarContainer"
data-agenda-link="{{ url_for("schedule.events", {id: ':id'}) }}"
data-calendar-type="{{ settings.CALENDAR_TYPE }}" class="col-sm-12"
data-default-lat="{{ defaultLat }}"
data-default-long="{{ defaultLong }}">
<div class="calendar-view" id="Calendar"></div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="cal-legend">
<ul>
<li class="event-always"><span
class="fa fa-retweet"></span> {% trans "Always showing" %}</li>
<li class="event-info"><span
class="fa fa-desktop"></span> {% trans "Single Display" %}</li>
<li class="event-success"><span
class="fa fa-desktop"></span> {% trans "Multi Display" %}</li>
<li class="event-important"><span
class="fa fa-bullseye"></span> {% trans "Priority" %}</li>
<li class="event-special"><span
class="fa fa-repeat"></span> {% trans "Recurring" %}</li>
<li class="event-inverse"><span
class="fa fa-lock"></span> {% trans "View Only" %}</li>
<li class="event-command"><span
class="fa fa-wrench"></span> {% trans "Command" %}</li>
<li class="event-interrupt"><span
class="fa fa-hand-paper"></span> {% trans "Interrupt" %}</li>
<li class="event-geo-location"><span
class="fa fa-map-marker"></span> {% trans "Geo Location" %}</li>
<li class="event-action"><span
class="fa fa-paper-plane "></span> {% trans "Interactive Action" %}
</li>
<li class="event-sync"><span
class="fa fa-refresh"></span> {% trans "Synchronised" %}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
{# Initialise JS variables #}
<script type="text/javascript" nonce="{{ cspNonce }}">
{# JS variables #}
var scheduleRecurrenceDeleteUrl = "{{ url_for("schedule.recurrence.delete.form", {id:':id'}) }}";
var layoutPreviewUrl = "{{ theme.rootUri() }}preview/layout/preview/:id";
var scheduleSearchUrl = "{{ url_for("schedule.search") }}";
var userAgendaViewEnabled = "{{ currentUser.featureEnabled('schedule.agenda') }}";
{# Custom translations #}
var schedulePageTrans = {
always: "{% trans "Always" %}",
adjustTimesofTimer: "{% trans "Adjust the times of this timer. To add or remove a day, use the Display Profile." %}",
daysOfTheWeek: {
monday: "{% trans "Monday" %}",
tuesday: "{% trans "Tuesday" %}",
wednesday: "{% trans "Wednesday" %}",
thursday: "{% trans "Thursday" %}",
friday: "{% trans "Friday" %}",
saturday: "{% trans "Saturday" %}",
sunday: "{% trans "Sunday" %}",
},
};
</script>
{# Add page source code bundle ( JS ) #}
<script src="{{ theme.rootUri() }}dist/leaflet.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
<script src="{{ theme.rootUri() }}dist/pages/schedule-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
{% endblock %}

View File

@@ -0,0 +1,190 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Sync Groups"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Sync Groups" %}</h1>
<p class="text-muted">{% trans "Create and manage synchronized Display groups." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="syncGroupGridView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Sync Groups" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "ID" %}{% endset %}
{{ inline.input("syncGroupId", title) }}
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('name', title) }}
{% set title %}{% trans "Lead Display ID" %}{% endset %}
{{ inline.input("leadDisplayId", title) }}
{{ inline.hidden("folderId") }}
</form>
</div>
</div>
</div>
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 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">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div 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">
<table id="syncgroups" class="table table-striped" data-content-type="syncGroup" data-content-id-name="syncGroupId" data-state-preference-name="syncGroupGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Created Date" %}</th>
<th>{% trans "Modified Date" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Modified By" %}</th>
<th>{% trans "Publisher Port" %}</th>
<th>{% trans "Switch Delay" %}</th>
<th>{% trans "Video Pause Delay" %}</th>
<th>{% trans "Lead Display" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
let syncGroupTable;
$(document).ready(function() {
syncGroupTable = $("#syncgroups").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
"filter": false,
searchDelay: 3000,
"order": [[ 1, "asc"]],
ajax: {
"url": "{{ url_for("syncgroup.search") }}",
"data": function(d) {
$.extend(d, $("#syncgroups").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{ "data": "syncGroupId", responsivePriority: 2 },
{ "data": "name", responsivePriority: 1 },
{ "data": "createdDt", responsivePriority: 2 },
{ "data": "modifiedDt", responsivePriority: 2 },
{ "data": "owner", responsivePriority: 3 },
{ "data": "modifiedByName", responsivePriority: 4 },
{ "data": "syncPublisherPort", responsivePriority: 3 },
{ "data": "syncSwitchDelay", responsivePriority: 3 },
{ "data": "syncVideoPauseDelay", responsivePriority: 3 },
{ "data": "leadDisplay", responsivePriority: 3 },
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
syncGroupTable.on('draw', dataTableDraw);
syncGroupTable.on('processing.dt', dataTableProcessing);
dataTableAddButtons(syncGroupTable, $('#syncgroups_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
syncGroupTable.ajax.reload();
});
});
</script>
{% endblock %}
{% block javaScriptTemplates %}
{{ parent() }}
{% verbatim %}
<script type="text/x-handlebars-template" id="template-display-group-multi-delete-checkbox">
<div class="form-group row">
<div class="offset-sm-2 col-sm-10 mt-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="checkbox-confirmDelete" name="confirmDelete">
<label class="form-check-label" for="checkbox-confirmDelete">
{% endverbatim %}{{ "Are you sure you want to delete?"|trans }}{% verbatim %}
</label>
</div>
<small class="form-text text-muted">{% endverbatim %}{{ "Check to confirm deletion of the selected records."|trans }}{% verbatim %}</small>
</div>
</div>
</script>
{% endverbatim %}
{% endblock %}

View File

@@ -0,0 +1,293 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Templates"|trans }} | {% endblock %}
{% block actionMenu %}
<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 pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Templates" %}</h1>
<p class="text-muted">{% trans "Manage your reusable templates." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="templateView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Templates" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-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('template', title) }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{{ inline.hidden("folderId") }}
</form>
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none">
<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">
<table id="templates" class="table table-striped" data-content-type="layout" data-content-id-name="layoutId" data-state-preference-name="templateGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Description" %}</th>
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
<th>{% trans "Orientation" %}</th>
<th>{% trans "Thumbnail" %}</th>
<th>{% trans "Sharing" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
{% if not currentUser.featureEnabled("folder.view") %}
disableFolders();
{% endif %}
var table = $("#templates").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
responsive: true,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
searchDelay: 3000,
"order": [[ 1, "asc"]],
ajax: {
"url": "{{ url_for("template.search") }}",
"data": function(d) {
$.extend(d, $("#templates").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
}
},
"columns": [
{ "data": "layout", responsivePriority: 2},
{
"name": "publishedStatus",
responsivePriority: 2,
"data": function (data, type) {
if (data.publishedDate != null) {
var now = moment();
var published = moment(data.publishedDate);
var differenceMinutes = published.diff(now, 'minutes');
var momentDifference = moment(now).to(published);
if (differenceMinutes < -5) {
return data.publishedStatus.concat(" - ", translations.publishedStatusFailed);
} else {
return data.publishedStatus.concat(" - ", translations.publishedStatusFuture + " " + momentDifference);
}
} else {
return data.publishedStatus;
}
}
},
{ "data": "owner", responsivePriority: 3},
{
"name": "description",
"data": null,
responsivePriority: 3,
"render": {"_": "description", "display": "descriptionWithMarkup", "sort": "description"}
},
{% if currentUser.featureEnabled("tag.tagging") %}{
"sortable": false,
"visible": false,
"data": dataTableCreateTags,
responsivePriority: 3
},{% endif %}
{ data: 'orientation', responsivePriority: 10, visible: false},
{
responsivePriority: 3,
data: 'thumbnail',
render: function (data, type, row) {
if (type !== 'display') {
return row.layoutId;
}
if (data) {
return '<a class="img-replace" data-toggle="lightbox" data-type="image" href="' + data + '">' +
'<img class="img-fluid" src="' + data + '" alt="{{ "Thumbnail"|trans }}" />' +
'</a>';
} else {
var addUrl = '{{ url_for("layout.thumbnail.add", {id: ":id"}) }}'.replace(':id', row.layoutId);
return '<a class="img-replace generate-layout-thumbnail" href="' + addUrl + '">' +
'<img class="img-fluid" src="{{ theme.uri("img/thumbs/placeholder.png") }}" alt="{{ "Add Thumbnail"|trans }}" />' +
'</a>';
}
return '';
},
sortable: false
},
{
"data": "groupsWithPermissions",
responsivePriority: 4,
"render": dataTableCreatePermissions
},
{
"orderable": false,
responsivePriority: 1,
"data": dataTableButtonsColumn
}
]
});
table.on('draw', dataTableDraw);
table.on('draw', { form: $("#templates").closest(".XiboGrid").find(".FilterDiv form") } ,dataTableCreateTagEvents);
table.on('draw', function(e, settings) {
$('#' + e.target.id + ' .generate-layout-thumbnail').on('click', function(e) {
e.preventDefault();
var $anchor = $(this);
$.ajax({
url: $anchor.attr('href'),
method: 'POST',
success: function() {
$anchor.find('img').attr('src', $anchor.attr('href'));
$anchor.removeClass('generate-layout-thumbnail').attr('data-toggle', 'lightbox');
}
});
});
});
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#templates_wrapper').find('.dataTables_buttons'));
$("#refreshGrid").click(function () {
table.ajax.reload();
});
function templateFormOpen() {
if ($('#folder-tree-form-modal').length === 0) {
// compile tree folder modal and append it to Form
var folderTreeModal = templates['folder-tree'];
var treeConfig = {"container": "container-folder-form-tree", "modal": "folder-tree-form-modal"};
treeConfig.trans = translations.folderTree;
$("body").append(folderTreeModal(treeConfig));
$("#folder-tree-form-modal").on('hidden.bs.modal', function () {
// Fix for 2nd/overlay modal
$('.modal:visible').length && $(document.body).addClass('modal-open');
$(this).data('bs.modal', null);
});
}
// select current working folder if one is selected in the grid
if ($('#container-folder-tree').jstree("get_selected", true)[0] !== undefined) {
$('#templateAddForm' + ' #folderId').val($('#container-folder-tree').jstree("get_selected", true)[0].id);
}
initJsTreeAjax($('#folder-tree-form-modal').find('#container-folder-form-tree'), 'templateAddForm', true, 600);
$("#templateAddForm").submit(function(e) {
e.preventDefault();
var form = $(this);
var url = $(this).data().redirect;
$.ajax({
type: $(this).attr("method"),
url: $(this).attr("action"),
data: $(this).serialize(),
cache: false,
dataType:"json",
success: function(xhr, textStatus, error) {
XiboSubmitResponse(xhr, form);
if (xhr.success) {
// Reload the designer
XiboRedirect(url.replace(":id", xhr.id));
}
}
});
});
}
function layoutPublishFormOpen() {
// Nothing to do here, but we use the same form on the layout designer and have a callback registered there
}
function layoutEditFormSaved() {
// Nothing to do here.
}
</script>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -2,7 +2,4 @@
OTS Signage Theme override
Optional dashboard message block included with ignore missing
#}
<div class="ots-dashboard-message">
<div class="ots-dashboard-message__title">OTS Theme Active</div>
<div class="ots-dashboard-message__body">This is a low-risk override for troubleshooting. Remove or restyle at any time.</div>
</div>

View File

@@ -103,6 +103,7 @@
const filterContent = document.querySelector('#ots-filter-content');
if (filterCollapseBtn && filterContent) {
const storageKey = `ots-filter-collapsed:${window.location.pathname}`;
let isCollapsed = false;
filterCollapseBtn.addEventListener('click', function() {
@@ -115,17 +116,19 @@
icon.classList.toggle('fa-chevron-down');
// Save preference to localStorage
localStorage.setItem('ots-filter-collapsed', isCollapsed);
localStorage.setItem(storageKey, isCollapsed);
});
// Restore saved preference
const savedState = localStorage.getItem('ots-filter-collapsed');
const savedState = localStorage.getItem(storageKey);
if (savedState === 'true') {
isCollapsed = true;
filterContent.classList.add('collapsed');
const icon = filterCollapseBtn.querySelector('i');
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
} else {
filterContent.classList.remove('collapsed');
}
}