Fix role migration, remove extra rosamunds (#2190)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
Co-authored-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Denis Bykhov 2022-07-03 00:49:22 +06:00 committed by GitHub
parent a4b3cb4a44
commit a5b1d5c278
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 102 additions and 52 deletions

View File

@ -88,7 +88,8 @@ async function setRole (client: MigrationClient): Promise<void> {
DOMAIN_TX, DOMAIN_TX,
{ {
_class: core.class.TxCreateDoc, _class: core.class.TxCreateDoc,
objectClass: contact.class.Employee objectClass: contact.class.EmployeeAccount,
'attributes.role': { $exists: false }
}, },
{ {
'attributes.role': AccountRole.User 'attributes.role': AccountRole.User

View File

@ -13,10 +13,11 @@
// limitations under the License. // limitations under the License.
// //
import core, { AccountRole, TxOperations } from '@anticrm/core' import core, { AccountRole, DOMAIN_TX, TxCreateDoc, TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model' import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import contact, { EmployeeAccount } from '@anticrm/contact' import contact, { EmployeeAccount } from '@anticrm/contact'
import recruit from '@anticrm/model-recruit' import recruit from '@anticrm/model-recruit'
import { DOMAIN_CONTACT } from '@anticrm/model-contact'
async function createCandidate ( async function createCandidate (
tx: TxOperations, tx: TxOperations,
@ -45,20 +46,37 @@ async function createCandidate (
} }
export const demoOperation: MigrateOperation = { export const demoOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {}, async migrate (client: MigrationClient): Promise<void> {
const rosamunds = await client.find<TxCreateDoc<EmployeeAccount>>(DOMAIN_TX, {
_class: core.class.TxCreateDoc,
objectClass: contact.class.EmployeeAccount,
'attributes.email': 'rosamund@hc.engineering'
})
const docs = await client.find(DOMAIN_CONTACT, {
_id: { $in: rosamunds.map((p) => p.attributes.employee) }
})
const currentEmployees = new Set(docs.map((p) => p._id))
for (const rosamund of rosamunds) {
if (!currentEmployees.has(rosamund.attributes.employee)) await client.delete(DOMAIN_TX, rosamund._id)
}
},
async upgrade (client: MigrationUpgradeClient): Promise<void> { async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System) const ops = new TxOperations(client, core.account.System)
const current = await tx.findOne(contact.class.EmployeeAccount, { const tx = await ops.findOne(core.class.TxCreateDoc, {
objectClass: contact.class.EmployeeAccount,
'attributes.email': 'rosamund@hc.engineering'
})
const current = await ops.findOne(contact.class.EmployeeAccount, {
email: 'rosamund@hc.engineering' email: 'rosamund@hc.engineering'
}) })
if (current === undefined) { if (tx === undefined && current === undefined) {
const employee = await tx.createDoc(contact.class.Employee, contact.space.Employee, { const employee = await ops.createDoc(contact.class.Employee, contact.space.Employee, {
name: 'Chen,Rosamund', name: 'Chen,Rosamund',
city: 'Mountain View', city: 'Mountain View',
active: true active: true
}) })
await tx.createDoc<EmployeeAccount>(contact.class.EmployeeAccount, core.space.Model, { await ops.createDoc<EmployeeAccount>(contact.class.EmployeeAccount, core.space.Model, {
email: 'rosamund@hc.engineering', email: 'rosamund@hc.engineering',
employee, employee,
name: 'Chen,Rosamund', name: 'Chen,Rosamund',
@ -66,8 +84,8 @@ export const demoOperation: MigrateOperation = {
}) })
} }
await createCandidate(tx, 'P.,Andrey', 'Monte Carlo', 'andrey@hc.engineering', 'Chief Architect') await createCandidate(ops, 'P.,Andrey', 'Monte Carlo', 'andrey@hc.engineering', 'Chief Architect')
await createCandidate(tx, 'M.,Marina', 'Los Angeles', 'marina@hc.engineering', 'Chief Designer') await createCandidate(ops, 'M.,Marina', 'Los Angeles', 'marina@hc.engineering', 'Chief Designer')
await createCandidate(tx, 'P.,Alex', 'Krasnodar, Russia', 'alex@hc.engineering', 'Frontend Engineer') await createCandidate(ops, 'P.,Alex', 'Krasnodar, Russia', 'alex@hc.engineering', 'Frontend Engineer')
} }
} }

View File

@ -170,6 +170,7 @@ export function createModel (builder: Builder): void {
icon: notification.icon.Notifications, icon: notification.icon.Notifications,
component: notification.component.NotificationSettings, component: notification.component.NotificationSettings,
group: 'settings', group: 'settings',
secured: false,
order: 2500 order: 2500
}, },
notification.ids.NotificationSettings notification.ids.NotificationSettings

View File

@ -84,7 +84,7 @@
} }
okProcessing = true okProcessing = true
const r = okAction() const r = okAction()
if (r instanceof Promise && !createMore) { if (r instanceof Promise && createMore) {
r.then(() => { r.then(() => {
okProcessing = false okProcessing = false
dispatch('close') dispatch('close')

View File

@ -67,6 +67,7 @@
"KickEmployee": "Kick an employee", "KickEmployee": "Kick an employee",
"KickEmployeeDescr": "Are you sure you want to kick the employee out of the workspace? This action cannot be undone", "KickEmployeeDescr": "Are you sure you want to kick the employee out of the workspace? This action cannot be undone",
"Email": "Email", "Email": "Email",
"CreateEmployee": "Create an employee" "CreateEmployee": "Create an employee",
"Inactive": "Inactive"
} }
} }

View File

@ -67,6 +67,7 @@
"KickEmployee": "Исключить сотрудника", "KickEmployee": "Исключить сотрудника",
"KickEmployeeDescr": "Вы действительно хотите выгнать сотрудника из рабочего пространства? Это действие нельзя отменить", "KickEmployeeDescr": "Вы действительно хотите выгнать сотрудника из рабочего пространства? Это действие нельзя отменить",
"Email": "Email", "Email": "Email",
"CreateEmployee": "Создать сотрудника" "CreateEmployee": "Создать сотрудника",
"Inactive": "Не активный"
} }
} }

