Add custom modal to plugin helpers (#2631)
authorKim <1877318+kimsible@users.noreply.github.com>
Wed, 15 Apr 2020 13:35:41 +0000 (15:35 +0200)
committerGitHub <noreply@github.com>
Wed, 15 Apr 2020 13:35:41 +0000 (15:35 +0200)
* Add custom modal component

* Add custom modal to app and plugins helpers

* Fixes custom modal component

* Add doc for custom modal

* Fix newline end of file html and scss files

* Move my-custom-modal component outside component for UserLoggedIn modals

* Move initializeCustomModal to ngAfterViewInit()

* Wrap events and conditionnals

* Replace ng-show with ngIf*

* Add modalRef to open only one modal + onCloseClick

* Refacto + Fix access methods of custom modal

* Fix methods names custom-modal.component

* Fix implement AfterViewInit & no default boolean

Co-authored-by: kimsible <kimsible@users.noreply.github.com>
client/src/app/app.component.html
client/src/app/app.component.ts
client/src/app/app.module.ts
client/src/app/core/plugins/plugin.service.ts
client/src/app/modal/custom-modal.component.html [new file with mode: 0644]
client/src/app/modal/custom-modal.component.scss [new file with mode: 0644]
client/src/app/modal/custom-modal.component.ts [new file with mode: 0644]
client/src/types/register-client-option.model.ts
support/doc/plugins/guide.md

index d1eb1646f58321a63b80b0ca658462b16784af52..84cff4812606247517beddc0ab6fe86c7be06ebb 100644 (file)
@@ -54,3 +54,5 @@
   <my-welcome-modal #welcomeModal></my-welcome-modal>
   <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal>
 </ng-template>
+
+<my-custom-modal #customModal></my-custom-modal>
index 1d077646cae4f76ceae5d90d480f407882e83f54..12c0efd8a5f9efa09d1dfe6ec5a6ae7d1a8195a4 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnInit, ViewChild } from '@angular/core'
+import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core'
 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
 import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
 import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
@@ -14,6 +14,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
 import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
 import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
+import { CustomModalComponent } from '@app/modal/custom-modal.component'
 import { ServerConfig, UserRole } from '@shared/models'
 import { User } from '@app/shared'
 import { InstanceService } from '@app/shared/instance/instance.service'
@@ -24,9 +25,10 @@ import { MenuService } from './core/menu/menu.service'
   templateUrl: './app.component.html',
   styleUrls: [ './app.component.scss' ]
 })
