v11: Umbraco Marketplace replaces packages repo (#13371)

* add lang keys for marketplace

* remove old 'repo' page and deprecate related services

* add new view for Umbraco Marketplace

* optimise margin/padding for other tabs

* mark Our Repository constants as obsolete

* improve css path to iframe slightly with more aliases and classnames

* remove style qs

* update URL of Marketplace

* add ng-controller with utitlities for future PostMessage API

* rename marketplace loaded function

* remove iframe postmessage logic for time being

* add handling of dynamic querystring params

* assume url does not change

* Added support for additional parameters for marketplace

* Update src/JsonSchema/AppSettings.cs

Fix styling issue

Co-authored-by: Ronald Barendse <ronald@barend.se>

* Update src/Umbraco.Core/Configuration/Models/MarketplaceSettings.cs

Fix styling issue

Co-authored-by: Ronald Barendse <ronald@barend.se>

* Update src/Umbraco.Core/Configuration/Models/MarketplaceSettings.cs

Make comment more descriptive

Co-authored-by: Ronald Barendse <ronald@barend.se>

* Update src/Umbraco.Core/Constants-Marketplace.cs

Fix styling issue

Co-authored-by: Ronald Barendse <ronald@barend.se>

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
Co-authored-by: Ronald Barendse <ronald@barend.se>
This commit is contained in:
Jacob Overgaard
2022-11-18 15:06:24 +01:00
committed by GitHub
parent d089825537
commit 4e98df799f
20 changed files with 279 additions and 689 deletions

View File

@@ -2,8 +2,9 @@
* @ngdoc service
* @name umbraco.resources.ourPackageRepositoryResource
* @description handles data for package installations
* @deprecated This resource is deprecated and will be removed in future versions. Umbraco no longer supports the Our Umbraco repository.
**/
function ourPackageRepositoryResource($q, $http, umbDataFormatter, umbRequestHelper) {
function ourPackageRepositoryResource($http, umbRequestHelper) {
var baseurl = Umbraco.Sys.ServerVariables.umbracoUrls.packagesRestApiBaseUrl;

View File

@@ -1,3 +1,41 @@
[data-element="editor-packages"] {
.umb-pane {
height: 100%;
margin: 0;
.umb-pane-content,
.umb-editor-sub-views {
height: 100%;
.umb-editor-sub-view {
padding: 20px;
}
.sub-view-Marketplace {
height: 100%;
margin: 0;
padding: 0;
.umb-editor-sub-view__content {
height: 100%;
}
}
}
}
}
.umb-marketplace-view-wrapper {
height: 100%;
display: flex;
align-items: stretch;
}
.umb-marketplace-view {
width: 100%;
height: 100%;
overflow: hidden;
}
.umb-packages-view-title {
font-size: 20px;
font-weight: bold;

View File

@@ -6,6 +6,6 @@
</div>
<div class="umb-pane">
<div ng-transclude></div>
<div class="umb-pane-content" ng-transclude></div>
</div>
</div>

View File

@@ -1,17 +1,12 @@
<div class="umb-editor-sub-views">
<div
id="sub-view-{{$index}}"
class="umb-editor-sub-view"
ng-repeat="subView in subViews track by subView.alias"
ng-class="'sub-view-' + subView.name"
val-sub-view="subView">
<div class="umb-editor-sub-view__content"
ng-show="subView.active === true"
ng-include="subView.view">
</div>
</div>
<div
id="sub-view-{{$index}}"
class="umb-editor-sub-view"
ng-repeat="subView in subViews track by subView.alias"
ng-class="'sub-view-' + subView.name"
val-sub-view="subView"
ng-if="subView.active"
>
<div class="umb-editor-sub-view__content" ng-include="subView.view"></div>
</div>
</div>

View File

@@ -1,99 +1,99 @@
(function () {
"use strict";
"use strict";
function PackagesOverviewController($scope, $location, $routeParams, localizationService, localStorageService) {
function PackagesOverviewController($location, $routeParams, localizationService, localStorageService) {
//Hack!
// if there is a local storage value for packageInstallData then we need to redirect there,
// the issue is that we still have webforms and we cannot go to a hash location and then window.reload
// because it will double load it.
// we will refresh and then navigate there.
//Hack!
// if there is a local storage value for packageInstallData then we need to redirect there,
// the issue is that we still have webforms and we cannot go to a hash location and then window.reload
// because it will double load it.
// we will refresh and then navigate there.
let packageInstallData = localStorageService.get("packageInstallData");
let packageUri = $routeParams.method;
let packageInstallData = localStorageService.get("packageInstallData");
let packageUri = $routeParams.method;
if (packageInstallData) {
localStorageService.remove("packageInstallData");
if (packageInstallData) {
localStorageService.remove("packageInstallData");
if (packageInstallData.postInstallationPath) {
//navigate to the custom installer screen if set
$location.path(packageInstallData.postInstallationPath).search("packageId", packageInstallData.id);
return;
}
if (packageInstallData.postInstallationPath) {
//navigate to the custom installer screen if set
$location.path(packageInstallData.postInstallationPath).search("packageId", packageInstallData.id);
return;
}
//if it is "installed" then set the uri/path to that
if (packageInstallData === "installed") {
packageUri = "installed";
}
}
var vm = this;
vm.page = {};
vm.page.labels = {};
vm.page.name = "";
vm.page.navigation = [];
onInit();
function onInit() {
loadNavigation();
setPageName();
}
function loadNavigation() {
var labels = ["sections_packages", "packager_installed", "packager_installLocal", "packager_created"];
localizationService.localizeMany(labels).then(function (data) {
vm.page.labels.packages = data[0];
vm.page.labels.installed = data[1];
vm.page.labels.install = data[2];
vm.page.labels.created = data[3];
vm.page.navigation = [
{
"name": vm.page.labels.packages,
"icon": "icon-cloud",
"view": "views/packages/views/repo.html",
"active": !packageUri || packageUri === "repo",
"alias": "umbPackages",
"action": function () {
$location.path("/packages/packages/repo");
}
},
{
"name": vm.page.labels.installed,
"icon": "icon-box",
"view": "views/packages/views/installed.html",
"active": packageUri === "installed",
"alias": "umbInstalled",
"action": function () {
$location.path("/packages/packages/installed");
}
},
{
"name": vm.page.labels.created,
"icon": "icon-files",
"view": "views/packages/views/created.html",
"active": packageUri === "created",
"alias": "umbCreatedPackages",
"action": function () {
$location.path("/packages/packages/created");
}
}
];
});
}
function setPageName() {
localizationService.localize("sections_packages").then(function (data) {
vm.page.name = data;
})
}
//if it is "installed" then set the uri/path to that
if (packageInstallData === "installed") {
packageUri = "installed";
}
}
angular.module("umbraco").controller("Umbraco.Editors.Packages.OverviewController", PackagesOverviewController);
var vm = this;
vm.page = {};
vm.page.labels = {};
vm.page.name = "";
vm.page.navigation = [];
onInit();
function onInit() {
loadNavigation();
setPageName();
}
function loadNavigation() {
var labels = ["sections_marketplace", "packager_installed", "packager_installLocal", "packager_created"];
localizationService.localizeMany(labels).then(function (data) {
vm.page.labels.marketplace = data[0];
vm.page.labels.installed = data[1];
vm.page.labels.install = data[2];
vm.page.labels.created = data[3];
vm.page.navigation = [
{
"name": vm.page.labels.marketplace,
"icon": "icon-cloud",
"view": "views/packages/views/marketplace.html",
"active": !packageUri || packageUri === "repo",
"alias": "umbMarketplace",
"action": function () {
$location.path("/packages/packages/repo");
}
},
{
"name": vm.page.labels.installed,
"icon": "icon-box",
"view": "views/packages/views/installed.html",
"active": packageUri === "installed",
"alias": "umbInstalled",
"action": function () {
$location.path("/packages/packages/installed");
}
},
{
"name": vm.page.labels.created,
"icon": "icon-files",
"view": "views/packages/views/created.html",
"active": packageUri === "created",
"alias": "umbCreatedPackages",
"action": function () {
$location.path("/packages/packages/created");
}
}
];
});
}
function setPageName() {
localizationService.localize("sections_marketplace").then(function (data) {
vm.page.name = data;
})
}
}
angular.module("umbraco").controller("Umbraco.Editors.Packages.OverviewController", PackagesOverviewController);
})();

View File

@@ -0,0 +1,18 @@
(function () {
"use strict";
function MarketplaceController($sce) {
var vm = this;
var marketplaceUrl = new URL(Umbraco.Sys.ServerVariables.umbracoUrls.marketplaceUrl);
function init() {
vm.marketplaceUrl = $sce.trustAsResourceUrl(marketplaceUrl.toString());
}
init();
}
angular.module("umbraco").controller("Umbraco.Editors.Packages.MarketplaceController", MarketplaceController);
})();

View File

@@ -0,0 +1,13 @@
<div
class="umb-marketplace-view-wrapper clearfix"
ng-controller="Umbraco.Editors.Packages.MarketplaceController as vm"
>
<iframe
ng-if="::vm.marketplaceUrl"
ng-src="{{ ::vm.marketplaceUrl }}"
class="umb-marketplace-view"
title="Umbraco Marketplace"
allowfullscreen
allow="geolocation; autoplay; clipboard-write; encrypted-media"
></iframe>
</div>

View File

@@ -1,265 +0,0 @@
(function () {
"use strict";
function PackagesRepoController($scope, $timeout, ourPackageRepositoryResource, $q, localizationService, notificationsService) {
var vm = this;
vm.packageViewState = "packageList";
vm.categories = [];
vm.loading = true;
vm.pagination = {
pageNumber: 1,
totalPages: 10,
pageSize: 24
};
vm.searchQuery = "";
vm.selectCategory = selectCategory;
vm.showPackageDetails = showPackageDetails;
vm.setPackageViewState = setPackageViewState;
vm.nextPage = nextPage;
vm.prevPage = prevPage;
vm.goToPage = goToPage;
vm.openLightbox = openLightbox;
vm.closeLightbox = closeLightbox;
vm.search = search;
vm.installCompleted = false;
vm.highlightedPackageCollections = [];
vm.labels = {};
var defaultSort = "Latest";
var currSort = defaultSort;
//used to cancel any request in progress if another one needs to take it's place
var canceler = null;
function getActiveCategory() {
if (vm.searchQuery !== "") {
return "";
}
for (var i = 0; i < vm.categories.length; i++) {
if (vm.categories[i].active === true) {
return vm.categories[i].name;
}
}
return "";
}
function init() {
vm.loading = true;
localizationService.localizeMany(["packager_packagesPopular", "packager_packagesPromoted"])
.then(function (labels) {
vm.labels.popularPackages = labels[0];
vm.labels.promotedPackages = labels[1];
var popularPackages, promotedPackages;
$q.all([
ourPackageRepositoryResource.getCategories()
.then(function (cats) {
vm.categories = cats.filter(function (cat) {
return cat.name !== "Umbraco Pro";
});
}),
ourPackageRepositoryResource.getPopular(10)
.then(function (pack) {
popularPackages = { title: vm.labels.popularPackages, packages: pack.packages };
}),
ourPackageRepositoryResource.getPromoted(20)
.then(function (pack) {
promotedPackages = { title: vm.labels.promotedPackages, packages: pack.packages };
}),
ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort)
.then(function (pack) {
vm.packages = pack.packages;
vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize);
})
])
.then(function () {
vm.highlightedPackageCollections = [popularPackages, promotedPackages];
vm.loading = false;
});
});
}
function selectCategory(selectedCategory, categories) {
for (var i = 0; i < categories.length; i++) {
var category = categories[i];
if (category.name === selectedCategory.name) {
//it's already selected, let's unselect to show all again
if (category.active === true) {
category.active = false;
}
else {
category.active = true;
}
}
else {
category.active = false;
}
}
vm.loading = true;
vm.searchQuery = "";
var reset = selectedCategory.active === false;
var searchCategory = reset ? "" : selectedCategory.name;
currSort = defaultSort;
var popularPackages, promotedPackages;
$q.all([
ourPackageRepositoryResource.getPopular(10, searchCategory)
.then(function (pack) {
popularPackages = { title: vm.labels.popularPackages, packages: pack.packages };
}),
ourPackageRepositoryResource.getPromoted(20, searchCategory)
.then(function (pack) {
promotedPackages = { title: vm.labels.promotedPackages, packages: pack.packages };
}),
ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, searchCategory, vm.searchQuery)
.then(function (pack) {
vm.packages = pack.packages;
vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize);
vm.pagination.pageNumber = 1;
})
])
.then(function () {
vm.highlightedPackageCollections = [popularPackages, promotedPackages];
vm.loading = false;
});
}
function showPackageDetails(selectedPackage) {
ourPackageRepositoryResource.getDetails(selectedPackage.id)
.then(function (pack) {
vm.package = pack;
vm.packageViewState = "packageDetails";
});
}
function setPackageViewState(state) {
if (state) {
vm.packageViewState = state;
}
}
function nextPage(pageNumber) {
ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery)
.then(function (pack) {
vm.packages = pack.packages;
vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize);
});
}
function prevPage(pageNumber) {
ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery)
.then(function (pack) {
vm.packages = pack.packages;
vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize);
});
}
function goToPage(pageNumber) {
ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery)
.then(function (pack) {
vm.packages = pack.packages;
vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize);
});
}
var previousElement = null;
function openLightbox(itemIndex, items) {
previousElement = ( document.activeElement || document.body );
vm.lightbox = {
show: true,
items: items,
activeIndex: itemIndex,
focus: true
};
}
function closeLightbox() {
vm.lightbox.show = false;
vm.lightbox = null;
if(previousElement){
setTimeout(function(){
previousElement.focus();
previousElement = null;
}, 100)
}
document.activeElement.blur();
}
var searchDebounced = _.debounce(function (e) {
//a canceler exists, so perform the cancelation operation and reset
if (canceler) {
canceler.resolve();
}
canceler = $q.defer();
$scope.$apply(function () {
currSort = vm.searchQuery ? "Default" : "Latest";
ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1,
vm.pagination.pageSize,
currSort,
"",
vm.searchQuery,
canceler.promise)
.then(function (pack) {
vm.packages = pack.packages;
vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize);
vm.pagination.pageNumber = 1;
vm.loading = false;
//set back to null so it can be re-created
canceler = null;
})
.catch(function (err) {
canceler = null;
if (err) {
// If an abort happened, ignore it since it happened because of a new search
if (err.xhrStatus === 'abort') {
return;
}
// Otherwise, show the error
if (err.errorMsg) {
notificationsService.error(err.errorMsg);
return;
}
}
console.error(err);
});
});
}, 200);
function search(searchQuery) {
vm.loading = true;
searchDebounced();
}
vm.reloadPage = function () {
//reload on next digest (after cookie)
$timeout(function () {
window.location.reload(true);
});
}
init();
}
angular.module("umbraco").controller("Umbraco.Editors.Packages.RepoController", PackagesRepoController);
})();