View File

@ -14,8 +14,10 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Contact, Organization } from '@anticrm/contact' import { Contact, Employee, Organization } from '@anticrm/contact'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import { Label } from '@anticrm/ui'
import contact from '../plugin'
import OrganizationPresenter from './OrganizationPresenter.svelte' import OrganizationPresenter from './OrganizationPresenter.svelte'
import PersonPresenter from './PersonPresenter.svelte' import PersonPresenter from './PersonPresenter.svelte'
@ -28,11 +30,22 @@
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
return hierarchy.isDerived(value._class, contact.class.Person) return hierarchy.isDerived(value._class, contact.class.Person)
} }
function isEmployee (value: Contact): boolean {
const client = getClient()
const hierarchy = client.getHierarchy()
return hierarchy.isDerived(value._class, contact.class.Employee)
}
const toOrg = (contact: Contact) => contact as Organization const toOrg = (contact: Contact) => contact as Organization
const toEmployee = (contact: Contact) => contact as Employee
</script> </script>
{#if isPerson(value)} {#if isPerson(value)}
<PersonPresenter {isInteractive} {value} /> <PersonPresenter {isInteractive} {value} />
{#if isEmployee(value) && toEmployee(value)?.active === false}
<div class="ml-1">
(<Label label={contact.string.Inactive} />)
</div>
{/if}
{:else} {:else}
<OrganizationPresenter value={toOrg(value)} /> <OrganizationPresenter value={toOrg(value)} />
{/if} {/if}

View File

@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Employee } from '@anticrm/contact' import { Employee } from '@anticrm/contact'
import EmployeeStatusPresenter from './EmployeeStatusPresenter.svelte'
import PersonPresenter from '../components/PersonPresenter.svelte'
import { showPopup } from '@anticrm/ui'
import EmployeePreviewPopup from './EmployeePreviewPopup.svelte'
import { WithLookup } from '@anticrm/core' import { WithLookup } from '@anticrm/core'
import { IntlString } from '@anticrm/platform' import { IntlString } from '@anticrm/platform'
import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui' import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui'
import { showPopup } from '@anticrm/ui'
import PersonPresenter from '../components/PersonPresenter.svelte'
import EmployeePreviewPopup from './EmployeePreviewPopup.svelte'
import EmployeeStatusPresenter from './EmployeeStatusPresenter.svelte'
export let value: WithLookup<Employee> | null | undefined export let value: WithLookup<Employee> | null | undefined
export let tooltipLabels: export let tooltipLabels:

View File

@ -60,6 +60,7 @@ export default mergeIds(contactId, contact, {
KickEmployee: '' as IntlString, KickEmployee: '' as IntlString,
KickEmployeeDescr: '' as IntlString, KickEmployeeDescr: '' as IntlString,
Email: '' as IntlString, Email: '' as IntlString,
CreateEmployee: '' as IntlString CreateEmployee: '' as IntlString,
Inactive: '' as IntlString
} }
}) })

