mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-30 12:20:00 +00:00
Fix members in Review/Board etc. (#2036)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
e7b5d0e9b3
commit
a9392e1921
@ -229,6 +229,11 @@ export function createModel (builder: Builder): void {
|
||||
builder.mixin(contact.class.Member, core.class.Class, view.mixin.CollectionEditor, {
|
||||
editor: contact.component.Members
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.Employee, core.class.Class, view.mixin.ArrayEditor, {
|
||||
editor: contact.component.EmployeeArrayEditor
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.Member, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: contact.component.MemberPresenter
|
||||
})
|
||||
|
@ -38,7 +38,8 @@ export default mergeIds(contactId, contact, {
|
||||
PersonEditor: '' as AnyComponent,
|
||||
Members: '' as AnyComponent,
|
||||
MemberPresenter: '' as AnyComponent,
|
||||
EditMember: '' as AnyComponent
|
||||
EditMember: '' as AnyComponent,
|
||||
EmployeeArrayEditor: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Persons: '' as IntlString,
|
||||
|
@ -13,10 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { Class, Doc, DOMAIN_TX, Ref, Space, TxOperations } from '@anticrm/core'
|
||||
import core, { Doc, Ref, Space, TxOperations } from '@anticrm/core'
|
||||
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
|
||||
import { DOMAIN_CALENDAR } from '@anticrm/model-calendar'
|
||||
import { DOMAIN_SPACE } from '@anticrm/model-core'
|
||||
import tags, { TagCategory } from '@anticrm/model-tags'
|
||||
import { createKanbanTemplate, createSequence } from '@anticrm/model-task'
|
||||
import { getCategories } from '@anticrm/skillset'
|
||||
@ -35,19 +34,6 @@ export const recruitOperation: MigrateOperation = {
|
||||
space: recruit.space.Reviews
|
||||
}
|
||||
)
|
||||
const categories = await client.find(DOMAIN_SPACE, {
|
||||
_class: 'recruit:class:ReviewCategory' as Ref<Class<Doc>>
|
||||
})
|
||||
for (const cat of categories) {
|
||||
await client.delete(DOMAIN_SPACE, cat._id)
|
||||
}
|
||||
|
||||
const catTx = await client.find(DOMAIN_TX, {
|
||||
objectClass: 'recruit:class:ReviewCategory' as Ref<Class<Doc>>
|
||||
})
|
||||
for (const cat of catTx) {
|
||||
await client.delete(DOMAIN_TX, cat._id)
|
||||
}
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
|
@ -22,6 +22,7 @@ import type { AnyComponent } from '@anticrm/ui'
|
||||
import type {
|
||||
Action,
|
||||
ActionCategory,
|
||||
ArrayEditor,
|
||||
AttributeEditor,
|
||||
AttributeFilter,
|
||||
AttributePresenter,
|
||||
@ -114,6 +115,11 @@ export class TCollectionEditor extends TClass implements CollectionEditor {
|
||||
editor!: AnyComponent
|
||||
}
|
||||
|
||||
@Mixin(view.mixin.ArrayEditor, core.class.Class)
|
||||
export class TArrayEditor extends TClass implements ArrayEditor {
|
||||
editor!: AnyComponent
|
||||
}
|
||||
|
||||
@Mixin(view.mixin.AttributePresenter, core.class.Class)
|
||||
export class TAttributePresenter extends TClass implements AttributePresenter {
|
||||
presenter!: AnyComponent
|
||||
@ -274,7 +280,8 @@ export function createModel (builder: Builder): void {
|
||||
TTextPresenter,
|
||||
TIgnoreActions,
|
||||
TPreviewPresenter,
|
||||
TLinkPresenter
|
||||
TLinkPresenter,
|
||||
TArrayEditor
|
||||
)
|
||||
|
||||
classPresenter(
|
||||
|
@ -103,7 +103,7 @@ export interface TxMixin<D extends Doc, M extends D> extends TxCUD<D> {
|
||||
* @public
|
||||
*/
|
||||
export type ArrayAsElement<T> = {
|
||||
[P in keyof T]: T[P] extends Arr<infer X> ? X : never
|
||||
[P in keyof T]: T[P] extends Arr<infer X> ? X | PullArray<X> : never
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AnyAttribute, Class, Client, Doc, Ref, TxOperations } from '@anticrm/core'
|
||||
import core, { AnyAttribute, Class, Client, Doc, Ref, TxOperations } from '@anticrm/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -22,7 +22,21 @@ export async function updateAttribute (
|
||||
if (client.getHierarchy().isMixin(attr.attributeOf)) {
|
||||
await client.updateMixin(doc._id, _class, doc.space, attr.attributeOf, { [attributeKey]: value })
|
||||
} else {
|
||||
await client.update(object, { [attributeKey]: value })
|
||||
if (client.getHierarchy().isDerived(attribute.attr.type._class, core.class.ArrOf)) {
|
||||
const oldvalue: any[] = (object as any)[attributeKey] ?? []
|
||||
const val: any[] = value
|
||||
const toPull = oldvalue.filter((it: any) => !val.includes(it))
|
||||
|
||||
const toPush = val.filter((it) => !oldvalue.includes(it))
|
||||
if (toPull.length > 0) {
|
||||
await client.update(object, { $pull: { [attributeKey]: { $in: toPull } } })
|
||||
}
|
||||
if (toPush.length > 0) {
|
||||
await client.update(object, { $push: { [attributeKey]: { $each: toPush, $position: 0 } } })
|
||||
}
|
||||
} else {
|
||||
await client.update(object, { [attributeKey]: value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,40 +16,47 @@
|
||||
<script lang="ts">
|
||||
import type { AnyAttribute, Class, Doc, Ref } from '@anticrm/core'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import type { AnySvelteComponent } from '@anticrm/ui'
|
||||
import { CircleButton, Label } from '@anticrm/ui'
|
||||
import { AnySvelteComponent, Label, tooltip } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { getAttribute, KeyedAttribute, updateAttribute } from '../attributes'
|
||||
import { getAttributePresenterClass, getClient } from '../utils'
|
||||
import { AttributeCategory, getAttributePresenterClass, getClient } from '../utils'
|
||||
|
||||
export let key: KeyedAttribute | string
|
||||
export let object: Doc
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let maxWidth: string | undefined = undefined
|
||||
export let focus: boolean = false
|
||||
export let minimize: boolean = false
|
||||
export let showHeader: boolean = true
|
||||
export let vertical: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: attribute = typeof key === 'string' ? hierarchy.getAttribute(_class, key) : key.attr
|
||||
$: attributeKey = typeof key === 'string' ? key : key.key
|
||||
$: typeClassId = attribute !== undefined ? getAttributePresenterClass(attribute) : undefined
|
||||
$: presenterClass = attribute !== undefined ? getAttributePresenterClass(hierarchy, attribute) : undefined
|
||||
|
||||
let editor: Promise<void | AnySvelteComponent> | undefined
|
||||
|
||||
function update (attribute: AnyAttribute, typeClassId?: Ref<Class<Doc>>): void {
|
||||
if (typeClassId !== undefined) {
|
||||
const typeClass = hierarchy.getClass(typeClassId)
|
||||
function update (
|
||||
attribute: AnyAttribute,
|
||||
presenterClass?: { attrClass: Ref<Class<Doc>>; category: AttributeCategory }
|
||||
): void {
|
||||
if (presenterClass?.attrClass !== undefined && presenterClass?.category === 'attribute') {
|
||||
const typeClass = hierarchy.getClass(presenterClass.attrClass)
|
||||
const editorMixin = hierarchy.as(typeClass, view.mixin.AttributeEditor)
|
||||
editor = getResource(editorMixin.editor).catch((cause) => {
|
||||
console.error(`failed to find editor for ${_class} ${attribute} ${typeClassId} cause: ${cause}`)
|
||||
console.error(`failed to find editor for ${_class} ${attribute} ${presenterClass.attrClass} cause: ${cause}`)
|
||||
})
|
||||
}
|
||||
if (presenterClass?.attrClass !== undefined && presenterClass?.category === 'array') {
|
||||
const typeClass = hierarchy.getClass(presenterClass.attrClass)
|
||||
const editorMixin = hierarchy.as(typeClass, view.mixin.ArrayEditor)
|
||||
editor = getResource(editorMixin.editor).catch((cause) => {
|
||||
console.error(`failed to find editor for ${_class} ${attribute} ${presenterClass.attrClass} cause: ${cause}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
$: update(attribute, typeClassId)
|
||||
$: update(attribute, presenterClass)
|
||||
|
||||
function onChange (value: any) {
|
||||
const doc = object as Doc
|
||||
@ -58,48 +65,25 @@
|
||||
</script>
|
||||
|
||||
{#if editor}
|
||||
{#await editor}
|
||||
...
|
||||
{:then instance}
|
||||
{#if attribute.icon}
|
||||
{#if !vertical}
|
||||
<div class="flex-row-center">
|
||||
<CircleButton icon={attribute.icon} size={'large'} />
|
||||
{#if !minimize}
|
||||
<div class="flex-col with-icon ml-2">
|
||||
{#if showHeader}
|
||||
<Label label={attribute.label} />
|
||||
{/if}
|
||||
<div class="value">
|
||||
<svelte:component
|
||||
this={instance}
|
||||
label={attribute?.label}
|
||||
placeholder={attribute?.label}
|
||||
type={attribute?.type}
|
||||
{maxWidth}
|
||||
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
|
||||
space={object.space}
|
||||
{onChange}
|
||||
{focus}
|
||||
{object}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{#if showHeader}
|
||||
<span class="fs-bold overflow-label"><Label label={attribute.label} /></span>
|
||||
{/if}
|
||||
{#await editor then instance}
|
||||
{#if showHeader}
|
||||
<span
|
||||
class="fs-bold overflow-label"
|
||||
use:tooltip={{
|
||||
component: Label,
|
||||
props: { label: attribute.label }
|
||||
}}><Label label={attribute.label} /></span
|
||||
>
|
||||
<div class="flex flex-grow min-w-0">
|
||||
<svelte:component
|
||||
this={instance}
|
||||
label={attribute?.label}
|
||||
placeholder={attribute?.label}
|
||||
type={attribute?.type}
|
||||
kind={'link'}
|
||||
size={'large'}
|
||||
width={'100%'}
|
||||
justify={'left'}
|
||||
type={attribute?.type}
|
||||
{maxWidth}
|
||||
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
|
||||
space={object.space}
|
||||
@ -107,47 +91,7 @@
|
||||
{focus}
|
||||
{object}
|
||||
/>
|
||||
{/if}
|
||||
{:else if showHeader}
|
||||
{#if !vertical}
|
||||
<div class="flex-col">
|
||||
<span class="fs-bold"><Label label={attribute.label} /></span>
|
||||
<div class="value">
|
||||
<svelte:component
|
||||
this={instance}
|
||||
label={attribute?.label}
|
||||
placeholder={attribute?.label}
|
||||
type={attribute?.type}
|
||||
{maxWidth}
|
||||
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
|
||||
space={object.space}
|
||||
{onChange}
|
||||
{focus}
|
||||
{object}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="fs-bold"><Label label={attribute.label} /></span>
|
||||
<div class="flex flex-grow min-w-0">
|
||||
<svelte:component
|
||||
this={instance}
|
||||
label={attribute?.label}
|
||||
placeholder={attribute?.label}
|
||||
kind={'link'}
|
||||
size={'large'}
|
||||
width={'100%'}
|
||||
justify={'left'}
|
||||
type={attribute?.type}
|
||||
{maxWidth}
|
||||
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
|
||||
space={object.space}
|
||||
{onChange}
|
||||
{focus}
|
||||
{object}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div style="grid-column: 1/3;">
|
||||
<svelte:component
|
||||
|
@ -35,12 +35,12 @@
|
||||
|
||||
$: attribute = typeof key === 'string' ? hierarchy.getAttribute(_class, key) : key.attr
|
||||
$: attributeKey = typeof key === 'string' ? key : key.key
|
||||
$: typeClassId = attribute !== undefined ? getAttributePresenterClass(attribute) : undefined
|
||||
$: presenterClass = attribute !== undefined ? getAttributePresenterClass(hierarchy, attribute) : undefined
|
||||
|
||||
let editor: Promise<AnySvelteComponent> | undefined
|
||||
|
||||
$: if (typeClassId !== undefined) {
|
||||
const typeClass = hierarchy.getClass(typeClassId)
|
||||
$: if (presenterClass !== undefined) {
|
||||
const typeClass = hierarchy.getClass(presenterClass.attrClass)
|
||||
const editorMixin = hierarchy.as(typeClass, view.mixin.AttributeEditor)
|
||||
editor = getResource(editorMixin.editor)
|
||||
}
|
||||
|
@ -22,18 +22,11 @@
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let keys: (string | KeyedAttribute)[]
|
||||
export let showHeader: boolean = true
|
||||
export let vertical: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="attributes-bar-container {vertical ? 'vertical' : 'horizontal'}">
|
||||
<div class="attributes-bar-container vertical">
|
||||
{#each keys as key (typeof key === 'string' ? key : key.key)}
|
||||
{#if !vertical}
|
||||
<div class="flex-center column">
|
||||
<AttributeBarEditor {key} {_class} {object} {showHeader} />
|
||||
</div>
|
||||
{:else}
|
||||
<AttributeBarEditor {key} {_class} {object} {showHeader} vertical />
|
||||
{/if}
|
||||
<AttributeBarEditor {key} {_class} {object} {showHeader} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
@ -55,9 +55,9 @@
|
||||
selectedUsers: items
|
||||
},
|
||||
evt.target as HTMLElement,
|
||||
() => {},
|
||||
undefined,
|
||||
(result) => {
|
||||
if (result !== undefined) {
|
||||
if (result != null) {
|
||||
items = result
|
||||
dispatch('update', items)
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import core, {
|
||||
FindOptions,
|
||||
FindResult,
|
||||
getCurrentAccount,
|
||||
Hierarchy,
|
||||
Ref,
|
||||
RefTo,
|
||||
Tx,
|
||||
@ -143,20 +144,28 @@ export async function getBlobURL (blob: Blob): Promise<string> {
|
||||
})
|
||||
}
|
||||
|
||||
export type AttributeCategory = 'attribute' | 'collection' | 'array'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getAttributePresenterClass (attribute: AnyAttribute): Ref<Class<Doc>> {
|
||||
export function getAttributePresenterClass (
|
||||
hierarchy: Hierarchy,
|
||||
attribute: AnyAttribute
|
||||
): { attrClass: Ref<Class<Doc>>, category: AttributeCategory } {
|
||||
let attrClass = attribute.type._class
|
||||
if (attrClass === core.class.RefTo) {
|
||||
let category: AttributeCategory = 'attribute'
|
||||
if (hierarchy.isDerived(attrClass, core.class.RefTo)) {
|
||||
attrClass = (attribute.type as RefTo<Doc>).to
|
||||
category = 'attribute'
|
||||
}
|
||||
if (attrClass === core.class.Collection) {
|
||||
if (hierarchy.isDerived(attrClass, core.class.Collection)) {
|
||||
attrClass = (attribute.type as Collection<AttachedDoc>).of
|
||||
category = 'collection'
|
||||
}
|
||||
if (attrClass === core.class.ArrOf) {
|
||||
if (hierarchy.isDerived(attrClass, core.class.ArrOf)) {
|
||||
const of = (attribute.type as ArrOf<AttachedDoc>).of
|
||||
attrClass = of._class === core.class.RefTo ? (of as RefTo<Doc>).to : of._class
|
||||
category = 'array'
|
||||
}
|
||||
return attrClass
|
||||
return { attrClass, category }
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
import { Asset, getResource } from '@anticrm/platform'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import {
|
||||
Button,
|
||||
Component,
|
||||
Icon,
|
||||
IconEdit,
|
||||
@ -28,8 +29,7 @@
|
||||
Menu,
|
||||
ShowMore,
|
||||
showPopup,
|
||||
TimeSince,
|
||||
Button
|
||||
TimeSince
|
||||
} from '@anticrm/ui'
|
||||
import type { AttributeModel } from '@anticrm/view'
|
||||
import { getActions } from '@anticrm/view-resources'
|
||||
@ -116,7 +116,22 @@
|
||||
return attr?.type._class === core.class.TypeMarkup
|
||||
}
|
||||
|
||||
$: hasMessageType = model.find((m) => isMessageType(m.attribute))
|
||||
async function updateMessageType (model: AttributeModel[], tx: DisplayTx): Promise<boolean> {
|
||||
for (const m of model) {
|
||||
if (isMessageType(m.attribute)) {
|
||||
return true
|
||||
}
|
||||
const val = await getValue(client, m, tx)
|
||||
if (val.added.length > 1 || val.removed.length > 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
let hasMessageType = false
|
||||
$: updateMessageType(model, tx).then((res) => {
|
||||
hasMessageType = res
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if (viewlet !== undefined && !((viewlet?.hideOnRemove ?? false) && tx.removed)) || model.length > 0}
|
||||
@ -182,8 +197,11 @@
|
||||
<Label label={activity.string.To} />
|
||||
<Label label={m.label} />
|
||||
</span>
|
||||
{#if hasMessageType}
|
||||
<div class="time"><TimeSince value={tx.tx.modifiedOn} /></div>
|
||||
{/if}
|
||||
<div class="strong">
|
||||
<div class="flex">
|
||||
<div class="flex flex-wrap gap-2" class:emphasized={value.added.length > 1}>
|
||||
{#each value.added as value}
|
||||
<svelte:component this={m.presenter} {value} />
|
||||
{/each}
|
||||
@ -195,8 +213,11 @@
|
||||
<Label label={activity.string.From} />
|
||||
<Label label={m.label} />
|
||||
</span>
|
||||
{#if hasMessageType}
|
||||
<div class="time"><TimeSince value={tx.tx.modifiedOn} /></div>
|
||||
{/if}
|
||||
<div class="strong">
|
||||
<div class="flex">
|
||||
<div class="flex flex-wrap gap-2 flex-grow" class:emphasized={value.removed.length > 1}>
|
||||
{#each value.removed as value}
|
||||
<svelte:component this={m.presenter} {value} />
|
||||
{/each}
|
||||
|
@ -50,7 +50,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
padding: 1rem;
|
||||
padding: 0.5rem;
|
||||
min-width: 0;
|
||||
color: var(--accent-color);
|
||||
background: var(--theme-bg-accent-color);
|
||||
|
@ -57,17 +57,7 @@
|
||||
let checklists: TodoItem[] = []
|
||||
const mixins: Mixin<Doc>[] = []
|
||||
const allowedCollections = ['labels']
|
||||
const ignoreKeys = [
|
||||
'isArchived',
|
||||
'location',
|
||||
'title',
|
||||
'description',
|
||||
'state',
|
||||
'members',
|
||||
'number',
|
||||
'assignee',
|
||||
'doneState'
|
||||
]
|
||||
const ignoreKeys = ['isArchived', 'location', 'title', 'description', 'state', 'number', 'assignee', 'doneState']
|
||||
|
||||
function change (field: string, value: any) {
|
||||
if (object) {
|
||||
|
@ -16,21 +16,13 @@
|
||||
<script lang="ts">
|
||||
import type { Card } from '@anticrm/board'
|
||||
import board from '@anticrm/board'
|
||||
import { Employee } from '@anticrm/contact'
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { CheckBox, Label } from '@anticrm/ui'
|
||||
|
||||
import plugin from '../../plugin'
|
||||
import { updateCardMembers } from '../../utils/CardUtils'
|
||||
import UserBoxList from '../UserBoxList.svelte'
|
||||
|
||||
export let value: Card
|
||||
const client = getClient()
|
||||
|
||||
function updateMembers (e: CustomEvent<Ref<Employee>[]>) {
|
||||
updateCardMembers(value, client, e.detail)
|
||||
}
|
||||
function updateState (e: CustomEvent<boolean>) {
|
||||
if (e.detail) {
|
||||
client.update(value, { doneState: board.state.Completed })
|
||||
@ -48,12 +40,6 @@
|
||||
<div class="ml-4">
|
||||
<CheckBox checked={value.doneState === board.state.Completed} on:value={updateState} />
|
||||
</div>
|
||||
<div class="label fs-bold">
|
||||
<Label label={plugin.string.Members} />
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<UserBoxList value={value.members ?? []} on:update={updateMembers} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -28,7 +28,6 @@
|
||||
"Reminder": "Reminder",
|
||||
"ReminderTime": "Reminder time",
|
||||
"RemindMeAt": "Remind me at",
|
||||
"EditReminder": "Edit reminder",
|
||||
"CreateReminder": "Create reminder",
|
||||
"CreatedReminder": "Created a reminder",
|
||||
"Reminders": "Reminders",
|
||||
|
@ -28,7 +28,6 @@
|
||||
"Reminder": "Напоминание",
|
||||
"RemindMeAt": "Напомнить мне",
|
||||
"ReminderTime": "Время напоминания",
|
||||
"EditReminder": "Редактировать напоминание",
|
||||
"CreateReminder": "Создать напоминание",
|
||||
"CreatedReminder": "Создал напоминание",
|
||||
"Reminders": "Напоминания",
|
||||
|
@ -74,22 +74,6 @@
|
||||
<svelte:fragment slot="pool">
|
||||
<!-- <TimeShiftPicker title={calendar.string.Date} bind:value direction="after" /> -->
|
||||
<DateRangePresenter bind:value withTime={true} editable={true} labelNull={ui.string.SelectDate} />
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={participants}
|
||||
label={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
participants.push(evt.detail._id)
|
||||
participants = participants
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
const _id = evt.detail._id
|
||||
const index = participants.findIndex((p) => p === _id)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<UserBoxList _class={contact.class.Employee} bind:items={participants} label={calendar.string.Participants} />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
@ -14,10 +14,9 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Event } from '@anticrm/calendar'
|
||||
import contact from '@anticrm/contact'
|
||||
import { getClient, UserBoxList } from '@anticrm/presentation'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { StyledTextBox } from '@anticrm/text-editor'
|
||||
import { Label, StylishEdit } from '@anticrm/ui'
|
||||
import { StylishEdit } from '@anticrm/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import calendar from '../plugin'
|
||||
|
||||
@ -54,24 +53,5 @@
|
||||
placeholder={calendar.string.Description}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<div class="mt-4 mb-2">
|
||||
<Label label={calendar.string.Participants} />
|
||||
</div>
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={object.participants}
|
||||
label={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
client.update(object, { $push: { participants: evt.detail._id } })
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
client.update(object, { $pull: { participants: evt.detail._id } })
|
||||
}}
|
||||
on:update={(evt) => {
|
||||
client.update(object, { participants: evt.detail })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -1,95 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Event } from '@anticrm/calendar'
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import { Ref } from '@anticrm/core'
|
||||
import presentation, { Card, getClient, UserBoxList } from '@anticrm/presentation'
|
||||
import { StyledTextBox } from '@anticrm/text-editor'
|
||||
import { DatePicker, Grid, StylishEdit } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import calendar from '../plugin'
|
||||
|
||||
export let value: Event
|
||||
|
||||
let title: string = value.title
|
||||
let description: string = value.description
|
||||
let startDate: number = value.date
|
||||
let participants: Ref<Employee>[] = value.participants ?? []
|
||||
const space = calendar.space.PersonalEvents
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
export function canClose (): boolean {
|
||||
return title.trim().length === 0 && participants.length === 0
|
||||
}
|
||||
|
||||
async function saveReminder () {
|
||||
await client.updateDoc(value._class, value.space, value._id, {
|
||||
date: startDate,
|
||||
description,
|
||||
participants,
|
||||
title
|
||||
})
|
||||
|
||||
await client.updateMixin(value._id, value._class, space, calendar.mixin.Reminder, {
|
||||
shift: 0,
|
||||
state: 'active'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={calendar.string.EditReminder}
|
||||
okAction={saveReminder}
|
||||
okLabel={presentation.string.Save}
|
||||
canSave={title.trim().length > 0 && startDate > 0 && participants.length > 0}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<Grid column={1} rowGap={1.75}>
|
||||
<StylishEdit bind:value={title} label={calendar.string.Title} />
|
||||
<StyledTextBox
|
||||
emphasized
|
||||
showButtons={false}
|
||||
alwaysEdit
|
||||
bind:content={description}
|
||||
label={calendar.string.Description}
|
||||
placeholder={calendar.string.Description}
|
||||
/>
|
||||
<div class="antiComponentBox">
|
||||
<DatePicker title={calendar.string.Date} bind:value={startDate} withTime />
|
||||
</div>
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={participants}
|
||||
label={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
participants.push(evt.detail._id)
|
||||
participants = participants
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
const _id = evt.detail._id
|
||||
const index = participants.findIndex((p) => p === _id)
|
||||
if (index !== -1) {
|
||||
participants.splice(index, 1)
|
||||
participants = participants
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Card>
|
@ -26,7 +26,6 @@ export default mergeIds(calendarId, calendar, {
|
||||
Events: '' as IntlString,
|
||||
RemindMeAt: '' as IntlString,
|
||||
CreateReminder: '' as IntlString,
|
||||
EditReminder: '' as IntlString,
|
||||
ReminderTime: '' as IntlString,
|
||||
ModeDay: '' as IntlString,
|
||||
ModeWeek: '' as IntlString,
|
||||
|
@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Person } from '@anticrm/contact'
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { UserBoxList } from '@anticrm/presentation'
|
||||
import contact from '../plugin'
|
||||
|
||||
export let label: IntlString
|
||||
export let value: Ref<Person>[]
|
||||
export let onChange: (refs: Ref<Person>[]) => void
|
||||
|
||||
let timer: any
|
||||
|
||||
function onUpdate (evt: CustomEvent<Ref<Person>[]>): void {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
onChange(evt.detail)
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={value}
|
||||
{label}
|
||||
on:update={onUpdate}
|
||||
kind={'link'}
|
||||
size={'medium'}
|
||||
justify={'left'}
|
||||
width={'100%'}
|
||||
/>
|
@ -43,6 +43,7 @@ import OrganizationSelector from './components/OrganizationSelector.svelte'
|
||||
import Members from './components/Members.svelte'
|
||||
import MemberPresenter from './components/MemberPresenter.svelte'
|
||||
import EditMember from './components/EditMember.svelte'
|
||||
import EmployeeArrayEditor from './components/EmployeeArrayEditor.svelte'
|
||||
|
||||
export {
|
||||
Channels,
|
||||
@ -90,7 +91,8 @@ export default async (): Promise<Resources> => ({
|
||||
EmployeePresenter,
|
||||
Members,
|
||||
MemberPresenter,
|
||||
EditMember
|
||||
EditMember,
|
||||
EmployeeArrayEditor
|
||||
},
|
||||
completion: {
|
||||
EmployeeQuery: async (client: Client, query: string) => await queryContact(contact.class.Employee, client, query),
|
||||
|
@ -57,15 +57,8 @@
|
||||
"ManageVacancyStatuses": "Manage vacancy statuses",
|
||||
"EditVacancy": "Edit",
|
||||
"FullDescription": "Full description",
|
||||
"CreateReviewCategory": "Create Review category",
|
||||
"ReviewCategoryName": "Review category title*",
|
||||
"ReviewCategoryPlaceholder": "Interview",
|
||||
"ReviewCategory": "Reviews",
|
||||
"ReviewCategoryDescription": "Description",
|
||||
"ThisReviewCategoryIsPrivate": "This category is private",
|
||||
"CreateReview": "Schedule an Review",
|
||||
"CreateReviewParams": "Schedule {label}",
|
||||
"SelectReviewCategory": "select category",
|
||||
"Reviews": "Reviews",
|
||||
"Review": "Review",
|
||||
"ReviewCreateLabel": "Review",
|
||||
@ -76,7 +69,6 @@
|
||||
"ReviewShortLabel": "RVE",
|
||||
"StartDate": "Start date",
|
||||
"DueDate": "Due date",
|
||||
"ReviewCategoryTitle":"Category",
|
||||
"Verdict": "Verdict",
|
||||
"OpinionSave": "Save",
|
||||
"TalentReviews": "All Talent Reviews",
|
||||
@ -99,7 +91,6 @@
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Please select talent",
|
||||
"VacancyRequired": "Please select vacancy",
|
||||
"ReviewCategoryRequired": "Please select review category"
|
||||
"VacancyRequired": "Please select vacancy"
|
||||
}
|
||||
}
|
@ -58,15 +58,8 @@
|
||||
"EditVacancy": "Редактировать",
|
||||
"FullDescription": "Детальное описание",
|
||||
|
||||
"CreateReviewCategory": "Создать категорию ревью",
|
||||
"ReviewCategoryName": "Имя категории*",
|
||||
"ReviewCategoryPlaceholder": "Интервью",
|
||||
"ReviewCategory": "Ревью",
|
||||
"ReviewCategoryDescription": "Описание",
|
||||
"ThisReviewCategoryIsPrivate": "Эта категория личная",
|
||||
"CreateReview": "Запланировать Ревью",
|
||||
"CreateReviewParams": "Запланировать {label}",
|
||||
"SelectReviewCategory": "выбрать категорию",
|
||||
"Reviews": "Ревью",
|
||||
"Review": "Ревью",
|
||||
"ReviewCreateLabel": "Ревью",
|
||||
@ -77,7 +70,6 @@
|
||||
"ReviewShortLabel": "RVE",
|
||||
"StartDate": "Дата начала",
|
||||
"DueDate": "Дата окончания",
|
||||
"ReviewCategoryTitle":"Категория",
|
||||
"Verdict": "Вердикт",
|
||||
"OpinionSave": "Сохранить",
|
||||
"TalentReviews": "Ревью таланта",
|
||||
@ -101,7 +93,6 @@
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Пожалуйста выберите таланта",
|
||||
"VacancyRequired": "Пожалуйста выберите вакансию",
|
||||
"ReviewCategoryRequired": "Пожалуйста выберети категорию"
|
||||
"VacancyRequired": "Пожалуйста выберите вакансию"
|
||||
}
|
||||
}
|
@ -71,14 +71,6 @@
|
||||
return (preserveCandidate || candidate === undefined) && title.length === 0
|
||||
}
|
||||
|
||||
let spaceLabel: string = ''
|
||||
|
||||
$: client.findOne(recruit.class.ReviewCategory, { _id: doc.space }).then((res) => {
|
||||
if (res !== undefined) {
|
||||
spaceLabel = res.name
|
||||
}
|
||||
})
|
||||
|
||||
async function createReview () {
|
||||
const sequence = await client.findOne(task.class.Sequence, { attachedTo: recruit.class.Review })
|
||||
if (sequence === undefined) {
|
||||
@ -144,9 +136,9 @@
|
||||
|
||||
<Card
|
||||
label={recruit.string.CreateReviewParams}
|
||||
labelProps={{ label: spaceLabel }}
|
||||
labelProps={{ label: '' }}
|
||||
okAction={createReview}
|
||||
canSave={status.severity === Severity.OK && title.trim().length > 0}
|
||||
canSave={status.severity === Severity.OK && title.trim().length > 0 && doc.attachedTo !== undefined}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
@ -171,6 +163,7 @@
|
||||
placeholder={recruit.string.Talents}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
create={{ component: recruit.component.CreateCandidate, label: recruit.string.CreateTalent }}
|
||||
/>
|
||||
{/if}
|
||||
<OrganizationSelector bind:value={company} label={recruit.string.Company} kind={'no-border'} size={'small'} />
|
||||
@ -182,13 +175,6 @@
|
||||
on:change={updateStart}
|
||||
/>
|
||||
<DateRangePresenter bind:value={dueDate} labelNull={recruit.string.DueDate} withTime editable />
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={doc.participants}
|
||||
label={calendar.string.Participants}
|
||||
on:update={(evt) => {
|
||||
doc.participants = evt.detail
|
||||
}}
|
||||
/>
|
||||
<UserBoxList _class={contact.class.Employee} bind:items={doc.participants} label={calendar.string.Participants} />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
@ -16,10 +16,10 @@
|
||||
<script lang="ts">
|
||||
import calendar from '@anticrm/calendar'
|
||||
import contact, { Contact } from '@anticrm/contact'
|
||||
import { getClient, UserBox, UserBoxList } from '@anticrm/presentation'
|
||||
import { getClient, UserBox } from '@anticrm/presentation'
|
||||
import type { Review } from '@anticrm/recruit'
|
||||
import { StyledTextBox } from '@anticrm/text-editor'
|
||||
import { Grid, Label, showPanel, StylishEdit, EditBox } from '@anticrm/ui'
|
||||
import { EditBox, Grid, showPanel, StylishEdit } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import recruit from '../../plugin'
|
||||
@ -85,22 +85,6 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-row mb-2">
|
||||
<Label label={calendar.string.Participants} />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={object.participants}
|
||||
label={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
client.update(object, { $push: { participants: evt.detail._id } })
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
client.update(object, { $pull: { participants: evt.detail._id } })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<StylishEdit
|
||||
label={recruit.string.Verdict}
|
||||
bind:value={object.verdict}
|
||||
|
@ -1,138 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { Attachments } from '@anticrm/attachment-resources'
|
||||
import type { Ref } from '@anticrm/core'
|
||||
import { AttributesBar, createQuery, getClient } from '@anticrm/presentation'
|
||||
import { ReviewCategory } from '@anticrm/recruit'
|
||||
import { TextEditor } from '@anticrm/text-editor'
|
||||
import { Panel } from '@anticrm/panel'
|
||||
import { EditBox, Grid, Label, ToggleWithLabel } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../../plugin'
|
||||
|
||||
export let _id: Ref<ReviewCategory>
|
||||
|
||||
let object: ReviewCategory
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const query = createQuery()
|
||||
const clazz = client.getHierarchy().getClass(recruit.class.ReviewCategory)
|
||||
$: query.query(recruit.class.ReviewCategory, { _id }, (result) => {
|
||||
object = result[0]
|
||||
})
|
||||
|
||||
// const tabs: IntlString[] = ['General' as IntlString, 'Members' as IntlString, 'Activity' as IntlString]
|
||||
let textEditor: TextEditor
|
||||
|
||||
function onChange (key: string, value: any): void {
|
||||
client.updateDoc(object._class, object.space, object._id, { [key]: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
<Panel
|
||||
icon={clazz.icon}
|
||||
title={object.name}
|
||||
subtitle={object.description}
|
||||
isHeader={false}
|
||||
isAside={true}
|
||||
{object}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="attributes" let:direction={dir}>
|
||||
{#if dir === 'column'}
|
||||
<div class="flex-row-center subtitle">
|
||||
<AttributesBar {object} _class={object._class} keys={[]} vertical />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Grid column={1} rowGap={1.5}>
|
||||
<EditBox
|
||||
label={recruit.string.ReviewCategoryName}
|
||||
bind:value={object.name}
|
||||
placeholder={recruit.string.ReviewCategoryPlaceholder}
|
||||
maxWidth="39rem"
|
||||
focus
|
||||
on:change={() => {
|
||||
onChange('name', object.name)
|
||||
}}
|
||||
/>
|
||||
<EditBox
|
||||
label={recruit.string.Description}
|
||||
bind:value={object.description}
|
||||
placeholder={recruit.string.ReviewCategoryDescription}
|
||||
maxWidth="39rem"
|
||||
focus
|
||||
on:change={() => {
|
||||
onChange('description', object.description)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<div class="mt-10">
|
||||
<span class="title">Description</span>
|
||||
<div class="description-container">
|
||||
<TextEditor
|
||||
bind:this={textEditor}
|
||||
bind:content={object.fullDescription}
|
||||
on:blur={textEditor.submit}
|
||||
on:content={() => {
|
||||
onChange('fullDescription', object.fullDescription)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-10">
|
||||
<Attachments objectId={object._id} _class={object._class} space={object.space} />
|
||||
</div>
|
||||
<div class="mt-10">
|
||||
<span class="title"><Label label={recruit.string.Members} /></span>
|
||||
<ToggleWithLabel
|
||||
label={recruit.string.ThisReviewCategoryIsPrivate}
|
||||
description={recruit.string.MakePrivateDescription}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.title {
|
||||
margin-right: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
.description-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow-y: auto;
|
||||
height: 100px;
|
||||
padding: 0px 16px;
|
||||
background-color: var(--theme-bg-accent-color);
|
||||
border: 1px solid var(--theme-bg-accent-color);
|
||||
border-top: 20px solid transparent;
|
||||
border-bottom: 20px solid transparent;
|
||||
border-radius: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
</style>
|
@ -42,7 +42,6 @@
|
||||
_class={recruit.class.Review}
|
||||
config={[
|
||||
'',
|
||||
{ key: '$lookup.space.name', label: recruit.string.ReviewCategoryTitle },
|
||||
'verdict',
|
||||
{
|
||||
key: '',
|
||||
|
@ -16,7 +16,7 @@
|
||||
import type { Client, Doc } from '@anticrm/core'
|
||||
import { IntlString, OK, Resources, Severity, Status, translate } from '@anticrm/platform'
|
||||
import { ObjectSearchResult } from '@anticrm/presentation'
|
||||
import { Applicant, Review } from '@anticrm/recruit'
|
||||
import { Applicant } from '@anticrm/recruit'
|
||||
import task from '@anticrm/task'
|
||||
import { showPopup } from '@anticrm/ui'
|
||||
import ApplicationItem from './components/ApplicationItem.svelte'
|
||||
@ -29,6 +29,7 @@ import CreateVacancy from './components/CreateVacancy.svelte'
|
||||
import EditApplication from './components/EditApplication.svelte'
|
||||
import EditVacancy from './components/EditVacancy.svelte'
|
||||
import KanbanCard from './components/KanbanCard.svelte'
|
||||
import NewCandidateHeader from './components/NewCandidateHeader.svelte'
|
||||
import CreateOpinion from './components/review/CreateOpinion.svelte'
|
||||
import CreateReview from './components/review/CreateReview.svelte'
|
||||
import EditReview from './components/review/EditReview.svelte'
|
||||
@ -44,7 +45,6 @@ import VacancyCountPresenter from './components/VacancyCountPresenter.svelte'
|
||||
import VacancyItemPresenter from './components/VacancyItemPresenter.svelte'
|
||||
import VacancyModifiedPresenter from './components/VacancyModifiedPresenter.svelte'
|
||||
import VacancyPresenter from './components/VacancyPresenter.svelte'
|
||||
import NewCandidateHeader from './components/NewCandidateHeader.svelte'
|
||||
import recruit from './plugin'
|
||||
|
||||
async function createOpinion (object: Doc): Promise<void> {
|
||||
@ -68,16 +68,6 @@ export async function applicantValidator (applicant: Applicant, client: Client):
|
||||
return OK
|
||||
}
|
||||
|
||||
export async function reviewValidator (review: Review, client: Client): Promise<Status> {
|
||||
if (review.attachedTo === undefined) {
|
||||
return new Status(Severity.INFO, recruit.status.TalentRequired, {})
|
||||
}
|
||||
if (review.space === undefined) {
|
||||
return new Status(Severity.INFO, recruit.status.ReviewCategoryRequired, {})
|
||||
}
|
||||
return OK
|
||||
}
|
||||
|
||||
export async function queryApplication (client: Client, search: string): Promise<ObjectSearchResult[]> {
|
||||
const _class = recruit.class.Applicant
|
||||
const cl = client.getHierarchy().getClass(_class)
|
||||
@ -125,8 +115,7 @@ export default async (): Promise<Resources> => ({
|
||||
CreateOpinion: createOpinion
|
||||
},
|
||||
validator: {
|
||||
ApplicantValidator: applicantValidator,
|
||||
ReviewValidator: reviewValidator
|
||||
ApplicantValidator: applicantValidator
|
||||
},
|
||||
component: {
|
||||
CreateVacancy,
|
||||
|
@ -24,8 +24,7 @@ export default mergeIds(recruitId, recruit, {
|
||||
status: {
|
||||
ApplicationExists: '' as StatusCode,
|
||||
TalentRequired: '' as StatusCode,
|
||||
VacancyRequired: '' as StatusCode,
|
||||
ReviewCategoryRequired: '' as StatusCode
|
||||
VacancyRequired: '' as StatusCode
|
||||
},
|
||||
string: {
|
||||
CreateVacancy: '' as IntlString,
|
||||
@ -79,16 +78,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
|
||||
Review: '' as IntlString,
|
||||
ReviewCreateLabel: '' as IntlString,
|
||||
ReviewCategory: '' as IntlString,
|
||||
CreateReviewCategory: '' as IntlString,
|
||||
ReviewCategoryName: '' as IntlString,
|
||||
ReviewCategoryTitle: '' as IntlString,
|
||||
ReviewCategoryPlaceholder: '' as IntlString,
|
||||
ReviewCategoryDescription: '' as IntlString,
|
||||
ThisReviewCategoryIsPrivate: '' as IntlString,
|
||||
CreateReview: '' as IntlString,
|
||||
CreateReviewParams: '' as IntlString,
|
||||
SelectReviewCategory: '' as IntlString,
|
||||
Reviews: '' as IntlString,
|
||||
NoReviewForCandidate: '' as IntlString,
|
||||
CreateAnReview: '' as IntlString,
|
||||
@ -105,7 +96,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
TalentReviews: '' as IntlString,
|
||||
AddDescription: '' as IntlString,
|
||||
NumberSkills: '' as IntlString,
|
||||
AddDropHere: '' as IntlString
|
||||
AddDropHere: '' as IntlString,
|
||||
TalentSelect: '' as IntlString
|
||||
},
|
||||
space: {
|
||||
CandidatesPublic: '' as Ref<Space>
|
||||
@ -119,7 +111,6 @@ export default mergeIds(recruitId, recruit, {
|
||||
VacancyCountPresenter: '' as AnyComponent,
|
||||
OpinionsPresenter: '' as AnyComponent,
|
||||
VacancyModifiedPresenter: '' as AnyComponent,
|
||||
EditReviewCategory: '' as AnyComponent,
|
||||
CreateVacancy: '' as AnyComponent,
|
||||
CreateCandidate: '' as AnyComponent
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Calendar, Event } from '@anticrm/calendar'
|
||||
import { Event } from '@anticrm/calendar'
|
||||
import type { Organization, Person } from '@anticrm/contact'
|
||||
import type { AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@anticrm/core'
|
||||
import type { Asset, Plugin } from '@anticrm/platform'
|
||||
@ -32,15 +32,6 @@ export interface Vacancy extends SpaceWithStates {
|
||||
company?: Ref<Organization>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ReviewCategory extends Calendar {
|
||||
fullDescription?: string
|
||||
attachments?: number
|
||||
comments?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -110,7 +101,6 @@ const recruit = plugin(recruitId, {
|
||||
Applicant: '' as Ref<Class<Applicant>>,
|
||||
Candidates: '' as Ref<Class<Candidates>>,
|
||||
Vacancy: '' as Ref<Class<Vacancy>>,
|
||||
ReviewCategory: '' as Ref<Class<ReviewCategory>>,
|
||||
Review: '' as Ref<Class<Review>>,
|
||||
Opinion: '' as Ref<Class<Opinion>>
|
||||
},
|
||||
|
@ -24,7 +24,6 @@
|
||||
export let to: Ref<Class<Doc>> | undefined
|
||||
export let ignoreKeys: string[] = []
|
||||
export let allowedCollections: string[] = []
|
||||
export let vertical: boolean
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
@ -41,47 +40,45 @@
|
||||
$: label = hierarchy.getClass(_class).label
|
||||
</script>
|
||||
|
||||
{#if vertical}
|
||||
<div
|
||||
class="attrbar-header"
|
||||
class:collapsed
|
||||
on:click={() => {
|
||||
collapsed = !collapsed
|
||||
}}
|
||||
>
|
||||
<div class="flex-row-center">
|
||||
<span class="overflow-label">
|
||||
<Label {label} />
|
||||
</span>
|
||||
<div class="icon-arrow">
|
||||
<svg fill="var(--dark-color)" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0,0L6,3L0,6Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool">
|
||||
<Tooltip label={setting.string.ClassSetting}>
|
||||
<Button
|
||||
icon={setting.icon.Setting}
|
||||
kind={'transparent'}
|
||||
on:click={(ev) => {
|
||||
ev.stopPropagation()
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = setting.ids.SettingApp
|
||||
loc.path[2] = 'classes'
|
||||
loc.path.length = 3
|
||||
loc.query = { _class }
|
||||
loc.fragment = undefined
|
||||
navigate(loc)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<div
|
||||
class="attrbar-header"
|
||||
class:collapsed
|
||||
on:click={() => {
|
||||
collapsed = !collapsed
|
||||
}}
|
||||
>
|
||||
<div class="flex-row-center">
|
||||
<span class="overflow-label">
|
||||
<Label {label} />
|
||||
</span>
|
||||
<div class="icon-arrow">
|
||||
<svg fill="var(--dark-color)" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0,0L6,3L0,6Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if keys.length || !vertical}
|
||||
<div class="tool">
|
||||
<Tooltip label={setting.string.ClassSetting}>
|
||||
<Button
|
||||
icon={setting.icon.Setting}
|
||||
kind={'transparent'}
|
||||
on:click={(ev) => {
|
||||
ev.stopPropagation()
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[1] = setting.ids.SettingApp
|
||||
loc.path[2] = 'classes'
|
||||
loc.path.length = 3
|
||||
loc.query = { _class }
|
||||
loc.fragment = undefined
|
||||
navigate(loc)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{#if keys.length}
|
||||
<div class="collapsed-container" class:collapsed>
|
||||
<AttributesBar {_class} {object} keys={keys.map((p) => p.key)} {vertical} />
|
||||
<AttributesBar {_class} {object} keys={keys.map((p) => p.key)} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -128,8 +128,8 @@
|
||||
}
|
||||
|
||||
async function getCollectionEditor (key: KeyedAttribute): Promise<AnyComponent> {
|
||||
const attrClass = getAttributePresenterClass(key.attr)
|
||||
const clazz = hierarchy.getClass(attrClass)
|
||||
const attrClass = getAttributePresenterClass(hierarchy, key.attr)
|
||||
const clazz = hierarchy.getClass(attrClass.attrClass)
|
||||
const editorMixin = hierarchy.as(clazz, view.mixin.CollectionEditor)
|
||||
return editorMixin.editor
|
||||
}
|
||||
|
@ -107,7 +107,7 @@
|
||||
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) continue
|
||||
const value = getValue(attribute.name, attribute.type)
|
||||
if (result.findIndex((p) => p.value === value) !== -1) continue
|
||||
const typeClassId = getAttributePresenterClass(attribute)
|
||||
const typeClassId = getAttributePresenterClass(hierarchy, attribute).attrClass
|
||||
const typeClass = hierarchy.getClass(typeClassId)
|
||||
let presenter = hierarchy.as(typeClass, view.mixin.AttributePresenter).presenter
|
||||
let parent = typeClass.extends
|
||||
|
@ -102,15 +102,15 @@ async function getAttributePresenter (
|
||||
): Promise<AttributeModel> {
|
||||
const hierarchy = client.getHierarchy()
|
||||
const attribute = hierarchy.getAttribute(_class, key)
|
||||
let attrClass = getAttributePresenterClass(attribute)
|
||||
const isCollectionAttr = hierarchy.isDerived(attribute.type._class, core.class.Collection)
|
||||
const presenterClass = getAttributePresenterClass(hierarchy, attribute)
|
||||
const isCollectionAttr = presenterClass.category === 'collection'
|
||||
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.AttributePresenter
|
||||
const clazz = hierarchy.getClass(attrClass)
|
||||
const clazz = hierarchy.getClass(presenterClass.attrClass)
|
||||
let presenterMixin = hierarchy.as(clazz, mixin)
|
||||
let parent = clazz.extends
|
||||
while (presenterMixin.presenter === undefined && parent !== undefined) {
|
||||
const pclazz = hierarchy.getClass(parent)
|
||||
attrClass = parent
|
||||
presenterClass.attrClass = parent
|
||||
presenterMixin = hierarchy.as(pclazz, mixin)
|
||||
parent = pclazz.extends
|
||||
}
|
||||
@ -124,7 +124,7 @@ async function getAttributePresenter (
|
||||
return {
|
||||
key: preserveKey.key,
|
||||
sortingKey,
|
||||
_class: attrClass,
|
||||
_class: presenterClass.attrClass,
|
||||
label: preserveKey.label ?? attribute.shortLabel ?? attribute.label,
|
||||
presenter,
|
||||
props: {},
|
||||
|
@ -94,6 +94,13 @@ export interface CollectionEditor extends Class<Doc> {
|
||||
editor: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ArrayEditor extends Class<Doc> {
|
||||
editor: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -367,6 +374,7 @@ const view = plugin(viewId, {
|
||||
AttributeEditor: '' as Ref<Mixin<AttributeEditor>>,
|
||||
CollectionPresenter: '' as Ref<Mixin<CollectionPresenter>>,
|
||||
CollectionEditor: '' as Ref<Mixin<CollectionEditor>>,
|
||||
ArrayEditor: '' as Ref<Mixin<ArrayEditor>>,
|
||||
AttributePresenter: '' as Ref<Mixin<AttributePresenter>>,
|
||||
ObjectEditor: '' as Ref<Mixin<ObjectEditor>>,
|
||||
ObjectEditorHeader: '' as Ref<Mixin<ObjectEditorHeader>>,
|
||||
|
39
tests/sanity/tests/recruit.review.spec.ts
Normal file
39
tests/sanity/tests/recruit.review.spec.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { generateId, PlatformSetting, PlatformURI } from './utils'
|
||||
|
||||
test.use({
|
||||
storageState: PlatformSetting
|
||||
})
|
||||
|
||||
test.describe('recruit review tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Create user and workspace
|
||||
await page.goto(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp`)
|
||||
})
|
||||
test('create-review', async ({ page, context }) => {
|
||||
await page.click('[id="app-recruit\\:string\\:RecruitApplication"]')
|
||||
await page.click('text=Reviews')
|
||||
await page.click('button:has-text("Review")')
|
||||
await page.click('[placeholder="Title"]')
|
||||
const reviewId = 'review-' + generateId()
|
||||
await page.fill('[placeholder="Title"]', reviewId)
|
||||
|
||||
await page.click('button:has-text("1 member")')
|
||||
|
||||
await page.click('button:has-text("Rosamund Chen")')
|
||||
|
||||
await page.press('[placeholder="Search\\.\\.\\."]', 'Escape')
|
||||
|
||||
await page.click('form button :has-text("Talent")')
|
||||
// Click button:has-text("Rosamund Chen")
|
||||
await page.click('button:has-text("Rosamund Chen")')
|
||||
|
||||
await page.click('button:has-text("Create")')
|
||||
|
||||
await page.click(`tr:has-text('${reviewId}') td a`)
|
||||
await page.click('button:has-text("2 members")')
|
||||
await page.click('.popup button:has-text("Rosamund Chen")')
|
||||
await page.press('[placeholder="Search\\.\\.\\."]', 'Escape')
|
||||
await page.click('button:has-text("1 member")')
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user