From 8dd0a60a70b4a88f2c0bb9d06e54ae9fbc2e6887 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pete=20An=C3=B8ther?= <develop.pit@gmail.com>
Date: Tue, 5 Mar 2024 02:39:49 -0300
Subject: [PATCH] EZQMS-562: Introduced reusable `NotificationToast` component
 (#4873)

Signed-off-by: Petr Vyazovetskiy <develop.pit@gmail.com>
---
 .../src/components/NotificationToast.svelte   | 112 ++++++++++++++++
 packages/ui/src/index.ts                      |   1 +
 .../issues/IssueNotification.svelte           | 122 ++++--------------
 3 files changed, 140 insertions(+), 95 deletions(-)
 create mode 100644 packages/ui/src/components/NotificationToast.svelte

diff --git a/packages/ui/src/components/NotificationToast.svelte b/packages/ui/src/components/NotificationToast.svelte
new file mode 100644
index 0000000000..aba306a501
--- /dev/null
+++ b/packages/ui/src/components/NotificationToast.svelte
@@ -0,0 +1,112 @@
+<!--
+//
+// Copyright © 2024 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+-->
+
+<script lang="ts">
+  import type { ComponentType } from 'svelte'
+  import { fade } from 'svelte/transition'
+  import Button from './Button.svelte'
+  import Icon from './Icon.svelte'
+  import CheckCircle from './icons/CheckCircle.svelte'
+  import Close from './icons/Close.svelte'
+  import Info from './icons/Info.svelte'
+  import { NotificationSeverity } from './notifications/NotificationSeverity'
+
+  export let onClose: (() => void) | undefined = undefined
+  export let severity: NotificationSeverity | undefined = undefined
+  export let title: string
+
+  const getIcon = (): ComponentType | undefined => {
+    switch (severity) {
+      case NotificationSeverity.Success:
+        return CheckCircle
+      case NotificationSeverity.Error:
+      case NotificationSeverity.Info:
+      case NotificationSeverity.Warning:
+        return Info
+    }
+  }
+
+  $: icon = getIcon()
+</script>
+
+<div class="root" in:fade out:fade>
+  <div class="flex-between">
+    <div class="flex-row-center">
+      {#if icon}
+        <div
+          class="mr-2"
+          class:icon-success={severity === NotificationSeverity.Success}
+          class:icon-error={severity === NotificationSeverity.Error}
+          class:icon-info={severity === NotificationSeverity.Info}
+          class:icon-warning={severity === NotificationSeverity.Warning}
+        >
+          <Icon {icon} size="medium" />
+        </div>
+      {/if}
+      <span class="overflow-label fs-bold text-base caption-color">{title}</span>
+    </div>
+    {#if onClose !== undefined}
+      <Button icon={Close} kind="ghost" size="small" on:click={onClose} />
+    {/if}
+  </div>
+
+  <div class="content">
+    <slot name="content" />
+  </div>
+
+  {#if $$slots.buttons}
+    <div class="flex-between gap-2">
+      <slot name="buttons" />
+    </div>
+  {/if}
+</div>
+
+<style lang="scss">
+  .root {
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    margin: 0.75rem;
+    padding: 0.5rem;
+    min-width: 25rem;
+    max-width: 35rem;
+    min-height: 7rem;
+    color: var(--theme-caption-color);
+    background-color: var(--theme-popup-color);
+    border: 1px solid var(--theme-popup-divider);
+    border-radius: 0.5rem;
+    box-shadow: var(--theme-popup-shadow);
+
+    .icon-success {
+      color: var(--theme-won-color);
+    }
+    .icon-error {
+      color: var(--theme-lost-color);
+    }
+    .icon-info {
+      color: var(--primary-color-skyblue);
+    }
+    .icon-warning {
+      color: var(--theme-warning-color);
+    }
+
+    .content {
+      flex-grow: 1;
+      margin: 1rem 0 1.25rem;
+    }
+  }
+</style>
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index feab0dc87b..9ba0b1a78d 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -140,6 +140,7 @@ export { default as NavItem } from './components/NavItem.svelte'
 export { default as NavGroup } from './components/NavGroup.svelte'
 export { default as Modal } from './components/Modal.svelte'
 export { default as AccordionItem } from './components/AccordionItem.svelte'
+export { default as NotificationToast } from './components/NotificationToast.svelte'
 
 export { default as IconAdd } from './components/icons/Add.svelte'
 export { default as IconCircleAdd } from './components/icons/CircleAdd.svelte'
diff --git a/plugins/tracker-resources/src/components/issues/IssueNotification.svelte b/plugins/tracker-resources/src/components/issues/IssueNotification.svelte
index 84682fb04f..69672f6812 100644
--- a/plugins/tracker-resources/src/components/issues/IssueNotification.svelte
+++ b/plugins/tracker-resources/src/components/issues/IssueNotification.svelte
@@ -2,20 +2,8 @@
   import { getMetadata } from '@hcengineering/platform'
   import presentation, { copyTextToClipboard, createQuery } from '@hcengineering/presentation'
   import { Issue, IssueStatus } from '@hcengineering/tracker'
-  import {
-    AnySvelteComponent,
-    Button,
-    Icon,
-    IconCheckCircle,
-    IconClose,
-    IconInfo,
-    Notification,
-    NotificationSeverity,
-    navigate,
-    parseLocation
-  } from '@hcengineering/ui'
+  import { Button, Notification, navigate, parseLocation, NotificationToast } from '@hcengineering/ui'
   import view from '@hcengineering/view'
-  import { fade } from 'svelte/transition'
 
   import { statusStore } from '@hcengineering/view-resources'
   import tracker from '../../plugin'
@@ -27,10 +15,10 @@
 
   const issueQuery = createQuery()
 
-  let issue: Issue | undefined
-  let status: IssueStatus | undefined
+  let issue: Issue | undefined = undefined
+  let status: IssueStatus | undefined = undefined
 
-  const { title, subTitle, severity, params } = notification
+  const { subTitle, params } = notification
 
   $: issueQuery.query(
     tracker.class.Issue,
@@ -45,30 +33,7 @@
     status = $statusStore.byId.get(issue.status)
   }
 
-  const getIcon = (): AnySvelteComponent | undefined => {
-    switch (severity) {
-      case NotificationSeverity.Success:
-        return IconCheckCircle
-      case NotificationSeverity.Error:
-      case NotificationSeverity.Info:
-      case NotificationSeverity.Warning:
-        return IconInfo
-    }
-  }
-
-  const getIconColor = () => {
-    switch (severity) {
-      case NotificationSeverity.Success:
-        return '#34db80'
-      case NotificationSeverity.Error:
-        return '#eb5757'
-      case NotificationSeverity.Info:
-        return '#93caf3'
-      case NotificationSeverity.Warning:
-        return '#f2994a'
-    }
-  }
-  const handleIssueOpened = () => {
+  function handleIssueOpened (): void {
     if (params?.issueUrl) {
       const url = new URL(params?.issueUrl)
       const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
@@ -80,67 +45,34 @@
 
     onRemove()
   }
-  const handleCopyUrl = () => {
-    if (issue) {
-      copyTextToClipboard(params?.issueUrl)
+
+  function handleCopyUrl (): void {
+    if (issue === undefined) {
+      void copyTextToClipboard(params?.issueUrl)
     }
   }
-  $: icon = getIcon()
 </script>
 
-<div class="notify-container" in:fade out:fade>
-  <div class="flex-between">
-    <div class="flex-row-center">
-      {#if icon}
-        <div class="mr-2"><Icon {icon} size="medium" fill={getIconColor()} /></div>
+<NotificationToast title={notification.title} severity={notification.severity} onClose={onRemove}>
+  <svelte:fragment slot="content">
+    <div class="flex-row-center flex-wrap gap-2 reverse">
+      {#if status === undefined && issue}
+        <IssueStatusIcon value={status} space={issue.space} size="small" />
       {/if}
-      <span class="overflow-label fs-bold caption-color">{title}</span>
+      {#if issue}
+        <IssuePresenter value={issue} />
+      {/if}
+      <span class="overflow-label">
+        {subTitle}
+      </span>
+      <span class="content-dark-color">
+        {params?.subTitlePostfix}
+      </span>
     </div>
-    <Button icon={IconClose} kind="ghost" size="small" on:click={onRemove} />
-  </div>
+  </svelte:fragment>
 
-  <div class="content flex-row-center flex-wrap gap-2 reverse">
-    {#if status && issue}
-      <IssueStatusIcon value={status} space={issue.space} size="small" />
-    {/if}
-    {#if issue}
-      <IssuePresenter value={issue} />
-    {/if}
-    <span class="overflow-label">
-      {subTitle}
-    </span>
-    <span class="content-dark-color">
-      {params?.subTitlePostfix}
-    </span>
-  </div>
-  <div class="flex-between gap-2">
+  <svelte:fragment slot="buttons">
     <Button label={tracker.string.ViewIssue} on:click={handleIssueOpened} />
     <Button icon={view.icon.CopyLink} label={tracker.string.CopyIssueUrl} on:click={handleCopyUrl} />
-  </div>
-</div>
-
-<style lang="scss">
-  .notify-container {
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-    margin: 0.75rem;
-    padding: 0.5rem;
-    min-width: 25rem;
-    max-width: 35rem;
-    min-height: 7rem;
-    color: var(--theme-caption-color);
-    background-color: var(--theme-popup-color);
-    border: 1px solid var(--theme-popup-divider);
-    border-radius: 0.5rem;
-    box-shadow: var(--theme-popup-shadow);
-
-    .content {
-      flex-grow: 1;
-      margin: 1rem 0 1.25rem;
-      padding: 0 1rem;
-      // border: 1px solid var(--theme-divider-color);
-      border-radius: 0.5rem;
-    }
-  }
-</style>
+  </svelte:fragment>
+</NotificationToast>