TSK-833 Доработка Таблицы Vacations TSK-817 Click on Employee is broken, it should open Edit Contact TSK-805 Открывать задачу в новой вкладке при клике колесиком TSK-794 Прикрепление ссылки на вакансию/аппликейшн в чате (#2734)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-03-15 20:06:03 +06:00 committed by GitHub
parent 6b534e1a3d
commit ae86bfdacd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 1515 additions and 878 deletions

View File

@ -93,6 +93,7 @@ async function genVacansyApplicants (
fullDescription: faker.lorem.sentences(10),
location: faker.address.city(),
members: accountIds,
number: faker.datatype.number(),
private: false,
archived: false
}
@ -122,7 +123,7 @@ async function genVacansyApplicants (
)
}
console.log('Vacandy attachments generated', vacancy.name)
console.log('Vacancy attachments generated', vacancy.name)
const states = await ctx.with('create-kanbad', {}, (ctx) => createUpdateSpaceKanban(ctx, vacancyId, client))

View File

@ -98,7 +98,7 @@ export class TChunterMessage extends TAttachedDoc implements ChunterMessage {
}
@Model(chunter.class.ThreadMessage, chunter.class.ChunterMessage)
@UX(chunter.string.ThreadMessage)
@UX(chunter.string.ThreadMessage, undefined, 'TMSG')
export class TThreadMessage extends TChunterMessage implements ThreadMessage {
declare attachedTo: Ref<Message>
@ -106,7 +106,7 @@ export class TThreadMessage extends TChunterMessage implements ThreadMessage {
}
@Model(chunter.class.Message, chunter.class.ChunterMessage)
@UX(chunter.string.Message)
@UX(chunter.string.Message, undefined, 'MSG')
export class TMessage extends TChunterMessage implements Message {
declare attachedTo: Ref<Space>
@ -132,7 +132,7 @@ export class TReaction extends TAttachedDoc implements Reaction {
}
@Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT)
@UX(chunter.string.Comment)
@UX(chunter.string.Comment, undefined, 'COM')
export class TComment extends TAttachedDoc implements Comment {
@Prop(TypeMarkup(), chunter.string.Message)
@Index(IndexKind.FullText)
@ -330,6 +330,7 @@ export function createModel (builder: Builder, options = { addApplication: true
{
label: chunter.string.ApplicationLabelChunter,
icon: chunter.icon.Chunter,
locationResolver: chunter.resolver.Location,
alias: chunterId,
hidden: false,
navigatorModel: {
@ -432,6 +433,95 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.ids.TxCommentCreate
)
createAction(
builder,
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: chunter.function.GetLink
},
label: chunter.string.CopyLink,
icon: chunter.icon.Thread,
keyBinding: [],
input: 'none',
category: chunter.category.Chunter,
target: chunter.class.Comment,
context: {
mode: ['context', 'browser'],
group: 'copy'
}
},
chunter.action.CopyCommentLink
)
builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open]
})
builder.mixin(chunter.class.Message, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open]
})
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open]
})
builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.LinkProvider, {
encode: chunter.function.GetFragment
})
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.LinkProvider, {
encode: chunter.function.GetFragment
})
builder.mixin(chunter.class.Message, core.class.Class, view.mixin.LinkProvider, {
encode: chunter.function.GetFragment
})
createAction(
builder,
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: chunter.function.GetLink
},
label: chunter.string.CopyLink,
icon: chunter.icon.Thread,
keyBinding: [],
input: 'none',
category: chunter.category.Chunter,
target: chunter.class.Message,
context: {
mode: ['context', 'browser'],
application: chunter.app.Chunter,
group: 'copy'
}
},
chunter.action.CopyMessageLink
)
createAction(
builder,
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: chunter.function.GetLink
},
label: chunter.string.CopyLink,
icon: chunter.icon.Thread,
keyBinding: [],
input: 'none',
category: chunter.category.Chunter,
target: chunter.class.ThreadMessage,
context: {
mode: ['context', 'browser'],
application: chunter.app.Chunter,
group: 'copy'
}
},
chunter.action.CopyThreadMessageLink
)
// We need to define this one, to hide default attached object removed case
builder.createDoc(
activity.class.TxViewlet,

View File

@ -38,7 +38,10 @@ export default mergeIds(chunterId, chunter, {
MarkUnread: '' as Ref<Action>,
ArchiveChannel: '' as Ref<Action>,
UnarchiveChannel: '' as Ref<Action>,
ConvertToPrivate: '' as Ref<Action>
ConvertToPrivate: '' as Ref<Action>,
CopyCommentLink: '' as Ref<Action>,
CopyThreadMessageLink: '' as Ref<Action>,
CopyMessageLink: '' as Ref<Action>
},
actionImpl: {
MarkUnread: '' as ViewAction,
@ -93,7 +96,9 @@ export default mergeIds(chunterId, chunter, {
Random: '' as Ref<Channel>
},
function: {
ChunterBrowserVisible: '' as Resource<(spaces: Space[]) => Promise<boolean>>
ChunterBrowserVisible: '' as Resource<(spaces: Space[]) => Promise<boolean>>,
GetLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
},
filter: {
CommentsFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>,

View File

@ -76,7 +76,7 @@ export class TChannelProvider extends TDoc implements ChannelProvider {
}
@Model(contact.class.Contact, core.class.Doc, DOMAIN_CONTACT)
@UX(contact.string.Contact, contact.icon.Person, undefined, 'name')
@UX(contact.string.Contact, contact.icon.Person, 'CONT', 'name')
export class TContact extends TDoc implements Contact {
@Prop(TypeString(), contact.string.Name)
@Index(IndexKind.FullText)
@ -120,7 +120,7 @@ export class TChannel extends TAttachedDoc implements Channel {
}
@Model(contact.class.Person, contact.class.Contact)
@UX(contact.string.Person, contact.icon.Person, undefined, 'name')
@UX(contact.string.Person, contact.icon.Person, 'PRSN', 'name')
export class TPerson extends TContact implements Person {
@Prop(TypeDate(DateRangeMode.DATE, false), contact.string.Birthday)
birthday?: Timestamp
@ -134,7 +134,7 @@ export class TMember extends TAttachedDoc implements Member {
}
@Model(contact.class.Organization, contact.class.Contact)
@UX(contact.string.Organization, contact.icon.Company, undefined, 'name')
@UX(contact.string.Organization, contact.icon.Company, 'ORG', 'name')
export class TOrganization extends TContact implements Organization {
@Prop(Collection(contact.class.Member), contact.string.Members)
members!: number
@ -150,7 +150,7 @@ export class TStatus extends TAttachedDoc implements Status {
}
@Model(contact.class.Employee, contact.class.Person)
@UX(contact.string.Employee, contact.icon.Person, undefined, 'name')
@UX(contact.string.Employee, contact.icon.Person, 'EMP', 'name')
export class TEmployee extends TPerson implements Employee {
active!: boolean
@ -221,7 +221,8 @@ export function createModel (builder: Builder): void {
icon: contact.icon.ContactApplication,
alias: contactId,
hidden: false,
component: contact.component.ContactsTabs
component: contact.component.ContactsTabs,
locationResolver: contact.resolver.Location
},
contact.app.Contacts
)
@ -332,6 +333,10 @@ export function createModel (builder: Builder): void {
presenter: contact.component.ChannelsPresenter
})
builder.mixin(contact.class.Contact, core.class.Class, view.mixin.LinkProvider, {
encode: contact.function.GetContactLink
})
builder.createDoc(
contact.class.ChannelProvider,
core.space.Model,

View File

@ -182,6 +182,10 @@ export function createModel (builder: Builder): void {
component: document.component.CreateDocument
})
builder.mixin(document.class.Document, core.class.Class, view.mixin.ObjectPanel, {
component: document.component.EditDoc
})
builder.createDoc(
view.class.Viewlet,
core.space.Model,

View File

@ -18,6 +18,7 @@ import { Doc, FindOptions, IndexKind, Lookup, Ref, Timestamp } from '@hcengineer
import {
Builder,
Collection,
Hidden,
Index,
Mixin,
Model,
@ -59,7 +60,7 @@ import { createReviewModel, reviewTableConfig, reviewTableOptions } from './revi
import { TOpinion, TReview } from './review-model'
@Model(recruit.class.Vacancy, task.class.SpaceWithStates)
@UX(recruit.string.Vacancy, recruit.icon.Vacancy, undefined, 'name')
@UX(recruit.string.Vacancy, recruit.icon.Vacancy, 'VCN', 'name')
export class TVacancy extends TSpaceWithStates implements Vacancy {
@Prop(TypeMarkup(), recruit.string.FullDescription)
@Index(IndexKind.FullText)
@ -83,6 +84,11 @@ export class TVacancy extends TSpaceWithStates implements Vacancy {
@Prop(Collection(chunter.class.Backlink), chunter.string.Comments)
relations!: number
@Prop(TypeString(), recruit.string.Vacancy)
@Index(IndexKind.FullText)
@Hidden()
number!: number
}
@Model(recruit.class.Candidates, core.class.Space)
@ -135,7 +141,7 @@ export class TVacancyList extends TOrganization implements VacancyList {
}
@Model(recruit.class.Applicant, task.class.Task)
@UX(recruit.string.Application, recruit.icon.Application, recruit.string.ApplicationShort, 'number')
@UX(recruit.string.Application, recruit.icon.Application, 'APP', 'number')
export class TApplicant extends TTask implements Applicant {
// We need to declare, to provide property with label
@Prop(TypeRef(recruit.mixin.Candidate), recruit.string.Talent)
@ -162,7 +168,7 @@ export class TApplicant extends TTask implements Applicant {
}
@Model(recruit.class.ApplicantMatch, core.class.AttachedDoc, DOMAIN_TASK)
@UX(recruit.string.Application, recruit.icon.Application, recruit.string.ApplicationShort, 'number')
@UX(recruit.string.Application, recruit.icon.Application, 'APP', 'number')
export class TApplicantMatch extends TAttachedDoc implements ApplicantMatch {
// We need to declare, to provide property with label
@Prop(TypeRef(recruit.mixin.Candidate), recruit.string.Talent)
@ -234,6 +240,7 @@ export function createModel (builder: Builder): void {
{
label: recruit.string.RecruitApplication,
icon: recruit.icon.RecruitApplication,
locationResolver: recruit.resolver.Location,
alias: recruitId,
hidden: false,
navigatorModel: {
@ -616,7 +623,31 @@ export function createModel (builder: Builder): void {
})
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: recruit.function.ApplicationTitleProvider
titleProvider: recruit.function.AppTitleProvider
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: recruit.function.RevTitleProvider
})
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: recruit.function.VacTitleProvider
})
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.LinkProvider, {
encode: recruit.function.GetObjectLinkFragment
})
builder.mixin(recruit.class.Opinion, core.class.Class, view.mixin.LinkProvider, {
encode: recruit.function.GetObjectLinkFragment
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.LinkProvider, {
encode: recruit.function.GetObjectLinkFragment
})
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.LinkProvider, {
encode: recruit.function.GetObjectLinkFragment
})
builder.createDoc(
@ -745,6 +776,7 @@ export function createModel (builder: Builder): void {
},
input: 'focus',
category: recruit.category.Recruit,
override: [view.action.Open],
keyBinding: ['keyE'],
target: recruit.class.Vacancy,
context: {
@ -757,6 +789,10 @@ export function createModel (builder: Builder): void {
actions: [view.action.Delete]
})
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ObjectPanel, {
component: recruit.component.EditVacancy
})
builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ClassFilters, {
filters: ['_class']
})
@ -887,7 +923,7 @@ export function createModel (builder: Builder): void {
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: recruit.function.GetApplicationId
textProvider: recruit.function.IdProvider
},
label: recruit.string.CopyId,
icon: recruit.icon.Application,
@ -908,7 +944,7 @@ export function createModel (builder: Builder): void {
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: recruit.function.GetApplicationLink
textProvider: recruit.function.GetObjectLink
},
label: recruit.string.CopyLink,
icon: recruit.icon.Application,
@ -929,14 +965,14 @@ export function createModel (builder: Builder): void {
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: recruit.function.GetRecruitLink
textProvider: recruit.function.GetObjectLink
},
label: recruit.string.CopyLink,
icon: recruit.icon.Application,
keyBinding: [],
input: 'none',
category: recruit.category.Recruit,
target: contact.class.Person,
target: recruit.class.Vacancy,
context: {
mode: ['context', 'browser'],
application: recruit.app.Recruit,

View File

@ -15,15 +15,25 @@
import { getCategories } from '@anticrm/skillset'
import { Organization } from '@hcengineering/contact'
import core, { Doc, DOMAIN_TX, Ref, Space, TxCollectionCUD, TxCreateDoc, TxOperations } from '@hcengineering/core'
import core, {
Doc,
DOMAIN_TX,
Ref,
Space,
TxCollectionCUD,
TxCreateDoc,
TxFactory,
TxOperations,
TxProcessor
} from '@hcengineering/core'
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import { DOMAIN_CALENDAR } from '@hcengineering/model-calendar'
import contact, { DOMAIN_CONTACT } from '@hcengineering/model-contact'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import tags, { TagCategory } from '@hcengineering/model-tags'
import { createKanbanTemplate, createSequence, DOMAIN_TASK } from '@hcengineering/model-task'
import { createKanbanTemplate, createSequence, DOMAIN_KANBAN, DOMAIN_TASK } from '@hcengineering/model-task'
import { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import task, { KanbanTemplate } from '@hcengineering/task'
import task, { KanbanTemplate, Sequence } from '@hcengineering/task'
import recruit from './plugin'
async function fixImportedTitle (client: MigrationClient): Promise<void> {
@ -79,6 +89,57 @@ async function setCreate (client: MigrationClient): Promise<void> {
}
}
async function fillVacancyNumbers (client: MigrationClient): Promise<void> {
const docs = await client.find<Vacancy>(DOMAIN_SPACE, {
_class: recruit.class.Vacancy,
number: { $exists: false }
})
if (docs.length === 0) return
const txex = await client.find<TxCreateDoc<Vacancy>>(DOMAIN_TX, {
objectId: { $in: docs.map((it) => it._id) },
_class: core.class.TxCreateDoc
})
let number = 1
for (const doc of docs) {
await client.update(
DOMAIN_SPACE,
{
_id: doc._id
},
{
number
}
)
const tx = txex.find((it) => it.objectId === doc._id)
if (tx !== undefined) {
await client.update(
DOMAIN_TX,
{
_id: tx._id
},
{
'attributes.number': number
}
)
}
number++
}
const current = await client.find<Sequence>(DOMAIN_KANBAN, {
_class: task.class.Sequence,
attachedto: recruit.class.Vacancy
})
if (current.length === 0) {
const factory = new TxFactory(core.account.System)
const tx = factory.createTxCreateDoc(task.class.Sequence, task.space.Sequence, {
attachedTo: recruit.class.Vacancy,
sequence: number
})
const doc = TxProcessor.createDoc2Doc(tx)
await client.create(DOMAIN_KANBAN, doc)
await client.create(DOMAIN_TX, tx)
}
}
async function fillCreatedBy (client: MigrationClient): Promise<void> {
const objects = await client.find<Vacancy>(DOMAIN_SPACE, {
_class: recruit.class.Vacancy,
@ -115,6 +176,7 @@ export const recruitOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await setCreate(client)
await fixImportedTitle(client)
await fillVacancyNumbers(client)
await client.update(
DOMAIN_CALENDAR,
{
@ -196,6 +258,7 @@ async function createDefaults (tx: TxOperations): Promise<void> {
await createSequence(tx, recruit.class.Review)
await createSequence(tx, recruit.class.Opinion)
await createSequence(tx, recruit.class.Applicant)
await createSequence(tx, recruit.class.Vacancy)
await createDefaultKanbanTemplate(tx)
}

View File

@ -39,12 +39,10 @@ export default mergeIds(recruitId, recruit, {
Recruit: '' as Ref<ActionCategory>
},
function: {
GetApplicationId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetApplicationLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetRecruitLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
GetObjectLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetObjectLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
},
string: {
ApplicationShort: '' as IntlString,
ApplicationsShort: '' as IntlString,
RecruitApplication: '' as IntlString,
TalentPools: '' as IntlString,

View File

@ -11,7 +11,7 @@ import { Applicant, Candidate, Opinion, Review } from '@hcengineering/recruit'
import recruit from './plugin'
@Model(recruit.class.Review, calendar.class.Event)
@UX(recruit.string.Review, recruit.icon.Review, recruit.string.ReviewShortLabel, 'number')
@UX(recruit.string.Review, recruit.icon.Review, 'RVE', 'number')
export class TReview extends TEvent implements Review {
// We need to declare, to provide property with label
@Prop(TypeRef(recruit.mixin.Candidate), recruit.string.Talent)
@ -35,7 +35,7 @@ export class TReview extends TEvent implements Review {
}
@Model(recruit.class.Opinion, core.class.AttachedDoc, 'recruit' as Domain)
@UX(recruit.string.Opinion, recruit.icon.Opinion, recruit.string.OpinionShortLabel)
@UX(recruit.string.Opinion, recruit.icon.Opinion, 'OPE')
export class TOpinion extends TAttachedDoc implements Opinion {
@Prop(TypeString(), task.string.TaskNumber)
number!: number

View File

@ -159,7 +159,7 @@ export class TTypeSprintStatus extends TType {}
* @public
*/
@Model(tracker.class.Team, core.class.Space, DOMAIN_SPACE)
@UX(tracker.string.Team, tracker.icon.Team, tracker.string.Team)
@UX(tracker.string.Team, tracker.icon.Team, 'TEAM')
export class TTeam extends TSpace implements Team {
@Prop(TypeString(), tracker.string.Title)
@Index(IndexKind.FullText)
@ -197,7 +197,7 @@ export function TypeReportedTime (): Type<number> {
* @public
*/
@Model(tracker.class.Issue, core.class.AttachedDoc, DOMAIN_TRACKER)
@UX(tracker.string.Issue, tracker.icon.Issue, tracker.string.Issue, 'title')
@UX(tracker.string.Issue, tracker.icon.Issue, 'TSK', 'title')
export class TIssue extends TAttachedDoc implements Issue {
@Prop(TypeRef(tracker.class.Issue), tracker.string.Parent)
declare attachedTo: Ref<Issue>
@ -280,7 +280,7 @@ export class TIssue extends TAttachedDoc implements Issue {
* @public
*/
@Model(tracker.class.IssueTemplate, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.IssueTemplate, tracker.icon.Issue, tracker.string.IssueTemplate)
@UX(tracker.string.IssueTemplate, tracker.icon.Issue, 'PROCESS')
export class TIssueTemplate extends TDoc implements IssueTemplate {
@Prop(TypeString(), tracker.string.Title)
@Index(IndexKind.FullText)
@ -330,7 +330,7 @@ export class TIssueTemplate extends TDoc implements IssueTemplate {
* @public
*/
@Model(tracker.class.TimeSpendReport, core.class.AttachedDoc, DOMAIN_TRACKER)
@UX(tracker.string.TimeSpendReport, tracker.icon.TimeReport, tracker.string.TimeSpendReport)
@UX(tracker.string.TimeSpendReport, tracker.icon.TimeReport)
export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport {
@Prop(TypeRef(tracker.class.Issue), tracker.string.Parent)
declare attachedTo: Ref<Issue>
@ -352,7 +352,7 @@ export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport {
* @public
*/
@Model(tracker.class.Project, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Project, tracker.icon.Project, tracker.string.Project)
@UX(tracker.string.Project, tracker.icon.Project, 'PROJECT')
export class TProject extends TDoc implements Project {
@Prop(TypeString(), tracker.string.Title)
// @Index(IndexKind.FullText)
@ -392,7 +392,7 @@ export class TProject extends TDoc implements Project {
* @public
*/
@Model(tracker.class.Sprint, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Sprint, tracker.icon.Sprint, tracker.string.Sprint)
@UX(tracker.string.Sprint, tracker.icon.Sprint)
export class TSprint extends TDoc implements Sprint {
@Prop(TypeString(), tracker.string.Title)
// @Index(IndexKind.FullText)
@ -437,7 +437,7 @@ export class TSprint extends TDoc implements Sprint {
* @public
*/
@Model(tracker.class.Scrum, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Scrum, tracker.icon.Scrum, tracker.string.Scrum)
@UX(tracker.string.Scrum, tracker.icon.Scrum)
export class TScrum extends TDoc implements Scrum {
@Prop(TypeString(), tracker.string.Title)
title!: string
@ -467,7 +467,7 @@ export class TScrum extends TDoc implements Scrum {
* @public
*/
@Model(tracker.class.ScrumRecord, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.ScrumRecord, tracker.icon.Scrum, tracker.string.ScrumRecord)
@UX(tracker.string.ScrumRecord, tracker.icon.Scrum)
export class TScrumRecord extends TAttachedDoc implements ScrumRecord {
@Prop(TypeString(), tracker.string.Title)
label!: string
@ -925,6 +925,18 @@ export function createModel (builder: Builder): void {
func: tracker.function.GetAllSprints
})
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.LinkProvider, {
encode: tracker.function.GetIssueLinkFragment
})
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectPanel, {
component: tracker.component.EditIssue
})
builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ObjectPanel, {
component: tracker.component.EditIssueTemplate
})
builder.createDoc(
workbench.class.Application,
core.space.Model,

View File

@ -63,7 +63,9 @@ import type {
ViewletPreference,
ViewOptionsModel,
ViewOptions,
AllValuesFunc
AllValuesFunc,
LinkProvider,
ObjectPanel
} from '@hcengineering/view'
import view from './plugin'
@ -289,6 +291,16 @@ export class TLinkPresenter extends TDoc implements LinkPresenter {
component!: AnyComponent
}
@Mixin(view.mixin.LinkProvider, core.class.Class)
export class TLinkProvider extends TClass implements LinkProvider {
encode!: Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
}
@Mixin(view.mixin.ObjectPanel, core.class.Class)
export class TObjectPanel extends TClass implements ObjectPanel {
component!: AnyComponent
}
export type ActionTemplate = Partial<Data<Action>>
/**
@ -318,6 +330,8 @@ export const actionTemplates = template({
export function createModel (builder: Builder): void {
builder.createModel(
TLinkProvider,
TObjectPanel,
TFilterMode,
TClassFilters,
TAttributeFilter,

View File

@ -161,7 +161,7 @@ export interface Class<T extends Obj> extends Classifier {
extends?: Ref<Class<Obj>>
implements?: Ref<Interface<Doc>>[]
domain?: Domain
shortLabel?: IntlString
shortLabel?: string
sortingKey?: string
}

View File

@ -75,7 +75,7 @@ interface ClassTxes {
icon?: Asset
txes: Array<Tx>
kind: ClassifierKind
shortLabel?: IntlString
shortLabel?: string | IntlString
sortingKey?: string
}
@ -226,7 +226,7 @@ export function Mixin<T extends Obj> (_class: Ref<Class<T>>, _extends: Ref<Class
* @param icon -
* @returns
*/
export function UX<T extends Obj> (label: IntlString, icon?: Asset, shortLabel?: IntlString, sortingKey?: string) {
export function UX<T extends Obj> (label: IntlString, icon?: Asset, shortLabel?: string, sortingKey?: string) {
return function classDecorator<C extends new () => T> (constructor: C): void {
const txes = getTxes(constructor.prototype)
txes.label = label

View File

@ -0,0 +1,72 @@
<!--
// 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">
export let href: string | undefined
export let disableClick = false
export let onClick: ((event: MouseEvent) => void) | undefined = undefined
export let noUnderline = false
export let inline = false
function clickHandler (e: MouseEvent) {
if (disableClick) return
onClick?.(e)
}
</script>
{#if disableClick || onClick || href === undefined}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class:cursor-pointer={!disableClick} class:noUnderline class:inline on:click={clickHandler}>
<slot />
</span>
{:else}
<a {href} class:noUnderline class:inline>
<slot />
</a>
{/if}
<style lang="scss">
span,
a {
display: flex;
align-items: center;
flex-shrink: 0;
color: var(--content-color);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
&.inline {
display: inline-flex;
align-items: baseline;
}
&.noUnderline {
color: var(--caption-color);
font-weight: 500;
}
&:not(.noUnderline) {
&:hover {
color: var(--caption-color);
text-decoration: underline;
}
}
&:active {
color: var(--accent-color);
}
}
</style>

View File

@ -48,6 +48,7 @@ export { default as ObjectSearchPopup } from './components/ObjectSearchPopup.sve
export { default as IndexedDocumentPreview } from './components/IndexedDocumentPreview.svelte'
export { default as IndexedDocumentCompare } from './components/IndexedDocumentCompare.svelte'
export { default as DraggableList } from './components/DraggableList.svelte'
export { default as NavLink } from './components/NavLink.svelte'
export { connect, versionError } from './connect'
export { default } from './plugin'
export * from './types'

View File

@ -20,6 +20,7 @@ import core, {
Client,
Doc,
DocumentQuery,
DOMAIN_MODEL,
FindOptions,
findProperty,
FindResult,
@ -46,7 +47,7 @@ import core, {
} from '@hcengineering/core'
import { deepEqual } from 'fast-equals'
const CACHE_SIZE = 20
const CACHE_SIZE = 100
type Callback = (result: FindResult<Doc>) => void
@ -104,22 +105,39 @@ export class LiveQuery extends TxProcessor implements Client {
return true
}
private createDumpQuery<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Query {
const callback: () => void = () => {}
const q = this.createQuery(_class, query, callback, options)
const index = q.callbacks.indexOf(callback as (result: Doc[]) => void)
if (index !== -1) {
q.callbacks.splice(index, 1)
}
if (q.callbacks.length === 0) {
this.queue.push(q)
}
return q
}
async findAll<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
const q = this.findQuery(_class, query, options)
if (q !== undefined) {
if (q.result instanceof Promise) {
q.result = await q.result
}
if (this.removeFromQueue(q)) {
this.queue.push(q)
}
return toFindResult(this.clone(q.result), q.total) as FindResult<T>
if (this.client.getHierarchy().getDomain(_class) === DOMAIN_MODEL) {
return await this.client.findAll(_class, query, options)
}
return await this.client.findAll(_class, query, options)
const q = this.findQuery(_class, query, options) ?? this.createDumpQuery(_class, query, options)
if (q.result instanceof Promise) {
q.result = await q.result
}
if (this.removeFromQueue(q)) {
this.queue.push(q)
}
return toFindResult(this.clone(q.result), q.total) as FindResult<T>
}
async findOne<T extends Doc>(
@ -127,17 +145,17 @@ export class LiveQuery extends TxProcessor implements Client {
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<WithLookup<T> | undefined> {
const q = this.findQuery(_class, query, options)
if (q !== undefined) {
if (q.result instanceof Promise) {
q.result = await q.result
}
if (this.removeFromQueue(q)) {
this.queue.push(q)
}
return this.clone(q.result)[0] as WithLookup<T>
if (this.client.getHierarchy().getDomain(_class) === DOMAIN_MODEL) {
return await this.client.findOne(_class, query, options)
}
return await this.client.findOne(_class, query, options)
const q = this.findQuery(_class, query, options) ?? this.createDumpQuery(_class, query, options)
if (q.result instanceof Promise) {
q.result = await q.result
}
if (this.removeFromQueue(q)) {
this.queue.push(q)
}
return this.clone(q.result)[0] as WithLookup<T>
}
private findQuery<T extends Doc>(
@ -253,7 +271,7 @@ export class LiveQuery extends TxProcessor implements Client {
}
private async checkSearch (q: Query, _id: Ref<Doc>): Promise<boolean> {
const match = await this.findOne(q._class, { $search: q.query.$search, _id }, q.options)
const match = await this.client.findOne(q._class, { $search: q.query.$search, _id }, q.options)
if (q.result instanceof Promise) {
q.result = await q.result
}
@ -274,7 +292,7 @@ export class LiveQuery extends TxProcessor implements Client {
}
private async getCurrentDoc (q: Query, _id: Ref<Doc>): Promise<boolean> {
const current = await this.findOne(q._class, { _id }, q.options)
const current = await this.client.findOne(q._class, { _id }, q.options)
if (q.result instanceof Promise) {
q.result = await q.result
}
@ -360,7 +378,7 @@ export class LiveQuery extends TxProcessor implements Client {
await this.updatedDocCallback(udoc, q)
} else if (isMixin) {
// Mixin potentially added to object we doesn't have in out results
const doc = await this.findOne(q._class, { _id: tx.objectId }, q.options)
const doc = await this.client.findOne(q._class, { _id: tx.objectId }, q.options)
if (doc !== undefined) {
await this.handleDocAdd(q, doc, false)
}
@ -606,7 +624,7 @@ export class LiveQuery extends TxProcessor implements Client {
const tkey = checkMixinKey(key, _class, this.client.getHierarchy())
if (Array.isArray(value)) {
const [_class, nested] = value
const objects = await this.findAll(_class, { _id: getObjectValue(tkey, doc) })
const objects = await this.client.findAll(_class, { _id: getObjectValue(tkey, doc) })
;(result as any)[key] = objects[0]
const nestedResult = {}
const parent = (result as any)[key]
@ -617,7 +635,7 @@ export class LiveQuery extends TxProcessor implements Client {
})
}
} else {
const objects = await this.findAll(value, { _id: getObjectValue(tkey, doc) })
const objects = await this.client.findAll(value, { _id: getObjectValue(tkey, doc) })
;(result as any)[key] = objects[0]
}
}
@ -640,7 +658,7 @@ export class LiveQuery extends TxProcessor implements Client {
} else {
_class = value
}
const objects = await this.findAll(_class, { [attr]: doc._id })
const objects = await this.client.findAll(_class, { [attr]: doc._id })
;(result as any)[key] = objects
}
}
@ -679,7 +697,7 @@ export class LiveQuery extends TxProcessor implements Client {
// If query contains search we must check use fulltext
if (q.query.$search != null && q.query.$search.length > 0) {
const match = await this.findOne(q._class, { $search: q.query.$search, _id: doc._id }, q.options)
const match = await this.client.findOne(q._class, { $search: q.query.$search, _id: doc._id }, q.options)
if (match === undefined) return
}
q.result.push(doc)
@ -740,8 +758,10 @@ export class LiveQuery extends TxProcessor implements Client {
if (Array.isArray(value)) {
if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc)) {
if (reverseLookupKey !== undefined && (doc as any)[reverseLookupKey] === obj._id) {
value.push(doc)
needCallback = true
if ((value as Doc[]).find((p) => p._id === doc._id) === undefined) {
value.push(doc)
needCallback = true
}
}
}
} else {

View File

@ -66,7 +66,6 @@
display: flex;
flex-wrap: nowrap;
min-width: 0;
cursor: default;
}
.ac-header__wrap-description {
flex-direction: column;

View File

@ -109,11 +109,13 @@ window.addEventListener('popstate', () => {
export const location = derived(locationWritable, (loc) => loc)
export function navigate (location: PlatformLocation): void {
export function navigate (location: PlatformLocation, store = true): void {
const url = locationToUrl(location)
if (locationToUrl(getCurrentLocation()) !== url) {
history.pushState(null, '', url)
localStorage.setItem('platform_last_loc', JSON.stringify(location))
if (store) {
history.pushState(null, '', url)
localStorage.setItem('platform_last_loc', JSON.stringify(location))
}
locationWritable.set(location)
}
}

View File

@ -1,5 +1,5 @@
import { writable } from 'svelte/store'
import { getCurrentLocation, location, navigate } from './location'
import { getCurrentLocation, navigate } from './location'
import { AnyComponent, PopupAlignment } from './types'
export interface PanelProps {
@ -13,27 +13,6 @@ export interface PanelProps {
export const panelstore = writable<{ panel?: PanelProps | undefined }>({ panel: undefined })
let currentLocation: string | undefined
location.subscribe((loc) => {
if (loc.fragment !== currentLocation && loc.fragment !== undefined && loc.fragment.trim().length > 0) {
const props = decodeURIComponent(loc.fragment).split('|')
if (props.length >= 3) {
showPanel(
props[0] as AnyComponent,
props[1],
props[2],
(props[3] ?? undefined) as PopupAlignment,
(props[4] ?? undefined) as AnyComponent
)
}
} else if (
(loc.fragment === undefined || (loc.fragment !== undefined && loc.fragment.trim().length === 0)) &&
currentLocation !== undefined
) {
closePanel()
}
})
export function getPanelURI (component: AnyComponent, _id: string, _class: string, element?: PopupAlignment): string {
const panelProps = [component, _id, _class]
if (typeof element === 'string') {
@ -48,6 +27,21 @@ export function showPanel (
_class: string,
element?: PopupAlignment,
rightSection?: AnyComponent
): void {
openPanel(component, _id, _class, element, rightSection)
const location = getCurrentLocation()
if (location.fragment !== currentLocation) {
location.fragment = currentLocation
navigate(location)
}
}
export function openPanel (
component: AnyComponent,
_id: string,
_class: string,
element?: PopupAlignment,
rightSection?: AnyComponent
): void {
const newLoc = getPanelURI(component, _id, _class, element)
if (currentLocation === newLoc) {
@ -57,19 +51,16 @@ export function showPanel (
panelstore.update(() => {
return { panel: { component, _id, _class, element, rightSection } }
})
const location = getCurrentLocation()
if (location.fragment !== currentLocation) {
location.fragment = currentLocation
navigate(location)
}
}
export function closePanel (): void {
export function closePanel (shoulRedirect: boolean = true): void {
panelstore.update(() => {
return { panel: undefined }
})
const location = getCurrentLocation()
location.fragment = undefined
currentLocation = undefined
navigate(location)
if (shoulRedirect) {
const location = getCurrentLocation()
location.fragment = undefined
currentLocation = undefined
navigate(location)
}
}

View File

@ -17,7 +17,7 @@
import type { TxViewlet } from '@hcengineering/activity'
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import core, { AnyAttribute, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import { Asset, getResource } from '@hcengineering/platform'
import { Asset } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
Button,
@ -26,13 +26,12 @@
IconEdit,
IconMoreH,
Label,
Menu,
ShowMore,
showPopup,
TimeSince
} from '@hcengineering/ui'
import type { AttributeModel } from '@hcengineering/view'
import { getActions, ObjectPresenter } from '@hcengineering/view-resources'
import { Menu, ObjectPresenter } from '@hcengineering/view-resources'
import { ActivityKey, DisplayTx } from '../activity'
import activity from '../plugin'
import { getValue, TxDisplayViewlet, updateViewlet } from '../utils'
@ -105,10 +104,10 @@
)
const showMenu = async (ev: MouseEvent): Promise<void> => {
const actions = await getActions(client, tx.doc as Doc)
showPopup(
Menu,
{
object: tx.doc as Doc,
actions: [
{
label: activity.string.Edit,
@ -117,15 +116,7 @@
edit = true
props = getProps(props, edit)
}
},
...actions.map((a) => ({
label: a.label,
icon: a.icon,
action: async (_: any, evt: Event) => {
const impl = await getResource(a.action)
await impl(tx.doc as Doc, evt)
}
}))
}
]
},
ev.target as HTMLElement

View File

@ -13,26 +13,25 @@
// limitations under the License.
-->
<script lang="ts">
import type { Channel } from '@hcengineering/chunter'
import { Channel, chunterId } from '@hcengineering/chunter'
import { getClient } from '@hcengineering/presentation'
import { Icon } from '@hcengineering/ui'
import { getSpaceLink } from '../utils'
import { Icon, NavLink } from '@hcengineering/ui'
export let value: Channel
const client = getClient()
$: icon = client.getHierarchy().getClass(value._class).icon
$: link = getSpaceLink(value._id)
</script>
{#if value}
<a class="flex-presenter" href={link}>
<div class="icon">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
<NavLink app={chunterId} space={value._id}>
<div class="flex-presenter">
<div class="icon">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
</div>
<span class="label">{value.name}</span>
</div>
<span class="label">{value.name}</span>
</a>
</NavLink>
{/if}

View File

@ -13,28 +13,29 @@
// limitations under the License.
-->
<script lang="ts">
import type { DirectMessage } from '@hcengineering/chunter'
import { chunterId, DirectMessage } from '@hcengineering/chunter'
import { getClient } from '@hcengineering/presentation'
import { Icon } from '@hcengineering/ui'
import { Icon, NavLink } from '@hcengineering/ui'
import { getSpaceLink, getDmName } from '../utils'
import { getDmName } from '../utils'
export let value: DirectMessage
const client = getClient()
$: icon = client.getHierarchy().getClass(value._class).icon
$: link = getSpaceLink(value._id)
</script>
{#if value}
{#await getDmName(client, value) then name}
<a class="flex-presenter" href={link}>
<div class="icon">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
<NavLink app={chunterId} space={value._id}>
<div class="flex-presenter">
<div class="icon">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
</div>
<span class="label">{name}</span>
</div>
<span class="label">{name}</span>
</a>
</NavLink>
{/await}
{/if}

View File

@ -21,21 +21,11 @@
import { getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
import { Avatar, copyTextToClipboard, createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import { Avatar, createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import { EmojiPopup } from '@hcengineering/text-editor'
import ui, {
ActionIcon,
Button,
getCurrentLocation,
IconMoreH,
Label,
locationToUrl,
Menu,
showPopup,
tooltip
} from '@hcengineering/ui'
import ui, { ActionIcon, Button, IconMoreH, Label, showPopup, tooltip } from '@hcengineering/ui'
import { Action } from '@hcengineering/view'
import { getActions, LinkPresenter } from '@hcengineering/view-resources'
import { LinkPresenter, Menu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { AddMessageToSaved, DeleteMessageFromSaved, UnpinMessage } from '../index'
import chunter from '../plugin'
@ -115,36 +105,20 @@
}
}
const copyLinkAction = {
label: chunter.string.CopyLink,
action: async () => {
const location = getCurrentLocation()
location.fragment = message._id
location.path[3] = message.space
if (message.attachedToClass === chunter.class.Message) {
location.path.length = 5
location.path[4] = message.attachedTo
} else {
location.path.length = 4
}
const text = `${window.location.origin}${locationToUrl(location)}`
await copyTextToClipboard(text)
}
}
let menuShowed = false
const showMenu = async (ev: Event): Promise<void> => {
const actions = await getActions(client, message, message._class)
actions.push(subscribeAction)
actions.push(pinActions)
const actions = [pinActions]
if (message._class === chunter.class.Message) {
actions.push(subscribeAction)
}
menuShowed = true
showPopup(
Menu,
{
object: message,
baseMenuClass: message._class,
actions: [
...actions.map((a) => ({
label: a.label,
@ -154,7 +128,6 @@
await impl(message, evt)
}
})),
copyLinkAction,
...(getCurrentAccount()._id === message.createBy ? [editAction, deleteAction] : [])
]
},

View File

@ -1,9 +1,9 @@
<script lang="ts">
import { DirectMessage, Message } from '@hcengineering/chunter'
import { getCurrentLocation, navigate } from '@hcengineering/ui'
import { chunterId, DirectMessage, Message } from '@hcengineering/chunter'
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import { getDmName, getSpaceLink } from '../utils'
import { NavLink } from '@hcengineering/ui'
import chunter from '../plugin'
import { getDmName } from '../utils'
export let value: Message
const client = getClient()
@ -13,23 +13,13 @@
$: query.query(chunter.class.DirectMessage, { _id: value.space }, (result) => {
dm = result[0]
})
$: link = getSpaceLink(value.space)
function goto () {
const loc = getCurrentLocation()
loc.path[1] = 'chunter'
loc.path[2] = value.space
loc.query = undefined
navigate(loc)
}
</script>
{#if dm}
{#await getDmName(client, dm) then name}
<a class="flex-presenter" href={link} on:click={() => goto()}>
<NavLink app={chunterId} space={value.space}>
<span class="label">{name}</span>
</a>
</NavLink>
<div><MessageViewer message={value.content} /></div>
{/await}
{/if}

View File

@ -13,16 +13,16 @@
// limitations under the License.
//
import core, { Data, Doc, DocumentQuery, Ref, RelatedDocument, Space } from '@hcengineering/core'
import chunter, {
ChunterSpace,
Backlink,
Channel,
ChunterMessage,
Message,
ThreadMessage,
ChunterSpace,
DirectMessage,
Backlink
Message,
ThreadMessage
} from '@hcengineering/chunter'
import core, { Data, Doc, DocumentQuery, Ref, RelatedDocument, Space } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { IntlString, Resources, translate } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
@ -32,29 +32,29 @@ import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte'
import TxBacklinkReference from './components/activity/TxBacklinkReference.svelte'
import TxCommentCreate from './components/activity/TxCommentCreate.svelte'
import TxMessageCreate from './components/activity/TxMessageCreate.svelte'
import ChannelPresenter from './components/ChannelPresenter.svelte'
import DmPresenter from './components/DmPresenter.svelte'
import MessagePresenter from './components/MessagePresenter.svelte'
import ChannelView from './components/ChannelView.svelte'
import ChannelHeader from './components/ChannelHeader.svelte'
import DmHeader from './components/DmHeader.svelte'
import ChannelPresenter from './components/ChannelPresenter.svelte'
import ChannelView from './components/ChannelView.svelte'
import ChunterBrowser from './components/ChunterBrowser.svelte'
import CommentInput from './components/CommentInput.svelte'
import CommentPopup from './components/CommentPopup.svelte'
import CommentPresenter from './components/CommentPresenter.svelte'
import CommentsPresenter from './components/CommentsPresenter.svelte'
import ConvertDmToPrivateChannelModal from './components/ConvertDmToPrivateChannel.svelte'
import CreateChannel from './components/CreateChannel.svelte'
import CreateDirectMessage from './components/CreateDirectMessage.svelte'
import DmHeader from './components/DmHeader.svelte'
import DmPresenter from './components/DmPresenter.svelte'
import EditChannel from './components/EditChannel.svelte'
import ChunterBrowser from './components/ChunterBrowser.svelte'
import ThreadView from './components/ThreadView.svelte'
import Threads from './components/Threads.svelte'
import MessagePresenter from './components/MessagePresenter.svelte'
import SavedMessages from './components/SavedMessages.svelte'
import ConvertDmToPrivateChannelModal from './components/ConvertDmToPrivateChannel.svelte'
import Threads from './components/Threads.svelte'
import ThreadView from './components/ThreadView.svelte'
import { getDmName } from './utils'
import { writable } from 'svelte/store'
import { updateBacklinksList } from './backlinks'
import { DisplayTx } from '../../activity/lib'
import { updateBacklinksList } from './backlinks'
import { getDmName, getFragment, getLink, resolveLocation } from './utils'
export { default as Header } from './components/Header.svelte'
export { classIcon } from './utils'
@ -242,7 +242,9 @@ export default async (): Promise<Resources> => ({
},
function: {
GetDmName: getDmName,
ChunterBrowserVisible: chunterBrowserVisible
ChunterBrowserVisible: chunterBrowserVisible,
GetFragment: getFragment,
GetLink: getLink
},
activity: {
TxCommentCreate,
@ -263,5 +265,8 @@ export default async (): Promise<Resources> => ({
},
backreference: {
Update: update
},
resolver: {
Location: resolveLocation
}
})

View File

@ -1,8 +1,22 @@
import { chunterId, ChunterMessage } from '@hcengineering/chunter'
import { chunterId, ChunterMessage, Comment, ThreadMessage } from '@hcengineering/chunter'
import contact, { EmployeeAccount, getName } from '@hcengineering/contact'
import { Class, Client, getCurrentAccount, Obj, Ref, Space, Timestamp } from '@hcengineering/core'
import {
Class,
Client,
Doc,
getCurrentAccount,
matchQuery,
Obj,
Ref,
SortingOrder,
Space,
Timestamp
} from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import { getCurrentLocation, locationToUrl, navigate } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import { getCurrentLocation, navigate, Location, getPanelURI } from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import view from '@hcengineering/view'
import { get, writable } from 'svelte/store'
import chunter from './plugin'
@ -54,17 +68,6 @@ export async function getDmName (client: Client, dm: Space): Promise<string> {
return name
}
export function getSpaceLink (id: Ref<Space>): string {
const loc = getCurrentLocation()
loc.path[2] = chunterId
loc.path[3] = id
loc.path.length = 4
loc.fragment = undefined
return locationToUrl(loc)
}
export function getDay (time: Timestamp): Timestamp {
const date: Date = new Date(time)
date.setHours(0, 0, 0, 0)
@ -120,3 +123,108 @@ export function scrollAndHighLight (): void {
isMessageHighlighted.set(false)
}, 2000)
}
export async function getLink (doc: Doc): Promise<string> {
const fragment = await getFragment(doc)
const location = getCurrentLocation()
return await Promise.resolve(
`${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${chunterId}#${fragment}`
)
}
export async function getFragment (doc: Doc): Promise<string> {
const client = getClient()
const hierarchy = client.getHierarchy()
let clazz = hierarchy.getClass(doc._class)
let label = clazz.shortLabel
while (label === undefined && clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
label = clazz.shortLabel
}
label = label ?? doc._class
let length = 5
let id = doc._id.slice(-length)
const contacts = await client.findAll(chunter.class.Comment, {}, { projection: { _id: 1 } })
let res = matchQuery(contacts, { _id: { $like: `@${id}` } }, chunter.class.Comment, hierarchy)
while (res.length > 1) {
length++
id = doc._id.slice(-length)
res = matchQuery(contacts, { _id: { $like: `@${id}` } }, chunter.class.Comment, hierarchy)
}
return `${chunterId}|${label}-${id}`
}
export async function resolveLocation (loc: Location): Promise<Location | undefined> {
const split = loc.fragment?.split('|') ?? []
if (split[0] !== chunterId) {
return undefined
}
const shortLink = split[1]
// shortlink
if (isShortId(shortLink)) {
return await generateLocation(loc, shortLink)
}
return undefined
}
async function generateLocation (loc: Location, shortLink: string): Promise<Location | undefined> {
const tokens = shortLink.split('-')
if (tokens.length < 2) {
return undefined
}
const classLabel = tokens[0]
const lastId = tokens[1]
const client = getClient()
const hierarchy = client.getHierarchy()
const classes = [chunter.class.Message, chunter.class.ThreadMessage, chunter.class.Comment]
let _class: Ref<Class<Doc>> | undefined
for (const clazz of classes) {
if (hierarchy.getClass(clazz).shortLabel === classLabel) {
_class = clazz
break
}
}
if (_class === undefined) {
console.error(`Could not find class ${classLabel}.`)
return undefined
}
const doc = await client.findOne(_class, { _id: { $like: `%${lastId}` } }, { sort: { _id: SortingOrder.Descending } })
if (doc === undefined) {
console.error(`Could not find message ${lastId}.`)
return undefined
}
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
if (hierarchy.isDerived(doc._class, chunter.class.Message)) {
return {
path: [appComponent, workspace, chunterId, doc.space],
fragment: doc._id
}
}
if (hierarchy.isDerived(doc._class, chunter.class.Comment)) {
const comment = doc as Comment
const targetClass = hierarchy.getClass(comment.attachedToClass)
const panelComponent = hierarchy.as(targetClass, view.mixin.ObjectPanel)
const component = panelComponent.component ?? view.component.EditDoc
return {
path: [appComponent, workspace, chunterId],
fragment: getPanelURI(component, comment.attachedTo, comment.attachedToClass, 'content')
}
}
if (hierarchy.isDerived(doc._class, chunter.class.ThreadMessage)) {
const msg = doc as ThreadMessage
return {
path: [appComponent, workspace, chunterId, doc.space, msg.attachedTo],
fragment: doc._id
}
}
}
function isShortId (shortLink: string): boolean {
return /^\w+-\w+$/.test(shortLink)
}

View File

@ -156,6 +156,9 @@ export default plugin(chunterId, {
UnarchiveConfirm: '' as IntlString,
ConvertToPrivate: '' as IntlString
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<Location | undefined>>
},
app: {
Chunter: '' as Ref<Doc>
},

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import type { IntlString, Asset } from '@hcengineering/platform'
import type { Asset, IntlString } from '@hcengineering/platform'
import { copyTextToClipboard } from '@hcengineering/presentation'
import { CircleButton, closeTooltip, Label } from '@hcengineering/ui'
import IconCopy from './icons/Copy.svelte'

View File

@ -2,11 +2,10 @@
import { Employee } from '@hcengineering/contact'
import { WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Label, showPopup } from '@hcengineering/ui'
import { Label } from '@hcengineering/ui'
import { PersonLabelTooltip } from '..'
import PersonPresenter from '../components/PersonPresenter.svelte'
import contact from '../plugin'
import EmployeePreviewPopup from './EmployeePreviewPopup.svelte'
export let value: WithLookup<Employee> | null | undefined
export let tooltipLabels: PersonLabelTooltip | undefined = undefined
@ -20,45 +19,28 @@
export let disableClick = false
export let defaultName: IntlString | undefined = undefined
export let element: HTMLElement | undefined = undefined
const onEdit = (evt: MouseEvent) => {
if (disableClick) {
return
}
evt?.preventDefault()
evt?.stopPropagation()
if (value) {
showPopup(
EmployeePreviewPopup,
{
employeeId: value._id
},
element
)
}
}
$: handlePersonEdit = onEmployeeEdit ?? onEdit
</script>
<PersonPresenter
bind:element
{value}
{tooltipLabels}
onEdit={isInteractive ? handlePersonEdit : () => {}}
{shouldShowAvatar}
{shouldShowName}
{avatarSize}
{shouldShowPlaceholder}
{isInteractive}
{inline}
{defaultName}
/>
{#if value?.active === false}
<div class="status ml-1">
(<Label label={contact.string.Inactive} />)
</div>
{/if}
<span class="flex-presenter">
<PersonPresenter
bind:element
{value}
{tooltipLabels}
onEdit={onEmployeeEdit}
{shouldShowAvatar}
{shouldShowName}
{avatarSize}
{shouldShowPlaceholder}
isInteractive={isInteractive && !disableClick}
{inline}
{defaultName}
/>
{#if value?.active === false && shouldShowName}
<span class="status ml-1">
(<Label label={contact.string.Inactive} />)
</span>
{/if}
</span>
<style lang="scss">
.status {

View File

@ -1,9 +1,9 @@
<script lang="ts">
import { Employee, EmployeeAccount, getName, Status } from '@hcengineering/contact'
import { getCurrentAccount, Hierarchy, Ref, WithLookup } from '@hcengineering/core'
import { getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import { Avatar, createQuery, getClient } from '@hcengineering/presentation'
import { Button, getPanelURI, Label, resizeObserver, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Button, Label, resizeObserver, showPopup } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import contact from '../plugin'
import EmployeeSetStatusPopup from './EmployeeSetStatusPopup.svelte'
@ -63,9 +63,9 @@
<Avatar size="x-large" avatar={employee.avatar} />
</div>
<div class="pb-2">{getName(employee)}</div>
<a href={`#${getPanelURI(view.component.EditDoc, employee._id, Hierarchy.mixinOrClass(employee), 'content')}`}
><Label label={contact.string.ViewFullProfile} /></a
>
<DocNavLink object={employee}>
<Label label={contact.string.ViewFullProfile} />
</DocNavLink>
{#if status}
<div class="pb-2">
<Label label={contact.string.Status} />

View File

@ -15,10 +15,8 @@
-->
<script lang="ts">
import contact, { Member } from '@hcengineering/contact'
import { Hierarchy } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { getPanelURI } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { DocNavLink } from '@hcengineering/view-resources'
import { ContactPresenter } from '..'
export let value: Member
@ -26,10 +24,10 @@
const contactRef = getClient().findOne(contact.class.Contact, { _id: value.contact })
</script>
<a href={`#${getPanelURI(view.component.EditDoc, value._id, Hierarchy.mixinOrClass(value), 'content')}`}>
<DocNavLink object={value}>
{#await contactRef then ct}
{#if ct}
<ContactPresenter isInteractive={false} value={ct} />
{/if}
{/await}
</a>
</DocNavLink>

View File

@ -15,8 +15,7 @@
-->
<script lang="ts">
import { Organization } from '@hcengineering/contact'
import { getPanelURI } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { DocNavLink } from '@hcengineering/view-resources'
import Company from './icons/Company.svelte'
export let value: Organization
@ -24,12 +23,10 @@
</script>
{#if value}
<a
class="flex-presenter"
class:inline-presenter={inline}
href="#{getPanelURI(view.component.EditDoc, value._id, value._class, 'content')}"
>
<div class="icon circle"><Company size={'small'} /></div>
<span class="label">{value.name}</span>
</a>
<DocNavLink {inline} object={value}>
<div class="flex-presenter">
<div class="icon circle"><Company size={'small'} /></div>
<span class="label">{value.name}</span>
</div>
</DocNavLink>
{/if}

View File

@ -15,10 +15,9 @@
<script lang="ts">
import attachment from '@hcengineering/attachment'
import contact, { Channel, Contact, getName } from '@hcengineering/contact'
import { Hierarchy } from '@hcengineering/core'
import { Avatar, createQuery } from '@hcengineering/presentation'
import { Component, Label, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Component, Label } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import ChannelsEditor from './ChannelsEditor.svelte'
export let object: Contact
@ -44,15 +43,11 @@
</div>
{#if object}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="name lines-limit-2"
class:over-underline={!disabled}
on:click={() => {
if (!disabled) showPanel(view.component.EditDoc, object._id, Hierarchy.mixinOrClass(object), 'content')
}}
>
{getName(object)}
</div>
<DocNavLink {object} disableClick={disabled}>
<div class="name lines-limit-2">
{getName(object)}
</div>
</DocNavLink>
<div class="description overflow-label">{object.city ?? ''}</div>
<div class="footer flex flex-reverse flex-grow">
<div class="flex-center flex-wrap">

View File

@ -14,11 +14,10 @@
-->
<script lang="ts">
import { Employee, getName, Person } from '@hcengineering/contact'
import { Hierarchy } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Avatar } from '@hcengineering/presentation'
import { getPanelURI, Label, LabelAndProps, tooltip } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Label, LabelAndProps, tooltip } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
export let value: Person | Employee | undefined | null
export let inline: boolean = false
@ -32,88 +31,54 @@
export let showTooltip: LabelAndProps | undefined = undefined
export let enlargedText = false
export let element: HTMLElement | undefined = undefined
$: el = getElement(value, onEdit, shouldShowPlaceholder, isInteractive)
const getElement = (
person: Person | undefined | null,
onEdit: Function | undefined,
shouldShowEmpty: boolean,
isInteractive: boolean
) => {
if (!person && !shouldShowEmpty) {
return undefined
}
if (!isInteractive) {
return 'div'
}
if (person && !onEdit) {
return 'a'
}
return 'div'
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<svelte:element
this={el}
bind:this={element}
use:tooltip={showTooltip}
class="contentPresenter"
class:inline-presenter={inline}
class:mContentPresenterNotInteractive={!isInteractive}
class:text-base={enlargedText}
on:click={onEdit}
href={!isInteractive || onEdit || !value
? undefined
: `#${getPanelURI(view.component.EditDoc, value._id, Hierarchy.mixinOrClass(value), 'content')}`}
>
{#if shouldShowAvatar}
<div
class="eContentPresenterIcon"
class:mr-2={shouldShowName && !enlargedText}
class:mr-3={shouldShowName && enlargedText}
>
<Avatar size={avatarSize} avatar={value?.avatar} />
</div>
{/if}
{#if value && shouldShowName}
<span class="eContentPresenterLabel">{getName(value)}</span>
{/if}
{#if !value && shouldShowName && defaultName}
<div class="eContentPresenterLabel">
<Label label={defaultName} />
</div>
{/if}
</svelte:element>
{#if value}
<DocNavLink object={value} onClick={onEdit} disableClick={!isInteractive}>
<span use:tooltip={showTooltip} class="contentPresenter" class:text-base={enlargedText}>
{#if shouldShowAvatar}
<span
class="eContentPresenterIcon"
class:mr-2={shouldShowName && !enlargedText}
class:mr-3={shouldShowName && enlargedText}
>
<Avatar size={avatarSize} avatar={value.avatar} />
</span>
{/if}
{#if shouldShowName}
<span class="eContentPresenterLabel">{getName(value)}</span>
{/if}
</span>
</DocNavLink>
{:else if shouldShowPlaceholder}
<span use:tooltip={showTooltip} class="contentPresenter" class:text-base={enlargedText}>
{#if shouldShowAvatar}
<span
class="eContentPresenterIcon"
class:mr-2={shouldShowName && !enlargedText}
class:mr-3={shouldShowName && enlargedText}
>
<Avatar size={avatarSize} />
</span>
{/if}
{#if shouldShowName && defaultName}
<span class="eContentPresenterLabel">
<Label label={defaultName} />
</span>
{/if}
</span>
{/if}
<style lang="scss">
.contentPresenter {
display: flex;
align-items: center;
flex-wrap: nowrap;
cursor: pointer;
&.inline-presenter {
display: inline-flex;
align-items: baseline;
}
&.mContentPresenterNotInteractive {
cursor: default;
&:hover {
.eContentPresenterIcon {
color: var(--accent-color);
}
.eContentPresenterLabel {
text-decoration: none;
color: var(--accent-color);
}
}
}
.eContentPresenterIcon {
color: var(--dark-color);
}

View File

@ -62,9 +62,11 @@ import {
employeeSort,
filterChannelInResult,
filterChannelNinResult,
getContactLink,
getContactName,
getCurrentEmployeeEmail,
getCurrentEmployeeName
getCurrentEmployeeName,
resolveLocation
} from './utils'
export {
@ -210,6 +212,10 @@ export default async (): Promise<Resources> => ({
FilterChannelNinResult: filterChannelNinResult,
GetCurrentEmployeeName: getCurrentEmployeeName,
GetCurrentEmployeeEmail: getCurrentEmployeeEmail,
GetContactName: getContactName
GetContactName: getContactName,
GetContactLink: getContactLink
},
resolver: {
Location: resolveLocation
}
})

View File

@ -15,7 +15,8 @@
//
import contact, { contactId } from '@hcengineering/contact'
import { IntlString, mergeIds } from '@hcengineering/platform'
import { Doc } from '@hcengineering/core'
import { IntlString, mergeIds, Resource } from '@hcengineering/platform'
import { FilterFunction, SortFunc } from '@hcengineering/view'
export default mergeIds(contactId, contact, {
@ -65,6 +66,7 @@ export default mergeIds(contactId, contact, {
DisplayName: '' as IntlString
},
function: {
GetContactLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
EmployeeSort: '' as SortFunc,
FilterChannelInResult: '' as FilterFunction,
FilterChannelNinResult: '' as FilterFunction

View File

@ -14,10 +14,28 @@
// limitations under the License.
//
import { ChannelProvider, Contact, Employee, EmployeeAccount, formatName, getName } from '@hcengineering/contact'
import { Doc, getCurrentAccount, ObjQueryType, Ref, Timestamp, toIdMap } from '@hcengineering/core'
import {
ChannelProvider,
Contact,
contactId,
Employee,
EmployeeAccount,
formatName,
getName
} from '@hcengineering/contact'
import {
Doc,
getCurrentAccount,
matchQuery,
ObjQueryType,
Ref,
SortingOrder,
Timestamp,
toIdMap
} from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { TemplateDataProvider } from '@hcengineering/templates'
import { getPanelURI, Location } from '@hcengineering/ui'
import view, { Filter } from '@hcengineering/view'
import { FilterQuery } from '@hcengineering/view-resources'
import contact from './plugin'
@ -133,3 +151,76 @@ export async function getContactName (provider: TemplateDataProvider): Promise<s
return value.name
}
}
export async function getContactLink (doc: Doc): Promise<string> {
const client = getClient()
const hierarchy = client.getHierarchy()
let clazz = hierarchy.getClass(doc._class)
let label = clazz.shortLabel
while (label === undefined && clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
label = clazz.shortLabel
}
label = label ?? 'CONT'
let length = 5
let id = doc._id.slice(-length)
const contacts = await client.findAll(clazz._id, {}, { projection: { _id: 1 } })
let res = matchQuery(contacts, { _id: { $like: `@${id}` } }, clazz._id, hierarchy)
while (res.length > 1) {
length++
id = doc._id.slice(-length)
res = matchQuery(contacts, { _id: { $like: `@${id}` } }, clazz._id, hierarchy)
}
return `${contactId}|${label}-${id}`
}
function isShortId (shortLink: string): boolean {
return /^\w+-\w+$/.test(shortLink)
}
export async function resolveLocation (loc: Location): Promise<Location | undefined> {
const split = loc.fragment?.split('|') ?? []
if (split[0] !== contactId) {
return undefined
}
const shortLink = split[1]
// shortlink
if (isShortId(shortLink)) {
return await generateLocation(loc, shortLink)
}
return undefined
}
async function generateLocation (loc: Location, shortLink: string): Promise<Location | undefined> {
const tokens = shortLink.split('-')
if (tokens.length < 2) {
return undefined
}
const classLabel = tokens[0]
const lastId = tokens[1]
const client = getClient()
const hierarchy = client.getHierarchy()
const classes = hierarchy.getDescendants(contact.class.Contact)
let _class = contact.class.Contact
for (const clazz of classes) {
if (hierarchy.getClass(clazz).shortLabel === classLabel) {
_class = clazz
break
}
}
const doc = await client.findOne(_class, { _id: { $like: `%${lastId}` } }, { sort: { _id: SortingOrder.Descending } })
if (doc === undefined) {
console.error(`Could not find contact ${lastId}.`)
return undefined
}
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
return {
path: [appComponent, workspace, contactId],
fragment: getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')
}
}

View File

@ -314,6 +314,9 @@ const contactPlugin = plugin(contactId, {
FilterChannelIn: '' as Ref<FilterMode>,
FilterChannelNin: '' as Ref<FilterMode>
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<Location | undefined>>
},
templateFieldCategory: {
CurrentEmployee: '' as Ref<TemplateFieldCategory>,
Contact: '' as Ref<TemplateFieldCategory>

View File

@ -15,21 +15,16 @@
-->
<script lang="ts">
import { Category } from '@hcengineering/inventory'
import { getPanelURI } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { DocNavLink } from '@hcengineering/view-resources'
export let value: Category
export let inline: boolean = false
</script>
{#if value}
<a
class="flex-presenter"
class:inline-presenter={inline}
href="#{getPanelURI(view.component.EditDoc, value._id, value._class, 'content')}"
>
<div class="overflow-label sm-tool-icon">
<DocNavLink object={value} {inline}>
<div class="flex-presenter overflow-label sm-tool-icon">
{value.name}
</div>
</a>
</DocNavLink>
{/if}

View File

@ -15,8 +15,8 @@
-->
<script lang="ts">
import { Product } from '@hcengineering/inventory'
import { getPanelURI, Icon } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Icon } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import inventory from '../plugin'
export let value: Product
@ -24,12 +24,10 @@
</script>
{#if value}
<a
class="flex-presenter"
class:inline-presenter={inline}
href="#{getPanelURI(view.component.EditDoc, value._id, value._class, 'content')}"
>
<div class="icon"><Icon icon={inventory.icon.Products} size={'small'} /></div>
<span class="label">{value.name}</span>
</a>
<DocNavLink {inline} object={value}>
<div class="flex-presenter" class:inline-presenter={inline}>
<div class="icon"><Icon icon={inventory.icon.Products} size={'small'} /></div>
<span class="label">{value.name}</span>
</div>
</DocNavLink>
{/if}

View File

@ -15,8 +15,8 @@
-->
<script lang="ts">
import type { Lead } from '@hcengineering/lead'
import { getPanelURI, Icon } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Icon } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import lead from '../plugin'
export let value: Lead
@ -24,14 +24,12 @@
</script>
{#if value}
<a
class="flex-presenter"
class:inline-presenter={inline}
href="#{getPanelURI(view.component.EditDoc, value._id, value._class, 'content')}"
>
<div class="icon">
<Icon icon={lead.icon.Lead} size={'small'} />
<DocNavLink object={value} {inline}>
<div class="flex-presenter" class:inline-presenter={inline}>
<div class="icon">
<Icon icon={lead.icon.Lead} size={'small'} />
</div>
<span class="label nowrap">LEAD-{value.number}</span>
</div>
<span class="label nowrap">LEAD-{value.number}</span>
</a>
</DocNavLink>
{/if}

View File

@ -12,6 +12,7 @@
"VacancyPlaceholder": "Software Engineer",
"CreateAnApplication": "New Application",
"NoApplicationsForTalent": "There are no applications for this talent.",
"NoApplicationsForVacany": "There are no applications for this vacancy.",
"CreateApplication": "New Application",
"ApplicationCreateLabel": "Application",
"SelectVacancy": "Select vacancy",
@ -46,7 +47,6 @@
"Yes": "Yes",
"No": "No",
"NA": "N/A",
"ApplicationShort": "APP",
"ApplicationsShort": "Apps",
"Due": "Due date",
"Location": "Location",
@ -66,8 +66,6 @@
"Opinions": "Opinions",
"Opinion": "Opinion",
"OpinionValue": "Rating",
"OpinionShortLabel": "OPE",
"ReviewShortLabel": "RVE",
"StartDate": "Start date",
"DueDate": "Due date",
"Verdict": "Verdict",

View File

@ -12,6 +12,7 @@
"VacancyPlaceholder": "Разработчик",
"CreateAnApplication": "Новый Кандидат",
"NoApplicationsForTalent": "Нет кандидатов для данного таланта.",
"NoApplicationsForVacany": "Нет кандидатов для данной вакансии.",
"CreateApplication": "Новый Кандидат",
"SelectVacancy": "Выбрать вакансию",
"Talent": "Талант",
@ -46,7 +47,6 @@
"Yes": "Да",
"No": "Нет",
"NA": "Н/Д",
"ApplicationShort": "APP",
"ApplicationsShort": "Apps",
"Due": "Срок",
"Location": "Местоположение",
@ -67,8 +67,6 @@
"Opinions": "Мнения",
"Opinion": "Мнение",
"OpinionValue": "Значение",
"OpinionShortLabel": "OPE",
"ReviewShortLabel": "RVE",
"StartDate": "Дата начала",
"DueDate": "Дата окончания",
"Verdict": "Вердикт",

View File

@ -18,7 +18,7 @@
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import type { Applicant } from '@hcengineering/recruit'
import { Icon, Label } from '@hcengineering/ui'
import { Icon } from '@hcengineering/ui'
import recruit from '../plugin'
export let value: Applicant
@ -36,7 +36,7 @@
<div class="flex item">
<Icon icon={recruit.icon.Application} size={'medium'} />
<div class="ml-2">
{#if shortLabel}<Label label={shortLabel} />-{/if}{value.number}
{#if shortLabel}{shortLabel}-{/if}{value.number}
</div>
{#if person}
<div class="ml-1">{getName(person)}</div>

View File

@ -17,9 +17,8 @@
import { getClient } from '@hcengineering/presentation'
import type { Applicant } from '@hcengineering/recruit'
import recruit from '@hcengineering/recruit'
import { Icon, Label } from '@hcengineering/ui'
import { getPanelURI } from '@hcengineering/ui/src/panelup'
import view from '@hcengineering/view'
import { Icon } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
export let value: Applicant
export let inline: boolean = false
@ -30,16 +29,14 @@
</script>
{#if value && shortLabel}
<a
class="flex-presenter"
class:inline-presenter={inline}
href="#{disableClick ? null : getPanelURI(view.component.EditDoc, value._id, value._class, 'content')}"
>
<div class="icon">
<Icon icon={recruit.icon.Application} size={'small'} />
<DocNavLink object={value} {inline} {disableClick}>
<div class="flex-presenter" class:inline-presenter={inline}>
<div class="icon">
<Icon icon={recruit.icon.Application} size={'small'} />
</div>
<span class="label nowrap">
{#if shortLabel}{shortLabel}-{/if}{value.number}
</span>
</div>
<span class="label nowrap"
>{#if shortLabel}<Label label={shortLabel} />-{/if}{value.number}</span
>
</a>
</DocNavLink>
{/if}

View File

@ -14,17 +14,15 @@
-->
<script lang="ts">
import type { Doc, Ref } from '@hcengineering/core'
import { Button, IconAdd, Label, showPopup, Icon, Scroller, resizeObserver } from '@hcengineering/ui'
import { Button, Icon, IconAdd, Label, resizeObserver, Scroller, showPopup } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import { Table } from '@hcengineering/view-resources'
import recruit from '../plugin'
import CreateApplication from './CreateApplication.svelte'
import FileDuo from './icons/FileDuo.svelte'
import IconApplication from './icons/Application.svelte'
import FileDuo from './icons/FileDuo.svelte'
export let objectId: Ref<Doc>
// export let space: Ref<Space>
// export let _class: Ref<Class<Doc>>
export let applications: number

View File

@ -17,10 +17,9 @@
import chunter from '@hcengineering/chunter'
import contact, { Channel, getName, Person } from '@hcengineering/contact'
import { ChannelsEditor } from '@hcengineering/contact-resources'
import { Hierarchy } from '@hcengineering/core'
import { Avatar, createQuery, getClient } from '@hcengineering/presentation'
import { Component, Label, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Component, Label } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import recruit from '../plugin'
export let candidate: Person | undefined
@ -48,18 +47,11 @@
<div class="label uppercase"><Label label={recruit.string.Talent} /></div>
<Avatar avatar={candidate?.avatar} size={'large'} />
{#if candidate}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="name lines-limit-2"
class:over-underline={!disabled}
on:click={() => {
if (!disabled && candidate) {
showPanel(view.component.EditDoc, candidate._id, Hierarchy.mixinOrClass(candidate), 'content')
}
}}
>
{getName(candidate)}
</div>
<DocNavLink object={candidate} disableClick={disabled}>
<div class="name lines-limit-2">
{getName(candidate)}
</div>
</DocNavLink>
{#if client.getHierarchy().hasMixin(candidate, recruit.mixin.Candidate)}
{@const cand = client.getHierarchy().as(candidate, recruit.mixin.Candidate)}
<div class="description lines-limit-2">{cand.title ?? ''}</div>

View File

@ -40,7 +40,7 @@
UserBox
} from '@hcengineering/presentation'
import type { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import task, { calcRank, SpaceWithStates, State } from '@hcengineering/task'
import task, { calcRank, State } from '@hcengineering/task'
import ui, {
Button,
ColorPopup,
@ -59,7 +59,7 @@
import VacancyCard from './VacancyCard.svelte'
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
export let space: Ref<SpaceWithStates>
export let space: Ref<Vacancy>
export let candidate: Ref<Candidate>
export let assignee: Ref<Employee>
export let comment: Markup = ''

View File

@ -124,6 +124,13 @@
throw Error(`Failed to find target kanban template: ${templateId}`)
}
const sequence = await client.findOne(task.class.Sequence, { attachedTo: recruit.class.Vacancy })
if (sequence === undefined) {
throw new Error('sequence object not found')
}
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const id = await client.createDoc(
recruit.class.Vacancy,
core.space.Space,
@ -134,6 +141,7 @@
fullDescription,
private: false,
archived: false,
number: (incResult as any).object.sequence,
company,
createdBy: getCurrentAccount()._id,
members: [getCurrentAccount()._id]

View File

@ -21,11 +21,12 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import { Vacancy } from '@hcengineering/recruit'
import { FullDescriptionBox } from '@hcengineering/text-editor'
import tracker from '@hcengineering/tracker'
import { Button, Component, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui'
import { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
import tracker from '@hcengineering/tracker'
import VacancyApplications from './VacancyApplications.svelte'
export let _id: Ref<Vacancy>
@ -139,6 +140,7 @@
space={object.space}
attachments={object.attachments ?? 0}
/>
<VacancyApplications objectId={object._id} />
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
</Grid>
</Panel>

View File

@ -0,0 +1,82 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 type { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { recruitId, Vacancy } from '@hcengineering/recruit'
import { Button, Icon, IconAdd, Label, NavLink, resizeObserver, Scroller, showPopup } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import { Table } from '@hcengineering/view-resources'
import recruit from '../plugin'
import CreateApplication from './CreateApplication.svelte'
import IconApplication from './icons/Application.svelte'
import FileDuo from './icons/FileDuo.svelte'
export let objectId: Ref<Vacancy>
let applications: number
const query = createQuery()
$: query.query(recruit.class.Applicant, { space: objectId }, (res) => {
applications = res.length
})
const createApp = (ev: MouseEvent): void => {
showPopup(CreateApplication, { space: objectId, preserveVacancy: true }, ev.target as HTMLElement)
}
const config: (BuildModelKey | string)[] = [
'',
'$lookup.space.name',
'$lookup.space.$lookup.company',
'state',
'doneState'
]
let wSection: number
</script>
<div class="antiSection max-h-125" use:resizeObserver={(element) => (wSection = element.clientWidth)}>
<div class="antiSection-header">
<div class="antiSection-header__icon">
<Icon icon={IconApplication} size={'small'} />
</div>
<span class="antiSection-header__title">
<NavLink app={recruitId} space={objectId}>
<Label label={recruit.string.Applications} />
</NavLink>
</span>
<Button id="appls.add" icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={createApp} />
</div>
{#if applications > 0}
<Scroller horizontal={wSection < 640}>
<Table
_class={recruit.class.Applicant}
{config}
query={{ space: objectId }}
loadingProps={{ length: applications }}
/>
</Scroller>
{:else}
<div class="antiSection-empty solid flex-col-center mt-3">
<div class="caption-color">
<FileDuo size={'large'} />
</div>
<span class="dark-color">
<Label label={recruit.string.NoApplicationsForVacany} />
</span>
<span class="over-underline content-accent-color" on:click={createApp}>
<Label label={recruit.string.CreateAnApplication} />
</span>
</div>
{/if}
</div>

View File

@ -19,16 +19,8 @@
import { ChannelsEditor } from '@hcengineering/contact-resources'
import { Ref, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { Vacancy } from '@hcengineering/recruit'
import {
closePanel,
closePopup,
closeTooltip,
Component,
getCurrentLocation,
Label,
navigate
} from '@hcengineering/ui'
import { recruitId, Vacancy } from '@hcengineering/recruit'
import { Component, Label, NavLink } from '@hcengineering/ui'
import recruit from '../plugin'
import VacancyIcon from './icons/Vacancy.svelte'
@ -80,35 +72,22 @@
{/if}
{#if vacancy}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="name lines-limit-2"
class:over-underline={!disabled}
on:click={() => {
if (!disabled && vacancy) {
closeTooltip()
closePopup()
closePanel()
const loc = getCurrentLocation()
loc.path[2] = 'recruit'
loc.path[3] = vacancy._id
loc.path.length = 4
navigate(loc)
}
}}
>
<div class="text-md">
{#if inline}
<div class="flex-row-center">
<VacancyIcon size={'small'} />
<span class="ml-1">
{vacancy.name}
</span>
</div>
{:else}
{vacancy.name}
{/if}
<NavLink {disabled} space={vacancy._id} app={recruitId}>
<div class="name lines-limit-2">
<div class="text-md">
{#if inline}
<div class="flex-row-center">
<VacancyIcon size={'small'} />
<span class="ml-1">
{vacancy.name}
</span>
</div>
{:else}
{vacancy.name}
{/if}
</div>
</div>
</div>
</NavLink>
{#if company}
<span class="label">{company.name}</span>
{/if}

View File

@ -14,8 +14,8 @@
-->
<script lang="ts">
import { Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { Vacancy } from '@hcengineering/recruit'
import { Icon, tooltip } from '@hcengineering/ui'
import { recruitId, Vacancy } from '@hcengineering/recruit'
import { getCurrentLocation, Icon, navigate, tooltip } from '@hcengineering/ui'
import recruit from '../plugin'
import VacancyApplicationsPopup from './VacancyApplicationsPopup.svelte'
@ -23,10 +23,17 @@
export let applications: Map<Ref<Vacancy>, { count: number; modifiedOn: number }> | undefined
export let resultQuery: DocumentQuery<Doc>
$: count = applications?.get(value._id)?.count ?? 0
function click () {
const loc = getCurrentLocation()
loc.fragment = undefined
loc.query = undefined
loc.path[2] = recruitId
loc.path[3] = value._id
navigate(loc)
}
</script>
{#if value && count > 0}
{#if value}
<div
class="sm-tool-icon"
use:tooltip={{
@ -34,6 +41,7 @@
component: VacancyApplicationsPopup,
props: { value: value._id, resultQuery }
}}
on:click={click}
>
<div class="icon">
<Icon icon={recruit.icon.Application} size={'small'} />

View File

@ -16,44 +16,14 @@
<script lang="ts">
import { getEmbeddedLabel } from '@hcengineering/platform'
import type { Vacancy } from '@hcengineering/recruit'
import {
ActionIcon,
getCurrentLocation,
Icon,
IconEdit,
Location,
locationToUrl,
navigate,
showPanel,
tooltip
} from '@hcengineering/ui'
import { Vacancy } from '@hcengineering/recruit'
import { Icon, tooltip } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import recruit from '../plugin'
export let value: Vacancy
export let inline: boolean = false
export let disableClick = false
function editVacancy (): void {
if (disableClick) {
return
}
showPanel(recruit.component.EditVacancy, value._id, value._class, 'content')
}
function getLoc (): Location {
const loc = getCurrentLocation()
loc.path[2] = 'recruit'
loc.path[3] = value._id
loc.path.length = 4
loc.fragment = ''
return loc
}
function getLink (): string {
const loc = getLoc()
return document.location.origin + locationToUrl(loc)
}
</script>
{#if value}
@ -61,23 +31,8 @@
<div class="icon">
<Icon icon={recruit.icon.Vacancy} size={'small'} />
</div>
<a
on:click|preventDefault={(e) => {
if (inline) {
editVacancy()
return
}
navigate(getLoc())
e.preventDefault()
}}
href={getLink()}
>
<DocNavLink {disableClick} object={value} {inline} component={recruit.component.EditVacancy}>
<span class="label">{value.name}</span>
</a>
{#if !inline}
<div class="action">
<ActionIcon label={recruit.string.Edit} size={'small'} icon={IconEdit} action={editVacancy} />
</div>
{/if}
</DocNavLink>
</div>
{/if}

View File

@ -14,7 +14,6 @@
// limitations under the License.
-->
<script lang="ts">
import { translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import type { Opinion } from '@hcengineering/recruit'
import recruit from '@hcengineering/recruit'
@ -29,15 +28,9 @@
}
const client = getClient()
let shortLabel = ''
const label = client.getHierarchy().getClass(value._class).shortLabel
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel ?? ''
if (label !== undefined) {
translate(label, {}).then((r) => {
shortLabel = r
})
}
let element: HTMLElement
</script>

View File

@ -14,37 +14,27 @@
// limitations under the License.
-->
<script lang="ts">
import { translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import type { Review } from '@hcengineering/recruit'
import recruit from '@hcengineering/recruit'
import { getPanelURI, Icon } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Icon } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
export let value: Review
export let inline: boolean = false
const client = getClient()
let shortLabel = ''
const label = client.getHierarchy().getClass(value?._class)?.shortLabel
if (label !== undefined) {
translate(label, {}).then((r) => {
shortLabel = r
})
}
</script>
{#if value && shortLabel}
<a
class="flex-presenter"
class:inline-presenter={inline}
href="#{getPanelURI(view.component.EditDoc, value._id, value._class, 'content')}"
>
<div class="icon">
<Icon icon={recruit.icon.Review} size={'small'} />
{#if value && label}
<DocNavLink object={value} {inline}>
<div class="flex-presenter" class:inline-presenter={inline}>
<div class="icon">
<Icon icon={recruit.icon.Review} size={'small'} />
</div>
<span class="label nowrap">{label}-{value.number}</span>
</div>
<span class="label nowrap">{shortLabel}-{value.number}</span>
</a>
</DocNavLink>
{/if}

View File

@ -23,7 +23,7 @@ import {
RelatedDocument,
toIdMap
} from '@hcengineering/core'
import { IntlString, OK, Resources, Severity, Status, translate } from '@hcengineering/platform'
import { OK, Resources, Severity, Status } from '@hcengineering/platform'
import { ObjectSearchResult } from '@hcengineering/presentation'
import { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import task from '@hcengineering/task'
@ -41,7 +41,9 @@ 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 MatchVacancy from './components/MatchVacancy.svelte'
import NewCandidateHeader from './components/NewCandidateHeader.svelte'
import Organizations from './components/Organizations.svelte'
import CreateOpinion from './components/review/CreateOpinion.svelte'
import CreateReview from './components/review/CreateReview.svelte'
import EditReview from './components/review/EditReview.svelte'
@ -56,14 +58,20 @@ import Vacancies from './components/Vacancies.svelte'
import VacancyCountPresenter from './components/VacancyCountPresenter.svelte'
import VacancyItem from './components/VacancyItem.svelte'
import VacancyItemPresenter from './components/VacancyItemPresenter.svelte'
import VacancyList from './components/VacancyList.svelte'
import VacancyModifiedPresenter from './components/VacancyModifiedPresenter.svelte'
import VacancyPresenter from './components/VacancyPresenter.svelte'
import recruit from './plugin'
import { objectIdProvider, objectLinkProvider, getApplicationTitle } from './utils'
import VacancyList from './components/VacancyList.svelte'
import VacancyTemplateEditor from './components/VacancyTemplateEditor.svelte'
import MatchVacancy from './components/MatchVacancy.svelte'
import Organizations from './components/Organizations.svelte'
import recruit from './plugin'
import {
getAppTitle,
getRevTitle,
getSequenceId,
getSequenceLink,
getVacTitle,
objectLinkProvider,
resolveLocation
} from './utils'
import { MoveApplicant } from './actionImpl'
@ -95,7 +103,7 @@ export async function queryApplication (
): Promise<ObjectSearchResult[]> {
const _class = recruit.class.Applicant
const cl = client.getHierarchy().getClass(_class)
const shortLabel = (await translate(cl.shortLabel ?? ('' as IntlString), {})).toUpperCase()
const shortLabel = cl.shortLabel?.toUpperCase() ?? ''
// Check number pattern
@ -320,12 +328,17 @@ export default async (): Promise<Resources> => ({
await queryVacancy(client, query, filter)
},
function: {
ApplicationTitleProvider: getApplicationTitle,
AppTitleProvider: getAppTitle,
VacTitleProvider: getVacTitle,
RevTitleProvider: getRevTitle,
IdProvider: getSequenceId,
HasActiveApplicant: hasActiveApplicant,
HasNoActiveApplicant: hasNoActiveApplicant,
NoneApplications: noneApplicant,
GetApplicationId: objectIdProvider,
GetApplicationLink: objectLinkProvider,
GetRecruitLink: objectLinkProvider
GetObjectLink: objectLinkProvider,
GetObjectLinkFragment: getSequenceLink
},
resolver: {
Location: resolveLocation
}
})

View File

@ -36,6 +36,7 @@ export default mergeIds(recruitId, recruit, {
CandidatesDescription: '' as IntlString,
CreateAnApplication: '' as IntlString,
NoApplicationsForTalent: '' as IntlString,
NoApplicationsForVacany: '' as IntlString,
FirstName: '' as IntlString,
LastName: '' as IntlString,
Talents: '' as IntlString,
@ -96,8 +97,6 @@ export default mergeIds(recruitId, recruit, {
OpinionValuePlaceholder: '' as IntlString,
OpinionSave: '' as IntlString,
Opinions: '' as IntlString,
OpinionShortLabel: '' as IntlString,
ReviewShortLabel: '' as IntlString,
StartDate: '' as IntlString,
DueDate: '' as IntlString,
TalentReviews: '' as IntlString,
@ -143,7 +142,10 @@ export default mergeIds(recruitId, recruit, {
CreateCandidate: '' as AnyComponent
},
function: {
ApplicationTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
IdProvider: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
AppTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
VacTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
RevTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
HasActiveApplicant: '' as FilterFunction,
HasNoActiveApplicant: '' as FilterFunction,
NoneApplications: '' as FilterFunction

View File

@ -1,31 +1,114 @@
import core, { Doc, Ref, TxOperations } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import { Class, Client, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Applicant, Candidate } from '@hcengineering/recruit'
import { getPanelURI } from '@hcengineering/ui'
import { Applicant, recruitId, Review, Vacancy } from '@hcengineering/recruit'
import { getCurrentLocation, getPanelURI, Location } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
import recruit from './plugin'
export async function getApplicationTitle (client: TxOperations, ref: Ref<Doc>): Promise<string> {
const object = await client.findOne(
recruit.class.Applicant,
{ _id: ref as Ref<Applicant> },
{ lookup: { _class: core.class.Class } }
)
if (object?.$lookup?._class?.shortLabel === undefined) {
throw new Error(`Application shortLabel not found, _id: ${ref}`)
}
const label = await translate(object.$lookup._class.shortLabel, {})
return `${label}-${object.number}`
}
type RecruitDocument = Vacancy | Applicant | Review
export async function objectIdProvider (doc: Applicant | Candidate): Promise<string> {
const client = getClient()
return await getApplicationTitle(client, doc._id)
}
export async function objectLinkProvider (doc: Applicant | Candidate): Promise<string> {
export async function objectLinkProvider (doc: RecruitDocument): Promise<string> {
const location = getCurrentLocation()
return await Promise.resolve(
`${window.location.href}#${getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')}`
`${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}#${await getSequenceLink(
doc
)}`
)
}
function isShortId (shortLink: string): boolean {
return /^\w+-\d+$/.test(shortLink)
}
export async function resolveLocation (loc: Location): Promise<Location | undefined> {
const split = loc.fragment?.split('|') ?? []
if (split[0] !== recruitId) {
return undefined
}
const shortLink = split[1]
// shortlink
if (isShortId(shortLink)) {
return await generateLocation(loc, shortLink)
}
return undefined
}
async function generateLocation (loc: Location, shortLink: string): Promise<Location | undefined> {
const tokens = shortLink.split('-')
if (tokens.length < 2) {
return undefined
}
const classLabel = tokens[0]
const number = Number(tokens[1])
const client = getClient()
const hierarchy = client.getHierarchy()
const classes = [recruit.class.Applicant, recruit.class.Vacancy, recruit.class.Review]
let _class: Ref<Class<Doc>> | undefined
for (const clazz of classes) {
if (hierarchy.getClass(clazz).shortLabel === classLabel) {
_class = clazz
break
}
}
if (_class === undefined) {
console.error(`Not found class with short label ${classLabel}`)
return undefined
}
const doc = await client.findOne(_class, { number })
if (doc === undefined) {
console.error(`Could not find ${_class} with number ${number}.`)
return undefined
}
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
const targetClass = hierarchy.getClass(_class)
const panelComponent = hierarchy.as(targetClass, view.mixin.ObjectPanel)
const component = panelComponent.component ?? view.component.EditDoc
return {
path: [appComponent, workspace, recruitId],
fragment: getPanelURI(component, doc._id, doc._class, 'content')
}
}
export async function getSequenceLink (doc: RecruitDocument): Promise<string> {
return `${recruitId}|${await getSequenceId(doc)}`
}
async function getTitle<T extends RecruitDocument> (
client: Client,
ref: Ref<T>,
_class: Ref<Class<T>>
): Promise<string> {
const object = await client.findOne<RecruitDocument>(_class, { _id: ref as Ref<any> })
return object != null ? await getSequenceId(object) : ''
}
export async function getVacTitle (client: Client, ref: Ref<Vacancy>): Promise<string> {
const object = await client.findOne(recruit.class.Vacancy, { _id: ref })
return object != null ? object.name : ''
}
export async function getAppTitle (client: Client, ref: Ref<Applicant>): Promise<string> {
return await getTitle(client, ref, recruit.class.Applicant)
}
export async function getRevTitle (client: Client, ref: Ref<Review>): Promise<string> {
return await getTitle(client, ref, recruit.class.Review)
}
export async function getSequenceId (doc: RecruitDocument): Promise<string> {
const client = getClient()
const hierarchy = client.getHierarchy()
let clazz = hierarchy.getClass(doc._class)
let label = clazz.shortLabel
while (label === undefined && clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
label = clazz.shortLabel
}
return label !== undefined ? `${label}-${doc.number}` : doc.number.toString()
}

View File

@ -16,7 +16,7 @@
import { Event } from '@hcengineering/calendar'
import type { Channel, Organization, Person } from '@hcengineering/contact'
import type { AttachedData, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
import type { Asset, Plugin } from '@hcengineering/platform'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@hcengineering/task'
import { AnyComponent } from '@hcengineering/ui'
@ -32,6 +32,7 @@ export interface Vacancy extends SpaceWithStates {
location?: string
company?: Ref<Organization>
comments?: number
number: number
}
/**
@ -172,6 +173,9 @@ const recruit = plugin(recruitId, {
Skills: '' as Asset,
Issue: '' as Asset
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<Location | undefined>>
},
space: {
VacancyTemplates: '' as Ref<KanbanTemplateSpace>,
Reviews: '' as Ref<Space>

View File

@ -13,45 +13,33 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { EmployeeAccount } from '@hcengineering/contact'
import { WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Request, RequestStatus } from '@hcengineering/request'
import { getPanelURI, Icon, IconCheck, IconClose, IconInfo } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Icon, IconCheck, IconClose, IconInfo } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import TxView from './TxView.svelte'
export let value: Request
export let inline: boolean = false
const client = getClient()
let accounts: WithLookup<EmployeeAccount>[] = []
$: client.findAll(contact.class.EmployeeAccount, { _id: { $in: value.requested } }).then((res) => {
accounts = res
})
$: dte = new Date(value.tx.modifiedOn)
</script>
<div class="flex">
<a
class="flex-presenter mr-1"
class:inline-presenter={inline}
href="#{getPanelURI(view.component.EditDoc, value._id, value._class, 'content')}"
>
<div class="flex flex-row-center">
<div class="mr-2">
{#if value.status === RequestStatus.Completed || value.status === RequestStatus.Rejected}
<Icon icon={value.status === RequestStatus.Completed ? IconCheck : IconClose} size={'small'} />
{:else}
<Icon icon={IconInfo} size={'small'} />
{/if}
<DocNavLink {inline} object={value}>
<div class="flex-presenter mr-1" class:inline-presenter={inline}>
<div class="flex flex-row-center">
<div class="mr-2">
{#if value.status === RequestStatus.Completed || value.status === RequestStatus.Rejected}
<Icon icon={value.status === RequestStatus.Completed ? IconCheck : IconClose} size={'small'} />
{:else}
<Icon icon={IconInfo} size={'small'} />
{/if}
</div>
<span class="label nowrap">
{dte.getMonth() + 1}/{dte.getDay() + 1}-{(dte.getHours() * 60 + dte.getMinutes()).toString(7)}
</span>
</div>
<span class="label nowrap">
{dte.getMonth() + 1}/{dte.getDay() + 1}-{(dte.getHours() * 60 + dte.getMinutes()).toString(7)}
</span>
</div>
</a>
</DocNavLink>
<TxView tx={value.tx} />
</div>

View File

@ -15,7 +15,6 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import type { KanbanTemplate } from '@hcengineering/task'
import { Label } from '@hcengineering/ui'
export let value: KanbanTemplate
@ -25,5 +24,5 @@
<span class="label nowrap">
{#if shortLabel}
<Label label={shortLabel} />-{/if}{value.title}</span
{shortLabel}-{/if}{value.title}</span
>

View File

@ -16,7 +16,7 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import type { Issue, Task } from '@hcengineering/task'
import { Icon, Label } from '@hcengineering/ui'
import { Icon } from '@hcengineering/ui'
import task from '../plugin'
export let value: Task
@ -34,7 +34,7 @@
<div class="flex item">
<Icon icon={task.icon.Task} size={'large'} />
<div class="ml-2">
{#if shortLabel}<Label label={shortLabel} />-{/if}{value.number}
{#if shortLabel}{shortLabel}-{/if}{value.number}
</div>
{#if name}
<div class="ml-1">{name}</div>

View File

@ -16,8 +16,8 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import type { Issue } from '@hcengineering/task'
import { getPanelURI, Icon, Label } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Icon } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import task from '../plugin'
export let value: Issue
@ -28,16 +28,14 @@
</script>
{#if value && shortLabel}
<a
class="flex-presenter"
class:inline-presenter={inline}
href="#{getPanelURI(view.component.EditDoc, value._id, value._class, 'content')}"
>
<div class="icon">
<Icon icon={task.icon.Task} size={'small'} />
<DocNavLink object={value} {inline}>
<div class="flex-presenter" class:inline-presenter={inline}>
<div class="icon">
<Icon icon={task.icon.Task} size={'small'} />
</div>
<span class="label nowrap"
>{#if shortLabel}{shortLabel}-{/if}{value.number}</span
>
</div>
<span class="label nowrap"
>{#if shortLabel}<Label label={shortLabel} />-{/if}{value.number}</span
>
</a>
</DocNavLink>
{/if}

View File

@ -16,7 +16,8 @@
import { Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import type { Issue, Team } from '@hcengineering/tracker'
import { Icon, showPanel, tooltip } from '@hcengineering/ui'
import { Icon, tooltip } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import tracker from '../../plugin'
export let value: WithLookup<Issue>
@ -29,18 +30,6 @@
// Extra properties
export let teams: Map<Ref<Team>, Team> | undefined = undefined
function handleIssueEditorOpened () {
if (disableClick) {
return
}
if (onClick) {
onClick()
}
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
}
const spaceQuery = createQuery()
let currentTeam: Team | undefined = value?.$lookup?.space
@ -58,23 +47,18 @@
</script>
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class="issuePresenterRoot"
class:noPointer={disableClick}
class:noUnderline
class:inline
on:click={handleIssueEditorOpened}
>
{#if withIcon}
<div class="icon" use:tooltip={{ label: tracker.string.Issue }}>
<Icon icon={tracker.icon.Issues} size={'small'} />
</div>
{/if}
<span class="select-text" title={value?.title}>
{title}
<DocNavLink object={value} {onClick} {disableClick} {noUnderline} {inline} component={tracker.component.EditIssue}>
<span class="issuePresenterRoot" class:inline>
{#if withIcon}
<div class="icon" use:tooltip={{ label: tracker.string.Issue }}>
<Icon icon={tracker.icon.Issues} size={'small'} />
</div>
{/if}
<span class="select-text" title={value?.title}>
{title}
</span>
</span>
</span>
</DocNavLink>
{/if}
<style lang="scss">
@ -106,28 +90,5 @@
color: var(--accent-color);
}
}
&.noPointer {
cursor: default;
}
&.noUnderline {
color: var(--caption-color);
font-weight: 500;
}
&:not(.noUnderline) {
&:hover {
color: var(--caption-color);
text-decoration: underline;
.icon {
color: var(--caption-color);
}
}
}
&:active {
color: var(--accent-color);
}
}
</style>

View File

@ -13,59 +13,44 @@
// limitations under the License.
-->
<script lang="ts">
import { WithLookup } from '@hcengineering/core'
import type { Issue } from '@hcengineering/tracker'
import { showPanel } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
export let value: Issue
export let value: WithLookup<Issue>
export let shouldUseMargin: boolean = false
export let showParent = true
export let onClick: (() => void) | undefined = undefined
function handleIssueEditorOpened () {
if (onClick) {
onClick()
}
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
}
</script>
{#if value}
<span class="titlePresenter-container" class:with-margin={shouldUseMargin} title={value.title}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<DocNavLink object={value} {onClick} component={tracker.component.EditIssue}>
<span
class="name overflow-label cursor-pointer select-text"
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
on:click={handleIssueEditorOpened}>{value.title}</span
class="name overflow-label select-text"
class:with-margin={shouldUseMargin}
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}>{value.title}</span
>
{#if showParent}
<ParentNamesPresenter {value} />
{/if}
</span>
</DocNavLink>
{/if}
<style lang="scss">
.titlePresenter-container {
display: flex;
flex-grow: 0;
min-width: 1.5rem;
// flex-shrink: 10;
.name {
flex-shrink: 0;
&:hover {
text-decoration: underline;
}
&:active {
color: var(--accent-color);
}
.name {
flex-shrink: 0;
&:hover {
text-decoration: underline;
}
&.with-margin {
margin-left: 0.5rem;
&:active {
color: var(--accent-color);
}
}
.with-margin {
margin-left: 0.5rem;
}
</style>

View File

@ -28,7 +28,6 @@ import { getClient, MessageBox, ObjectSearchResult } from '@hcengineering/presen
import { Issue, Scrum, ScrumRecord, Sprint, Team } from '@hcengineering/tracker'
import { showPopup } from '@hcengineering/ui'
import CreateIssue from './components/CreateIssue.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
import Inbox from './components/inbox/Inbox.svelte'
import Active from './components/issues/Active.svelte'
import AssigneePresenter from './components/issues/AssigneePresenter.svelte'
@ -42,9 +41,11 @@ import Issues from './components/issues/Issues.svelte'
import IssuesView from './components/issues/IssuesView.svelte'
import KanbanView from './components/issues/KanbanView.svelte'
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
import PriorityRefPresenter from './components/issues/PriorityRefPresenter.svelte'
import PriorityEditor from './components/issues/PriorityEditor.svelte'
import PriorityPresenter from './components/issues/PriorityPresenter.svelte'
import PriorityRefPresenter from './components/issues/PriorityRefPresenter.svelte'
import RelatedIssueSelector from './components/issues/related/RelatedIssueSelector.svelte'
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
import StatusEditor from './components/issues/StatusEditor.svelte'
import StatusPresenter from './components/issues/StatusPresenter.svelte'
import TitlePresenter from './components/issues/TitlePresenter.svelte'
@ -66,18 +67,18 @@ import TeamProjects from './components/projects/TeamProjects.svelte'
import RelationsPopup from './components/RelationsPopup.svelte'
import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
import Views from './components/views/Views.svelte'
import Statuses from './components/workflow/Statuses.svelte'
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
import RelatedIssueSelector from './components/issues/related/RelatedIssueSelector.svelte'
import SprintProjectEditor from './components/sprints/SprintProjectEditor.svelte'
import SprintDatePresenter from './components/sprints/SprintDatePresenter.svelte'
import SprintLeadPresenter from './components/sprints/SprintLeadPresenter.svelte'
import SprintProjectEditor from './components/sprints/SprintProjectEditor.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
import Views from './components/views/Views.svelte'
import Statuses from './components/workflow/Statuses.svelte'
import {
getIssueId,
getIssueTitle,
issueIdProvider,
issueLinkFragmentProvider,
issueLinkProvider,
issueTitleProvider,
resolveLocation
@ -91,8 +92,8 @@ import SprintSelector from './components/sprints/SprintSelector.svelte'
import SprintStatusPresenter from './components/sprints/SprintStatusPresenter.svelte'
import SprintTitlePresenter from './components/sprints/SprintTitlePresenter.svelte'
import Scrums from './components/scrums/Scrums.svelte'
import ScrumRecordPanel from './components/scrums/ScrumRecordPanel.svelte'
import Scrums from './components/scrums/Scrums.svelte'
import SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte'
import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte'
@ -107,31 +108,31 @@ import ProjectSelector from './components/ProjectSelector.svelte'
import IssueTemplatePresenter from './components/templates/IssueTemplatePresenter.svelte'
import IssueTemplates from './components/templates/IssueTemplates.svelte'
import { deleteObject } from '@hcengineering/view-resources/src/utils'
import MoveAndDeleteSprintPopup from './components/sprints/MoveAndDeleteSprintPopup.svelte'
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
import MoveAndDeleteSprintPopup from './components/sprints/MoveAndDeleteSprintPopup.svelte'
import {
moveIssuesToAnotherSprint,
issueStatusSort,
issuePrioritySort,
sprintSort,
subIssueQuery,
getAllStatuses,
getAllPriority,
getAllProjects,
getAllSprints,
removeTeam
getAllStatuses,
issuePrioritySort,
issueStatusSort,
moveIssuesToAnotherSprint,
removeTeam,
sprintSort,
subIssueQuery
} from './utils'
import { deleteObject } from '@hcengineering/view-resources/src/utils'
import { EmployeeAccount } from '@hcengineering/contact'
import StatusRefPresenter from './components/issues/StatusRefPresenter.svelte'
import TimeSpendReportPopup from './components/issues/timereport/TimeSpendReportPopup.svelte'
import DeleteProjectPresenter from './components/projects/DeleteProjectPresenter.svelte'
import IssueStatistics from './components/sprints/IssueStatistics.svelte'
import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte'
import CreateTeam from './components/teams/CreateTeam.svelte'
import TeamPresenter from './components/teams/TeamPresenter.svelte'
import IssueStatistics from './components/sprints/IssueStatistics.svelte'
import StatusRefPresenter from './components/issues/StatusRefPresenter.svelte'
import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte'
import { EmployeeAccount } from '@hcengineering/contact'
import DeleteProjectPresenter from './components/projects/DeleteProjectPresenter.svelte'
import TimeSpendReportPopup from './components/issues/timereport/TimeSpendReportPopup.svelte'
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
@ -425,6 +426,7 @@ export default async (): Promise<Resources> => ({
IssueTitleProvider: getIssueTitle,
GetIssueId: issueIdProvider,
GetIssueLink: issueLinkProvider,
GetIssueLinkFragment: issueLinkFragmentProvider,
GetIssueTitle: issueTitleProvider,
IssueStatusSort: issueStatusSort,
IssuePrioritySort: issuePrioritySort,

View File

@ -1,7 +1,7 @@
import { Doc, DocumentUpdate, Ref, RelatedDocument, TxOperations } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Issue, Project, Sprint, Team, trackerId } from '@hcengineering/tracker'
import { getCurrentLocation, getPanelURI, Location } from '@hcengineering/ui'
import { getCurrentLocation, getPanelURI, Location, navigate } from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import { writable } from 'svelte/store'
import tracker from './plugin'
@ -36,6 +36,11 @@ export async function issueIdProvider (doc: Doc): Promise<string> {
return await getIssueTitle(client, doc._id)
}
export async function issueLinkFragmentProvider (doc: Doc): Promise<string> {
const client = getClient()
return await getIssueTitle(client, doc._id).then((p) => `${trackerId}|${p}`)
}
export async function issueTitleProvider (doc: Issue): Promise<string> {
return await Promise.resolve(doc.title)
}
@ -47,7 +52,7 @@ export async function issueLinkProvider (doc: Doc): Promise<string> {
export function generateIssueShortLink (issueId: string): string {
const location = getCurrentLocation()
return `${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${trackerId}/${issueId}`
return `${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${trackerId}#${trackerId}|${issueId}`
}
export async function generateIssueLocation (loc: Location, issueId: string): Promise<Location | undefined> {
@ -78,14 +83,25 @@ export async function generateIssueLocation (loc: Location, issueId: string): Pr
}
}
function checkOld (loc: Location): void {
const short = loc.path[3]
if (isIssueId(short)) {
loc.fragment = short
loc.path.length = 3
navigate(loc)
}
}
export async function resolveLocation (loc: Location): Promise<Location | undefined> {
const app = loc.path.length > 2 ? loc.path[2] : undefined
if (app !== trackerId) {
const split = loc.fragment?.split('|') ?? []
const app = loc.path[2]
if (app !== trackerId && split[0] !== trackerId) {
return undefined
}
const shortLink = loc.path.length > 3 ? loc.path[3] : undefined
if (shortLink === undefined || shortLink === null) {
const shortLink = split[1] ?? loc.fragment
if (shortLink === undefined || shortLink === null || shortLink.trim() === '') {
checkOld(loc)
return undefined
}

View File

@ -378,6 +378,7 @@ export default mergeIds(trackerId, tracker, {
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
GetIssueId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueTitle: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
IssueStatusSort: '' as SortFunc,
IssuePrioritySort: '' as SortFunc,

View File

@ -0,0 +1,42 @@
<!--
// 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 bgetObjectLinkFragmentr 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 { Doc, Hierarchy } from '@hcengineering/core'
import { getClient, NavLink } from '@hcengineering/presentation'
import { AnyComponent, getPanelURI } from '@hcengineering/ui'
import view from '../plugin'
import { getObjectLinkFragment } from '../utils'
export let object: Doc
export let disableClick = false
export let onClick: ((event: MouseEvent) => void) | undefined = undefined
export let noUnderline = false
export let inline = false
export let component: AnyComponent = view.component.EditDoc
export let props: Record<string, any> = {}
const client = getClient()
const hierarchy = client.getHierarchy()
let href: string | undefined = getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content')
async function getHref (object: Doc): Promise<void> {
href = `#${await getObjectLinkFragment(hierarchy, object, props, component)}`
}
$: getHref(object)
</script>
<NavLink {disableClick} {onClick} {noUnderline} {inline} {href}><slot /></NavLink>

View File

@ -24,10 +24,11 @@
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let actions: Action[] = []
export let mode: ViewContextType | undefined = undefined
let resActions = actions
const client = getClient()
let loaded = 0
let loaded = false
const order: Record<ActionGroup, number> = {
create: 1,
@ -39,23 +40,24 @@
}
getActions(client, object, baseMenuClass, mode).then((result) => {
actions = result
.sort((a, b) => order[a.context.group ?? 'other'] - order[b.context.group ?? 'other'])
.map((a) => ({
label: a.label,
icon: a.icon as Asset,
inline: a.inline,
group: a.context.group ?? 'other',
action: async (_: any, evt: Event) => {
invokeAction(object, evt, a.action, a.actionProps)
},
component: a.actionPopup,
props: { ...a.actionProps, value: object }
}))
loaded = 1
const newActions: Action[] = result.map((a) => ({
label: a.label,
icon: a.icon as Asset,
inline: a.inline,
group: a.context.group ?? 'other',
action: async (_: any, evt: Event) => {
invokeAction(object, evt, a.action, a.actionProps)
},
component: a.actionPopup,
props: { ...a.actionProps, value: object }
}))
resActions = [...newActions, ...actions].sort(
(a, b) => (order as any)[a.group ?? 'other'] - (order as any)[b.group ?? 'other']
)
loaded = true
})
</script>
{#if loaded}
<Menu {actions} on:close on:changeContent />
<Menu actions={resActions} on:close on:changeContent />
{/if}

View File

@ -1,31 +1,38 @@
<script lang="ts">
import { Doc } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import ui, {
Button,
IconNavPrev,
closeTooltip,
getCurrentLocation,
IconDownOutline,
IconNavPrev,
IconUpOutline,
panelstore,
showPanel,
closeTooltip
navigate,
panelstore
} from '@hcengineering/ui'
import { tick } from 'svelte'
import { select } from '../actionImpl'
import { focusStore } from '../selection'
import { getObjectLinkFragment } from '../utils'
export let element: Doc
const client = getClient()
async function next (evt: Event, pn: boolean): Promise<void> {
select(evt, pn ? 1 : -1, element, 'vertical')
await tick()
if ($focusStore.focus !== undefined && $panelstore.panel !== undefined) {
showPanel(
$panelstore.panel.component,
$focusStore.focus._id,
$focusStore.focus._class,
$panelstore.panel?.element ?? 'content',
$panelstore.panel.rightSection
)
const doc = await client.findOne($focusStore.focus._class, { _id: $focusStore.focus._id })
if (doc !== undefined) {
const link = await getObjectLinkFragment(client.getHierarchy(), doc, {}, $panelstore.panel.component)
const location = getCurrentLocation()
if (location.fragment !== link) {
location.fragment = link
navigate(location)
}
}
}
}

View File

@ -67,6 +67,7 @@ import EnumArrayEditor from './components/EnumArrayEditor.svelte'
import EnumPresenter from './components/EnumPresenter.svelte'
import TreeNode from './components/navigator/TreeNode.svelte'
import TreeItem from './components/navigator/TreeItem.svelte'
import DocNavLink from './components/DocNavLink.svelte'
import {
afterResult,
@ -143,7 +144,8 @@ export {
MarkupEditor,
TreeNode,
TreeItem,
StringEditor
StringEditor,
DocNavLink
}
export default async (): Promise<Resources> => ({

View File

@ -38,6 +38,7 @@ import {
AnyComponent,
ErrorPresenter,
getCurrentLocation,
getPanelURI,
getPlatformColorForText,
Location,
locationToUrl
@ -674,3 +675,25 @@ export function getAdditionalHeader (client: TxOperations, _class: Ref<Class<Doc
}
return presenterMixin.presenters
}
export async function getObjectLinkFragment (
hierarchy: Hierarchy,
object: Doc,
props: Record<string, any> = {},
component: AnyComponent = view.component.EditDoc
): Promise<string> {
let clazz = hierarchy.getClass(object._class)
let provider = hierarchy.as(clazz, view.mixin.LinkProvider)
while (provider.encode === undefined && clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
provider = hierarchy.as(clazz, view.mixin.LinkProvider)
}
if (provider?.encode !== undefined) {
const f = await getResource(provider.encode)
const res = await f(object, props)
if (res !== undefined) {
return res
}
}
return getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content')
}

View File

@ -543,6 +543,20 @@ export type ViewOptionModel = ToggleViewOption | DropdownViewOption
*/
export type OrderOption = [string, SortingOrder]
/**
* @public
*/
export interface LinkProvider extends Class<Doc> {
encode: Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
}
/**
* @public
*/
export interface ObjectPanel extends Class<Doc> {
component: AnyComponent
}
/**
* @public
*/
@ -580,7 +594,9 @@ const view = plugin(viewId, {
PreviewPresenter: '' as Ref<Mixin<PreviewPresenter>>,
ListHeaderExtra: '' as Ref<Mixin<ListHeaderExtra>>,
SortFuncs: '' as Ref<Mixin<ClassSortFuncs>>,
AllValuesFunc: '' as Ref<Mixin<AllValuesFunc>>
AllValuesFunc: '' as Ref<Mixin<AllValuesFunc>>,
ObjectPanel: '' as Ref<Mixin<ObjectPanel>>,
LinkProvider: '' as Ref<Mixin<LinkProvider>>
},
class: {
ViewletPreference: '' as Ref<Class<ViewletPreference>>,

View File

@ -13,24 +13,42 @@
// limitations under the License.
-->
<script lang="ts">
import type { Ref, Class, Doc } from '@hcengineering/core'
import type { Asset } from '@hcengineering/platform'
import { Icon } from '@hcengineering/ui'
import { FilterButton } from '@hcengineering/view-resources'
import type { Class, Doc, Ref, Space } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { AnyComponent, Icon } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { DocNavLink, FilterButton } from '@hcengineering/view-resources'
import plugin from '../plugin'
import { classIcon } from '../utils'
export let icon: Asset | undefined
export let label: string
export let description: string | undefined
export let space: Space
export let _class: Ref<Class<Doc>> | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
$: description = space.description
function getEditor (_class: Ref<Class<Doc>>): AnyComponent | undefined {
const clazz = hierarchy.getClass(_class)
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor)
if (editorMixin?.editor == null && clazz.extends != null) return getEditor(clazz.extends)
return editorMixin.editor
}
const icon = classIcon(client, space._class)
const editor = getEditor(space._class) ?? plugin.component.SpacePanel
</script>
<div class="ac-header__wrap-description">
<div class="flex-row-center clear-mins">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="ac-header__wrap-title" on:click>
{#if icon}<div class="ac-header__icon"><Icon {icon} size={'small'} /></div>{/if}
<span class="ac-header__title">{label}</span>
</div>
<DocNavLink object={space} component={editor}>
<div class="ac-header__wrap-title">
{#if icon}<div class="ac-header__icon"><Icon {icon} size={'small'} /></div>{/if}
<span class="ac-header__title">{space.name}</span>
</div>
</DocNavLink>
{#if _class}<div class="ml-4"><FilterButton {_class} /></div>{/if}
</div>
{#if description}
@ -47,7 +65,6 @@
<style lang="scss">
.ac-header__wrap-title:hover {
cursor: pointer;
span {
text-decoration: underline;
}

View File

@ -16,22 +16,19 @@
import type { Class, Doc, Ref, Space } from '@hcengineering/core'
import core, { WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import presentation, { createQuery } from '@hcengineering/presentation'
import {
AnyComponent,
Button,
deviceOptionsStore as deviceInfo,
IconAdd,
SearchEdit,
showPanel,
showPopup,
TabList
} from '@hcengineering/ui'
import view, { Viewlet, ViewOptions } from '@hcengineering/view'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import { getActiveViewletId, setActiveViewletId, ViewletSettingButton } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
import { classIcon } from '../utils'
import Header from './Header.svelte'
export let spaceId: Ref<Space> | undefined
@ -43,8 +40,6 @@
export let _class: Ref<Class<Doc>> | undefined = undefined
export let viewOptions: ViewOptions
const client = getClient()
const hierarchy = client.getHierarchy()
const query = createQuery()
let space: Space | undefined
const dispatch = createEventDispatcher()
@ -66,19 +61,6 @@
dispatch('search', '')
}
async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent | undefined> {
const clazz = hierarchy.getClass(_class)
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor)
if (editorMixin?.editor == null && clazz.extends != null) return getEditor(clazz.extends)
return editorMixin.editor
}
async function onSpaceEdit (): Promise<void> {
if (space === undefined) return
const editor = await getEditor(space._class)
showPanel(editor ?? plugin.component.SpacePanel, space._id, space._class, 'content')
}
function updateViewlets (viewlets: WithLookup<Viewlet>[]) {
const _id = getActiveViewletId()
const index = viewlets.findIndex((p) => p._id === (viewlet?._id ?? _id))
@ -99,13 +81,7 @@
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
{#if space}
<div class:ac-header-full={!twoRows} class:flex-stretch={twoRows}>
<Header
icon={classIcon(client, space._class)}
label={space.name}
description={space.description}
{_class}
on:click={onSpaceEdit}
/>
<Header {space} {_class} />
<SearchEdit
bind:value={search}
on:change={() => {

View File

@ -16,7 +16,6 @@
import calendar from '@hcengineering/calendar'
import contact, { Employee, EmployeeAccount } from '@hcengineering/contact'
import core, { Class, Client, Doc, getCurrentAccount, Ref, setCurrentAccount, Space } from '@hcengineering/core'
import { getWorkspaces, Workspace } from '@hcengineering/login-resources'
import notification, { NotificationStatus } from '@hcengineering/notification'
import { BrowserNotificatator, NotificationClientImpl } from '@hcengineering/notification-resources'
import { getMetadata, getResource, IntlString } from '@hcengineering/platform'
@ -25,6 +24,7 @@
import {
AnyComponent,
areLocationsEqual,
closePanel,
closePopup,
closeTooltip,
Component,
@ -35,8 +35,10 @@
location,
Location,
navigate,
openPanel,
PanelInstance,
Popup,
PopupAlignment,
PopupPosAlignment,
resizeObserver,
showPopup,
@ -48,7 +50,6 @@
import { getContext, onDestroy, onMount, tick } from 'svelte'
import { subscribeMobile } from '../mobile'
import workbench from '../plugin'
import { workspacesStore } from '../utils'
import AccountPopup from './AccountPopup.svelte'
import AppItem from './AppItem.svelte'
import Applications from './Applications.svelte'
@ -71,6 +72,7 @@
let currentSpecial: string | undefined
let specialComponent: SpecialNavModel | undefined
let asideId: string | undefined
let currentFragment: string | undefined = ''
let currentApplication: Application | undefined
let navigatorModel: NavigatorModel | undefined
@ -78,19 +80,13 @@
let createItemDialog: AnyComponent | undefined
let createItemLabel: IntlString | undefined
let apps: Application[] = []
migrateViewOpttions()
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
const query = createQuery()
$: query.query(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } }, (result) => {
apps = result
})
getWorkspaces().then((ws: Workspace[]) => {
$workspacesStore = ws
})
let apps: Application[] | Promise<Application[]> = client
.findAll(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } })
.then((res) => (apps = res))
let panelInstance: PanelInstance
@ -197,10 +193,39 @@
}
}
async function resolveShortLink (loc: Location): Promise<Location | undefined> {
let locationResolver = currentApplication?.locationResolver
if (loc.fragment !== undefined && loc.fragment.trim().length > 0) {
const split = loc.fragment.split('|')
if (apps instanceof Promise) {
apps = await apps
}
const app = apps.find((p) => p.alias === split[0])
if (app?.locationResolver) {
locationResolver = app?.locationResolver
}
}
if (locationResolver) {
const resolver = await getResource(locationResolver)
return await resolver?.(loc)
}
}
async function syncLoc (loc: Location): Promise<void> {
const app = loc.path.length > 2 ? loc.path[2] : undefined
const space = loc.path.length > 3 ? (loc.path[3] as Ref<Space>) : undefined
const special = loc.path.length > 4 ? loc.path[4] : undefined
let app = loc.path[2]
let space = loc.path[3] as Ref<Space>
let special = loc.path[4]
let fragment = loc.fragment
// resolve short links
const resolvedLocation = await resolveShortLink(loc)
if (resolvedLocation && !areLocationsEqual(loc, resolvedLocation)) {
loc.path[2] = app = resolvedLocation.path[2] ?? app
loc.path[3] = space = (resolvedLocation.path[3] as Ref<Space>) ?? space
loc.path[4] = special = resolvedLocation.path[4] ?? special
loc.fragment = fragment = resolvedLocation.fragment ?? fragment
navigate(resolvedLocation, false)
}
if (currentAppAlias !== app) {
clear(1)
@ -209,17 +234,6 @@
navigatorModel = currentApplication?.navigatorModel
}
// resolve short links
if (currentApplication?.locationResolver) {
const resolver = await getResource(currentApplication.locationResolver)
const resolvedLocation = await resolver?.(loc)
if (resolvedLocation && !areLocationsEqual(loc, resolvedLocation)) {
// make sure not to go into infinite loop here
navigate(resolvedLocation)
return
}
}
if (space === undefined) {
const last = localStorage.getItem(`platform_last_loc_${app}`)
if (last !== null) {
@ -252,6 +266,26 @@
if (app !== undefined) {
localStorage.setItem(`platform_last_loc_${app}`, JSON.stringify(loc))
}
if (fragment !== currentFragment) {
currentFragment = fragment
if (fragment !== undefined && fragment.trim().length > 0) {
const props = decodeURIComponent(fragment).split('|')
if (props.length >= 3) {
openPanel(
props[0] as AnyComponent,
props[1],
props[2],
(props[3] ?? undefined) as PopupAlignment,
(props[4] ?? undefined) as AnyComponent
)
} else {
closePanel(false)
}
} else {
closePanel()
}
}
}
function clear (level: number): void {
@ -403,6 +437,14 @@
)())
)
}
function getApps (apps: Application[] | Promise<Application[]>): Application[] {
if (apps instanceof Promise) {
return []
} else {
return apps
}
}
</script>
{#if employee?.active === true}
@ -449,7 +491,12 @@
<Settings size={'small'} />
</div>
</div>
<Applications {apps} active={currentApplication?._id} direction={appsDirection} bind:shown={shownMenu} />
<Applications
apps={getApps(apps)}
active={currentApplication?._id}
direction={appsDirection}
bind:shown={shownMenu}
/>
<div class="info-box {appsDirection}" class:vertical-mobile={appsDirection === 'vertical' && appsMini}>
<AppItem
icon={request.icon.Requests}

View File

@ -23,6 +23,7 @@ import core, {
Collection,
concatLink,
Doc,
matchQuery,
Obj,
Ref,
RefTo,
@ -33,7 +34,6 @@ import core, {
import login from '@hcengineering/login'
import { getMetadata } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import view from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
/**
@ -271,13 +271,36 @@ export async function OnEmployeeUpdate (tx: Tx, control: TriggerControl): Promis
return result
}
async function getContactLink (doc: Doc, control: TriggerControl): Promise<string> {
const hierarchy = control.hierarchy
let clazz = hierarchy.getClass(doc._class)
let label = clazz.shortLabel
while (label === undefined && clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
label = clazz.shortLabel
}
label = label ?? 'CONT'
let length = 5
let id = doc._id.slice(-length)
const contacts = await control.findAll(clazz._id, {}, { projection: { _id: 1 } })
let res = matchQuery(contacts, { _id: { $like: `@${id}` } }, clazz._id, hierarchy)
while (res.length > 1) {
length++
id = doc._id.slice(-length)
res = matchQuery(contacts, { _id: { $like: `@${id}` } }, clazz._id, hierarchy)
}
return `${contactId}|${label}-${id}`
}
/**
* @public
*/
export function personHTMLPresenter (doc: Doc, control: TriggerControl): string {
export async function personHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const person = doc as Person
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const path = `${workbenchId}/${control.workspace.name}/${contactId}#${view.component.EditDoc}|${person._id}|${person._class}|content`
const fragment = await getContactLink(doc, control)
const path = `${workbenchId}/${control.workspace.name}/${contactId}#${fragment}`
const link = concatLink(front, path)
return `<a href="${link}">${getName(person)}</a>`
}
@ -293,10 +316,11 @@ export function personTextPresenter (doc: Doc): string {
/**
* @public
*/
export function organizationHTMLPresenter (doc: Doc, control: TriggerControl): string {
export async function organizationHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const organization = doc as Organization
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const path = `${workbenchId}/${control.workspace.name}/${contactId}#${view.component.EditDoc}|${organization._id}|${organization._class}|content`
const fragment = await getContactLink(doc, control)
const path = `${workbenchId}/${control.workspace.name}/${contactId}#${fragment}`
const link = concatLink(front, path)
return `<a href="${link}">${organization.name}</a>`
}

View File

@ -31,16 +31,30 @@ import { getMetadata } from '@hcengineering/platform'
import recruit, { Applicant, recruitId, Vacancy } from '@hcengineering/recruit'
import { TriggerControl } from '@hcengineering/server-core'
import { addAssigneeNotification } from '@hcengineering/server-task-resources'
import view from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
function getSequenceId (doc: Vacancy | Applicant, control: TriggerControl): string {
const hierarchy = control.hierarchy
let clazz = hierarchy.getClass(doc._class)
let label = clazz.shortLabel
while (label === undefined && clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
label = clazz.shortLabel
}
return label !== undefined ? `${label}-${doc.number}` : doc.number.toString()
}
/**
* @public
*/
export async function vacancyHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const vacancy = doc as Vacancy
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const path = `${workbenchId}/${control.workspace.name}/${recruitId}/${vacancy._id}/#${recruit.component.EditVacancy}|${vacancy._id}|${vacancy._class}|content`
const path = `${workbenchId}/${control.workspace.name}/${recruitId}/${vacancy._id}/#${recruitId}|${getSequenceId(
vacancy,
control
)}`
const link = concatLink(front, path)
return `<a href="${link}">${vacancy.name}</a>`
}
@ -59,17 +73,19 @@ export async function vacancyTextPresenter (doc: Doc): Promise<string> {
export async function applicationHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const applicant = doc as Applicant
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const path = `${workbenchId}/${control.workspace.name}/${recruitId}/${applicant.space}/#${view.component.EditDoc}|${applicant._id}|${applicant._class}|content`
const id = getSequenceId(applicant, control)
const path = `${workbenchId}/${control.workspace.name}/${recruitId}/${applicant.space}/#${recruitId}|${id}`
const link = concatLink(front, path)
return `<a href="${link}">APP-${applicant.number}</a>`
return `<a href="${link}">id</a>`
}
/**
* @public
*/
export async function applicationTextPresenter (doc: Doc): Promise<string> {
export async function applicationTextPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const applicant = doc as Applicant
return `APP-${applicant.number}`
const id = getSequenceId(applicant, control)
return id
}
/**

View File

@ -31,12 +31,12 @@ import core, {
WithLookup
} from '@hcengineering/core'
import login from '@hcengineering/login'
import { workbenchId } from '@hcengineering/workbench'
import { getMetadata } from '@hcengineering/platform'
import { Resource } from '@hcengineering/platform/lib/platform'
import { TriggerControl } from '@hcengineering/server-core'
import { addAssigneeNotification } from '@hcengineering/server-task-resources'
import tracker, { Issue, IssueParentInfo, Project, Team, TimeSpendReport, trackerId } from '@hcengineering/tracker'
import { workbenchId } from '@hcengineering/workbench'
async function updateSubIssues (
updateTx: TxUpdateDoc<Issue>,
@ -55,12 +55,9 @@ async function updateSubIssues (
* @public
*/
export async function issueHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const issue = doc as Issue
const team = (await control.findAll(tracker.class.Team, { _id: issue.space })).shift()
const issueName = `${team?.identifier ?? '?'}-${issue.number}`
const issueName = await issueTextPresenter(doc, control)
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const path = `${workbenchId}/${control.workspace.name}/${trackerId}/${issue.space}/#${tracker.component.EditIssue}|${issue._id}|${issue._class}|content`
const path = `${workbenchId}/${control.workspace.name}/${trackerId}/${doc.space}/issues/#${trackerId}|${issueName}`
const link = concatLink(front, path)
return `<a href="${link}">${issueName}</a>`
}
@ -70,7 +67,7 @@ export async function issueHTMLPresenter (doc: Doc, control: TriggerControl): Pr
*/
export async function issueTextPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const issue = doc as Issue
const team = (await control.findAll(tracker.class.Team, { _id: issue.space })).shift()
const team = (await control.findAll(tracker.class.Team, { _id: issue.space }))[0]
const issueName = `${team?.identifier ?? '?'}-${issue.number}`
return issueName

View File

@ -54,6 +54,7 @@ test.describe('recruit tests', () => {
await expect(page.locator(`text=${loc}`).first()).toBeVisible()
const activity = page.locator('[id="activity\\:string\\:Activity"]')
await activity.locator('[id="gmail\\:string\\:Email"]').scrollIntoViewIfNeeded()
await activity.locator('[id="gmail\\:string\\:Email"]').hover()
await expect(page.locator(`text=${email}`).first()).toBeVisible()
})
@ -107,7 +108,7 @@ test.describe('recruit tests', () => {
await page.click('form button:has-text("Create")')
await page.waitForSelector('form.antiCard', { state: 'detached' })
await page.click(`tr > :has-text("${vacancyId}")`)
await page.click(`tr:has-text("${vacancyId}") > td:nth-child(3) >> .sm-tool-icon`)
// Create Applicatio n1
await page.click('button:has-text("Application")')
@ -126,6 +127,7 @@ test.describe('recruit tests', () => {
await page.click('text=Software Engineer')
// await page.click('[name="tooltip-task:string:Kanban"]')
await page.click('.antiSection-header >> text=Applications')
await page.click('.tablist-container div:nth-child(2)')
await expect(page.locator('text=M. Marina').first()).toBeVisible()

View File

@ -31,6 +31,7 @@ test.describe('workbench tests', () => {
await expect(page).toHaveURL(`${PlatformURI}/workbench/sanity-ws/recruit/vacancies`)
// Click text=Software Engineer
await page.click('text=Software Engineer')
await page.click('.antiSection-header >> text=Applications')
await expect(page.locator('text=Software Engineer')).toBeDefined()
await expect(page.locator('text="APP-1"')).toBeDefined()
// await page.click('[name="tooltip-task:string:Kanban"]')