almost functional
This commit is contained in:
@@ -187,68 +187,274 @@ hr {
|
||||
#sidebar-wrapper {
|
||||
background: var(--ots-surface);
|
||||
border-right: 1px solid var(--ots-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* OTS sidebar override marker */
|
||||
.ots-sidebar-wrapper {
|
||||
box-shadow: inset 0 0 0 1px rgba(79, 140, 255, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--ots-surface);
|
||||
}
|
||||
|
||||
.ots-sidebar .sidebar-list a,
|
||||
.ots-sidebar .sidebar-main a {
|
||||
/* Sidebar Header */
|
||||
.ots-sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--ots-border);
|
||||
background: var(--ots-surface);
|
||||
}
|
||||
|
||||
.ots-brand-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(79, 140, 255, 0.15);
|
||||
border: 1px solid rgba(79, 140, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
font-size: 18px;
|
||||
color: var(--ots-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ots-brand-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ots-brand-name {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ots-text);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.ots-sidebar-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--ots-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color var(--ots-transition);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ots-sidebar-close:hover {
|
||||
color: var(--ots-text);
|
||||
}
|
||||
|
||||
/* Sidebar Content */
|
||||
.ots-sidebar {
|
||||
flex: 1;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 12px 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.ots-sidebar li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Sidebar Main Item */
|
||||
.ots-sidebar li.sidebar-main > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 0 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
color: var(--ots-text);
|
||||
text-decoration: none;
|
||||
transition: background var(--ots-transition), color var(--ots-transition);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ots-sidebar li.sidebar-main > a:hover {
|
||||
background: rgba(79, 140, 255, 0.12);
|
||||
color: var(--ots-primary);
|
||||
}
|
||||
|
||||
.ots-sidebar li.sidebar-main > a.active {
|
||||
background: rgba(79, 140, 255, 0.2);
|
||||
color: var(--ots-primary);
|
||||
}
|
||||
|
||||
/* Sidebar Section (Collapsible) */
|
||||
.ots-sidebar li.sidebar-section > a.sidebar-section-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 0 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
color: var(--ots-text);
|
||||
text-decoration: none;
|
||||
transition: background var(--ots-transition), color var(--ots-transition);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
|
||||
.ots-sidebar li.sidebar-section > a.sidebar-section-toggle:hover {
|
||||
background: rgba(79, 140, 255, 0.12);
|
||||
color: var(--ots-primary);
|
||||
}
|
||||
|
||||
.ots-sidebar li.sidebar-section > a.sidebar-section-toggle.active {
|
||||
background: rgba(79, 140, 255, 0.12);
|
||||
color: var(--ots-primary);
|
||||
}
|
||||
|
||||
.ots-section-toggle-icon {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
transition: transform var(--ots-transition);
|
||||
}
|
||||
|
||||
.ots-sidebar li.sidebar-section > a.sidebar-section-toggle.active .ots-section-toggle-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Sidebar Subsection */
|
||||
.ots-sidebar .sidebar-subsection {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
display: none;
|
||||
background: rgba(79, 140, 255, 0.05);
|
||||
}
|
||||
|
||||
.ots-sidebar .sidebar-subsection.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ots-sidebar .sidebar-subsection li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Sidebar List Item */
|
||||
.ots-sidebar li.sidebar-list > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 2px 12px 2px 28px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
color: var(--ots-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: background var(--ots-transition), color var(--ots-transition);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ots-sidebar li.sidebar-list > a:hover {
|
||||
background: rgba(79, 140, 255, 0.12);
|
||||
color: var(--ots-primary);
|
||||
}
|
||||
|
||||
.ots-sidebar li.sidebar-list > a.active {
|
||||
background: rgba(79, 140, 255, 0.15);
|
||||
color: var(--ots-primary);
|
||||
}
|
||||
|
||||
/* Navigation Icons and Text */
|
||||
.ots-nav-icon {
|
||||
width: 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
opacity: 0.85;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ots-nav-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Xibo sidebar refinements (dark) */
|
||||
#sidebar-wrapper .sidebar-title a {
|
||||
color: var(--ots-text-faint);
|
||||
/* Sidebar Spacer */
|
||||
.sidebar-spacer {
|
||||
height: 12px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* Sidebar Footer */
|
||||
.ots-sidebar-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--ots-border);
|
||||
background: var(--ots-surface);
|
||||
}
|
||||
|
||||
.ots-user-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ots-user-role {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--ots-text-secondary);
|
||||
text-transform: uppercase;
|
||||
padding: 12px 16px 6px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#sidebar-wrapper .sidebar-list a,
|
||||
#sidebar-wrapper .sidebar-main a {
|
||||
display: block;
|
||||
margin: 2px 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--ots-radius-sm);
|
||||
.ots-user-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--ots-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ots-user-profile-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
color: var(--ots-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: background var(--ots-transition), color var(--ots-transition);
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#sidebar-wrapper .sidebar-list a:hover,
|
||||
#sidebar-wrapper .sidebar-main a:hover {
|
||||
.ots-user-profile-link:hover {
|
||||
background: rgba(79, 140, 255, 0.12);
|
||||
color: var(--ots-primary);
|
||||
}
|
||||
|
||||
#sidebar-wrapper .sidebar {
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
#sidebar-wrapper .sidebar-main a,
|
||||
#sidebar-wrapper .sidebar-list a {
|
||||
display: block;
|
||||
padding: 10px 18px;
|
||||
color: var(--ots-text);
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
#sidebar-wrapper .sidebar-main a:hover,
|
||||
#sidebar-wrapper .sidebar-list a:hover {
|
||||
background: rgba(79, 140, 255, 0.12);
|
||||
@@ -534,6 +740,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 {
|
||||
|
||||
@@ -55,8 +55,8 @@ body {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.ots-main {
|
||||
@@ -66,6 +66,11 @@ body {
|
||||
margin-left: 260px;
|
||||
}
|
||||
|
||||
.ots-main,
|
||||
#page-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ots-content {
|
||||
flex: 1;
|
||||
padding: 32px;
|
||||
@@ -106,12 +111,13 @@ body {
|
||||
============================================================================ */
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px 16px;
|
||||
padding: 18px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-link {
|
||||
@@ -139,16 +145,18 @@ body {
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 12px 0 120px;
|
||||
padding: 12px 0 140px;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* Compatibility: Xibo sidebar markup uses `sidebar-list`, `sidebar-main`, `sidebar-title`.
|
||||
@@ -157,10 +165,9 @@ body {
|
||||
.ots-sidebar li.sidebar-list > a,
|
||||
.ots-sidebar li.sidebar-main > a,
|
||||
.ots-sidebar li.sidebar-title > a {
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
color: #c8d5ee;
|
||||
text-decoration: none;
|
||||
@@ -171,7 +178,7 @@ body {
|
||||
border-left: 2px solid transparent;
|
||||
margin: 3px 10px;
|
||||
border-radius: 12px;
|
||||
min-height: 40px;
|
||||
min-height: 48px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@@ -192,15 +199,15 @@ body {
|
||||
}
|
||||
|
||||
.ots-sidebar .ots-nav-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
color: currentColor;
|
||||
justify-self: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.ots-sidebar .ots-nav-text {
|
||||
@@ -210,6 +217,11 @@ body {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sidebar-group-toggle .ots-nav-icon,
|
||||
.ots-sidebar-nav .ots-nav-icon {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.ots-sidebar li.sidebar-title > a {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
@@ -223,6 +235,181 @@ body {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 18px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
height: 72px;
|
||||
box-sizing: border-box;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #08132a;
|
||||
z-index: 1210;
|
||||
}
|
||||
|
||||
.sidebar-collapse-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #d7e2f8;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar-collapse-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.ots-sidebar.collapsed {
|
||||
width: 88px;
|
||||
}
|
||||
|
||||
.ots-sidebar.collapsed .brand-text,
|
||||
.ots-sidebar.collapsed .sidebar-group-toggle .ots-nav-text,
|
||||
.ots-sidebar.collapsed .sidebar-list .ots-nav-text,
|
||||
.ots-sidebar.collapsed .sidebar-submenu,
|
||||
.ots-sidebar.collapsed .sidebar-group-caret,
|
||||
.ots-sidebar.collapsed .user-details {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ots-sidebar.collapsed .sidebar-group-toggle,
|
||||
.ots-sidebar.collapsed .sidebar-list > a {
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Center icons when collapsed */
|
||||
.ots-sidebar.collapsed .ots-nav-icon {
|
||||
justify-self: center !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.ots-sidebar-collapsed #page-wrapper,
|
||||
.ots-sidebar-collapsed .ots-main {
|
||||
margin-left: 88px !important;
|
||||
}
|
||||
|
||||
.ots-sidebar-nav {
|
||||
padding: 12px 0 140px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.sidebar-group {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.sidebar-group-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
margin: 6px 10px;
|
||||
border-radius: 12px;
|
||||
color: #d7e2f8;
|
||||
text-decoration: none;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar-group-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.sidebar-group-caret {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.sidebar-submenu {
|
||||
list-style: none;
|
||||
margin: 4px 0 8px;
|
||||
padding: 0 0 0 12px;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.sidebar-group.is-open .sidebar-group-caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sidebar-submenu .sidebar-list > a {
|
||||
margin: 4px 10px 4px 18px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.sidebar-submenu .sidebar-list > a:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-submenu .sidebar-list.active > a,
|
||||
.sidebar-submenu .sidebar-list > a.active {
|
||||
color: #0b1221;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.25);
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
background-color: rgba(15, 23, 42, 0.6);
|
||||
}
|
||||
|
||||
.sidebar-theme-toggle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar-theme-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.user-role {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nav-section-divider {
|
||||
padding: 12px 16px 8px;
|
||||
margin-top: 8px;
|
||||
@@ -299,6 +486,7 @@ body {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding: 16px;
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-user {
|
||||
@@ -353,7 +541,6 @@ body {
|
||||
|
||||
.ots-topbar {
|
||||
background-color: var(--color-surface-elevated);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
padding: 10px 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -395,10 +582,13 @@ body {
|
||||
white-space: nowrap;
|
||||
transition: all var(--transition-fast);
|
||||
position: relative;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ots-topbar .nav-link:hover {
|
||||
background-color: rgba(59, 130, 246, 0.08);
|
||||
background-color: rgba(59, 130, 246, 0.06);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@@ -430,8 +620,8 @@ body {
|
||||
border-radius: 8px;
|
||||
padding: 6px 0;
|
||||
margin-top: 4px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: none;
|
||||
border: 0 !important;
|
||||
background-color: var(--color-surface);
|
||||
min-width: 180px;
|
||||
z-index: 1100;
|
||||
@@ -688,6 +878,99 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Make DataTables button collections compatible with OTS dropdown styling */
|
||||
.dt-buttons {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dt-button-collection {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 6px;
|
||||
display: none;
|
||||
z-index: 1002;
|
||||
min-width: 160px;
|
||||
background-color: var(--color-surface-elevated);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.dt-buttons.active .dt-button-collection,
|
||||
.dt-button-collection.active,
|
||||
.dt-button-collection.show {
|
||||
display: block;
|
||||
animation: slideDown 150ms ease-out;
|
||||
}
|
||||
|
||||
/* Ensure collection items are visible and styled */
|
||||
.dt-button-collection .dt-button,
|
||||
.dt-button-collection button,
|
||||
.dt-button-collection a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
color: var(--color-text-primary);
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dt-button-collection .dt-button:hover,
|
||||
.dt-button-collection a:hover,
|
||||
.dt-button-collection button:hover {
|
||||
background-color: rgba(59, 130, 246, 0.06);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.dt-button-collection input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dt-button-collection .dt-button:focus,
|
||||
.dt-button-collection a:focus,
|
||||
.dt-button-collection button:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* DataTables sometimes nests a `div.dropdown-menu` inside the collection.
|
||||
Ensure that inner dropdown-menu is visible when the collection is shown. */
|
||||
.dt-button-collection .dropdown-menu {
|
||||
display: block !important;
|
||||
position: static !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
min-width: 160px !important;
|
||||
}
|
||||
|
||||
.dt-button-collection .dropdown-menu .dropdown-item,
|
||||
.dt-button-collection .dropdown-menu a,
|
||||
.dt-button-collection .dropdown-menu .dt-button {
|
||||
display: block !important;
|
||||
padding: 6px 12px !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
text-decoration: none !important;
|
||||
background: transparent !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.dt-button-collection .dropdown-menu .dropdown-item:hover,
|
||||
.dt-button-collection .dropdown-menu a:hover,
|
||||
.dt-button-collection .dropdown-menu .dt-button:hover {
|
||||
background-color: rgba(59, 130, 246, 0.06) !important;
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.dropdown.active .dropdown-menu,
|
||||
.dropdown:focus-within .dropdown-menu {
|
||||
display: block;
|
||||
@@ -995,90 +1278,138 @@ body {
|
||||
background: linear-gradient(180deg, rgba(59, 130, 246, 0.06), rgba(59, 130, 246, 0.02));
|
||||
border: 1px solid rgba(59, 130, 246, 0.18);
|
||||
box-shadow: 0 12px 26px rgba(8, 15, 30, 0.35);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 360px;
|
||||
align-items: stretch;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
min-height: 380px;
|
||||
height: 380px;
|
||||
align-items: stretch !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-card > * {
|
||||
width: 100% !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
float: none;
|
||||
flex-wrap: wrap;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 10px !important;
|
||||
align-items: flex-start !important;
|
||||
padding: 12px 16px !important;
|
||||
box-sizing: border-box !important;
|
||||
width: 100% !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
position: static !important;
|
||||
inset: auto !important;
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
max-width: none !important;
|
||||
flex: 0 0 auto !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-card .widget-title,
|
||||
.dashboard-chart-card .widget-body {
|
||||
width: 100%;
|
||||
float: none !important;
|
||||
clear: both;
|
||||
display: block !important;
|
||||
position: static !important;
|
||||
.panel .dashboard-chart-header {
|
||||
padding: 12px 20px !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-card .widget-title {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.dashboard-chart-card .widget-body {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
.dashboard-chart-info {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 10px !important;
|
||||
flex: 0 0 auto !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: rgba(59, 130, 246, 0.18);
|
||||
color: var(--color-primary-light);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
border-radius: 10px !important;
|
||||
background: rgba(59, 130, 246, 0.18) !important;
|
||||
color: var(--color-primary-light) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
font-size: 16px !important;
|
||||
flex-shrink: 0 !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-meta {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 2px !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-heading {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.dashboard-chart-subtitle {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.dashboard-chart-link {
|
||||
.dashboard-chart-actions {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 12px !important;
|
||||
width: 100% !important;
|
||||
flex-shrink: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
}
|
||||
|
||||
.dashboard-chart-toggle-button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
padding: 6px 10px;
|
||||
width: 40px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(59, 130, 246, 0.24);
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dashboard-chart-link:hover {
|
||||
.dashboard-chart-toggle-button:hover {
|
||||
color: var(--color-text-primary);
|
||||
border-color: rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.dashboard-chart-toggle-button.is-active {
|
||||
background: rgba(59, 130, 246, 0.22);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dashboard-chart-toggle-button i {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dashboard-chart-body {
|
||||
@@ -1097,11 +1428,12 @@ body {
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
height: clamp(220px, 32vh, 280px);
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dashboard-chart-canvas canvas {
|
||||
@@ -1118,6 +1450,103 @@ body {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
/* Enhanced chart container styling */
|
||||
.dashboard-chart-card {
|
||||
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25) !important;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Chart action improvements */
|
||||
.dashboard-chart-actions {
|
||||
gap: 16px !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-link {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary-light);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: all 150ms ease;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dashboard-chart-link:hover {
|
||||
color: var(--color-primary);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Modern toggle improvements */
|
||||
.dashboard-chart-toggle {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-chart-toggle-button {
|
||||
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.dashboard-chart-toggle-button:hover {
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
border-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.dashboard-chart-toggle-button.is-active {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(59, 130, 246, 0.2));
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
box-shadow: 0 0 12px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.dashboard-chart-toggle-button i {
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
|
||||
.dashboard-chart-toggle-button:hover i {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
/* Responsive chart cards */
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-chart-canvas {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-chart-card {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.dashboard-chart-header {
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
gap: 12px !important;
|
||||
}
|
||||
|
||||
.dashboard-chart-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-chart-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dashboard-chart-canvas {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -1329,8 +1758,8 @@ body .panel .panel-heading,
|
||||
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);
|
||||
border-bottom: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ots-filter-title {
|
||||
@@ -1362,15 +1791,19 @@ body .panel .panel-heading,
|
||||
|
||||
.ots-filter-content {
|
||||
padding: 16px;
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
@@ -1936,6 +2369,58 @@ body .panel .panel-heading,
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* Minimal Icon-Only Buttons */
|
||||
.btn-icon {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 8px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
background-color: var(--color-surface-elevated) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
color: var(--color-text-secondary) !important;
|
||||
font-size: 0 !important;
|
||||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
cursor: pointer !important;
|
||||
position: relative !important;
|
||||
margin-left: 6px !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background-color: rgba(59, 130, 246, 0.12) !important;
|
||||
border-color: rgba(59, 130, 246, 0.3) !important;
|
||||
color: var(--color-primary-light) !important;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1) !important;
|
||||
}
|
||||
|
||||
.btn-icon:active {
|
||||
background-color: rgba(59, 130, 246, 0.2) !important;
|
||||
border-color: rgba(59, 130, 246, 0.4) !important;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-icon i,
|
||||
.btn-icon svg {
|
||||
font-size: 17px !important;
|
||||
line-height: 1 !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.btn-icon.btn-success {
|
||||
background-color: rgba(34, 197, 94, 0.08) !important;
|
||||
border-color: rgba(34, 197, 94, 0.2) !important;
|
||||
color: rgb(34, 197, 94) !important;
|
||||
}
|
||||
|
||||
.btn-icon.btn-success:hover {
|
||||
background-color: rgba(34, 197, 94, 0.15) !important;
|
||||
border-color: rgba(34, 197, 94, 0.4) !important;
|
||||
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.1) !important;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
UTILITY CLASSES
|
||||
============================================================================ */
|
||||
@@ -2083,7 +2568,9 @@ canvas {
|
||||
|
||||
#sidebar-wrapper .sidebar-list a,
|
||||
#sidebar-wrapper .sidebar-main a {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 2px 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
@@ -2383,17 +2870,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;
|
||||
@@ -2666,3 +3170,30 @@ a.text-muted:hover {
|
||||
.invisible {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
FILTER FIELDS PADDING
|
||||
============================================================================ */
|
||||
|
||||
.FilterDiv.card-body {
|
||||
padding-top: 20px !important;
|
||||
}
|
||||
/* ============================================================================
|
||||
LOGO WITH TEXT STYLING
|
||||
============================================================================ */
|
||||
|
||||
.navbar-brand.xibo-logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.xibo-logo-text {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -16,19 +16,51 @@
|
||||
function initSidebarToggle() {
|
||||
const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
const closeBtn = document.querySelector('.ots-sidebar-close');
|
||||
const collapseBtn = document.querySelector('.sidebar-collapse-btn');
|
||||
const body = document.body;
|
||||
|
||||
if (!toggleBtn || !sidebar) return;
|
||||
if (!sidebar) return;
|
||||
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
// Handle sidebar close button
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
sidebar.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
|
||||
if (collapseBtn) {
|
||||
const isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true';
|
||||
if (isCollapsed) {
|
||||
sidebar.classList.add('collapsed');
|
||||
body.classList.add('ots-sidebar-collapsed');
|
||||
}
|
||||
|
||||
collapseBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const nowCollapsed = !sidebar.classList.contains('collapsed');
|
||||
sidebar.classList.toggle('collapsed');
|
||||
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize sidebar section toggles
|
||||
initSidebarSectionToggles();
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', function(e) {
|
||||
if (window.innerWidth <= 768) {
|
||||
const isClickInsideSidebar = sidebar.contains(e.target);
|
||||
const isClickOnToggle = toggleBtn.contains(e.target);
|
||||
const isClickOnToggle = toggleBtn && toggleBtn.contains(e.target);
|
||||
|
||||
if (!isClickInsideSidebar && !isClickOnToggle && sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
@@ -37,27 +69,47 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize sidebar section collapse/expand functionality
|
||||
*/
|
||||
function initSidebarSectionToggles() {
|
||||
const groupToggles = document.querySelectorAll('.sidebar-group-toggle');
|
||||
|
||||
groupToggles.forEach(toggle => {
|
||||
toggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const group = toggle.closest('.sidebar-group');
|
||||
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
|
||||
if (!submenu) return;
|
||||
|
||||
const isOpen = group.classList.contains('is-open');
|
||||
group.classList.toggle('is-open', !isOpen);
|
||||
toggle.setAttribute('aria-expanded', (!isOpen).toString());
|
||||
submenu.style.display = isOpen ? 'none' : 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize dropdown menus
|
||||
*/
|
||||
function initDropdowns() {
|
||||
const dropdowns = document.querySelectorAll('.dropdown');
|
||||
|
||||
|
||||
dropdowns.forEach(dropdown => {
|
||||
const button = dropdown.querySelector('.dropdown-menu');
|
||||
|
||||
if (!button) return;
|
||||
|
||||
const toggle = dropdown.querySelector('.dropdown-toggle, [data-toggle="dropdown"], .dt-button');
|
||||
const menu = dropdown.querySelector('.dropdown-menu');
|
||||
|
||||
// Toggle menu on button click
|
||||
dropdown.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.user-btn') || e.target.closest('[aria-label="User menu"]')) {
|
||||
e.preventDefault();
|
||||
dropdown.classList.toggle('active');
|
||||
}
|
||||
|
||||
if (!toggle || !menu) return;
|
||||
|
||||
// Toggle menu on toggle click
|
||||
toggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dropdown.classList.toggle('active');
|
||||
});
|
||||
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!dropdown.contains(e.target)) {
|
||||
@@ -65,6 +117,99 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
// Support DataTables Buttons collections which are not wrapped by .dropdown
|
||||
document.addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.dt-button');
|
||||
|
||||
if (btn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const wrapper = btn.closest('.dt-buttons') || btn.parentElement;
|
||||
|
||||
// close other open dt-buttons collections
|
||||
document.querySelectorAll('.dt-buttons.active').forEach(w => {
|
||||
if (w !== wrapper) w.classList.remove('active');
|
||||
});
|
||||
|
||||
wrapper.classList.toggle('active');
|
||||
|
||||
// If DataTables placed the collection on the body, find it and position it under the clicked button
|
||||
const allCollections = Array.from(document.querySelectorAll('.dt-button-collection'));
|
||||
let collection = wrapper.querySelector('.dt-button-collection') || allCollections.find(c => !wrapper.contains(c));
|
||||
|
||||
// If DataTables didn't create a collection element, create one as a fallback
|
||||
if (!collection) {
|
||||
collection = document.createElement('div');
|
||||
collection.className = 'dt-button-collection';
|
||||
// prefer to append near wrapper for positioning; fallback to body
|
||||
(wrapper || document.body).appendChild(collection);
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
// hide other collections
|
||||
allCollections.forEach(c => { if (c !== collection) { c.classList.remove('show'); c.style.display = 'none'; } });
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const top = rect.bottom + window.scrollY;
|
||||
const left = rect.left + window.scrollX;
|
||||
|
||||
collection.style.position = 'absolute';
|
||||
collection.style.top = `${top}px`;
|
||||
collection.style.left = `${left}px`;
|
||||
collection.style.display = 'block';
|
||||
collection.classList.add('show');
|
||||
// DEBUG: log collection contents
|
||||
try {
|
||||
console.log('dt-button-collection opened, children:', collection.children.length, collection);
|
||||
} catch (err) {}
|
||||
|
||||
// If the collection is empty or visually empty, build a fallback column list from the nearest table
|
||||
const isEmpty = collection.children.length === 0 || collection.textContent.trim() === '' || collection.offsetHeight < 10;
|
||||
if (isEmpty) {
|
||||
try {
|
||||
let table = btn.closest('table') || wrapper.querySelector('table') || document.querySelector('table');
|
||||
if (table && window.jQuery && jQuery.fn && jQuery.fn.dataTable && jQuery.fn.dataTable.isDataTable(table)) {
|
||||
const dt = jQuery(table).DataTable();
|
||||
// clear existing
|
||||
collection.innerHTML = '';
|
||||
const thead = table.querySelectorAll('thead th');
|
||||
thead.forEach((th, idx) => {
|
||||
const text = (th.textContent || th.innerText || `Column ${idx+1}`).trim();
|
||||
const item = document.createElement('div');
|
||||
item.style.padding = '6px 12px';
|
||||
item.style.display = 'flex';
|
||||
item.style.alignItems = 'center';
|
||||
item.style.gap = '8px';
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.checked = dt.column(idx).visible();
|
||||
checkbox.addEventListener('change', function() {
|
||||
dt.column(idx).visible(this.checked);
|
||||
});
|
||||
const label = document.createElement('span');
|
||||
label.textContent = text;
|
||||
label.style.color = 'var(--color-text-primary)';
|
||||
item.appendChild(checkbox);
|
||||
item.appendChild(label);
|
||||
collection.appendChild(item);
|
||||
});
|
||||
console.log('Fallback: populated collection with', collection.children.length, 'items');
|
||||
} else {
|
||||
console.log('Fallback: no DataTable instance found to populate column visibility');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Error building fallback column list', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// click outside dt-button -> close any open collections
|
||||
document.querySelectorAll('.dt-buttons.active').forEach(w => w.classList.remove('active'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
91
custom/otssignange/views/about-page.twig
Normal file
91
custom/otssignange/views/about-page.twig
Normal file
@@ -0,0 +1,91 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2026 OTS Signs
|
||||
*
|
||||
* About page for OTS Signs.
|
||||
*/
|
||||
#}
|
||||
{% extends "non-authed.twig" %}
|
||||
|
||||
{% block title %}{{ "About"|trans }} | {% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style type="text/css">
|
||||
.about-container {
|
||||
padding: 24px 30px 30px;
|
||||
margin: 10px auto 20px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.about-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.about-links a {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.about-meta {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.about-disclaimer {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}{% endblock %}
|
||||
{% block contentClass %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<a class="btn btn-icon btn-info" href="{{ url_for("home") }}" title="{% trans "Home" %}"><i class="fa fa-home"></i></a>
|
||||
<div class="about-container">
|
||||
<h1>
|
||||
{% trans "About" %}
|
||||
<a href="https://ots-signs.com" target="_blank" rel="noopener noreferrer">OTS Signs</a>
|
||||
</h1>
|
||||
<p>
|
||||
{% trans "An" %}
|
||||
<a href="https://oribi-tech.com" target="_blank" rel="noopener noreferrer">Oribi Technology Services</a>
|
||||
{% trans "product." %}
|
||||
</p>
|
||||
<p class="text-muted">{% trans "OTS Signs provides a compact, focused admin UI and proxy for Xibo CMS" %}</p>
|
||||
|
||||
{% set appVersion = version|default("dev") %}
|
||||
{% set appEnvironment = appEnvironment|default(environment|default("local")) %}
|
||||
{% set commitSha = revision|default("") %}
|
||||
|
||||
<div class="about-meta">
|
||||
<div>{% trans "Version" %}: {{ appVersion }}</div>
|
||||
<div>{% trans "Environment" %}: {{ appEnvironment }}</div>
|
||||
{% if commitSha %}
|
||||
<div>{% trans "Commit" %}: {{ commitSha|slice(0, 7) }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="about-links">
|
||||
<a href="/privacy">{% trans "Privacy Policy" %}</a>
|
||||
<a href="/terms">{% trans "Terms of Service" %}</a>
|
||||
<a href="/open-source-licenses">{% trans "Open Source Licenses" %}</a>
|
||||
</div>
|
||||
|
||||
<div class="about-disclaimer">
|
||||
<strong>{% trans "Disclaimer:" %}</strong>
|
||||
{% trans "OTS Signs is an independent product developed by Oribi Technology Services. It is not affiliated with or endorsed by the Xibo project or its maintainers. Xibo is a trademark of Xibo Digital Signage Ltd. Use of Xibo APIs is subject to their terms and conditions." %}
|
||||
<div class="mt-2">
|
||||
<a href="https://github.com/xibosignage/xibo" target="_blank" rel="noopener noreferrer">{% trans "Xibo CMS on GitHub" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
55
custom/otssignange/views/about-text.twig
Normal file
55
custom/otssignange/views/about-text.twig
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
135
custom/otssignange/views/authed.twig
Normal file
135
custom/otssignange/views/authed.twig
Normal 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 %}
|
||||
186
custom/otssignange/views/campaign-page.twig
Normal file
186
custom/otssignange/views/campaign-page.twig
Normal 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 %}
|
||||
162
custom/otssignange/views/command-page.twig
Normal file
162
custom/otssignange/views/command-page.twig
Normal 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 %}
|
||||
@@ -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"
|
||||
|
||||
599
custom/otssignange/views/dataset-page.twig
Normal file
599
custom/otssignange/views/dataset-page.twig
Normal 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"> </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 %}
|
||||
261
custom/otssignange/views/daypart-page.twig
Normal file
261
custom/otssignange/views/daypart-page.twig
Normal file
@@ -0,0 +1,261 @@
|
||||
{#
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Dayparting"|trans }} | {% endblock %}
|
||||
|
||||
{% block actionMenu %}
|
||||
<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 %}
|
||||
@@ -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">
|
||||
|
||||
381
custom/otssignange/views/displaygroup-page.twig
Normal file
381
custom/otssignange/views/displaygroup-page.twig
Normal 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 %}
|
||||
168
custom/otssignange/views/displayprofile-page.twig
Normal file
168
custom/otssignange/views/displayprofile-page.twig
Normal 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 %}
|
||||
327
custom/otssignange/views/inline.twig
Normal file
327
custom/otssignange/views/inline.twig
Normal file
@@ -0,0 +1,327 @@
|
||||
|
||||
{% macro disabled(name, title, value, helpText, groupClass) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}">{{ title }}</label>
|
||||
<input readonly class="form-control" value="{{ value }}"></input>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro hidden(name, value) %}
|
||||
<input name="{{ name }}" type="hidden" id="{{ name }}" value="{{ value }}" />
|
||||
{% endmacro %}
|
||||
|
||||
{% macro raw(text, groupClass) %}
|
||||
<div class="{{ groupClass }}">
|
||||
{{ text|raw }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro message(message, groupClass, messageStyleClass) %}
|
||||
<div class="{% if messageStyleClass %}{{messageStyleClass}}{% endif %} mr-1 {{ groupClass }}">
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro alert(message, alertType, groupClass) %}
|
||||
<div class="row">
|
||||
<div class="mr-3 alert alert-{% if alertType %}{{alertType}}{% else %}primary{% endif %} {{ groupClass }}" role="alert">{{ message }}</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro button(title, type, link, groupClass) %}
|
||||
<div class="form-group {{ groupClass }}">
|
||||
{% if type == "link" %}
|
||||
<a class="btn btn-white xibo-inline-btn mr-1 ml-0" href="{{ link }}">{{ title }}</a>
|
||||
{% else %}
|
||||
<button class="btn btn-white xibo-inline-btn mr-1 ml-0" type="{{ type }}">{{ title }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro input(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro inputWithTags(name, title, value, helpText, groupClass, validation, accessKey, exactTag, exactTagTitle, logicalOperatorTitle, autoCompleteEnabled = 1) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
{% if exactTag %}
|
||||
<div class="input-group input-group-tags-exact">
|
||||
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" data-role="tagsInputInline" {% if autoCompleteEnabled == 1 %} data-auto-complete-url="{{ url_for('tag.search') }}?allTags=1" {% endif %} {{ validation }} />
|
||||
<div class="input-group-append input-group-addon">
|
||||
<div class="input-group-text">
|
||||
<input title="{{ exactTagTitle }}" type="checkbox" id="{{ exactTag }}" name="{{ exactTag }}">
|
||||
</div>
|
||||
<select class="custom-select" id="logicalOperator" name="logicalOperator" title="{{ logicalOperatorTitle }}" style="min-width:auto!important">
|
||||
<option value="OR" selected>OR</option>
|
||||
<option value="AND">AND</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" data-role="tagsInputInline" {% if autoCompleteEnabled == 1 %} data-auto-complete-url="{{ url_for('tag.search') }}?allTags=1" {% endif %} {{ validation }} />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro number(name, title, value, helpText, groupClass, validation, accessKey, maxNumber, minNumber) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control" name="{{ name }}" {% if maxNumber %}max="{{maxNumber}}" {% endif %}{% if minNumber %}min="{{minNumber}}" {% endif %}type="number" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro email(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control" name="{{ name }}" type="email" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro password(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" for="{{ name }}" title="{{ helpText }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control" name="{{ name }}" type="password" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro checkbox(name, title, value, groupClass, accessKey) %}
|
||||
<div class="form-group ml-2 mr-3 mb-1 {{ groupClass }}">
|
||||
<div class="form-check">
|
||||
<input title="{{ title }}" class="form-check-input" type="checkbox" id="{{ name }}" name="{{ name }}" {% if value == 1 %}checked{% endif %}>
|
||||
<label class="form-check-label" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro radio(name, id, title, value, helpText, groupClass, accessKey, setValue) %}
|
||||
<div class="form-group ml-2 mr-3 mb-1 {{ groupClass }}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="{{ id }}" name="{{ name }}" value="{{ setValue }}" {% if value == setValue %}checked{% endif %}>
|
||||
<label class="form-check-label" for="{{ name }}" title="{{ helpText }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dropdown(name, type, title, value, options, optionId, optionValue, helpText, groupClass, validation, accessKey, callBack, dataAttributes, optionGroups) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" for="{{ name }}" title="{{ helpText }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<select class="form-control" {% if type == "dropdownmulti" %}multiple{% endif %} name="{{ name }}" id="{{ name }}" {{ callBack }}
|
||||
{% if type == "dropdownmulti" %}
|
||||
data-allow-clear="true"
|
||||
data-placeholder--id=null
|
||||
data-placeholder--value=""
|
||||
{% endif %}
|
||||
{% if dataAttributes|length > 0 %}
|
||||
{% for attribute in dataAttributes %}
|
||||
{{ attribute.name }}="{{ attribute.value }}"
|
||||
{% endfor %}
|
||||
{% endif %}>
|
||||
|
||||
{% set hasGroups = optionGroups|length > 0 %}
|
||||
{% if not hasGroups %}
|
||||
{% set optionGroups = {label: "General"} %}
|
||||
{% endif %}
|
||||
|
||||
{% for group in optionGroups %}
|
||||
{% if hasGroups %}
|
||||
<optgroup label="{{ group.label }}">
|
||||
{% set tempOptions = attribute(options, group.id) %}
|
||||
{% else %}
|
||||
{% set tempOptions = options %}
|
||||
{% endif %}
|
||||
|
||||
{% for option in tempOptions %}
|
||||
|
||||
{% set itemOptionId = attribute(option, optionId) %}
|
||||
{% set itemOptionValue = attribute(option, optionValue) %}
|
||||
|
||||
{% if type == "dropdownmulti" %}
|
||||
{% set selected = (itemOptionId in value) %}
|
||||
{% else %}
|
||||
{% set selected = (itemOptionId == value) %}
|
||||
{% endif %}
|
||||
|
||||
<option value="{{ itemOptionId }}" {% if selected %}selected{% endif %}>{{ itemOptionValue }}</option>
|
||||
{% endfor %}
|
||||
|
||||
{% if hasGroups %}
|
||||
</optgroup>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro permissions(name, options) %}
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>{% trans "Group" %}</th>
|
||||
<th>{% trans "View" %}</th>
|
||||
<th>{% trans "Edit" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
{% for item in options %}
|
||||
<tr>
|
||||
<td>{{ name }}</td>
|
||||
<td><input type="checkbox" name="{{ name }}" value="{{ value_view }}" {{ value_view_checked }}></td>
|
||||
<td><input type="checkbox" name="{{ name }}" value="{{ value_edit }}" {{ value_edit_checked }}></td>
|
||||
<td><input type="checkbox" name="{{ name }}" value="{{ value_del }}" {{ value_del_checked }}></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro date(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></div>
|
||||
<input class="form-control dateControl date" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dateMonth(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></span>
|
||||
<input class="form-control dateControl month" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dateTime(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ linkedName }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></span>
|
||||
<input class="form-control dateControl dateTime" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro time(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1 {% if title == '' %}d-none{% endif %}" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-prepend input-group-text date-open-button" role="button"><i class="fa fa-calendar"></i></span>
|
||||
<input class="form-control dateControl time" type="text" {{ validation }} name="{{ name }}" id="{{ name }}" value="{{ value }}" />
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none" role="button"><i class="fa fa-times"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro switch(name, title, value, labelWidth, switchSize, onText, offText, groupClass, accessKey, disabled) %}
|
||||
<div class="form-group {{ groupClass }}">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="bootstrap-switch-target" id="{{ name }}" name="{{ name }}" accesskey="{{ accessKey }}"
|
||||
{% if value == 1 %}checked{% endif %}
|
||||
{% if disabled == 1 %}disabled{% endif %}
|
||||
data-label-text="{{ title }}"
|
||||
{% if onText not in [null, undefined, ""] %} data-on-text="{{ onText }}"{% endif %}
|
||||
{% if offText not in [null, undefined, ""] %} data-off-text="{{ offText }}"{% endif %}
|
||||
{% if switchSize not in [null, undefined, ""] %}data-size="{{ switchSize }}"{% else %}data-size="small"{% endif %}
|
||||
{% if labelWidth not in [null, undefined, ""] %} data-label-width="{{ labelWidth }}"{% endif %}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro color(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">{{ title }}</label>
|
||||
<input class="form-control XiboColorPicker" name="{{ name }}" type="text" id="{{ name }}" value="{{ value }}" {{ validation }} />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro inputNameGrid(name, title, groupClass, useRegexName, logicalOperatorName) %}
|
||||
<div class="form-group mr-1 mb-1 {{ groupClass }}">
|
||||
<label class="control-label mr-1" title="" for="{{ name }}" accesskey="">{{ title }}</label>
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<input class="form-control" name="{{ name }}" type="text" id="{{ name }}" value="">
|
||||
<div class="input-group-append input-group-addon">
|
||||
<div class="input-group-text">
|
||||
<input title="{% trans "Use Regex?" %}" type="checkbox" {% if useRegexName %} id="{{ useRegexName }}" name="{{ useRegexName }}" {% else %} id="useRegexForName" name="useRegexForName"{% endif %}>
|
||||
</div>
|
||||
<select class="custom-select" {% if logicalOperatorName %} id="{{ logicalOperatorName }}" name="{{ logicalOperatorName }}" {% else %} id="logicalOperatorName" name="logicalOperatorName"{% endif %}
|
||||
title="{% trans "When filtering by multiple names, which logical operator should be used?" %}" style="min-width:auto!important">
|
||||
<option value="OR" selected>OR</option>
|
||||
<option value="AND">AND</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dateRangeFilter(name, title, value, helpText, groupClass, validation, accessKey) %}
|
||||
<div class="form-group mr-1 mb-1 d-flex flex-row {{ groupClass }}">
|
||||
{% set today = now | date_modify('today') | date("Y-m-d H:i:s") %}
|
||||
<div class="form-group mr-1">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">
|
||||
{{ title }}
|
||||
</label>
|
||||
<div class="d-inline-flex">
|
||||
<select class="form-control XiboDateRangeFilter" name="{{ name }}" id="{{ name }}">
|
||||
<option value="" >{% trans "Select a range" %}</option>
|
||||
<option value="today" selected>{% trans "Today" %}</option>
|
||||
<option value="yesterday">{% trans "Yesterday" %}</option>
|
||||
<option value="thisweek">{% trans "This Week" %}</option>
|
||||
<option value="thismonth">{% trans "This Month" %}</option>
|
||||
<option value="thisyear">{% trans "This Year" %}</option>
|
||||
<option value="lastweek">{% trans "Last Week" %}</option>
|
||||
<option value="lastmonth">{% trans "Last Month" %}</option>
|
||||
<option value="lastyear">{% trans "Last Year" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group hidden mr-1 {{ 'rangeFilterInput_' ~ name }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">
|
||||
{% trans "From Date" %}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend input-group-text date-open-button" role="button">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</div>
|
||||
<input class="form-control dateControl date rangeInput"
|
||||
type="text" name="fromDt" id="{{ 'fromDt_' ~ name }}"
|
||||
value="{{ today }}"
|
||||
/>
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none"
|
||||
role="button"
|
||||
>
|
||||
<i class="fa fa-times"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group hidden {{ 'rangeFilterInput_' ~ name }}">
|
||||
<label class="control-label mr-1" title="{{ helpText }}" for="{{ name }}" accesskey="{{ accessKey }}">
|
||||
{% trans "To Date" %}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend input-group-text date-open-button" role="button">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</div>
|
||||
<input class="form-control dateControl date rangeInput"
|
||||
type="text" name="toDt" id="{{ 'toDt_' ~ name }}"
|
||||
value="{{ today | date_modify('+1 day -1 second') | date("Y-m-d H:i:s") }}"
|
||||
/>
|
||||
<span class="input-group-append input-group-addon input-group-text date-clear-button d-none"
|
||||
role="button"
|
||||
>
|
||||
<i class="fa fa-times"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
536
custom/otssignange/views/layout-page.twig
Normal file
536
custom/otssignange/views/layout-page.twig
Normal 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 %}
|
||||
576
custom/otssignange/views/library-page.twig
Normal file
576
custom/otssignange/views/library-page.twig
Normal 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 %}
|
||||
198
custom/otssignange/views/menuboard-page.twig
Normal file
198
custom/otssignange/views/menuboard-page.twig
Normal 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 %}
|
||||
@@ -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 {
|
||||
|
||||
198
custom/otssignange/views/playersoftware-page.twig
Normal file
198
custom/otssignange/views/playersoftware-page.twig
Normal 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 %}
|
||||
554
custom/otssignange/views/playlist-page.twig
Normal file
554
custom/otssignange/views/playlist-page.twig
Normal 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 %}
|
||||
133
custom/otssignange/views/resolution-page.twig
Normal file
133
custom/otssignange/views/resolution-page.twig
Normal 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 %}
|
||||
355
custom/otssignange/views/schedule-page.twig
Normal file
355
custom/otssignange/views/schedule-page.twig
Normal 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 %}
|
||||
190
custom/otssignange/views/syncgroup-page.twig
Normal file
190
custom/otssignange/views/syncgroup-page.twig
Normal 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 %}
|
||||
293
custom/otssignange/views/template-page.twig
Normal file
293
custom/otssignange/views/template-page.twig
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
173
tmp/xibo-campaign-page.twig
Normal file
173
tmp/xibo-campaign-page.twig
Normal file
@@ -0,0 +1,173 @@
|
||||
{#
|
||||
/**
|
||||
* 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-success XiboFormButton" title="{% trans "Add a new Campaign" %}" href="{{ url_for("campaign.add.form") }}"> <i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Campaign" %}</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-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="widget">
|
||||
<div class="widget-title">{% trans "Campaigns" %}</div>
|
||||
<div class="widget-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="campaignView">
|
||||
<div class="XiboFilter card mb-3 bg-light">
|
||||
<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 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">
|
||||
<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 %}
|
||||
1
tmp/xibo-context-menu.twig
Normal file
1
tmp/xibo-context-menu.twig
Normal file
@@ -0,0 +1 @@
|
||||
404: Not Found
|
||||
584
tmp/xibo-dataset-page.twig
Normal file
584
tmp/xibo-dataset-page.twig
Normal file
@@ -0,0 +1,584 @@
|
||||
{#
|
||||
/**
|
||||
* 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-success XiboFormButton btns" title="{% trans "Add a new DataSet" %}" href="{{ url_for("dataSet.add.form") }}"> <i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add DataSet" %}</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-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="widget">
|
||||
<div class="widget-title">{% trans "DataSets" %}</div>
|
||||
<div class="widget-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="dataSetView">
|
||||
<div class="XiboFilter card mb-3 bg-light">
|
||||
<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">
|
||||
<table id="datasets" class="table table-striped" data-state-preference-name="dataSetGrid" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Code" %}</th>
|
||||
<th>{% trans "Remote?" %}</th>
|
||||
<th>{% trans "Real time?" %}</th>
|
||||
<th>{% trans "Owner" %}</th>
|
||||
<th>{% trans "Sharing" %}</th>
|
||||
<th>{% trans "Last Sync" %}</th>
|
||||
<th>{% trans "Data Last Modified" %}</th>
|
||||
<th class="rowMenu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var table;
|
||||
$(document).ready(function() {
|
||||
{% if not currentUser.featureEnabled("folder.view") %}
|
||||
disableFolders();
|
||||
{% endif %}
|
||||
|
||||
table = $("#datasets").DataTable({
|
||||
"language": dataTablesLanguage,
|
||||
dom: dataTablesTemplate,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
responsive: true,
|
||||
stateLoadCallback: dataTableStateLoadCallback,
|
||||
stateSaveCallback: dataTableStateSaveCallback,
|
||||
filter: false,
|
||||
searchDelay: 3000,
|
||||
"order": [[ 0, "asc"]],
|
||||
ajax: {
|
||||
"url": "{{ url_for("dataSet.search") }}",
|
||||
"data": function(d) {
|
||||
$.extend(d, $("#datasets").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
{ "data": "dataSetId", responsivePriority: 2 },
|
||||
{ "data": "dataSet", "render": dataTableSpacingPreformatted, responsivePriority: 2 },
|
||||
{ "data": "description", responsivePriority: 4 },
|
||||
{ "data": "code", responsivePriority: 3 },
|
||||
{
|
||||
"data": "isRemote",
|
||||
responsivePriority: 3,
|
||||
"render": dataTableTickCrossColumn
|
||||
},
|
||||
{
|
||||
data: 'isRealTime',
|
||||
responsivePriority: 3,
|
||||
render: dataTableTickCrossColumn,
|
||||
},
|
||||
{ "data": "owner", responsivePriority: 3 },
|
||||
{
|
||||
"data": "groupsWithPermissions",
|
||||
responsivePriority: 3,
|
||||
"render": dataTableCreatePermissions
|
||||
},
|
||||
{
|
||||
"data": "lastSync",
|
||||
responsivePriority: 4,
|
||||
"render": dataTableDateFromUnix
|
||||
},
|
||||
{
|
||||
"data": "lastDataEdit",
|
||||
responsivePriority: 4,
|
||||
"render": dataTableDateFromUnix
|
||||
},
|
||||
{
|
||||
"orderable": false,
|
||||
responsivePriority: 1,
|
||||
"data": dataTableButtonsColumn
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
table.on('draw', function(e, settings) {
|
||||
dataTableDraw(e, settings);
|
||||
|
||||
// Upload form
|
||||
$(".dataSetImportForm").click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var template = Handlebars.compile($("#template-dataset-upload").html());
|
||||
var data = table.row($(this).closest("tr")).data();
|
||||
var columns = [];
|
||||
var i = 1;
|
||||
|
||||
$.each(data.columns, function (index, element) {
|
||||
if (element.dataSetColumnTypeId === 1) {
|
||||
element.index = i;
|
||||
columns.push(element);
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle bars and open a dialog
|
||||
bootbox.dialog({
|
||||
message: template({
|
||||
trans: {
|
||||
addFiles: "{% trans "Add CSV Files" %}",
|
||||
startUpload: "{% trans "Start upload" %}",
|
||||
cancelUpload: "{% trans "Cancel upload" %}",
|
||||
processing: "{% trans "Processing..." %}"
|
||||
},
|
||||
upload: {
|
||||
maxSize: {{ libraryUpload.maxSize }},
|
||||
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
|
||||
validExt: "{{ libraryUpload.validExt }}",
|
||||
utf8Message: "{% trans "If the CSV file contains non-ASCII characters please ensure the file is UTF-8 encoded" %}"
|
||||
},
|
||||
columns: columns
|
||||
}),
|
||||
title: "{% trans "CSV Import" %}",
|
||||
size: 'large',
|
||||
buttons: {
|
||||
main: {
|
||||
label: "{% trans "Done" %}",
|
||||
className: "btn-primary btn-bb-main",
|
||||
callback: function() {
|
||||
table.ajax.reload();
|
||||
XiboDialogClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).on('shown.bs.modal', function() {
|
||||
// Configure the upload form
|
||||
var url = "{{ url_for("dataSet.import", {id: ':id'}) }}".replace(":id", data.dataSetId);
|
||||
var form = $(this).find("form");
|
||||
var refreshSessionInterval;
|
||||
|
||||
// Initialize the jQuery File Upload widget:
|
||||
form.fileupload({
|
||||
url: url,
|
||||
disableImageResize: true
|
||||
});
|
||||
|
||||
// Upload server status check for browsers with CORS support:
|
||||
if ($.support.cors) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'HEAD'
|
||||
}).fail(function () {
|
||||
$('<span class="alert alert-error"/>')
|
||||
.text('Upload server currently unavailable - ' + new Date())
|
||||
.appendTo(form);
|
||||
});
|
||||
}
|
||||
|
||||
// Enable iframe cross-domain access via redirect option:
|
||||
form.fileupload(
|
||||
'option',
|
||||
'redirect',
|
||||
window.location.href.replace(
|
||||
/\/[^\/]*$/,
|
||||
'/cors/result.html?%s'
|
||||
)
|
||||
);
|
||||
|
||||
form.bind('fileuploadsubmit', function (e, data) {
|
||||
var inputs = data.context.find(':input');
|
||||
if (inputs.filter('[required][value=""]').first().focus().length) {
|
||||
return false;
|
||||
}
|
||||
data.formData = inputs.serializeArray().concat(form.serializeArray());
|
||||
|
||||
inputs.filter("input").prop("disabled", true);
|
||||
}).bind('fileuploadstart', function (e, data) {
|
||||
|
||||
// Show progress data
|
||||
form.find('.fileupload-progress .progress-extended').show();
|
||||
form.find('.fileupload-progress .progress-end').hide();
|
||||
|
||||
if (form.fileupload("active") <= 0)
|
||||
refreshSessionInterval = setInterval("XiboPing('" + pingUrl + "?refreshSession=true')", 1000 * 60 * 3);
|
||||
|
||||
return true;
|
||||
}).bind('fileuploaddone', function (e, data) {
|
||||
if (refreshSessionInterval != null && form.fileupload("active") <= 0)
|
||||
clearInterval(refreshSessionInterval);
|
||||
}).bind('fileuploadprogressall', function (e, data) {
|
||||
// Hide progress data and show processing
|
||||
if(data.total > 0 && data.loaded == data.total) {
|
||||
form.find('.fileupload-progress .progress-extended').hide();
|
||||
form.find('.fileupload-progress .progress-end').show();
|
||||
}
|
||||
}).bind('fileuploadadded fileuploadcompleted fileuploadfinished', function (e, data) {
|
||||
// Get uploaded and downloaded files and toggle Done button
|
||||
var filesToUploadCount = form.find('tr.template-upload').length;
|
||||
var $button = form.parents('.modal:first').find('button.btn-bb-main');
|
||||
|
||||
if(filesToUploadCount == 0) {
|
||||
$button.removeAttr('disabled');
|
||||
} else {
|
||||
$button.attr('disabled', 'disabled');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
table.on('processing.dt', dataTableProcessing);
|
||||
dataTableAddButtons(table, $('#datasets_wrapper').find('.dataTables_buttons'));
|
||||
|
||||
$("#refreshGrid").click(function () {
|
||||
table.ajax.reload();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function dataSetFormOpen(dialog) {
|
||||
// Bind the remote dataset test button
|
||||
$(dialog).find("#dataSetRemoteTestButton").on('click', function() {
|
||||
var $form = $(dialog).find("form");
|
||||
XiboRemoteRequest("{{ url_for("dataSet.test.remote") }}", $form.serializeObject(), function(response) {
|
||||
if (!response.success || !$.trim(response.data.entries)) {
|
||||
response.data = response.message;
|
||||
}
|
||||
$("#datasetRemoteTestRequestResult").html('<pre style="height: 300px; overflow: scroll">' + JSON.stringify(response.data, null, 3) + '</pre>');
|
||||
});
|
||||
});
|
||||
|
||||
// Set up some dependencies between the isRemote checkbox and the tabs related to remote datasets
|
||||
onRemoteFieldChanged(dialog);
|
||||
|
||||
// show data source dropdown if real time is checked
|
||||
onIsRealTimeFieldChanged(dialog);
|
||||
|
||||
$(dialog).find("#isRemote").on('change', function() {
|
||||
onRemoteFieldChanged(dialog);
|
||||
});
|
||||
|
||||
$(dialog).find("#isRealTime").on('change', function() {
|
||||
onIsRealTimeFieldChanged(dialog);
|
||||
});
|
||||
|
||||
// Auth field
|
||||
onAuthenticationFieldChanged(dialog);
|
||||
|
||||
$(dialog).find("#authentication").on('change', function() {
|
||||
onAuthenticationFieldChanged(dialog);
|
||||
});
|
||||
|
||||
// remote DataSet source
|
||||
onSourceFieldChanged(dialog);
|
||||
$(dialog).find('#sourceId').on('change', function() {
|
||||
onSourceFieldChanged(dialog);
|
||||
});
|
||||
|
||||
// Validate form manually because
|
||||
// uri field depends on isRemote being checked
|
||||
if (forms != undefined) {
|
||||
const $form = $(dialog).find('form');
|
||||
forms.validateForm(
|
||||
$form, // form
|
||||
$form.parent(), // container
|
||||
{
|
||||
submitHandler: XiboFormSubmit,
|
||||
rules: {
|
||||
uri: {
|
||||
required: function(element) {
|
||||
return $form.find('#isRemote').is(':checked')
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onIsRealTimeFieldChanged(dialog) {
|
||||
var isRealTime = $(dialog).find("#isRealTime").is(":checked");
|
||||
var dataSourceField = $(dialog).find("#dataSourceField");
|
||||
var dataConnectorSource = $(dialog).find("#dataConnectorSource");
|
||||
|
||||
if (isRealTime) {
|
||||
// show and enable data connector source
|
||||
dataSourceField.removeClass("d-none");
|
||||
dataConnectorSource.prop('disabled', false)
|
||||
} else {
|
||||
// hide and disable data connector source
|
||||
dataSourceField.addClass("d-none");
|
||||
dataConnectorSource.prop('disabled', true)
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoteFieldChanged(dialog) {
|
||||
var isRemote = $(dialog).find("#isRemote").is(":checked");
|
||||
var $remoteTabs = $(dialog).find(".tabForRemoteDataSet");
|
||||
|
||||
if (isRemote) {
|
||||
$remoteTabs.removeClass("d-none");
|
||||
} else {
|
||||
$remoteTabs.addClass("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
function onAuthenticationFieldChanged(dialog) {
|
||||
var authentication = $(dialog).find("#authentication").val();
|
||||
var $authFieldUserName = $(dialog).find(".auth-field-username");
|
||||
var $authFieldPassword = $(dialog).find(".auth-field-password");
|
||||
|
||||
if (authentication === "none") {
|
||||
$authFieldUserName.addClass("d-none");
|
||||
$authFieldPassword.addClass("d-none");
|
||||
} else if (authentication === "bearer") {
|
||||
$authFieldUserName.addClass("d-none");
|
||||
$authFieldPassword.removeClass("d-none");
|
||||
} else {
|
||||
$authFieldUserName.removeClass("d-none");
|
||||
$authFieldPassword.removeClass("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
function onSourceFieldChanged(dialog) {
|
||||
var sourceId = $(dialog).find('#sourceId').val();
|
||||
var $jsonSource = $(dialog).find(".json-source-field");
|
||||
var $csvSource = $(dialog).find(".csv-source-field");
|
||||
|
||||
if (sourceId == 1) {
|
||||
$jsonSource.removeClass('d-none');
|
||||
$csvSource.addClass('d-none');
|
||||
} else {
|
||||
$jsonSource.addClass('d-none');
|
||||
$csvSource.removeClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function deleteMultiSelectFormOpen(dialog) {
|
||||
{% set message = 'Delete any associated data?' %}
|
||||
|
||||
var $input = $('<input type=checkbox id="deleteData" name="deleteData"> {{ message|trans|e }} </input>');
|
||||
$input.on('change', function() {
|
||||
dialog.data().commitData = {deleteData: $(this).val()};
|
||||
});
|
||||
$(dialog).find('.modal-body').append($input);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScriptTemplates %}
|
||||
{{ parent() }}
|
||||
|
||||
{% verbatim %}
|
||||
|
||||
<script type="text/x-handlebars-template" id="template-dataset-upload">
|
||||
<form class="form-horizontal" method="post" enctype="multipart/form-data" data-max-file-size="{{ upload.maxSize }}" data-accept-file-types="/(\.|\/)csv/i">
|
||||
<div class="row fileupload-buttonbar">
|
||||
<div class="card p-3 mb-3 bg-light">
|
||||
{{ upload.maxSizeMessage }} <br>
|
||||
{{ upload.utf8Message }}
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<!-- The fileinput-button span is used to style the file input field as button -->
|
||||
<span class="btn btn-success fileinput-button">
|
||||
<i class="fa fa-plus"></i>
|
||||
<span>{{ trans.addFiles }}</span>
|
||||
<input type="file" name="files">
|
||||
</span>
|
||||
<button type="submit" class="btn btn-primary start">
|
||||
<i class="fa fa-upload"></i>
|
||||
<span>{{ trans.startUpload }}</span>
|
||||
</button>
|
||||
<button type="reset" class="btn btn-warning cancel">
|
||||
<i class="fa fa-ban"></i>
|
||||
<span>{{ trans.cancelUpload }}</span>
|
||||
</button>
|
||||
<!-- The loading indicator is shown during file processing -->
|
||||
<span class="fileupload-loading"></span>
|
||||
</div>
|
||||
<!-- The global progress information -->
|
||||
<div class="col-md-4 fileupload-progress fade">
|
||||
<!-- The global progress bar -->
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width:0%;">
|
||||
<div class="sr-only"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The extended global progress information -->
|
||||
<div class="progress-extended"> </div>
|
||||
<!-- Processing info container -->
|
||||
<div class="progress-end" style="display:none;">{{ trans.processing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% endverbatim %}
|
||||
{% set title %}{% trans "Overwrite existing data?" %}{% endset %}
|
||||
{% set helpText %}{% trans "Erase all content in this DataSet and overwrite it with the new content in this import." %}{% endset %}
|
||||
{{ forms.checkbox("overwrite", title, "", helpText) }}
|
||||
|
||||
{% set title %}{% trans "Ignore first row?" %}{% endset %}
|
||||
{% set helpText %}{% trans "Ignore the first row? Useful if the CSV has headings." %}{% endset %}
|
||||
{{ forms.checkbox("ignorefirstrow", title, "", helpText) }}
|
||||
|
||||
{% set message %}{% trans "In the fields below please enter the column number in the CSV file that corresponds to the Column Heading listed. This should be done before Adding the file." %}{% endset %}
|
||||
{{ forms.message(message) }}
|
||||
|
||||
{% verbatim %}
|
||||
{{#each columns}}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 control-label" for="csvImport_{{dataSetColumnId}}">{{heading}}</label>
|
||||
<div class="col-sm-10">
|
||||
<input class="form-control" name="csvImport_{{dataSetColumnId}}" type="number" id="csvImport_{{dataSetColumnId}}" value="{{ index }}" />
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The table listing the files available for upload/download -->
|
||||
<table role="presentation" class="table table-striped"><tbody class="files"></tbody></table>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<!-- The template to display files available for upload -->
|
||||
<script id="template-dataset-upload" type="text/x-tmpl">
|
||||
{% for (var i=0, file; file=o.files[i]; i++) { %}
|
||||
<tr class="template-upload">
|
||||
<td>
|
||||
<span class="fileupload-preview"></span>
|
||||
</td>
|
||||
<td class="title">
|
||||
{% if (file.error) { %}
|
||||
<div><span class="label label-danger">{%=file.error%}</span></div>
|
||||
{% } %}
|
||||
{% if (!file.error) { %}
|
||||
<label for="name[]"><input name="name[]" type="text" id="name" value="" /></label>
|
||||
{% } %}
|
||||
</td>
|
||||
<td>
|
||||
<p class="size">{%=o.formatFileSize(file.size)%}</p>
|
||||
{% if (!o.files.error) { %}
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" style="width:0%;">
|
||||
<div class="sr-only"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
</td>
|
||||
<td class="btn-group">
|
||||
{% if (!o.files.error && !i && !o.options.autoUpload) { %}
|
||||
<button class="btn btn-primary start">
|
||||
<i class="fa fa-upload"></i>
|
||||
</button>
|
||||
{% } %}
|
||||
{% if (!i) { %}
|
||||
<button class="btn btn-warning cancel">
|
||||
<i class="fa fa-ban"></i>
|
||||
</button>
|
||||
{% } %}
|
||||
</td>
|
||||
</tr>
|
||||
{% } %}
|
||||
</script>
|
||||
<!-- The template to display files available for download -->
|
||||
<script id="template-dataset-download" type="text/x-tmpl">
|
||||
{% for (var i=0, file; file=o.files[i]; i++) { %}
|
||||
<tr class="template-download">
|
||||
<td>
|
||||
<p class="name" id="{%=file.storedas%}" status="{% if (file.error) { %}error{% } %}">
|
||||
{%=file.name%}
|
||||
</p>
|
||||
{% if (file.error) { %}
|
||||
<div><span class="label label-danger">{%=file.error%}</span></div>
|
||||
{% } %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="size">{%=o.formatFileSize(file.size)%}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% } %}
|
||||
</script>
|
||||
|
||||
{% endverbatim %}
|
||||
{% endblock %}
|
||||
248
tmp/xibo-daypart-page.twig
Normal file
248
tmp/xibo-daypart-page.twig
Normal file
@@ -0,0 +1,248 @@
|
||||
{#
|
||||
/**
|
||||
* 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-success XiboFormButton" href="{{ url_for("daypart.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Daypart" %}</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-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="widget">
|
||||
<div class="widget-title">{% trans "Dayparting" %}</div>
|
||||
<div class="widget-body">
|
||||
<div class="XiboGrid" id="{{ random() }}">
|
||||
<div class="XiboFilter card mb-3 bg-light">
|
||||
<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 class="XiboData card pt-3">
|
||||
<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 %}
|
||||
523
tmp/xibo-layout-page.twig
Normal file
523
tmp/xibo-layout-page.twig
Normal file
@@ -0,0 +1,523 @@
|
||||
{% 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> {% trans "Add Layout" %}
|
||||
</button>
|
||||
<button class="btn btn-info" id="layoutUploadForm" title="{% trans "Import a Layout from a ZIP file." %}" href="#"> <i class="fa fa-cloud-download" aria-hidden="true"></i> {% trans "Import" %}</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-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="widget">
|
||||
<div class="widget-title">{% trans "Layouts" %}</div>
|
||||
<div class="widget-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-type="layout" data-grid-name="layoutView">
|
||||
<div class="XiboFilter card mb-3 bg-light">
|
||||
<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 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">
|
||||
<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 %}
|
||||
194
tmp/xibo-menuboard-page.twig
Normal file
194
tmp/xibo-menuboard-page.twig
Normal file
@@ -0,0 +1,194 @@
|
||||
{#
|
||||
/**
|
||||
* 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-success XiboFormButton" title="{% trans "Add a new Menu Board" %}" href="{{ url_for("menuBoard.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Menu Board" %}</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-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="widget">
|
||||
<div class="widget-title">{% trans "Menu Boards" %}</div>
|
||||
<div class="widget-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-type="menuBoard" data-grid-name="menuBoardView">
|
||||
<div class="XiboFilter card mb-3 bg-light">
|
||||
<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 class="row">
|
||||
<div class="col-sm-1 form-group" style="padding: 0">
|
||||
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary btn-sm" title="{% trans "Open / Close Folder Search options" %}"><i class="fas fa-bars fa-1x"></i></button>
|
||||
</div>
|
||||
<div class="form-group col-sm-11" style="padding: 0">
|
||||
<div id="breadcrumbs" style="margin-top: 5px;"></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">
|
||||
<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 %}
|
||||
120
tmp/xibo-resolution-page.twig
Normal file
120
tmp/xibo-resolution-page.twig
Normal file
@@ -0,0 +1,120 @@
|
||||
{#
|
||||
/**
|
||||
* 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-success XiboFormButton btns" title="{% trans "Add a new resolution for use on layouts" %}" href="{{ url_for("resolution.add.form") }}"><span class="fa fa-plus"></span> {% trans "Add Resolution" %}</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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="widget">
|
||||
<div class="widget-title">{% trans "Resolution" %}</div>
|
||||
<div class="widget-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="resolutionView">
|
||||
<div class="XiboFilter card mb-3 bg-light">
|
||||
<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 class="XiboData card pt-3">
|
||||
<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 %}
|
||||
342
tmp/xibo-schedule-page.twig
Normal file
342
tmp/xibo-schedule-page.twig
Normal file
@@ -0,0 +1,342 @@
|
||||
{#
|
||||
/**
|
||||
* 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-success XiboFormButton btns" title="{% trans "Add a new Scheduled event" %}"
|
||||
href="{{ url_for("schedule.add.form") }}"><span class="fa fa-plus"></span> {% trans "Add Event" %}
|
||||
</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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="widget">
|
||||
<div class="widget-title">{% trans "Schedule" %}</div>
|
||||
<div class="widget-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="scheduleGridView">
|
||||
<div class="XiboFilter card mb-3 bg-light">
|
||||
<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 class="XiboSchedule 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 %}
|
||||
1
tmp/xibo-table-context-menu.twig
Normal file
1
tmp/xibo-table-context-menu.twig
Normal file
@@ -0,0 +1 @@
|
||||
404: Not Found
|
||||
277
tmp/xibo-template-page-new.twig
Normal file
277
tmp/xibo-template-page-new.twig
Normal file
@@ -0,0 +1,277 @@
|
||||
{#
|
||||
/**
|
||||
* 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-success XiboFormButton btns" title="{% trans "Add a new Template and jump to the layout editor." %}" href="{{ url_for("template.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Template" %}</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-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="widget">
|
||||
<div class="widget-title">{% trans "Templates" %}</div>
|
||||
<div class="widget-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="templateView">
|
||||
<div class="XiboFilter card mb-3 bg-light">
|
||||
<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">
|
||||
<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>
|
||||
{% endblock %}
|
||||
277
tmp/xibo-template-page.twig
Normal file
277
tmp/xibo-template-page.twig
Normal file
@@ -0,0 +1,277 @@
|
||||
{#
|
||||
/**
|
||||
* 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-success XiboFormButton btns" title="{% trans "Add a new Template and jump to the layout editor." %}" href="{{ url_for("template.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Template" %}</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-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="widget">
|
||||
<div class="widget-title">{% trans "Templates" %}</div>
|
||||
<div class="widget-body">
|
||||
<div class="XiboGrid" id="{{ random() }}" data-grid-name="templateView">
|
||||
<div class="XiboFilter card mb-3 bg-light">
|
||||
<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">
|
||||
<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>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user