View File

@ -49,7 +49,7 @@
<span class="antiSection-header__title"> <span class="antiSection-header__title">
<Label label={recruit.string.Applications} /> <Label label={recruit.string.Applications} />
</span> </span>
<Button icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={createApp} /> <Button id="appls.add" icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={createApp} />
</div> </div>
{#if applications > 0} {#if applications > 0}
<Table <Table

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Employee, EmployeeAccount } from '@anticrm/contact' import contact, { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
import { PersonPresenter } from '@anticrm/contact-resources' import { PersonPresenter } from '@anticrm/contact-resources'
import { AccountRole, getCurrentAccount, Ref, SortingOrder } from '@anticrm/core' import { AccountRole, getCurrentAccount, Ref, SortingOrder } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation' import { createQuery, getClient } from '@anticrm/presentation'
@ -71,8 +71,13 @@
<div class="ac-body columns"> <div class="ac-body columns">
<div class="ac-column max"> <div class="ac-column max">
{#each accounts as account (account._id)} {#each accounts as account (account._id)}
{@const employee = employees.get(account.employee)}
<div class="flex-between"> <div class="flex-between">
<PersonPresenter value={employees.get(account.employee)} isInteractive={false} /> {#if employee}
<PersonPresenter value={employee} isInteractive={false} />
{:else}
{formatName(account.name)}
{/if}
<DropdownLabelsIntl <DropdownLabelsIntl
label={setting.string.Role} label={setting.string.Role}
disabled={account.role > currentRole || (account.role === AccountRole.Owner && owners.length === 1)} disabled={account.role > currentRole || (account.role === AccountRole.Owner && owners.length === 1)}

View File

@ -17,7 +17,8 @@
import { AccountRole, getCurrentAccount } from '@anticrm/core' import { AccountRole, getCurrentAccount } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation' import { createQuery } from '@anticrm/presentation'
import setting, { SettingsCategory } from '@anticrm/setting' import setting, { SettingsCategory } from '@anticrm/setting'
import { Component, Label } from '@anticrm/ui' import { Component, getCurrentLocation, Label, location, navigate } from '@anticrm/ui'
import { onDestroy } from 'svelte'
import CategoryElement from './CategoryElement.svelte' import CategoryElement from './CategoryElement.svelte'
let category: SettingsCategory | undefined let category: SettingsCategory | undefined
@ -41,9 +42,18 @@
return categories.find((x) => x.name === name) return categories.find((x) => x.name === name)
} }
function selectCategory (value: SettingsCategory) { onDestroy(
categoryId = value.name location.subscribe(async (loc) => {
category = value categoryId = loc.path[3]
category = findCategory(categoryId)
})
)
function selectCategory (id: string): void {
const loc = getCurrentLocation()
loc.path[3] = id
loc.path.length = 4
navigate(loc)
} }
</script> </script>
@ -60,7 +70,7 @@
label={category.label} label={category.label}
selected={category.name === categoryId} selected={category.name === categoryId}
on:click={() => { on:click={() => {
selectCategory(category) selectCategory(category.name)
}} }}
/> />
{/each} {/each}

View File

@ -37,9 +37,11 @@
} }
async function newIssue (): Promise<void> { async function newIssue (): Promise<void> {
if (space) { if (!space) {
showPopup(CreateIssue, { space }, 'top') const team = await client.findOne(tracker.class.Team, {})
space = team?._id
} }
showPopup(CreateIssue, { space }, 'top')
} }
</script> </script>

