Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-07-01 21:22:58 +06:00 committed by GitHub
parent 25afe12cc2
commit efc3583376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 502 additions and 229 deletions

View File

@ -5,9 +5,10 @@
Core:
- Allow to leave workspace
- Allow to kick employee
- Allow to kick employee (Only for owner)
- Browser notifications
- Allow to create employee
- Owner role for employee
HR:

View File

@ -23,6 +23,7 @@ import {
dropWorkspace,
getAccount,
getWorkspace,
setRole,
listAccounts,
listWorkspaces,
upgradeWorkspace
@ -114,6 +115,14 @@ program
})
})
program
.command('set-user-role <email> <workspace> <role>')
.description('set user role')
.action(async (email: string, workspace: string, role: number, cmd) => {
console.log(`set user ${email} role for ${workspace}...`)
await setRole(email, workspace, role)
})
program
.command('upgrade-workspace <name>')
.description('upgrade workspace')

View File

@ -71,14 +71,15 @@ export function createModel (builder: Builder): void {
/** Disable Automation UI
builder.createDoc(
setting.class.SettingsCategory,
setting.class.WorkspaceSettingCategory,
core.space.Model,
{
name: 'automation',
label: automation.string.Automation,
icon: automation.icon.Automation, // TODO: update icon
component: plugin.component.AutomationSettingsElement,
order: 3600
order: 3600,
secured: false
},
plugin.ids.Automation
)

View File

@ -420,7 +420,8 @@ export function createModel (builder: Builder): void {
context: {
mode: ['context'],
group: 'other'
}
},
secured: true
},
contact.action.KickEmployee
)

View File

@ -1,6 +1,5 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
@ -14,7 +13,8 @@
// limitations under the License.
//
import { TxOperations } from '@anticrm/core'
import { Employee, EmployeeAccount } from '@anticrm/contact'
import { AccountRole, DOMAIN_TX, TxCreateDoc, TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import core from '@anticrm/model-core'
import contact, { DOMAIN_CONTACT } from './index'
@ -66,11 +66,40 @@ async function setActiveEmployee (client: MigrationClient): Promise<void> {
active: true
}
)
await setActiveEmployeeTx(client)
}
async function setActiveEmployeeTx (client: MigrationClient): Promise<void> {
await client.update<TxCreateDoc<Employee>>(
DOMAIN_TX,
{
_class: core.class.TxCreateDoc,
objectClass: contact.class.Employee,
'attributes.active': { $exists: false }
},
{
'attributes.active': true
}
)
}
async function setRole (client: MigrationClient): Promise<void> {
await client.update<TxCreateDoc<EmployeeAccount>>(
DOMAIN_TX,
{
_class: core.class.TxCreateDoc,
objectClass: contact.class.Employee
},
{
'attributes.role': AccountRole.User
}
)
}
export const contactOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await setActiveEmployee(client)
await setRole(client)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import { Account, Arr, Domain, DOMAIN_MODEL, IndexKind, Ref, Space } from '@anticrm/core'
import { Account, AccountRole, Arr, Domain, DOMAIN_MODEL, IndexKind, Ref, Space } from '@anticrm/core'
import { Index, Model, Prop, TypeBoolean, TypeString, UX } from '@anticrm/model'
import core from './component'
import { TDoc } from './core'
@ -45,4 +45,5 @@ export class TSpace extends TDoc implements Space {
@Model(core.class.Account, core.class.Doc, DOMAIN_MODEL)
export class TAccount extends TDoc implements Account {
email!: string
role!: AccountRole
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import core, { TxOperations } from '@anticrm/core'
import core, { AccountRole, TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import contact, { EmployeeAccount } from '@anticrm/contact'
import recruit from '@anticrm/model-recruit'
@ -61,7 +61,8 @@ export const demoOperation: MigrateOperation = {
await tx.createDoc<EmployeeAccount>(contact.class.EmployeeAccount, core.space.Model, {
email: 'rosamund@hc.engineering',
employee,
name: 'Chen,Rosamund'
name: 'Chen,Rosamund',
role: AccountRole.Owner
})
}

View File

@ -40,6 +40,16 @@ export class TSettingsCategory extends TDoc implements SettingsCategory {
label!: IntlString
icon!: Asset
component!: AnyComponent
secured!: boolean
}
@Model(setting.class.WorkspaceSettingCategory, core.class.Doc, DOMAIN_MODEL)
export class TWorkspaceSettingCategory extends TDoc implements SettingsCategory {
name!: string
label!: IntlString
icon!: Asset
component!: AnyComponent
secured!: boolean
}
@Model(setting.class.IntegrationType, core.class.Doc, DOMAIN_MODEL)
@ -56,7 +66,7 @@ export class TIntegrationType extends TDoc implements IntegrationType {
export class TEditable extends TClass implements Editable {}
export function createModel (builder: Builder): void {
builder.createModel(TIntegration, TIntegrationType, TSettingsCategory, TEditable)
builder.createModel(TIntegration, TIntegrationType, TSettingsCategory, TWorkspaceSettingCategory, TEditable)
builder.createDoc(
setting.class.SettingsCategory,
@ -66,7 +76,8 @@ export function createModel (builder: Builder): void {
label: setting.string.EditProfile,
icon: setting.icon.EditProfile,
component: setting.component.Profile,
order: 0
order: 0,
secured: false
},
setting.ids.Profile
)
@ -79,7 +90,8 @@ export function createModel (builder: Builder): void {
label: setting.string.ChangePassword,
icon: setting.icon.Password,
component: setting.component.Password,
order: 1000
order: 1000,
secured: false
},
setting.ids.Password
)
@ -88,10 +100,11 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
name: 'setting',
label: setting.string.Setting,
label: setting.string.WorkspaceSetting,
icon: setting.icon.Setting,
component: setting.component.Setting,
order: 2000
component: setting.component.WorkspaceSettings,
order: 2000,
secured: false
},
setting.ids.Setting
)
@ -103,43 +116,60 @@ export function createModel (builder: Builder): void {
label: setting.string.Integrations,
icon: setting.icon.Integrations,
component: setting.component.Integrations,
order: 3000
order: 3000,
secured: false
},
setting.ids.Integrations
)
builder.createDoc(
setting.class.SettingsCategory,
setting.class.WorkspaceSettingCategory,
core.space.Model,
{
name: 'owners',
label: setting.string.Owners,
icon: setting.icon.Password,
component: setting.component.Owners,
order: 1000,
secured: true
},
setting.ids.Owners
)
builder.createDoc(
setting.class.WorkspaceSettingCategory,
core.space.Model,
{
name: 'statuses',
label: setting.string.ManageStatuses,
icon: task.icon.ManageStatuses,
component: setting.component.ManageStatuses,
order: 4000
order: 4000,
secured: false
},
setting.ids.ManageStatuses
)
builder.createDoc(
setting.class.SettingsCategory,
setting.class.WorkspaceSettingCategory,
core.space.Model,
{
name: 'classes',
label: setting.string.ClassSetting,
icon: setting.icon.Setting,
component: setting.component.ClassSetting,
order: 4500
order: 4500,
secured: false
},
setting.ids.ClassSetting
)
builder.createDoc(
setting.class.SettingsCategory,
setting.class.WorkspaceSettingCategory,
core.space.Model,
{
name: 'enums',
label: setting.string.Enums,
icon: setting.icon.Setting,
component: setting.component.EnumSetting,
order: 4600
order: 4600,
secured: false
},
setting.ids.EnumSetting
)
@ -151,7 +181,8 @@ export function createModel (builder: Builder): void {
label: setting.string.Support,
icon: setting.icon.Support,
component: setting.component.Support,
order: 5000
order: 5000,
secured: false
},
setting.ids.Support
)
@ -163,7 +194,8 @@ export function createModel (builder: Builder): void {
label: setting.string.Privacy,
icon: setting.icon.Privacy,
component: setting.component.Privacy,
order: 6000
order: 6000,
secured: false
},
setting.ids.Privacy
)
@ -175,7 +207,8 @@ export function createModel (builder: Builder): void {
label: setting.string.Terms,
icon: setting.icon.Terms,
component: setting.component.Terms,
order: 10000
order: 10000,
secured: false
},
setting.ids.Terms
)

View File

@ -36,6 +36,7 @@ export default mergeIds(settingId, setting, {
NumberTypeEditor: '' as AnyComponent,
DateTypeEditor: '' as AnyComponent,
RefEditor: '' as AnyComponent,
EnumTypeEditor: '' as AnyComponent
EnumTypeEditor: '' as AnyComponent,
Owners: '' as AnyComponent
}
})

View File

@ -39,14 +39,15 @@ export function createModel (builder: Builder): void {
builder.createModel(TMessageTemplate)
builder.createDoc(
setting.class.SettingsCategory,
setting.class.WorkspaceSettingCategory,
core.space.Model,
{
name: 'message-templates',
label: templates.string.Templates,
icon: templates.icon.Templates,
component: templates.component.Templates,
order: 3500
order: 3500,
secured: false
},
templates.ids.Templates
)

View File

@ -202,6 +202,7 @@ export class TAction extends TDoc implements Action {
description!: IntlString
category!: Ref<ActionCategory>
context!: ViewContext
secured?: boolean
}
@Model(view.class.ActionCategory, core.class.Doc, DOMAIN_MODEL)

View File

@ -236,7 +236,7 @@ describe('memdb', () => {
members: [],
archived: false
})
const account = await model.createDoc(core.class.Account, core.space.Model, { email: 'email' })
const account = await model.createDoc(core.class.Account, core.space.Model, { email: 'email', role: 0 })
await model.updateDoc(core.class.Space, core.space.Model, space, { $push: { members: account } })
const txSpace = await model.findAll(core.class.Space, { _id: space })
expect(txSpace[0].members).toEqual(expect.arrayContaining([account]))

View File

@ -279,6 +279,16 @@ export interface Space extends Doc {
*/
export interface Account extends Doc {
email: string
role: AccountRole
}
/**
* @public
*/
export enum AccountRole {
User,
Maintainer,
Owner
}
/**

View File

@ -194,8 +194,8 @@ export function genMinModel (): TxCUD<Doc>[] {
const u1 = 'User1' as Ref<Account>
const u2 = 'User2' as Ref<Account>
txes.push(
createDoc(core.class.Account, { email: 'user1@site.com' }, u1),
createDoc(core.class.Account, { email: 'user2@site.com' }, u2),
createDoc(core.class.Account, { email: 'user1@site.com', role: 0 }, u1),
createDoc(core.class.Account, { email: 'user2@site.com', role: 0 }, u2),
createDoc(core.class.Space, {
name: 'Sp1',
description: '',

View File

@ -100,7 +100,8 @@ describe('query', () => {
})
await factory.createDoc(core.class.Account, core.space.Model, {
email: 'user1@site.com'
email: 'user1@site.com',
role: 0
})
await factory.createDoc<Channel>(core.class.Space, core.space.Model, {
private: true,

View File

@ -16,7 +16,6 @@
import { IntlString, Asset } from '@anticrm/platform'
import { createEventDispatcher } from 'svelte'
import type { AnySvelteComponent, TooltipAlignment, ButtonKind, ButtonSize, DropdownIntlItem } from '../types'
import ui from '../plugin'
import { showPopup } from '../popups'
import Button from './Button.svelte'
import DropdownLabelsPopupIntl from './DropdownLabelsPopupIntl.svelte'
@ -24,10 +23,9 @@
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let label: IntlString
export let placeholder: IntlString | undefined = ui.string.SearchDots
export let items: DropdownIntlItem[]
export let selected: DropdownIntlItem['id'] | undefined = undefined
export let disabled: boolean = false
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
@ -52,12 +50,13 @@
width={width ?? 'min-content'}
{size}
{kind}
{disabled}
{justify}
showTooltip={{ label, direction: labelDirection }}
on:click={() => {
if (!opened) {
opened = true
showPopup(DropdownLabelsPopupIntl, { placeholder, items, selected }, container, (result) => {
showPopup(DropdownLabelsPopupIntl, { items, selected }, container, (result) => {
if (result) {
selected = result
dispatch('selected', result)
@ -68,11 +67,7 @@
}}
>
<span slot="content" class="overflow-label disabled">
{#if selectedItem}
<Label label={selectedItem.label} />
{:else}
<Label label={label ?? ui.string.NotSelected} />
{/if}
<Label label={selectedItem ? selectedItem.label : label} />
</span>
</Button>
</div>

View File

@ -13,26 +13,16 @@
// limitations under the License.
-->
<script lang="ts">
import type { IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform'
import { createEventDispatcher, onMount } from 'svelte'
import plugin from '../plugin'
import { createEventDispatcher } from 'svelte'
import type { DropdownIntlItem } from '../types'
import CheckBox from './CheckBox.svelte'
import Label from './Label.svelte'
export let placeholder: IntlString = plugin.string.SearchDots
export let items: DropdownIntlItem[]
export let selected: DropdownIntlItem['id'] | undefined = undefined
let search: string = ''
let phTraslate: string = ''
$: translate(placeholder, {}).then((res) => {
phTraslate = res
})
const dispatch = createEventDispatcher()
const btns: HTMLButtonElement[] = []
let searchInput: HTMLInputElement
const keyDown = (ev: KeyboardEvent, n: number): void => {
if (ev.key === 'ArrowDown') {
@ -41,28 +31,14 @@
} else if (ev.key === 'ArrowUp') {
if (n === 0) btns[btns.length - 1].focus()
else btns[n - 1].focus()
} else searchInput.focus()
}
}
onMount(() => {
if (searchInput) searchInput.focus()
})
</script>
<div class="selectPopup">
<div class="header">
<input
bind:this={searchInput}
type="text"
bind:value={search}
placeholder={phTraslate}
on:input={(ev) => {}}
on:change
/>
</div>
<div class="scroll">
<div class="box">
{#each items.filter((x) => x.label.toLowerCase().includes(search.toLowerCase())) as item, i}
{#each items as item, i}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
class="menu-item flex-between"

View File

@ -51,7 +51,6 @@
<div class="simpleFilterButton">
<DropdownLabelsIntl
items={dateFileBrowserFilters}
placeholder={attachment.string.FileBrowserFilterDate}
label={attachment.string.FileBrowserFilterDate}
bind:selected={selectedDateId}
/>
@ -59,7 +58,6 @@
<div class="simpleFilterButton">
<DropdownLabelsIntl
items={fileTypeFileBrowserFilters}
placeholder={attachment.string.FileBrowserFilterFileType}
label={attachment.string.FileBrowserFilterFileType}
bind:selected={selectedFileTypeId}
/>

View File

@ -15,7 +15,7 @@
<script lang="ts">
import attachment from '@anticrm/attachment'
import { Channel, combineName, Employee, findPerson, Person } from '@anticrm/contact'
import core, { AttachedData, Data, generateId, Ref } from '@anticrm/core'
import core, { AccountRole, AttachedData, Data, generateId, Ref } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { Card, EditableAvatar, getClient } from '@anticrm/presentation'
import { EditBox, IconInfo, Label, createFocusManager, FocusHandler } from '@anticrm/ui'
@ -69,7 +69,8 @@
await client.createDoc(contact.class.EmployeeAccount, core.space.Model, {
email: email.trim(),
name,
employee: id
employee: id,
role: AccountRole.User
})
for (const channel of channels) {

View File

@ -58,6 +58,7 @@ export {
OrganizationSelector,
ChannelsDropdown,
EmployeePresenter,
PersonPresenter,
EmployeeBrowser,
MemberPresenter,
EmployeeEditor

View File

@ -45,6 +45,14 @@
"Enums": "Enums",
"NewValue": "New value",
"Leave": "Leave workspace",
"LeaveDescr": "Are you sure you want to leave the workspace? This action cannot be undone."
"LeaveDescr": "Are you sure you want to leave the workspace? This action cannot be undone.",
"Owners": "Owners",
"WorkspaceSetting": "Workspace",
"Select": "Select",
"AddOwner": "Add owner",
"User": "User",
"Maintainer": "Maintainer",
"Owner": "Owner",
"Role": "Role"
}
}

View File

@ -45,6 +45,14 @@
"Enums": "Справочники",
"NewValue": "Новое значение",
"Leave": "Покинуть рабочее пространство",
"LeaveDescr": "Вы действительно хотите покинуть рабочее пространство? Отменить это действие невозможно"
"LeaveDescr": "Вы действительно хотите покинуть рабочее пространство? Отменить это действие невозможно",
"Owners": "Владельцы",
"WorkspaceSetting": "Рабочее пространство",
"Select": "Выбрать",
"AddOwner": "Добавить владельца",
"User": "Пользователь",
"Maintainer": "Maintainer",
"Owner": "Владелец",
"Role": "Роль"
}
}

View File

@ -20,6 +20,7 @@
export let icon: Asset | undefined = undefined
export let label: IntlString | undefined = undefined
export let selected: boolean = false
export let expandable = false
const dispatch = createEventDispatcher()
</script>
@ -28,6 +29,7 @@
<div
class="antiNav-element"
class:selected
class:expandable
on:click|stopPropagation={() => {
dispatch('click')
}}
@ -38,6 +40,21 @@
{/if}
</div>
<span class="an-element__label title">
{#if label}<Label {label} />{:else}{label}{/if}
{#if label}<Label {label} />{/if}
</span>
</div>
<style lang="scss">
.expandable {
position: relative;
&::after {
content: '▶';
position: absolute;
top: 50%;
right: 0.5rem;
font-size: 0.375rem;
color: var(--dark-color);
transform: translateY(-50%);
}
}
</style>

View File

@ -77,7 +77,7 @@
{/each}
</div>
</div>
<div class="ac-column">
<div class="ac-column max">
{#if selected !== undefined}
<EnumValues value={selected} />
{/if}

View File

@ -0,0 +1,91 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee, EmployeeAccount } from '@anticrm/contact'
import { PersonPresenter } from '@anticrm/contact-resources'
import { AccountRole, getCurrentAccount, Ref, SortingOrder } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { DropdownIntlItem, DropdownLabelsIntl, Icon, Label } from '@anticrm/ui'
import setting from '../plugin'
const client = getClient()
const query = createQuery()
const employeeQuery = createQuery()
const currentRole = getCurrentAccount().role
const items: DropdownIntlItem[] = [
{ id: AccountRole.User.toString(), label: setting.string.User },
{ id: AccountRole.Maintainer.toString(), label: setting.string.Maintainer },
{ id: AccountRole.Owner.toString(), label: setting.string.Owner }
]
let accounts: EmployeeAccount[] = []
$: owners = accounts.filter((p) => p.role === AccountRole.Owner)
let employees: Map<Ref<Employee>, Employee> = new Map<Ref<Employee>, Employee>()
query.query(
contact.class.EmployeeAccount,
{},
(res) => {
accounts = res
},
{
sort: { name: SortingOrder.Descending }
}
)
employeeQuery.query(contact.class.Employee, {}, (res) => {
employees = new Map(
res.map((p) => {
return [p._id, p]
})
)
})
async function change (account: EmployeeAccount, value: AccountRole): Promise<void> {
await client.update(account, {
role: value
})
}
</script>
<div class="antiComponent">
<div class="ac-header short divide">
<div class="ac-header__icon"><Icon icon={setting.icon.Password} size={'medium'} /></div>
<div class="ac-header__title"><Label label={setting.string.Owners} /></div>
</div>
<div class="ac-body columns">
<div class="ac-column max">
{#each accounts as account (account._id)}
<div class="flex-between">
<PersonPresenter value={employees.get(account.employee)} isInteractive={false} />
<DropdownLabelsIntl
label={setting.string.Role}
disabled={account.role > currentRole || (account.role === AccountRole.Owner && owners.length === 1)}
kind={'transparent'}
size={'medium'}
{items}
selected={account.role.toString()}
on:selected={(e) => {
change(account, Number(e.detail))
}}
/>
</div>
{/each}
</div>
</div>
</div>

View File

@ -1,25 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import setting from '@anticrm/setting'
import { Icon, Label } from '@anticrm/ui'
</script>
<div class="antiComponent">
<div class="ac-header short divide">
<div class="ac-header__icon"><Icon icon={setting.icon.Setting} size={'medium'} /></div>
<div class="ac-header__title"><Label label={setting.string.Setting} /></div>
</div>
</div>

View File

@ -1,5 +1,19 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { getClient } from '@anticrm/presentation'
import { createQuery } from '@anticrm/presentation'
import setting, { SettingsCategory } from '@anticrm/setting'
import {
Component,
@ -13,17 +27,25 @@
import { onDestroy } from 'svelte'
import CategoryElement from './CategoryElement.svelte'
import login from '@anticrm/login'
const client = getClient()
import { AccountRole, getCurrentAccount } from '@anticrm/core'
import { EmployeeAccount } from '@anticrm/contact'
let category: SettingsCategory | undefined
let categoryId: string = ''
let categories: SettingsCategory[] = []
client.findAll(setting.class.SettingsCategory, {}, { sort: { order: 1 } }).then((s) => {
categories = s
category = findCategory(categoryId)
})
const account = getCurrentAccount() as EmployeeAccount
const settingsQuery = createQuery()
settingsQuery.query(
setting.class.SettingsCategory,
{},
(res) => {
categories = account.role > AccountRole.User ? res : res.filter((p) => p.secured === false)
category = findCategory(categoryId)
},
{ sort: { order: 1 } }
)
onDestroy(
location.subscribe(async (loc) => {
@ -68,6 +90,7 @@
icon={category.icon}
label={category.label}
selected={category.name === categoryId}
expandable={category._id === setting.ids.Setting}
on:click={() => {
selectCategory(category.name)
}}

View File

@ -0,0 +1,74 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { EmployeeAccount } from '@anticrm/contact'
import { AccountRole, getCurrentAccount } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import setting, { SettingsCategory } from '@anticrm/setting'
import { Component, Label } from '@anticrm/ui'
import CategoryElement from './CategoryElement.svelte'
let category: SettingsCategory | undefined
let categoryId: string = ''
let categories: SettingsCategory[] = []
const account = getCurrentAccount() as EmployeeAccount
const settingsQuery = createQuery()
settingsQuery.query(
setting.class.WorkspaceSettingCategory,
{},
(res) => {
categories = account.role > AccountRole.User ? res : res.filter((p) => p.secured === false)
category = findCategory(categoryId)
},
{ sort: { order: 1 } }
)
function findCategory (name: string): SettingsCategory | undefined {
return categories.find((x) => x.name === name)
}
function selectCategory (value: SettingsCategory) {
categoryId = value.name
category = value
}
</script>
<div class="flex h-full">
<div class="antiPanel-navigator filled indent">
<div class="antiNav-header">
<span class="fs-title overflow-label">
<Label label={setting.string.WorkspaceSetting} />
</span>
</div>
{#each categories as category}
<CategoryElement
icon={category.icon}
label={category.label}
selected={category.name === categoryId}
on:click={() => {
selectCategory(category)
}}
/>
{/each}
</div>
<div class="antiPanel-component border-left filled">
{#if category}
<Component is={category.component} />
{/if}
</div>
</div>

View File

@ -16,7 +16,7 @@
import { Resources } from '@anticrm/platform'
import Profile from './components/Profile.svelte'
import Password from './components/Password.svelte'
import Setting from './components/Setting.svelte'
import WorkspaceSettings from './components/WorkspaceSettings.svelte'
import Integrations from './components/Integrations.svelte'
import ManageStatuses from './components/statuses/ManageStatuses.svelte'
import Support from './components/Support.svelte'
@ -34,6 +34,7 @@ import RefEditor from './components/typeEditors/RefEditor.svelte'
import EnumTypeEditor from './components/typeEditors/EnumTypeEditor.svelte'
import EditEnum from './components/EditEnum.svelte'
import EnumSetting from './components/EnumSetting.svelte'
import Owners from './components/Owners.svelte'
export default async (): Promise<Resources> => ({
activity: {
@ -44,7 +45,7 @@ export default async (): Promise<Resources> => ({
Settings,
Profile,
Password,
Setting,
WorkspaceSettings,
Integrations,
Support,
Privacy,
@ -58,6 +59,7 @@ export default async (): Promise<Resources> => ({
DateTypeEditor,
EnumTypeEditor,
EditEnum,
EnumSetting
EnumSetting,
Owners
}
})

View File

@ -41,6 +41,12 @@ export default mergeIds(settingId, setting, {
Enums: '' as IntlString,
NewValue: '' as IntlString,
Leave: '' as IntlString,
LeaveDescr: '' as IntlString
LeaveDescr: '' as IntlString,
Select: '' as IntlString,
AddOwner: '' as IntlString,
User: '' as IntlString,
Maintainer: '' as IntlString,
Owner: '' as IntlString,
Role: '' as IntlString
}
})

View File

@ -60,6 +60,7 @@ export interface SettingsCategory extends Doc {
// If defined, will sort using order.
order?: number
secured: boolean
}
/**
@ -78,13 +79,15 @@ export default plugin(settingId, {
Support: '' as Ref<Doc>,
Privacy: '' as Ref<Doc>,
Terms: '' as Ref<Doc>,
ClassSetting: '' as Ref<Doc>
ClassSetting: '' as Ref<Doc>,
Owners: '' as Ref<Doc>
},
mixin: {
Editable: '' as Ref<Mixin<Editable>>
},
class: {
SettingsCategory: '' as Ref<Class<SettingsCategory>>,
WorkspaceSettingCategory: '' as Ref<Class<SettingsCategory>>,
Integration: '' as Ref<Class<Integration>>,
IntegrationType: '' as Ref<Class<IntegrationType>>
},
@ -92,7 +95,7 @@ export default plugin(settingId, {
Settings: '' as AnyComponent,
Profile: '' as AnyComponent,
Password: '' as AnyComponent,
Setting: '' as AnyComponent,
WorkspaceSettings: '' as AnyComponent,
Integrations: '' as AnyComponent,
ManageStatuses: '' as AnyComponent,
Support: '' as AnyComponent,
@ -103,6 +106,7 @@ export default plugin(settingId, {
string: {
Settings: '' as IntlString,
Setting: '' as IntlString,
WorkspaceSetting: '' as IntlString,
Integrations: '' as IntlString,
ManageStatuses: '' as IntlString,
Support: '' as IntlString,
@ -127,7 +131,8 @@ export default plugin(settingId, {
InviteWorkspace: '' as IntlString,
SelectWorkspace: '' as IntlString,
Reconnect: '' as IntlString,
ClassSetting: '' as IntlString
ClassSetting: '' as IntlString,
Owners: '' as IntlString
},
icon: {
EditProfile: '' as Asset,

View File

@ -14,8 +14,7 @@
// limitations under the License.
//
import type { Doc, WithLookup } from '@anticrm/core'
import core, { Class, Client, matchQuery, Ref } from '@anticrm/core'
import core, { AccountRole, Doc, getCurrentAccount, WithLookup, Class, Client, matchQuery, Ref } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import type { Action, ViewAction, ViewActionInput, ViewContextType } from '@anticrm/view'
import view from './plugin'
@ -118,6 +117,7 @@ export function filterActions (
): Array<WithLookup<Action>> {
let result: Array<WithLookup<Action>> = []
const hierarchy = client.getHierarchy()
const role = getCurrentAccount().role
const clazz = hierarchy.getClass(doc._class)
const ignoreActions = hierarchy.as(clazz, view.mixin.IgnoreActions)
const ignore = ignoreActions?.actions ?? []
@ -126,6 +126,9 @@ export function filterActions (
if (ignore.includes(action._id)) {
continue
}
if (role < AccountRole.Maintainer && action.secured === true) {
continue
}
if (action.override !== undefined) {
overrideRemove.push(...action.override)
}

View File

@ -22,7 +22,7 @@
export let value: any
export let withoutUndefined: boolean = false
export let onChange: (value: any) => void
export let disabled: boolean = false
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
@ -36,8 +36,9 @@
{size}
{justify}
{width}
{disabled}
on:click={(ev) => {
if (!shown) {
if (!shown && !disabled) {
showPopup(BooleanEditorPopup, { value, withoutUndefined }, eventToHTMLElement(ev), (res) => {
if (res !== undefined) {
if (res === 1) value = true

View File

@ -262,6 +262,9 @@ export interface Action<T extends Doc = Doc, P = Record<string, any>> extends Do
// A list of actions replaced by this one.
// For example it could be global action and action for focus class, second one fill override first one.
override?: Ref<Action>[]
// Avaible only for workspace owners
secured?: boolean
}
/**

View File

@ -14,10 +14,10 @@
-->
<script lang="ts">
import contact, { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
import { getCurrentAccount } from '@anticrm/core'
import { AccountRole, getCurrentAccount } from '@anticrm/core'
import login from '@anticrm/login'
import { Avatar, createQuery, getClient } from '@anticrm/presentation'
import setting, { SettingsCategory, settingId } from '@anticrm/setting'
import { Avatar, createQuery } from '@anticrm/presentation'
import setting, { settingId, SettingsCategory } from '@anticrm/setting'
import {
closePanel,
closePopup,
@ -26,17 +26,20 @@
Label,
navigate,
setMetadataLocalStorage,
showPopup,
Submenu,
locationToUrl
showPopup
} from '@anticrm/ui'
import type { Action } from '@anticrm/ui'
import view from '@anticrm/view'
const client = getClient()
async function getItems (): Promise<SettingsCategory[]> {
return await client.findAll(setting.class.SettingsCategory, {}, { sort: { order: 1 } })
}
let items: SettingsCategory[] = []
const settingsQuery = createQuery()
settingsQuery.query(
setting.class.SettingsCategory,
{},
(res) => {
items = account.role > AccountRole.User ? res : res.filter((p) => p.secured === false)
},
{ sort: { order: 1 } }
)
const account = getCurrentAccount() as EmployeeAccount
let employee: Employee | undefined
@ -80,84 +83,61 @@
}
function filterItems (items: SettingsCategory[]): SettingsCategory[] {
return items?.filter((p) => p.name !== 'profile' && p.name !== 'password')
return items.filter((p) => p._id !== setting.ids.Profile && p._id !== setting.ids.Password)
}
function editProfile (items: SettingsCategory[] | undefined): void {
const profile = items?.find((p) => p.name === 'profile')
function editProfile (items: SettingsCategory[]): void {
const profile = items.find((p) => p._id === setting.ids.Profile)
if (profile === undefined) return
selectCategory(profile)
}
function getURLCategory (sp: SettingsCategory): string {
const loc = getCurrentLocation()
loc.path[1] = settingId
loc.path[2] = sp.name
loc.path.length = 3
return locationToUrl(loc)
}
const getSubmenu = (items: SettingsCategory[]): Action[] => {
const actions: Action[] = filterItems(items).map((i) => {
return {
icon: i.icon,
label: i.label,
action: async () => selectCategory(i),
link: getURLCategory(i),
inline: true
}
})
return actions
}
</script>
<div class="selectPopup autoHeight">
<div class="scroll">
<div class="box">
{#await getItems() then items}
<div
class="menu-item high flex-row-center"
on:click={() => {
editProfile(items)
}}
>
{#if employee}
<Avatar avatar={employee.avatar} size={'medium'} />
{/if}
<div class="ml-2 flex-col">
{#if account}
<div class="overflow-label fs-bold caption-color">{formatName(account.name)}</div>
<div class="overflow-label text-sm content-dark-color">{account.email}</div>
{/if}
</div>
</div>
{#if items}
<Submenu
icon={view.icon.Setting}
label={setting.string.Settings}
props={{ actions: getSubmenu(items) }}
withHover
/>
<div
class="menu-item high flex-row-center"
on:click={() => {
editProfile(items)
}}
>
{#if employee}
<Avatar avatar={employee.avatar} size={'medium'} />
{/if}
<button class="menu-item" on:click={selectWorkspace}>
<div class="icon mr-3">
<Icon icon={setting.icon.SelectWorkspace} size={'small'} />
<div class="ml-2 flex-col">
{#if account}
<div class="overflow-label fs-bold caption-color">{formatName(account.name)}</div>
<div class="overflow-label text-sm content-dark-color">{account.email}</div>
{/if}
</div>
</div>
{#each filterItems(items) as item}
<button class="menu-item" on:click={() => selectCategory(item)}>
<div class="mr-2">
<Icon icon={item.icon} size={'small'} />
</div>
<Label label={setting.string.SelectWorkspace} />
<Label label={item.label} />
</button>
<button class="menu-item" on:click={inviteWorkspace}>
<div class="icon mr-3">
<Icon icon={login.icon.InviteWorkspace} size={'small'} />
</div>
<Label label={setting.string.InviteWorkspace} />
</button>
<button class="menu-item" on:click={signOut}>
<div class="icon mr-3">
<Icon icon={setting.icon.Signout} size={'small'} />
</div>
<Label label={setting.string.Signout} />
</button>
{/await}
{/each}
<button class="menu-item" on:click={selectWorkspace}>
<div class="icon mr-3">
<Icon icon={setting.icon.SelectWorkspace} size={'small'} />
</div>
<Label label={setting.string.SelectWorkspace} />
</button>
<button class="menu-item" on:click={inviteWorkspace}>
<div class="icon mr-3">
<Icon icon={login.icon.InviteWorkspace} size={'small'} />
</div>
<Label label={setting.string.InviteWorkspace} />
</button>
<button class="menu-item" on:click={signOut}>
<div class="icon mr-3">
<Icon icon={setting.icon.Signout} size={'small'} />
</div>
<Label label={setting.string.Signout} />
</button>
</div>
</div>
</div>

View File

@ -15,7 +15,7 @@
<script lang="ts">
import calendar from '@anticrm/calendar'
import contact, { Employee, EmployeeAccount } from '@anticrm/contact'
import core, { Class, Client, Doc, getCurrentAccount, Ref, Space } from '@anticrm/core'
import core, { Class, Client, Doc, getCurrentAccount, Ref, setCurrentAccount, Space } from '@anticrm/core'
import notification, { NotificationStatus } from '@anticrm/notification'
import { NotificationClientImpl, BrowserNotificatator } from '@anticrm/notification-resources'
import { getMetadata, getResource, IntlString } from '@anticrm/platform'
@ -93,7 +93,20 @@
}
}
const account = getCurrentAccount() as EmployeeAccount
let account = getCurrentAccount() as EmployeeAccount
const accountQ = createQuery()
accountQ.query(
contact.class.EmployeeAccount,
{
_id: account._id
},
(res) => {
account = res[0]
setCurrentAccount(account)
},
{ limit: 1 }
)
let employee: Employee | undefined
const employeeQ = createQuery()

View File

@ -14,7 +14,7 @@
//
import contact, { combineName, Employee } from '@anticrm/contact'
import core, { Ref, TxOperations } from '@anticrm/core'
import core, { AccountRole, Ref, TxOperations } from '@anticrm/core'
import platform, {
getMetadata,
PlatformError,
@ -380,6 +380,7 @@ export async function createUserWorkspace (db: Db, token: string, workspace: str
const { email } = decodeToken(token)
await createWorkspace(db, workspace, '')
await assignWorkspace(db, email, workspace)
await setRole(email, workspace, AccountRole.Owner)
const result = {
endpoint: getEndpoint(),
email,
@ -445,6 +446,26 @@ async function getWorkspaceAndAccount (
return { accountId, workspaceId }
}
/**
* @public
*/
export async function setRole (email: string, workspace: string, role: AccountRole): Promise<void> {
const connection = await connect(getTransactor(), workspace, email)
try {
const ops = new TxOperations(connection, core.account.System)
const existingAccount = await ops.findOne(contact.class.EmployeeAccount, { email })
if (existingAccount !== undefined) {
await ops.update(existingAccount, {
role
})
}
} finally {
await connection.close()
}
}
/**
* @public
*/
@ -485,7 +506,8 @@ async function createEmployeeAccount (account: Account, workspace: string): Prom
await ops.createDoc(contact.class.EmployeeAccount, core.space.Model, {
email: account.email,
employee,
name
name,
role: 0
})
} else {
const employee = await ops.findOne(contact.class.Employee, { _id: existingAccount.employee })

View File

@ -174,8 +174,8 @@ export function genMinModel (): TxCUD<Doc>[] {
const u1 = 'User1' as Ref<Account>
const u2 = 'User2' as Ref<Account>
txes.push(
createDoc(core.class.Account, { email: 'user1@site.com' }, u1),
createDoc(core.class.Account, { email: 'user2@site.com' }, u2),
createDoc(core.class.Account, { email: 'user1@site.com', role: 0 }, u1),
createDoc(core.class.Account, { email: 'user2@site.com', role: 0 }, u2),
createDoc(core.class.Space, {
name: 'Sp1',
description: '',

View File

@ -174,8 +174,8 @@ export function genMinModel (): TxCUD<Doc>[] {
const u1 = 'User1' as Ref<Account>
const u2 = 'User2' as Ref<Account>
txes.push(
createDoc(core.class.Account, { email: 'user1@site.com' }, u1),
createDoc(core.class.Account, { email: 'user2@site.com' }, u2),
createDoc(core.class.Account, { email: 'user1@site.com', role: 0 }, u1),
createDoc(core.class.Account, { email: 'user2@site.com', role: 0 }, u2),
createDoc(core.class.Space, {
name: 'Sp1',
description: '',

View File

@ -14,9 +14,7 @@ test.describe('contact tests', () => {
await page.goto(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp`)
// Click #profile-button
await page.click('#profile-button')
await page.click('.antiPopup-submenu >> text=Settings')
// Click button:has-text("Setting")
await page.click('button:has-text("Setting")')
await page.click('text=Workspace')
await expect(page).toHaveURL(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp/setting/setting`)
// Click text=Edit profile
await page.click('text=Edit profile')
@ -43,12 +41,8 @@ test.describe('contact tests', () => {
await page.goto(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp`)
// Click #profile-button
await page.click('#profile-button')
await page.click('.antiPopup-submenu >> text=Settings')
// Click button:has-text("Templates")
await page.click('button:has-text("Templates")')
await expect(page).toHaveURL(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp/setting/message-templates`)
// Go to http://localhost:8083/workbench%3Acomponent%3AWorkbenchApp/setting/message-templates
await page.goto(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp/setting/message-templates`)
await page.click('text=Workspace')
await page.click('text=Templates')
// Click .flex-center.icon-button
await page.click('#create-template >> .flex-center.icon-button')
// Click [placeholder="New\ template"]
@ -76,10 +70,9 @@ test.describe('contact tests', () => {
await page.goto(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp`)
// Click #profile-button
await page.click('#profile-button')
await page.click('.antiPopup-submenu >> text=Settings')
await page.click('text=Workspace')
// Click button:has-text("Manage Statuses")
await page.click('button:has-text("Manage Statuses")')
await expect(page).toHaveURL(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp/setting/statuses`)
await page.click('text="Manage Statuses"')
// Click text=Vacancies
await page.click('text=Vacancies')
// Click #create-template div

View File

@ -50,16 +50,4 @@ test.describe('workbench tests', () => {
// Click text=John Appleseed
await expect(page.locator('text=John Appleseed')).toBeVisible()
})
test('submenu', async ({ page }) => {
// await page.goto('http://localhost:8080/workbench%3Acomponent%3AWorkbenchApp');
// Click #profile-button
await page.click('#profile-button')
// Click text=Settings
await page.click('.antiPopup-submenu >> text=Settings')
// Click button:has-text("Terms")
await page.click('button:has-text("Terms")')
await expect(page).toHaveURL(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp/setting/terms`)
// Click .ac-header
await expect(page.locator('.ac-header >> text=Terms')).toBeVisible()
})
})