TSK-212: add notification on issue created (#2325)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2022-10-31 23:08:57 +04:00 committed by GitHub
parent bfcbb73bc7
commit 15c2aa802b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 406 additions and 27 deletions

View 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>

View File

@ -1,20 +1,20 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
//
// 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">
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">

View 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} />

View 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
}

View File

@ -0,0 +1,6 @@
export enum NotificationPosition {
BottomLeft,
BottomRight,
TopLeft,
TopRight
}

View File

@ -0,0 +1,6 @@
export enum NotificationSeverity {
Info,
Success,
Warning,
Error
}

View File

@ -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>

View 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))
}

View 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)
}

View File

@ -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'

View File

@ -10,6 +10,8 @@
"Members": "Members",
"Inbox": "Inbox",
"MyIssues": "My issues",
"ViewIssue": "View issue",
"IssueCreated": "Issue Created",
"Issues": "Issues",
"Views": "Views",
"Active": "Active",

View File

@ -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": "Зависимости",

View File

@ -1,14 +1,14 @@
<!--
// Copyright © 2022 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.
-->
@ -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()
}

View File

@ -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}

View File

@ -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>

View File

@ -1,14 +1,14 @@
<!--
// Copyright © 2022 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.
-->
@ -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')
}

View File

@ -1,14 +1,14 @@
<!--
// Copyright © 2022 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.
-->
@ -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

View File

@ -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,

View File

@ -1,22 +1,22 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
//
// 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 { 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>