mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-20 15:20:18 +00:00
[UBER-196] Fix duplicated project ids when creating a project (#3247)
Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@icloud.com>
This commit is contained in:
parent
60dcf3ad02
commit
c0cb5a87e7
@ -96,5 +96,7 @@ export class TTxApplyIf extends TTx implements TxApplyIf {
|
|||||||
|
|
||||||
// All matches should be true with at least one document.
|
// All matches should be true with at least one document.
|
||||||
match!: DocumentClassQuery<Doc>[]
|
match!: DocumentClassQuery<Doc>[]
|
||||||
|
// All matches should be false for all documents.
|
||||||
|
notMatch!: DocumentClassQuery<Doc>[]
|
||||||
txes!: TxCUD<Doc>[]
|
txes!: TxCUD<Doc>[]
|
||||||
}
|
}
|
||||||
|
@ -281,6 +281,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
|
|||||||
export class ApplyOperations extends TxOperations {
|
export class ApplyOperations extends TxOperations {
|
||||||
txes: TxCUD<Doc>[] = []
|
txes: TxCUD<Doc>[] = []
|
||||||
matches: DocumentClassQuery<Doc>[] = []
|
matches: DocumentClassQuery<Doc>[] = []
|
||||||
|
notMatches: DocumentClassQuery<Doc>[] = []
|
||||||
constructor (readonly ops: TxOperations, readonly scope: string) {
|
constructor (readonly ops: TxOperations, readonly scope: string) {
|
||||||
const txClient: Client = {
|
const txClient: Client = {
|
||||||
getHierarchy: () => ops.client.getHierarchy(),
|
getHierarchy: () => ops.client.getHierarchy(),
|
||||||
@ -303,10 +304,15 @@ export class ApplyOperations extends TxOperations {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notMatch<T extends Doc>(_class: Ref<Class<T>>, query: DocumentQuery<T>): ApplyOperations {
|
||||||
|
this.notMatches.push({ _class, query })
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
async commit (): Promise<boolean> {
|
async commit (): Promise<boolean> {
|
||||||
if (this.txes.length > 0) {
|
if (this.txes.length > 0) {
|
||||||
return await ((await this.ops.tx(
|
return await ((await this.ops.tx(
|
||||||
this.ops.txFactory.createTxApplyIf(core.space.Tx, this.scope, this.matches, this.txes)
|
this.ops.txFactory.createTxApplyIf(core.space.Tx, this.scope, this.matches, this.notMatches, this.txes)
|
||||||
)) as Promise<boolean>)
|
)) as Promise<boolean>)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -120,6 +120,9 @@ export interface TxApplyIf extends Tx {
|
|||||||
// All matches should be true with at least one document.
|
// All matches should be true with at least one document.
|
||||||
match: DocumentClassQuery<Doc>[]
|
match: DocumentClassQuery<Doc>[]
|
||||||
|
|
||||||
|
// All matches should be false for all documents.
|
||||||
|
notMatch: DocumentClassQuery<Doc>[]
|
||||||
|
|
||||||
// If all matched execute following transactions.
|
// If all matched execute following transactions.
|
||||||
txes: TxCUD<Doc>[]
|
txes: TxCUD<Doc>[]
|
||||||
}
|
}
|
||||||
@ -586,6 +589,7 @@ export class TxFactory {
|
|||||||
space: Ref<Space>,
|
space: Ref<Space>,
|
||||||
scope: string,
|
scope: string,
|
||||||
match: DocumentClassQuery<Doc>[],
|
match: DocumentClassQuery<Doc>[],
|
||||||
|
notMatch: DocumentClassQuery<Doc>[],
|
||||||
txes: TxCUD<Doc>[],
|
txes: TxCUD<Doc>[],
|
||||||
modifiedOn?: Timestamp,
|
modifiedOn?: Timestamp,
|
||||||
modifiedBy?: Ref<Account>
|
modifiedBy?: Ref<Account>
|
||||||
@ -599,6 +603,7 @@ export class TxFactory {
|
|||||||
objectSpace: space,
|
objectSpace: space,
|
||||||
scope,
|
scope,
|
||||||
match,
|
match,
|
||||||
|
notMatch,
|
||||||
txes
|
txes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
import presentation, { Card } from '@hcengineering/presentation'
|
||||||
import { Project } from '@hcengineering/tracker'
|
|
||||||
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
|
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
|
|
||||||
export let project: Project
|
export let identifier: string
|
||||||
|
export let projectsIdentifiers: Set<string>
|
||||||
|
|
||||||
let identifier = project.identifier
|
let newIdentifier = identifier
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
function save () {
|
function save () {
|
||||||
dispatch('close', identifier)
|
dispatch('close', newIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
let projects: Set<string> = new Set()
|
|
||||||
|
|
||||||
$: getClient()
|
|
||||||
.findAll(tracker.class.Project, {})
|
|
||||||
.then((pr) => {
|
|
||||||
projects = new Set(pr.map((p) => p.identifier))
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
label={projects.has(identifier) ? tracker.string.IdentifierExists : tracker.string.ProjectIdentifier}
|
label={projectsIdentifiers.has(newIdentifier) ? tracker.string.IdentifierExists : tracker.string.ProjectIdentifier}
|
||||||
okLabel={presentation.string.Save}
|
okLabel={presentation.string.Save}
|
||||||
okAction={save}
|
okAction={save}
|
||||||
canSave={identifier !== project.identifier && !projects.has(identifier)}
|
canSave={!!newIdentifier && newIdentifier !== identifier && !projectsIdentifiers.has(newIdentifier)}
|
||||||
on:close={() => {
|
on:close={() => {
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
}}
|
}}
|
||||||
@ -36,7 +28,7 @@
|
|||||||
>
|
>
|
||||||
<div class="float-left-box">
|
<div class="float-left-box">
|
||||||
<div class="float-left p-2">
|
<div class="float-left p-2">
|
||||||
<EditBox bind:value={identifier} />
|
<EditBox bind:value={newIdentifier} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -15,9 +15,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Employee } from '@hcengineering/contact'
|
import { Employee } from '@hcengineering/contact'
|
||||||
import { AccountArrayEditor, AssigneeBox } from '@hcengineering/contact-resources'
|
import { AccountArrayEditor, AssigneeBox } from '@hcengineering/contact-resources'
|
||||||
import core, { Account, DocumentUpdate, Ref, SortingOrder, generateId, getCurrentAccount } from '@hcengineering/core'
|
import core, {
|
||||||
|
Account,
|
||||||
|
ApplyOperations,
|
||||||
|
DocumentUpdate,
|
||||||
|
Ref,
|
||||||
|
SortingOrder,
|
||||||
|
generateId,
|
||||||
|
getCurrentAccount
|
||||||
|
} from '@hcengineering/core'
|
||||||
import { Asset } from '@hcengineering/platform'
|
import { Asset } from '@hcengineering/platform'
|
||||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||||
import { IssueStatus, Project, TimeReportDayType, genRanks } from '@hcengineering/tracker'
|
import { IssueStatus, Project, TimeReportDayType, genRanks } from '@hcengineering/tracker'
|
||||||
import {
|
import {
|
||||||
@ -43,6 +51,7 @@
|
|||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
|
const projectsQuery = createQuery()
|
||||||
|
|
||||||
let name: string = project?.name ?? ''
|
let name: string = project?.name ?? ''
|
||||||
let description: string = project?.description ?? ''
|
let description: string = project?.description ?? ''
|
||||||
@ -52,13 +61,21 @@
|
|||||||
let defaultAssignee: Ref<Employee> | null | undefined = project?.defaultAssignee ?? null
|
let defaultAssignee: Ref<Employee> | null | undefined = project?.defaultAssignee ?? null
|
||||||
let members: Ref<Account>[] =
|
let members: Ref<Account>[] =
|
||||||
project?.members !== undefined ? hierarchy.clone(project.members) : [getCurrentAccount()._id]
|
project?.members !== undefined ? hierarchy.clone(project.members) : [getCurrentAccount()._id]
|
||||||
|
let projectsIdentifiers: Set<string> = new Set()
|
||||||
|
let isSaving = false
|
||||||
|
|
||||||
|
let changeIdentityRef: HTMLElement
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: isNew = !project
|
$: isNew = !project
|
||||||
|
|
||||||
async function handleSave () {
|
async function handleSave () {
|
||||||
isNew ? createProject() : updateProject()
|
if (isNew) {
|
||||||
|
await createProject()
|
||||||
|
} else {
|
||||||
|
await updateProject()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let identifier: string = project?.identifier ?? 'TSK'
|
let identifier: string = project?.identifier ?? 'TSK'
|
||||||
@ -84,6 +101,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateProject () {
|
async function updateProject () {
|
||||||
|
if (!project) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const { sequence, issueStatuses, defaultIssueStatus, ...projectData } = getProjectData()
|
const { sequence, issueStatuses, defaultIssueStatus, ...projectData } = getProjectData()
|
||||||
const update: DocumentUpdate<Project> = {}
|
const update: DocumentUpdate<Project> = {}
|
||||||
if (projectData.name !== project?.name) {
|
if (projectData.name !== project?.name) {
|
||||||
@ -121,21 +142,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Object.keys(update).length > 0) {
|
if (Object.keys(update).length > 0) {
|
||||||
await client.update(project!, update)
|
const ops = client.apply(project._id).notMatch(tracker.class.Project, { identifier: projectData.identifier })
|
||||||
|
|
||||||
|
isSaving = true
|
||||||
|
await ops.update(project, update)
|
||||||
|
const succeeded = await ops.commit()
|
||||||
|
isSaving = false
|
||||||
|
|
||||||
|
if (succeeded) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
changeIdentity(changeIdentityRef)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createProject () {
|
async function createProject () {
|
||||||
const id = await client.createDoc(tracker.class.Project, core.space.Space, getProjectData())
|
const projectId = generateId<Project>()
|
||||||
await createProjectIssueStatuses(id, defaultStatusId)
|
const projectData = getProjectData()
|
||||||
|
const ops = client.apply(projectId).notMatch(tracker.class.Project, { identifier: projectData.identifier })
|
||||||
|
|
||||||
|
isSaving = true
|
||||||
|
await ops.createDoc(tracker.class.Project, core.space.Space, projectData, projectId)
|
||||||
|
await createProjectIssueStatuses(ops, projectId, defaultStatusId)
|
||||||
|
const succeeded = await ops.commit()
|
||||||
|
isSaving = false
|
||||||
|
|
||||||
|
if (succeeded) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
changeIdentity(changeIdentityRef)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createProjectIssueStatuses (
|
async function createProjectIssueStatuses (
|
||||||
|
ops: ApplyOperations,
|
||||||
projectId: Ref<Project>,
|
projectId: Ref<Project>,
|
||||||
defaultStatusId: Ref<IssueStatus>,
|
defaultStatusId: Ref<IssueStatus>,
|
||||||
defaultCategoryId = tracker.issueStatusCategory.Backlog
|
defaultCategoryId = tracker.issueStatusCategory.Backlog
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const categories = await client.findAll(
|
const categories = await ops.findAll(
|
||||||
core.class.StatusCategory,
|
core.class.StatusCategory,
|
||||||
{ ofAttribute: tracker.attribute.IssueStatus },
|
{ ofAttribute: tracker.attribute.IssueStatus },
|
||||||
{ sort: { order: SortingOrder.Ascending } }
|
{ sort: { order: SortingOrder.Ascending } }
|
||||||
@ -147,7 +193,7 @@
|
|||||||
const rank = issueStatusRanks[i]
|
const rank = issueStatusRanks[i]
|
||||||
|
|
||||||
if (defaultStatusName !== undefined) {
|
if (defaultStatusName !== undefined) {
|
||||||
await client.createDoc(
|
await ops.createDoc(
|
||||||
tracker.class.IssueStatus,
|
tracker.class.IssueStatus,
|
||||||
projectId,
|
projectId,
|
||||||
{
|
{
|
||||||
@ -170,26 +216,37 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function changeIdentity (ev: MouseEvent) {
|
function changeIdentity (element: HTMLElement) {
|
||||||
showPopup(ChangeIdentity, { project }, eventToHTMLElement(ev), (result) => {
|
showPopup(ChangeIdentity, { identifier, projectsIdentifiers }, element, (result) => {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
identifier = result
|
identifier = result
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function close () {
|
||||||
|
dispatch('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
$: projectsQuery.query(
|
||||||
|
tracker.class.Project,
|
||||||
|
{ _id: { $nin: project ? [project._id] : [] } },
|
||||||
|
(res) => (projectsIdentifiers = new Set(res.map(({ identifier }) => identifier)))
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
label={isNew ? tracker.string.NewProject : tracker.string.EditProject}
|
label={isNew ? tracker.string.NewProject : tracker.string.EditProject}
|
||||||
okLabel={isNew ? presentation.string.Create : presentation.string.Save}
|
okLabel={isNew ? presentation.string.Create : presentation.string.Save}
|
||||||
okAction={handleSave}
|
okAction={handleSave}
|
||||||
canSave={name.length > 0 && !(members.length === 0 && isPrivate)}
|
canSave={name.length > 0 &&
|
||||||
|
identifier.length > 0 &&
|
||||||
|
!projectsIdentifiers.has(identifier) &&
|
||||||
|
!(members.length === 0 && isPrivate)}
|
||||||
accentHeader
|
accentHeader
|
||||||
width={'medium'}
|
width={'medium'}
|
||||||
gap={'gapV-6'}
|
gap={'gapV-6'}
|
||||||
on:close={() => {
|
onCancel={close}
|
||||||
dispatch('close')
|
|
||||||
}}
|
|
||||||
on:changeContent
|
on:changeContent
|
||||||
>
|
>
|
||||||
<div class="antiGrid">
|
<div class="antiGrid">
|
||||||
@ -217,7 +274,7 @@
|
|||||||
<Label label={tracker.string.Identifier} />
|
<Label label={tracker.string.Identifier} />
|
||||||
<span><Label label={tracker.string.UsedInIssueIDs} /></span>
|
<span><Label label={tracker.string.UsedInIssueIDs} /></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="padding flex-row-center">
|
<div bind:this={changeIdentityRef} class="padding flex-row-center relative">
|
||||||
<EditBox
|
<EditBox
|
||||||
bind:value={identifier}
|
bind:value={identifier}
|
||||||
disabled={!isNew}
|
disabled={!isNew}
|
||||||
@ -226,7 +283,13 @@
|
|||||||
uppercase
|
uppercase
|
||||||
/>
|
/>
|
||||||
{#if !isNew}
|
{#if !isNew}
|
||||||
<Button size={'small'} icon={IconEdit} on:click={changeIdentity} />
|
<div class="ml-1">
|
||||||
|
<Button size={'small'} icon={IconEdit} on:click={(ev) => changeIdentity(eventToHTMLElement(ev))} />
|
||||||
|
</div>
|
||||||
|
{:else if !isSaving && projectsIdentifiers.has(identifier)}
|
||||||
|
<div class="absolute overflow-label duplicated-identifier">
|
||||||
|
<Label label={tracker.string.IdentifierExists} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -305,3 +368,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.duplicated-identifier {
|
||||||
|
left: 0;
|
||||||
|
bottom: -0.25rem;
|
||||||
|
color: var(--theme-warning-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -594,6 +594,15 @@ class TServerStorage implements ServerStorage {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (passed) {
|
||||||
|
for (const { _class, query } of applyIf.notMatch) {
|
||||||
|
const res = await findAll(ctx, _class, query, { limit: 1 })
|
||||||
|
if (res.length > 0) {
|
||||||
|
passed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return { passed, onEnd }
|
return { passed, onEnd }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user