Requests inbox (#3125)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-05-03 13:32:23 +06:00 committed by GitHub
parent d435dbfa2e
commit f24e3e6743
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 225 additions and 110 deletions

View File

@ -34,6 +34,8 @@
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/request": "^0.6.0",
"@hcengineering/notification": "^0.6.10",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/chunter": "^0.6.5",
"@hcengineering/request-resources": "^0.6.0"

View File

@ -37,6 +37,8 @@ import view from '@hcengineering/model-view'
import { Request, RequestDecisionComment, RequestPresenter, RequestStatus } from '@hcengineering/request'
import { AnyComponent } from '@hcengineering/ui'
import request from './plugin'
import notification from '@hcengineering/notification'
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
export { requestId } from '@hcengineering/request'
export { default } from './plugin'
@ -89,10 +91,54 @@ export function createModel (builder: Builder): void {
presenter: request.component.RequestPresenter
})
builder.mixin(request.class.Request, core.class.Class, notification.mixin.NotificationObjectPresenter, {
presenter: request.component.NotificationRequestView
})
builder.mixin(request.class.Request, core.class.Class, request.mixin.RequestPresenter, {
presenter: request.component.RequestView
})
builder.mixin(request.class.Request, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['requested', 'createdBy']
})
builder.createDoc(
notification.class.NotificationGroup,
core.space.Model,
{
label: request.string.Requests,
icon: request.icon.Requests,
objectClass: request.class.Request
},
request.ids.RequestNotificationGroup
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
objectClass: request.class.Request,
txClasses: [core.class.TxCreateDoc],
generated: false,
group: request.ids.RequestNotificationGroup,
label: request.string.Requested,
providers: {
[notification.providers.PlatformNotification]: true
}
},
request.ids.CreateRequestNotification
)
generateClassNotificationTypes(
builder,
request.class.Request,
request.ids.RequestNotificationGroup,
[],
['comments', 'approved', 'rejected', 'status']
)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,

View File

