mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-26 13:47:26 +03:00
TSK-212: add notification on issue created (#2325)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
bfcbb73bc7
commit
15c2aa802b
12
packages/ui/src/components/icons/CheckCircle.svelte
Normal file
12
packages/ui/src/components/icons/CheckCircle.svelte
Normal file
@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" viewBox="0 0 16 16" {fill} xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM11.7836 6.42901C12.0858 6.08709 12.0695 5.55006 11.7472 5.22952C11.4248 4.90897 10.9186 4.9263 10.6164 5.26821L7.14921 9.19122L5.3315 7.4773C5.00127 7.16593 4.49561 7.19748 4.20208 7.54777C3.90855 7.89806 3.93829 8.43445 4.26852 8.74581L6.28032 10.6427C6.82041 11.152 7.64463 11.1122 8.13886 10.553L11.7836 6.42901Z"
|
||||
/>
|
||||
</svg>
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let size: 'tiny' | 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
|
26
packages/ui/src/components/notifications/Notification.svelte
Normal file
26
packages/ui/src/components/notifications/Notification.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
import { Notification } from './Notification'
|
||||
import store from './store'
|
||||
|
||||
export let notification: Notification
|
||||
|
||||
const { id, closeTimeout } = notification
|
||||
|
||||
const removeNotificationHandler = () => store.removeNotification(id)
|
||||
|
||||
let timeout: number | null = null
|
||||
|
||||
if (closeTimeout) {
|
||||
timeout = setTimeout(removeNotificationHandler, closeTimeout)
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (closeTimeout && timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:component this={notification.component} {notification} onRemove={removeNotificationHandler} />
|
15
packages/ui/src/components/notifications/Notification.ts
Normal file
15
packages/ui/src/components/notifications/Notification.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { NotificationPosition } from './NotificationPosition'
|
||||
import { NotificationSeverity } from './NotificationSeverity'
|
||||
import { AnyComponent } from '../../types'
|
||||
|
||||
export interface Notification {
|
||||
id: string
|
||||
title: string
|
||||
component: AnyComponent
|
||||
subTitle?: string
|
||||
subTitlePostfix?: string
|
||||
position: NotificationPosition
|
||||
severity?: NotificationSeverity
|
||||
params?: { [key: string]: any }
|
||||
closeTimeout?: number
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export enum NotificationPosition {
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
TopLeft,
|
||||
TopRight
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export enum NotificationSeverity {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import Notification from './Notification.svelte'
|
||||
import { NotificationPosition } from './NotificationPosition'
|
||||
import store from './store'
|
||||
|
||||
const positionByClassName = {
|
||||
'bottom-left': NotificationPosition.BottomLeft,
|
||||
'bottom-right': NotificationPosition.BottomRight,
|
||||
'top-left': NotificationPosition.TopLeft,
|
||||
'top-right': NotificationPosition.TopRight
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
<div class="notifications">
|
||||
{#each Object.entries(positionByClassName) as [className, position]}
|
||||
<div class={className} style:z-index={9999}>
|
||||
{#each $store as notification (notification.id)}
|
||||
{#if notification.position === position}
|
||||
<Notification {notification} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.top-left {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.bottom-left {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.bottom-right {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
34
packages/ui/src/components/notifications/actions.ts
Normal file
34
packages/ui/src/components/notifications/actions.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Writable } from 'svelte/store'
|
||||
import { generateId } from '@hcengineering/core'
|
||||
|
||||
import { Notification } from './Notification'
|
||||
import { NotificationPosition } from './NotificationPosition'
|
||||
import { NotificationSeverity } from './NotificationSeverity'
|
||||
|
||||
export const addNotification = (notification: Notification, store: Writable<Notification[]>): void => {
|
||||
if (notification === undefined || notification === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { update } = store
|
||||
|
||||
const newNotification = {
|
||||
severity: NotificationSeverity.Info,
|
||||
...notification,
|
||||
id: generateId()
|
||||
}
|
||||
|
||||
update((notifications: Notification[]) =>
|
||||
[NotificationPosition.TopRight, NotificationPosition.TopLeft].includes(newNotification.position)
|
||||
? [newNotification, ...notifications]
|
||||
: [...notifications, newNotification]
|
||||
)
|
||||
}
|
||||
|
||||
export const removeNotification = (notificationId: string, { update }: Writable<Notification[]>): void => {
|
||||
if (notificationId === '') {
|
||||
return
|
||||
}
|
||||
|
||||
update((notifications) => notifications.filter(({ id }) => id !== notificationId))
|
||||
}
|
12
packages/ui/src/components/notifications/store.ts
Normal file
12
packages/ui/src/components/notifications/store.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
import { Notification } from './Notification'
|
||||
import { addNotification, removeNotification } from './actions'
|
||||
|
||||
const store = writable<Notification[]>([])
|
||||
|
||||
export default {
|
||||
subscribe: store.subscribe,
|
||||
addNotification: (notification: Notification) => addNotification(notification, store),
|
||||
removeNotification: (notificationId: string) => removeNotification(notificationId, store)
|
||||
}
|
@ -135,6 +135,7 @@ export { default as IconDetailsFilled } from './components/icons/DetailsFilled.s
|
||||
export { default as IconScale } from './components/icons/Scale.svelte'
|
||||
export { default as IconScaleFull } from './components/icons/ScaleFull.svelte'
|
||||
export { default as IconOpen } from './components/icons/Open.svelte'
|
||||
export { default as IconCheckCircle } from './components/icons/CheckCircle.svelte'
|
||||
|
||||
export { default as PanelInstance } from './components/PanelInstance.svelte'
|
||||
export { default as Panel } from './components/Panel.svelte'
|
||||
@ -148,6 +149,11 @@ export { default as ListView } from './components/ListView.svelte'
|
||||
export { default as ToggleButton } from './components/ToggleButton.svelte'
|
||||
export { default as ExpandCollapse } from './components/ExpandCollapse.svelte'
|
||||
export { default as BarDashboard } from './components/BarDashboard.svelte'
|
||||
export { default as Notifications } from './components/notifications/Notifications.svelte'
|
||||
export { default as notificationsStore } from './components/notifications/store'
|
||||
export { NotificationPosition } from './components/notifications/NotificationPosition'
|
||||
export { NotificationSeverity } from './components/notifications/NotificationSeverity'
|
||||
export { Notification } from './components/notifications/Notification'
|
||||
|
||||
export * from './types'
|
||||
export * from './location'
|
||||
|
@ -10,6 +10,8 @@
|
||||
"Members": "Members",
|
||||
"Inbox": "Inbox",
|
||||
"MyIssues": "My issues",
|
||||
"ViewIssue": "View issue",
|
||||
"IssueCreated": "Issue Created",
|
||||
"Issues": "Issues",
|
||||
"Views": "Views",
|
||||
"Active": "Active",
|
||||
|
@ -9,6 +9,8 @@
|
||||
"Open": "Открыть",
|
||||
"Members": "Участиники",
|
||||
"Inbox": "Входящие",
|
||||
"ViewIssue": "Открыть задачу",
|
||||
"IssueCreated": "Задача создана",
|
||||
"MyIssues": "Мои задачи",
|
||||
"Issues": "Задачи",
|
||||
"Views": "Отображения",
|
||||
@ -143,7 +145,7 @@
|
||||
"FilterIsEither": "является ли любой из",
|
||||
"FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}",
|
||||
"Assigned": "Назначенные",
|
||||
"Created": "Созданные",
|
||||
"Created": "{value, plural, =1 {Создана} other {Созданные}}",
|
||||
"Subscribed": "Отслеживаемые",
|
||||
|
||||
"Relations": "Зависимости",
|
||||
|
@ -17,7 +17,7 @@
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import core, { Account, AttachedData, Doc, generateId, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
import { Card, createQuery, getClient, KeyedAttribute, MessageBox, SpaceSelector } from '@hcengineering/presentation'
|
||||
import tags, { TagElement, TagReference } from '@hcengineering/tags'
|
||||
import {
|
||||
@ -42,7 +42,11 @@
|
||||
Label,
|
||||
Menu,
|
||||
showPopup,
|
||||
Spinner
|
||||
Spinner,
|
||||
NotificationPosition,
|
||||
NotificationSeverity,
|
||||
Notification,
|
||||
notificationsStore
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { ObjectBox } from '@hcengineering/view-resources'
|
||||
@ -59,6 +63,7 @@
|
||||
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
|
||||
import SprintSelector from './sprints/SprintSelector.svelte'
|
||||
import IssueTemplateChilds from './templates/IssueTemplateChilds.svelte'
|
||||
import IssueNotification from './issues/IssueNotification.svelte'
|
||||
|
||||
export let space: Ref<Team>
|
||||
export let status: Ref<IssueStatus> | undefined = undefined
|
||||
@ -394,6 +399,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
const notification: Notification = {
|
||||
title: tracker.string.IssueCreated,
|
||||
subTitle: getTitle(object.title),
|
||||
severity: NotificationSeverity.Success,
|
||||
position: NotificationPosition.BottomRight,
|
||||
component: IssueNotification,
|
||||
closeTimeout: 10000,
|
||||
params: {
|
||||
issueId: objectId,
|
||||
subTitlePostfix: (await translate(tracker.string.Created, { value: 1 })).toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
notificationsStore.addNotification(notification)
|
||||
|
||||
objectId = generateId()
|
||||
resetObject()
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { Button } from '@hcengineering/ui'
|
||||
|
||||
export let mode: string
|
||||
export let config: [string, IntlString][]
|
||||
export let config: [string, IntlString, object][]
|
||||
export let onChange: (_mode: string) => void
|
||||
|
||||
function getButtonShape (i: number) {
|
||||
@ -21,10 +21,11 @@
|
||||
|
||||
<div class="itemsContainer">
|
||||
<div class="flex-center">
|
||||
{#each config as [_mode, label], i}
|
||||
{#each config as [_mode, label, params], i}
|
||||
<div class="buttonWrapper">
|
||||
<Button
|
||||
{label}
|
||||
labelParams={params}
|
||||
size="small"
|
||||
on:click={() => onChange(_mode)}
|
||||
selected={_mode === mode}
|
||||
|
@ -0,0 +1,177 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import {
|
||||
NotificationSeverity,
|
||||
Notification,
|
||||
Button,
|
||||
Icon,
|
||||
IconClose,
|
||||
IconInfo,
|
||||
IconCheckCircle,
|
||||
Label,
|
||||
showPanel
|
||||
} from '@hcengineering/ui'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
import IssuePresenter from './IssuePresenter.svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let notification: Notification = {}
|
||||
export let onRemove: () => void
|
||||
|
||||
const issueQuery = createQuery()
|
||||
const statusQuery = createQuery()
|
||||
|
||||
let issue: Issue | undefined
|
||||
let status: IssueStatus | undefined
|
||||
|
||||
const { title, subTitle, severity, params } = notification
|
||||
|
||||
$: issueQuery.query(
|
||||
tracker.class.Issue,
|
||||
{ _id: params.issueId },
|
||||
(res) => {
|
||||
issue = res[0]
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
|
||||
$: if (issue?.status !== undefined) {
|
||||
statusQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ _id: issue.status },
|
||||
(res) => {
|
||||
status = res[0]
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
}
|
||||
|
||||
const getIcon = () => {
|
||||
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 = () => {
|
||||
if (issue) {
|
||||
showPanel(tracker.component.EditIssue, issue._id, issue._class, 'content')
|
||||
}
|
||||
|
||||
onRemove()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root" in:fade out:fade>
|
||||
<Icon icon={getIcon()} size="medium" fill={getIconColor()} />
|
||||
|
||||
<div class="content">
|
||||
<div class="title">
|
||||
<Label label={title} />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="issue">
|
||||
{#if status}
|
||||
<IssueStatusIcon value={status} size="small" />
|
||||
{/if}
|
||||
{#if issue}
|
||||
<IssuePresenter value={issue} onClick={onRemove} />
|
||||
{/if}
|
||||
<div class="sub-title">
|
||||
{subTitle}
|
||||
</div>
|
||||
<div class="postfix">
|
||||
{params.subTitlePostfix}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-issue-button">
|
||||
<Button label={tracker.string.ViewIssue} kind="link" size="medium" on:click={handleIssueOpened} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="close-button">
|
||||
<Button icon={IconClose} kind="transparent" size="small" on:click={onRemove} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
box-shadow: 0 4px 10px var(--divider-color);
|
||||
height: 100px;
|
||||
width: 400px;
|
||||
overflow: hidden;
|
||||
color: var(--caption-color);
|
||||
background-color: var(--body-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
max-width: 210px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.issue {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.view-issue-button {
|
||||
margin-top: 10px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--caption-color);
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.postfix {
|
||||
color: var(--dark-color);
|
||||
}
|
||||
</style>
|
@ -20,13 +20,18 @@
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
// export let inline: boolean = false
|
||||
export let disableClick = false
|
||||
export let onClick: () => void
|
||||
|
||||
function handleIssueEditorOpened () {
|
||||
if (disableClick) {
|
||||
return
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
onClick()
|
||||
}
|
||||
|
||||
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,9 @@
|
||||
import IssuesView from '../issues/IssuesView.svelte'
|
||||
import ModeSelector from '../ModeSelector.svelte'
|
||||
|
||||
const config: [string, IntlString][] = [
|
||||
const config: [string, IntlString, object][] = [
|
||||
['assigned', tracker.string.Assigned],
|
||||
['created', tracker.string.Created],
|
||||
['created', tracker.string.Created, { value: 0 }],
|
||||
['subscribed', tracker.string.Subscribed]
|
||||
]
|
||||
const currentUser = getCurrentAccount() as EmployeeAccount
|
||||
|
@ -28,6 +28,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
Members: '' as IntlString,
|
||||
Inbox: '' as IntlString,
|
||||
MyIssues: '' as IntlString,
|
||||
ViewIssue: '' as IntlString,
|
||||
IssueCreated: '' as IntlString,
|
||||
Issues: '' as IntlString,
|
||||
Views: '' as IntlString,
|
||||
Active: '' as IntlString,
|
||||
|
@ -14,9 +14,9 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
|
||||
import { connect, versionError } from '@hcengineering/presentation'
|
||||
import { Loading } from '@hcengineering/ui'
|
||||
import { Loading, Notifications } from '@hcengineering/ui'
|
||||
|
||||
import Workbench from './Workbench.svelte'
|
||||
import workbench from '../plugin'
|
||||
</script>
|
||||
@ -30,7 +30,9 @@
|
||||
{versionError}
|
||||
</div>
|
||||
{:else if client}
|
||||
<Workbench {client} />
|
||||
<Notifications>
|
||||
<Workbench {client} />
|
||||
</Notifications>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<div>{error} -- {error.stack}</div>
|
||||
|
Loading…
Reference in New Issue
Block a user