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. // Copyright © 2020 Anticrm Platform Contributors.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // 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 // 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 // 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 // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// //
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
export let size: 'tiny' | 'small' | 'medium' | 'large' export let size: 'tiny' | 'small' | 'medium' | 'large'
const fill: string = 'currentColor' export let fill: string = 'currentColor'
</script> </script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <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 IconScale } from './components/icons/Scale.svelte'
export { default as IconScaleFull } from './components/icons/ScaleFull.svelte' export { default as IconScaleFull } from './components/icons/ScaleFull.svelte'
export { default as IconOpen } from './components/icons/Open.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 PanelInstance } from './components/PanelInstance.svelte'
export { default as Panel } from './components/Panel.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 ToggleButton } from './components/ToggleButton.svelte'
export { default as ExpandCollapse } from './components/ExpandCollapse.svelte' export { default as ExpandCollapse } from './components/ExpandCollapse.svelte'
export { default as BarDashboard } from './components/BarDashboard.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 './types'
export * from './location' export * from './location'

View File

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

View File

@ -9,6 +9,8 @@
"Open": "Открыть", "Open": "Открыть",
"Members": "Участиники", "Members": "Участиники",
"Inbox": "Входящие", "Inbox": "Входящие",
"ViewIssue": "Открыть задачу",
"IssueCreated": "Задача создана",
"MyIssues": "Мои задачи", "MyIssues": "Мои задачи",
"Issues": "Задачи", "Issues": "Задачи",
"Views": "Отображения", "Views": "Отображения",
@ -143,7 +145,7 @@
"FilterIsEither": "является ли любой из", "FilterIsEither": "является ли любой из",
"FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}", "FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}",
"Assigned": "Назначенные", "Assigned": "Назначенные",
"Created": "Созданные", "Created": "{value, plural, =1 {Создана} other {Созданные}}",
"Subscribed": "Отслеживаемые", "Subscribed": "Отслеживаемые",
"Relations": "Зависимости", "Relations": "Зависимости",

View File

@ -1,14 +1,14 @@
<!-- <!--
// Copyright © 2022 Hardcore Engineering Inc. // Copyright © 2022 Hardcore Engineering Inc.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // 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 // 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 // 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 // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// //
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
@ -17,7 +17,7 @@
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import { Employee } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import core, { Account, AttachedData, Doc, generateId, Ref, SortingOrder, WithLookup } from '@hcengineering/core' 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 { Card, createQuery, getClient, KeyedAttribute, MessageBox, SpaceSelector } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags' import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { import {
@ -42,7 +42,11 @@
Label, Label,
Menu, Menu,
showPopup, showPopup,
Spinner Spinner,
NotificationPosition,
NotificationSeverity,
Notification,
notificationsStore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { ObjectBox } from '@hcengineering/view-resources' import { ObjectBox } from '@hcengineering/view-resources'
@ -59,6 +63,7 @@
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte' import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SprintSelector from './sprints/SprintSelector.svelte' import SprintSelector from './sprints/SprintSelector.svelte'
import IssueTemplateChilds from './templates/IssueTemplateChilds.svelte' import IssueTemplateChilds from './templates/IssueTemplateChilds.svelte'
import IssueNotification from './issues/IssueNotification.svelte'
export let space: Ref<Team> export let space: Ref<Team>
export let status: Ref<IssueStatus> | undefined = undefined 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() objectId = generateId()
resetObject() resetObject()
} }

View File

@ -3,7 +3,7 @@
import { Button } from '@hcengineering/ui' import { Button } from '@hcengineering/ui'
export let mode: string export let mode: string
export let config: [string, IntlString][] export let config: [string, IntlString, object][]
export let onChange: (_mode: string) => void export let onChange: (_mode: string) => void
function getButtonShape (i: number) { function getButtonShape (i: number) {
@ -21,10 +21,11 @@
<div class="itemsContainer"> <div class="itemsContainer">
<div class="flex-center"> <div class="flex-center">
{#each config as [_mode, label], i} {#each config as [_mode, label, params], i}
<div class="buttonWrapper"> <div class="buttonWrapper">
<Button <Button
{label} {label}
labelParams={params}
size="small" size="small"
on:click={() => onChange(_mode)} on:click={() => onChange(_mode)}
selected={_mode === 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. // Copyright © 2022 Hardcore Engineering Inc.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // 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 // 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 // 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 // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// //
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
@ -20,13 +20,18 @@
import tracker from '../../plugin' import tracker from '../../plugin'
export let value: WithLookup<Issue> export let value: WithLookup<Issue>
// export let inline: boolean = false
export let disableClick = false export let disableClick = false
export let onClick: () => void
function handleIssueEditorOpened () { function handleIssueEditorOpened () {
if (disableClick) { if (disableClick) {
return return
} }
if (onClick) {
onClick()
}
showPanel(tracker.component.EditIssue, value._id, value._class, 'content') showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
} }

View File

@ -1,14 +1,14 @@
<!-- <!--
// Copyright © 2022 Hardcore Engineering Inc. // Copyright © 2022 Hardcore Engineering Inc.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // 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 // 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 // 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 // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// //
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
@ -26,9 +26,9 @@
import IssuesView from '../issues/IssuesView.svelte' import IssuesView from '../issues/IssuesView.svelte'
import ModeSelector from '../ModeSelector.svelte' import ModeSelector from '../ModeSelector.svelte'
const config: [string, IntlString][] = [ const config: [string, IntlString, object][] = [
['assigned', tracker.string.Assigned], ['assigned', tracker.string.Assigned],
['created', tracker.string.Created], ['created', tracker.string.Created, { value: 0 }],
['subscribed', tracker.string.Subscribed] ['subscribed', tracker.string.Subscribed]
] ]
const currentUser = getCurrentAccount() as EmployeeAccount const currentUser = getCurrentAccount() as EmployeeAccount

View File

@ -28,6 +28,8 @@ export default mergeIds(trackerId, tracker, {
Members: '' as IntlString, Members: '' as IntlString,
Inbox: '' as IntlString, Inbox: '' as IntlString,
MyIssues: '' as IntlString, MyIssues: '' as IntlString,
ViewIssue: '' as IntlString,
IssueCreated: '' as IntlString,
Issues: '' as IntlString, Issues: '' as IntlString,
Views: '' as IntlString, Views: '' as IntlString,
Active: '' as IntlString, Active: '' as IntlString,

View File

@ -1,22 +1,22 @@
<!-- <!--
// Copyright © 2020 Anticrm Platform Contributors. // Copyright © 2020 Anticrm Platform Contributors.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // 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 // 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 // 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 // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// //
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import { connect, versionError } from '@hcengineering/presentation' 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 './Workbench.svelte'
import workbench from '../plugin' import workbench from '../plugin'
</script> </script>
@ -30,7 +30,9 @@
{versionError} {versionError}
</div> </div>
{:else if client} {:else if client}
<Workbench {client} /> <Notifications>
<Workbench {client} />
</Notifications>
{/if} {/if}
{:catch error} {:catch error}
<div>{error} -- {error.stack}</div> <div>{error} -- {error.stack}</div>