View File

@ -36,39 +36,32 @@
let defaultProjectLabel = '' let defaultProjectLabel = ''
const query = createQuery() const query = createQuery()
let projects: Map<Ref<Project>, Project> = new Map<Ref<Project>, Project>() let rawProjects: Project[] = []
query.query( query.query(
tracker.class.Project, tracker.class.Project,
{}, {},
(res) => { (res) => {
projects = new Map( rawProjects = res
res.map((p) => {
return [p._id, p]
})
)
}, },
{ {
sort: { modifiedOn: SortingOrder.Ascending } sort: { modifiedOn: SortingOrder.Ascending }
} }
) )
$: handleSelectedProjectIdUpdated(value, projects) $: handleSelectedProjectIdUpdated(value, rawProjects)
$: translate(tracker.string.Project, {}).then((result) => (defaultProjectLabel = result)) $: translate(tracker.string.Project, {}).then((result) => (defaultProjectLabel = result))
$: projectIcon = selectedProject?.icon ?? tracker.icon.Projects $: projectIcon = selectedProject?.icon ?? tracker.icon.Projects
$: projectText = shouldShowLabel ? selectedProject?.label ?? defaultProjectLabel : undefined $: projectText = shouldShowLabel ? selectedProject?.label ?? defaultProjectLabel : undefined
const handleSelectedProjectIdUpdated = async ( const handleSelectedProjectIdUpdated = async (newProjectId: Ref<Project> | null | undefined, projects: Project[]) => {
newProjectId: Ref<Project> | null | undefined,
projects: Map<Ref<Project>, Project>
) => {
if (newProjectId === null || newProjectId === undefined) { if (newProjectId === null || newProjectId === undefined) {
selectedProject = undefined selectedProject = undefined
return return
} }
selectedProject = projects.get(newProjectId) selectedProject = projects.find((it) => it._id === newProjectId)
} }
const handleProjectEditorOpened = async (event: MouseEvent): Promise<void> => { const handleProjectEditorOpened = async (event: MouseEvent): Promise<void> => {
@ -79,7 +72,7 @@
const projectsInfo = [ const projectsInfo = [
{ id: null, icon: tracker.icon.Projects, label: tracker.string.NoProject }, { id: null, icon: tracker.icon.Projects, label: tracker.string.NoProject },
...Array.from(projects.values()).map((p) => ({ ...rawProjects.map((p) => ({
id: p._id, id: p._id,
icon: p.icon, icon: p.icon,
text: p.label text: p.label

View File

@ -66,8 +66,9 @@
ev.stopPropagation() ev.stopPropagation()
const loc = getCurrentLocation() const loc = getCurrentLocation()
loc.path[1] = settingId loc.path[1] = settingId
loc.path[2] = 'classes' loc.path[2] = 'setting'
loc.path.length = 3 loc.path[3] = 'classes'
loc.path.length = 4
loc.query = { _class } loc.query = { _class }
loc.fragment = undefined loc.fragment = undefined
navigate(loc) navigate(loc)

View File

@ -457,8 +457,9 @@ export async function setRole (email: string, workspace: string, role: AccountRo
const existingAccount = await ops.findOne(contact.class.EmployeeAccount, { email }) const existingAccount = await ops.findOne(contact.class.EmployeeAccount, { email })
if (existingAccount !== undefined) { if (existingAccount !== undefined) {
const value = isNaN(Number(role)) ? 0 : Number(role)
await ops.update(existingAccount, { await ops.update(existingAccount, {
role role: value
}) })
} }
} finally { } finally {

View File

@ -78,7 +78,7 @@ test.describe('recruit tests', () => {
// Click on Add button // Click on Add button
// await page.click('.applications-container .flex-row-center .flex-center') // await page.click('.applications-container .flex-row-center .flex-center')
await page.click('text=Applications There are no applications for this talent. New Application >> button') await page.click('button[id="appls.add"]')
await page.click('button:has-text("Vacancy")') await page.click('button:has-text("Vacancy")')

View File

@ -44,13 +44,13 @@ test.describe('contact tests', () => {
// Click button:has-text("Settings") // Click button:has-text("Settings")
await page.hover('button:has-text("Settings")') await page.hover('button:has-text("Settings")')
await page.click('button:has-text("Settings")') await page.click('button:has-text("Settings")')
// Click text=Workspace Integrations >> button // Click text=Workspace Notifications >> button
await page.click('text=Workspace Integrations >> button') await page.click('text=Workspace Notifications >> button')
await page.click('text=Templates') await page.click('text=Templates')
// Click .flex-center.icon-button // Click .flex-center.icon-button
await page.click('#create-template >> .flex-center.icon-button') await page.click('#create-template >> .flex-center.icon-button')
// Click [placeholder="New\ template"] // Click [placeholder="New\ template"]
await page.click('[placeholder="New\\ template"]') // await page.click('[placeholder="New\\ template"]')
// Fill [placeholder="New\ template"] // Fill [placeholder="New\ template"]
await page.fill('[placeholder="New\\ template"]', 't1') await page.fill('[placeholder="New\\ template"]', 't1')
@ -77,7 +77,8 @@ test.describe('contact tests', () => {
// await page.click('text=Workspace') // await page.click('text=Workspace')
await page.hover('button:has-text("Settings")') await page.hover('button:has-text("Settings")')
await page.click('button:has-text("Settings")') await page.click('button:has-text("Settings")')
await page.click('text=Workspace Integrations >> button') // Click text=Workspace Notifications >> button
await page.click('text=Workspace Notifications >> button')
// Click button:has-text("Manage Statuses") // Click button:has-text("Manage Statuses")
await page.click('text="Manage Statuses"') await page.click('text="Manage Statuses"')
// Click text=Vacancies // Click text=Vacancies

View File

@ -49,6 +49,7 @@ async function fillIssueForm (
} }
async function createIssue (page: Page, props: IssueProps): Promise<void> { async function createIssue (page: Page, props: IssueProps): Promise<void> {
await page.waitForSelector('span:has-text("Default")')
await page.click('button:has-text("New issue")') await page.click('button:has-text("New issue")')
await fillIssueForm(page, props) await fillIssueForm(page, props)
await page.click('button:has-text("Save issue")') await page.click('button:has-text("Save issue")')

View File

@ -12,7 +12,7 @@ function toHex (value: number, chars: number): string {
return result return result
} }
let counter = (Math.random() * (1 << 24)) | 0 let counter = 0
const random = toHex((Math.random() * (1 << 24)) | 0, 6) + toHex((Math.random() * (1 << 16)) | 0, 4) const random = toHex((Math.random() * (1 << 24)) | 0, 6) + toHex((Math.random() * (1 << 16)) | 0, 4)
function timestamp (): string { function timestamp (): string {