diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 5ebf604b28..e1117a2967 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -51,7 +51,7 @@ import workbench, { createNavigateAction } from '@hcengineering/model-workbench' import notification from '@hcengineering/notification' import { Asset, IntlString } from '@hcengineering/platform' import setting from '@hcengineering/setting' -import tags from '@hcengineering/tags' +import tags, { TagElement } from '@hcengineering/tags' import task from '@hcengineering/task' import { Document, @@ -61,6 +61,8 @@ import { IssuePriority, IssueStatus, IssueStatusCategory, + IssueTemplate, + IssueTemplateChild, Project, ProjectStatus, Sprint, @@ -254,6 +256,53 @@ export class TIssue extends TAttachedDoc implements Issue { declare childInfo: IssueChildInfo[] } +/** + * @public + */ +@Model(tracker.class.IssueTemplate, core.class.Doc, DOMAIN_TRACKER) +@UX(tracker.string.IssueTemplate, tracker.icon.Issue, tracker.string.IssueTemplate) +export class TIssueTemplate extends TDoc implements IssueTemplate { + @Prop(TypeString(), tracker.string.Title) + @Index(IndexKind.FullText) + title!: string + + @Prop(TypeMarkup(), tracker.string.Description) + @Index(IndexKind.FullText) + description!: Markup + + @Prop(TypeIssuePriority(), tracker.string.Priority) + priority!: IssuePriority + + @Prop(TypeRef(contact.class.Employee), tracker.string.Assignee) + assignee!: Ref | null + + @Prop(TypeRef(tracker.class.Project), tracker.string.Project) + project!: Ref | null + + @Prop(ArrOf(TypeRef(tags.class.TagElement)), tracker.string.Labels) + labels?: Ref[] + + declare space: Ref + + @Prop(TypeDate(true), tracker.string.DueDate) + dueDate!: Timestamp | null + + @Prop(TypeRef(tracker.class.Sprint), tracker.string.Sprint) + sprint!: Ref | null + + @Prop(TypeNumber(), tracker.string.Estimation) + estimation!: number + + @Prop(ArrOf(TypeRef(tracker.class.IssueTemplate)), tracker.string.IssueTemplate) + children!: IssueTemplateChild[] + + @Prop(Collection(chunter.class.Comment), tracker.string.Comments) + comments!: number + + @Prop(Collection(attachment.class.Attachment), tracker.string.Attachments) + attachments!: number +} + /** * @public */ @@ -389,6 +438,7 @@ export function createModel (builder: Builder): void { TTeam, TProject, TIssue, + TIssueTemplate, TIssueStatus, TIssueStatusCategory, TTypeIssuePriority, @@ -403,16 +453,20 @@ export function createModel (builder: Builder): void { attachTo: tracker.class.Issue, descriptor: tracker.viewlet.List, config: [ - { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } }, - { key: '', presenter: tracker.component.IssuePresenter }, + { + key: '', + presenter: tracker.component.PriorityEditor, + props: { type: 'priority', kind: 'list', size: 'small' } + }, + { key: '', presenter: tracker.component.IssuePresenter, props: { type: 'issue' } }, { key: '', presenter: tracker.component.StatusEditor, props: { kind: 'list', size: 'small', justify: 'center' } }, - { key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true, fixed: 'left' } }, + { key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } }, { key: '', presenter: tracker.component.SubIssuesSelector, props: {} }, - { key: '', presenter: tracker.component.GrowPresenter, props: {} }, + { key: '', presenter: tracker.component.GrowPresenter, props: { type: 'grow' } }, { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } }, { key: '', @@ -429,7 +483,35 @@ export function createModel (builder: Builder): void { { key: '$lookup.assignee', presenter: tracker.component.AssigneePresenter, - props: { defaultClass: contact.class.Employee, shouldShowLabel: false } + props: { issueClass: tracker.class.Issue, defaultClass: contact.class.Employee, shouldShowLabel: false } + } + ] + }) + + builder.createDoc(view.class.Viewlet, core.space.Model, { + attachTo: tracker.class.IssueTemplate, + descriptor: tracker.viewlet.List, + config: [ + // { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } }, + { key: '', presenter: tracker.component.IssueTemplatePresenter, props: { type: 'issue', shouldUseMargin: true } }, + { key: '', presenter: tracker.component.GrowPresenter, props: { type: 'grow' } }, + // { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } }, + { + key: '', + presenter: tracker.component.ProjectEditor, + props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false } + }, + { + key: '', + presenter: tracker.component.SprintEditor, + props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false } + }, + // { key: '', presenter: tracker.component.EstimationEditor, props: { kind: 'list', size: 'small' } }, + { key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter, props: { fixed: 'right' } }, + { + key: '$lookup.assignee', + presenter: tracker.component.AssigneePresenter, + props: { issueClass: tracker.class.IssueTemplate, defaultClass: contact.class.Employee, shouldShowLabel: false } } ] }) @@ -533,11 +615,16 @@ export function createModel (builder: Builder): void { const boardId = 'board' const projectsId = 'projects' const sprintsId = 'sprints' + const templatesId = 'templates' builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.AttributePresenter, { presenter: tracker.component.IssuePresenter }) + builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.AttributePresenter, { + presenter: tracker.component.IssueTemplatePresenter + }) + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.PreviewPresenter, { presenter: tracker.component.IssuePreview }) @@ -671,6 +758,12 @@ export function createModel (builder: Builder): void { label: tracker.string.Sprints, icon: tracker.icon.Sprint, component: tracker.component.Sprints + }, + { + id: templatesId, + label: tracker.string.IssueTemplates, + icon: tracker.icon.Issues, + component: tracker.component.IssueTemplates } ] } @@ -846,6 +939,10 @@ export function createModel (builder: Builder): void { filters: ['status', 'priority', 'assignee', 'project', 'sprint', 'dueDate', 'modifiedOn'] }) + builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ClassFilters, { + filters: ['priority', 'assignee', 'project', 'sprint', 'dueDate', 'modifiedOn'] + }) + builder.createDoc( presentation.class.ObjectSearchCategory, core.space.Model, diff --git a/packages/core/src/operator.ts b/packages/core/src/operator.ts index 3f22318a19..a4e37d6e26 100644 --- a/packages/core/src/operator.ts +++ b/packages/core/src/operator.ts @@ -15,7 +15,7 @@ // import type { Doc, PropertyType } from './classes' -import type { Position, PullArray } from './tx' +import type { Position, PullArray, QueryUpdate } from './tx' /** * @internal @@ -72,6 +72,45 @@ function $pull (document: Doc, keyval: Record): void { } } +function matchArrayElement (docs: any[], query: Partial): any[] { + let result = [...docs] + for (const key in query) { + const value = (query as any)[key] + + const tresult: any[] = [] + for (const object of result) { + const val = object[key] + if (val === value) { + tresult.push(object) + } + } + result = tresult + if (tresult.length === 0) { + break + } + } + return result +} + +function $update (document: Doc, keyval: Record): void { + const doc = document as any + for (const key in keyval) { + if (doc[key] === undefined) { + doc[key] = [] + } + const val = keyval[key] + if (typeof val === 'object') { + const arr = doc[key] as Array + const desc = val as QueryUpdate + for (const m of matchArrayElement(arr, desc.$query)) { + for (const [k, v] of Object.entries(desc.$update)) { + m[k] = v + } + } + } + } +} + function $move (document: Doc, keyval: Record): void { const doc = document as any for (const key in keyval) { @@ -114,6 +153,7 @@ function $inc (document: Doc, keyval: Record): void { const operators: Record = { $push, $pull, + $update, $move, $pushMixin, $inc diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index 9c593222d8..3393a6f4ff 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -114,6 +114,14 @@ export interface Position { $position: number } +/** + * @public + */ +export interface QueryUpdate { + $query: Partial + $update: Partial +} + /** * @public */ @@ -136,6 +144,13 @@ export type ArrayAsElementPosition = { [P in keyof T]-?: T[P] extends Arr ? X | Position : never } +/** + * @public + */ +export type ArrayAsElementUpdate = { + [P in keyof T]-?: T[P] extends Arr ? X | QueryUpdate : never +} + /** * @public */ @@ -164,6 +179,13 @@ export interface PushOptions { $move?: Partial>>> } +/** + * @public + */ +export interface UpdateArrayOptions { + $update?: Partial>>> +} + /** * @public */ @@ -193,6 +215,7 @@ export interface SpaceUpdate { */ export type DocumentUpdate = Partial> & PushOptions & +UpdateArrayOptions & PushMixinOptions & IncOptions & SpaceUpdate diff --git a/packages/presentation/src/components/ObjectPopup.svelte b/packages/presentation/src/components/ObjectPopup.svelte index 00f7119be5..8dcd386596 100644 --- a/packages/presentation/src/components/ObjectPopup.svelte +++ b/packages/presentation/src/components/ObjectPopup.svelte @@ -104,7 +104,11 @@ const person = objects[selection] if (!multiSelect) { - selected = person._id === selected ? undefined : person._id + if (allowDeselect) { + selected = person._id === selected ? undefined : person._id + } else { + selected = person._id + } dispatch('close', selected !== undefined ? person : undefined) } else { checkSelected(person, objects) diff --git a/packages/presentation/src/components/SpaceSelect.svelte b/packages/presentation/src/components/SpaceSelect.svelte index 1dc4178e14..085808e74e 100644 --- a/packages/presentation/src/components/SpaceSelect.svelte +++ b/packages/presentation/src/components/SpaceSelect.svelte @@ -66,6 +66,7 @@ if (selected !== undefined) { value = selected._id dispatch('change', value) + dispatch('space', selected) } } } diff --git a/packages/presentation/src/components/SpaceSelector.svelte b/packages/presentation/src/components/SpaceSelector.svelte index ed3f338f1b..7beb6160b6 100644 --- a/packages/presentation/src/components/SpaceSelector.svelte +++ b/packages/presentation/src/components/SpaceSelector.svelte @@ -55,4 +55,5 @@ on:change={(evt) => { space = evt.detail }} + on:space /> diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index 1822e94127..b9c7ee7efd 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -653,7 +653,6 @@ export class LiveQuery extends TxProcessor implements Client { if (q.result instanceof Promise) { q.result = await q.result } - if (q.options?.lookup !== undefined && handleLookup) { await this.lookup(q._class, doc, q.options.lookup) } diff --git a/packages/text-editor/src/components/StyleButton.svelte b/packages/text-editor/src/components/StyleButton.svelte index 2b94379be6..72146dc315 100644 --- a/packages/text-editor/src/components/StyleButton.svelte +++ b/packages/text-editor/src/components/StyleButton.svelte @@ -14,11 +14,11 @@ --> @@ -83,7 +100,7 @@ /> - + + + + + + + + {/if} + diff --git a/plugins/view-resources/src/components/ObjectBoxPopup.svelte b/plugins/view-resources/src/components/ObjectBoxPopup.svelte new file mode 100644 index 0000000000..d6afb40915 --- /dev/null +++ b/plugins/view-resources/src/components/ObjectBoxPopup.svelte @@ -0,0 +1,73 @@ + + + + dispatch('changeContent')} +> + +
+ +
+
+
diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index e9866e9be6..805123357e 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -77,6 +77,8 @@ export { default as ContextMenu } from './components/Menu.svelte' export { default as TableBrowser } from './components/TableBrowser.svelte' export { default as FixedColumn } from './components/FixedColumn.svelte' export { default as ViewOptionsButton } from './components/ViewOptionsButton.svelte' +export { default as ValueSelector } from './components/ValueSelector.svelte' +export { default as ObjectBox } from './components/ObjectBox.svelte' export * from './context' export * from './filter' export * from './selection' diff --git a/server/mongo/src/storage.ts b/server/mongo/src/storage.ts index 0facd106a5..6297584198 100644 --- a/server/mongo/src/storage.ts +++ b/server/mongo/src/storage.ts @@ -28,6 +28,7 @@ import core, { Lookup, Mixin, ModelDb, + QueryUpdate, Ref, ReverseLookups, SortingOrder, @@ -649,6 +650,37 @@ class MongoAdapter extends MongoAdapterBase { } ] return await this.db.collection(domain).bulkWrite(ops as any) + } else if (operator === '$update') { + const keyval = (tx.operations as any).$update + const arr = Object.keys(keyval)[0] + const desc = keyval[arr] as QueryUpdate + const ops = [ + { + updateOne: { + filter: { + _id: tx.objectId, + ...Object.fromEntries(Object.entries(desc.$query).map((it) => [arr + '.' + it[0], it[1]])) + }, + update: { + $set: { + ...Object.fromEntries(Object.entries(desc.$update).map((it) => [arr + '.$.' + it[0], it[1]])) + } + } + } + }, + { + updateOne: { + filter: { _id: tx.objectId }, + update: { + $set: { + modifiedBy: tx.modifiedBy, + modifiedOn: tx.modifiedOn + } + } + } + } + ] + return await this.db.collection(domain).bulkWrite(ops as any) } else { if (tx.retrieve === true) { const result = await this.db.collection(domain).findOneAndUpdate( diff --git a/tests/sanity/tests/recruit.spec.ts b/tests/sanity/tests/recruit.spec.ts index ee3f235391..639f472fad 100644 --- a/tests/sanity/tests/recruit.spec.ts +++ b/tests/sanity/tests/recruit.spec.ts @@ -42,7 +42,7 @@ test.describe('recruit tests', () => { await page.locator('.antiPopup').locator('text=Email').click() const emailInput = page.locator('[placeholder="john\\.appleseed@apple\\.com"]') await emailInput.fill(email) - await emailInput.press('Enter') + await page.locator('#channel-ok.button').click() await page.locator('.antiCard button:has-text("Create")').click() await page.waitForSelector('form.antiCard', { state: 'detached' })