Fix members in Review/Board etc. (#2036)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-06-08 20:55:01 +07:00 committed by GitHub
parent e7b5d0e9b3
commit a9392e1921
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 255 additions and 573 deletions

View File

@ -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
})

View File

@ -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,

View File

@ -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)

View File

@ -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(

View File

@ -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
}
/**

View File

@ -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 })
}
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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>

View File

@ -55,9 +55,9 @@
selectedUsers: items
},
evt.target as HTMLElement,
() => {},
undefined,
(result) => {
if (result !== undefined) {
if (result != null) {
items = result
dispatch('update', items)
}

View File

@ -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 }
}

View File

@ -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}

View File

@ -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);

View File

@ -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) {

View File

@ -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}

View File

@ -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",

View File

@ -28,7 +28,6 @@
"Reminder": "Напоминание",
"RemindMeAt": "Напомнить мне",
"ReminderTime": "Время напоминания",
"EditReminder": "Редактировать напоминание",
"CreateReminder": "Создать напоминание",
"CreatedReminder": "Создал напоминание",
"Reminders": "Напоминания",

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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,

View File

@ -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%'}
/>

View File

@ -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),

View File

@ -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"
}
}

View File

@ -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": "Пожалуйста выберите вакансию"
}
}

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -42,7 +42,6 @@
_class={recruit.class.Review}
config={[
'',
{ key: '$lookup.space.name', label: recruit.string.ReviewCategoryTitle },
'verdict',
{
key: '',

View File

@ -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,

View File

@ -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
}

View File

@ -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>>
},

View File

@ -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}

View File

@ -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
}

View File

@ -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

View File

@ -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: {},

View File

@ -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>>,

View 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")')
})
})