View File

@@ -1,318 +0,0 @@
<div ng-controller="Umbraco.Editors.Packages.RepoController as vm" class="clearfix">
<umb-load-indicator ng-show="vm.loading"></umb-load-indicator>
<!-- LIST -->
<div class="umb-packages-view-wrapper" ng-if="vm.packageViewState === 'packageList'">
<div class="umb-packages-section">
<div class="umb-packages-search">
<label for="package-query" class="sr-only">
<localize key="packager_packageSearch">Search for packages</localize>
</label>
<input type="text"
class="-full-width-input"
name="query"
id="package-query"
localize="placeholder"
placeholder="@packager_packageSearch"
ng-model="vm.searchQuery"
ng-on-input="vm.search()"
no-dirty-check />
</div>
</div>
<div class="umb-packages-section" ng-if="vm.searchQuery == ''">
<div class="umb-packages-categories">
<button type="button"
class="umb-packages-category"
ng-click="vm.selectCategory(category, vm.categories)"
ng-repeat="category in vm.categories"
ng-class="{'-active': category.active, '-first': $first, '-last': $last}">
{{category.name}}
</button>
</div>
</div>
<div ng-show="vm.loading === false">
<div ng-repeat="highlightedPackageCollection in vm.highlightedPackageCollections">
<div class="umb-packages-section" ng-if="vm.searchQuery == '' && highlightedPackageCollection.packages.length > 0">
<h4><strong>{{highlightedPackageCollection.title}}</strong></h4>
<div class="umb-packages">
<div class="umb-package" ng-repeat="package in highlightedPackageCollection.packages">
<button type="button" class="umb-package-link" ng-click="vm.showPackageDetails(package)">
<div class="flex flex-column">
<div class="umb-package-icon">
<img ng-src="{{package.icon}}" alt="" />
</div>
<div class="umb-package-info">
<div class="umb-package-name">{{package.name}}</div>
<div class="umb-package-description">{{package.excerpt | limitTo: 40}}<span ng-if="package.excerpt > (package.excerpt | limitTo: 40)">...</span></div>
<div class="umb-package-numbers">
<small class="umb-package-downloads">
<umb-icon icon="icon-download-alt"></umb-icon>
<strong>{{package.downloads}}</strong>
</small>
<small class="umb-package-likes">
<umb-icon icon="icon-hearts" class="icon-hearts"></umb-icon>
<strong>{{package.likes}}</strong>
</small>
</div>
<div class="umb-package-cloud">
<div ng-if="package.certifiedToWorkOnUmbracoCloud">
<umb-icon icon="icon-cloud" class="icon-cloud"></umb-icon>
<span><localize key="packager_verifiedToWorkOnUmbracoCloud">Verified to work on Umbraco Cloud</localize></span>
</div>
</div>
</div>
</div>
</button>
</div> <!-- end package -->
</div> <!-- end packages -->
</div>
</div>
<div class="umb-packages-section" ng-if="vm.packages.length > 0">
<h4 ng-if="vm.searchQuery === ''"><strong><localize key="packager_packagesNew">New Releases</localize></strong></h4>
<h4 ng-if="vm.searchQuery !== ''"><strong><localize key="packager_packageSearchResults">Results for</localize> '{{ vm.searchQuery }}'</strong></h4>
<div class="umb-packages">
<div class="umb-package" ng-repeat="package in vm.packages">
<button type="button" class="umb-package-link" ng-click="vm.showPackageDetails(package)">
<div class="flex flex-column">
<div class="umb-package-icon">
<img ng-src="{{package.icon}}" alt="" />
</div>
<div class="umb-package-info">
<div class="umb-package-name">{{ package.name }}</div>
<div class="umb-package-description">{{ package.excerpt | limitTo: 40 }}<span ng-if="package.excerpt > (package.excerpt | limitTo: 40)">...</span></div>
<div class="umb-package-numbers">
<small class="umb-package-downloads">
<umb-icon icon="icon-download-alt"></umb-icon>
<strong>{{package.downloads}}</strong>
</small>
<small class="umb-package-likes">
<umb-icon icon="icon-hearts" class="icon-hearts"></umb-icon>
<strong>{{package.likes}}</strong>
</small>
</div>
<div class="umb-package-cloud">
<div ng-if="package.certifiedToWorkOnUmbracoCloud">
<umb-icon icon="icon-cloud" class="icon-cloud"></umb-icon>
<span><localize key="packager_verifiedToWorkOnUmbracoCloud">Verified to work on Umbraco Cloud</localize></span>
</div>
</div>
</div>
</div>
</button>
</div> <!-- end package -->
</div> <!-- end packages -->
</div>
<div class="umb-packages__pagination" ng-if="vm.pagination.totalPages > 1 && vm.loading === false">
<umb-pagination page-number="vm.pagination.pageNumber"
total-pages="vm.pagination.totalPages"
on-next="vm.nextPage"
on-prev="vm.prevPage"
on-go-to-page="vm.goToPage">
</umb-pagination>
</div>
<!-- Empty state -->
<umb-empty-state ng-if="vm.packages.length === 0 && vm.loading === false && vm.searchQuery !== ''"
position="center">
<h4><strong><localize key="packager_packageNoResults">We couldn't find anything for</localize> '{{ vm.searchQuery }}'</strong></h4>
<p class="faded"><localize key="packager_packageNoResultsDescription">Please try searching for another package or browse through the categories</localize>.</p>
</umb-empty-state>
<umb-empty-state ng-if="vm.popular.length === 0 && vm.loading === false && vm.searchQuery === ''"
position="center">
<h4><strong><localize key="general_searchNoResult">Sorry, we can not find what you are looking for.</localize></strong></h4>
<p class="faded"><localize key="packager_packageNoResultsDescription">Please try searching for another package or browse through the categories</localize>.</p>
</umb-empty-state>
</div>
</div>
<!-- DETAILS -->
<div ng-if="vm.packageViewState === 'packageDetails' && vm.loading === false">
<umb-editor-sub-header>
<umb-editor-sub-header-content-left>
<button type="button" class="umb-package-details__back-action" ng-click="vm.setPackageViewState('packageList');" prevent-default><span aria-hidden="true">&larr;</span> <localize key="general_back">Back</localize></button>
</umb-editor-sub-header-content-left>
</umb-editor-sub-header>
<div class="umb-packages-view-wrapper">
<div class="umb-package-details">
<div class="umb-package-details__main-content">
<umb-box>
<umb-box-content>
<div class="umb-packages-view-title">{{ vm.package.name }}</div>
<div class="umb-package-details__description" ng-bind-html="vm.package.description"></div>
<div class="umb-gallery">
<div class="umb-gallery__thumbnails">
<button type="button" class="umb-gallery__thumbnail" ng-repeat="image in vm.package.images" ng-click="vm.openLightbox($index, vm.package.images)">
<img ng-src="{{ image.thumbnail }}" />
</button>
</div>
</div>
</umb-box-content>
</umb-box>
<umb-lightbox
ng-if="vm.lightbox.show"
items="vm.lightbox.items"
active-item-index="vm.lightbox.activeIndex"
on-close="vm.closeLightbox">
</umb-lightbox>
</div>
<div class="umb-package-details__sidebar">
<umb-box>
<umb-box-content>
<div class="umb-package-details__section-title"><localize key="packager_installInstructions">Install Instructions</localize></div>
<div class="umb-package-details__install-instructions">
dotnet add package <span>{{vm.package.nuGetPackageId}}</span>
</div>
</umb-box-content>
</umb-box>
<umb-box>
<umb-box-content>
<div class="umb-package-details__owner-profile">
<div class="umb-package-details__owner-profile-avatar">
<umb-avatar
size="m"
img-src="{{ 'https://our.umbraco.com' + vm.package.ownerInfo.ownerAvatar }}">
</umb-avatar>
</div>
<div>
<div class="umb-package-details__owner-profile-name">{{ vm.package.ownerInfo.owner }}</div>
<div class="umb-package-details__owner-profile-karma">
{{ vm.package.ownerInfo.owner }} <localize key="packager_packageHas">has</localize> <strong>{{ vm.package.ownerInfo.karma }}</strong> <localize key="packager_packageKarmaPoints">karma points</localize>
</div>
</div>
</div>
</umb-box-content>
</umb-box>
<umb-box>
<umb-box-content>
<div class="umb-package-details__section-title"><localize key="packager_packageInfo">Information</localize></div>
<div>
<div class="umb-package-details__information-item" ng-if="vm.package.ownerInfo.owner">
<div class="umb-package-details__information-item-label"><localize key="packager_packageOwner">Owner</localize>:</div>
<div class="umb-package-details__information-item-content">{{vm.package.ownerInfo.owner}}</div>
</div>
<div class="umb-package-details__information-item" ng-if="vm.package.ownerInfo.contributors">
<div class="umb-package-details__information-item-label"><localize key="packager_packageContrib">Contributors</localize>:</div>
<div class="umb-package-details__information-item-content">
<span ng-repeat="contributor in vm.package.ownerInfo.contributors">{{ contributor }}<span ng-if="!$last">,&nbsp;</span></span>
</div>
</div>
<div class="umb-package-details__information-item" ng-if="vm.package.created">
<div class="umb-package-details__information-item-label"><localize key="packager_packageCreated">Created</localize>:</div>
<div class="umb-package-details__information-item-content">{{vm.package.created | date:'yyyy-MM-dd HH:mm:ss'}}</div>
</div>
<div class="umb-package-details__information-item" ng-if="vm.package.latestVersion">
<div class="umb-package-details__information-item-label"><localize key="packager_packageCurrentVersion">Current version</localize>:</div>
<div class="umb-package-details__information-item-content">{{vm.package.latestVersion}}</div>
</div>
<div class="umb-package-details__information-item" ng-if="vm.package.information.netVersion">
<div class="umb-package-details__information-item-label"><localize key="packager_packageNetVersion">.NET Version</localize>:</div>
<div class="umb-package-details__information-item-content">{{vm.package.information.netVersion}}</div>
</div>
<div class="umb-package-details__information-item" ng-if="vm.package.licenseName">
<div class="umb-package-details__information-item-label"><localize key="packager_packageLicense">License</localize>:</div>
<div class="umb-package-details__information-item-content">{{vm.package.licenseName}}</div>
</div>
<div class="umb-package-details__information-item" ng-if="vm.package.downloads">
<div class="umb-package-details__information-item-label"><localize key="packager_packageDownloads">Downloads</localize>:</div>
<div class="umb-package-details__information-item-content">{{vm.package.downloads}}</div>
</div>
<div class="umb-package-details__information-item" ng-if="vm.package.ownerInfo.karma">
<div class="umb-package-details__information-item-label"><localize key="packager_packageLikes">Likes</localize>:</div>
<div class="umb-package-details__information-item-content">{{vm.package.likes}}</div>
</div>
<div class="umb-package-details__information-item" ng-if="vm.package.certifiedToWorkOnUmbracoCloud">
<div class="umb-package-details__information-item-label"><localize key="packager_verifiedToWorkOnUmbracoCloud">Verified to work on Umbraco CLoud</localize></div>
</div>
</div>
</umb-box-content>
</umb-box>
<umb-box>
<umb-box-content>
<div class="umb-package-details__section-title"><localize key="packager_packageCompatibility">Compatibility</localize></div>
<div class="umb-package-details__section-description"><localize key="packager_packageCompatibilityDescription">This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100%</localize></div>
<div class="umb-package-details__compatability" ng-repeat="compatibility in vm.package.compatibility | filter:percentage > 0">
<div class="umb-package-details__compatability-label">
<span class="umb-package-details__information-item-label">{{compatibility.version}}</span>
<span class="umb-package-details__information-item-label-2">({{compatibility.percentage}}%)</span>
</div>
<umb-progress-bar
percentage="{{compatibility.percentage}}">
</umb-progress-bar>
</div>
</umb-box-content>
</umb-box>
<umb-box ng-if="vm.package.externalSources">
<umb-box-content>
<div class="umb-package-details__section-title"><localize key="packager_packageExternalSources">External sources</localize></div>
<div>
<div class="umb-package-details__information-item" ng-repeat="externalSource in vm.package.externalSources">
<a class="umb-package-details__link" href="{{externalSource.url}}" target="_blank" rel="noopener noreferrer">
<umb-icon icon="icon-out"></umb-icon>
{{externalSource.name}}
</a>
</div>
</div>
</umb-box-content>
</umb-box>
</div>
</div>
</div>
</div>
</div>