mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-12 12:47:45 +02:00
Approve requests (#10486)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -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>
|
||||
+43
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user