diff --git a/dev/generator/src/recruit.ts b/dev/generator/src/recruit.ts index 0d8a06b4cd..2051901a61 100644 --- a/dev/generator/src/recruit.ts +++ b/dev/generator/src/recruit.ts @@ -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)) diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index bc153081f6..660f0939f2 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -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 @@ -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 @@ -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, diff --git a/models/chunter/src/plugin.ts b/models/chunter/src/plugin.ts index 15381e1187..a5c24c7b25 100644 --- a/models/chunter/src/plugin.ts +++ b/models/chunter/src/plugin.ts @@ -38,7 +38,10 @@ export default mergeIds(chunterId, chunter, { MarkUnread: '' as Ref, ArchiveChannel: '' as Ref, UnarchiveChannel: '' as Ref, - ConvertToPrivate: '' as Ref + ConvertToPrivate: '' as Ref, + CopyCommentLink: '' as Ref, + CopyThreadMessageLink: '' as Ref, + CopyMessageLink: '' as Ref }, actionImpl: { MarkUnread: '' as ViewAction, @@ -93,7 +96,9 @@ export default mergeIds(chunterId, chunter, { Random: '' as Ref }, function: { - ChunterBrowserVisible: '' as Resource<(spaces: Space[]) => Promise> + ChunterBrowserVisible: '' as Resource<(spaces: Space[]) => Promise>, + GetLink: '' as Resource<(doc: Doc, props: Record) => Promise>, + GetFragment: '' as Resource<(doc: Doc, props: Record) => Promise> }, filter: { CommentsFilter: '' as Resource<(tx: DisplayTx, _class?: Ref) => boolean>, diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index cba6992166..d9ec0182e5 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -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, diff --git a/models/document/src/index.ts b/models/document/src/index.ts index fef7f68beb..820cc620cc 100644 --- a/models/document/src/index.ts +++ b/models/document/src/index.ts @@ -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, diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index f7979fdb08..2532e888cc 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -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, diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index 8502cc2243..02d11069ef 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -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 { @@ -79,6 +89,57 @@ async function setCreate (client: MigrationClient): Promise { } } +async function fillVacancyNumbers (client: MigrationClient): Promise { + const docs = await client.find(DOMAIN_SPACE, { + _class: recruit.class.Vacancy, + number: { $exists: false } + }) + if (docs.length === 0) return + const txex = await client.find>(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(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 { const objects = await client.find(DOMAIN_SPACE, { _class: recruit.class.Vacancy, @@ -115,6 +176,7 @@ export const recruitOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await setCreate(client) await fixImportedTitle(client) + await fillVacancyNumbers(client) await client.update( DOMAIN_CALENDAR, { @@ -196,6 +258,7 @@ async function createDefaults (tx: TxOperations): Promise { 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) } diff --git a/models/recruit/src/plugin.ts b/models/recruit/src/plugin.ts index 16c6cab36a..7a6555451e 100644 --- a/models/recruit/src/plugin.ts +++ b/models/recruit/src/plugin.ts @@ -39,12 +39,10 @@ export default mergeIds(recruitId, recruit, { Recruit: '' as Ref }, function: { - GetApplicationId: '' as Resource<(doc: Doc, props: Record) => Promise>, - GetApplicationLink: '' as Resource<(doc: Doc, props: Record) => Promise>, - GetRecruitLink: '' as Resource<(doc: Doc, props: Record) => Promise> + GetObjectLinkFragment: '' as Resource<(doc: Doc, props: Record) => Promise>, + GetObjectLink: '' as Resource<(doc: Doc, props: Record) => Promise> }, string: { - ApplicationShort: '' as IntlString, ApplicationsShort: '' as IntlString, RecruitApplication: '' as IntlString, TalentPools: '' as IntlString, diff --git a/models/recruit/src/review-model.ts b/models/recruit/src/review-model.ts index e6fbe8910b..a75555259d 100644 --- a/models/recruit/src/review-model.ts +++ b/models/recruit/src/review-model.ts @@ -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 diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 5e8f6d4bcf..87f19297fc 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -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 { * @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 @@ -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 @@ -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, diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 10de390c40..aa80abef1f 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -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) => Promise> +} + +@Mixin(view.mixin.ObjectPanel, core.class.Class) +export class TObjectPanel extends TClass implements ObjectPanel { + component!: AnyComponent +} + export type ActionTemplate = Partial> /** @@ -318,6 +330,8 @@ export const actionTemplates = template({ export function createModel (builder: Builder): void { builder.createModel( + TLinkProvider, + TObjectPanel, TFilterMode, TClassFilters, TAttributeFilter, diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 7736e4ada5..b7c557de2a 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -161,7 +161,7 @@ export interface Class extends Classifier { extends?: Ref> implements?: Ref>[] domain?: Domain - shortLabel?: IntlString + shortLabel?: string sortingKey?: string } diff --git a/packages/model/src/dsl.ts b/packages/model/src/dsl.ts index 38e3f15e79..1ba152717a 100644 --- a/packages/model/src/dsl.ts +++ b/packages/model/src/dsl.ts @@ -75,7 +75,7 @@ interface ClassTxes { icon?: Asset txes: Array kind: ClassifierKind - shortLabel?: IntlString + shortLabel?: string | IntlString sortingKey?: string } @@ -226,7 +226,7 @@ export function Mixin (_class: Ref>, _extends: Ref (label: IntlString, icon?: Asset, shortLabel?: IntlString, sortingKey?: string) { +export function UX (label: IntlString, icon?: Asset, shortLabel?: string, sortingKey?: string) { return function classDecorator T> (constructor: C): void { const txes = getTxes(constructor.prototype) txes.label = label diff --git a/packages/presentation/src/components/NavLink.svelte b/packages/presentation/src/components/NavLink.svelte new file mode 100644 index 0000000000..a508877e04 --- /dev/null +++ b/packages/presentation/src/components/NavLink.svelte @@ -0,0 +1,72 @@ + + + +{#if disableClick || onClick || href === undefined} + + + + +{:else} + + + +{/if} + + diff --git a/packages/presentation/src/index.ts b/packages/presentation/src/index.ts index b91bbb8d7c..242c1fc769 100644 --- a/packages/presentation/src/index.ts +++ b/packages/presentation/src/index.ts @@ -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' diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index 2fa1ddb844..797c9c4ca6 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -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) => void @@ -104,22 +105,39 @@ export class LiveQuery extends TxProcessor implements Client { return true } + private createDumpQuery( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): 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( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { - 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 + 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 } async findOne( @@ -127,17 +145,17 @@ export class LiveQuery extends TxProcessor implements Client { query: DocumentQuery, options?: FindOptions ): Promise | 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 + 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 } private findQuery( @@ -253,7 +271,7 @@ export class LiveQuery extends TxProcessor implements Client { } private async checkSearch (q: Query, _id: Ref): Promise { - 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): Promise { - 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 { diff --git a/packages/theme/styles/components.scss b/packages/theme/styles/components.scss index 67cdee18d8..fb0af8d1a9 100644 --- a/packages/theme/styles/components.scss +++ b/packages/theme/styles/components.scss @@ -66,7 +66,6 @@ display: flex; flex-wrap: nowrap; min-width: 0; - cursor: default; } .ac-header__wrap-description { flex-direction: column; diff --git a/packages/ui/src/location.ts b/packages/ui/src/location.ts index 83666ce4bc..91b2481640 100644 --- a/packages/ui/src/location.ts +++ b/packages/ui/src/location.ts @@ -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) } } diff --git a/packages/ui/src/panelup.ts b/packages/ui/src/panelup.ts index 4748243821..4d35e11b9f 100644 --- a/packages/ui/src/panelup.ts +++ b/packages/ui/src/panelup.ts @@ -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) + } } diff --git a/plugins/activity-resources/src/components/TxView.svelte b/plugins/activity-resources/src/components/TxView.svelte index 7d73acf04e..d2a0dccf1f 100644 --- a/plugins/activity-resources/src/components/TxView.svelte +++ b/plugins/activity-resources/src/components/TxView.svelte @@ -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 => { - 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 diff --git a/plugins/chunter-resources/src/components/ChannelPresenter.svelte b/plugins/chunter-resources/src/components/ChannelPresenter.svelte index caa7a9e61d..8d212fdca7 100644 --- a/plugins/chunter-resources/src/components/ChannelPresenter.svelte +++ b/plugins/chunter-resources/src/components/ChannelPresenter.svelte @@ -13,26 +13,25 @@ // limitations under the License. --> {#if value} - -
- {#if icon} - - {/if} + +
+
+ {#if icon} + + {/if} +
+ {value.name}
- {value.name} -
+ {/if} diff --git a/plugins/chunter-resources/src/components/DmPresenter.svelte b/plugins/chunter-resources/src/components/DmPresenter.svelte index c0cb9076a9..46c8caee82 100644 --- a/plugins/chunter-resources/src/components/DmPresenter.svelte +++ b/plugins/chunter-resources/src/components/DmPresenter.svelte @@ -13,28 +13,29 @@ // limitations under the License. --> {#if value} {#await getDmName(client, value) then name} - -
- {#if icon} - - {/if} + +
+
+ {#if icon} + + {/if} +
+ {name}
- {name} -
+ {/await} {/if} diff --git a/plugins/chunter-resources/src/components/Message.svelte b/plugins/chunter-resources/src/components/Message.svelte index 114650fcf9..147e572da4 100644 --- a/plugins/chunter-resources/src/components/Message.svelte +++ b/plugins/chunter-resources/src/components/Message.svelte @@ -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 => { - 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] : []) ] }, diff --git a/plugins/chunter-resources/src/components/MessagePresenter.svelte b/plugins/chunter-resources/src/components/MessagePresenter.svelte index 7639280570..26a78bb3f9 100644 --- a/plugins/chunter-resources/src/components/MessagePresenter.svelte +++ b/plugins/chunter-resources/src/components/MessagePresenter.svelte @@ -1,9 +1,9 @@ {#if dm} {#await getDmName(client, dm) then name} - goto()}> + {name} - +
{/await} {/if} diff --git a/plugins/chunter-resources/src/index.ts b/plugins/chunter-resources/src/index.ts index 20271ed96c..c9e4094b17 100644 --- a/plugins/chunter-resources/src/index.ts +++ b/plugins/chunter-resources/src/index.ts @@ -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 => ({ }, function: { GetDmName: getDmName, - ChunterBrowserVisible: chunterBrowserVisible + ChunterBrowserVisible: chunterBrowserVisible, + GetFragment: getFragment, + GetLink: getLink }, activity: { TxCommentCreate, @@ -263,5 +265,8 @@ export default async (): Promise => ({ }, backreference: { Update: update + }, + resolver: { + Location: resolveLocation } }) diff --git a/plugins/chunter-resources/src/utils.ts b/plugins/chunter-resources/src/utils.ts index 6226d705d2..4b8ae2f18c 100644 --- a/plugins/chunter-resources/src/utils.ts +++ b/plugins/chunter-resources/src/utils.ts @@ -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 { return name } -export function getSpaceLink (id: Ref): 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 { + 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 { + 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 { + 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 { + 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> | 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) +} diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts index 252afd8a9c..8aaf9aeaaa 100644 --- a/plugins/chunter/src/index.ts +++ b/plugins/chunter/src/index.ts @@ -156,6 +156,9 @@ export default plugin(chunterId, { UnarchiveConfirm: '' as IntlString, ConvertToPrivate: '' as IntlString }, + resolver: { + Location: '' as Resource<(loc: Location) => Promise> + }, app: { Chunter: '' as Ref }, diff --git a/plugins/contact-resources/src/components/ChannelsPopup.svelte b/plugins/contact-resources/src/components/ChannelsPopup.svelte index c22f98ee23..74c74272a7 100644 --- a/plugins/contact-resources/src/components/ChannelsPopup.svelte +++ b/plugins/contact-resources/src/components/ChannelsPopup.svelte @@ -14,7 +14,7 @@ // limitations under the License. --> - {}} - {shouldShowAvatar} - {shouldShowName} - {avatarSize} - {shouldShowPlaceholder} - {isInteractive} - {inline} - {defaultName} -/> -{#if value?.active === false} -
- (
-{/if} + + + {#if value?.active === false && shouldShowName} + + ( + {/if} + diff --git a/plugins/tracker-resources/src/components/issues/TitlePresenter.svelte b/plugins/tracker-resources/src/components/issues/TitlePresenter.svelte index e515eaf91b..95bd43e320 100644 --- a/plugins/tracker-resources/src/components/issues/TitlePresenter.svelte +++ b/plugins/tracker-resources/src/components/issues/TitlePresenter.svelte @@ -13,59 +13,44 @@ // limitations under the License. --> {#if value} - - + {value.title}{value.title} {#if showParent} {/if} - + {/if} diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index 16dcc80367..8ac44b5fd8 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -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 => ({ IssueTitleProvider: getIssueTitle, GetIssueId: issueIdProvider, GetIssueLink: issueLinkProvider, + GetIssueLinkFragment: issueLinkFragmentProvider, GetIssueTitle: issueTitleProvider, IssueStatusSort: issueStatusSort, IssuePrioritySort: issuePrioritySort, diff --git a/plugins/tracker-resources/src/issues.ts b/plugins/tracker-resources/src/issues.ts index c2f53cf798..ce3edfe935 100644 --- a/plugins/tracker-resources/src/issues.ts +++ b/plugins/tracker-resources/src/issues.ts @@ -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 { return await getIssueTitle(client, doc._id) } +export async function issueLinkFragmentProvider (doc: Doc): Promise { + const client = getClient() + return await getIssueTitle(client, doc._id).then((p) => `${trackerId}|${p}`) +} + export async function issueTitleProvider (doc: Issue): Promise { return await Promise.resolve(doc.title) } @@ -47,7 +52,7 @@ export async function issueLinkProvider (doc: Doc): Promise { 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 { @@ -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 { - 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 } diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index 5a24d6d74c..1411231d80 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -378,6 +378,7 @@ export default mergeIds(trackerId, tracker, { IssueTitleProvider: '' as Resource<(client: Client, ref: Ref) => Promise>, GetIssueId: '' as Resource<(doc: Doc, props: Record) => Promise>, GetIssueLink: '' as Resource<(doc: Doc, props: Record) => Promise>, + GetIssueLinkFragment: '' as Resource<(doc: Doc, props: Record) => Promise>, GetIssueTitle: '' as Resource<(doc: Doc, props: Record) => Promise>, IssueStatusSort: '' as SortFunc, IssuePrioritySort: '' as SortFunc, diff --git a/plugins/view-resources/src/components/DocNavLink.svelte b/plugins/view-resources/src/components/DocNavLink.svelte new file mode 100644 index 0000000000..bc49d81945 --- /dev/null +++ b/plugins/view-resources/src/components/DocNavLink.svelte @@ -0,0 +1,42 @@ + + + + diff --git a/plugins/view-resources/src/components/Menu.svelte b/plugins/view-resources/src/components/Menu.svelte index 89fc9a7ccd..543cb0cc8b 100644 --- a/plugins/view-resources/src/components/Menu.svelte +++ b/plugins/view-resources/src/components/Menu.svelte @@ -24,10 +24,11 @@ export let baseMenuClass: Ref> | 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 = { 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 }) {#if loaded} - + {/if} diff --git a/plugins/view-resources/src/components/UpDownNavigator.svelte b/plugins/view-resources/src/components/UpDownNavigator.svelte index 66e9fda944..f9208a9f80 100644 --- a/plugins/view-resources/src/components/UpDownNavigator.svelte +++ b/plugins/view-resources/src/components/UpDownNavigator.svelte @@ -1,31 +1,38 @@
-
- {#if icon}
{/if} - {label} -
+ +
+ {#if icon}
{/if} + {space.name} +
+
{#if _class}
{/if}
{#if description} @@ -47,7 +65,6 @@