diff --git a/addon/components/chat-container.hbs b/addon/components/chat-container.hbs index 26819fe..34d0446 100644 --- a/addon/components/chat-container.hbs +++ b/addon/components/chat-container.hbs @@ -1,4 +1,4 @@ -
+
{{#each this.chat.openChannels as |chatChannel|}} {{/each}} diff --git a/addon/components/chat-tray.hbs b/addon/components/chat-tray.hbs index 12639e7..8b03404 100644 --- a/addon/components/chat-tray.hbs +++ b/addon/components/chat-tray.hbs @@ -4,13 +4,13 @@ @defaultClass={{@wrapperClass}} @onOpen={{this.unlockAudio}} @onClose={{@onClose}} + @calculatePosition={{this.calculatePosition}} @verticalPosition={{@verticalPosition}} @horizontalPosition={{@horizontalPosition}} - @renderInPlace={{true}} - @initiallyOpened={{@initiallyOpened}} + @renderInPlace={{or @renderInPlace (not (media "isMobile"))}} as |dd| > - +
{{#if this.unreadCount}} @@ -18,7 +18,7 @@ {{/if}}
- +
+ {{/if}} +
+
+ +
+ + {{#if this.dashboard.isAddingWidget}} + + + + {{/if}} +
\ No newline at end of file diff --git a/addon/components/dashboard.js b/addon/components/dashboard.js new file mode 100644 index 0000000..c7984c0 --- /dev/null +++ b/addon/components/dashboard.js @@ -0,0 +1,113 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; + +/** + * DashboardComponent for managing dashboards in an Ember application. + * This component handles actions such as selecting, creating, deleting dashboards, + * and managing widget selectors and dashboard editing states. + * + * @extends Component + */ +export default class DashboardComponent extends Component { + @service store; + @service intl; + @service notifications; + @service modalsManager; + @service fetch; + @service dashboard; + @service universe; + + /** + * Creates an instance of DashboardComponent. + * @memberof DashboardComponent + */ + constructor(owner, { defaultDashboardId = 'dashboard', defaultDashboardName = 'Default Dashboard', showPanelWhenZeroWidgets = false }) { + super(...arguments); + this.dashboard.showPanelWhenZeroWidgets = showPanelWhenZeroWidgets; + this.dashboard.loadDashboards.perform(defaultDashboardId, defaultDashboardName); + } + + /** + * Action to select a dashboard. + * @param {Object} dashboard - The dashboard to be selected. + */ + @action selectDashboard(dashboard) { + this.dashboard.selectDashboard.perform(dashboard); + } + + /** + * Sets the context for the widget selector panel. + * @param {Object} widgetSelectorContext - The context object for the widget selector. + */ + @action setWidgetSelectorPanelContext(widgetSelectorContext) { + this.widgetSelectorContext = widgetSelectorContext; + } + + /** + * Creates a new dashboard. + * @param {Object} dashboard - The dashboard to be created. + * @param {Object} [options={}] - Optional parameters for dashboard creation. + */ + @action createDashboard(dashboard, options = {}) { + this.modalsManager.show('modals/create-dashboard', { + title: this.intl.t('component.dashboard.create-a-new-dashboard'), + acceptButtonText: this.intl.t('component.dashboard.confirm-create-dashboard'), + confirm: async (modal, done) => { + modal.startLoading(); + + // Get the name from the modal options + const { name } = modal.getOptions(); + + await this.dashboard.createDashboard.perform(name); + done(); + }, + ...options, + }); + } + + /** + * Deletes a dashboard. + * @param {Object} dashboard - The dashboard to be deleted. + * @param {Object} [options={}] - Optional parameters for dashboard deletion. + */ + @action deleteDashboard(dashboard, options = {}) { + if (this.dashboard.dashboards?.length === 1) { + return this.notifications.error(this.intl.t('component.dashboard.you-cannot-delete-this-dashboard')); + } + + this.modalsManager.confirm({ + title: this.intl.t('component.dashboard.are-you-sure-you-want-delete-dashboard', { dashboardName: dashboard.name }), + confirm: async (modal, done) => { + modal.startLoading(); + await this.dashboard.deleteDashboard.perform(dashboard); + done(); + }, + ...options, + }); + } + + /** + * Action to handle the addition of a widget. + * @param {boolean} [state=true] - The state to set for adding a widget. + */ + @action onAddingWidget(state = true) { + this.dashboard.onAddingWidget(state); + } + + /** + * Sets the current dashboard. + * @param {Object} dashboard - The dashboard to be set as current. + */ + @action setCurrentDashboard(dashboard) { + this.dashboard.setCurrentDashboard.perform(dashboard); + } + + /** + * Changes the editing state of the dashboard. + * @param {boolean} [state=true] - The state to set for editing the dashboard. + */ + @action onChangeEdit(state = true) { + this.dashboard.onChangeEdit(state); + } +} diff --git a/addon/components/dashboard/create.hbs b/addon/components/dashboard/create.hbs new file mode 100644 index 0000000..e197f83 --- /dev/null +++ b/addon/components/dashboard/create.hbs @@ -0,0 +1,16 @@ +
+ + {{#each @dashboard.widgets as |widget|}} + {{#if (component-resolvable widget.component)}} + + {{component widget.component options=widget.options}} + {{#if @isEdit}} +
+
+ {{/if}} +
+ {{/if}} + {{/each}} +
+
\ No newline at end of file diff --git a/addon/components/dashboard/create.js b/addon/components/dashboard/create.js new file mode 100644 index 0000000..cbad60c --- /dev/null +++ b/addon/components/dashboard/create.js @@ -0,0 +1,99 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action, computed } from '@ember/object'; +import { inject as service } from '@ember/service'; + +/** + * Component responsible for creating and managing the dashboard layout. + * Provides functionalities such as toggling widget float, changing grid layout, and removing widgets. + * + * @extends Component + */ +export default class DashboardCreateComponent extends Component { + /** + * Notifications service for displaying alerts or errors. + * @type {Service} + */ + @service notifications; + + /** + * Tracked array to keep track of widgets that have been updated. + * @type {Array} + */ + @tracked updatedWidgets = []; + + /** + * Action to toggle the floating state of widgets on the grid. + */ + @action toggleFloat() { + this.shouldFloat = !this.shouldFloat; + } + + /** + * Handles changes to the grid layout, such as repositioning or resizing widgets. + * Iterates over each widget event detail and updates the corresponding widget's properties if necessary. + * + * @param {Event} event - Event containing details about the grid change. + * @action + */ + @action onChangeGrid(event) { + const { dashboard } = this.args; + + event.detail.forEach((currentWidgetEvent) => { + const alreadyUpdated = this.updatedWidgets.find((item) => item.id === currentWidgetEvent.id); + if (alreadyUpdated || !this.dashboard) { + return; + } + + const changedWidget = dashboard.widgets.find((widget) => widget.id === currentWidgetEvent.id); + if (!changedWidget) { + return; + } + + const { x, y, w, h } = currentWidgetEvent; + const response = changedWidget.updateProperties({ + grid_options: { x, y, w, h }, + }); + if (response) { + this.updatedWidgets.push(changedWidget); + } + }); + } + + /** + * Removes a specified widget from the dashboard. + * Performs a removal operation on the dashboard and handles any errors that occur during the process. + * + * @param {Object} widget - The widget object to be removed. + * @action + */ + @action removeWidget(widget) { + const { dashboard } = this.args; + + if (dashboard) { + dashboard.removeWidget(widget.id).catch((error) => { + this.notifications.serverError(error); + }); + } + } + + /** + * Computed property that returns grid options based on the current edit state. + * Configures grid behavior such as floating, animation, and drag and resize capabilities. + * + * @computed + * @returns {Object} An object containing grid configuration options. + */ + @computed('args.isEdit') get gridOptions() { + return { + float: true, + animate: true, + acceptWidgets: true, + alwaysShowResizeHandle: this.args.isEdit, + disableDrag: !this.args.isEdit, + disableResize: !this.args.isEdit, + resizable: { handles: 'all' }, + cellHeight: 30, + }; + } +} diff --git a/addon/components/dashboard/widget-panel.hbs b/addon/components/dashboard/widget-panel.hbs new file mode 100644 index 0000000..e6e7969 --- /dev/null +++ b/addon/components/dashboard/widget-panel.hbs @@ -0,0 +1,31 @@ + + +
+
+
+ + +
+ {{#each this.availableWidgets as |widget|}} +
+
+
+ +
+

+ {{t "component.dashboard-widget-panel.widget-name" widgetName=widget.name}} +

+
+
+

{{widget.description}}

+
+
+ {{/each}} +
+ +
+
\ No newline at end of file diff --git a/addon/components/dashboard/widget-panel.js b/addon/components/dashboard/widget-panel.js new file mode 100644 index 0000000..882e62b --- /dev/null +++ b/addon/components/dashboard/widget-panel.js @@ -0,0 +1,60 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class DashboardWidgetPanelComponent extends Component { + @service universe; + @tracked availableWidgets = []; + @tracked dashboard; + @tracked isOpen = true; + @service notifications; + + /** + * Constructs the component and applies initial state. + */ + constructor(owner, { dashboard, defaultDashboardId = 'dashboard' }) { + super(...arguments); + + this.availableWidgets = this.universe.getWidgets(defaultDashboardId); + this.dashboard = dashboard; + } + + /** + * Sets the overlay context. + * + * @action + * @param {OverlayContextObject} overlayContext + */ + @action setOverlayContext(overlayContext) { + this.context = overlayContext; + + if (typeof this.args.onLoad === 'function') { + this.args.onLoad(...arguments); + } + } + + @action addWidgetToDashboard(widget) { + // If widget is a component definition/class + if (typeof widget.component === 'function') { + widget.component = widget.component.name; + } + + this.args.dashboard.addWidget(widget).catch((error) => { + this.notifications.serverError(error); + }); + } + + /** + * Handles cancel button press. + * + * @action + */ + @action onPressClose() { + this.isOpen = false; + + if (typeof this.args.onClose === 'function') { + this.args.onClose(); + } + } +} diff --git a/addon/components/layout/header.hbs b/addon/components/layout/header.hbs index 7c41ba5..bc22f8e 100644 --- a/addon/components/layout/header.hbs +++ b/addon/components/layout/header.hbs @@ -3,7 +3,7 @@ - {{#if @showSidebarToggle}} + {{#if (and @showSidebarToggle (not (media "isMobile")))}} {{/if}} {{#unless (media "isMobile")}} diff --git a/addon/components/layout/header.js b/addon/components/layout/header.js index 6d0156e..987e64c 100644 --- a/addon/components/layout/header.js +++ b/addon/components/layout/header.js @@ -40,7 +40,7 @@ export default class LayoutHeaderComponent extends Component { const visibleMenuItems = []; for (let i = 0; i < headerMenuItems.length; i++) { const menuItem = headerMenuItems[i]; - if (this.abilities.can(`${menuItem.slug} see extension`)) { + if (this.abilities.can(`${menuItem.id} see extension`)) { visibleMenuItems.pushObject(menuItem); } } @@ -274,6 +274,7 @@ export default class LayoutHeaderComponent extends Component { href: 'javascript:;', text: 'Logout', action: 'invalidateSession', + icon: 'person-running', }, ]); diff --git a/addon/components/layout/header/dropdown.hbs b/addon/components/layout/header/dropdown.hbs index 0c41576..a0d17db 100644 --- a/addon/components/layout/header/dropdown.hbs +++ b/addon/components/layout/header/dropdown.hbs @@ -1,18 +1,19 @@ -
+
- + {{yield dd}} - +
{{#each @items as |item|}} diff --git a/addon/components/layout/header/dropdown.js b/addon/components/layout/header/dropdown.js index e247cfd..af90342 100644 --- a/addon/components/layout/header/dropdown.js +++ b/addon/components/layout/header/dropdown.js @@ -1,7 +1,10 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import calculatePosition from 'ember-basic-dropdown/utils/calculate-position'; export default class LayoutHeaderDropdownComponent extends Component { + @service media; @action onAction(dd, action, ...params) { if (typeof dd?.actions?.close === 'function') { dd.actions.close(); @@ -15,4 +18,24 @@ export default class LayoutHeaderDropdownComponent extends Component { this.args[action](...params); } } + + /** + * Calculate dropdown content position. + * + * @param {HTMLElement} trigger + * @param {HTMLElement} content + * @return {Object} + * @memberof LayoutHeaderDropdownComponent + */ + @action calculatePosition(trigger, content) { + if (this.media.isMobile) { + content.classList.add('is-mobile'); + const triggerRect = trigger.getBoundingClientRect(); + const top = triggerRect.height + triggerRect.top; + + return { style: { left: '0px', right: '0px', top, padding: '0 0.5rem', width: '100%' } }; + } + + return calculatePosition(...arguments); + } } diff --git a/addon/components/layout/header/dropdown/item.js b/addon/components/layout/header/dropdown/item.js index f60cb5b..b6990e7 100644 --- a/addon/components/layout/header/dropdown/item.js +++ b/addon/components/layout/header/dropdown/item.js @@ -4,27 +4,29 @@ import { computed, action } from '@ember/object'; import { isBlank } from '@ember/utils'; import { bool } from '@ember/object/computed'; import isMenuItemActive from '../../../../utils/is-menu-item-active'; +import isEmptyObject from '../../../../utils/is-empty-object'; export default class LayoutHeaderDropdownItemComponent extends Component { @service router; @service hostRouter; + @service abilities; @bool('args.item.onClick') isInteractive; @bool('args.item.href') isAnchor; @bool('args.item.seperator') isSeperator; @computed('args.item.{route,onClick}') get isLink() { - return typeof this.args.item.route === 'string' && typeof this.args.item.onClick !== 'function'; + return this.args.item && typeof this.args.item.route === 'string' && typeof this.args.item.onClick !== 'function'; } @computed('args.item.{component,onClick}') get isComponent() { - return typeof this.args.item.component === 'string' && typeof this.args.item.onClick !== 'function'; + return this.args.item && typeof this.args.item.component === 'string' && typeof this.args.item.onClick !== 'function'; } @computed('args.item.text', 'isAnchor', 'isLink', 'isComponent', 'isSeperator', 'isInteractive') get isTextOnly() { const { isAnchor, isLink, isComponent, isSeperator, isInteractive } = this; - const { text } = this.args.item; + const { text } = this.args.item ?? { text: null }; return [isAnchor, isLink, isComponent, isSeperator, isInteractive].every((prop) => prop === false) && text; } @@ -44,10 +46,26 @@ export default class LayoutHeaderDropdownItemComponent extends Component { } @action onClick(event) { - const { url, target, route, model, onClick, options } = this.args; + const { url, target, route, model, onClick, permission, options = {}, queryParams = {} } = this.args; + if (permission && this.abilities.cannot(permission)) { + return; + } + + const hasTransitionOptions = !isEmptyObject(options); + const hasQueryParams = !isEmptyObject(queryParams); + const modelHasQueryParams = !isEmptyObject(model) && model.queryParams !== undefined; const router = this.getRouter(); const anchor = event.target?.closest('a'); + if (hasQueryParams) { + options.queryParams = queryParams; + } + + if (modelHasQueryParams) { + options.queryParams = model.queryParams; + delete model.queryParams; + } + if (anchor && anchor.attributes?.disabled && anchor.attributes.disabled !== 'disabled="false"') { return; } @@ -64,11 +82,11 @@ export default class LayoutHeaderDropdownItemComponent extends Component { return onClick(); } - if (!isBlank(options) && route && model) { + if (hasTransitionOptions && route && model) { return router.transitionTo(route, model, options); } - if (!isBlank(options) && route) { + if (hasTransitionOptions && route && !model) { return router.transitionTo(route, options); } diff --git a/addon/components/layout/mobile-navbar.hbs b/addon/components/layout/mobile-navbar.hbs index d72b233..968d96f 100644 --- a/addon/components/layout/mobile-navbar.hbs +++ b/addon/components/layout/mobile-navbar.hbs @@ -2,7 +2,7 @@