State editor (#411)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2021-11-29 17:32:17 +06:00 committed by GitHub
parent 9f9123c546
commit 4ae06a7677
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1398 additions and 1138 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
import type { IntlString } from '@anticrm/platform'
import type { Account, Arr, Ref, Space, Domain, State } from '@anticrm/core'
import { DOMAIN_MODEL } from '@anticrm/core'
import { Model, Prop, TypeString } from '@anticrm/model'
import { Model, Prop, TypeState, TypeString } from '@anticrm/model'
import core from './component'
import { TDoc } from './core'
@ -49,7 +49,7 @@ export class TState extends TDoc implements State {
@Model(core.class.DocWithState, core.class.AttachedDoc)
export class TDocWithState extends TDoc {
@Prop(TypeString(), 'State' as IntlString)
@Prop(TypeState(), 'State' as IntlString)
state!: Ref<State>
@Prop(TypeString(), 'No.' as IntlString)

View File

@ -19,6 +19,7 @@ import type { Ref, FindOptions, Doc, Domain, Class } from '@anticrm/core'
import core, { TSpace, TSpaceWithStates, TDocWithState } from '@anticrm/model-core'
import type { Vacancy, Candidates, Candidate, Applicant } from '@anticrm/recruit'
import activity from '@anticrm/activity'
import type { Employee } from '@anticrm/contact'
import workbench from '@anticrm/model-workbench'
@ -73,6 +74,9 @@ export class TApplicant extends TDocWithState implements Applicant {
@Prop(TypeString(), 'Attachments' as IntlString)
attachments?: number
@Prop(TypeString(), 'Assigned recruiter' as IntlString)
employee!: Ref<Employee>
}
export function createModel (builder: Builder): void {

View File

@ -109,6 +109,10 @@ export function createModel (builder: Builder): void {
presenter: view.component.TimestampPresenter
})
builder.mixin(core.class.State, core.class.Class, view.mixin.AttributeEditor, {
editor: view.component.StateEditor
})
builder.mixin(core.class.State, core.class.Class, view.mixin.AttributePresenter, {
presenter: view.component.StatePresenter
})

View File

@ -32,6 +32,7 @@ export default mergeIds(viewId, view, {
BooleanPresenter: '' as AnyComponent,
BooleanEditor: '' as AnyComponent,
StatePresenter: '' as AnyComponent,
StateEditor: '' as AnyComponent,
TimestampPresenter: '' as AnyComponent,
TableView: '' as AnyComponent,

View File

@ -280,6 +280,13 @@ export function TypeTimestamp (): Type<string> {
return { _class: core.class.TypeTimestamp, label: 'TypeTimestamp' as IntlString }
}
/**
* @public
*/
export function TypeState (): Type<string> {
return { _class: core.class.State, label: 'TypeState' as IntlString }
}
/**
* @public
*/

View File

@ -25,7 +25,7 @@
export let title: string
export let icon: Asset | AnySvelteComponent
export let fullSize: boolean = false
export let rightSection: AnyComponent | undefined
export let rightSection: AnyComponent | undefined = undefined
export let object: Doc
const dispatch = createEventDispatcher()

View File

@ -28,6 +28,7 @@
export let maxWidth: string
export let focus: boolean = false
export let minimize: boolean = false
export let showHeader: boolean = true
const _class = object._class
const client = getClient()
@ -59,14 +60,18 @@
<CircleButton icon={attribute.icon} size={'large'} />
{#if !minimize}
<div class="flex-col with-icon">
<Label label={attribute.label} />
{#if showHeader}
<Label label={attribute.label} />
{/if}
<div class="value"><svelte:component this={instance} label={attribute?.label} placeholder={attribute?.label} {maxWidth} bind:value={object[key]} {onChange} {focus}/></div>
</div>
{/if}
</div>
{:else}
<div class="flex-col">
<Label label={attribute.label} />
{#if showHeader}
<Label label={attribute.label} />
{/if}
<div class="value"><svelte:component this={instance} label={attribute?.label} placeholder={attribute?.label} {maxWidth} bind:value={object[key]} {onChange} {focus}/></div>
</div>
{/if}

View File

@ -24,12 +24,14 @@
import type { Ref, Class } from '@anticrm/core'
import { formatName, Person } from '@anticrm/contact'
import { createEventDispatcher } from 'svelte'
export let _class: Ref<Class<Person>>
export let title: IntlString
export let caption: IntlString
export let value: Ref<Person>
export let show: boolean = false
const dispatch = createEventDispatcher()
let selected: Person | undefined
let btn: HTMLElement
@ -58,7 +60,10 @@
if (!opened) {
opened = true
showPopup(UsersPopup, { _class, title, caption }, container, (result) => {
if (result) { value = result._id }
if (result) {
value = result._id
dispatch('change', value)
}
opened = false
})
}

View File

@ -0,0 +1,40 @@
<!--
// 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 { AttributeBarEditor, getClient, UserBox } from '@anticrm/presentation'
import { Applicant } from '@anticrm/recruit'
import contact from '@anticrm/contact'
export let object: Applicant
const client = getClient()
function change () {
client.updateDoc(object._class, object.space, object._id, { employee: object.employee })
}
</script>
<div class="flex-between header">
<UserBox _class={contact.class.Employee} title='Assigned recruiter' caption='Recruiters' bind:value={object.employee} on:change={change} />
<AttributeBarEditor key={'state'} {object} showHeader={false} />
</div>
<style lang="scss">
.header {
width: 100%;
padding: 0 .5rem;
}
</style>

View File

@ -26,7 +26,7 @@ export let value: Applicant
const client = getClient()
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
function show() {
function show () {
closeTooltip()
showPopup(EditApplication, { _id: value._id }, 'full')
}

View File

@ -43,11 +43,11 @@
const dispatch = createEventDispatcher()
const client = getClient()
export function canClose(): boolean {
export function canClose (): boolean {
return candidate === undefined && employee === undefined
}
async function createApplication() {
async function createApplication () {
const state = await client.findOne(core.class.State, { space: _space })
if (state === undefined) {
throw new Error('create application: state not found')
@ -62,19 +62,20 @@
const id = await client.addCollection(recruit.class.Applicant, _space, candidate, recruit.class.Candidate, 'applications', {
state: state._id,
number: incResult.object.sequence,
employee: employee
})
}
async function validate(candidate: Ref<Candidate>, space: Ref<Space>) {
async function validate (candidate: Ref<Candidate>, space: Ref<Space>) {
if (candidate === undefined) {
status = new Status(Severity.INFO, recruit.status.CandidateRequired, {})
} else {
if (space === undefined) {
status = new Status(Severity.INFO, recruit.status.VacancyRequired, {})
} else {
const applicants = await client.findAll(recruit.class.Applicant, { space, attachedTo: candidate})
const applicants = await client.findAll(recruit.class.Applicant, { space, attachedTo: candidate })
if (applicants.length > 0) {
status = new Status(Severity.ERROR, recruit.status.ApplicationExists, {})
status = new Status(Severity.ERROR, recruit.status.ApplicationExists, {})
} else {
status = OK
}
@ -94,7 +95,6 @@
spacePlaceholder={'Select vacancy'}
bind:space={_space}
on:close={() => { dispatch('close') }}>
<StatusControl slot="error" {status} />
<Grid column={1} rowGap={1.75}>
{#if !preserveCandidate}

View File

@ -15,33 +15,24 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import type { Ref, Space, Doc, Class } from '@anticrm/core'
import { CircleButton, EditBox, Link, showPopup, IconFile as FileIcon } from '@anticrm/ui'
import type { Attachment } from '@anticrm/chunter'
import FileUpload from './icons/FileUpload.svelte'
import { getClient, createQuery, Channels, AttributeEditor, PDFViewer } from '@anticrm/presentation'
import type { Ref } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { Panel } from '@anticrm/panel'
import type { Candidate, Applicant, Vacancy } from '@anticrm/recruit'
import Contact from './icons/Contact.svelte'
import Avatar from './icons/Avatar.svelte'
import Attachments from './Attachments.svelte'
import Edit from './icons/Edit.svelte'
import SocialEditor from './SocialEditor.svelte'
import CandidateCard from './CandidateCard.svelte'
import VacancyCard from './VacancyCard.svelte'
import chunter from '@anticrm/chunter'
import recruit from '../plugin'
import { formatName } from '@anticrm/contact'
import ApplicantHeader from './ApplicantHeader.svelte'
export let _id: Ref<Applicant>
let object: Applicant
let candidate: Candidate
let vacancy: Vacancy
const client = getClient()
const query = createQuery()
$: query.query(recruit.class.Applicant, { _id }, result => { object = result[0] })
@ -52,26 +43,11 @@
$: if (object !== undefined) vacancyQuery.query(recruit.class.Vacancy, { _id: object.space }, result => { vacancy = result[0] })
const dispatch = createEventDispatcher()
// function saveChannels(result: any) {
// object.channels = result
// client.updateDoc(recruit.class.Candidate, object.space, object._id, { channels: result })
// }
function getVacancyName() {
return client.getModel().getObject(object.space).name
}
</script>
{#if object !== undefined && candidate !== undefined}
<Panel icon={Contact} title={formatName(candidate.name)} {object} on:close={() => { dispatch('close') }}>
<!-- <svelte:fragment slot="subtitle">
<div class="flex-between flex-reverse" style="width: 100%">
<Channels value={object.channels}/>
<CircleButton icon={Edit} label={'Edit'} on:click={(ev) => showPopup(SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => { saveChannels(result) })} />
</div>
</svelte:fragment> -->
<ApplicantHeader {object} slot="subtitle" />
<div class="grid-cards">
<CandidateCard {candidate}/>

View File

@ -24,7 +24,6 @@ import Attachments from './components/Attachments.svelte'
import KanbanCard from './components/KanbanCard.svelte'
import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import ApplicationsPresenter from './components/ApplicationsPresenter.svelte'
import TxApplicantUpdate from './components/activity/TxApplicantUpdate.svelte'
import { showPopup } from '@anticrm/ui'

View File

@ -16,7 +16,7 @@
import { plugin } from '@anticrm/platform'
import type { Plugin, Asset } from '@anticrm/platform'
import type { Space, SpaceWithStates, DocWithState, Ref, Class, AttachedDoc } from '@anticrm/core'
import type { Person } from '@anticrm/contact'
import type { Employee, Person } from '@anticrm/contact'
/**
* @public
@ -46,6 +46,7 @@ export interface Candidate extends Person {
*/
export interface Applicant extends DocWithState, AttachedDoc {
attachments?: number
employee: Ref<Employee>
}
/**

View File

@ -0,0 +1,52 @@
<!--
// 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 core, { Ref, State } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { showPopup } from '@anticrm/ui'
import StatePresenter from './StatePresenter.svelte'
import StatesPopup from './StatesPopup.svelte'
export let value: Ref<State>
export let onChange: (value: any) => void
let state: State
let container: HTMLElement
let opened: boolean = false
const query = createQuery()
$: query.query(core.class.State, { _id: value }, (res) => {
state = res[0]
}, { limit: 1 })
</script>
{#if state}
<div class="flex-row-center" bind:this={container}
on:click|preventDefault={() => {
if (!opened) {
opened = true
showPopup(StatesPopup, { space: state.space }, container, (result) => {
if (result) {
value = result._id
onChange(value)
}
opened = false
})
}
}} >
<StatePresenter value={state} />
</div>
{/if}

View File

@ -0,0 +1,65 @@
<!--
// 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 core, { Ref, SpaceWithStates, State } from "@anticrm/core"
import { createQuery } from "@anticrm/presentation"
import { createEventDispatcher } from "svelte"
export let space: Ref<SpaceWithStates>
let states: State[] = []
const dispatch = createEventDispatcher()
const statesQuery = createQuery()
statesQuery.query(core.class.State, { space }, (res) => states = res)
</script>
<div class="container">
{#each states as state}
<div class="state flex" on:click={() => { dispatch('close', state) }}>
<div class="color" style="background-color: {state.color}"></div>
{state.title}
</div>
{/each}
</div>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
padding: 1rem;
max-height: 100%;
min-width: 10rem;
color: var(--theme-caption-color);
background-color: var(--theme-button-bg-hovered);
border: 1px solid var(--theme-button-border-enabled);
border-radius: .75rem;
user-select: none;
filter: drop-shadow(0 1.5rem 4rem rgba(0, 0, 0, .35));
.state {
margin: 1rem;
cursor: pointer;
.color {
margin-right: .75rem;
width: 1rem;
height: 1rem;
border-radius: .25rem;
}
}
}
</style>

View File

@ -21,6 +21,7 @@ import StringPresenter from './components/StringPresenter.svelte'
import BooleanEditor from './components/BooleanEditor.svelte'
import BooleanPresenter from './components/BooleanPresenter.svelte'
import StatePresenter from './components/StatePresenter.svelte'
import StateEditor from './components/StateEditor.svelte'
import TimestampPresenter from './components/TimestampPresenter.svelte'
import TableView from './components/TableView.svelte'
import Table from './components/Table.svelte'
@ -60,6 +61,7 @@ export default async () => ({
BooleanPresenter,
BooleanEditor,
StatePresenter,
StateEditor,
TableView,
KanbanView,
TimestampPresenter

File diff suppressed because it is too large Load Diff