From 622b1996c2caa8f25b9289f3e840a4261e5835cf Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 12 Sep 2024 15:45:33 +0100 Subject: [PATCH 01/22] Layout Editor: exchange tab should not be visible if the connector is disabled (#2734) * Connector: add a template provider list route, event and handle in exchange connector. * Editor: Add template connector dynamically fixes xibosignage/xibo#3497 --------- Co-authored-by: maurofmferrao --- lib/Connector/XiboExchangeConnector.php | 23 +++++ lib/Controller/Template.php | 19 +++++ lib/Event/TemplateProviderListEvent.php | 60 ++++++++++++++ lib/routes-web.php | 2 + ui/src/editor-core/toolbar.js | 106 +++++++++++++++++------- ui/src/style/toolbar.scss | 2 +- ui/src/templates/toolbar-content.hbs | 2 +- views/common.twig | 9 +- 8 files changed, 186 insertions(+), 37 deletions(-) create mode 100644 lib/Event/TemplateProviderListEvent.php diff --git a/lib/Connector/XiboExchangeConnector.php b/lib/Connector/XiboExchangeConnector.php index d7d405bf65..38270145b9 100644 --- a/lib/Connector/XiboExchangeConnector.php +++ b/lib/Connector/XiboExchangeConnector.php @@ -30,6 +30,7 @@ use Xibo\Entity\SearchResult; use Xibo\Event\TemplateProviderEvent; use Xibo\Event\TemplateProviderImportEvent; +use Xibo\Event\TemplateProviderListEvent; use Xibo\Support\Sanitizer\SanitizerInterface; /** @@ -50,6 +51,7 @@ public function registerWithDispatcher(EventDispatcherInterface $dispatcher): Co { $dispatcher->addListener('connector.provider.template', [$this, 'onTemplateProvider']); $dispatcher->addListener('connector.provider.template.import', [$this, 'onTemplateProviderImport']); + $dispatcher->addListener('connector.provider.template.list', [$this, 'onTemplateList']); return $this; } @@ -239,4 +241,25 @@ private function createSearchResult($template) : SearchResult $searchResult->download = $template->downloadUrl; return $searchResult; } + + /** + * Add this connector to the list of providers. + * @param \Xibo\Event\TemplateProviderListEvent $event + * @return void + */ + public function onTemplateList(TemplateProviderListEvent $event): void + { + $this->getLogger()->debug('onTemplateList:event'); + + $providerDetails = new ProviderDetails(); + $providerDetails->id = $this->getSourceName(); + $providerDetails->link = 'https://xibosignage.com'; + $providerDetails->logoUrl = $this->getThumbnail(); + $providerDetails->iconUrl = 'exchange-alt'; + $providerDetails->message = $this->getTitle(); + $providerDetails->backgroundColor = ''; + $providerDetails->mediaTypes = ['xlf']; + + $event->addProvider($providerDetails); + } } diff --git a/lib/Controller/Template.php b/lib/Controller/Template.php index db8e7b9d63..ff1ffc0808 100644 --- a/lib/Controller/Template.php +++ b/lib/Controller/Template.php @@ -22,11 +22,13 @@ namespace Xibo\Controller; use Parsedown; +use Psr\Http\Message\ResponseInterface; use Slim\Http\Response as Response; use Slim\Http\ServerRequest as Request; use Xibo\Entity\SearchResult; use Xibo\Entity\SearchResults; use Xibo\Event\TemplateProviderEvent; +use Xibo\Event\TemplateProviderListEvent; use Xibo\Factory\LayoutFactory; use Xibo\Factory\TagFactory; use Xibo\Support\Exception\AccessDeniedException; @@ -767,4 +769,21 @@ public function editForm(Request $request, Response $response, $id) return $this->render($request, $response); } + + /** + * Get list of Template providers with their details. + * + * @param Request $request + * @param Response $response + * @return Response|ResponseInterface + */ + public function providersList(Request $request, Response $response): Response|\Psr\Http\Message\ResponseInterface + { + $event = new TemplateProviderListEvent(); + $this->getDispatcher()->dispatch($event, $event->getName()); + + $providers = $event->getProviders(); + + return $response->withJson($providers); + } } diff --git a/lib/Event/TemplateProviderListEvent.php b/lib/Event/TemplateProviderListEvent.php new file mode 100644 index 0000000000..4dfd71ec7d --- /dev/null +++ b/lib/Event/TemplateProviderListEvent.php @@ -0,0 +1,60 @@ +. + */ + +namespace Xibo\Event; + +use Xibo\Connector\ProviderDetails; + +/** + * Get a list of template providers + */ +class TemplateProviderListEvent extends Event +{ + protected static $NAME = 'connector.provider.template.list'; + /** + * @var array + */ + private mixed $providers; + + public function __construct($providers = []) + { + $this->providers = $providers; + } + + /** + * @param ProviderDetails $provider + * @return TemplateProviderListEvent + */ + public function addProvider(ProviderDetails $provider): TemplateProviderListEvent + { + $this->providers[] = $provider; + return $this; + } + + /** + * @return ProviderDetails[] + */ + public function getProviders(): array + { + return $this->providers; + } +} diff --git a/lib/routes-web.php b/lib/routes-web.php index 1fa0372ccc..54cbb4bbae 100644 --- a/lib/routes-web.php +++ b/lib/routes-web.php @@ -378,6 +378,8 @@ // // template // +$app->get('/template/connector/list', ['\Xibo\Controller\Template','providersList']) + ->setName('template.search.providers'); $app->get('/template/search', ['\Xibo\Controller\Template', 'search'])->setName('template.search.all'); $app->get('/template/view', ['\Xibo\Controller\Template','displayPage']) ->addMiddleware(new FeatureAuth($app->getContainer(), ['template.view'])) diff --git a/ui/src/editor-core/toolbar.js b/ui/src/editor-core/toolbar.js index e7b9ada2f1..1b5fa1be25 100644 --- a/ui/src/editor-core/toolbar.js +++ b/ui/src/editor-core/toolbar.js @@ -612,29 +612,6 @@ Toolbar.prototype.init = function({isPlaylist = false} = {}) { state: '', itemCount: 0, }, - { - name: 'layout_exchange', - disabled: isPlaylist, - itemName: toolbarTrans.menuItems.layoutExchangeName, - itemIcon: 'exchange-alt', // TODO: Change icon! - itemTitle: toolbarTrans.menuItems.layoutExchangeTitle, - contentType: 'layout_exchange', - filters: { - name: { - value: '', - key: 'template', - }, - provider: { - value: 'remote', - locked: true, - }, - orientation: { - value: '', - }, - }, - state: '', - itemCount: 0, - }, ]; // Menu items @@ -666,7 +643,71 @@ Toolbar.prototype.init = function({isPlaylist = false} = {}) { this.moduleListOtherTypes = moduleListOtherTypes; this.moduleGroups = moduleGroups; - // Get providers + // Get template providers + $.ajax(urlsForApi.template.getProviders).done(function(res) { + // Stop if not available + if (Object.keys(self).length === 0) { + return; + } + + if ( + Array.isArray(res) + ) { + res.forEach((provider) => { + // Add provider to menu items + self.menuItems.push( + { + name: provider.id, + disabled: isPlaylist, + itemName: provider.message, + // link: provider.link, + itemTitle: toolbarTrans.menuItems + .layoutExchangeTitle.replace('%obj%', provider.message), + itemIcon: provider.iconUrl, + contentType: 'template_exchange', + filters: { + name: { + value: '', + key: 'template', + }, + provider: { + value: 'remote', + locked: true, + }, + orientation: { + value: '', + }, + }, + state: '', + itemCount: 0, + }); + }); + } else { + // Login Form needed? + if (res.login) { + window.location.href = window.location.href; + location.reload(); + } else { + // Just an error we dont know about + if (res.message == undefined) { + console.error(res); + } else { + console.error(res.message); + } + } + } + + // Render + self.render(); + }).catch(function(jqXHR, textStatus, errorThrown) { + console.error(jqXHR, textStatus, errorThrown); + console.error(errorMessagesTrans.getProvidersFailed); + + // Render + self.render(); + }); + + // Get media providers $.ajax(urlsForApi.media.getProviders).done(function(res) { // Stop if not available if (Object.keys(self).length === 0) { @@ -1326,7 +1367,7 @@ Toolbar.prototype.createContent = function( this.elementsContentCreateWindow(menu, savePrefs); } else if ( content.contentType === 'layout_templates' || - content.contentType === 'layout_exchange' + content.contentType === 'template_exchange' ) { this.layoutTemplatesContentCreateWindow(menu); } else if (content.contentType === 'playlists') { @@ -2413,7 +2454,7 @@ Toolbar.prototype.elementsContentCreateWindow = function( Toolbar.prototype.layoutTemplatesContentCreateWindow = function(menu) { const self = this; const app = this.parent; - const tabContentName = this.menuItems[menu].name; + const tabContentType = this.menuItems[menu].contentType; // Deselect previous selections self.deselectCardsAndDropZones(); @@ -2424,18 +2465,19 @@ Toolbar.prototype.layoutTemplatesContentCreateWindow = function(menu) { filters: this.menuItems[menu].filters, trans: toolbarTrans, formClass: 'layout_tempates-search-form', - headerMessage: (tabContentName == 'layout_exchange') ? - toolbarTrans.layoutExchangeTemplatesMessage : + headerMessage: (tabContentType == 'template_exchange') ? + toolbarTrans.layoutExchangeTemplatesMessage + .replace('%obj%', this.menuItems[menu].itemName) : toolbarTrans.layoutTemplatesMessage, }); // Clear temp data app.common.clearContainer( - self.DOMObject.find('#' + tabContentName + '-container-' + menu), + self.DOMObject.find('#' + tabContentType + '-container-' + menu), ); // Append template to the search main div - self.DOMObject.find('#' + tabContentName + '-container-' + menu).html(html); + self.DOMObject.find('#' + tabContentType + '-container-' + menu).html(html); // Load content this.layoutTemplatesContentPopulate(menu); @@ -2473,9 +2515,9 @@ Toolbar.prototype.playlistsContentCreateWindow = function(menu) { Toolbar.prototype.layoutTemplatesContentPopulate = function(menu) { const app = this.parent; const self = this; - const tabContentName = this.menuItems[menu].name; + const tabContentType = this.menuItems[menu].contentType; const $container = self.DOMObject.find( - '#' + tabContentName + '-container-' + menu, + '#' + tabContentType + '-container-' + menu, ); const $content = self.DOMObject.find('#media-content-' + menu); const $searchForm = $container.parent().find('.toolbar-search-form'); diff --git a/ui/src/style/toolbar.scss b/ui/src/style/toolbar.scss index 2e0a166107..303a80d124 100644 --- a/ui/src/style/toolbar.scss +++ b/ui/src/style/toolbar.scss @@ -629,7 +629,7 @@ } } - &.toolbar-layout_templates-pane, &.toolbar-layout_exchange-pane { + &.toolbar-layout_templates-pane, &.toolbar-template_exchange-pane { .toolbar-card:not(.has-thumb):hover .media-title { overflow: visible; white-space: normal; diff --git a/ui/src/templates/toolbar-content.hbs b/ui/src/templates/toolbar-content.hbs index 51bc623b3e..546603021b 100644 --- a/ui/src/templates/toolbar-content.hbs +++ b/ui/src/templates/toolbar-content.hbs @@ -1,5 +1,5 @@
diff --git a/views/common.twig b/views/common.twig index 2fe86d6faf..8e058ec5bf 100644 --- a/views/common.twig +++ b/views/common.twig @@ -142,6 +142,10 @@ list: { url: "{{ url_for("template.view") }}" }, + getProviders: { + url: "{{ url_for("template.search.providers") }}", + type: 'GET' + }, }, region: { transform: { @@ -765,8 +769,7 @@ actionsTitle: "{{ "Interactive actions"|trans }}", layoutTemplateName: "{{ "Layout Templates" |trans }}", layoutTemplateTitle: "{{ "Search for Layout Templates"|trans }}", - layoutExchangeName: "{{ "Xibo Layout Exchange" |trans }}", - layoutExchangeTitle: "{{ "Search for templates available from the Xibo Exchange."|trans }}", + layoutExchangeTitle: "{{ "Search for templates available from the %obj%."|trans }}", playlistsName: "{{ "Playlists" |trans }}", playlistsTitle: "{{ "Add Playlists"|trans }}", providerTitle: "{{ "Provider: %obj%"|trans }}", @@ -816,7 +819,7 @@ layout: "{{ "Layout"|trans }}", region: "{{ "Zone"|trans }}", layoutTemplatesMessage: "{{ "Replace your Layout with a template?"|trans }}", - layoutExchangeTemplatesMessage: "{{ "Replace your Layout with a Xibo Exchange template?"|trans }}", + layoutExchangeTemplatesMessage: "{{ "Replace your Layout with a %obj% template?"|trans }}", isRequired: "{{ "Required"|trans }}", libraryTypes: { image: "{{ "Image" |trans }}", From b27e95cca6eb7cc4e933e51a5f209cdd9c00348b Mon Sep 17 00:00:00 2001 From: Mae Grace Baybay <87640602+mgbaybay@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:36:52 +0800 Subject: [PATCH 02/22] Help Button: Hide in mobile view (#2730) --- views/authed.twig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/authed.twig b/views/authed.twig index 393de28ac2..0ad8e10c79 100644 --- a/views/authed.twig +++ b/views/authed.twig @@ -109,7 +109,8 @@ {% set helpLinks = helpService.getLinksForPage(route) %} {% set isXiboThemed = theme.getThemeConfig("app_name") == "Xibo" %} {% if helpLinks|length > 0 %} -
+ {# Hide in mobile view (sm/<768px) #} +
{{ "Help"|trans }}
    From 3b09d00bd3ffb6205087301e84833bcbdfba4642 Mon Sep 17 00:00:00 2001 From: Nadz Mangandog <61860661+nadzpogi@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:51:00 +0800 Subject: [PATCH 03/22] Bugfix: assigning a display to a group from the Display page doesn't clear the cache (#2735) relates to xibosignage/xibo#3481 --- lib/Controller/Display.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php index 2c6f0911e5..d2b92941aa 100644 --- a/lib/Controller/Display.php +++ b/lib/Controller/Display.php @@ -2167,6 +2167,9 @@ public function assignDisplayGroup(Request $request, Response $response, $id) $displayGroup->save(['validate' => false]); } + // Queue display to check for cache updates + $display->notify(); + // Return $this->getState()->hydrate([ 'httpStatus' => 204, From 2cb3e69f1d634d26b52edafd6fad0fb967f2852d Mon Sep 17 00:00:00 2001 From: Mae Grace Baybay <87640602+mgbaybay@users.noreply.github.com> Date: Fri, 20 Sep 2024 18:57:06 +0800 Subject: [PATCH 04/22] Reports: Add Proof of Play MySQL grouping (#2737) --- lib/Report/ProofOfPlay.php | 99 ++++++++++++++++++++++++---- reports/proofofplay-report-form.twig | 76 ++++++++++++++++++--- tests/XMDS.http | 19 ++++++ 3 files changed, 170 insertions(+), 24 deletions(-) diff --git a/lib/Report/ProofOfPlay.php b/lib/Report/ProofOfPlay.php index cef45eeed6..7c42a359f3 100644 --- a/lib/Report/ProofOfPlay.php +++ b/lib/Report/ProofOfPlay.php @@ -32,6 +32,7 @@ use Xibo\Factory\LayoutFactory; use Xibo\Factory\MediaFactory; use Xibo\Factory\ReportScheduleFactory; +use Xibo\Factory\DisplayGroupFactory; use Xibo\Helper\ApplicationState; use Xibo\Helper\DateFormatHelper; use Xibo\Helper\SanitizerService; @@ -69,6 +70,11 @@ class ProofOfPlay implements ReportInterface */ private $reportScheduleFactory; + /** + * @var DisplayGroupFactory + */ + private $displayGroupFactory; + /** * @var SanitizerService */ @@ -94,6 +100,7 @@ public function setFactories(ContainerInterface $container) $this->mediaFactory = $container->get('mediaFactory'); $this->layoutFactory = $container->get('layoutFactory'); $this->reportScheduleFactory = $container->get('reportScheduleFactory'); + $this->displayGroupFactory = $container->get('displayGroupFactory'); $this->sanitizer = $container->get('sanitizerService'); return $this; @@ -325,6 +332,12 @@ public function getResults(SanitizerInterface $sanitizedParams) $operator = $sanitizedParams->getString('logicalOperator', ['default' => 'OR']); $parentCampaignId = $sanitizedParams->getInt('parentCampaignId'); + // Group the data by display, display group, or by tag + $groupBy = $sanitizedParams->getString('groupBy'); + + // Used with groupBy in case we want to filter by specific display groups only + $displayGroupIds = $sanitizedParams->getIntArray('displayGroupId', ['default' => []]); + // Display filter. try { // Get an array of display id this user has access to. @@ -472,7 +485,8 @@ public function getResults(SanitizerInterface $sanitizedParams) $tags, $tagsType, $exactTags, - $operator + $operator, + $groupBy ); } @@ -505,7 +519,10 @@ public function getResults(SanitizerInterface $sanitizedParams) $entry['minStart'] = Carbon::createFromTimestamp($row['minStart'])->format(DateFormatHelper::getSystemFormat()); $entry['maxEnd'] = Carbon::createFromTimestamp($row['maxEnd'])->format(DateFormatHelper::getSystemFormat()); $entry['mediaId'] = $sanitizedRow->getInt('mediaId'); - + $entry['displayGroup'] = $sanitizedRow->getString('displayGroup'); + $entry['displayGroupId'] = $sanitizedRow->getInt('displayGroupId'); + $entry['tagName'] = $sanitizedRow->getString('tagName'); + $entry['tagId'] = $sanitizedRow->getInt('tagId'); $rows[] = $entry; } @@ -541,6 +558,7 @@ public function getResults(SanitizerInterface $sanitizedParams) * @param $tags string * @param $tagsType string * @param $exactTags mixed + * @param $groupBy string * @return array[array result, date periodStart, date periodEnd, int count, int totalStats] */ private function getProofOfPlayReportMySql( @@ -555,7 +573,8 @@ private function getProofOfPlayReportMySql( $tags, $tagsType, $exactTags, - $logicalOperator + $logicalOperator, + $groupBy ) { $fromDt = $fromDt->format('U'); $toDt = $toDt->format('U'); @@ -563,7 +582,6 @@ private function getProofOfPlayReportMySql( // Media on Layouts Ran $select = ' SELECT stat.type, - display.Display, stat.parentCampaignId, campaign.campaign as parentCampaign, IFNULL(layout.Layout, @@ -581,10 +599,23 @@ private function getProofOfPlayReportMySql( stat.tag, stat.layoutId, stat.mediaId, - stat.widgetId, - stat.displayId + stat.widgetId '; + // We get the ID and name - either by display, display group or tag + if ($groupBy === 'display') { + $select .= ', display.Display, stat.displayId '; + } else if ($groupBy === 'displayGroup') { + $select .= ', displaydg.displayGroup, displaydg.displayGroupId '; + } else if ($groupBy === 'tag') { + if ($tagsType === 'dg' || $tagsType === 'media') { + $select .= ', taglink.value, taglink.tagId '; + } else { + // For layouts, we need to manually select taglink.tag + $select .= ', taglink.tag AS value, taglink.tagId '; + } + } + $body = ' FROM stat LEFT OUTER JOIN display @@ -615,6 +646,17 @@ private function getProofOfPlayReportMySql( } } + if ($groupBy === 'displayGroup') { + // Group the data by display group + $body .= 'INNER JOIN `lkdisplaydg` AS linkdg + ON linkdg.DisplayID = display.displayid + INNER JOIN `displaygroup` AS displaydg + ON displaydg.displaygroupId = linkdg.displaygroupId + AND `displaydg`.isDisplaySpecific = 0 '; + } else if ($groupBy === 'tag') { + $body .= $this->groupByTagType($tagsType); + } + $body .= ' WHERE stat.type <> \'displaydown\' AND stat.end > :fromDt AND stat.start < :toDt @@ -799,23 +841,29 @@ private function getProofOfPlayReportMySql( $body .= ' AND `media`.mediaId IN (' . trim($mediaSql, ',') . ')'; } + // We first implement default groupings $body .= ' GROUP BY stat.type, stat.tag, - display.Display, stat.parentCampaignId, - stat.displayId, stat.campaignId, layout.layout, IFNULL(stat.mediaId, stat.widgetId), IFNULL(`media`.name, IFNULL(`widgetoption`.value, `widget`.type)), - stat.tag, stat.layoutId, stat.mediaId, - stat.widgetId, - stat.displayId + stat.widgetId '; + // Then add the optional groupings + if ($groupBy === 'display') { + $body .= ', display.Display, stat.displayId'; + } else if ($groupBy === 'displayGroup') { + $body .= ', displaydg.displayGroupId, displaydg.displayGroup'; + } else if ($groupBy === 'tag') { + $body .= ', value, taglink.tagId'; + } + $order = ''; if ($columns != null) { $order = 'ORDER BY ' . implode(',', $columns); @@ -829,8 +877,8 @@ private function getProofOfPlayReportMySql( $entry = []; $entry['type'] = $row['type']; - $entry['displayId'] = $row['displayId']; - $entry['display'] = $row['Display']; + $entry['displayId'] = $row['displayId'] ?? ''; + $entry['display'] = $row['Display'] ?? ''; $entry['layout'] = $row['Layout']; $entry['parentCampaignId'] = $row['parentCampaignId']; $entry['parentCampaign'] = $row['parentCampaign']; @@ -843,7 +891,10 @@ private function getProofOfPlayReportMySql( $entry['widgetId'] = $row['widgetId']; $entry['mediaId'] = $row['mediaId']; $entry['tag'] = $row['tag']; - + $entry['displayGroupId'] = $row['displayGroupId'] ?? ''; + $entry['displayGroup'] = $row['displayGroup'] ?? ''; + $entry['tagId'] = $row['tagId'] ?? ''; + $entry['tagName'] = $row['value'] ?? ''; $rows[] = $entry; } @@ -1157,4 +1208,24 @@ private function getProofOfPlayReportMongoDb( 'count' => count($rows) ]; } + + /** + * Add grouping by tag type + * @param string $tagType + * @return string + */ + private function groupByTagType(string $tagType) : string + { + return match ($tagType) { + 'media' => 'INNER JOIN `lktagmedia` AS taglink ON taglink.mediaId = stat.mediaId', + 'layout' => 'INNER JOIN `lktaglayout` ON `lktaglayout`.layoutId = stat.layoutId + INNER JOIN `tag` AS taglink ON taglink.tagId = `lktaglayout`.tagId', + 'dg' => 'INNER JOIN `lkdisplaydg` AS linkdg + ON linkdg.DisplayID = display.displayid + INNER JOIN `displaygroup` AS displaydg + ON displaydg.displaygroupId = linkdg.displaygroupId + AND `displaydg`.isDisplaySpecific = 1 INNER JOIN + `lktagdisplaygroup` AS taglink ON taglink.displaygroupId = displaydg.displaygroupId', + }; + } } diff --git a/reports/proofofplay-report-form.twig b/reports/proofofplay-report-form.twig index 7309068976..c9d611e2ac 100644 --- a/reports/proofofplay-report-form.twig +++ b/reports/proofofplay-report-form.twig @@ -80,6 +80,14 @@ {% set title %}{% trans "Time" %}{% endset %} {{ inline.time("statsToDtTime", title, "00:00", "", "stats-to-dt-time") }} + {% set title %}{% trans "Group By" %}{% endset %} + {% set options = [ + { id: "display", name: "Display" }, + { id: "displayGroup", name: "Display Group"|trans }, + { id: "tag", name: "Tag"|trans } + ] %} + {{ inline.dropdown("groupBy", "single", title, "", options, "id", "name", "") }} + {% set title %}{% trans "Display" %}{% endset %} {% set attributes = [ { name: "data-width", value: "200px" }, @@ -164,7 +172,7 @@ {{ inline.dropdown("type", "single", title, "", options, "typeid", "type") }} {% set title %}{% trans "Tags from" %}{% endset %} - {% set dg %}{% trans "Display Group" %}{% endset %} + {% set dg %}{% trans "Display" %}{% endset %} {% set layout %}{% trans "Layout" %}{% endset %} {% set media %}{% trans "Media" %}{% endset %} {% set options = [ @@ -219,6 +227,10 @@ {% trans "Type" %} {% trans "Display ID" %} {% trans "Display" %} + {% trans "Display Group ID" %} + {% trans "Display Group" %} + {% trans "Tag ID" %} + {% trans "Tag Name" %} {% trans "Campaign" %} {% trans "Layout ID" %} {% trans "Layout" %} @@ -251,6 +263,10 @@ + + + + @@ -290,6 +306,21 @@ setTimeout(function() { $("#applyBtn").removeClass('disabled'); }, 300); + + // We hide empty columns and display appropriate columns (ie ID and name) + switch ($('select[name="groupBy"]').val()) { + case 'displayGroup': + $(this.api().columns([1, 2, 5, 6]).visible(false)); + $(this.api().columns([3, 4]).visible(true)); + break; + case 'tag': + $(this.api().columns([1,2, 3, 4]).visible(false)); + $(this.api().columns([5, 6]).visible(true)); + break; + default: + $(this.api().columns([3, 4, 5, 6]).visible(false)); + $(this.api().columns([1, 2]).visible(true)); + } }, filter: false, "order": [[1, "asc"]], @@ -298,6 +329,10 @@ {"data": "type"}, {"data": "displayId"}, {"data": "display"}, + {"data": "displayGroupId"}, + {"data": "displayGroup"}, + {"data": "tagId"}, + {"data": "tagName"}, {"data": "parentCampaign"}, {"data": "layoutId"}, {"data": "layout"}, @@ -341,28 +376,27 @@ {"data": "duration"}, {"data": "minStart"}, {"data": "maxEnd"} - ], footerCallback: function (row, data, start, end, display) { let api = this.api(); // Total over all pages - let totalNumberPlays = api.column(9).data().reduce(function (a, b) { + let totalNumberPlays = api.column(13).data().reduce(function (a, b) { return a + b; }, 0); - let totalDuration = api.column(11).data().reduce(function (a, b) { + let totalDuration = api.column(15).data().reduce(function (a, b) { return a + b; }, 0); - let totalNumberPlaysPage = api.column(9, { page: 'current'}).data().reduce(function (a, b) { + let totalNumberPlaysPage = api.column(13, { page: 'current'}).data().reduce(function (a, b) { return a + b; }, 0); - let totalDurationPage = api.column(11, { page: 'current'}).data().reduce(function (a, b) { + let totalDurationPage = api.column(13, { page: 'current'}).data().reduce(function (a, b) { return a + b; }, 0); // Update footer - $(api.column(9).footer()).html(totalNumberPlaysPage + ' (' + totalNumberPlays + ' total)'); - $(api.column(11).footer()).html(Math.floor(totalDurationPage) + ' (' + Math.floor(totalDuration) + ' total)'); + $(api.column(13).footer()).html(totalNumberPlaysPage + ' (' + totalNumberPlays + ' total)'); + $(api.column(15).footer()).html(Math.floor(totalDurationPage) + ' (' + Math.floor(totalDuration) + ' total)'); }, }); @@ -442,16 +476,38 @@ getData($dataTable.data().url); }); - // If we select a displayId we hide the display group filter + // If we select a displayId, we hide the display group filter $('#displayId').off('change').change( function() { - let displayId = $('#displayId').val(); + if (displayId) { $('select[name="displayGroupId[]"] option').remove(); $('select[name="displayGroupId[]"]').next(".select2-container").parent().hide(); + $('select[name="groupBy[]"] option').remove(); + $('select[name="groupBy"]').parent().hide(); } else { $('#displayId option').remove(); $('select[name="displayGroupId[]"]').next(".select2-container").parent().show(); + $('select[name="groupBy"]').parent().show(); + } + }); + + // If we select a groupBy data, we hide the display filter + $("select[name='groupBy']").on('change', function() { + let optionSelected = $(this).find("option:selected").val(); + + if (optionSelected === 'displayGroup') { + $('select[name="groupBy"]').parent().show(); + $('select[name="displayGroupId[]"]').next(".select2-container").parent().show(); + } else { + $('select[name="displayGroupId[]"] option').remove(); + $('select[name="displayGroupId[]"]').next(".select2-container").parent().hide(); + } + + if (optionSelected === 'display') { + $("select[name='displayId']").parent().show(); + } else { + $("select[name='displayId']").parent().hide(); } }); diff --git a/tests/XMDS.http b/tests/XMDS.http index a3366e99d1..77d41185b4 100644 --- a/tests/XMDS.http +++ b/tests/XMDS.http @@ -166,6 +166,25 @@ Content-Type: application/xml ### +POST {{url}}/xmds.php?v=7 +Content-Type: application/xml + + + + + {{serverKey}} + {{hardwareKey}} + <records><stat fromdt="2024-08-01 00:00:00" todt="2024-08-01 00:05:00" type="layout" scheduleid="48" layoutid="133" mediaid="null" tag="" count="250" /></records> + + + + +### + # Get the fileID from the Required Files response. GET {{url}}/xmds.php?file=12.xlf&displayId=1&type=L&itemId=12&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230502T000000Z&X-Amz-Expires=1683048895&X-Amz-SignedHeaders=host&X-Amz-Signature=7c876be170afb29d194e7b035be6969198c22a32c22e163e2696754bb1163f5d From 92d8b7862352bebd89b10026d268e2fc91a5dd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauro=20Ferr=C3=A3o?= Date: Fri, 27 Sep 2024 11:35:19 +0100 Subject: [PATCH 05/22] Editor Toolbox: Size popup buttons disappear on drag (#2744) relates to xibosignage/xibo#3492 --- ui/src/editor-core/toolbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/editor-core/toolbar.js b/ui/src/editor-core/toolbar.js index 1b5fa1be25..040603b90a 100644 --- a/ui/src/editor-core/toolbar.js +++ b/ui/src/editor-core/toolbar.js @@ -1199,7 +1199,7 @@ Toolbar.prototype.render = function({savePrefs = true} = {}) { }); this.DOMObject.find('.toolbar-level-control-select') - .on('click', (ev) => { + .on('click', '.toolbar-level-control', (ev) => { const newLevel = $(ev.target).data('level'); // Close menu From 34a8be1e3fbed675c9c40bb86bfd7b73c5d24313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauro=20Ferr=C3=A3o?= Date: Fri, 27 Sep 2024 11:36:31 +0100 Subject: [PATCH 06/22] Playlist Page: Top bar styled like the Editor topbar (#2743) relates to xibosignage/xibo#3494 --- ui/src/style/layout-editor.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/style/layout-editor.scss b/ui/src/style/layout-editor.scss index 76c1b826e7..ede299a61d 100644 --- a/ui/src/style/layout-editor.scss +++ b/ui/src/style/layout-editor.scss @@ -40,7 +40,7 @@ body.editor-opened { } // Change top bar to match layout editor -.row.header.header-side { +body.editor-opened .row.header.header-side { position: relative; background-color: $xibo-color-neutral-0; @include box-shadow(0px 2px 2px $xibo-color-shadow); @@ -123,7 +123,7 @@ body.editor-opened { } /* Page content wrapper */ -#content-wrapper .page-content>.row { +body.editor-opened #content-wrapper .page-content>.row { >div { margin: 0; } From 1f1ba0f699cbef696c1875c375fa5ff27df13233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauro=20Ferr=C3=A3o?= Date: Fri, 27 Sep 2024 15:27:34 +0100 Subject: [PATCH 07/22] Widget: weather stencil unable to copy asset (#2747) relates to xibosignage/xibo#3495 --- modules/templates/forecast-elements.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/templates/forecast-elements.xml b/modules/templates/forecast-elements.xml index 9bcddfa9f1..410ff0678c 100644 --- a/modules/templates/forecast-elements.xml +++ b/modules/templates/forecast-elements.xml @@ -1182,9 +1182,9 @@ $(target).find('.date').each(function(_idx, dateEl){ - - - + + +