[NEW] Internal marketplace for apps (#11864)

* Update page details

* Adds icon to marketplace install button

* Add icon and animation for installing apps

* Fixes animation svg for app installation

* Changes to actions

* Fixing conflicts

* Adding the right return to apps()

* Fetching apps according tab context ('marketplace' or 'installed')

* Set loading on tables

* Adding POST to install the app on click

* Fixing linting

* Tagging already installed apps with prop '_installed' to show the 'v' icon

* Adding helper to render the 'v' icon when the app is already downloaded

* Commenting _appOnAppAdded method for now

* Fixing arrow functions returning objects

* Adding isInstalled helper to appManage to hide 'install' button on details page

* Linting

* Fixing event propagation on table, adding uninstall button

* Removing unnecessary comments

* Fix missing image for isntalled apps

* Add translation for Apps_Details

* Improvements on app details screen

* Show categories for installed apps

* Allow edit app settings

* Improve app settings layout

* Allow update apps

* Small layout fixes for marketplace

* some css fixes

* Adding marketplaceApiVersion to rockethat.info

* Replacing hardcoded colors for vars and removing \!important

* Removing unused class, removing \!important

* Fixing page scroll

* codacy
pull/11879/head^2
Rodrigo Nascimento 7 years ago committed by Diego Sampaio
parent 9317946c4c
commit c15d3ca8bd
  1. 50
      package-lock.json
  2. 158
      packages/rocketchat-apps/assets/stylesheets/apps.css
  3. 793
      packages/rocketchat-apps/client/admin/appManage.html
  4. 140
      packages/rocketchat-apps/client/admin/appManage.js
  5. 38
      packages/rocketchat-apps/client/admin/apps.html
  6. 155
      packages/rocketchat-apps/client/admin/apps.js
  7. 2
      packages/rocketchat-apps/client/orchestrator.js
  8. 9
      packages/rocketchat-i18n/i18n/en.i18n.json
  9. 3
      packages/rocketchat-lib/rocketchat.info
  10. 2
      packages/rocketchat-theme/client/imports/components/tabs.css
  11. 60
      packages/rocketchat-theme/client/imports/general/apps.css
  12. 2
      packages/rocketchat-ui-master/client/main.html
  13. 1323
      packages/rocketchat-ui-master/public/icons.svg
  14. 3
      packages/rocketchat-ui/client/components/tabs.js

50
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "Rocket.Chat",
"version": "0.69.0-develop",
"version": "0.67.0-develop",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -139,12 +139,12 @@
"@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
"integrity": "sha1-TIVzDlm5ofHzSQR9vyQpYDS7JzU="
},
"@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
"integrity": "sha1-fvN/DQEPsCitGtWXIuUG2SYoFcs="
},
"@protobufjs/eventemitter": {
"version": "1.1.0",
@ -274,7 +274,7 @@
"@types/long": {
"version": "3.0.32",
"resolved": "https://registry.npmjs.org/@types/long/-/long-3.0.32.tgz",
"integrity": "sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA=="
"integrity": "sha1-9OWvMenpsZbY5fyopeLiCqPWC2k="
},
"@types/node": {
"version": "8.10.18",
@ -4298,7 +4298,7 @@
"diff": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz",
"integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==",
"integrity": "sha1-qoVnpu7QPFMfyJ0/cRzQ5SWd7HU=",
"dev": true
},
"diff-match-patch": {
@ -4318,7 +4318,7 @@
"doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"integrity": "sha1-XNAfwQFiG0LEzX9dGmYkNxbT850=",
"dev": true,
"requires": {
"esutils": "2.0.2"
@ -4734,7 +4734,7 @@
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=",
"dev": true,
"requires": {
"ms": "2.0.0"
@ -4785,7 +4785,7 @@
"eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
"integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
"integrity": "sha1-PzGA+y4pEBdxastMnW1bXDSmqB0=",
"dev": true
},
"espree": {
@ -5475,7 +5475,7 @@
"fs-minipass": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"integrity": "sha1-BsJ3IYRU7CiN93raVKA7hwKqy50=",
"requires": {
"minipass": "2.3.3"
}
@ -6497,7 +6497,7 @@
"growl": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
"integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
"integrity": "sha1-GSa6kM8+3+KttJJ/WIC8IsZseQ8=",
"dev": true
},
"grpc": {
@ -7498,7 +7498,7 @@
"domutils": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
"integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
"integrity": "sha1-Vuo0HoNOBuZ0ivehyyXaZ+qfjCo=",
"requires": {
"dom-serializer": "0.1.0",
"domelementtype": "1.3.0"
@ -9286,7 +9286,7 @@
"minizlib": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz",
"integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==",
"integrity": "sha1-EeE2WM5GvDpwomeqxYNZ0eDCnOs=",
"requires": {
"minipass": "2.3.3"
}
@ -9394,7 +9394,7 @@
"modelo": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.3.tgz",
"integrity": "sha512-9DITV2YEMcw7XojdfvGl3gDD8J9QjZTJ7ZOUuSAkP+F3T6rDbzMJuPktxptsdHYEvZcmXrCD3LMOhdSAEq6zKA=="
"integrity": "sha1-snhYik24f8HlEHrjonfAh2842JQ="
},
"modify-values": {
"version": "1.0.1",
@ -10256,7 +10256,7 @@
"pluralize": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz",
"integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
"integrity": "sha1-KYuJ34uTsCIdv0Ia0rGx6iP8Z3c=",
"dev": true
},
"pop-iterate": {
@ -10951,7 +10951,7 @@
"postcss-sorting": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-3.1.0.tgz",
"integrity": "sha512-YCPTcJwGIInF1LpMD1lIYvMHTGUL4s97o/OraA6eKvoauhhk6vjwOWDDjm6uRKqug/kyDPMKEzmYZ6FtW6RDgw==",
"integrity": "sha1-r3yQ7nOtElaaV2ZOrwZzXC4lvsA=",
"dev": true,
"requires": {
"lodash": "4.17.10",
@ -10998,7 +10998,7 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
"dev": true
},
"supports-color": {
@ -11211,7 +11211,7 @@
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=",
"requires": {
"asap": "2.0.6"
}
@ -11271,7 +11271,7 @@
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"integrity": "sha1-Ejma3W5M91Jtlzy8i1zi4pCLOQk=",
"requires": {
"end-of-stream": "1.4.1",
"once": "1.4.0"
@ -11558,7 +11558,7 @@
"redis": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
"integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
"integrity": "sha1-ICKI4/WMSfYHnZevehDhMDrhSwI=",
"requires": {
"double-ended-queue": "2.1.0-0",
"redis-commands": "1.3.5",
@ -12501,7 +12501,7 @@
"split": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
"integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
"integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=",
"dev": true,
"requires": {
"through": "2.3.8"
@ -12527,7 +12527,7 @@
"split2": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz",
"integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==",
"integrity": "sha1-GGsldbz4PoW30YRldWI47k7kJJM=",
"dev": true,
"requires": {
"through2": "2.0.3"
@ -12703,7 +12703,7 @@
"string-format-obj": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.1.tgz",
"integrity": "sha512-Mm+sROy+pHJmx0P/0Bs1uxIX6UhGJGj6xDGQZ5zh9v/SZRmLGevp+p0VJxV7lirrkAmQ2mvva/gHKpnF/pTb+Q=="
"integrity": "sha1-x2EspOKtkjgSqB2xktwpGFCqH2U="
},
"string-width": {
"version": "1.0.2",
@ -13426,7 +13426,7 @@
"text-extensions": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.7.0.tgz",
"integrity": "sha512-AKXZeDq230UaSzaO5s3qQUZOaC7iKbzq0jOFL614R7d9R593HLqAOL0cYoqLdkNrjBSOdmoQI06yigq1TSBXAg==",
"integrity": "sha1-+qq6JiXtdG1WiiPk0KrNm/CKizk=",
"dev": true
},
"text-table": {
@ -14443,7 +14443,7 @@
"write-file-atomic": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
"integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==",
"integrity": "sha1-H/YVdcLipOjlENb6TiQ8zhg5mas=",
"requires": {
"graceful-fs": "4.1.11",
"imurmurhash": "0.1.4",
@ -14751,7 +14751,7 @@
"xpath.js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz",
"integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ=="
"integrity": "sha1-OBakTtS7NSCRCD0AKjg91RBKX/E="
},
"xtend": {
"version": "4.0.1",

@ -1,28 +1,139 @@
.rc-apps-marketplace {
.app-author-name {
width: auto !important;
display: inline-block !important;
overflow: auto;
font-size: 14px;
&.page-settings .rc-apps-container {
a {
color: var(--rc-color-button-primary);
font-weight: 500;
}
}
h1 {
margin-bottom: 0;
letter-spacing: 0;
text-transform: initial;
color: #54585e;
font-size: 22px;
font-weight: normal;
line-height: 28px;
}
h2 {
margin-top: 10px;
margin-bottom: 0;
letter-spacing: 0;
text-transform: initial;
color: var(--color-dark);
font-size: 16px;
font-weight: 500;
line-height: 24px;
}
h3 {
margin-bottom: 0;
text-align: left;
letter-spacing: 0;
text-transform: initial;
color: var(--color-gray);
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
.rc-apps-details {
margin-bottom: 0;
padding: 0;
&__description {
padding-bottom: 50px;
border-bottom: 1.5px solid #efefef;
}
&__photo {
width: 96px;
height: 96px;
margin-right: 21px;
background-color: #f7f7f7;
}
&__content {
padding: 0;
}
&__col {
display: inline-block;
margin-right: 8px;
}
}
.rc-apps-container {
padding-bottom: 15px;
}
.rc-apps-container__header {
padding-top: 10px;
border-bottom: 1.5px solid #efefef;
}
/*
.js-install {
margin-top: 6px;
} */
.rc-header .rc-button {
margin: 0;
min-height: 0;
margin: 0;
}
.rc-apps-category {
margin-right: 8px;
padding: 8px;
text-align: left;
letter-spacing: -0.17px;
text-transform: uppercase;
color: #9da2a9;
border-radius: 2px;
background: #f3f4f5;
font-size: 12px;
font-weight: 500;
}
.app-enable-loading .loading-animation {
justify-content: left;
margin-left: 50px;
justify-content: left;
}
.apps-error {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: calc(100% - 60px);
padding: 25px 40px;
width: 100%;
font-size: 45px;
align-items: center;
justify-content: center;
}
.rc-table-avatar {
@ -35,7 +146,34 @@
margin: 0 7px;
}
.rc-table-content {
padding: 0;
.installer {
position: relative;
overflow: hidden;
width: 20px;
height: 20px;
&--marketplace-installer {
position: absolute;
top: 0;
width: 818px;
height: 20px;
&.play {
animation: play90 steps(40) 4s forwards;
}
}
}
}
@keyframes play90 {
0% {
right: -798px;
}
100% {
right: 2px;
}
}

@ -1,409 +1,446 @@
<!-- This page allows for settings to be changed but not all of them are implemented yet -->
<template name="appManage">
{{#with app}}
{{# header sectionName='Manage_the_App' fixedHeight=true hideHelp=true fullpage=true}}
<div class="rc-header__block rc-header__block-action">
<div class="rc-switch rc-switch--blue">
<label class="rc-switch__label">
<input type="checkbox" class="rc-switch__input js-input-check" id="enabled" name="enabledToggle" checked="{{isEnabled}}">
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
<span class="rc-switch__text">
{{_ "Activate"}}
</span>
</label>
</div>
</div>
{{/header}}
<section class="page-settings page-settings--new flex-tab-main-content">
{{#requiresPermission 'manage-apps'}}
{{#if isReady}}
<div class="rc-apps-details">
<div class="rc-apps-container">
<div class="rc-apps-details__photo" style="background-image:url({{iconFileContent}})"></div>
<div class="rc-apps-details__content">
<div class="rc-apps-details__row">
<div class="rc-apps-details__name">{{name}}</div>
<div class="rc-apps-details__version">v{{version}}</div>
</div>
<div class="rc-apps-details__row">{{description}}</div>
<div class="rc-apps-details__row">
{{#if author.name}}
<div class="rc-apps-details__item">
{{> icon icon='user'}}
{{author.name}}
</div>
{{/if}}
{{#if author.homepage}}
<a href="{{author.homepage}}" target="_blank" class="rc-apps-details__item">
{{> icon icon='permalink'}}
{{_ "App_author_homepage"}}
</a>
{{/if}}
{{#if author.support}}
<a href="{{author.support}}" target="_blank" class="rc-apps-details__item">
{{> icon icon='at'}}
{{_ "App_support_url"}}
</a>
{{/if}}
</div>
</div>
</div>
</div>
<div class="rc-apps-container rc-apps-settings">
{{#each settings}}
<div class="rc-apps-settings__item">
{{#if $eq type 'string'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="text" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
{{/if}}
</div>
</label>
{{#if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{#if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'boolean'}}
<div class="rc-switch rc-switch--blue">
<label class="rc-switch__label" tabindex="-1" for="{{id}}">
<input type="checkbox" class="rc-switch__input" name="{{id}}" id="{{id}}" checked="{{$eq value true}}">
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
<span class="rc-switch__text">
{{_ i18nLabel}}
</span>
</label>
<span class="rc-switch__description">{{{parseDescription i18nDescription}}}</span>
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
<section class="page-container page-home page-static page-settings rc-apps-marketplace">
{{# header sectionName='App_Details' fixedHeight=true hideHelp=true fullpage=true}}
<div class="rc-header__block rc-header__block-action">
<button class="rc-button rc-button--nude js-cancel">{{> icon icon="cross"}}</button>
</div>
{{/header}}
<div class="content">
{{#requiresPermission 'manage-apps'}}
{{#if isReady}}
<div class="rc-apps-details">
<div class="rc-apps-container rc-apps-container__header">
{{#if iconFileData}}
<div class="rc-apps-details__photo" style="background-image:url(data:image/png;base64,{{iconFileData}})"></div>
{{else}}
<div class="rc-apps-details__photo" style="background-image:url({{iconFileContent}})"></div>
{{/if}}
</div>
{{else if $eq type 'int'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="number" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'password'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="password" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'relativeUrl'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if $eq type 'relativeUrl'}}
<input class="rc-input__element" type="text" name="{{id}}" value="{{relativeUrl value}}" placeholder="{{_ i18nPlaceholder}}" {{isReadonly}}/>
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
<div class="rc-apps-details__content">
<div class="rc-apps-details__row">
<h1>{{name}}</h1>
</div>
{{#if author.name}}
<div class="rc-apps-details__version">
<strong class="rc-apps-details__author">by {{author.name}}</strong> | Version {{version}}
</div>
{{/if}}
</div>
{{else if $eq type 'font'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="text" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
<div class="rc-apps-details__row">
{{#if isInstalled}}
{{#if newVersion}}
<button class="rc-button rc-button--primary js-install">{{> icon icon="circled-arrow-down"}} {{_ "Update_to_version" version=newVersion }}</button>
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'code'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if isDisabled.disabled}}
{{> CodeMirror name=id options=(getEditorOptions true) code=(i18nDefaultValue) }}
<button class="rc-button rc-button--nude js-uninstall">{{> icon icon="trash"}} {{_ "Delete" }}</button>
{{#if isEnabled}}
<button class="rc-button rc-button--nude js-deactivate">{{> icon icon="ban"}} {{_ "Deactivate" }}</button>
{{else}}
<div class="code-mirror-box" data-editor-id="{{id}}">
<div class="title">
{{label}}
</div>
{{> CodeMirror name=id options=getEditorOptions code=value }}
<div class="buttons">
<button class="button primary button-fullscreen">Full Screen</button>
<button class="button primary button-restore">Exit Full Screen</button>
</div>
</div>
<button class="rc-button rc-button--nude js-activate">{{> icon icon="check"}} {{_ "Activate" }}</button>
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
<button class="rc-button rc-button--nude js-view-logs">{{> icon icon="list-alt"}} {{_ "View_Logs" }}</button>
{{else}}
<button class="rc-button rc-button--primary js-install">{{> icon icon="circled-arrow-down"}} {{_ "Install"}}</button>
{{/if}}
</div>
{{/if}}
</div>
{{else if $eq type 'select'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-select">
<select class="rc-select__element" name="{{id}}">
{{#each values}}
<option class="rc-select__option" value="{{key}}" selected="{{selectedOption ../id key}}">{{_ i18nLabel}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
</div>
<div class="rc-apps-container">
<div class="rc-apps-details__content">
{{#if categories}}
<div class="rc-apps-details__row rc-apps-details__block">
<h2> {{_ "Categories"}} </h2>
<div class="rc-apps-details__row">
{{#each category in categories}}
<span class="rc-apps-category">{{ category }}</span>
{{/each}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'color'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="color" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
{{/if}}
{{/if}}
<div class="rc-apps-details__row rc-apps-details__block">
<h2> {{_ "Contact"}} </h2>
<div class="rc-apps-details__row">
<div class="rc-apps-details__col">
{{#if author.homepage}}
<h3>{{_ "Author_Site"}}</h3>
<a href="{{author.homepage}}">{{author.homepage}}</a>
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
<div class="rc-apps-details__col">
{{#if author.support}}
<h3>{{_ "Support"}}</h3>
<a href="{{author.support}}">{{author.support}}</a>
{{/if}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
<!-- <div class="horizontal">
{{#if $eq editor 'color'}}
<div class="flex-grow-1">
<input class="input-monitor colorpicker-input" type="text" name="{{id}}" value="{{value}}" autocomplete="off"/>
<span class="colorpicker-swatch border-component-color" style="background-color: {{value}}"></span>
</div>
{{/if}}
{{#if $eq editor 'expression'}}
<div class="flex-grow-1">
<input class="input-monitor" type="" name="{{id}}" value="{{value}}"/>
<div class="rc-apps-details__row">
<h2> {{_ "Details"}} </h2>
</div>
{{#if summary}}
<div>
{{ summary }}
</div>
{{/if}}
<div class="color-editor">
<div class="select-arrow">
<i class="icon-down-open secondary-font-color"></i>
</div>
<select name="color-editor">
{{#each allowedTypes}}
<option value="{{.}}" selected="{{$eq ../editor .}}">{{_ .}}</option>
{{/each}}
</select>
{{/if}}
{{#if description}}
<div class="rc-apps-details__description">
<p>{{ description }}</p>
</div>
</div>
<div class="settings-description">Variable name: {{getColorVariable id}}</div> -->
{{ else if $eq type 'language'}}
{{/if}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-select">
<select class="rc-select__element" name="{{id}}">
{{#each languages}}
<option class="rc-select__option" value="{{key}}" selected="{{selectedOption key}}">{{_ name}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
{{#if settings}}
<div class="rc-apps-details__row">
<h2> {{_ "Settings"}} </h2>
</div>
{{/if}}
</div>
{{else}}
<div class="input-line double-col">
<label class="setting-label">{{_ i18nLabel}}</label>
<div class="setting-field">
{{#if $eq type 'action'}}
{{#if hasChanges section}}
<span style="line-height: 40px" class="secondary-font-color">{{_ "Save_to_enable_this_action"}}</span>
{{else}}
<button type="button" class="button primary action" data-setting="{{id}}" data-action="{{value}}">{{_ actionText}}</button>
{{/if}}
{{/if}}
<div class="rc-apps-settings">
{{#each settings}}
<div class="rc-apps-settings__item">
{{#if $eq type 'string'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="text" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
{{/if}}
</div>
</label>
{{#if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{#if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'boolean'}}
<div class="rc-switch rc-switch--blue">
<label class="rc-switch__label" tabindex="-1" for="{{id}}">
<input type="checkbox" class="rc-switch__input" name="{{id}}" id="{{id}}" checked="{{$eq value true}}">
<span class="rc-switch__button">
<span class="rc-switch__button-inside"></span>
</span>
<span class="rc-switch__text">
{{_ i18nLabel}}
</span>
</label>
<span class="rc-switch__description">{{{parseDescription i18nDescription}}}</span>
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'int'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="number" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'password'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="password" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'relativeUrl'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if $eq type 'relativeUrl'}}
<input class="rc-input__element" type="text" name="{{id}}" value="{{relativeUrl value}}" placeholder="{{_ i18nPlaceholder}}" {{isReadonly}}/>
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'font'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="text" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'code'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if isDisabled.disabled}}
{{> CodeMirror name=id options=(getEditorOptions true) code=(i18nDefaultValue) }}
{{else}}
<div class="code-mirror-box" data-editor-id="{{id}}">
<div class="title">
{{label}}
</div>
{{> CodeMirror name=id options=getEditorOptions code=value }}
<div class="buttons">
<button class="button primary button-fullscreen">Full Screen</button>
<button class="button primary button-restore">Exit Full Screen</button>
</div>
</div>
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{#if $eq type 'asset'}}
{{#if value.url}}
<div class="settings-file-preview">
<div class="preview" style="background-image:url({{value.url}}?_dc={{random}});"></div>
<div class="action">
<button type="button" class="button danger delete-asset"><i class="icon-trash secondary-font-color"></i>{{_ 'Delete'}}</button>
</div>
</div>
{{else}}
<div class="settings-file-preview">
<div class="preview no-file background-transparent-light secondary-font-color"><i class="icon-upload secondary-font-color"></i></div>
<div class="action">
<div class="button primary">{{_ 'Select_file'}}
<input type="file" accept="{{assetAccept fileConstraints}}" />
{{else if $eq type 'select'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-select">
<select class="rc-select__element" name="{{id}}">
{{#each values}}
<option class="rc-select__option" value="{{key}}" selected="{{selectedOption ../id key}}">{{_ i18nLabel}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else if $eq type 'color'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-input__wrapper">
{{#if multiline}}
<textarea class="rc-input__element" name="{{id}}" rows="4" style="height: auto">{{value}}</textarea>
{{else}}
<input class="rc-input__element" type="color" name="{{id}}" value="{{value}}" placeholder="{{_ i18nPlaceholder}}" />
{{/if}}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
<!-- <div class="horizontal">
{{#if $eq editor 'color'}}
<div class="flex-grow-1">
<input class="input-monitor colorpicker-input" type="text" name="{{id}}" value="{{value}}" autocomplete="off"/>
<span class="colorpicker-swatch border-component-color" style="background-color: {{value}}"></span>
</div>
{{/if}}
{{#if $eq editor 'expression'}}
<div class="flex-grow-1">
<input class="input-monitor" type="" name="{{id}}" value="{{value}}"/>
</div>
{{/if}}
<div class="color-editor">
<div class="select-arrow">
<i class="icon-down-open secondary-font-color"></i>
</div>
<select name="color-editor">
{{#each allowedTypes}}
<option value="{{.}}" selected="{{$eq ../editor .}}">{{_ .}}</option>
{{/each}}
</select>
</div>
</div>
<div class="settings-description">Variable name: {{getColorVariable id}}</div> -->
{{ else if $eq type 'language'}}
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ i18nLabel}}</div>
<div class="rc-select">
<select class="rc-select__element" name="{{id}}">
{{#each languages}}
<option class="rc-select__option" value="{{key}}" selected="{{selectedOption key}}">{{_ name}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</label>
{{# if i18nDescription}}
<div class="rc-input__description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{# if i18nAlert}}
<div class="rc-input__error">
<div class="rc-input__error-icon">
{{> icon block="rc-input__error-icon" icon="warning" classes="rc-input__error-icon-svg"}}
</div>
<div class="rc-input__error-message">{{{parseDescription i18nAlert}}}</div>
</div>
{{/if}}
</div>
{{else}}
<div class="input-line double-col">
<label class="setting-label">{{_ i18nLabel}}</label>
<div class="setting-field">
{{#if $eq type 'action'}}
{{#if hasChanges section}}
<span style="line-height: 40px" class="secondary-font-color">{{_ "Save_to_enable_this_action"}}</span>
{{else}}
<button type="button" class="button primary action" data-setting="{{id}}" data-action="{{value}}">{{_ actionText}}</button>
{{/if}}
{{/if}}
{{#if $eq type 'asset'}}
{{#if value.url}}
<div class="settings-file-preview">
<div class="preview" style="background-image:url({{value.url}}?_dc={{random}});"></div>
<div class="action">
<button type="button" class="button danger delete-asset"><i class="icon-trash secondary-font-color"></i>{{_ 'Delete'}}</button>
</div>
</div>
{{else}}
<div class="settings-file-preview">
<div class="preview no-file background-transparent-light secondary-font-color"><i class="icon-upload secondary-font-color"></i></div>
<div class="action">
<div class="button primary">{{_ 'Select_file'}}
<input type="file" accept="{{assetAccept fileConstraints}}" />
</div>
</div>
</div>
{{/if}}
{{/if}}
{{#if $eq type 'roomPick'}}
<div>
{{> inputAutocomplete settings=autocompleteRoom id=id name=id class="search autocomplete" autocomplete="off" disabled=isDisabled.disabled}}
<ul class="selected-rooms">
{{#each selectedRooms}}
<li class="remove-room" data-setting={{../id}}>{{name}} <i class="icon-cancel secondary-font-color"></i></li>
{{/each}}
</ul>
</div>
{{/if}}
{{#if i18nDescription}}
<div class="settings-description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{#if i18nAlert}}
<div class="settings-alert pending-color pending-background pending-border"><i class="icon-attention secondary-font-color"></i>{{{parseDescription i18nAlert}}}</div>
{{/if}}
</div>
</div>
</div>
</div>
{{/if}}
{{/if}}
{{/if}}
{{#if $eq type 'roomPick'}}
<div>
{{> inputAutocomplete settings=autocompleteRoom id=id name=id class="search autocomplete" autocomplete="off" disabled=isDisabled.disabled}}
<ul class="selected-rooms">
{{#each selectedRooms}}
<li class="remove-room" data-setting={{../id}}>{{name}} <i class="icon-cancel secondary-font-color"></i></li>
{{/each}}
</ul>
</div>
{{/each}}
<div class="rc-button-group">
<button class="rc-button rc-button--outline js-cancel-editing" disabled='{{disabled}}'>{{_ "Cancel" }}</button>
<button class="rc-button rc-button--primary js-save {{#if saving}} loading{{/if}}" disabled='{{disabled}}'>{{_ "Save" }}</button>
</div>
{{/if}}
{{#if i18nDescription}}
<div class="settings-description">{{{parseDescription i18nDescription}}}</div>
{{/if}}
{{#if i18nAlert}}
<div class="settings-alert pending-color pending-background pending-border"><i class="icon-attention secondary-font-color"></i>{{{parseDescription i18nAlert}}}</div>
{{/if}}
</div>
</div>
{{/if}}
</div>
{{/if}}
</div>
{{/each}}
</div>
<div class="rc-apps-container">
<div class="rc-button-group">
<button class="rc-button rc-button--secondary js-cancel">{{_ "Cancel" }}</button>
<button class="rc-button rc-button--primary js-save {{#if saving}} loading{{/if}}" disabled='{{disabled}}'>{{_ "Save" }}</button>
<button class="rc-button rc-button--cancel js-uninstall">{{_ "Uninstall" }}</button>
<button class="rc-button rc-button--secondary js-update">{{_ "Update" }}</button>
<button class="rc-button rc-button--secondary js-view-logs">{{_ "View_Logs" }}</button>
{{else if hasError}}
<div class="apps-error error-color">
<i class="icon-attention"></i>
<p>Sadly, an error has occured while loading this page.</p>
</div>
</div>
{{else if hasError}}
<div class="apps-error error-color">
<i class="icon-attention"></i>
<p>Sadly, an error has occured while loading this page.</p>
</div>
{{else}}
{{> loading}}
{{/if}}
{{/requiresPermission}}
{{else}}
{{> loading}}
{{/if}}
{{/requiresPermission}}
</div>
</section>
{{/with}}
</template>

@ -4,6 +4,40 @@ import s from 'underscore.string';
import { AppEvents } from '../communication';
import { Utilities } from '../../lib/misc/Utilities';
const HOST = 'https://marketplace.rocket.chat'; // TODO move this to inside RocketChat.API
function getApps(instance) {
const id = instance.id.get();
return Promise.all([
fetch(`${ HOST }/v1/apps/${ id }`).then((data) => data.json()),
RocketChat.API.get('apps/').then((result) => result.apps.filter((app) => app.id === id)),
]).then(([[remoteApp], [localApp]]) => {
if (localApp) {
localApp.installed = true;
if (remoteApp) {
localApp.categories = remoteApp.categories;
if (localApp.version !== remoteApp.version) {
localApp.newVersion = remoteApp.version;
}
}
instance.onSettingUpdated({ appId: id });
window.Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, instance.onStatusChanged);
window.Apps.getWsListener().unregisterListener(AppEvents.APP_SETTING_UPDATED, instance.onSettingUpdated);
window.Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, instance.onStatusChanged);
window.Apps.getWsListener().registerListener(AppEvents.APP_SETTING_UPDATED, instance.onSettingUpdated);
}
instance.app.set(localApp || remoteApp);
instance.ready.set(true);
}).catch((e) => {
instance.hasError.set(true);
instance.theError.set(e.message);
});
}
Template.appManage.onCreated(function() {
const instance = this;
@ -13,6 +47,7 @@ Template.appManage.onCreated(function() {
this.theError = new ReactiveVar('');
this.processingEnabled = new ReactiveVar(false);
this.app = new ReactiveVar({});
this.appsList = new ReactiveVar([]);
this.settings = new ReactiveVar({});
this.loading = new ReactiveVar(false);
@ -34,19 +69,7 @@ Template.appManage.onCreated(function() {
instance.settings.set(settings);
}
Promise.all([
RocketChat.API.get(`apps/${ id }`),
RocketChat.API.get(`apps/${ id }/settings`),
]).then((results) => {
instance.app.set(results[0].app);
console.log(instance.app.get());
_morphSettings(results[1].settings);
this.ready.set(true);
}).catch((e) => {
instance.hasError.set(true);
instance.theError.set(e.message);
});
getApps(instance);
instance.onStatusChanged = function _onStatusChanged({ appId, status }) {
if (appId !== id) {
@ -67,9 +90,6 @@ Template.appManage.onCreated(function() {
_morphSettings(result.settings);
});
};
window.Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, instance.onStatusChanged);
window.Apps.getWsListener().registerListener(AppEvents.APP_SETTING_UPDATED, instance.onSettingUpdated);
});
Template.apps.onDestroyed(function() {
@ -110,6 +130,11 @@ Template.appManage.helpers({
getColorVariable(color) {
return color.replace(/theme-color-/, '@');
},
dirty() {
const t = Template.instance();
const settings = t.settings.get();
return Object.keys(settings).some((k) => settings[k].hasChanged);
},
disabled() {
const t = Template.instance();
const settings = t.settings.get();
@ -152,9 +177,17 @@ Template.appManage.helpers({
return info.status === 'auto_enabled' || info.status === 'manually_enabled';
},
isInstalled() {
const instance = Template.instance();
return instance.app.get().installed === true;
},
app() {
return Template.instance().app.get();
},
categories() {
return Template.instance().app.get().categories;
},
settings() {
return Object.values(Template.instance().settings.get());
},
@ -170,6 +203,27 @@ Template.appManage.helpers({
},
});
async function setActivate(actiavate, e, t) {
t.processingEnabled.set(true);
const el = $(e.currentTarget);
el.prop('disabled', true);
const status = actiavate ? 'manually_enabled' : 'manually_disabled';
try {
const result = await RocketChat.API.post(`apps/${ t.id.get() }/status`, { status });
const info = t.app.get();
info.status = result.status;
t.app.set(info);
} catch (e) {
// el.prop('checked', !el.prop('checked'));
// TODO alert
}
t.processingEnabled.set(false);
el.prop('disabled', false);
}
Template.appManage.events({
'click .expand': (e) => {
$(e.currentTarget).closest('.section').removeClass('section-collapsed');
@ -181,28 +235,17 @@ Template.appManage.events({
$(e.currentTarget).closest('.section').addClass('section-collapsed');
$(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand'));
},
'click .js-cancel'() {
FlowRouter.go('/admin/apps');
},
'change #enabled': (e, t) => {
t.processingEnabled.set(true);
$('#enabled').prop('disabled', true);
const status = $('#enabled').prop('checked') ? 'manually_enabled' : 'manually_disabled';
RocketChat.API.post(`apps/${ t.id.get() }/status`, { status }).then((result) => {
const info = t.app.get();
info.status = result.status;
t.app.set(info);
if (info.status.indexOf('disabled') !== -1) {
$('#enabled').prop('checked', false);
}
}).catch(() => {
$('#enabled').prop('checked', !$('#enabled').prop('checked'));
}).then(() => {
t.processingEnabled.set(false);
$('#enabled').prop('disabled', false);
});
'click .js-activate'(e, t) {
setActivate(true, e, t);
},
'click .js-deactivate'(e, t) {
setActivate(false, e, t);
},
'click .js-uninstall': async(e, t) => {
@ -217,6 +260,29 @@ Template.appManage.events({
}
},
'click .js-install': async(e, t) => {
const el = $(e.currentTarget);
el.prop('disabled', true);
el.addClass('loading');
const app = t.app.get();
const url = `${ HOST }/v1/apps/${ t.id.get() }/download`;
const api = app.newVersion ? `apps/${ t.id.get() }` : 'apps/';
RocketChat.API.post(api, { url }).then(() => {
getApps(t).then(() => {
el.prop('disabled', false);
el.removeClass('loading');
});
});
// play animation
// TODO this icon and animation are not working
$(e.currentTarget).find('.rc-icon').addClass('play');
},
'click .js-update': (e, t) => {
FlowRouter.go(`/admin/app/install?isUpdatingId=${ t.id.get() }`);
},
@ -225,6 +291,10 @@ Template.appManage.events({
FlowRouter.go(`/admin/apps/${ t.id.get() }/logs`);
},
'click .js-cancel-editing': async(e, t) => {
t.onSettingUpdated({ appId: t.id.get() });
},
'click .js-save': async(e, t) => {
if (t.loading.get()) {
return;

@ -3,9 +3,8 @@
{{#header sectionName="Apps" hideHelp=true fixedHeight=true fullpage=true}}
<button class="rc-button rc-button--small rc-button--primary rc-directory-plus" data-button="install">{{> icon icon="plus"}}</button>
{{/header}}
<div class="rc-table-content">
<!-- {{>tabs tabs=tabsData}} -->
{{>tabs tabs=tabsData}}
<div class="rc-input rc-input--small rc-directory-search">
<label class="rc-input__label">
<div class="rc-input__wrapper">
@ -33,36 +32,49 @@
</thead>
<tbody>
{{#each apps}}
<tr class="rc-table-tr manage" data-name="{{name}}">
<tr class="rc-table-tr manage" data-name="{{latest.name}}">
<td>
<div class="rc-table-wrapper">
<div class="rc-table-avatar" style="background-image:url({{iconFileContent}})"></div>
{{#if latest.iconFileData}}
<div class="rc-table-avatar" style="background-image:url(data:image/png;base64,{{latest.iconFileData}})"></div>
{{else}}
<div class="rc-table-avatar" style="background-image:url({{latest.iconFileContent}})"></div>
{{/if}}
<div class="rc-table-info">
<span class="rc-table-title">
{{name}}
{{latest.name}}
</span>
{{#if author.name}}
<span class="rc-table-subtitle">by {{author.name}}</span>
{{#if latest.author.name}}
<span class="rc-table-subtitle">by {{latest.author.name}}</span>
{{/if}}
</div>
</div>
</td>
<td>{{category}}</td>
<td>{{latest.categories}}</td>
<td>
<p class="rc-table-title">
{{#if summary}}
{{summary}}
{{#if latest.summary}}
{{latest.summary}}
{{else}}
{{description}}
{{latest.description}}
{{/if}}
</p>
{{#if summary}}
{{#if latest.summary}}
<p>
{{description}}
{{latest.description}}
</p>
{{/if}}
</td>
<td>
<!-- {{> icon icon="app-installed"}} -->
{{#if $eq latest._installed true}}
{{> icon icon="app-installed"}}
{{/if}}
{{#if renderDownloadButton latest}}
<button class="js-install installer" data-app="{{appId}}">
{{> icon block="installer" icon="marketplace-installer"}}
</button>
{{/if}}
</td>
</tr>
{{/each}}

@ -1,18 +1,60 @@
import { AppEvents } from '../communication';
const ENABLED_STATUS = ['auto_enabled', 'manually_enabled'];
const HOST = 'https://marketplace.rocket.chat';
const enabled = ({ status }) => ENABLED_STATUS.includes(status);
const sortByColumn = (array, column, inverted) => array.sort((a, b) => {
if (a[column] < b[column] && !inverted) {
return -1;
}
return 1;
});
const sortByColumn = (array, column, inverted) =>
array.sort((a, b) => {
if (a.latest[column] < b.latest[column] && !inverted) {
return -1;
}
return 1;
});
const tagAlreadyInstalledApps = (installedApps, apps) => {
const installedIds = installedApps.map((app) => app.latest.id);
const tagged = apps.map((app) =>
({
latest: {
...app.latest,
_installed: installedIds.includes(app.latest.id),
},
})
);
return tagged;
};
const getApps = (instance) => {
instance.isLoading.set(true);
fetch(`${ HOST }/v1/apps?version=${ RocketChat.Info.marketplaceApiVersion }`)
.then((response) => response.json())
.then((data) => {
const tagged = tagAlreadyInstalledApps(instance.installedApps.get(), data);
instance.isLoading.set(false);
instance.apps.set(tagged);
instance.ready.set(true);
});
};
const getInstalledApps = (instance) => {
RocketChat.API.get('apps').then((data) => {
const apps = data.apps.map((app) => ({ latest: app }));
instance.installedApps.set(apps);
});
};
Template.apps.onCreated(function() {
const instance = this;
this.ready = new ReactiveVar(false);
this.apps = new ReactiveVar([]);
this.installedApps = new ReactiveVar([]);
this.categories = new ReactiveVar([]);
this.searchText = new ReactiveVar('');
this.searchSortBy = new ReactiveVar('name');
this.sortDirection = new ReactiveVar('asc');
@ -20,19 +62,28 @@ Template.apps.onCreated(function() {
this.page = new ReactiveVar(0);
this.end = new ReactiveVar(false);
this.isLoading = new ReactiveVar(false);
this.searchType = new ReactiveVar('marketplace');
getApps(instance);
getInstalledApps(instance);
RocketChat.API.get('apps').then((result) => {
instance.apps.set(result.apps);
instance.ready.set(true);
});
instance.onAppAdded = function _appOnAppAdded(appId) {
RocketChat.API.get(`apps/${ appId }`).then((result) => {
const apps = instance.apps.get();
apps.push(result.app);
instance.apps.set(apps);
fetch('https://marketplace.rocket.chat/v1/categories')
.then((response) => response.json())
.then((data) => {
instance.categories.set(data);
});
instance.onAppAdded = function _appOnAppAdded() {
// ToDo: fix this formatting data to add an app to installedApps array without to fetch all
// fetch(`https://marketplace.rocket.chat/v1/apps/${ appId }`).then((result) => {
// const installedApps = instance.installedApps.get();
// installedApps.push({
// latest: result.app,
// });
// instance.installedApps.set(installedApps);
// });
};
instance.onAppRemoved = function _appOnAppRemoved(appId) {
@ -75,7 +126,10 @@ Template.apps.helpers({
const searchText = instance.searchText.get().toLowerCase();
const sortColumn = instance.searchSortBy.get();
const inverted = instance.sortDirection.get() === 'desc';
return sortByColumn(instance.apps.get().filter(({ name }) => name.toLowerCase().includes(searchText)), sortColumn, inverted);
return sortByColumn(instance.apps.get().filter((app) => app.latest.name.toLowerCase().includes(searchText)), sortColumn, inverted);
},
categories() {
return Template.instance().categories.get();
},
parseStatus(status) {
return t(`App_status_${ status }`);
@ -83,9 +137,6 @@ Template.apps.helpers({
isActive(status) {
return enabled({ status });
},
searchResults() {
return Template.instance().results.get();
},
sortIcon(key) {
const {
sortDirection,
@ -133,20 +184,76 @@ Template.apps.helpers({
sortDirection.set('asc');
};
},
searchType() {
return Template.instance().searchType.get();
},
renderDownloadButton(latest) {
const isMarketplace = Template.instance().searchType.get() === 'marketplace';
const isDownloaded = latest._installed === false;
return isMarketplace && isDownloaded;
},
tabsData() {
const instance = Template.instance();
const {
searchType,
} = instance;
return {
tabs: [
{
label: t('Marketplace'),
value: 'marketplace',
condition() {
return true;
},
active: true,
},
{
label: t('Installed'),
value: 'installed',
condition() {
return true;
},
},
],
onChange(value) {
searchType.set(value);
if (value === 'marketplace') {
getApps(instance);
} else {
instance.apps.set(instance.installedApps.get());
}
},
};
},
});
Template.apps.events({
'click .manage'(e) {
e.preventDefault();
'click .manage'() {
const rl = this;
if (rl && rl.id) {
FlowRouter.go(`/admin/apps/${ rl.id }`);
if (rl && rl.latest && rl.latest.id) {
FlowRouter.go(`/admin/apps/${ rl.latest.id }`);
}
},
'click [data-button="install"]'() {
FlowRouter.go('/admin/app/install');
},
'click .installer'(e, template) {
e.stopPropagation();
const url = `${ HOST }/v1/apps/${ this.latest.id }/download`;
RocketChat.API.post('apps/', { url }).then(() => {
getInstalledApps(template);
});
// play animation
$(e.currentTarget).find('.rc-icon').addClass('play');
},
'keyup .js-search'(e, t) {
t.searchText.set(e.currentTarget.value);
},

@ -101,7 +101,7 @@ Meteor.startup(function _rlClientOrch() {
const appsRouteAction = function _theRealAction(whichCenter) {
Meteor.defer(() => window.Apps.getLoadingPromise().then((isEnabled) => {
if (isEnabled) {
BlazeLayout.render('main', { center: whichCenter });
BlazeLayout.render('main', { center: whichCenter, old: false }); // TODO remove old
} else {
FlowRouter.go('app-what-is-it');
}

@ -298,6 +298,7 @@
"Apiai_Key": "Api.ai Key",
"Apiai_Language": "Api.ai Language",
"App_author_homepage": "author homepage",
"App_Details": "App details",
"App_Information": "App Information",
"App_Installation": "App Installation",
"App_status_auto_enabled": "Enabled",
@ -365,6 +366,7 @@
"AutoLinker_Urls_www": "AutoLinker 'www' URLs",
"AutoLinker_UrlsRegExp": "AutoLinker URL Regular Expression",
"Automatic_Translation": "Automatic Translation",
"Author_Site": "Author site",
"AutoTranslate_Change_Language_Description": "Changing the auto-translate language does not translate previous messages.",
"AutoTranslate_Enabled": "Enable Auto-Translate",
"AutoTranslate_Enabled_Description": "Enabling auto-translation will allow people with the <code class=\"inline\">auto-translate</code> permission to have all messages automatically translated into their selected language. Fees may apply, see <a target=\"_blank\" href=\"https://cloud.google.com/translate/pricing\">Google's Documentation</a>",
@ -441,6 +443,7 @@
"CAS_Sync_User_Data_FieldMap_Description": "Use this JSON input to build internal attributes (key) from external attributes (value). External attribute names enclosed with '%' will interpolated in value strings.<br/>Example, `{\"email\":\"%email%\", \"name\":\"%firstname%, %lastname%\"}`<br/><br/>The attribute map is always interpolated. In CAS 1.0 only the `username` attribute is available. Available internal attributes are: username, name, email, rooms; rooms is a comma separated list of rooms to join upon user creation e.g: {\"rooms\": \"%team%,%department%\"} would join CAS users on creation to their team and department channel.",
"CAS_version": "CAS Version",
"CAS_version_Description": "Only use a supported CAS version supported by your CAS SSO service.",
"Categories": "Categories",
"CDN_PREFIX": "CDN Prefix",
"CDN_PREFIX_ALL": "Use CDN Prefix for all assets",
"CDN_JSCSS_PREFIX": "CDN Prefix for JS/CSS",
@ -558,6 +561,7 @@
"Consulting": "Consulting",
"Consumer_Goods": "Consumer Goods",
"Contains_Security_Fixes": "Contains Security Fixes",
"Contact": "Contact",
"Content": "Content",
"Continue": "Continue",
"Continuous_sound_notifications_for_new_livechat_room": "Continuous sound notifications for new livechat room",
@ -902,6 +906,7 @@
"Desktop_Notifications_Duration": "Desktop Notifications Duration",
"Desktop_Notifications_Duration_Description": "Seconds to display desktop notification. This may affect OS X Notification Center. Enter 0 to use default browser settings and not affect OS X Notification Center.",
"Desktop_Notifications_Enabled": "Desktop Notifications are Enabled",
"Details": "Details",
"Different_Style_For_User_Mentions": "Different style for user mentions",
"Direct_message_someone": "Direct message someone",
"Direct_Messages": "Direct Messages",
@ -1348,6 +1353,7 @@
"Industry": "Industry",
"initials_avatar": "Initials Avatar",
"inline_code": "inline code",
"Install": "Install",
"Install_Extension": "Install Extension",
"Install_FxOs": "Install Rocket.Chat on your Firefox",
"Install_FxOs_done": "Great! You can now use Rocket.Chat via the icon on your homescreen. Have fun with Rocket.Chat!",
@ -2604,6 +2610,7 @@
"Unread_Rooms_Mode": "Unread Rooms Mode",
"Unread_Tray_Icon_Alert": "Unread Tray Icon Alert",
"Unstar_Message": "Remove Star",
"Update_to_version": "Update to __version__",
"Update_your_RocketChat": "Update your Rocket.Chat",
"Updated_at": "Updated at",
"Upload_file_description": "File description",
@ -2845,4 +2852,4 @@
"Your_push_was_sent_to_s_devices": "Your push was sent to %s devices",
"Your_server_link": "Your server link",
"Your_workspace_is_ready": "Your workspace is ready to use 🎉"
}
}

@ -1,3 +1,4 @@
{
"version": "0.69.0-develop"
"version": "0.69.0-develop",
"marketplaceApiVersion": "0.9.13"
}

@ -9,7 +9,7 @@
width: 100%;
margin: 0 -1rem;
margin: 0 -1rem -1px -1rem;
}
.tab {

@ -12,59 +12,83 @@
padding: 25px 0;
max-width: 705px;
margin: auto;
width: 100%;
width: 100%;
display: flex;
padding-bottom: 25px;
&__header {
border-bottom: 1px solid #E1E1E1;
}
}
&-marketplace {
padding: 0 24px;
}
&-details {
display: flex;
padding: 25px;
border-bottom: 1px solid #E1E1E1;
&__photo {
width: 141px;
height: 141px;
width: 95px;
height: 95px;
flex: 0 0 auto;
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
background-repeat: no-repeat;
background-position: center center;
border: 1px solid #f7f7f7;
}
&__content {
flex: 1 1 auto;
justify-content: space-between;
padding: 0 28px;
display: flex;
flex-direction: column;
overflow: hidden;
justify-content: space-between;
padding: 0 15px;
display: flex;
flex-direction: column;
overflow: hidden;
}
&__row {
display: flex;
align-items: flex-end;
& button svg {
margin: 0 5px 0 -5px;
font-size: 18px;
}
h2 {
font-size: 18px;
padding: 5px 0;
}
}
&__row + &__row {
padding-top: 15px
padding-top: 5px
}
&__block {
flex-direction: column;
align-items: flex-start;
}
&__name {
font-size: 24px;
font-weight: bold;
}
&__version {
font-size: 17px;
padding: 0 14px;
color: var(--rc-color-primary-light);
}
&__author {
font-weight: 500;
}
&__item {
white-space: nowrap;
flex: 1 1 1px;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 1px;
overflow: hidden;
text-overflow: ellipsis;
}
}

@ -52,7 +52,7 @@
{{#unless removeSidenav}}
{{> sideNav }}
{{/unless}}
<div class="rc-old main-content content-background-color {{readReceiptsEnabled}} {{#if modal}}main-modal{{/if}}">
<div class="{{#unless $eq old false}} rc-old {{/unless}} main-content content-background-color {{readReceiptsEnabled}} {{#if modal}}main-modal{{/if}}">
{{> Template.dynamic template=center}}
</div>
</div>

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 109 KiB

@ -5,6 +5,9 @@ Template.tabs.onCreated(function() {
Template.tabs.events({
'click .tab'(e) {
const { value } = e.currentTarget.dataset;
if (value === Template.instance().activeTab.get()) {
return;
}
Template.instance().activeTab.set(value);
Template.instance().data.tabs.onChange(value);
},

Loading…
Cancel
Save