mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-11 01:40:32 +00:00
Process roles (#8654)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
c07db83d1b
commit
470c629f9f
@ -19,7 +19,8 @@ import {
|
||||
type Card,
|
||||
type MasterTag,
|
||||
type ParentInfo,
|
||||
type Tag
|
||||
type Tag,
|
||||
type Role
|
||||
} from '@hcengineering/card'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import core, {
|
||||
@ -47,7 +48,7 @@ import {
|
||||
type Builder
|
||||
} from '@hcengineering/model'
|
||||
import attachment from '@hcengineering/model-attachment'
|
||||
import { TClass, TDoc, TMixin, TSpace } from '@hcengineering/model-core'
|
||||
import { TAttachedDoc, TClass, TDoc, TMixin, TSpace } from '@hcengineering/model-core'
|
||||
import presentation from '@hcengineering/model-presentation'
|
||||
import setting from '@hcengineering/model-setting'
|
||||
import view, { createAction } from '@hcengineering/model-view'
|
||||
@ -115,6 +116,13 @@ export class MasterTagEditorSection extends TDoc implements MasterTagEditorSecti
|
||||
component!: AnyComponent
|
||||
}
|
||||
|
||||
@Model(card.class.Role, core.class.AttachedDoc, DOMAIN_MODEL)
|
||||
export class TRole extends TAttachedDoc implements Role {
|
||||
name!: string
|
||||
declare attachedTo: Ref<MasterTag | Tag>
|
||||
declare collection: 'roles'
|
||||
}
|
||||
|
||||
export * from './migration'
|
||||
|
||||
const listConfig: (BuildModelKey | string)[] = [
|
||||
@ -204,7 +212,7 @@ export function createSystemType (
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(TMasterTag, TTag, TCard, MasterTagEditorSection, TCardSpace)
|
||||
builder.createModel(TMasterTag, TTag, TCard, MasterTagEditorSection, TCardSpace, TRole)
|
||||
|
||||
createSystemType(builder, card.types.File, card.icon.File, attachment.string.File, attachment.string.Files)
|
||||
createSystemType(builder, card.types.Document, card.icon.Document, card.string.Document, card.string.Documents)
|
||||
@ -681,6 +689,12 @@ export function createModel (builder: Builder): void {
|
||||
component: card.component.RelationsSection
|
||||
})
|
||||
|
||||
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
|
||||
id: 'roles',
|
||||
label: core.string.Roles,
|
||||
component: card.component.RolesSection
|
||||
})
|
||||
|
||||
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
|
||||
id: 'views',
|
||||
label: card.string.Views,
|
||||
|
@ -15,11 +15,12 @@
|
||||
//
|
||||
|
||||
import activity from '@hcengineering/activity'
|
||||
import card, { type Role, type Card } from '@hcengineering/card'
|
||||
import {
|
||||
AvatarType,
|
||||
type UserRole,
|
||||
contactId,
|
||||
type AvatarProvider,
|
||||
type SocialIdentity,
|
||||
type Channel,
|
||||
type ChannelProvider,
|
||||
type Contact,
|
||||
@ -29,26 +30,27 @@ import {
|
||||
type Member,
|
||||
type Organization,
|
||||
type Person,
|
||||
type Status,
|
||||
type PersonSpace
|
||||
type PersonSpace,
|
||||
type SocialIdentity,
|
||||
type Status
|
||||
} from '@hcengineering/contact'
|
||||
import {
|
||||
AccountRole,
|
||||
ClassifierKind,
|
||||
DOMAIN_MODEL,
|
||||
DateRangeMode,
|
||||
IndexKind,
|
||||
type Collection,
|
||||
type AccountUuid,
|
||||
type Blob,
|
||||
type Class,
|
||||
type MarkupBlobRef,
|
||||
type Collection,
|
||||
type Domain,
|
||||
type Ref,
|
||||
type Timestamp,
|
||||
type SocialIdType,
|
||||
type PersonUuid,
|
||||
type MarkupBlobRef,
|
||||
type PersonId,
|
||||
type AccountUuid,
|
||||
ClassifierKind
|
||||
type PersonUuid,
|
||||
type Ref,
|
||||
type SocialIdType,
|
||||
type Timestamp
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
Collection as CollectionType,
|
||||
@ -84,7 +86,6 @@ import setting from '@hcengineering/setting'
|
||||
import templates from '@hcengineering/templates'
|
||||
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import { type Action } from '@hcengineering/view'
|
||||
import card, { type Card } from '@hcengineering/card'
|
||||
import contact from './plugin'
|
||||
|
||||
export { contactId } from '@hcengineering/contact'
|
||||
@ -92,6 +93,7 @@ export { contactOperation } from './migration'
|
||||
export { contact as default }
|
||||
|
||||
export const DOMAIN_CONTACT = 'contact' as Domain
|
||||
export const DOMAIN_ROLE = 'role' as Domain
|
||||
export const DOMAIN_CHANNEL = 'channel' as Domain
|
||||
|
||||
@Model(contact.class.AvatarProvider, core.class.Doc, DOMAIN_MODEL)
|
||||
@ -272,6 +274,12 @@ export class TPersonSpace extends TSpace implements PersonSpace {
|
||||
person!: Ref<Person>
|
||||
}
|
||||
|
||||
@Model(contact.class.UserRole, core.class.Doc, DOMAIN_ROLE)
|
||||
export class TUserRole extends TDoc implements UserRole {
|
||||
user!: Ref<Employee>
|
||||
role!: Ref<Role>
|
||||
}
|
||||
|
||||
function createUserProfileTag (builder: Builder): void {
|
||||
builder.createDoc(
|
||||
card.class.MasterTag,
|
||||
@ -334,7 +342,8 @@ export function createModel (builder: Builder): void {
|
||||
TStatus,
|
||||
TMember,
|
||||
TContactsTab,
|
||||
TPersonSpace
|
||||
TPersonSpace,
|
||||
TUserRole
|
||||
)
|
||||
|
||||
builder.mixin(contact.class.Contact, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
@ -164,6 +164,7 @@ export class TProcessFunction extends TDoc implements ProcessFunction {
|
||||
label!: IntlString
|
||||
editor?: AnyComponent
|
||||
allowMany?: boolean
|
||||
type!: 'transform' | 'reduce' | 'context'
|
||||
}
|
||||
|
||||
export * from './migration'
|
||||
@ -239,7 +240,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
of: core.class.TypeString,
|
||||
category: 'attribute',
|
||||
label: process.string.UpperCase
|
||||
label: process.string.UpperCase,
|
||||
type: 'transform'
|
||||
},
|
||||
process.function.UpperCase
|
||||
)
|
||||
@ -250,7 +252,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
of: core.class.TypeString,
|
||||
category: 'attribute',
|
||||
label: process.string.LowerCase
|
||||
label: process.string.LowerCase,
|
||||
type: 'transform'
|
||||
},
|
||||
process.function.LowerCase
|
||||
)
|
||||
@ -261,7 +264,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
of: core.class.TypeString,
|
||||
category: 'attribute',
|
||||
label: process.string.Trim
|
||||
label: process.string.Trim,
|
||||
type: 'transform'
|
||||
},
|
||||
process.function.Trim
|
||||
)
|
||||
@ -272,7 +276,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
of: core.class.ArrOf,
|
||||
category: undefined,
|
||||
label: process.string.FirstValue
|
||||
label: process.string.FirstValue,
|
||||
type: 'reduce'
|
||||
},
|
||||
process.function.FirstValue
|
||||
)
|
||||
@ -282,6 +287,7 @@ export function createModel (builder: Builder): void {
|
||||
core.space.Model,
|
||||
{
|
||||
of: core.class.ArrOf,
|
||||
type: 'reduce',
|
||||
category: undefined,
|
||||
label: process.string.LastValue
|
||||
},
|
||||
@ -293,6 +299,7 @@ export function createModel (builder: Builder): void {
|
||||
core.space.Model,
|
||||
{
|
||||
of: core.class.ArrOf,
|
||||
type: 'reduce',
|
||||
category: undefined,
|
||||
label: process.string.Random
|
||||
},
|
||||
@ -304,6 +311,7 @@ export function createModel (builder: Builder): void {
|
||||
core.space.Model,
|
||||
{
|
||||
of: core.class.TypeNumber,
|
||||
type: 'transform',
|
||||
category: 'attribute',
|
||||
label: process.string.Add,
|
||||
allowMany: true,
|
||||
@ -320,7 +328,8 @@ export function createModel (builder: Builder): void {
|
||||
category: 'attribute',
|
||||
label: process.string.Subtract,
|
||||
allowMany: true,
|
||||
editor: process.component.NumberOffsetEditor
|
||||
editor: process.component.NumberOffsetEditor,
|
||||
type: 'transform'
|
||||
},
|
||||
process.function.Subtract
|
||||
)
|
||||
@ -332,7 +341,8 @@ export function createModel (builder: Builder): void {
|
||||
of: core.class.TypeDate,
|
||||
category: 'attribute',
|
||||
label: process.string.Offset,
|
||||
editor: process.component.DateOffsetEditor
|
||||
editor: process.component.DateOffsetEditor,
|
||||
type: 'transform'
|
||||
},
|
||||
process.function.Offset
|
||||
)
|
||||
@ -343,11 +353,25 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
of: core.class.TypeDate,
|
||||
category: 'attribute',
|
||||
label: process.string.FirstWorkingDayAfter
|
||||
label: process.string.FirstWorkingDayAfter,
|
||||
type: 'transform'
|
||||
},
|
||||
process.function.FirstWorkingDayAfter
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
process.class.ProcessFunction,
|
||||
core.space.Model,
|
||||
{
|
||||
of: contact.mixin.Employee,
|
||||
editor: process.component.RoleEditor,
|
||||
category: 'array',
|
||||
label: core.string.Role,
|
||||
type: 'context'
|
||||
},
|
||||
process.function.RoleContext
|
||||
)
|
||||
|
||||
builder.mixin(process.class.Process, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: process.component.ProcessPresenter
|
||||
})
|
||||
|
@ -95,6 +95,10 @@ export function createModel (builder: Builder): void {
|
||||
func: serverProcess.transform.FirstWorkingDayAfter
|
||||
})
|
||||
|
||||
builder.mixin(process.function.RoleContext, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
|
||||
func: serverProcess.transform.RoleContext
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverProcess.trigger.OnExecutionContinue,
|
||||
txMatch: {
|
||||
|
@ -0,0 +1,47 @@
|
||||
<!--
|
||||
// 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 { MasterTag, Tag } from '@hcengineering/card'
|
||||
import core from '@hcengineering/core'
|
||||
import { Card, getClient } from '@hcengineering/presentation'
|
||||
import { EditBox } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import card from '../../plugin'
|
||||
|
||||
export let masterTag: MasterTag | Tag
|
||||
|
||||
let value: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
async function handleSave (): Promise<void> {
|
||||
const _id = await client.addCollection(
|
||||
card.class.Role,
|
||||
core.space.Model,
|
||||
masterTag._id,
|
||||
masterTag._class,
|
||||
'roles',
|
||||
{
|
||||
name: value
|
||||
}
|
||||
)
|
||||
dispatch('close', _id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card okAction={handleSave} label={core.string.Role} on:close width={'menu'} canSave={value.trim().length > 0}>
|
||||
<EditBox bind:value placeholder={core.string.Role} />
|
||||
</Card>
|
186
plugins/card-resources/src/components/settings/EditRole.svelte
Normal file
186
plugins/card-resources/src/components/settings/EditRole.svelte
Normal file
@ -0,0 +1,186 @@
|
||||
<!--
|
||||
// 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, { Role } from '@hcengineering/card'
|
||||
import contact, { Employee, UserRole } from '@hcengineering/contact'
|
||||
import { SelectUsersPopup, UserDetails } from '@hcengineering/contact-resources'
|
||||
import core, { Ref, WithLookup } from '@hcengineering/core'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { clearSettingsStore } from '@hcengineering/setting-resources'
|
||||
import { ButtonIcon, Icon, IconAdd, IconDelete, Modal, ModernButton, Scroller, showPopup } from '@hcengineering/ui'
|
||||
|
||||
export let _id: Ref<Role>
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const query = createQuery()
|
||||
let role: Role | undefined
|
||||
query.query(card.class.Role, { _id }, (res) => {
|
||||
role = res[0]
|
||||
})
|
||||
$: name = role?.name
|
||||
|
||||
const usersQ = createQuery()
|
||||
|
||||
let roles: WithLookup<UserRole>[] = []
|
||||
|
||||
usersQ.query(
|
||||
contact.class.UserRole,
|
||||
{ role: _id },
|
||||
(res) => {
|
||||
roles = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
user: contact.class.Person
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let users: Employee[] = []
|
||||
$: users = roles.map((role) => role.$lookup?.user).filter((p) => p !== undefined)
|
||||
|
||||
async function remove (): Promise<void> {
|
||||
if (role === undefined) {
|
||||
return
|
||||
}
|
||||
await client.remove(role)
|
||||
clearSettingsStore()
|
||||
}
|
||||
|
||||
async function removeAssignment (val: Ref<Employee>): Promise<void> {
|
||||
const toRemove = roles.filter((role) => role.user === val)
|
||||
for (const obj of toRemove) {
|
||||
await client.remove(obj)
|
||||
}
|
||||
}
|
||||
|
||||
function openSelectUsersPopup (): void {
|
||||
const selected = users.map((user) => user._id)
|
||||
showPopup(
|
||||
SelectUsersPopup,
|
||||
{
|
||||
okLabel: presentation.string.Add,
|
||||
disableDeselectFor: selected,
|
||||
skipInactive: true,
|
||||
selected,
|
||||
showStatus: true
|
||||
},
|
||||
'top',
|
||||
(result?: Ref<Employee>[]) => {
|
||||
if (result != null) {
|
||||
void changeUsers(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function changeUsers (persons: Ref<Employee>[]): Promise<void> {
|
||||
const current = new Set(users.map((user) => user._id))
|
||||
const newPersons = persons.filter((person) => !current.has(person))
|
||||
if (newPersons.length === 0) {
|
||||
return
|
||||
}
|
||||
const apply = client.apply()
|
||||
for (const person of newPersons) {
|
||||
await apply.createDoc(contact.class.UserRole, contact.space.Contacts, { role: _id, user: person })
|
||||
}
|
||||
await apply.commit()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
label={core.string.Role}
|
||||
type={'type-aside'}
|
||||
showCancelButton={false}
|
||||
canSave={true}
|
||||
okLabel={presentation.string.Close}
|
||||
okAction={clearSettingsStore}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
<ButtonIcon icon={IconDelete} size={'small'} kind={'tertiary'} on:click={remove} />
|
||||
</svelte:fragment>
|
||||
<div class="hulyModal-content__titleGroup">
|
||||
<div class="flex fs-title items-center flex-gap-2">
|
||||
<Icon icon={contact.icon.Person} size={'medium'} />
|
||||
<div>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hulyModal-content__settingsSet">
|
||||
<div class="item" style:padding="var(--spacing-1_5)" class:withoutBorder={users.length === 0}>
|
||||
<ModernButton
|
||||
label={presentation.string.Add}
|
||||
icon={IconAdd}
|
||||
iconSize="small"
|
||||
kind="secondary"
|
||||
size="small"
|
||||
on:click={openSelectUsersPopup}
|
||||
/>
|
||||
</div>
|
||||
{#each users as user, index (user._id)}
|
||||
<div class="item" class:withoutBorder={index === users.length - 1}>
|
||||
<div class="item__content">
|
||||
<UserDetails person={user} showStatus />
|
||||
<div class="item__action">
|
||||
<ButtonIcon
|
||||
icon={IconDelete}
|
||||
size="small"
|
||||
on:click={async () => {
|
||||
await removeAssignment(user._id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
.item {
|
||||
padding: var(--spacing-0_75);
|
||||
|
||||
&.withoutBorder {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.item__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-0_75);
|
||||
border-radius: var(--small-BorderRadius);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--global-ui-highlight-BackgroundColor);
|
||||
|
||||
.item__action {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item__action {
|
||||
visibility: hidden;
|
||||
|
||||
&:hover {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,72 @@
|
||||
<!--
|
||||
// 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 { MasterTag, Role, Tag } from '@hcengineering/card'
|
||||
import contact from '@hcengineering/contact'
|
||||
import core from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { clearSettingsStore, settingsStore } from '@hcengineering/setting-resources'
|
||||
import { ButtonIcon, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
import card from '../../plugin'
|
||||
import CreateRolePopup from './CreateRolePopup.svelte'
|
||||
|
||||
export let masterTag: MasterTag | Tag
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const ancestors = client.getHierarchy().getAncestors(masterTag._id)
|
||||
|
||||
let roles = client.getModel().findAllSync(card.class.Role, { attachedTo: { $in: ancestors } })
|
||||
const query = createQuery()
|
||||
query.query(card.class.Role, { attachedTo: { $in: ancestors } }, (res) => {
|
||||
roles = res
|
||||
})
|
||||
|
||||
function addRole (): void {
|
||||
showPopup(CreateRolePopup, { masterTag }, undefined, (res) => {
|
||||
if (res != null) {
|
||||
$settingsStore = { id: res, component: card.component.EditRole, props: { _id: res } }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSelect = (role: Role): void => {
|
||||
$settingsStore = { id: role._id, component: card.component.EditRole, props: { _id: role._id } }
|
||||
}
|
||||
onDestroy(() => {
|
||||
clearSettingsStore()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="hulyTableAttr-header font-medium-12">
|
||||
<Icon icon={contact.icon.User} size="small" />
|
||||
<span><Label label={core.string.Roles} /></span>
|
||||
<ButtonIcon kind="primary" icon={IconAdd} size="small" dataId={'btnAdd'} on:click={addRole} />
|
||||
</div>
|
||||
<div class="hulyTableAttr-content task">
|
||||
{#each roles as role}
|
||||
<button
|
||||
class="hulyTableAttr-content__row justify-start"
|
||||
on:click|stopPropagation={() => {
|
||||
handleSelect(role)
|
||||
}}
|
||||
>
|
||||
<div class="hulyTableAttr-content__row-label font-medium-14 cursor-pointer">
|
||||
{role.name}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
@ -47,6 +47,8 @@ import CardArrayEditor from './components/CardArrayEditor.svelte'
|
||||
import NewCardHeader from './components/navigator/NewCardHeader.svelte'
|
||||
import SpacePresenter from './components/navigator/SpacePresenter.svelte'
|
||||
import LabelsPresenter from './components/LabelsPresenter.svelte'
|
||||
import RolesSection from './components/settings/RolesSection.svelte'
|
||||
import EditRole from './components/settings/EditRole.svelte'
|
||||
|
||||
export { default as CardSelector } from './components/CardSelector.svelte'
|
||||
|
||||
@ -76,7 +78,9 @@ export default async (): Promise<Resources> => ({
|
||||
CardArrayEditor,
|
||||
NewCardHeader,
|
||||
SpacePresenter,
|
||||
LabelsPresenter
|
||||
LabelsPresenter,
|
||||
RolesSection,
|
||||
EditRole
|
||||
},
|
||||
completion: {
|
||||
CardQuery: queryCard
|
||||
|
@ -46,7 +46,9 @@ export default mergeIds(cardId, card, {
|
||||
CardArrayEditor: '' as AnyComponent,
|
||||
NewCardHeader: '' as AnyComponent,
|
||||
SpacePresenter: '' as AnyComponent,
|
||||
LabelsPresenter: '' as AnyComponent
|
||||
LabelsPresenter: '' as AnyComponent,
|
||||
RolesSection: '' as AnyComponent,
|
||||
EditRole: '' as AnyComponent
|
||||
},
|
||||
completion: {
|
||||
CardQuery: '' as Resource<ObjectSearchFactory>,
|
||||
|
@ -11,7 +11,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Blobs, Class, Doc, Domain, MarkupBlobRef, Mixin, Rank, Ref, Space } from '@hcengineering/core'
|
||||
import {
|
||||
AttachedDoc,
|
||||
Blobs,
|
||||
Class,
|
||||
CollectionSize,
|
||||
Doc,
|
||||
Domain,
|
||||
MarkupBlobRef,
|
||||
Mixin,
|
||||
Rank,
|
||||
Ref,
|
||||
Space
|
||||
} from '@hcengineering/core'
|
||||
import { Asset, IntlString, plugin, Plugin } from '@hcengineering/platform'
|
||||
import type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
|
||||
|
||||
@ -20,10 +32,16 @@ export * from './analytics'
|
||||
export interface MasterTag extends Class<Card> {
|
||||
color?: number
|
||||
removed?: boolean
|
||||
roles?: CollectionSize<Role>
|
||||
}
|
||||
|
||||
export interface Tag extends MasterTag, Mixin<Card> {}
|
||||
|
||||
export interface Role extends AttachedDoc<MasterTag | Tag, 'roles'> {
|
||||
name: string
|
||||
attachedTo: Ref<MasterTag | Tag>
|
||||
}
|
||||
|
||||
export interface Card extends Doc {
|
||||
_class: Ref<MasterTag>
|
||||
title: string
|
||||
@ -69,7 +87,8 @@ const cardPlugin = plugin(cardId, {
|
||||
MasterTag: '' as Ref<Class<MasterTag>>,
|
||||
Tag: '' as Ref<Class<Tag>>,
|
||||
MasterTagEditorSection: '' as Ref<Class<MasterTagEditorSection>>,
|
||||
CardSpace: '' as Ref<Class<CardSpace>>
|
||||
CardSpace: '' as Ref<Class<CardSpace>>,
|
||||
Role: '' as Ref<Class<Role>>
|
||||
},
|
||||
space: {
|
||||
Default: '' as Ref<CardSpace>
|
||||
|
@ -38,7 +38,7 @@ import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates'
|
||||
import type { AnyComponent, ColorDefinition, ResolvedLocation, Location, ComponentExtensionId } from '@hcengineering/ui'
|
||||
import { Action, FilterMode, Viewlet } from '@hcengineering/view'
|
||||
import type { Readable } from 'svelte/store'
|
||||
import { Card, MasterTag } from '@hcengineering/card'
|
||||
import { Card, MasterTag, Role } from '@hcengineering/card'
|
||||
import { PermissionsStore } from './types'
|
||||
|
||||
/**
|
||||
@ -144,6 +144,11 @@ export interface Person extends Contact, BasePerson {
|
||||
profile?: Ref<Card>
|
||||
}
|
||||
|
||||
export interface UserRole extends Doc {
|
||||
user: Ref<Employee>
|
||||
role: Ref<Role>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -213,7 +218,8 @@ export const contactPlugin = plugin(contactId, {
|
||||
ContactsTab: '' as Ref<Class<ContactsTab>>,
|
||||
PersonSpace: '' as Ref<Class<PersonSpace>>,
|
||||
SocialIdentity: '' as Ref<Class<SocialIdentity>>,
|
||||
UserProfile: '' as Ref<MasterTag>
|
||||
UserProfile: '' as Ref<MasterTag>,
|
||||
UserRole: '' as Ref<Class<UserRole>>
|
||||
},
|
||||
mixin: {
|
||||
Employee: '' as Ref<Class<Employee>>
|
||||
|
@ -65,6 +65,7 @@
|
||||
"ObjectNotFound": "Objekt nenalezen: {_id}",
|
||||
"EmptyRelatedObjectValue": "Prázdná hodnota {attr} souvisejícího objektu: {parent}",
|
||||
"InternalServerError": "Vnitřní chyba serveru, kontaktujte podporu, chybové id: {errorId}",
|
||||
"ResultNotProvided": "Výsledek nebyl poskytnut"
|
||||
"ResultNotProvided": "Výsledek nebyl poskytnut",
|
||||
"EmptyFunctionResult": "Prázdný výsledek funkce {func}"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,7 @@
|
||||
"ObjectNotFound": "Objekt nicht gefunden: {_id}",
|
||||
"EmptyRelatedObjectValue": "Leerer verwandtes Objekt {parent} Wert: {attr}",
|
||||
"InternalServerError": "Interner Serverfehler, bitte kontaktieren Sie den Support, Fehler-ID: {errorId}",
|
||||
"ResultNotProvided": "Ergebnis nicht bereitgestellt"
|
||||
"ResultNotProvided": "Ergebnis nicht bereitgestellt",
|
||||
"EmptyFunctionResult": "Leerer Funktionsresultat {func}"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,7 @@
|
||||
"ObjectNotFound": "Object not found: {_id}",
|
||||
"EmptyRelatedObjectValue": "Empty related object {parent} value: {attr}",
|
||||
"InternalServerError": "Internal server error, contact support, error id: {errorId}",
|
||||
"ResultNotProvided": "Result not provided"
|
||||
"ResultNotProvided": "Result not provided",
|
||||
"EmptyFunctionResult": "Empty function result {func}"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,7 @@
|
||||
"ObjectNotFound": "Objeto no encontrado: {_id}",
|
||||
"EmptyRelatedObjectValue": "Valor de objeto relacionado vacío: {parent} {attr}",
|
||||
"InternalServerError": "Error interno del servidor, contacte con el soporte, id de error: {errorId}",
|
||||
"ResultNotProvided": "Resultado no proporcionado"
|
||||
"ResultNotProvided": "Resultado no proporcionado",
|
||||
"EmptyFunctionResult": "Resultado de función vacío {func}"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,7 @@
|
||||
"ObjectNotFound": "Objet introuvable : {_id}",
|
||||
"EmptyRelatedObjectValue": "Valeur d'objet lié vide : {parent} {attr}",
|
||||
"InternalServerError": "Erreur interne du serveur, contactez le support, id d'erreur : {errorId}",
|
||||
"ResultNotProvided": "Résultat non fourni"
|
||||
"ResultNotProvided": "Résultat non fourni",
|
||||
"EmptyFunctionResult": "Résultat de fonction vide {func}"
|
||||
}
|
||||
}
|
@ -65,6 +65,7 @@
|
||||
"ObjectNotFound": "Oggetto non trovato: {_id}",
|
||||
"EmptyRelatedObjectValue": "Valore oggetto correlato vuoto: {parent} {attr}",
|
||||
"InternalServerError": "Errore interno del server, contatta il supporto, id errore: {errorId}",
|
||||
"ResultNotProvided": "Risultato non fornito"
|
||||
"ResultNotProvided": "Risultato non fornito",
|
||||
"EmptyFunctionResult": "Risultato funzione vuoto {func}"
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,7 @@
|
||||
"UserRequestedValueNotProvided": "ユーザーが要求した値が提供されていません: {attr}",
|
||||
"ObjectNotFound": "オブジェクトが見つかりません: {_id}",
|
||||
"EmptyRelatedObjectValue": "関連オブジェクト {parent} の値が空です: {attr}",
|
||||
"InternalServerError": "内部サーバーエラー。サポートにお問い合わせください。エラーID: {errorId}"
|
||||
"InternalServerError": "内部サーバーエラー。サポートにお問い合わせください。エラーID: {errorId}",
|
||||
"EmptyFunctionResult": "関数結果が空です {func}"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,7 @@
|
||||
"ObjectNotFound": "Objeto não encontrado: {_id}",
|
||||
"EmptyRelatedObjectValue": "Valor de objeto relacionado vazio: {parent} {attr}",
|
||||
"InternalServerError": "Erro interno do servidor, contate o suporte, id de erro: {errorId}",
|
||||
"ResultNotProvided": "Resultado não fornecido"
|
||||
"ResultNotProvided": "Resultado não fornecido",
|
||||
"EmptyFunctionResult": "Resultado da função vazio {func}"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,7 @@
|
||||
"UserRequestedValueNotProvided": "Знвчение не предоставлено пользователем: {attr}",
|
||||
"ObjectNotFound": "Объект не найден: {_id}",
|
||||
"AttributeNotExists": "Атрибут не существует: {key}",
|
||||
"ResultNotProvided": "Результат не предоставлен"
|
||||
"ResultNotProvided": "Результат не предоставлен",
|
||||
"EmptyFunctionResult": "Отсутствуют результат функции {func}"
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,7 @@
|
||||
"ObjectNotFound": "找不到对象:{_id}",
|
||||
"EmptyRelatedObjectValue": "空相关对象值:{parent} {attr}",
|
||||
"InternalServerError": "内部服务器错误,请联系支持,错误 ID:{errorId}",
|
||||
"ResultNotProvided": "未提供结果"
|
||||
"ResultNotProvided": "未提供结果",
|
||||
"EmptyFunctionResult": "空函数结果 {func}"
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,9 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ContextSelectorPopup from './attributeEditors/ContextSelectorPopup.svelte'
|
||||
import ContextValue from './attributeEditors/ContextValue.svelte'
|
||||
import { MasterTag, Tag } from '@hcengineering/card'
|
||||
|
||||
export let masterTag: Ref<MasterTag | Tag>
|
||||
export let value: any
|
||||
export let context: Context
|
||||
export let presenterClass: {
|
||||
@ -52,6 +54,7 @@
|
||||
showPopup(
|
||||
ContextSelectorPopup,
|
||||
{
|
||||
masterTag,
|
||||
context,
|
||||
attribute,
|
||||
onSelect
|
||||
@ -78,6 +81,7 @@
|
||||
<div class="text-input" class:context={contextValue}>
|
||||
{#if contextValue}
|
||||
<ContextValue
|
||||
{masterTag}
|
||||
{contextValue}
|
||||
{context}
|
||||
{attribute}
|
||||
|
@ -62,6 +62,7 @@
|
||||
{attribute}
|
||||
{presenterClass}
|
||||
{value}
|
||||
masterTag={process.masterTag}
|
||||
{allowRemove}
|
||||
on:remove
|
||||
on:change={(e) => {
|
||||
|
@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { generateId, Ref } from '@hcengineering/core'
|
||||
import core, { Ref } from '@hcengineering/core'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
|
||||
import { Process, SelectedUserRequest, State } from '@hcengineering/process'
|
||||
import { Process, State } from '@hcengineering/process'
|
||||
import { settingsStore } from '@hcengineering/setting-resources'
|
||||
import {
|
||||
Button,
|
||||
@ -29,21 +29,20 @@
|
||||
IconDescription,
|
||||
navigate,
|
||||
NavItem,
|
||||
ToggleWithLabel,
|
||||
Scroller,
|
||||
secondNavSeparators,
|
||||
Separator,
|
||||
showPopup
|
||||
showPopup,
|
||||
ToggleWithLabel
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import process from '../plugin'
|
||||
import { getToDoEndAction } from '../utils'
|
||||
import Aside from './Aside.svelte'
|
||||
import ArrowEnd from './icons/ArrowEnd.svelte'
|
||||
import ArrowStart from './icons/ArrowStart.svelte'
|
||||
import StateEditor from './StateEditor.svelte'
|
||||
import TransitionEditor from './TransitionEditor.svelte'
|
||||
import { getToDoEndAction } from '../utils'
|
||||
import ExecutionResultEditor from './ExecutionResultEditor.svelte'
|
||||
|
||||
export let _id: Ref<Process>
|
||||
export let visibleSecondNav: boolean = true
|
||||
|
@ -13,6 +13,9 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Context, Func, ProcessFunction, SelectedContext } from '@hcengineering/process'
|
||||
import {
|
||||
ButtonIcon,
|
||||
CheckBox,
|
||||
@ -26,14 +29,13 @@
|
||||
showPopup,
|
||||
Submenu
|
||||
} from '@hcengineering/ui'
|
||||
import { AttributeCategory } from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import plugin from '../../plugin'
|
||||
import core, { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { Context, Func, ProcessFunction, SelectedContext } from '@hcengineering/process'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { AttributeCategory } from '@hcengineering/view'
|
||||
import FallbackEditor from '../contextEditors/FallbackEditor.svelte'
|
||||
import { MasterTag, Tag } from '@hcengineering/card'
|
||||
|
||||
export let masterTag: Ref<MasterTag | Tag>
|
||||
export let contextValue: SelectedContext
|
||||
export let context: Context
|
||||
export let attribute: AnyAttribute
|
||||
@ -63,7 +65,7 @@
|
||||
|
||||
const reduceFuncs = client
|
||||
.getModel()
|
||||
.findAllSync(plugin.class.ProcessFunction, { of: core.class.ArrOf })
|
||||
.findAllSync(plugin.class.ProcessFunction, { type: 'reduce' })
|
||||
.map((it) => it._id)
|
||||
|
||||
$: availableFunctions = getAvailableFunctions(context, contextValue.functions, attrClass, category)
|
||||
@ -77,7 +79,9 @@
|
||||
category: AttributeCategory
|
||||
): Ref<ProcessFunction>[] {
|
||||
const result: Ref<ProcessFunction>[] = []
|
||||
const allFunctions = client.getModel().findAllSync(plugin.class.ProcessFunction, { of: attrClass, category })
|
||||
const allFunctions = client
|
||||
.getModel()
|
||||
.findAllSync(plugin.class.ProcessFunction, { of: attrClass, category, type: 'transform' })
|
||||
for (const f of allFunctions) {
|
||||
if (functions === undefined || f.allowMany === true || functions.findIndex((p) => p.func === f._id) === -1) {
|
||||
result.push(f._id)
|
||||
@ -179,6 +183,7 @@
|
||||
func.editor,
|
||||
{
|
||||
func,
|
||||
masterTag,
|
||||
context,
|
||||
attribute,
|
||||
props: val?.props ?? {}
|
||||
@ -192,7 +197,6 @@
|
||||
func.props = res
|
||||
contextValue.functions[pos] = func
|
||||
contextValue.functions = contextValue.functions
|
||||
console.log(contextValue.functions)
|
||||
onChange(contextValue)
|
||||
}
|
||||
}
|
||||
@ -309,8 +313,8 @@
|
||||
/>
|
||||
<!-- <div class="menu-separator" /> -->
|
||||
{/if}
|
||||
<div class="menu-separator" />
|
||||
{/if}
|
||||
<div class="menu-separator" />
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<button
|
||||
bind:this={elements[functionButtonIndex + 1]}
|
||||
|
@ -13,13 +13,16 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AnyAttribute, generateId } from '@hcengineering/core'
|
||||
import { Context, SelectedContext } from '@hcengineering/process'
|
||||
import { AnyAttribute, generateId, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Context, ProcessFunction, SelectedContext } from '@hcengineering/process'
|
||||
import { Label, resizeObserver, Scroller, Submenu } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import plugin from '../../plugin'
|
||||
import { getValueReduceFunc } from '../../utils'
|
||||
import { MasterTag, Tag } from '@hcengineering/card'
|
||||
|
||||
export let masterTag: Ref<MasterTag | Tag>
|
||||
export let context: Context
|
||||
export let attribute: AnyAttribute
|
||||
export let onSelect: (val: SelectedContext | null) => void
|
||||
@ -58,20 +61,20 @@
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
function getIndex (ownIndex: number, kind: 'attribute' | 'relation' | 'nested' | 'total'): number {
|
||||
if (kind === 'attribute') {
|
||||
return ownIndex + 1
|
||||
}
|
||||
if (kind === 'nested') {
|
||||
return ownIndex + context.attributes.length + 1
|
||||
}
|
||||
if (kind === 'relation') {
|
||||
return ownIndex + context.attributes.length + nested.length + 1
|
||||
}
|
||||
if (kind === 'total') {
|
||||
return ownIndex + context.attributes.length + relations.length + nested.length + 1
|
||||
}
|
||||
return ownIndex
|
||||
function onFunc (func: Ref<ProcessFunction>): void {
|
||||
onSelect({
|
||||
type: 'function',
|
||||
key: attribute.name,
|
||||
func,
|
||||
props: {}
|
||||
})
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
function getFunc (func: Ref<ProcessFunction>): ProcessFunction {
|
||||
const client = getClient()
|
||||
const f = client.getModel().getObject(func)
|
||||
return f
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -89,8 +92,38 @@
|
||||
</span>
|
||||
</button>
|
||||
<div class="menu-separator" />
|
||||
{#if context.functions.length > 0}
|
||||
{#each context.functions as f}
|
||||
{@const func = getFunc(f)}
|
||||
{#if func.editor !== undefined}
|
||||
<Submenu
|
||||
label={func.label}
|
||||
props={{
|
||||
masterTag,
|
||||
context: func,
|
||||
target: attribute,
|
||||
onSelect: onClick
|
||||
}}
|
||||
options={{ component: func.editor }}
|
||||
withHover
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
on:click={() => {
|
||||
onFunc(func._id)
|
||||
}}
|
||||
class="menu-item"
|
||||
>
|
||||
<span class="overflow-label pr-1">
|
||||
<Label label={func.label} />
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="menu-separator" />
|
||||
{/if}
|
||||
{#if context.attributes.length > 0}
|
||||
{#each context.attributes as attr, i}
|
||||
{#each context.attributes as attr}
|
||||
<button
|
||||
on:click={() => {
|
||||
onAttribute(attr)
|
||||
@ -105,8 +138,7 @@
|
||||
<div class="menu-separator" />
|
||||
{/if}
|
||||
{#if nested.length > 0}
|
||||
{#each nested as object, i}
|
||||
{@const index = getIndex(i, 'nested')}
|
||||
{#each nested as object}
|
||||
<Submenu
|
||||
label={object.attribute.label}
|
||||
props={{
|
||||
@ -121,8 +153,7 @@
|
||||
<div class="menu-separator" />
|
||||
{/if}
|
||||
{#if relations.length > 0}
|
||||
{#each relations as object, i}
|
||||
{@const index = getIndex(i, 'relation')}
|
||||
{#each relations as object}
|
||||
<Submenu
|
||||
text={object[0]}
|
||||
props={{
|
||||
|
@ -20,7 +20,9 @@
|
||||
import ContextValuePresenter from './ContextValuePresenter.svelte'
|
||||
import { AttributeCategory } from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { MasterTag, Tag } from '@hcengineering/card'
|
||||
|
||||
export let masterTag: Ref<MasterTag | Tag>
|
||||
export let contextValue: SelectedContext
|
||||
export let context: Context
|
||||
export let attribute: AnyAttribute
|
||||
@ -46,7 +48,7 @@
|
||||
}
|
||||
showPopup(
|
||||
ConfigurePopup,
|
||||
{ contextValue, attrClass, category, attribute, context, onChange },
|
||||
{ contextValue, attrClass, masterTag, category, attribute, context, onChange },
|
||||
eventToHTMLElement(e)
|
||||
)
|
||||
}
|
||||
|
@ -19,6 +19,7 @@
|
||||
import AttrContextPresenter from './AttrContextPresenter.svelte'
|
||||
import NestedContextPresenter from './NestedContextPresenter.svelte'
|
||||
import RelContextPresenter from './RelContextPresenter.svelte'
|
||||
import FunctionContextPresenter from './FunctionContextPresenter.svelte'
|
||||
|
||||
export let contextValue: SelectedContext
|
||||
export let context: Context
|
||||
@ -33,6 +34,8 @@
|
||||
<NestedContextPresenter {contextValue} {context} />
|
||||
{:else if contextValue.type === 'userRequest'}
|
||||
<Label label={plugin.string.RequestFromUser} />
|
||||
{:else if contextValue.type === 'function'}
|
||||
<FunctionContextPresenter {contextValue} {context} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
<!--
|
||||
// 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 { getClient } from '@hcengineering/presentation'
|
||||
import { Context, SelectedContextFunc } from '@hcengineering/process'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
|
||||
export let contextValue: SelectedContextFunc
|
||||
export let context: Context
|
||||
|
||||
const client = getClient()
|
||||
const func = client.getModel().findObject(contextValue.func)
|
||||
</script>
|
||||
|
||||
{#if func !== undefined}
|
||||
<Label label={func.label} />
|
||||
{/if}
|
@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { AnyAttribute } from '@hcengineering/core'
|
||||
import { MasterTag, Tag } from '@hcengineering/card'
|
||||
import core, { AnyAttribute, Ref } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
||||
import { Context, ProcessFunction } from '@hcengineering/process'
|
||||
@ -23,6 +24,7 @@
|
||||
import ProcessAttribute from '../ProcessAttribute.svelte'
|
||||
|
||||
export let func: ProcessFunction
|
||||
export let masterTag: Ref<MasterTag | Tag>
|
||||
export let context: Context
|
||||
export let attribute: AnyAttribute
|
||||
export let props: Record<string, any> = {}
|
||||
@ -59,6 +61,7 @@
|
||||
|
||||
<Card on:close width={'menu'} label={func.label} canSave okAction={save} okLabel={presentation.string.Save}>
|
||||
<ProcessAttribute
|
||||
{masterTag}
|
||||
{context}
|
||||
{attribute}
|
||||
presenterClass={{
|
||||
|
@ -0,0 +1,91 @@
|
||||
<!--
|
||||
// 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, { MasterTag, Role, Tag } from '@hcengineering/card'
|
||||
import { AnyAttribute, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { ProcessFunction, SelectedContext } from '@hcengineering/process'
|
||||
import { resizeObserver, Scroller } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getContextFunctionReduce } from '../../utils'
|
||||
|
||||
export let context: ProcessFunction
|
||||
export let masterTag: Ref<MasterTag | Tag>
|
||||
export let target: AnyAttribute
|
||||
export let onSelect: (val: SelectedContext) => void
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
const elements: HTMLButtonElement[] = []
|
||||
|
||||
const keyDown = (event: KeyboardEvent, index: number): void => {
|
||||
if (event.key === 'ArrowDown') {
|
||||
elements[(index + 1) % elements.length].focus()
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
elements[(elements.length + index - 1) % elements.length].focus()
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
dispatch('close')
|
||||
}
|
||||
}
|
||||
|
||||
function onValue (role: Ref<Role>): void {
|
||||
const pathReduce = getContextFunctionReduce(context, target)
|
||||
onSelect({
|
||||
type: 'function',
|
||||
func: context._id,
|
||||
key: target.name,
|
||||
functions: [],
|
||||
sourceFunction: pathReduce,
|
||||
props: {
|
||||
target: role
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const ancestors = client.getHierarchy().getAncestors(masterTag)
|
||||
const roles = client.getModel().findAllSync(card.class.Role, { attachedTo: { $in: ancestors } })
|
||||
</script>
|
||||
|
||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||
<div class="menu-space" />
|
||||
<Scroller>
|
||||
{#each roles as role, i}
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<button
|
||||
bind:this={elements[i]}
|
||||
on:keydown={(event) => {
|
||||
keyDown(event, i)
|
||||
}}
|
||||
on:mouseover={() => {
|
||||
elements[i]?.focus()
|
||||
}}
|
||||
on:click={() => {
|
||||
onValue(role._id)
|
||||
}}
|
||||
class="menu-item"
|
||||
>
|
||||
<span class="overflow-label pr-1">
|
||||
{role.name}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</Scroller>
|
||||
<div class="menu-space" />
|
||||
</div>
|
@ -35,6 +35,7 @@ import SubProcessEditor from './components/SubProcessEditor.svelte'
|
||||
import ToDoEditor from './components/ToDoEditor.svelte'
|
||||
import UpdateCardEditor from './components/UpdateCardEditor.svelte'
|
||||
import ResultInput from './components/contextEditors/ResultInput.svelte'
|
||||
import RoleEditor from './components/contextEditors/RoleEditor.svelte'
|
||||
|
||||
import { continueExecution, showDoneQuery } from './utils'
|
||||
import { ProcessMiddleware } from './middleware'
|
||||
@ -66,7 +67,8 @@ export default async (): Promise<Resources> => ({
|
||||
NumberOffsetEditor,
|
||||
ErrorPresenter,
|
||||
RequestUserInput,
|
||||
ResultInput
|
||||
ResultInput,
|
||||
RoleEditor
|
||||
},
|
||||
function: {
|
||||
ShowDoneQuery: showDoneQuery,
|
||||
|
@ -46,7 +46,8 @@ export default mergeIds(processId, process, {
|
||||
NumberOffsetEditor: '' as AnyComponent,
|
||||
ErrorPresenter: '' as AnyComponent,
|
||||
RequestUserInput: '' as AnyComponent,
|
||||
ResultInput: '' as AnyComponent
|
||||
ResultInput: '' as AnyComponent,
|
||||
RoleEditor: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
ShowDoneQuery: '' as ViewQueryAction,
|
||||
|
@ -66,6 +66,8 @@ export function getContext (
|
||||
if (attr !== undefined && category === 'object') {
|
||||
attributes = attributes.filter((it) => it._id !== attr)
|
||||
}
|
||||
|
||||
const functions = getContextFunctions(client, process.masterTag, target, category)
|
||||
const nested: Record<string, NestedContext> = {}
|
||||
const relations: Record<string, RelatedContext> = {}
|
||||
|
||||
@ -119,12 +121,55 @@ export function getContext (
|
||||
}
|
||||
|
||||
return {
|
||||
functions,
|
||||
attributes,
|
||||
nested,
|
||||
relations
|
||||
}
|
||||
}
|
||||
|
||||
function getContextFunctions (
|
||||
client: Client,
|
||||
_class: Ref<Class<Doc>>,
|
||||
target: Ref<Class<Type<any>>>,
|
||||
category: AttributeCategory
|
||||
): Array<Ref<ProcessFunction>> {
|
||||
const matched: Array<Ref<ProcessFunction>> = []
|
||||
const hierarchy = client.getHierarchy()
|
||||
const funcs = client.getModel().findAllSync(process.class.ProcessFunction, { type: 'context' })
|
||||
for (const func of funcs) {
|
||||
switch (category) {
|
||||
case 'object': {
|
||||
if (func.category === 'array') {
|
||||
if (hierarchy.isDerived(func.of, target)) {
|
||||
matched.push(func._id)
|
||||
}
|
||||
}
|
||||
if (func.category === 'object') {
|
||||
if (hierarchy.isDerived(func.of, target)) {
|
||||
matched.push(func._id)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'array': {
|
||||
if (func.category === 'array') {
|
||||
if (hierarchy.isDerived(func.of, target)) {
|
||||
matched.push(func._id)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (func.of === target) {
|
||||
matched.push(func._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
function getClassAttributes (
|
||||
client: Client,
|
||||
_class: Ref<Class<Doc>>,
|
||||
@ -192,6 +237,15 @@ export function getValueReduceFunc (source: AnyAttribute, target: AnyAttribute):
|
||||
return process.function.FirstValue
|
||||
}
|
||||
|
||||
export function getContextFunctionReduce (
|
||||
func: ProcessFunction,
|
||||
target: AnyAttribute
|
||||
): Ref<ProcessFunction> | undefined {
|
||||
if (func.category !== 'array') return undefined
|
||||
if (target.type._class === core.class.ArrOf) return undefined
|
||||
return process.function.FirstValue
|
||||
}
|
||||
|
||||
export function showDoneQuery (value: any, query: DocumentQuery<Doc>): DocumentQuery<Doc> {
|
||||
if (value === false) {
|
||||
return { ...query, done: false }
|
||||
|
@ -91,6 +91,7 @@ export interface Method<T extends Doc> extends Doc {
|
||||
}
|
||||
|
||||
export interface ProcessFunction extends Doc {
|
||||
type: 'transform' | 'reduce' | 'context'
|
||||
of: Ref<Class<Doc>>
|
||||
editor?: AnyComponent
|
||||
category: AttributeCategory | undefined
|
||||
@ -134,7 +135,8 @@ export default plugin(processId, {
|
||||
ObjectNotFound: '' as IntlString,
|
||||
AttributeNotExists: '' as IntlString,
|
||||
UserRequestedValueNotProvided: '' as IntlString,
|
||||
ResultNotProvided: '' as IntlString
|
||||
ResultNotProvided: '' as IntlString,
|
||||
EmptyFunctionResult: '' as IntlString
|
||||
},
|
||||
icon: {
|
||||
Process: '' as Asset,
|
||||
@ -153,6 +155,7 @@ export default plugin(processId, {
|
||||
Add: '' as Ref<ProcessFunction>,
|
||||
Subtract: '' as Ref<ProcessFunction>,
|
||||
Offset: '' as Ref<ProcessFunction>,
|
||||
FirstWorkingDayAfter: '' as Ref<ProcessFunction>
|
||||
FirstWorkingDayAfter: '' as Ref<ProcessFunction>,
|
||||
RoleContext: '' as Ref<ProcessFunction>
|
||||
}
|
||||
})
|
||||
|
@ -2,6 +2,7 @@ import { Class, Doc, type AnyAttribute, type Association, type Ref } from '@hcen
|
||||
import { ProcessFunction } from '.'
|
||||
|
||||
export interface Context {
|
||||
functions: Ref<ProcessFunction>[]
|
||||
attributes: AnyAttribute[]
|
||||
nested: Record<string, NestedContext>
|
||||
relations: Record<string, RelatedContext>
|
||||
@ -25,7 +26,7 @@ export interface Func {
|
||||
}
|
||||
|
||||
interface BaseSelectedContext {
|
||||
type: 'attribute' | 'relation' | 'nested' | 'userRequest'
|
||||
type: 'attribute' | 'relation' | 'nested' | 'userRequest' | 'function'
|
||||
// attribute key
|
||||
key: string
|
||||
|
||||
@ -60,4 +61,15 @@ export interface SelectedUserRequest extends BaseSelectedContext {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type SelectedContext = SelectedAttribute | SelectedRelation | SelectedNested | SelectedUserRequest
|
||||
export interface SelectedContextFunc extends BaseSelectedContext {
|
||||
type: 'function'
|
||||
func: Ref<ProcessFunction>
|
||||
props: Record<string, any>
|
||||
}
|
||||
|
||||
export type SelectedContext =
|
||||
| SelectedAttribute
|
||||
| SelectedRelation
|
||||
| SelectedNested
|
||||
| SelectedUserRequest
|
||||
| SelectedContextFunc
|
||||
|
@ -39,6 +39,7 @@
|
||||
"dependencies": {
|
||||
"@hcengineering/time": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/card": "^0.6.0",
|
||||
"@hcengineering/server-process": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
|
@ -14,6 +14,7 @@
|
||||
//
|
||||
|
||||
import card, { Card } from '@hcengineering/card'
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import core, {
|
||||
ArrOf,
|
||||
Doc,
|
||||
@ -40,6 +41,7 @@ import process, {
|
||||
ProcessError,
|
||||
ProcessToDo,
|
||||
SelectedContext,
|
||||
SelectedContextFunc,
|
||||
SelectedNested,
|
||||
SelectedRelation,
|
||||
SelectedUserRequest,
|
||||
@ -342,6 +344,8 @@ async function getContextValue (value: any, control: TriggerControl, execution:
|
||||
value = await getNestedValue(control, execution, context)
|
||||
} else if (context.type === 'userRequest') {
|
||||
value = getUserRequestValue(control, execution, context)
|
||||
} else if (context.type === 'function') {
|
||||
value = await getFunctionValue(control, execution, context)
|
||||
}
|
||||
return await fillValue(value, context, control, execution)
|
||||
} catch (err: any) {
|
||||
@ -355,6 +359,39 @@ async function getContextValue (value: any, control: TriggerControl, execution:
|
||||
}
|
||||
}
|
||||
|
||||
async function getFunctionValue (
|
||||
control: TriggerControl,
|
||||
execution: Execution,
|
||||
context: SelectedContextFunc
|
||||
): Promise<any> {
|
||||
const func = control.modelDb.findObject(context.func)
|
||||
if (func === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
|
||||
const impl = control.hierarchy.as(func, serverProcess.mixin.FuncImpl)
|
||||
if (impl === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
|
||||
const f = await getResource(impl.func)
|
||||
const res = await f(null, context.props, control, execution)
|
||||
if (context.sourceFunction !== undefined) {
|
||||
const transform = control.modelDb.findObject(context.sourceFunction)
|
||||
if (transform === undefined) {
|
||||
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
|
||||
}
|
||||
if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) {
|
||||
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
|
||||
}
|
||||
const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl)
|
||||
const f = await getResource(funcImpl.func)
|
||||
const val = await f(res, {}, control, execution)
|
||||
if (val == null) {
|
||||
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
|
||||
}
|
||||
return val
|
||||
}
|
||||
if (res == null) {
|
||||
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function getUserRequestValue (control: TriggerControl, execution: Execution, context: SelectedUserRequest): any {
|
||||
const userContext = execution.context?.[context.id]
|
||||
if (userContext !== undefined) return userContext
|
||||
@ -682,6 +719,18 @@ export function Trim (value: string): string {
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
export async function RoleContext (
|
||||
value: null,
|
||||
props: Record<string, any>,
|
||||
control: TriggerControl,
|
||||
execution: Execution
|
||||
): Promise<Ref<Employee>[]> {
|
||||
const targetRole = props.target
|
||||
if (targetRole === undefined) return []
|
||||
const users = await control.findAll(control.ctx, contact.class.UserRole, { role: targetRole })
|
||||
return users.map((it) => it.user)
|
||||
}
|
||||
|
||||
export async function Add (
|
||||
value: number,
|
||||
props: Record<string, any>,
|
||||
@ -787,7 +836,8 @@ export default async () => ({
|
||||
Add,
|
||||
Subtract,
|
||||
Offset,
|
||||
FirstWorkingDayAfter
|
||||
FirstWorkingDayAfter,
|
||||
RoleContext
|
||||
},
|
||||
trigger: {
|
||||
OnExecutionCreate,
|
||||
|
@ -44,7 +44,8 @@ export default plugin(serverProcessId, {
|
||||
Add: '' as Resource<TransformFunc>,
|
||||
Subtract: '' as Resource<TransformFunc>,
|
||||
Offset: '' as Resource<TransformFunc>,
|
||||
FirstWorkingDayAfter: '' as Resource<TransformFunc>
|
||||
FirstWorkingDayAfter: '' as Resource<TransformFunc>,
|
||||
RoleContext: '' as Resource<TransformFunc>
|
||||
},
|
||||
trigger: {
|
||||
OnExecutionCreate: '' as Resource<TriggerFunc>,
|
||||
|
Loading…
Reference in New Issue
Block a user