Approve requests (#10486)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2026-02-04 16:51:02 +05:00
committed by GitHub
parent 2bdf82d608
commit 773fee05be
45 changed files with 1496 additions and 60 deletions
@@ -0,0 +1,48 @@
<!--
// Copyright © 2026 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 { Card } from '@hcengineering/card'
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ApproveRequest } from '@hcengineering/process'
import { Button, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import SignatureDialog from './SignatureDialog.svelte'
import process from '../plugin'
export let todo: ApproveRequest
export let card: Ref<Card>
const client = getClient()
async function changeApprovalRequestState (ev: MouseEvent, isRejection: boolean): Promise<void> {
showPopup(SignatureDialog, { isRejection }, eventToHTMLElement(ev), async (res) => {
if (!res) return
const { rejectionNote } = res
if (isRejection && rejectionNote == null) {
return
}
await client.update(todo, {
doneOn: new Date().getTime(),
approved: !isRejection
})
})
}
</script>
<Button label={process.string.Approve} kind="positive" on:click={(ev) => changeApprovalRequestState(ev, false)} />
<Button label={process.string.Reject} kind="negative" on:click={(ev) => changeApprovalRequestState(ev, true)} />
@@ -0,0 +1,22 @@
<!--
// Copyright © 2026 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 { ApproveRequest } from '@hcengineering/process'
export let value: ApproveRequest
</script>
{value.title}
@@ -13,13 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import { createQuery, getClient } from '@hcengineering/presentation'
import plugin from '../plugin'
import { Execution, ProcessToDo } from '@hcengineering/process'
import { getCurrentEmployee } from '@hcengineering/contact'
import { Button, Component } from '@hcengineering/ui'
import time from '@hcengineering/time'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ApproveRequest, Execution, ProcessToDo } from '@hcengineering/process'
import { Button } from '@hcengineering/ui'
import plugin from '../plugin'
import ApproveRequestButtons from './ApproveRequestButtons.svelte'
export let value: Execution
@@ -47,8 +47,15 @@
doneOn: new Date().getTime()
})
}
function isRequest (todo: ProcessToDo): todo is ApproveRequest {
return todo._class === plugin.class.ApproveRequest
}
</script>
{#each todos as todo (todo._id)}
{#if isRequest(todo)}
<ApproveRequestButtons {todo} card={value.card} />
{/if}
<Button label={getEmbeddedLabel(todo.title)} on:click={() => checkTodo(todo)} />
{/each}
@@ -17,9 +17,10 @@
import { getCurrentEmployee } from '@hcengineering/contact'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { EventButton, Execution, ExecutionStatus, ProcessToDo } from '@hcengineering/process'
import { ApproveRequest, EventButton, Execution, ExecutionStatus, ProcessToDo } from '@hcengineering/process'
import { Button } from '@hcengineering/ui'
import process from '../plugin'
import ApproveRequestButtons from './ApproveRequestButtons.svelte'
export let card: Card
@@ -98,10 +99,18 @@
}
$: rollbacks = docs.filter((d) => d.rollback.length > 0)
function isRequest (todo: ProcessToDo): todo is ApproveRequest {
return todo._class === process.class.ApproveRequest
}
</script>
{#each todos as todo (todo._id)}
<Button kind={'primary'} label={getEmbeddedLabel(todo.title)} on:click={() => checkTodo(todo)} />
{#if isRequest(todo)}
<ApproveRequestButtons {todo} card={card._id} />
{:else}
<Button kind={'primary'} label={getEmbeddedLabel(todo.title)} on:click={() => checkTodo(todo)} />
{/if}
{/each}
{#each actions as action (action._id)}
<Button kind={'primary'} label={getEmbeddedLabel(action.title)} on:click={() => performAction(action)} />
@@ -0,0 +1,37 @@
<!--
// Copyright © 2026 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 { Card } from '@hcengineering/card'
import RequestsExtension from './RequestsExtension.svelte'
export let doc: Card
export let hidden: boolean = false
</script>
{#if !hidden}
<div class="requests__section">
<RequestsExtension card={doc} on:loaded />
</div>
{/if}
<style lang="scss">
.requests__section {
display: flex;
flex-direction: column;
padding: 0 1rem;
width: 100%;
}
</style>
@@ -0,0 +1,156 @@
<!--
// Copyright © 2025 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 { Card } from '@hcengineering/card'
import core, { Doc, FindOptions, SortingOrder } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { ApproveRequest } from '@hcengineering/process'
import { Label, registerFocus, resizeObserver, Section } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
import {
List,
ListSelectionProvider,
noCategory,
SelectDirection,
ViewletsSettingButton
} from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import process from '../plugin'
export let card: Card
const viewletId = process.viewlet.CardRequests
const dispatch = createEventDispatcher()
$: query = {
card: card._id
}
const options: FindOptions<ApproveRequest> = {
sort: {
modifiedOn: SortingOrder.Descending
}
}
let list: List
const listProvider = new ListSelectionProvider(
(offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection, noScroll?: boolean) => {
if (dir === 'vertical') {
// Select next
list?.select(offset, of, noScroll)
}
}
)
let docs: ApproveRequest[] = []
function select () {
listProvider.update(docs)
listProvider.updateFocus(docs[0])
list?.select(0, undefined)
}
const selection = listProvider.selection
// Focusable control with index
let focused = false
export let focusIndex = -1
registerFocus(focusIndex, {
focus: () => {
;(window.document.activeElement as HTMLElement).blur()
focused = true
select()
return true
},
isFocus: () => focused
})
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined = undefined
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: process.viewlet.CardRequests
},
(res) => {
preference = res[0]
}
)
let listWidth: number
let viewlet: Viewlet | undefined
let viewOptions: ViewOptions | undefined
let docsProvided = false
</script>
<Section icon={process.icon.Process} label={process.string.ApproveRequest} spaceBeforeContent>
<svelte:fragment slot="header">
<div class="buttons-group xsmall-gap">
<ViewletsSettingButton bind:viewOptions viewletQuery={{ _id: viewletId }} kind={'tertiary'} bind:viewlet />
</div>
</svelte:fragment>
<svelte:fragment slot="content">
<div
class="antiSection-empty {docsProvided && docs.length === 0 ? 'solid' : 'none-appearance flex-gap-2'}"
use:resizeObserver={(evt) => {
listWidth = evt.clientWidth
}}
>
{#if viewOptions && viewlet}
<List
bind:this={list}
_class={process.class.ApproveRequest}
{viewOptions}
baseMenuClass={process.class.ApproveRequest}
viewOptionsConfig={viewlet.viewOptions?.other}
config={preference?.config ?? viewlet.config}
configurations={undefined}
{query}
{options}
compactMode={listWidth <= 600}
flatHeaders={true}
disableHeader={viewOptions.groupBy?.length === 0 || viewOptions.groupBy[0] === noCategory}
{listProvider}
selectedObjectIds={$selection ?? []}
on:row-focus={(event) => {
listProvider.updateFocus(event.detail ?? undefined)
}}
on:check={(event) => {
listProvider.updateSelection(event.detail.docs, event.detail.value)
}}
on:content={(evt) => {
docsProvided = true
docs = evt.detail
listProvider.update(evt.detail)
dispatch('loaded')
}}
/>
{#if docsProvided && docs.length === 0}
<div class="flex-center content-color empty-content">
<Label label={process.string.NoProcesses} />
</div>
{/if}
{/if}
</div>
</svelte:fragment>
</Section>
<style lang="scss">
.antiSection-empty:has(.empty-content) :global(.list-container) {
display: none;
}
</style>
@@ -0,0 +1,177 @@
<!--
//
// Copyright © 2023 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 { getClient as getAccountClient } from '@hcengineering/account-client'
import contact, { SocialIdentityRef } from '@hcengineering/contact'
import { getCurrentAccount, SocialIdType } from '@hcengineering/core'
import login from '@hcengineering/login'
import {
ERROR,
getMetadata,
IntlString,
OK,
PlatformError,
Severity,
Status,
translate
} from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { EditBox, ModernDialog, StylishEdit, Status as StatusControl } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
export let confirmationTitle: IntlString = plugin.string.ConfirmApproval
export let rejectionTitle: IntlString = plugin.string.ConfirmRejection
export let isRejection: boolean = false
const dispatch = createEventDispatcher()
const account = getCurrentAccount()
const client = getClient()
let rejectionNote = ''
const object: LoginInfo = {
email: '',
password: ''
}
void client
.findOne(contact.class.SocialIdentity, {
_id: { $in: account.socialIds as SocialIdentityRef[] },
type: SocialIdType.EMAIL
})
.then((si) => {
if (si != null) {
object.email = si.value
}
})
const accountsUrl = getMetadata(login.metadata.AccountsUrl) ?? ''
$: disableEmailField = object.email !== ''
$: canSubmit = object.email !== '' && object.password !== '' && (!isRejection || rejectionNote.trim().length > 0)
let status = OK
async function submit (): Promise<void> {
if (object.email === '' || object.password === '') {
return
}
status = await validateAccount(object.email, object.password)
if (status === OK) {
dispatch('close', { rejectionNote: isRejection ? rejectionNote : undefined })
}
}
async function validateAccount (email: string, password: string): Promise<Status> {
const accountClient = getAccountClient(accountsUrl)
try {
await accountClient.login(email, password)
return OK
} catch (err: any) {
if (err instanceof PlatformError) {
return err.status
} else {
return ERROR
}
}
}
interface LoginInfo {
email: string
password: string
}
const loginIntlFieldNames: Readonly<{ [K in keyof LoginInfo]: IntlString }> = {
email: login.string.Email,
password: login.string.Password
}
async function validate (): Promise<void> {
for (const field of Object.keys(object)) {
const k = field as keyof LoginInfo
if (object[k] === '') {
status = new Status(Severity.INFO, plugin.string.FieldIsEmpty, {
field: await translate(loginIntlFieldNames[k], {})
})
return
}
}
if (isRejection && rejectionNote.trim().length === 0) {
status = new Status(Severity.INFO, plugin.string.FieldIsEmpty, {
field: await translate(plugin.string.RejectionReason, {})
})
return
}
status = OK
}
</script>
<ModernDialog
label={isRejection ? rejectionTitle : confirmationTitle}
{canSubmit}
on:submit={submit}
on:close
width="32rem"
shadow={true}
className={'signature-dialog'}
>
<div class="flex-col flex-gap-2">
<StylishEdit
label={login.string.Email}
name={login.string.Email}
password={false}
bind:value={object.email}
on:input={validate}
on:blur={() => object.email.trim()}
disabled={disableEmailField}
/>
<StylishEdit
label={login.string.Password}
name={login.string.Password}
password={true}
bind:value={object.password}
on:input={validate}
on:blur={() => object.email.trim()}
/>
{#if isRejection}
<div class="mt-2">
<EditBox
id="rejection-reason"
label={plugin.string.RejectionReason}
value={rejectionNote}
placeholder={plugin.string.ProvideRejectionReason}
kind="default"
required={true}
on:value={({ detail }) => {
rejectionNote = detail
void validate()
}}
/>
</div>
{/if}
</div>
<div slot="footerExtra">
<StatusControl {status} overflow={false} />
</div>
</ModernDialog>
@@ -38,6 +38,7 @@
import plugin from '../../plugin'
import ExecutionContextPresenter from '../attributeEditors/ExecutionContextPresenter.svelte'
import ProcessContextPresenter from './ProcessContextPresenter.svelte'
import { Class, Ref } from '@hcengineering/core'
export let readonly: boolean
export let process: Process
@@ -49,6 +50,8 @@
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = undefined
export let _class: Ref<Class<ProcessToDo>> = plugin.class.ProcessToDo
$: context = getContext(value)
const client = getClient()
@@ -71,7 +74,7 @@
const res: SelectPopupValueType[] = []
for (const key in process.context) {
const ctx = process.context[key as ContextId]
if (ctx._class === plugin.class.ProcessToDo) {
if (ctx._class === _class) {
if (skipRollback) {
const transition = client.getModel().findObject(ctx.producer)
if (transition === undefined) {
@@ -0,0 +1,112 @@
<!--
// Copyright © 2026 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 contact from '@hcengineering/contact'
import core, { AnyAttribute } from '@hcengineering/core'
import { getAttributeEditor, getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { ApproveRequest, Process, Step } from '@hcengineering/process'
import { AnySvelteComponent } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import { getContext, getMockAttribute } from '../../utils'
import ProcessAttribute from '../ProcessAttribute.svelte'
import ParamsEditor from './ParamsEditor.svelte'
export let process: Process
export let step: Step<ApproveRequest>
let params = step.params
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
function changeParams (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
params = e.detail
;(step.params as any) = params
dispatch('change', step)
}
}
const keys = ['title', 'dueDate']
$: value = params.user
const type = {
label: core.string.Array,
_class: core.class.ArrOf,
of: {
label: core.string.Ref,
_class: core.class.RefTo,
to: contact.mixin.Employee
}
}
const attribute = getMockAttribute(plugin.class.ApproveRequest, plugin.string.Reviewers, type)
const presenterClass = getAttributePresenterClass(hierarchy, attribute.type)
$: context = getContext(client, process, presenterClass.attrClass, presenterClass.category)
function onChange (e: CustomEvent<any>): void {
params.user = e.detail
;(step.params as any) = params
dispatch('change', step)
}
let editor: AnySvelteComponent | undefined
function getBaseEditor (attribute: AnyAttribute): void {
void getAttributeEditor(client, plugin.class.ApproveRequest, {
attr: attribute,
key: 'user'
}).then((p) => {
editor = p
})
}
getBaseEditor(attribute)
</script>
<div class="grid">
<ProcessAttribute
{process}
{context}
{editor}
{attribute}
{presenterClass}
{value}
masterTag={process.masterTag}
allowArray={true}
on:remove
on:change={onChange}
/>
</div>
<ParamsEditor _class={plugin.class.ApproveRequest} {process} {keys} {params} on:change={changeParams} />
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin: 0.25rem 2rem 0;
width: calc(100% - 4rem);
height: min-content;
}
</style>
@@ -0,0 +1,45 @@
<!--
// Copyright © 2025 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 { Process } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import ToDoContextSelector from '../contextEditors/ToDoContextSelector.svelte'
export let readonly: boolean
export let process: Process
export let params: Record<string, any>
const dispatch = createEventDispatcher()
function change (e: CustomEvent<string>): void {
if (readonly || e.detail == null) return
params._id = e.detail
dispatch('change', { params })
}
</script>
<div class="editor-grid">
<Label label={plugin.string.ApproveRequest} />
<ToDoContextSelector
{readonly}
_class={plugin.class.ApproveRequest}
{process}
value={params._id}
on:change={change}
/>
</div>
@@ -0,0 +1,43 @@
<!--
// Copyright © 2025 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 { parseContext, Process, SelectedContext, SelectedExecutionContext } from '@hcengineering/process'
import ui, { Label } from '@hcengineering/ui'
import ExecutionContextPresenter from '../attributeEditors/ExecutionContextPresenter.svelte'
export let process: Process
export let params: Record<string, any>
$: context = getContext(params._id)
function getContext (value: string | undefined): SelectedExecutionContext | undefined {
if (value === undefined) return
const context = parseContext(value)
if (context !== undefined && isExecutionContext(context)) {
return context
}
}
function isExecutionContext (context: SelectedContext): context is SelectedExecutionContext {
return context.type === 'context'
}
</script>
{#if context === undefined}
<Label label={ui.string.NotSelected} />
{:else}
<ExecutionContextPresenter {process} contextValue={context} />
{/if}
@@ -0,0 +1,53 @@
<!--
// Copyright © 2026 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 cardPlugin, { Tag } from '@hcengineering/card'
import { Ref } from '@hcengineering/core'
import { Process, Step } from '@hcengineering/process'
import { Label, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import TagSelector from './TagSelector.svelte'
export let process: Process
export let step: Step<Tag>
const params = step.params
let _id = params._id as Ref<Tag>
const dispatch = createEventDispatcher()
function changeTag (e: CustomEvent<{ tag: Ref<Tag> }>): void {
if (e.detail !== undefined) {
_id = e.detail.tag
params._id = _id
step.params = params
dispatch('change', step)
}
}
</script>
<div class="flex-col flex-gap-2">
<div class="editor-grid">
<span
class="labelOnPanel"
use:tooltip={{
props: { label: cardPlugin.string.Tag }
}}
>
<Label label={cardPlugin.string.Tag} />
</span>
<TagSelector {process} tag={_id} includeBase on:change={changeTag} />
</div>
</div>
@@ -0,0 +1,33 @@
<!--
// Copyright © 2026 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 { getClient } from '@hcengineering/presentation'
import { Process } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
import plugin from '../../plugin'
export let process: Process
export let params: Record<string, any>
const client = getClient()
$: _class = client.getHierarchy().findClass(params._id)
</script>
<Label label={plugin.string.LockSection} />:
{#if _class !== undefined}
<Label label={_class.label} />
{/if}
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import cardPlugin, { Tag } from '@hcengineering/card'
import cardPlugin, { Tag as MasterTag } from '@hcengineering/card'
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Process } from '@hcengineering/process'
@@ -21,7 +21,8 @@
import { createEventDispatcher } from 'svelte'
export let process: Process
export let tag: Ref<Tag> | undefined = undefined
export let tag: Ref<MasterTag> | undefined = undefined
export let includeBase: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -29,14 +30,14 @@
const dispatch = createEventDispatcher()
function open (e: MouseEvent): void {
const res: Tag[] = []
const res = new Set<MasterTag>(includeBase ? [hierarchy.getClass(process.masterTag)] : [])
const ancestors = hierarchy.getAncestors(process.masterTag)
const tags = client.getModel().findAllSync(cardPlugin.class.Tag, {})
for (const p of tags) {
try {
const base = hierarchy.getBaseClass(p._id)
if (process.masterTag === base || ancestors.includes(base)) {
res.push(p)
res.add(p)
}
} catch (err) {
console.log('error', err, p._id)
@@ -44,7 +45,6 @@
}
const items: SelectPopupValueType[] = []
res.forEach((cl) => {
if (cl._class !== cardPlugin.class.Tag) return
items.push({
id: cl._id,
label: cl.label,
+24 -2
View File
@@ -24,11 +24,14 @@ import ExecutionMyToDos from './components/ExecutionMyToDos.svelte'
import ExecutonPresenter from './components/ExecutonPresenter.svelte'
import ExecutonProgressPresenter from './components/ExecutonProgressPresenter.svelte'
import Main from './components/Main.svelte'
import ApproveRequestPresenter from './components/ApproveRequestPresenter.svelte'
import SubProcessPresenter from './components/presenters/SubProcessPresenter.svelte'
import ToDoPresenter from './components/presenters/ToDoPresenter.svelte'
import UpdateCardPresenter from './components/presenters/UpdateCardPresenter.svelte'
import ProcessesCardSection from './components/ProcessesCardSection.svelte'
import ProcessesExtension from './components/ProcessesExtension.svelte'
import RequestsCardSection from './components/RequestsCardSection.svelte'
import RequestsExtension from './components/RequestsExtension.svelte'
import ProcessesSettingSection from './components/ProcessesSection.svelte'
import ProcessPresenter from './components/ProcessPresenter.svelte'
import RunProcessCardPopup from './components/RunProcessCardPopup.svelte'
@@ -71,6 +74,11 @@ import TimeEditor from './components/settings/TimeEditor.svelte'
import TimePresenter from './components/settings/TimePresenter.svelte'
import ToDoSettingPresenter from './components/settings/ToDoPresenter.svelte'
import TransitionRefPresenter from './components/settings/TransitionRefPresenter.svelte'
import ApproveRequestEditor from './components/settings/ApproveRequestEditor.svelte'
import ApproveRequestTriggerEditor from './components/settings/ApproveRequestTriggerEditor.svelte'
import ApproveRequestTriggerPresenter from './components/settings/ApproveRequestTriggerPresenter.svelte'
import LockSectionEditor from './components/settings/LockSectionEditor.svelte'
import LockSectionPresenter from './components/settings/LockSectionPresenter.svelte'
import AppendEditor from './components/transformEditors/AppendEditor.svelte'
import CutEditor from './components/transformEditors/CutEditor.svelte'
import ReplaceEditor from './components/transformEditors/ReplaceEditor.svelte'
@@ -80,7 +88,10 @@ import RolePresenter from './components/transformPresenters/RolePresenter.svelte
import { exportProcess } from './exporter'
import { ProcessMiddleware } from './middleware'
import {
approveRequestApproved,
approveRequestRejected,
checkProcessSectionVisibility,
checkRequestsSectionVisibility,
continueExecution,
eventCheck,
fieldChangesCheck,
@@ -109,6 +120,8 @@ export default async (): Promise<Resources> => ({
ToDoPresenter,
UpdateCardPresenter,
ProcessesExtension,
RequestsExtension,
RequestsCardSection,
ExecutonPresenter,
ExecutonProgressPresenter,
ProcessPresenter,
@@ -148,7 +161,13 @@ export default async (): Promise<Resources> => ({
FunctionSubmenu,
SubProcessMatchEditor,
SubProcessMatchPresenter,
ProcessesHeaderExtension
ProcessesHeaderExtension,
ApproveRequestPresenter,
ApproveRequestEditor,
ApproveRequestTriggerEditor,
ApproveRequestTriggerPresenter,
LockSectionPresenter,
LockSectionEditor
},
criteriaEditor: {
BaseCriteria,
@@ -177,12 +196,15 @@ export default async (): Promise<Resources> => ({
SubProcessMatchCheck: subProcessMatchCheck,
ToDo: todoTranstionCheck,
Time: timeTransitionCheck,
OnEventCheck: eventCheck
OnEventCheck: eventCheck,
ApproveRequestApproved: approveRequestApproved,
ApproveRequestRejected: approveRequestRejected
},
function: {
ExportProcess: exportProcess,
ShowDoneQuery: showDoneQuery,
CheckProcessSectionVisibility: checkProcessSectionVisibility,
CheckRequestsSectionVisibility: checkRequestsSectionVisibility,
// eslint-disable-next-line @typescript-eslint/unbound-method
CreateMiddleware: ProcessMiddleware.create
}
+47 -8
View File
@@ -11,23 +11,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import cardPlugin, { type Card } from '@hcengineering/card'
import core, {
getCurrentAccount,
type TxCreateDoc,
type TxMixin,
SortingOrder,
TxOperations,
TxProcessor,
type Client,
type Tx,
type TxApplyIf,
type TxCreateDoc,
type TxMixin,
type TxResult,
type TxUpdateDoc,
TxProcessor,
SortingOrder
type TxUpdateDoc
} from '@hcengineering/core'
import { BasePresentationMiddleware, type PresentationMiddleware } from '@hcengineering/presentation'
import process, { ExecutionStatus, type ProcessToDo, isUpdateTx } from '@hcengineering/process'
import { createExecution, getNextStateUserInput, requestResult, pickTransition } from './utils'
import cardPlugin, { type Card } from '@hcengineering/card'
import { type ApproveRequest, ExecutionStatus, isUpdateTx, type ProcessToDo } from '@hcengineering/process'
import process from './plugin'
import { createExecution, getNextStateUserInput, pickTransition, requestResult } from './utils'
/**
* @public
@@ -65,6 +66,7 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
await this.handleCardUpdate(etx)
await this.handleTagAdd(etx)
await this.handleToDoDone(etx)
await this.handleApproveRequest(etx)
}
}
@@ -148,6 +150,43 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
}
}
private async handleApproveRequest (etx: Tx): Promise<void> {
if (etx._class === core.class.TxUpdateDoc) {
const cud = etx as TxUpdateDoc<ApproveRequest>
if (cud.objectClass !== process.class.ApproveRequest) return
if (cud.operations.doneOn == null || cud.operations.approved == null) return
const approveRequest = await this.client.findOne(process.class.ApproveRequest, {
_id: cud.objectId
})
if (approveRequest === undefined) return
const execution = await this.client.findOne(process.class.Execution, {
_id: approveRequest.execution
})
if (execution === undefined) return
const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId)
const transitions = this.client.getModel().findAllSync(
process.class.Transition,
{
process: execution.process,
from: execution.currentState,
trigger: cud.operations.approved
? process.trigger.OnApproveRequestApproved
: process.trigger.OnApproveRequestRejected
},
{ sort: { rank: SortingOrder.Ascending } }
)
const updatedApproveRequest = TxProcessor.updateDoc2Doc(approveRequest, cud)
const transition = await pickTransition(this.client, execution, transitions, {
todo: updatedApproveRequest
})
if (transition === undefined) return
const context = await getNextStateUserInput(execution, transition, execution.context)
await txop.update(execution, {
context
})
}
}
private async handleToDoDone (etx: Tx): Promise<void> {
if (etx._class === core.class.TxUpdateDoc) {
const cud = etx as TxUpdateDoc<ProcessToDo>
+28 -3
View File
@@ -22,19 +22,25 @@ export default mergeIds(processId, process, {
viewlet: {
ExecutionsList: '' as Ref<Viewlet>,
ExecutionLogList: '' as Ref<Viewlet>,
CardExecutions: '' as Ref<Viewlet>
CardExecutions: '' as Ref<Viewlet>,
CardRequests: '' as Ref<Viewlet>
},
component: {
Main: '' as AnyComponent,
ProcessEditor: '' as AnyComponent,
ProcessesSettingSection: '' as AnyComponent,
SubProcessEditor: '' as AnyComponent,
ApproveRequestEditor: '' as AnyComponent,
ApproveRequestPresenter: '' as AnyComponent,
ApproveRequestTriggerEditor: '' as AnyComponent,
ApproveRequestTriggerPresenter: '' as AnyComponent,
UpdateCardEditor: '' as AnyComponent,
ToDoEditor: '' as AnyComponent,
SubProcessPresenter: '' as AnyComponent,
ToDoPresenter: '' as AnyComponent,
RunProcessPopup: '' as AnyComponent,
UpdateCardPresenter: '' as AnyComponent,
RequestsExtension: '' as AnyComponent,
ProcessesExtension: '' as AnyComponent,
ProcessesHeaderExtension: '' as AnyComponent,
ProcessPresenter: '' as AnyComponent,
@@ -56,6 +62,7 @@ export default mergeIds(processId, process, {
ToDoCloseEditor: '' as AnyComponent,
ToDoRemoveEditor: '' as AnyComponent,
ProcessesCardSection: '' as AnyComponent,
RequestsCardSection: '' as AnyComponent,
TransitionEditor: '' as AnyComponent,
StateEditor: '' as AnyComponent,
TransitionRefPresenter: '' as AnyComponent,
@@ -77,7 +84,9 @@ export default mergeIds(processId, process, {
AddTagPresenter: '' as AnyComponent,
SubProcessMatchEditor: '' as AnyComponent,
SubProcessMatchPresenter: '' as AnyComponent,
FunctionSubmenu: '' as AnyComponent
FunctionSubmenu: '' as AnyComponent,
LockSectionEditor: '' as AnyComponent,
LockSectionPresenter: '' as AnyComponent
},
criteriaEditor: {
BaseCriteria: '' as AnyComponent,
@@ -217,7 +226,23 @@ export default mergeIds(processId, process, {
RunProcessPermission: '' as IntlString,
CancelProcessPermission: '' as IntlString,
ForbidRunProcessPermission: '' as IntlString,
ForbidCancelProcessPermission: '' as IntlString
ForbidCancelProcessPermission: '' as IntlString,
RequestApproval: '' as IntlString,
IsApproved: '' as IntlString,
Approve: '' as IntlString,
Reject: '' as IntlString,
Reason: '' as IntlString,
ApproveRequest: '' as IntlString,
OnApproveRequestApproved: '' as IntlString,
OnApproveRequestRejected: '' as IntlString,
ConfirmApproval: '' as IntlString,
ConfirmRejection: '' as IntlString,
RejectionReason: '' as IntlString,
ProvideRejectionReason: '' as IntlString,
FieldIsEmpty: '' as IntlString,
Reviewers: '' as IntlString,
LockCard: '' as IntlString,
LockSection: '' as IntlString
},
permission: {
RunProcess: '' as Ref<Permission>,
+26
View File
@@ -643,6 +643,26 @@ export function eventCheck (
return context.eventType === params.eventType
}
export async function approveRequestApproved (
client: Client,
execution: Execution,
params: Record<string, any>,
context: Record<string, any>
): Promise<boolean> {
if (params._id === undefined) return false
return context.todo?.group === params._id && context.todo?.approved === true
}
export async function approveRequestRejected (
client: Client,
execution: Execution,
params: Record<string, any>,
context: Record<string, any>
): Promise<boolean> {
if (params._id === undefined) return false
return context.todo?.group === params._id && context.todo?.approved === false
}
export function matchCardCheck (
client: Client,
execution: Execution,
@@ -756,3 +776,9 @@ export async function checkProcessSectionVisibility (doc: Card): Promise<boolean
const processes = client.getModel().findAllSync(process.class.Process, { masterTag: { $in: anc } })
return processes.length > 0
}
export async function checkRequestsSectionVisibility (doc: Card): Promise<boolean> {
const client = getClient()
const requests = await client.findOne(process.class.ApproveRequest, { card: doc._id })
return requests !== undefined
}