2022-12-27 15:37:40 +01:00
|
|
|
import m, { Children } from "mithril"
|
|
|
|
import type { VirtualRow } from "../gui/base/List.js"
|
|
|
|
import { List } from "../gui/base/List.js"
|
|
|
|
import { lang } from "../misc/LanguageViewModel.js"
|
|
|
|
import { NotFoundError } from "../api/common/error/RestError.js"
|
|
|
|
import { size } from "../gui/size.js"
|
|
|
|
import type { GroupInfo } from "../api/entities/sys/TypeRefs.js"
|
2023-03-29 16:29:16 +02:00
|
|
|
import { GroupInfoTypeRef, GroupMemberTypeRef, UserTypeRef } from "../api/entities/sys/TypeRefs.js"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { assertNotNull, contains, LazyLoaded, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils"
|
|
|
|
import { UserViewer } from "./UserViewer.js"
|
|
|
|
import type { SettingsView, UpdatableSettingsViewer } from "./SettingsView.js"
|
|
|
|
import { FeatureType, GroupType, OperationType } from "../api/common/TutanotaConstants.js"
|
|
|
|
import { Icon } from "../gui/base/Icon.js"
|
|
|
|
import { Icons } from "../gui/base/icons/Icons.js"
|
|
|
|
import { BootIcons } from "../gui/base/icons/BootIcons.js"
|
|
|
|
import type { EntityUpdateData } from "../api/main/EventController.js"
|
|
|
|
import { isUpdateForTypeRef } from "../api/main/EventController.js"
|
|
|
|
import { Button, ButtonType } from "../gui/base/Button.js"
|
|
|
|
import { compareGroupInfos } from "../api/common/utils/GroupUtils.js"
|
|
|
|
import { elementIdPart, GENERATED_MAX_ID } from "../api/common/utils/EntityUtils.js"
|
|
|
|
import { ListColumnWrapper } from "../gui/ListColumnWrapper.js"
|
|
|
|
import { assertMainOrNode } from "../api/common/Env.js"
|
|
|
|
import { locator } from "../api/main/MainLocator.js"
|
|
|
|
import Stream from "mithril/stream"
|
|
|
|
import { showNotAvailableForFreeDialog } from "../misc/SubscriptionDialogs.js"
|
2022-02-28 12:13:28 +01:00
|
|
|
import * as AddUserDialog from "./AddUserDialog.js"
|
2023-03-29 16:29:16 +02:00
|
|
|
import { SelectableRowContainer, setSelectedRowStyle, setVisibility } from "../gui/SelectableRowContainer.js"
|
|
|
|
import { theme } from "../gui/theme.js"
|
2022-01-07 15:58:30 +01:00
|
|
|
|
2017-08-15 13:54:22 +02:00
|
|
|
assertMainOrNode()
|
|
|
|
const className = "user-list"
|
2022-01-07 15:58:30 +01:00
|
|
|
|
2018-10-25 10:42:40 +02:00
|
|
|
export class UserListView implements UpdatableSettingsViewer {
|
2022-02-25 17:32:48 +01:00
|
|
|
readonly list: List<GroupInfo, UserRow>
|
|
|
|
|
|
|
|
private readonly listId: LazyLoaded<Id>
|
2023-03-29 16:29:16 +02:00
|
|
|
private readonly searchResultStreamDependency: Stream<unknown>
|
2022-02-25 17:32:48 +01:00
|
|
|
private adminUserGroupInfoIds: Id[] = []
|
|
|
|
|
2022-12-27 15:37:40 +01:00
|
|
|
constructor(private readonly settingsView: SettingsView) {
|
2022-02-25 17:32:48 +01:00
|
|
|
this.listId = new LazyLoaded(async () => {
|
2023-03-22 15:12:59 +01:00
|
|
|
const customer = await locator.logins.getUserController().loadCustomer()
|
2022-02-25 17:32:48 +01:00
|
|
|
return customer.userGroups
|
2022-01-07 15:58:30 +01:00
|
|
|
})
|
|
|
|
this.list = new List({
|
|
|
|
rowHeight: size.list_row_height,
|
2022-02-25 17:32:48 +01:00
|
|
|
fetch: async (startId, count) => {
|
|
|
|
if (startId !== GENERATED_MAX_ID) {
|
2022-01-07 15:58:30 +01:00
|
|
|
throw new Error("fetch user group infos called for specific start id")
|
|
|
|
}
|
2022-02-25 17:32:48 +01:00
|
|
|
await this.loadAdmins()
|
|
|
|
const listId = await this.listId.getAsync()
|
|
|
|
const allUserGroupInfos = await locator.entityClient.loadAll(GroupInfoTypeRef, listId)
|
|
|
|
|
|
|
|
// we return all users because we have already loaded all users and the scroll bar shall have the complete size.
|
2022-04-26 17:33:40 +02:00
|
|
|
let items: GroupInfo[]
|
2023-03-21 15:32:40 +01:00
|
|
|
if (locator.logins.getUserController().isGlobalAdmin()) {
|
2022-04-26 17:33:40 +02:00
|
|
|
items = allUserGroupInfos
|
2022-02-25 17:32:48 +01:00
|
|
|
} else {
|
2023-03-21 15:32:40 +01:00
|
|
|
let localAdminGroupIds = locator.logins
|
2022-02-25 17:32:48 +01:00
|
|
|
.getUserController()
|
|
|
|
.getLocalAdminGroupMemberships()
|
2022-12-27 15:37:40 +01:00
|
|
|
.map((gm) => gm.group)
|
2022-04-26 17:33:40 +02:00
|
|
|
items = allUserGroupInfos.filter((gi: GroupInfo) => gi.localAdmin && localAdminGroupIds.includes(gi.localAdmin))
|
2022-02-25 17:32:48 +01:00
|
|
|
}
|
2022-12-27 15:37:40 +01:00
|
|
|
return { items, complete: true }
|
2022-01-07 15:58:30 +01:00
|
|
|
},
|
2022-12-27 15:37:40 +01:00
|
|
|
loadSingle: async (elementId) => {
|
2022-02-25 17:32:48 +01:00
|
|
|
const listId = await this.listId.getAsync()
|
|
|
|
try {
|
|
|
|
return await locator.entityClient.load<GroupInfo>(GroupInfoTypeRef, [listId, elementId])
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof NotFoundError) {
|
|
|
|
// we return null if the GroupInfo does not exist
|
|
|
|
return null
|
|
|
|
} else {
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
}
|
2022-01-07 15:58:30 +01:00
|
|
|
},
|
|
|
|
sortCompare: compareGroupInfos,
|
|
|
|
elementSelected: (entities, elementClicked, selectionChanged, multiSelectionActive) =>
|
|
|
|
this.elementSelected(entities, elementClicked, selectionChanged, multiSelectionActive),
|
2023-03-29 16:29:16 +02:00
|
|
|
createVirtualRow: () => new UserRow((groupInfo) => this.isAdmin(groupInfo)),
|
2022-01-07 15:58:30 +01:00
|
|
|
className: className,
|
|
|
|
swipe: {
|
|
|
|
renderLeftSpacer: () => [],
|
|
|
|
renderRightSpacer: () => [],
|
2022-12-27 15:37:40 +01:00
|
|
|
swipeLeft: (listElement) => Promise.resolve(false),
|
|
|
|
swipeRight: (listElement) => Promise.resolve(false),
|
2022-01-07 15:58:30 +01:00
|
|
|
enabled: false,
|
|
|
|
},
|
|
|
|
multiSelectionAllowed: false,
|
|
|
|
emptyMessage: lang.get("noEntries_msg"),
|
|
|
|
})
|
2021-02-26 14:59:25 +01:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
this.list.loadInitial()
|
2023-03-21 15:32:40 +01:00
|
|
|
const searchBar = neverNull(locator.header.searchBar)
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-12-27 15:37:40 +01:00
|
|
|
this.listId.getAsync().then((listId) => {
|
2022-01-07 15:58:30 +01:00
|
|
|
searchBar.setGroupInfoRestrictionListId(listId)
|
|
|
|
})
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-12-27 15:37:40 +01:00
|
|
|
this.searchResultStreamDependency = searchBar.lastSelectedGroupInfoResult.map((groupInfo) => {
|
2022-02-25 17:32:48 +01:00
|
|
|
if (this.listId.isLoaded() && this.listId.getSync() === groupInfo._id[0]) {
|
2022-01-07 15:58:30 +01:00
|
|
|
this.list.scrollToIdAndSelect(groupInfo._id[1])
|
|
|
|
}
|
|
|
|
})
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-02-28 12:13:28 +01:00
|
|
|
this.onremove = this.onremove.bind(this)
|
|
|
|
this.view = this.view.bind(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
view(): Children {
|
2023-03-21 15:32:40 +01:00
|
|
|
if (locator.logins.isEnabled(FeatureType.WhitelabelChild)) {
|
2022-02-28 12:13:28 +01:00
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2022-12-27 15:37:40 +01:00
|
|
|
return m(
|
|
|
|
ListColumnWrapper,
|
|
|
|
{
|
|
|
|
headerContent: m(
|
2023-04-18 13:07:11 +02:00
|
|
|
".flex.flex-end.center-vertically.plr-l.list-border-bottom",
|
|
|
|
m(
|
|
|
|
".mr-negative-s",
|
|
|
|
m(Button, {
|
|
|
|
label: "addUsers_action",
|
|
|
|
type: ButtonType.Primary,
|
|
|
|
click: () => this.addButtonClicked(),
|
|
|
|
}),
|
|
|
|
),
|
2022-02-28 12:13:28 +01:00
|
|
|
),
|
|
|
|
},
|
|
|
|
m(this.list),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
onremove() {
|
|
|
|
if (this.searchResultStreamDependency) {
|
|
|
|
this.searchResultStreamDependency.end(true)
|
2022-01-07 15:58:30 +01:00
|
|
|
}
|
|
|
|
}
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-02-25 17:32:48 +01:00
|
|
|
private async loadAdmins(): Promise<void> {
|
2023-03-21 15:32:40 +01:00
|
|
|
const adminGroupMembership = locator.logins.getUserController().user.memberships.find((gm) => gm.groupType === GroupType.Admin)
|
2022-02-28 12:13:28 +01:00
|
|
|
if (adminGroupMembership == null) {
|
|
|
|
return
|
|
|
|
}
|
2022-01-07 15:58:30 +01:00
|
|
|
const members = await locator.entityClient.loadAll(GroupMemberTypeRef, adminGroupMembership.groupMember[0])
|
2022-12-27 15:37:40 +01:00
|
|
|
this.adminUserGroupInfoIds = members.map((adminGroupMember) => elementIdPart(adminGroupMember.userGroupInfo))
|
2022-01-07 15:58:30 +01:00
|
|
|
}
|
2017-11-30 12:26:46 +01:00
|
|
|
|
2022-02-25 17:32:48 +01:00
|
|
|
private elementSelected(groupInfos: GroupInfo[], elementClicked: boolean, selectionChanged: boolean, multiSelectOperation: boolean): void {
|
|
|
|
if (groupInfos.length === 0 && this.settingsView.detailsViewer) {
|
|
|
|
this.settingsView.detailsViewer = null
|
2022-01-07 15:58:30 +01:00
|
|
|
m.redraw()
|
|
|
|
} else if (groupInfos.length === 1 && selectionChanged) {
|
2022-02-25 17:32:48 +01:00
|
|
|
this.settingsView.detailsViewer = new UserViewer(groupInfos[0], this.isAdmin(groupInfos[0]))
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
if (elementClicked) {
|
2022-02-25 17:32:48 +01:00
|
|
|
this.settingsView.focusSettingsDetailsColumn()
|
2022-01-07 15:58:30 +01:00
|
|
|
}
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
m.redraw()
|
|
|
|
} else {
|
2022-02-25 17:32:48 +01:00
|
|
|
this.settingsView.focusSettingsDetailsColumn()
|
2022-01-07 15:58:30 +01:00
|
|
|
}
|
|
|
|
}
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
isAdmin(userGroupInfo: GroupInfo): boolean {
|
2022-02-25 17:32:48 +01:00
|
|
|
return contains(this.adminUserGroupInfoIds, userGroupInfo._id[1])
|
2022-01-07 15:58:30 +01:00
|
|
|
}
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-02-25 17:32:48 +01:00
|
|
|
private addButtonClicked() {
|
2023-03-21 15:32:40 +01:00
|
|
|
if (locator.logins.getUserController().isFreeAccount()) {
|
2022-01-07 15:58:30 +01:00
|
|
|
showNotAvailableForFreeDialog(false)
|
|
|
|
} else {
|
|
|
|
AddUserDialog.show()
|
|
|
|
}
|
|
|
|
}
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
entityEventsReceived<T>(updates: ReadonlyArray<EntityUpdateData>): Promise<void> {
|
2022-12-27 15:37:40 +01:00
|
|
|
return promiseMap(updates, (update) => {
|
|
|
|
const { instanceListId, instanceId, operation } = update
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-02-25 17:32:48 +01:00
|
|
|
if (isUpdateForTypeRef(GroupInfoTypeRef, update) && this.listId.getSync() === instanceListId) {
|
2023-03-21 15:32:40 +01:00
|
|
|
if (!locator.logins.getUserController().isGlobalAdmin()) {
|
2022-01-07 15:58:30 +01:00
|
|
|
let listEntity = this.list.getEntity(instanceId)
|
2022-12-27 15:37:40 +01:00
|
|
|
return locator.entityClient.load(GroupInfoTypeRef, [neverNull(instanceListId), instanceId]).then((gi) => {
|
2023-03-21 15:32:40 +01:00
|
|
|
let localAdminGroupIds = locator.logins
|
2022-01-07 15:58:30 +01:00
|
|
|
.getUserController()
|
|
|
|
.getLocalAdminGroupMemberships()
|
2022-12-27 15:37:40 +01:00
|
|
|
.map((gm) => gm.group)
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
if (listEntity) {
|
|
|
|
if (localAdminGroupIds.indexOf(assertNotNull(gi.localAdmin)) === -1) {
|
|
|
|
return this.list.entityEventReceived(instanceId, OperationType.DELETE)
|
|
|
|
} else {
|
|
|
|
return this.list.entityEventReceived(instanceId, operation)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (localAdminGroupIds.indexOf(assertNotNull(gi.localAdmin)) !== -1) {
|
|
|
|
return this.list.entityEventReceived(instanceId, OperationType.CREATE)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
return this.list.entityEventReceived(instanceId, operation)
|
|
|
|
}
|
|
|
|
} else if (isUpdateForTypeRef(UserTypeRef, update) && operation === OperationType.UPDATE) {
|
2022-02-25 17:32:48 +01:00
|
|
|
return this.loadAdmins().then(() => {
|
2022-01-07 15:58:30 +01:00
|
|
|
this.list.redraw()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}).then(noOp)
|
|
|
|
}
|
2021-12-23 14:03:23 +01:00
|
|
|
}
|
2022-01-07 15:58:30 +01:00
|
|
|
|
2021-11-04 13:35:55 +01:00
|
|
|
export class UserRow implements VirtualRow<GroupInfo> {
|
2023-03-29 16:29:16 +02:00
|
|
|
top: number = 0
|
2022-01-13 11:57:55 +01:00
|
|
|
domElement: HTMLElement | null = null // set from List
|
2023-03-29 16:29:16 +02:00
|
|
|
entity: GroupInfo | null = null
|
|
|
|
private nameDom!: HTMLElement
|
|
|
|
private addressDom!: HTMLElement
|
|
|
|
private adminIconDom!: HTMLElement
|
|
|
|
private deletedIconDom!: HTMLElement
|
|
|
|
private innerContainerDom!: HTMLElement
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2023-03-29 16:29:16 +02:00
|
|
|
constructor(private readonly isAdmin: (groupInfo: GroupInfo) => boolean) {}
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
update(groupInfo: GroupInfo, selected: boolean): void {
|
|
|
|
if (!this.domElement) {
|
|
|
|
return
|
|
|
|
}
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2023-03-29 16:29:16 +02:00
|
|
|
setSelectedRowStyle(this.innerContainerDom, selected)
|
2021-12-23 14:03:23 +01:00
|
|
|
|
2023-03-29 16:29:16 +02:00
|
|
|
this.nameDom.textContent = groupInfo.name
|
|
|
|
this.addressDom.textContent = groupInfo.mailAddress ? groupInfo.mailAddress : ""
|
2021-12-23 14:03:23 +01:00
|
|
|
|
2023-03-29 16:29:16 +02:00
|
|
|
setVisibility(this.adminIconDom, this.isAdmin(groupInfo))
|
|
|
|
setVisibility(this.deletedIconDom, groupInfo.deleted != null)
|
2022-01-07 15:58:30 +01:00
|
|
|
}
|
2021-12-23 14:03:23 +01:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
/**
|
|
|
|
* Only the structure is managed by mithril. We set all contents on our own (see update) in order to avoid the vdom overhead (not negligible on mobiles)
|
|
|
|
*/
|
2023-03-29 16:29:16 +02:00
|
|
|
render(): Children {
|
|
|
|
return m(
|
|
|
|
SelectableRowContainer,
|
|
|
|
{
|
|
|
|
oncreate: (vnode) => {
|
|
|
|
this.innerContainerDom = vnode.dom as HTMLElement
|
|
|
|
},
|
|
|
|
},
|
|
|
|
m(".flex.col.flex-grow", [
|
|
|
|
m(".smaller", [
|
|
|
|
m("", {
|
|
|
|
oncreate: (vnode) => (this.nameDom = vnode.dom as HTMLElement),
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
2023-03-29 16:29:16 +02:00
|
|
|
]),
|
|
|
|
m(".flex-space-between", [
|
|
|
|
m(".smaller", {
|
|
|
|
oncreate: (vnode) => (this.addressDom = vnode.dom as HTMLElement),
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
2023-03-29 16:29:16 +02:00
|
|
|
m(".icons.flex", [
|
|
|
|
m(Icon, {
|
|
|
|
icon: BootIcons.Settings,
|
|
|
|
oncreate: (vnode) => (this.adminIconDom = vnode.dom as HTMLElement),
|
|
|
|
class: "svg-list-accent-fg",
|
|
|
|
style: {
|
|
|
|
display: "none",
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
m(Icon, {
|
|
|
|
icon: Icons.Trash,
|
|
|
|
oncreate: (vnode) => (this.deletedIconDom = vnode.dom as HTMLElement),
|
|
|
|
class: "svg-list-accent-fg",
|
|
|
|
style: {
|
|
|
|
display: "none",
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
]),
|
2022-01-07 15:58:30 +01:00
|
|
|
]),
|
|
|
|
]),
|
2023-03-29 16:29:16 +02:00
|
|
|
)
|
2022-01-07 15:58:30 +01:00
|
|
|
}
|
2022-12-27 15:37:40 +01:00
|
|
|
}
|