mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
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:
parent
6b534e1a3d
commit
ae86bfdacd
@ -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))
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
72
packages/presentation/src/components/NavLink.svelte
Normal file
72
packages/presentation/src/components/NavLink.svelte
Normal 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>
|
@ -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'
|
||||
|
@ -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 {
|
||||
|
@ -66,7 +66,6 @@
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.ac-header__wrap-description {
|
||||
flex-direction: column;
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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] : [])
|
||||
]
|
||||
},
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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>
|
||||
},
|
||||
|
@ -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'
|
||||
|
@ -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 {
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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",
|
||||
|
@ -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": "Вердикт",
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 = ''
|
||||
|
@ -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]
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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}
|
||||
|
@ -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'} />
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
42
plugins/view-resources/src/components/DocNavLink.svelte
Normal file
42
plugins/view-resources/src/components/DocNavLink.svelte
Normal 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>
|
@ -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}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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> => ({
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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>>,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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={() => {
|
||||
|
@ -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}
|
||||
|
@ -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>`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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"]')
|
||||
|
Loading…
Reference in New Issue
Block a user