@ -20,6 +20,7 @@ import { requestId } from '@hcengineering/request'
import request from '@hcengineering/request-resources/src/plugin'
import { AnyComponent } from '@hcengineering/ui'
import type { TxViewlet } from '@hcengineering/activity'
import type { NotificationGroup, NotificationType } from '@hcengineering/notification'
export default mergeIds(requestId, request, {
activity: {
@ -27,10 +28,13 @@ export default mergeIds(requestId, request, {
RequestLabel: '' as AnyComponent
},
component: {
EditRequest: '' as AnyComponent
EditRequest: '' as AnyComponent,
NotificationRequestView: '' as AnyComponent
},
ids: {
TxRequestCreate: '' as Ref<TxViewlet>
TxRequestCreate: '' as Ref<TxViewlet>,
RequestNotificationGroup: '' as Ref<NotificationGroup>,
CreateRequestNotification: '' as Ref<NotificationType>
},
string: {
Status: '' as IntlString,

View File

@ -747,6 +747,7 @@ a.no-line {
.font-semi-bold { font-weight: 600; }
.fs-bold { font-weight: 500; }
.uppercase { text-transform: uppercase; }
.lower { text-transform: lowercase; }
.text-left { text-align: left; }
.over-underline {

View File

@ -202,7 +202,9 @@ export async function resolveLocation (loc: Location): Promise<ResolvedLocation
}
const id = loc.path[3] as Ref<Contact>
return await generateLocation(loc, id)
if (id !== undefined) {
return await generateLocation(loc, id)
}
}
async function generateLocation (loc: Location, id: Ref<Contact>): Promise<ResolvedLocation | undefined> {

View File

@ -4,7 +4,7 @@
"Request": "Request",
"Approve": "Approve",
"Approved": "Approved",
"CreatedRequest": "Created a request",
"CreatedRequest": "Created a ",
"For": "For",
"Change": "Change",
"Add": "Add",
@ -16,6 +16,8 @@
"Rejected": "Rejected",
"Comment": "Comment",
"PleaseTypeMessage": "Please type comment message to continue...",
"NoRequests": "No requests"
"NoRequests": "No requests",
"Cancel": "Cancel",
"Cancelled": "Cancelled"
}
}

View File

@ -4,7 +4,7 @@
"Request": "Запрос",
"Approve": "Одобрить",
"Approved": "Одобрено",
"CreatedRequest": "Создал запрос",
"CreatedRequest": "Создал(а)",
"For": "Для",
"Change": "Изменить",
"Add": "Добавить",
@ -16,6 +16,8 @@
"Rejected": "Отклонен",
"Comment": "Комментировать",
"PleaseTypeMessage": "Пожалуйста оставьте коментарий чтобы продолжить...",
"NoRequests": "Нет запросов"
"NoRequests": "Нет запросов",
"Cancel": "Отменить",
"Cancelled": "Отменен"
}
}

View File

@ -15,12 +15,14 @@
<script lang="ts">
import { Request } from '@hcengineering/request'
import { Label } from '@hcengineering/ui'
import { ObjectPresenter } from '@hcengineering/view-resources'
import { DocNavLink, ObjectPresenter } from '@hcengineering/view-resources'
import { createEventDispatcher, onMount } from 'svelte'
import request from '../plugin'
import RequestActions from './RequestActions.svelte'
import RequestDetail from './RequestDetail.svelte'
import TxView from './TxView.svelte'
import { createQuery } from '@hcengineering/presentation'
import { Doc } from '@hcengineering/core'
export let object: Request
@ -28,16 +30,28 @@
onMount(() => {
dispatch('open', {
ignoreKeys: ['comments', 'status', 'rejected', 'approved', 'requested'],
ignoreKeys: ['status'],
activityOptions: { enabled: true, showInput: false }
})
})
let doc: Doc | undefined = undefined
const query = createQuery()
query.query(object.attachedToClass, { _id: object.attachedTo }, (res) => {
;[doc] = res
})
</script>
{#if object !== undefined}
<div class="flex-row-center gap-1 mb-2">
<span class="mr-1"><Label label={request.string.For} /></span>
<span class="mr-1"><ObjectPresenter objectId={object.tx.objectId} _class={object.tx.objectClass} /></span>
{#if doc}
<span class="mr-1">
<DocNavLink object={doc}>
<ObjectPresenter value={doc} />
</DocNavLink>
</span>
{/if}
<TxView tx={object.tx} />
</div>
<RequestDetail value={object} />

View File

@ -0,0 +1,49 @@
<!--
// 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.
-->
<script lang="ts">
import { Doc } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Request } from '@hcengineering/request'
import { Label } from '@hcengineering/ui'
import { DocNavLink, ObjectPresenter } from '@hcengineering/view-resources'
import requests from '../plugin'
import RequestLabel from './RequestLabel.svelte'
export let value: Request
let doc: Doc | undefined = undefined
const query = createQuery()
query.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
;[doc] = res
})
</script>
<div class="inline-presenter">
<RequestLabel {value} size={'inline'} />
<span class="lower mx-1">
<Label label={requests.string.For} />
</span>
{#if doc}
<DocNavLink object={doc}>
<ObjectPresenter value={doc} />
</DocNavLink>
{/if}
</div>
<style lang="scss">
.lower {
text-transform: lowercase;
}
</style>

View File

@ -21,6 +21,7 @@
import { getClient } from '@hcengineering/presentation'
import { Request, RequestStatus } from '@hcengineering/request'
import type { RefAction } from '@hcengineering/text-editor'
import { Button } from '@hcengineering/ui'
import request from '../plugin'
import Comments from './icons/Comments.svelte'
import DocFail from './icons/DocFail.svelte'
@ -70,7 +71,18 @@
// We need to update backlinks before and after.
await updateBacklinks(client, value.attachedTo, value.attachedToClass, value._id, message)
refInput.submit()
refInput.createAttachments()
loading = false
}
async function comment (): Promise<void> {
await client.addCollection(chunter.class.Comment, value.space, value._id, value._class, 'comments', {
message,
attachments
})
// We need to update backlinks before and after.
await updateBacklinks(client, value.attachedTo, value.attachedToClass, value._id, message)
loading = false
}
@ -99,9 +111,20 @@
}
]
let loading = false
async function cancel () {
await client.update(value, {
status: RequestStatus.Cancelled
})
}
</script>
{#if value.status === RequestStatus.Active}
{#if value.createdBy === me}
<div class="mt-2">
<Button label={request.string.Cancel} on:click={cancel} />
</div>
{/if}
<div class="mt-2">
<AttachmentRefInput
bind:this={refInput}
@ -110,6 +133,7 @@
objectId={value._id}
iconSend={Comments}
labelSend={request.string.Comment}
on:message={comment}
on:update={onUpdate}
placeholder={request.string.PleaseTypeMessage}
extraActions={approvable ? extraActions : undefined}

View File

@ -14,31 +14,49 @@
-->
<script lang="ts">
import { Request, RequestStatus } from '@hcengineering/request'
import { Button, eventToHTMLElement, ProgressCircle, showPopup } from '@hcengineering/ui'
import RequestStatusPresenter from './RequestStatusPresenter.svelte'
import { Button, ButtonSize, Label, ProgressCircle, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import request from '../plugin'
import RequestDetailPopup from './RequestDetailPopup.svelte'
import RequestStatusPresenter from './RequestStatusPresenter.svelte'
export let value: Request
export let isOwnTx: boolean = false
export let size: ButtonSize = 'inline'
export let inline: boolean = true
</script>
<div class="flex gap-2">
<Button
on:click={(ev) => {
ev.stopPropagation()
showPopup(RequestDetailPopup, { value }, eventToHTMLElement(ev))
}}
>
<svelte:fragment slot="content">
{#if value.status !== RequestStatus.Active}
<RequestStatusPresenter value={value.status === RequestStatus.Completed} />
{:else}
<div class="flex-row-center content-color text-sm pointer-events-none">
<div class="mr-1">
<ProgressCircle max={value.requiredApprovesCount} value={value.approved.length} size={'inline'} primary />
<div class="flex">
{#if isOwnTx}
<div class="lower" class:inline-presenter={inline}>
<Label label={request.string.Request} />
</div>
{:else}
<DocNavLink {inline} object={value}>
<div class="flex-presenter lower" class:inline-presenter={inline}>
<Label label={request.string.Request} />
</div>
</DocNavLink>
<Button
{size}
kind="link"
on:click={(ev) => {
ev.stopPropagation()
showPopup(RequestDetailPopup, { value }, eventToHTMLElement(ev))
}}
>
<svelte:fragment slot="content">
{#if value.status !== RequestStatus.Active}
<RequestStatusPresenter value={value.status} />
{:else}
<div class="flex-row-center content-color text-sm pointer-events-none">
<div class="mr-1">
<ProgressCircle max={value.requiredApprovesCount} value={value.approved.length} size={'inline'} primary />
</div>
{value.approved.length}/{value.requiredApprovesCount}
</div>
{value.approved.length}/{value.requiredApprovesCount}
</div>
{/if}
</svelte:fragment>
</Button>
{/if}
</svelte:fragment>
</Button>
{/if}
</div>

View File

@ -13,33 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import { Request, RequestStatus } from '@hcengineering/request'
import { Icon, IconCheck, IconClose, IconInfo } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import { Request } from '@hcengineering/request'
import TxView from './TxView.svelte'
export let value: Request
export let inline: boolean = false
$: dte = new Date(value.tx.modifiedOn)
</script>
<div class="flex">
<DocNavLink {inline} object={value}>
<div class="flex-presenter mr-1" class:inline-presenter={inline}>
<div class="flex flex-row-center">
<div class="mr-2">
{#if value.status === RequestStatus.Completed || value.status === RequestStatus.Rejected}
<Icon icon={value.status === RequestStatus.Completed ? IconCheck : IconClose} size={'small'} />
{:else}
<Icon icon={IconInfo} size={'small'} />
{/if}
</div>
<span class="label nowrap">
{dte.getMonth() + 1}/{dte.getDay() + 1}-{(dte.getHours() * 60 + dte.getMinutes()).toString(7)}
</span>
</div>
</div>
</DocNavLink>
<TxView tx={value.tx} />
</div>

View File

@ -16,22 +16,19 @@
import { IntlString } from '@hcengineering/platform'
import { BooleanIcon, Label } from '@hcengineering/ui'
import request from '../plugin'
import { RequestStatus } from '@hcengineering/request'
export let value: boolean
export let value: RequestStatus
function getBooleanLabel (value: boolean): IntlString {
if (value) return request.string.Completed
return request.string.Rejected
function getBooleanLabel (value: RequestStatus): IntlString {
if (value === RequestStatus.Completed) return request.string.Completed
if (value === RequestStatus.Rejected) return request.string.Rejected
return request.string.Cancelled
}
</script>
<div
class="flex-row-center yesno-container"
class:yes={value === true}
class:no={value === false}
class:unknown={value === undefined}
>
<BooleanIcon {value} />
<div class="flex-row-center yesno-container">
<BooleanIcon value={value === RequestStatus.Completed} />
<span><Label label={getBooleanLabel(value)} /></span>
</div>

View File

@ -17,6 +17,9 @@
import RequestPresenter from '../RequestPresenter.svelte'
export let value: Request
export let isOwnTx: boolean
</script>
<RequestPresenter {value} />
{#if !isOwnTx}
<RequestPresenter {value} />
{/if}

View File

@ -18,6 +18,7 @@
import RequestLabel from '../RequestLabel.svelte'
export let value: Request
export let isOwnTx: boolean
let request: Request | undefined = undefined
const query = createQuery()
@ -25,7 +26,5 @@
</script>
{#if request}
<div class="ml-1">
<RequestLabel value={request} />
</div>
<RequestLabel value={request} {isOwnTx} />
{/if}

View File

@ -14,12 +14,12 @@
//
import { Resources } from '@hcengineering/platform'
import RequestsPopup from './components/RequestsPopup.svelte'
import TxCreateRequest from './components/activity/TxCreateRequest.svelte'
import RequestLabel from './components/activity/TxRequestLabel.svelte'
import EditRequest from './components/EditRequest.svelte'
import RequestPresenter from './components/RequestPresenter.svelte'
import RequestView from './components/RequestView.svelte'
import NotificationRequestView from './components/NotificationRequestView.svelte'
export default async (): Promise<Resources> => ({
activity: {
@ -27,9 +27,9 @@ export default async (): Promise<Resources> => ({
TxCreateRequest
},
component: {
RequestsPopup,
EditRequest,
RequestPresenter,
RequestView
RequestView,
NotificationRequestView
}
})

View File

@ -22,6 +22,7 @@ export default mergeIds(requestId, request, {
Approve: '' as IntlString,
Approved: '' as IntlString,
CreatedRequest: '' as IntlString,
Cancel: '' as IntlString,
For: '' as IntlString,
Change: '' as IntlString,
Add: '' as IntlString,
@ -32,6 +33,7 @@ export default mergeIds(requestId, request, {
Rejected: '' as IntlString,
Comment: '' as IntlString,
PleaseTypeMessage: '' as IntlString,
NoRequests: '' as IntlString
NoRequests: '' as IntlString,
Cancelled: '' as IntlString
}
})

View File

@ -44,7 +44,8 @@ export interface RequestDecisionComment extends Comment {}
export enum RequestStatus {
Active = 'Active',
Completed = 'Completed',
Rejected = 'Rejected'
Rejected = 'Rejected',
Cancelled = 'Cancelled'
}
/**
@ -71,7 +72,6 @@ const request = plugin(requestId, {
RequestPresenter: '' as Ref<Mixin<RequestPresenter>>
},
component: {
RequestsPopup: '' as AnyComponent,
RequestPresenter: '' as AnyComponent,
RequestView: '' as AnyComponent
},

View File

@ -20,8 +20,7 @@
import notification, { notificationId } from '@hcengineering/notification'
import { BrowserNotificatator, NotificationClientImpl } from '@hcengineering/notification-resources'
import { IntlString, getMetadata, getResource } from '@hcengineering/platform'
import { createQuery, getClient, configurationStore } from '@hcengineering/presentation'
import request, { RequestStatus, requestId } from '@hcengineering/request'
import { configurationStore, createQuery, getClient } from '@hcengineering/presentation'
import {
AnyComponent,
CompAndProps,
@ -60,11 +59,11 @@
import AccountPopup from './AccountPopup.svelte'
import AppItem from './AppItem.svelte'
import Applications from './Applications.svelte'
import Logo from './Logo.svelte'
import NavHeader from './NavHeader.svelte'
import Navigator from './Navigator.svelte'
import SpaceView from './SpaceView.svelte'
import Settings from './icons/Settings.svelte'
import Logo from './Logo.svelte'
import TopMenu from './icons/TopMenu.svelte'
let contentPanel: HTMLElement
@ -159,24 +158,6 @@
}
)
let hasRequests = false
const requestQuery = createQuery()
$: $configurationStore.has(requestId) ||
requestQuery.query(
request.class.Request,
{
requested: account._id,
status: RequestStatus.Active
},
(res) =>
(hasRequests =
res.filter(
(p) =>
p.requested.filter((a) => a === account._id).length > p.approved.filter((a) => a === account._id).length
).length > 0)
)
onDestroy(
location.subscribe(async (loc) => {
closeTooltip()
@ -606,14 +587,6 @@
on:click={() => showPopup(calendar.component.RemindersPopup, {}, notifyPosition)}
/>
{/if}
{#if $configurationStore.has(requestId)}
<AppItem
icon={request.icon.Requests}
label={request.string.Requests}
on:click={() => showPopup(request.component.RequestsPopup, {}, notifyPosition)}
notify={hasRequests}
/>
{/if}
<div class="divider" />
<Applications
apps={getApps(apps)}

View File

@ -31,6 +31,7 @@ export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise
const collectionTx = control.txFactory.createTxUpdateDoc(ctx.objectClass, ctx.objectSpace, ctx.objectId, {
status: RequestStatus.Completed
})
collectionTx.space = core.space.Tx
const resTx = control.txFactory.createTxCollectionCUD(
ptx.objectClass,
ptx.objectId,
@ -38,13 +39,9 @@ export async function OnRequestUpdate (tx: Tx, control: TriggerControl): Promise
'requests',
collectionTx
)
return [
{
...request.tx,
modifiedOn: resTx.modifiedOn
},
resTx
]
resTx.space = core.space.Tx
await control.apply([resTx], true)
}
return []
}