2021-12-28 13:53:11 +01:00
|
|
|
import m, {Children} from "mithril"
|
2022-02-28 12:13:28 +01:00
|
|
|
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"
|
2022-04-14 17:31:36 +02:00
|
|
|
import type {GroupInfo} from "../api/entities/sys/TypeRefs.js"
|
|
|
|
|
import {GroupInfoTypeRef} from "../api/entities/sys/TypeRefs.js"
|
|
|
|
|
import {CustomerTypeRef} from "../api/entities/sys/TypeRefs.js"
|
2022-02-25 17:32:48 +01:00
|
|
|
import {assertNotNull, contains, LazyLoaded, neverNull, noOp, promiseMap} from "@tutao/tutanota-utils"
|
2022-02-28 12:13:28 +01:00
|
|
|
import {UserViewer} from "./UserViewer.js"
|
|
|
|
|
import type {SettingsView, UpdatableSettingsViewer} from "./SettingsView.js"
|
|
|
|
|
import {FeatureType, GroupType, OperationType} from "../api/common/TutanotaConstants.js"
|
|
|
|
|
import {logins} from "../api/main/LoginController.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 {header} from "../gui/base/Header.js"
|
2022-04-14 17:31:36 +02:00
|
|
|
import {GroupMemberTypeRef} from "../api/entities/sys/TypeRefs.js"
|
|
|
|
|
import {UserTypeRef} from "../api/entities/sys/TypeRefs.js"
|
2022-02-28 12:13:28 +01:00
|
|
|
import type {EntityUpdateData} from "../api/main/EventController.js"
|
|
|
|
|
import {isUpdateForTypeRef} from "../api/main/EventController.js"
|
|
|
|
|
import {ButtonN, ButtonType} from "../gui/base/ButtonN.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"
|
2021-12-28 13:53:11 +01:00
|
|
|
import Stream from "mithril/stream";
|
2022-02-28 12:13:28 +01:00
|
|
|
import {showNotAvailableForFreeDialog} from "../misc/SubscriptionDialogs.js"
|
|
|
|
|
import * as AddUserDialog from "./AddUserDialog.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>
|
|
|
|
|
private readonly searchResultStreamDependency: Stream<any>
|
|
|
|
|
private adminUserGroupInfoIds: Id[] = []
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly settingsView: SettingsView
|
|
|
|
|
) {
|
|
|
|
|
this.listId = new LazyLoaded(async () => {
|
|
|
|
|
const customer = await locator.entityClient.load(CustomerTypeRef, logins.getUserController().user.customer!)
|
|
|
|
|
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 have to set loadedCompletely to make sure that fetch is never called again and also that new users are inserted into the list, even at the end
|
2022-02-28 12:13:28 +01:00
|
|
|
this.list.setLoadedCompletely()
|
2022-02-25 17:32:48 +01:00
|
|
|
|
|
|
|
|
// we return all users because we have already loaded all users and the scroll bar shall have the complete size.
|
|
|
|
|
if (logins.getUserController().isGlobalAdmin()) {
|
|
|
|
|
return allUserGroupInfos
|
|
|
|
|
} else {
|
|
|
|
|
let localAdminGroupIds = logins
|
|
|
|
|
.getUserController()
|
|
|
|
|
.getLocalAdminGroupMemberships()
|
|
|
|
|
.map(gm => gm.group)
|
2022-02-28 12:13:28 +01:00
|
|
|
return allUserGroupInfos.filter((gi: GroupInfo) => gi.localAdmin && localAdminGroupIds.includes(gi.localAdmin))
|
2022-02-25 17:32:48 +01:00
|
|
|
}
|
2022-01-07 15:58:30 +01:00
|
|
|
},
|
2022-02-25 17:32:48 +01:00
|
|
|
loadSingle: async elementId => {
|
|
|
|
|
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),
|
|
|
|
|
createVirtualRow: () => new UserRow(this),
|
|
|
|
|
showStatus: false,
|
|
|
|
|
className: className,
|
|
|
|
|
swipe: {
|
|
|
|
|
renderLeftSpacer: () => [],
|
|
|
|
|
renderRightSpacer: () => [],
|
|
|
|
|
swipeLeft: listElement => Promise.resolve(false),
|
|
|
|
|
swipeRight: listElement => Promise.resolve(false),
|
|
|
|
|
enabled: false,
|
|
|
|
|
},
|
|
|
|
|
multiSelectionAllowed: false,
|
|
|
|
|
emptyMessage: lang.get("noEntries_msg"),
|
|
|
|
|
})
|
2021-02-26 14:59:25 +01:00
|
|
|
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
this.list.loadInitial()
|
|
|
|
|
const searchBar = neverNull(header.searchBar)
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-02-25 17:32:48 +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-02-25 17:32:48 +01:00
|
|
|
this.searchResultStreamDependency = searchBar.lastSelectedGroupInfoResult.map(groupInfo => {
|
|
|
|
|
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 {
|
|
|
|
|
if (logins.isEnabled(FeatureType.WhitelabelChild)) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return m(ListColumnWrapper, {
|
|
|
|
|
headerContent: m(".mr-negative-s.align-self-end",
|
|
|
|
|
m(ButtonN, {
|
|
|
|
|
label: "addUsers_action",
|
|
|
|
|
type: ButtonType.Primary,
|
|
|
|
|
click: () => this.addButtonClicked(),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
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> {
|
2022-02-28 12:13:28 +01:00
|
|
|
const adminGroupMembership = logins.getUserController().user.memberships.find(gm => gm.groupType === GroupType.Admin)
|
|
|
|
|
if (adminGroupMembership == null) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-01-07 15:58:30 +01:00
|
|
|
const members = await locator.entityClient.loadAll(GroupMemberTypeRef, adminGroupMembership.groupMember[0])
|
2022-02-28 12:13:28 +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() {
|
2022-01-07 15:58:30 +01:00
|
|
|
if (logins.getUserController().isFreeAccount()) {
|
|
|
|
|
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> {
|
|
|
|
|
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) {
|
2022-01-07 15:58:30 +01:00
|
|
|
if (!logins.getUserController().isGlobalAdmin()) {
|
|
|
|
|
let listEntity = this.list.getEntity(instanceId)
|
|
|
|
|
return locator.entityClient.load(GroupInfoTypeRef, [neverNull(instanceListId), instanceId]).then(gi => {
|
|
|
|
|
let localAdminGroupIds = logins
|
|
|
|
|
.getUserController()
|
|
|
|
|
.getLocalAdminGroupMemberships()
|
|
|
|
|
.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> {
|
2022-01-07 15:58:30 +01:00
|
|
|
top: number
|
2022-01-13 11:57:55 +01:00
|
|
|
domElement: HTMLElement | null = null // set from List
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
entity: GroupInfo | null
|
2022-01-13 11:57:55 +01:00
|
|
|
private _domName!: HTMLElement
|
|
|
|
|
private _domAddress!: HTMLElement
|
|
|
|
|
private _domAdminIcon!: HTMLElement
|
|
|
|
|
private _domDeletedIcon!: HTMLElement
|
|
|
|
|
private readonly _userListView: UserListView
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
constructor(userListView: UserListView) {
|
|
|
|
|
this._userListView = userListView
|
|
|
|
|
this.top = 0
|
|
|
|
|
this.entity = null
|
|
|
|
|
}
|
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
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
if (selected) {
|
|
|
|
|
this.domElement.classList.add("row-selected")
|
|
|
|
|
} else {
|
|
|
|
|
this.domElement.classList.remove("row-selected")
|
|
|
|
|
}
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
this._domName.textContent = groupInfo.name
|
|
|
|
|
this._domAddress.textContent = groupInfo.mailAddress ? groupInfo.mailAddress : ""
|
2021-12-23 14:03:23 +01:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
if (this._userListView.isAdmin(groupInfo)) {
|
|
|
|
|
this._domAdminIcon.style.display = ""
|
|
|
|
|
} else {
|
|
|
|
|
this._domAdminIcon.style.display = "none"
|
|
|
|
|
}
|
2021-12-23 14:03:23 +01:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
if (groupInfo.deleted) {
|
|
|
|
|
this._domDeletedIcon.style.display = ""
|
|
|
|
|
} else {
|
|
|
|
|
this._domDeletedIcon.style.display = "none"
|
|
|
|
|
}
|
|
|
|
|
}
|
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)
|
|
|
|
|
*/
|
|
|
|
|
render(): any {
|
|
|
|
|
let elements = [
|
|
|
|
|
m(".top", [
|
|
|
|
|
m(".name", {
|
|
|
|
|
oncreate: vnode => (this._domName = vnode.dom as HTMLElement),
|
|
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
m(".bottom.flex-space-between", [
|
|
|
|
|
m("small.mail-address", {
|
|
|
|
|
oncreate: vnode => (this._domAddress = vnode.dom as HTMLElement),
|
|
|
|
|
}),
|
|
|
|
|
m(".icons.flex", [
|
|
|
|
|
m(Icon, {
|
|
|
|
|
icon: BootIcons.Settings,
|
|
|
|
|
oncreate: vnode => (this._domAdminIcon = vnode.dom as HTMLElement),
|
|
|
|
|
class: "svg-list-accent-fg",
|
|
|
|
|
style: {
|
|
|
|
|
display: "none",
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
m(Icon, {
|
|
|
|
|
icon: Icons.Trash,
|
|
|
|
|
oncreate: vnode => (this._domDeletedIcon = vnode.dom as HTMLElement),
|
|
|
|
|
class: "svg-list-accent-fg",
|
|
|
|
|
style: {
|
|
|
|
|
display: "none",
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
]
|
|
|
|
|
return elements
|
|
|
|
|
}
|
2021-12-23 14:03:23 +01:00
|
|
|
}
|