-export class AppComponent implements OnInit {
+export class AppComponent implements OnInit, AfterViewInit {
   @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent
   @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent
+  @ViewChild('customModal') customModal: CustomModalComponent
 
   customCSS: SafeHtml
 
@@ -87,6 +89,10 @@ export class AppComponent implements OnInit {
     this.openModalsIfNeeded()
   }
 
+  ngAfterViewInit () {
+    this.pluginService.initializeCustomModal(this.customModal)
+  }
+
   isUserLoggedIn () {
     return this.authService.isLoggedIn()
   }
index ef23c965561f5b28d691e06e122ed78ae27cc6b3..5a3b109da48bc34fd1d2e50b2b85d843cb81bb01 100644 (file)
@@ -20,6 +20,7 @@ import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-
 import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '@shared/models'
 import { APP_BASE_HREF } from '@angular/common'
 import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
+import { CustomModalComponent } from '@app/modal/custom-modal.component'
 
 export function metaFactory (serverService: ServerService): MetaLoader {
   return new MetaStaticLoader({
@@ -47,6 +48,7 @@ export function metaFactory (serverService: ServerService): MetaLoader {
     SuggestionsComponent,
     SuggestionComponent,
 
+    CustomModalComponent,
     WelcomeModalComponent,
     InstanceConfigWarningModalComponent
   ],
index aa6823060f648449a961febc00f795166e658eda..b4ed56cbe4723b4dbbdfdf97ff3bbc6aa73000fb 100644 (file)
@@ -20,6 +20,7 @@ import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
 import { RegisterClientHelpers } from '../../../types/register-client-option.model'
 import { PluginTranslation } from '@shared/models/plugins/plugin-translation.model'
 import { importModule } from '@app/shared/misc/utils'
+import { CustomModalComponent } from '@app/modal/custom-modal.component'
 
 interface HookStructValue extends RegisterClientHookOptions {
   plugin: ServerConfigPlugin
@@ -49,6 +50,8 @@ export class PluginService implements ClientHook {
 
   translationsObservable: Observable<PluginTranslation>
 
+  customModal: CustomModalComponent
+
   private plugins: ServerConfigPlugin[] = []
   private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
   private loadedScripts: { [ script: string ]: boolean } = {}
@@ -81,6 +84,10 @@ export class PluginService implements ClientHook {
       })
   }
 
+  initializeCustomModal (customModal: CustomModalComponent) {
+    this.customModal = customModal
+  }
+
   ensurePluginsAreBuilt () {
     return this.pluginsBuilt.asObservable()
                .pipe(first(), shareReplay())
@@ -279,6 +286,16 @@ export class PluginService implements ClientHook {
         success: (text: string, title?: string, timeout?: number) => this.notifier.success(text, title, timeout)
       },
 
+      showModal: (input: {
+        title: string,
+        content: string,
+        close?: boolean,
+        cancel?: { value: string, action?: () => void },
+        confirm?: { value: string, action?: () => void }
+      }) => {
+        this.customModal.show(input)
+      },
+
       translate: (value: string) => {
         return this.translationsObservable
             .pipe(map(allTranslations => allTranslations[npmName]))
diff --git a/client/src/app/modal/custom-modal.component.html b/client/src/app/modal/custom-modal.component.html
new file mode 100644 (file)
index 0000000..06ecc27
--- /dev/null
@@ -0,0 +1,20 @@
+<ng-template #modal let-hide="close">
+  <div class="modal-header">
+    <h4 class="modal-title">{{title}}</h4>
+    <my-global-icon *ngIf="close" iconName="cross" aria-label="Close" role="button" (click)="onCloseClick()"></my-global-icon>
+  </div>
+    
+  <div class="modal-body" [innerHTML]="content"></div>
+
+  <div *ngIf="hasCancel() || hasConfirm()" class="modal-footer inputs">
+    <input
+      *ngIf="hasCancel()" type="button" role="button" value="{{cancel.value}}" class="action-button action-button-cancel"
+      (click)="onCancelClick()" (key.enter)="onCancelClick()"
+    >
+
+    <input
+      *ngIf="hasConfirm()" type="button" role="button" value="{{confirm.value}}" class="action-button action-button-confirm"
+      (click)="onConfirmClick()" (key.enter)="onConfirmClick()"
+    >
+  </div>
+</ng-template>
diff --git a/client/src/app/modal/custom-modal.component.scss b/client/src/app/modal/custom-modal.component.scss
new file mode 100644 (file)
index 0000000..a7fa30c
--- /dev/null
@@ -0,0 +1,20 @@
+@import '_mixins';
+@import '_variables';
+
+.modal-body {
+  font-size: 15px;
+}
+
+li {
+  margin-bottom: 10px;
+}
+
+.action-button-cancel {
+  @include peertube-button;
+  @include grey-button;
+}
+
+.action-button-confirm {
+  @include peertube-button;
+  @include orange-button;
+}
diff --git a/client/src/app/modal/custom-modal.component.ts b/client/src/app/modal/custom-modal.component.ts
new file mode 100644 (file)
index 0000000..a985790
--- /dev/null
@@ -0,0 +1,93 @@
+import { Component, ElementRef, ViewChild, Input } from '@angular/core'
+import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
+
+@Component({
+  selector: 'my-custom-modal',
+  templateUrl: './custom-modal.component.html',
+  styleUrls: [ './custom-modal.component.scss' ]
+})
+export class CustomModalComponent {
+  @ViewChild('modal', { static: true }) modal: ElementRef
+
+  @Input() title: string
+  @Input() content: string
+  @Input() close?: boolean
+  @Input() cancel?: { value: string, action?: () => void }
+  @Input() confirm?: { value: string, action?: () => void }
+
+  private modalRef: NgbModalRef
+
+  constructor (
+    private modalService: NgbModal
+  ) { }
+
+  show (input: {
+    title: string,
+    content: string,
+    close?: boolean,
+    cancel?: { value: string, action?: () => void },
+    confirm?: { value: string, action?: () => void }
+  }) {
+    if (this.modalRef instanceof NgbModalRef && this.modalService.hasOpenModals()) {
+      console.error('Cannot open another custom modal, one is already opened.')
+      return
+    }
+
+    const { title, content, close, cancel, confirm } = input
+
+    this.title = title
+    this.content = content
+    this.close = close
+    this.cancel = cancel
+    this.confirm = confirm
+
+    this.modalRef = this.modalService.open(this.modal, {
+      centered: true,
+      backdrop: 'static',
+      keyboard: false,
+      size: 'lg'
+    })
+  }
+
+  onCancelClick () {
+    this.modalRef.close()
+
+    if (typeof this.cancel.action === 'function') {
+      this.cancel.action()
+    }
+
+    this.destroy()
+  }
+
+  onCloseClick () {
+    this.modalRef.close()
+    this.destroy()
+  }
+
+  onConfirmClick () {
+    this.modalRef.close()
+
+    if (typeof this.confirm.action === 'function') {
+      this.confirm.action()
+    }
+
+    this.destroy()
+  }
+
+  hasCancel () {
+    return typeof this.cancel !== 'undefined'
+  }
+
+  hasConfirm () {
+    return typeof this.confirm !== 'undefined'
+  }
+
+  private destroy () {
+    delete this.modalRef
+    delete this.title
+    delete this.content
+    delete this.close
+    delete this.cancel
+    delete this.confirm
+  }
+}
index b64652a0ff8ca4bd99a6ad13c9bc9527bd214997..1c235107a37cc622b9fe35a375c51f0b5086ae84 100644 (file)
@@ -19,5 +19,13 @@ export type RegisterClientHelpers = {
     success: (text: string, title?: string, timeout?: number) => void
   }
 
+  showModal: (input: {
+    title: string,
+    content: string,
+    close?: boolean,
+    cancel?: { value: string, action?: () => void },
+    confirm?: { value: string, action?: () => void }
+  }) => void
+
   translate: (toTranslate: string) => Promise<string>
 }
index 5251ce48a807843ccdefefd45a7c0a97bcc3ed0e..e6870ce173c864fed8ca0f9a6ab4e08cb0cac687 100644 (file)
@@ -216,6 +216,24 @@ notifier.success('Success message content.')
 notifier.error('Error message content.')
 ```
 
+#### Custom Modal
+
+To show a custom modal:
+
+```js
+ peertubeHelpers.showModal({
+   title: 'My custom modal title',
+   content: '<p>My custom modal content</p>',
+   // Optionals parameters :
+   // show close icon
+   close: true,
+   // show cancel button and call action() after hiding modal
+   cancel: { value: 'cancel', action: () => {} },
+   // show confirm button and call action() after hiding modal
+   confirm: { value: 'confirm', action: () => {} },
+ })
+```
+
 #### Translate
 
 You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file):