diff --git a/.gitattributes b/.gitattributes index fbcb8a02f0d47f56544c2b39408979c9de73b5b9..6afffcdb16dba71b82340ef1966b2c786b678400 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,8 @@ /apps/accessibility/js/accessibility.js.map binary /apps/comments/js/*.js binary /apps/comments/js/*.js.map binary +/apps/dashboard/js/*.js binary +/apps/dashboard/js/*.js.map binary /apps/files/js/dist/*.js binary /apps/files/js/dist/*.js.map binary /apps/files_sharing/js/dist/*.js binary diff --git a/apps/dashboard/appinfo/routes.php b/apps/dashboard/appinfo/routes.php index 34792a9d47dbd1cb367a849a8c03de03643c2577..4edca1a3ec56d966a5bc6d61ced61fc5bbf01b28 100644 --- a/apps/dashboard/appinfo/routes.php +++ b/apps/dashboard/appinfo/routes.php @@ -27,5 +27,6 @@ declare(strict_types=1); return [ 'routes' => [ ['name' => 'dashboard#index', 'url' => '/', 'verb' => 'GET'], + ['name' => 'dashboard#updateLayout', 'url' => '/layout', 'verb' => 'POST'], ] ]; diff --git a/apps/dashboard/js/dashboard.js b/apps/dashboard/js/dashboard.js index 650d57b54be00e0633b267cc9ff76747f1707518..03e6ced23819a5a625a8009277f3d20d5033b35a 100644 Binary files a/apps/dashboard/js/dashboard.js and b/apps/dashboard/js/dashboard.js differ diff --git a/apps/dashboard/js/dashboard.js.map b/apps/dashboard/js/dashboard.js.map index ac0af8ab77ba418f7fdc89c30e72637e2b89424e..5c0b96d6fb31456445dbebaa9c1bc903ca0b73f3 100644 Binary files a/apps/dashboard/js/dashboard.js.map and b/apps/dashboard/js/dashboard.js.map differ diff --git a/apps/dashboard/lib/Controller/DashboardController.php b/apps/dashboard/lib/Controller/DashboardController.php index 687fbace380d04a1226d52aae298a994c7f4609f..305759fa6e8e5b7a00af8b4e92fdc9f005ecb09d 100644 --- a/apps/dashboard/lib/Controller/DashboardController.php +++ b/apps/dashboard/lib/Controller/DashboardController.php @@ -26,13 +26,16 @@ declare(strict_types=1); namespace OCA\Dashboard\Controller; +use OCA\Files\Event\LoadSidebar; use OCA\Viewer\Event\LoadViewer; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\Dashboard\IManager; -use OCP\Dashboard\IPanel; -use OCP\Dashboard\RegisterPanelEvent; +use OCP\Dashboard\IWidget; +use OCP\Dashboard\RegisterWidgetEvent; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IInitialStateService; use OCP\IRequest; @@ -44,19 +47,27 @@ class DashboardController extends Controller { private $eventDispatcher; /** @var IManager */ private $dashboardManager; + /** @var IConfig */ + private $config; + /** @var string */ + private $userId; public function __construct( string $appName, IRequest $request, IInitialStateService $initialStateService, IEventDispatcher $eventDispatcher, - IManager $dashboardManager + IManager $dashboardManager, + IConfig $config, + $userId ) { parent::__construct($appName, $request); $this->inititalStateService = $initialStateService; $this->eventDispatcher = $eventDispatcher; $this->dashboardManager = $dashboardManager; + $this->config = $config; + $this->userId = $userId; } /** @@ -65,23 +76,37 @@ class DashboardController extends Controller { * @return TemplateResponse */ public function index(): TemplateResponse { - $this->eventDispatcher->dispatchTyped(new RegisterPanelEvent($this->dashboardManager)); - - $dashboardManager = $this->dashboardManager; - $panels = array_map(function (IPanel $panel) { - return [ - 'id' => $panel->getId(), - 'title' => $panel->getTitle(), - 'iconClass' => $panel->getIconClass(), - 'url' => $panel->getUrl() - ]; - }, $dashboardManager->getPanels()); - $this->inititalStateService->provideInitialState('dashboard', 'panels', $panels); - + $this->eventDispatcher->dispatchTyped(new LoadSidebar()); if (class_exists(LoadViewer::class)) { $this->eventDispatcher->dispatchTyped(new LoadViewer()); } + $this->eventDispatcher->dispatchTyped(new RegisterWidgetEvent($this->dashboardManager)); + + $userLayout = explode(',', $this->config->getUserValue($this->userId, 'dashboard', 'layout', 'recommendations,spreed,mail,calendar')); + $widgets = array_map(function (IWidget $widget) { + return [ + 'id' => $widget->getId(), + 'title' => $widget->getTitle(), + 'iconClass' => $widget->getIconClass(), + 'url' => $widget->getUrl() + ]; + }, $this->dashboardManager->getWidgets()); + $this->inititalStateService->provideInitialState('dashboard', 'panels', $widgets); + $this->inititalStateService->provideInitialState('dashboard', 'layout', $userLayout); + $this->inititalStateService->provideInitialState('dashboard', 'firstRun', $this->config->getUserValue($this->userId, 'dashboard', 'firstRun', '1') === '1'); + $this->config->setUserValue($this->userId, 'dashboard', 'firstRun', '0'); + return new TemplateResponse('dashboard', 'index'); } + + /** + * @NoAdminRequired + * @param string $layout + * @return JSONResponse + */ + public function updateLayout(string $layout): JSONResponse { + $this->config->setUserValue($this->userId, 'dashboard', 'layout', $layout); + return new JSONResponse(['layout' => $layout]); + } } diff --git a/apps/dashboard/src/App.vue b/apps/dashboard/src/App.vue index 87c76a603b43496370b326606cf74b6037ec4975..d73f895fc9141157a1dd95a5a0a1f4dfdd59a6c5 100644 --- a/apps/dashboard/src/App.vue +++ b/apps/dashboard/src/App.vue @@ -1,17 +1,57 @@ <template> <div id="app-dashboard"> <h2>{{ greeting.icon }} {{ greeting.text }}</h2> + <div class="statuses"> + <div v-for="status in registeredStatus" + :id="'status-' + status" + :key="status" + :ref="'status-' + status" /> + </div> - <div class="panels"> - <div v-for="panel in panels" :key="panel.id" class="panel"> - <a :href="panel.url"> - <h3 :class="panel.iconClass"> - {{ panel.title }} + <Draggable v-model="layout" + class="panels" + handle=".panel--header" + @end="saveLayout"> + <div v-for="panelId in layout" :key="panels[panelId].id" class="panel"> + <div class="panel--header"> + <h3 :class="panels[panelId].iconClass"> + {{ panels[panelId].title }} </h3> - </a> - <div :ref="panel.id" :data-id="panel.id" /> + </div> + <div class="panel--content"> + <div :ref="panels[panelId].id" :data-id="panels[panelId].id" /> + </div> </div> - </div> + </Draggable> + + <a v-tooltip="tooltip" + class="edit-panels icon-add" + :class="{ firstrun: firstRun }" + @click="showModal">{{ t('dashboard', 'Edit widgets') }}</a> + + <Modal v-if="modal" @close="closeModal"> + <div class="modal__content"> + <h3>{{ t('dashboard', 'Edit widgets') }}</h3> + <Draggable v-model="layout" + class="panels" + tag="ol" + handle=".draggable" + @end="saveLayout"> + <li v-for="panel in sortedPanels" :key="panel.id"> + <input :id="'panel-checkbox-' + panel.id" + type="checkbox" + class="checkbox" + :checked="isActive(panel)" + @input="updateCheckbox(panel, $event.target.checked)"> + <label :for="'panel-checkbox-' + panel.id" :class="isActive(panel) ? 'draggable ' + panel.iconClass : panel.iconClass"> + {{ panel.title }} + </label> + </li> + </Draggable> + + <a :href="appStoreUrl" class="button">{{ t('dashboard', 'Get more widgets from the app store') }}</a> + </div> + </Modal> </div> </template> @@ -19,49 +59,93 @@ import Vue from 'vue' import { loadState } from '@nextcloud/initial-state' import { getCurrentUser } from '@nextcloud/auth' +import { Modal } from '@nextcloud/vue' +import Draggable from 'vuedraggable' +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' const panels = loadState('dashboard', 'panels') +const firstRun = loadState('dashboard', 'firstRun') export default { name: 'App', + components: { + Modal, + Draggable, + }, data() { return { timer: new Date(), + registeredStatus: [], callbacks: {}, + callbacksStatus: {}, panels, - name: getCurrentUser()?.displayName, + firstRun, + displayName: getCurrentUser()?.displayName, + uid: getCurrentUser()?.uid, + layout: loadState('dashboard', 'layout').filter((panelId) => panels[panelId]), + modal: false, + appStoreUrl: generateUrl('/settings/apps/dashboard'), + statuses: {}, } }, computed: { + tooltip() { + if (!this.firstRun) { + return null + } + return { + content: t('dashboard', 'Adjust the dashboard to your needs'), + placement: 'top', + show: true, + trigger: 'manual', + } + }, greeting() { const time = this.timer.getHours() + const shouldShowName = this.displayName && this.uid !== this.displayName if (time > 18) { - return { icon: '🌙', text: t('dashboard', 'Good evening, {name}', { name: this.name }) } + return { icon: '🌙', text: shouldShowName ? t('dashboard', 'Good evening, {name}', { name: this.displayName }) : t('dashboard', 'Good evening') } } if (time > 12) { - return { icon: '☀', text: t('dashboard', 'Good afternoon, {name}', { name: this.name }) } + return { icon: '☀', text: shouldShowName ? t('dashboard', 'Good afternoon, {name}', { name: this.displayName }) : t('dashboard', 'Good afternoon') } } if (time === 12) { - return { icon: 'ðŸ½', text: t('dashboard', 'Time for lunch, {name}', { name: this.name }) } + return { icon: 'ðŸ½', text: shouldShowName ? t('dashboard', 'Time for lunch, {name}', { name: this.displayName }) : t('dashboard', 'Time for lunch') } } if (time > 5) { - return { icon: '🌄', text: t('dashboard', 'Good morning, {name}', { name: this.name }) } + return { icon: '🌄', text: shouldShowName ? t('dashboard', 'Good morning, {name}', { name: this.displayName }) : t('dashboard', 'Good morning') } } - return { icon: '🦉', text: t('dashboard', 'Have a night owl, {name}', { name: this.name }) } + return { icon: '🦉', text: shouldShowName ? t('dashboard', 'Have a night owl, {name}', { name: this.displayName }) : t('dashboard', 'Have a night owl') } + }, + isActive() { + return (panel) => this.layout.indexOf(panel.id) > -1 + }, + sortedPanels() { + return Object.values(this.panels).sort((a, b) => { + const indexA = this.layout.indexOf(a.id) + const indexB = this.layout.indexOf(b.id) + if (indexA === -1 || indexB === -1) { + return indexB - indexA || a.id - b.id + } + return indexA - indexB || a.id - b.id + }) }, }, watch: { callbacks() { - for (const app in this.callbacks) { - const element = this.$refs[app] - if (this.panels[app].mounted) { + this.rerenderPanels() + }, + callbacksStatus() { + for (const app in this.callbacksStatus) { + const element = this.$refs['status-' + app] + if (this.statuses[app] && this.statuses[app].mounted) { continue } - if (element) { - this.callbacks[app](element[0]) - Vue.set(this.panels[app], 'mounted', true) + this.callbacksStatus[app](element[0]) + Vue.set(this.statuses, app, { mounted: true }) } else { console.error('Failed to register panel in the frontend as no backend data was provided for ' + app) } @@ -72,11 +156,74 @@ export default { setInterval(() => { this.timer = new Date() }, 30000) + + if (this.firstRun) { + window.addEventListener('scroll', this.disableFirstrunHint) + } }, methods: { + /** + * Method to register panels that will be called by the integrating apps + * + * @param {string} app The unique app id for the widget + * @param {function} callback The callback function to register a panel which gets the DOM element passed as parameter + */ register(app, callback) { Vue.set(this.callbacks, app, callback) }, + registerStatus(app, callback) { + this.registeredStatus.push(app) + this.$nextTick(() => { + Vue.set(this.callbacksStatus, app, callback) + }) + }, + rerenderPanels() { + for (const app in this.callbacks) { + const element = this.$refs[app] + if (this.layout.indexOf(app) === -1) { + continue + } + if (this.panels[app] && this.panels[app].mounted) { + continue + } + if (element) { + this.callbacks[app](element[0]) + Vue.set(this.panels[app], 'mounted', true) + } else { + console.error('Failed to register panel in the frontend as no backend data was provided for ' + app) + } + } + }, + saveLayout() { + axios.post(generateUrl('/apps/dashboard/layout'), { + layout: this.layout.join(','), + }) + }, + showModal() { + this.modal = true + this.firstRun = false + }, + closeModal() { + this.modal = false + }, + updateCheckbox(panel, currentValue) { + const index = this.layout.indexOf(panel.id) + if (!currentValue && index > -1) { + this.layout.splice(index, 1) + + } else { + this.layout.push(panel.id) + } + Vue.set(this.panels[panel.id], 'mounted', false) + this.saveLayout() + this.$nextTick(() => this.rerenderPanels()) + }, + disableFirstrunHint() { + window.removeEventListener('scroll', this.disableFirstrunHint) + setTimeout(() => { + this.firstRun = false + }, 1000) + }, }, } </script> @@ -84,16 +231,20 @@ export default { <style lang="scss" scoped> #app-dashboard { width: 100%; + margin-bottom: 100px; } + h2 { text-align: center; font-size: 32px; line-height: 130%; - padding: 80px 16px 32px; + padding: 80px 16px 0px; } .panels { - width: 100%; + width: auto; + margin: auto; + max-width: 1500px; display: flex; justify-content: center; flex-direction: row; @@ -101,26 +252,125 @@ export default { flex-wrap: wrap; } - .panel { - width: 250px; + .panel, .panels > div { + width: 320px; + max-width: 100%; margin: 16px; + background-color: var(--color-main-background-translucent); + border-radius: var(--border-radius-large); + border: 2px solid var(--color-border); - & > a { - position: sticky; + &.sortable-ghost { + opacity: 0.1; + } + + & > .panel--header { + display: flex; + z-index: 1; top: 50px; - display: block; - background: linear-gradient(var(--color-main-background-translucent), var(--color-main-background-translucent) 80%, rgba(255, 255, 255, 0)); + padding: 16px; + // TO DO: use variables here + background: linear-gradient(170deg, rgba(0, 130,201, 0.2) 0%, rgba(255,255,255,.1) 50%, rgba(255,255,255,0) 100%); + border-top-left-radius: calc(var(--border-radius-large) - 2px); + border-top-right-radius: calc(var(--border-radius-large) - 2px); backdrop-filter: blur(4px); + cursor: grab; + + &, ::v-deep * { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + &:active { + cursor: grabbing; + } + + a { + flex-grow: 1; + } h3 { + display: block; + flex-grow: 1; margin: 0; font-size: 20px; font-weight: bold; background-size: 32px; - background-position: 10px 10px; - padding: 16px 8px 16px 52px; + background-position: 14px 12px; + padding: 16px 8px 16px 60px; + cursor: grab; } } + + & > .panel--content { + margin: 0 16px 16px 16px; + height: 420px; + overflow: auto; + } + } + + .edit-panels { + z-index: 99; + position: fixed; + bottom: 20px; + right: 20px; + padding: 10px 15px 10px 35px; + background-position: 10px center; + opacity: .7; + background-color: var(--color-main-background); + border-radius: var(--border-radius-pill); + transition: right var(--animation-slow) ease-in-out; + + &:hover { + opacity: 1; + background-color: var(--color-background-hover); + } + + &.firstrun { + right: 50%; + transform: translateX(50%); + max-width: 200px; + box-shadow: 0px 0px 3px var(--color-box-shadow); + opacity: 1; + text-align: center; + } + } + + .modal__content { + width: 30vw; + margin: 20px; + ol { + display: flex; + flex-direction: column; + list-style-type: none; + } + li label { + padding: 10px; + display: block; + list-style-type: none; + background-size: 16px; + background-position: left center; + padding-left: 26px; + } + } + + .flip-list-move { + transition: transform var(--animation-slow); + } + + .statuses { + display: flex; + flex-direction: row; + justify-content: center; + margin-bottom: 40px; + + & > div { + max-width: 200px; + } } </style> diff --git a/apps/dashboard/src/main.js b/apps/dashboard/src/main.js index 998f538356bc7df7582a6e7ea6d1cabc2603c9e5..17619cc22bf6ecc9f4957ceaa859c1ab0533fd13 100644 --- a/apps/dashboard/src/main.js +++ b/apps/dashboard/src/main.js @@ -1,9 +1,19 @@ import Vue from 'vue' import App from './App.vue' +import { translate as t } from '@nextcloud/l10n' +import VTooltip from '@nextcloud/vue/dist/Directives/Tooltip' + +Vue.directive('Tooltip', VTooltip) + +Vue.prototype.t = t + +// FIXME workaround to make the sidebar work +Object.assign(window.OCA.Files, { App: { fileList: { filesClient: OC.Files.getClient() } } }, window.OCA.Files) const Dashboard = Vue.extend(App) -const Instance = new Dashboard({}).$mount('#app') +const Instance = new Dashboard({}).$mount('#app-content-vue') window.OCA.Dashboard = { register: (app, callback) => Instance.register(app, callback), + registerStatus: (app, callback) => Instance.registerStatus(app, callback), } diff --git a/apps/dashboard/templates/index.php b/apps/dashboard/templates/index.php index bca9fa30494682d0dcf6789e8f50aeee96450579..e4056ce06e26903e55d03d26664e251d84eb517a 100644 --- a/apps/dashboard/templates/index.php +++ b/apps/dashboard/templates/index.php @@ -1,4 +1,4 @@ <?php \OCP\Util::addScript('dashboard', 'dashboard'); ?> -<div id="app"></div> +<div id="app-content-vue"></div> diff --git a/apps/user_status/js/user-status-menu.js b/apps/user_status/js/user-status-menu.js index 6c8889898c83c09e5bad39ce1137859626c8de86..865fcc1b3b5e39b35a669737c5aa736ac4c5a524 100644 Binary files a/apps/user_status/js/user-status-menu.js and b/apps/user_status/js/user-status-menu.js differ diff --git a/apps/user_status/js/user-status-menu.js.map b/apps/user_status/js/user-status-menu.js.map index 4b866db86448ad3ce3d2f8d9cc7a4e33f1ec72ef..77b8fc4702d0a729d1b46e0f8646f3b25f997306 100644 Binary files a/apps/user_status/js/user-status-menu.js.map and b/apps/user_status/js/user-status-menu.js.map differ diff --git a/apps/user_status/src/App.vue b/apps/user_status/src/App.vue index 116c7a0ca4701517aa176ab6f0bacaa77121d970..c9e95fa87fd10879a1485dbc5413bd352667466f 100644 --- a/apps/user_status/src/App.vue +++ b/apps/user_status/src/App.vue @@ -20,9 +20,10 @@ --> <template> - <li> + <li :class="{ inline }"> <div id="user-status-menu-item"> <span + v-if="!inline" id="user-status-menu-item__header" :title="displayName"> {{ displayName }} @@ -71,6 +72,12 @@ export default { ActionButton, SetStatusModal, }, + props: { + inline: { + type: Boolean, + default: false, + }, + }, data() { return { isModalOpen: false, @@ -237,7 +244,7 @@ export default { </script> <style lang="scss"> -#user-status-menu-item { +li:not(.inline) #user-status-menu-item { &__header { display: block; color: var(--color-main-text); @@ -270,4 +277,33 @@ export default { } } } + +.inline #user-status-menu-item__subheader { + width: 100%; + + > button { + background-color: var(--color-main-background); + background-size: 16px; + border: 0; + border-radius: var(--border-radius-pill); + font-weight: normal; + font-size: 0.875em; + padding-left: 40px; + + &:hover, + &:focus { + background-color: var(--color-background-hover); + } + + &.icon-loading-small { + &::after { + left: 21px; + } + } + } +} + + li { + list-style-type: none; + } </style> diff --git a/apps/user_status/src/main-user-status-menu.js b/apps/user_status/src/main-user-status-menu.js index 795f41df4e765f87f9c799027be7fba03723d863..c6d23337526ce4b3cfabe6a931af53ad9726b3bf 100644 --- a/apps/user_status/src/main-user-status-menu.js +++ b/apps/user_status/src/main-user-status-menu.js @@ -20,4 +20,20 @@ const app = new Vue({ store, }).$mount('li[data-id="user_status-menuitem"]') +document.addEventListener('DOMContentLoaded', function() { + if (!OCA.Dashboard) { + return + } + + OCA.Dashboard.registerStatus('status', (el) => { + const Dashboard = Vue.extend(App) + return new Dashboard({ + propsData: { + inline: true, + }, + store, + }).$mount(el) + }) +}) + export { app } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 5f61b848a8322f56f44dfd26321353930534a29b..0a64301f13bbde835c20b8900bb31107774b2d40 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -170,13 +170,13 @@ return array( 'OCP\\Dashboard\\IDashboardManager' => $baseDir . '/lib/public/Dashboard/IDashboardManager.php', 'OCP\\Dashboard\\IDashboardWidget' => $baseDir . '/lib/public/Dashboard/IDashboardWidget.php', 'OCP\\Dashboard\\IManager' => $baseDir . '/lib/public/Dashboard/IManager.php', - 'OCP\\Dashboard\\IPanel' => $baseDir . '/lib/public/Dashboard/IPanel.php', + 'OCP\\Dashboard\\IWidget' => $baseDir . '/lib/public/Dashboard/IWidget.php', 'OCP\\Dashboard\\Model\\IWidgetConfig' => $baseDir . '/lib/public/Dashboard/Model/IWidgetConfig.php', 'OCP\\Dashboard\\Model\\IWidgetRequest' => $baseDir . '/lib/public/Dashboard/Model/IWidgetRequest.php', 'OCP\\Dashboard\\Model\\WidgetSetting' => $baseDir . '/lib/public/Dashboard/Model/WidgetSetting.php', 'OCP\\Dashboard\\Model\\WidgetSetup' => $baseDir . '/lib/public/Dashboard/Model/WidgetSetup.php', 'OCP\\Dashboard\\Model\\WidgetTemplate' => $baseDir . '/lib/public/Dashboard/Model/WidgetTemplate.php', - 'OCP\\Dashboard\\RegisterPanelEvent' => $baseDir . '/lib/public/Dashboard/RegisterPanelEvent.php', + 'OCP\\Dashboard\\RegisterWidgetEvent' => $baseDir . '/lib/public/Dashboard/RegisterWidgetEvent.php', 'OCP\\Dashboard\\Service\\IEventsService' => $baseDir . '/lib/public/Dashboard/Service/IEventsService.php', 'OCP\\Dashboard\\Service\\IWidgetsService' => $baseDir . '/lib/public/Dashboard/Service/IWidgetsService.php', 'OCP\\Defaults' => $baseDir . '/lib/public/Defaults.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index a37769e9ed603ca4ee42cf8bc27eed698c62b619..43c45b2fe834f4798f2e41c0006329cb69dd6178 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -199,13 +199,13 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Dashboard\\IDashboardManager' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IDashboardManager.php', 'OCP\\Dashboard\\IDashboardWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IDashboardWidget.php', 'OCP\\Dashboard\\IManager' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IManager.php', - 'OCP\\Dashboard\\IPanel' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IPanel.php', + 'OCP\\Dashboard\\IWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IWidget.php', 'OCP\\Dashboard\\Model\\IWidgetConfig' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/IWidgetConfig.php', 'OCP\\Dashboard\\Model\\IWidgetRequest' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/IWidgetRequest.php', 'OCP\\Dashboard\\Model\\WidgetSetting' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetSetting.php', 'OCP\\Dashboard\\Model\\WidgetSetup' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetSetup.php', 'OCP\\Dashboard\\Model\\WidgetTemplate' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetTemplate.php', - 'OCP\\Dashboard\\RegisterPanelEvent' => __DIR__ . '/../../..' . '/lib/public/Dashboard/RegisterPanelEvent.php', + 'OCP\\Dashboard\\RegisterWidgetEvent' => __DIR__ . '/../../..' . '/lib/public/Dashboard/RegisterWidgetEvent.php', 'OCP\\Dashboard\\Service\\IEventsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IEventsService.php', 'OCP\\Dashboard\\Service\\IWidgetsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IWidgetsService.php', 'OCP\\Defaults' => __DIR__ . '/../../..' . '/lib/public/Defaults.php', diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index efcf9175b978e9fd02e0305e8eccc197889e05e4..4c37209739e5620e219febc723ce3fa7be3f38d2 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -100,10 +100,10 @@ class RegistrationContext { ); } - public function registerDashboardPanel(string $panelClass): void { + public function registerDashboardWidget(string $widgetClass): void { $this->context->registerDashboardPanel( $this->appId, - $panelClass + $widgetClass ); } @@ -282,7 +282,7 @@ class RegistrationContext { public function delegateDashboardPanelRegistrations(array $apps, IManager $dashboardManager): void { foreach ($this->dashboardPanels as $panel) { try { - $dashboardManager->lazyRegisterPanel($panel['class']); + $dashboardManager->lazyRegisterWidget($panel['class']); } catch (Throwable $e) { $appId = $panel['appId']; $this->logger->logException($e, [ diff --git a/lib/private/Dashboard/Manager.php b/lib/private/Dashboard/Manager.php index 0c285a8b53d1fa3be21d3b21648af0325445efe8..fda4c8b3893179a85c893576d9d5c416b0d367c8 100644 --- a/lib/private/Dashboard/Manager.php +++ b/lib/private/Dashboard/Manager.php @@ -29,7 +29,7 @@ namespace OC\Dashboard; use InvalidArgumentException; use OCP\AppFramework\QueryException; use OCP\Dashboard\IManager; -use OCP\Dashboard\IPanel; +use OCP\Dashboard\IWidget; use OCP\ILogger; use OCP\IServerContainer; use Throwable; @@ -37,10 +37,10 @@ use Throwable; class Manager implements IManager { /** @var array */ - private $lazyPanels = []; + private $lazyWidgets = []; - /** @var IPanel[] */ - private $panels = []; + /** @var IWidget[] */ + private $widgets = []; /** @var IServerContainer */ private $serverContainer; @@ -49,31 +49,31 @@ class Manager implements IManager { $this->serverContainer = $serverContainer; } - private function registerPanel(IPanel $panel): void { - if (array_key_exists($panel->getId(), $this->panels)) { - throw new InvalidArgumentException('Dashboard panel with this id has already been registered'); + private function registerWidget(IWidget $widget): void { + if (array_key_exists($widget->getId(), $this->widgets)) { + throw new InvalidArgumentException('Dashboard widget with this id has already been registered'); } - $this->panels[$panel->getId()] = $panel; + $this->widgets[$widget->getId()] = $widget; } - public function lazyRegisterPanel(string $panelClass): void { - $this->lazyPanels[] = $panelClass; + public function lazyRegisterWidget(string $widgetClass): void { + $this->lazyWidgets[] = $widgetClass; } public function loadLazyPanels(): void { - $classes = $this->lazyPanels; + $classes = $this->lazyWidgets; foreach ($classes as $class) { try { - /** @var IPanel $panel */ - $panel = $this->serverContainer->query($class); + /** @var IWidget $widget */ + $widget = $this->serverContainer->query($class); } catch (QueryException $e) { /* * There is a circular dependency between the logger and the registry, so * we can not inject it. Thus the static call. */ \OC::$server->getLogger()->logException($e, [ - 'message' => 'Could not load lazy dashbaord panel: ' . $e->getMessage(), + 'message' => 'Could not load lazy dashbaord widget: ' . $e->getMessage(), 'level' => ILogger::FATAL, ]); } @@ -82,32 +82,32 @@ class Manager implements IManager { * type, so we might get a TypeError here that we should catch. */ try { - $this->registerPanel($panel); + $this->registerWidget($widget); } catch (Throwable $e) { /* * There is a circular dependency between the logger and the registry, so * we can not inject it. Thus the static call. */ \OC::$server->getLogger()->logException($e, [ - 'message' => 'Could not register lazy dashboard panel: ' . $e->getMessage(), + 'message' => 'Could not register lazy dashboard widget: ' . $e->getMessage(), 'level' => ILogger::FATAL, ]); } try { - $panel->load(); + $widget->load(); } catch (Throwable $e) { \OC::$server->getLogger()->logException($e, [ - 'message' => 'Error during dashboard panel loading: ' . $e->getMessage(), + 'message' => 'Error during dashboard widget loading: ' . $e->getMessage(), 'level' => ILogger::FATAL, ]); } } - $this->lazyPanels = []; + $this->lazyWidgets = []; } - public function getPanels(): array { + public function getWidgets(): array { $this->loadLazyPanels(); - return $this->panels; + return $this->widgets; } } diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php index 82b7abf6c8f04b927878de9a2ae50e574de098ff..ab386ab61727ce8a1856a3fa6440ef193c286045 100644 --- a/lib/private/legacy/OC_Util.php +++ b/lib/private/legacy/OC_Util.php @@ -68,6 +68,7 @@ use OCP\IConfig; use OCP\IGroupManager; use OCP\ILogger; use OCP\IUser; +use OCP\IUserSession; class OC_Util { public static $scripts = []; @@ -1088,6 +1089,8 @@ class OC_Util { * @suppress PhanDeprecatedFunction */ public static function getDefaultPageUrl() { + /** @var IConfig $config */ + $config = \OC::$server->get(IConfig::class); $urlGenerator = \OC::$server->getURLGenerator(); // Deny the redirect if the URL contains a @ // This prevents unvalidated redirects like ?redirect_url=:user@domain.com @@ -1099,8 +1102,16 @@ class OC_Util { $location = $urlGenerator->getAbsoluteURL($defaultPage); } else { $appId = 'files'; - $config = \OC::$server->getConfig(); - $defaultApps = explode(',', $config->getSystemValue('defaultapp', 'files')); + $defaultApps = explode(',', $config->getSystemValue('defaultapp', 'dashboard,files')); + + /** @var IUserSession $userSession */ + $userSession = \OC::$server->get(IUserSession::class); + $user = $userSession->getUser(); + if ($user) { + $userDefaultApps = explode(',', $config->getUserValue($user->getUID(), 'core', 'defaultapp')); + $defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps)); + } + // find the first app that is enabled for the current user foreach ($defaultApps as $defaultApp) { $defaultApp = OC_App::cleanAppId(strip_tags($defaultApp)); diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index 94e3aed17e2e0037c159cacbe7c33b5b9d816587..0acf0c038bbccddca67d8ee506a2870c5eec44d2 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -59,11 +59,11 @@ interface IRegistrationContext { * Register an implementation of \OCP\Dashboard\IPanel that * will handle the implementation of a dashboard panel * - * @param string $panelClass + * @param string $widgetClass * @return void * @since 20.0.0 */ - public function registerDashboardPanel(string $panelClass): void; + public function registerDashboardWidget(string $widgetClass): void; /** * Register a service * diff --git a/lib/public/Dashboard/IManager.php b/lib/public/Dashboard/IManager.php index 985c8c7838f876162caff04162ac7b4025965ae6..81b1bb0dffee7fcc02f026a69350300924fadc88 100644 --- a/lib/public/Dashboard/IManager.php +++ b/lib/public/Dashboard/IManager.php @@ -35,15 +35,15 @@ namespace OCP\Dashboard; interface IManager { /** - * @param string $panelClass + * @param string $widgetClass * @since 20.0.0 */ - public function lazyRegisterPanel(string $panelClass): void; + public function lazyRegisterWidget(string $widgetClass): void; /** * @since 20.0.0 * - * @return IPanel[] + * @return IWidget[] */ - public function getPanels(): array; + public function getWidgets(): array; } diff --git a/lib/public/Dashboard/IPanel.php b/lib/public/Dashboard/IWidget.php similarity index 79% rename from lib/public/Dashboard/IPanel.php rename to lib/public/Dashboard/IWidget.php index 59d88f7a7e991a0d48c33dd2beff260746ab6894..42e2c7df5b20b0c05c2903cfd354965257265c7c 100644 --- a/lib/public/Dashboard/IPanel.php +++ b/lib/public/Dashboard/IWidget.php @@ -27,33 +27,33 @@ declare(strict_types=1); namespace OCP\Dashboard; /** - * Interface IPanel + * Interface IWidget * * @package OCP\Dashboard * @since 20.0.0 */ -interface IPanel { +interface IWidget { /** - * @return string Unique id that identifies the panel, e.g. the app id + * @return string Unique id that identifies the widget, e.g. the app id * @since 20.0.0 */ public function getId(): string; /** - * @return string User facing title of the panel + * @return string User facing title of the widget * @since 20.0.0 */ public function getTitle(): string; /** - * @return int Initial order for panel sorting + * @return int Initial order for widget sorting * @since 20.0.0 */ public function getOrder(): int; /** - * @return string css class that displays an icon next to the panel title + * @return string css class that displays an icon next to the widget title * @since 20.0.0 */ public function getIconClass(): string; @@ -65,7 +65,7 @@ interface IPanel { public function getUrl(): ?string; /** - * Execute panel bootstrap code like loading scripts and providing initial state + * Execute widget bootstrap code like loading scripts and providing initial state * @since 20.0.0 */ public function load(): void; diff --git a/lib/public/Dashboard/RegisterPanelEvent.php b/lib/public/Dashboard/RegisterWidgetEvent.php similarity index 91% rename from lib/public/Dashboard/RegisterPanelEvent.php rename to lib/public/Dashboard/RegisterWidgetEvent.php index 2bd157127fd0c1d5b7d3b1af496673f765303e30..0267a9e0d36ed1f5eba0aacb79552cfb70bbeb78 100644 --- a/lib/public/Dashboard/RegisterPanelEvent.php +++ b/lib/public/Dashboard/RegisterWidgetEvent.php @@ -40,7 +40,7 @@ use OCP\EventDispatcher\Event; * @since 20.0.0 * @deprecated 20.0.0 */ -class RegisterPanelEvent extends Event { +class RegisterWidgetEvent extends Event { private $manager; public function __construct(IManager $manager) { @@ -53,7 +53,7 @@ class RegisterPanelEvent extends Event { * @param string $panelClass * @since 20.0.0 */ - public function registerPanel(string $panelClass) { - $this->manager->lazyRegisterPanel($panelClass); + public function registerWidget(string $panelClass) { + $this->manager->lazyRegisterWidget($panelClass); } } diff --git a/package-lock.json b/package-lock.json index ab643d9beaa63718925b4b974add30841902fb79..70cc0d8d0fc39d9fa60ff41a8d6645cf9ec8d9f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8900,6 +8900,11 @@ } } }, + "sortablejs": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz", + "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==" + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -9963,6 +9968,14 @@ "date-format-parse": "^0.2.5" } }, + "vuedraggable": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.0.tgz", + "integrity": "sha512-IlslPpc+iZ2zPNSJbydFZIDrE+don5u+Nc/bjT2YaF+Azidc+wxxJKfKT0NwE68AKk0syb0YbZneAcnynqREZQ==", + "requires": { + "sortablejs": "^1.10.1" + } + }, "vuex": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz", diff --git a/package.json b/package.json index e8487b19d2c95ab76b201702277f9896761afbb8..37588737a2c92231a7675ee951d410c264eadfa2 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "vue-material-design-icons": "^4.8.0", "vue-multiselect": "^2.1.6", "vue-router": "^3.3.4", + "vuedraggable": "^2.24.0", "vuex": "^3.5.1", "vuex-router-sync": "^5.0.0" }, diff --git a/resources/app-info-shipped.xsd b/resources/app-info-shipped.xsd index c120a524844490eddbf74eba47692dfc17c379a5..90ca881c6866de22b574a508e9982f6495f8087f 100644 --- a/resources/app-info-shipped.xsd +++ b/resources/app-info-shipped.xsd @@ -341,6 +341,7 @@ <xs:simpleType name="category"> <xs:restriction base="xs:string"> + <xs:enumeration value="dashboard"/> <xs:enumeration value="security"/> <xs:enumeration value="customization"/> <xs:enumeration value="files"/> diff --git a/resources/app-info.xsd b/resources/app-info.xsd index ae8b170202d79aae09ba3ffe88ade186a31487a2..7c0f04efb591dbf9766703066d9b5abc7db54eae 100644 --- a/resources/app-info.xsd +++ b/resources/app-info.xsd @@ -341,6 +341,7 @@ <xs:simpleType name="category"> <xs:restriction base="xs:string"> + <xs:enumeration value="dashboard"/> <xs:enumeration value="security"/> <xs:enumeration value="customization"/> <xs:enumeration value="files"/> diff --git a/tests/acceptance/installAndConfigureServer.sh b/tests/acceptance/installAndConfigureServer.sh index d24405fa448f7a6b02fa4d90cc36e0e0352c16ec..99d51e951afd180b123372dff8c4bdc0dcc77d6e 100755 --- a/tests/acceptance/installAndConfigureServer.sh +++ b/tests/acceptance/installAndConfigureServer.sh @@ -39,6 +39,8 @@ OC_PASS=123456acb php occ user:add --password-from-env user1 OC_PASS=123456acb php occ user:add --password-from-env disabledUser php occ user:disable disabledUser +php occ app:disable dashboard + if [ "$NEXTCLOUD_SERVER_DOMAIN" != "" ]; then # Default first trusted domain is "localhost"; replace it with given domain. php occ config:system:set trusted_domains 0 --value="$NEXTCLOUD_SERVER_DOMAIN"