diff --git a/serene/src/Serene.Web/Imports/ClientTypes/Administration.PermissionCheckEditorAttribute.cs b/serene/src/Serene.Web/Imports/ClientTypes/Administration.PermissionCheckEditorAttribute.cs index 91b329d403..3baf9f52f2 100644 --- a/serene/src/Serene.Web/Imports/ClientTypes/Administration.PermissionCheckEditorAttribute.cs +++ b/serene/src/Serene.Web/Imports/ClientTypes/Administration.PermissionCheckEditorAttribute.cs @@ -15,9 +15,27 @@ public PermissionCheckEditorAttribute() { } + public object ImplicitPermissions + { + get { return GetOption("implicitPermissions"); } + set { SetOption("implicitPermissions", value); } + } + + public object RolePermissions + { + get { return GetOption("rolePermissions"); } + set { SetOption("rolePermissions", value); } + } + public bool ShowRevoke { get { return GetOption("showRevoke"); } set { SetOption("showRevoke", value); } } + + public object Value + { + get { return GetOption("value"); } + set { SetOption("value", value); } + } } \ No newline at end of file diff --git a/serene/src/Serene.Web/Modules/Administration/Language/LanguageDialog.ts b/serene/src/Serene.Web/Modules/Administration/Language/LanguageDialog.ts index a295fd7fd9..3c1cfa13ba 100644 --- a/serene/src/Serene.Web/Modules/Administration/Language/LanguageDialog.ts +++ b/serene/src/Serene.Web/Modules/Administration/Language/LanguageDialog.ts @@ -1,4 +1,4 @@ -import { LanguageRow, LanguageForm, LanguageService } from "../"; +import { LanguageRow, LanguageForm, LanguageService } from "@/ServerTypes/Administration"; import { Decorators, EntityDialog } from "@serenity-is/corelib" @Decorators.registerClass('Serene.Administration.LanguageDialog') diff --git a/serene/src/Serene.Web/Modules/Administration/Language/LanguageGrid.ts b/serene/src/Serene.Web/Modules/Administration/Language/LanguageGrid.ts index 28c274136b..296fb6cad2 100644 --- a/serene/src/Serene.Web/Modules/Administration/Language/LanguageGrid.ts +++ b/serene/src/Serene.Web/Modules/Administration/Language/LanguageGrid.ts @@ -1,5 +1,5 @@ +import { LanguageRow, LanguageColumns, LanguageService } from "@/ServerTypes/Administration"; import { LanguageDialog } from "./LanguageDialog"; -import { LanguageColumns, LanguageRow, LanguageService } from "../"; import { Decorators, EntityGrid } from "@serenity-is/corelib" @Decorators.registerClass('Serene.Administration.LanguageGrid') diff --git a/serene/src/Serene.Web/Modules/Administration/Role/RoleDialog.ts b/serene/src/Serene.Web/Modules/Administration/Role/RoleDialog.ts index c2cb38aa23..d5cf519143 100644 --- a/serene/src/Serene.Web/Modules/Administration/Role/RoleDialog.ts +++ b/serene/src/Serene.Web/Modules/Administration/Role/RoleDialog.ts @@ -1,7 +1,7 @@ -import { RoleRow, RoleForm, RoleService } from "../"; -import { RolePermissionDialog } from "../RolePermission/RolePermissionDialog"; -import { Texts } from "../../ServerTypes/Texts" +import { RoleForm, RoleRow, RoleService } from "@/ServerTypes/Administration"; import { Decorators, EntityDialog } from "@serenity-is/corelib"; +import { Texts } from "../../ServerTypes/Texts"; +import { RolePermissionDialog } from "../RolePermission/RolePermissionDialog"; const editPermissions = "edit-permissions"; @@ -15,16 +15,14 @@ export class RoleDialog extends EntityDialog { protected form = new RoleForm(this.idPrefix); - protected getToolbarButtons() - { + protected getToolbarButtons() { let buttons = super.getToolbarButtons(); buttons.push({ title: Texts.Site.RolePermissionDialog.EditButton, cssClass: editPermissions, icon: 'fa-lock text-green', - onClick: () => - { + onClick: () => { new RolePermissionDialog({ roleID: this.entity.RoleId, title: this.entity.RoleName diff --git a/serene/src/Serene.Web/Modules/Administration/Role/RoleGrid.ts b/serene/src/Serene.Web/Modules/Administration/Role/RoleGrid.ts index 72816e1931..ece5a1517c 100644 --- a/serene/src/Serene.Web/Modules/Administration/Role/RoleGrid.ts +++ b/serene/src/Serene.Web/Modules/Administration/Role/RoleGrid.ts @@ -1,5 +1,5 @@ +import { RoleRow, RoleColumns, RoleService } from "@/ServerTypes/Administration"; import { RoleDialog } from "./RoleDialog"; -import { RoleColumns, RoleRow, RoleService } from "../"; import { Decorators, EntityGrid } from "@serenity-is/corelib"; @Decorators.registerClass('Serene.Administration.RoleGrid') diff --git a/serene/src/Serene.Web/Modules/Administration/RolePermission/RolePermissionDialog.ts b/serene/src/Serene.Web/Modules/Administration/RolePermission/RolePermissionDialog.ts index e7fdf5ae24..63bfef9cc9 100644 --- a/serene/src/Serene.Web/Modules/Administration/RolePermission/RolePermissionDialog.ts +++ b/serene/src/Serene.Web/Modules/Administration/RolePermission/RolePermissionDialog.ts @@ -1,9 +1,9 @@ -import { Decorators, TemplatedDialog } from "@serenity-is/corelib"; -import { format, getRemoteData, notifySuccess, localText } from "@serenity-is/corelib"; -import { RolePermissionService, UserPermissionRow } from "../"; +import { BaseDialog, stringFormat, getRemoteData, localText, notifySuccess } from "@serenity-is/corelib"; import { PermissionCheckEditor } from "../UserPermission/PermissionCheckEditor"; +import { RolePermissionService, UserPermissionRow } from "@/ServerTypes/Administration"; +import { RemoteDataKeys } from "../../ServerTypes/RemoteDataKeys"; -export class RolePermissionDialog extends TemplatedDialog { +export class RolePermissionDialog extends BaseDialog { private permissions: PermissionCheckEditor; @@ -20,7 +20,7 @@ export class RolePermissionDialog extends TemplatedDialog ({ PermissionKey: x })); }); - this.permissions.implicitPermissions = getRemoteData('Administration.ImplicitPermissions'); + this.permissions.implicitPermissions = getRemoteData(RemoteDataKeys.Administration.ImplicitPermissions); } protected getDialogOptions() { @@ -43,7 +43,7 @@ export class RolePermissionDialog extends TemplatedDialog this.dialogClose() }]; - opt.title = format(localText('Site.RolePermissionDialog.DialogTitle'), + opt.title = stringFormat(localText('Site.RolePermissionDialog.DialogTitle'), this.options.title); return opt; diff --git a/serene/src/Serene.Web/Modules/Administration/Translation/TranslationGrid.ts b/serene/src/Serene.Web/Modules/Administration/Translation/TranslationGrid.ts index 6d5e1b13a2..89ccccbdd9 100644 --- a/serene/src/Serene.Web/Modules/Administration/Translation/TranslationGrid.ts +++ b/serene/src/Serene.Web/Modules/Administration/Translation/TranslationGrid.ts @@ -1,6 +1,7 @@ +import { TranslationService } from "@/ServerTypes/Administration"; import { confirmDialog, Decorators, EntityGrid, Fluent, GridUtils, isEmptyOrNull, isTrimmedEmpty, localText, LookupEditor, LookupEditorOptions, notifySuccess, outerHtml, stripDiacritics, ToolButton, trimToEmpty, trimToNull, Widget } from "@serenity-is/corelib"; +import { TranslationItem } from "@serenity-is/extensions"; import { Column } from "@serenity-is/sleekgrid"; -import { TranslationItem, TranslationService } from "../"; @Decorators.registerClass() export class TranslationGrid extends EntityGrid { @@ -10,7 +11,7 @@ export class TranslationGrid extends EntityGrid { private hasChanges: boolean; private searchText: string; - private sourceLanguage: LookupEditor; + private sourceLanguage: LookupEditor; private targetLanguage: LookupEditor; private targetLanguageKey: string; @@ -41,7 +42,7 @@ export class TranslationGrid extends EntityGrid { if (Fluent(e.target).hasClass('source-text')) { e.preventDefault(); - + done = () => { item.CustomText = item.SourceText; this.view.updateItem(item.Key, item); @@ -183,7 +184,7 @@ export class TranslationGrid extends EntityGrid { this.hasChanges = false; return super.onViewSubmit(); } - + protected getButtons(): ToolButton[] { return [{ title: localText('Db.Administration.Translation.SaveChangesButton'), diff --git a/serene/src/Serene.Web/Modules/Administration/User/Authentication/Authorization.ts b/serene/src/Serene.Web/Modules/Administration/User/Authentication/Authorization.ts index 6f0b7eaa73..729f78cffc 100644 --- a/serene/src/Serene.Web/Modules/Administration/User/Authentication/Authorization.ts +++ b/serene/src/Serene.Web/Modules/Administration/User/Authentication/Authorization.ts @@ -1,5 +1,5 @@ +import { ScriptUserDefinition } from "@/ServerTypes/ScriptUserDefinition"; import { getRemoteData } from "@serenity-is/corelib"; -import { ScriptUserDefinition } from "../.."; export function userDefinition() { return getRemoteData('UserData') as ScriptUserDefinition; diff --git a/serene/src/Serene.Web/Modules/Administration/User/UserDialog.ts b/serene/src/Serene.Web/Modules/Administration/User/UserDialog.ts index 8c23d5e384..8229430c1c 100644 --- a/serene/src/Serene.Web/Modules/Administration/User/UserDialog.ts +++ b/serene/src/Serene.Web/Modules/Administration/User/UserDialog.ts @@ -1,8 +1,7 @@ -import { UserForm, UserRow, UserService } from "../"; -import { UserPermissionDialog } from "../UserPermission/UserPermissionDialog"; -import { Decorators, EditorUtils, EntityDialog } from "@serenity-is/corelib" -import { format, localText } from "@serenity-is/corelib"; +import { UserForm, UserRow, UserService } from "@/ServerTypes/Administration"; import { Texts } from "@/ServerTypes/Texts"; +import { Decorators, EditorUtils, EntityDialog, localText, stringFormat } from "@serenity-is/corelib"; +import { UserPermissionDialog } from "../UserPermission/UserPermissionDialog"; @Decorators.registerClass() export class UserDialog extends EntityDialog { @@ -24,7 +23,7 @@ export class UserDialog extends EntityDialog { this.form.Password.addValidationRule(this.uniqueName, e => { if (this.form.Password.value.length < 6) - return format(localText(Texts.Validation.MinRequiredPasswordLength), 6); + return stringFormat(localText(Texts.Validation.MinRequiredPasswordLength), 6); }); this.form.PasswordConfirm.addValidationRule(this.uniqueName, e => { diff --git a/serene/src/Serene.Web/Modules/Administration/UserPermission/PermissionCheckEditor.ts b/serene/src/Serene.Web/Modules/Administration/UserPermission/PermissionCheckEditor.ts deleted file mode 100644 index 3b2801ca3a..0000000000 --- a/serene/src/Serene.Web/Modules/Administration/UserPermission/PermissionCheckEditor.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { DataGrid, Decorators, Dictionary, Fluent, GridUtils, Grouping, IGetEditValue, ISetEditValue, SlickFormatting, SlickTreeHelper, ToolButton, WidgetProps, any, count, getRemoteData, htmlEncode, localText, stripDiacritics, toGrouping, trimToNull, tryGetText, turkishLocaleCompare } from "@serenity-is/corelib"; -import { Column } from "@serenity-is/sleekgrid"; -import { UserPermissionRow } from "../"; - -@Decorators.registerEditor('Serene.Administration.PermissionCheckEditor', [IGetEditValue, ISetEditValue]) -export class PermissionCheckEditor extends DataGrid { - - protected getIdProperty() { return "Key"; } - - private searchText: string; - private byParentKey: Grouping; - - constructor(props: WidgetProps) { - super(props); - - let titleByKey: Dictionary = {}; - let permissionKeys = this.getSortedGroupAndPermissionKeys(titleByKey); - let items = permissionKeys.map(key => { - Key: key, - ParentKey: this.getParentKey(key), - Title: titleByKey[key], - GrantRevoke: null, - IsGroup: key.charAt(key.length - 1) === ':' - }); - - this.byParentKey = toGrouping(items, x => x.ParentKey); - this.setItems(items); - } - - private getItemGrantRevokeClass(item: PermissionCheckItem, grant: boolean): string { - if (!item.IsGroup) { - return ((item.GrantRevoke === grant) ? ' checked' : ''); - } - - let desc = this.getDescendants(item, true); - let granted = desc.filter(x => x.GrantRevoke === grant); - - if (!granted.length) { - return ''; - } - - if (desc.length === granted.length) { - return 'checked'; - } - - return 'checked partial'; - } - - private roleOrImplicit(key): boolean { - if (this._rolePermissions[key]) - return true; - - for (var k of Object.keys(this._rolePermissions)) { - var d = this._implicitPermissions[k]; - if (d && d[key]) - return true; - } - - for (var i of Object.keys(this._implicitPermissions)) { - var item = this.view.getItemById(i); - if (item && item.GrantRevoke == true) { - var d = this._implicitPermissions[i]; - if (d && d[key]) - return true; - } - } - } - - private getItemEffectiveClass(item: PermissionCheckItem): string { - - if (item.IsGroup) { - let desc = this.getDescendants(item, true); - let grantCount = count(desc, x => x.GrantRevoke === true || - (x.GrantRevoke == null && this.roleOrImplicit(x.Key))); - - if (grantCount === desc.length || desc.length === 0) { - return 'allow'; - } - - if (grantCount === 0) { - return 'deny'; - } - - return 'partial'; - } - - let granted = item.GrantRevoke === true || - (item.GrantRevoke == null && this.roleOrImplicit(item.Key)); - - return (granted ? ' allow' : ' deny'); - } - - protected getColumns(): Column[] { - let columns: Column[] = [{ - name: localText('Site.UserPermissionDialog.Permission'), - field: 'Title', - format: SlickFormatting.treeToggle(() => this.view, x => x.Key, ctx => { - let item = ctx.item; - let klass = this.getItemEffectiveClass(item); - return '' + htmlEncode(ctx.value) + ''; - }), - width: 495, - sortable: false - }, { - name: localText('Site.UserPermissionDialog.Grant'), field: 'Grant', - format: ctx => { - let item1 = ctx.item; - let klass1 = this.getItemGrantRevokeClass(item1, true); - return ""; - }, - width: 65, - sortable: false, - headerCssClass: 'align-center', - cssClass: 'align-center' - }]; - - if (this.options.showRevoke) { - columns.push({ - name: localText('Site.UserPermissionDialog.Revoke'), field: 'Revoke', - format: ctx => { - let item2 = ctx.item; - let klass2 = this.getItemGrantRevokeClass(item2, false); - return ''; - }, - width: 65, - sortable: false, - headerCssClass: 'align-center', - cssClass: 'align-center' - }); - } - - return columns; - } - - public setItems(items: PermissionCheckItem[]): void { - SlickTreeHelper.setIndents(items, x => x.Key, x => x.ParentKey, false); - this.view.setItems(items, true); - } - - protected onViewSubmit() { - return false; - } - - protected onViewFilter(item: PermissionCheckItem): boolean { - if (!super.onViewFilter(item)) { - return false; - } - - if (!SlickTreeHelper.filterById(item, this.view, x => x.ParentKey)) - return false; - - if (this.searchText) { - return this.matchContains(item) || item.IsGroup && any(this.getDescendants(item, false), x => this.matchContains(x)); - } - - return true; - } - - private matchContains(item: PermissionCheckItem): boolean { - return stripDiacritics(item.Title || '').toLowerCase().indexOf(this.searchText) >= 0 || - stripDiacritics(item.Key || '').toLowerCase().indexOf(this.searchText) >= 0; - } - - private getDescendants(item: PermissionCheckItem, excludeGroups: boolean): PermissionCheckItem[] { - let result: PermissionCheckItem[] = []; - let stack = [item]; - while (stack.length > 0) { - let i = stack.pop(); - let children = this.byParentKey[i.Key]; - if (!children) - continue; - - for (let child of children) { - if (!excludeGroups || !child.IsGroup) { - result.push(child); - } - - stack.push(child); - } - } - - return result; - } - - protected onClick(e: MouseEvent, row, cell): void { - super.onClick(e, row, cell); - - if (!e.defaultPrevented && !(e as any).isDefaultPrevented?.()) { - SlickTreeHelper.toggleClick(e, row, cell, this.view, x => x.Key); - } - - if (e.defaultPrevented || (e as any).isDefaultPrevented?.()) { - return; - } - - let target = Fluent(e.target); - let grant = target.hasClass('grant'); - - if (grant || target.hasClass('revoke')) { - e.preventDefault(); - - let item = this.itemAt(row); - let checkedOrPartial = target.hasClass('checked') || target.hasClass('partial'); - - if (checkedOrPartial) { - grant = null; - } - else { - grant = grant !== checkedOrPartial; - } - - if (item.IsGroup) { - for (var d of this.getDescendants(item, true)) { - d.GrantRevoke = grant; - } - } - else - item.GrantRevoke = grant; - - this.slickGrid.invalidate(); - } - } - - private getParentKey(key): string { - if (key.charAt(key.length - 1) === ':') { - key = key.substr(0, key.length - 1); - } - - let idx = key.lastIndexOf(':'); - if (idx >= 0) { - return key.substr(0, idx + 1); - } - return null; - } - - protected getButtons(): ToolButton[] { - return []; - } - - protected createToolbarExtensions(): void { - super.createToolbarExtensions(); - GridUtils.addQuickSearchInputCustom(this.toolbar.element, (_, text) => { - this.searchText = stripDiacritics(trimToNull(text) || '').toLowerCase(); - this.view.setItems(this.view.getItems(), true); - }); - } - - private getSortedGroupAndPermissionKeys(titleByKey: Dictionary): string[] { - let keys = getRemoteData('Administration.PermissionKeys'); - let titleWithGroup = {}; - for (var k of keys) { - let s = k; - - if (!s) { - continue; - } - - if (s.charAt(s.length - 1) == ':') { - s = s.substring(0, s.length - 1); - if (s.length === 0) { - continue; - } - } - - if (titleByKey[s]) { - continue; - } - - titleByKey[s] = tryGetText('Permission.' + s) ?? s; - let parts = s.split(':'); - let group = ''; - let groupTitle = ''; - for (let i = 0; i < parts.length - 1; i++) { - group = group + parts[i] + ':'; - let txt = tryGetText('Permission.' + group); - if (txt == null) { - txt = parts[i]; - } - titleByKey[group] = txt; - groupTitle = groupTitle + titleByKey[group] + ':'; - titleWithGroup[group] = groupTitle; - } - - titleWithGroup[s] = groupTitle + titleByKey[s]; - } - - keys = Object.keys(titleByKey); - keys = keys.sort((x, y) => turkishLocaleCompare(titleWithGroup[x], titleWithGroup[y])); - - return keys; - } - - get value(): UserPermissionRow[] { - - let result: UserPermissionRow[] = []; - - for (let item of this.view.getItems()) { - if (item.GrantRevoke != null && item.Key.charAt(item.Key.length - 1) != ':') { - result.push({ PermissionKey: item.Key, Granted: item.GrantRevoke }); - } - } - - return result; - } - - set value(value: UserPermissionRow[]) { - - for (let item of this.view.getItems()) { - item.GrantRevoke = null; - } - - if (value != null) { - for (let row of value) { - let r = this.view.getItemById(row.PermissionKey); - if (r) { - r.GrantRevoke = row.Granted ?? true; - } - } - } - - this.setItems(this.getItems()); - } - - private _rolePermissions: Dictionary = {}; - - get rolePermissions(): string[] { - return Object.keys(this._rolePermissions); - } - - set rolePermissions(value: string[]) { - this._rolePermissions = {}; - - if (value) { - for (let k of value) { - this._rolePermissions[k] = true; - } - } - - this.setItems(this.getItems()); - } - - private _implicitPermissions: Dictionary> = {}; - - set implicitPermissions(value: Dictionary) { - this._implicitPermissions = {}; - - if (value) { - for (var k of Object.keys(value)) { - this._implicitPermissions[k] = this._implicitPermissions[k] || {}; - var l = value[k]; - if (l) { - for (var s of l) - this._implicitPermissions[k][s] = true; - } - } - } - } -} - -export interface PermissionCheckEditorOptions { - showRevoke?: boolean; -} - -export interface PermissionCheckItem { - ParentKey?: string; - Key?: string; - Title?: string; - IsGroup?: boolean; - GrantRevoke?: boolean; -} diff --git a/serene/src/Serene.Web/Modules/Administration/UserPermission/PermissionCheckEditor.tsx b/serene/src/Serene.Web/Modules/Administration/UserPermission/PermissionCheckEditor.tsx new file mode 100644 index 0000000000..119068e6c4 --- /dev/null +++ b/serene/src/Serene.Web/Modules/Administration/UserPermission/PermissionCheckEditor.tsx @@ -0,0 +1,292 @@ +import { RemoteDataKeys } from "@/ServerTypes/RemoteDataKeys"; +import { Texts } from "@/ServerTypes/Texts"; +import { + DataGrid, Decorators, Dictionary, Fluent, GridUtils, Grouping, IGetEditValue, ISetEditValue, SlickFormatting, SlickTreeHelper, ToolButton, WidgetProps, count, getRemoteDataAsync, stripDiacritics, toGrouping, tryGetText, turkishLocaleCompare +} from "@serenity-is/corelib"; +import { Column } from "@serenity-is/sleekgrid"; +import { UserPermissionRow } from "../../ServerTypes/Administration"; + +export interface PermissionCheckEditorOptions { + showRevoke?: boolean; + value?: (UserPermissionRow | string)[]; + rolePermissions?: string[]; + implicitPermissions?: Record; +} + +export interface PermissionCheckItem { + ParentKey?: string; + Key?: string; + Title?: string; + IsGroup?: boolean; + GrantRevoke?: boolean; +} + +@Decorators.registerEditor('Serene.Administration.PermissionCheckEditor', [IGetEditValue, ISetEditValue]) +export class PermissionCheckEditor

extends DataGrid { + + protected getIdProperty() { return "Key"; } + + declare private searchText: string; + declare private byParentKey: Grouping; + + constructor(props: WidgetProps

) { + super(props); + + let titleByKey: Dictionary = {}; + this.getSortedGroupAndPermissionKeys(titleByKey, (permissionKeys) => { + if (!this.domNode) + return; + + let items = permissionKeys.map(key => ({ + Key: key, + ParentKey: this.getParentKey(key), + Title: titleByKey[key], + GrantRevoke: null, + IsGroup: key.charAt(key.length - 1) === ':' + } satisfies PermissionCheckItem)); + + this.byParentKey = toGrouping(items, x => x.ParentKey); + this.setItems(items); + this.value = this._value; + }); + + if (this.options.value) { + if (typeof this.options.value[0] === "string") + this.valueAsStrings = this.options.value as string[]; + else + this.value = this.options.value as UserPermissionRow[]; + } + + this.implicitPermissions = props.implicitPermissions; + this.rolePermissions = props.rolePermissions; + } + + private getItemGrantRevokeClass(item: PermissionCheckItem, grant: boolean): string { + if (!item.IsGroup) + return ((item.GrantRevoke === grant) ? ' checked' : ''); + + const desc = this.getDescendants(item, true); + const granted = desc.filter(x => x.GrantRevoke === grant); + return !granted.length ? '' : ((desc.length === granted.length) ? 'checked' : 'partial'); + } + + private hasByRoleOrImplicitly(permission: string): boolean { + return this.rolePermSet.has(permission) || Object.entries(this.implicitSets).some(([ifperm, permset]) => + permset.has(permission) && (this.rolePermSet.has(ifperm) || this.view.getItemById(ifperm)?.GrantRevoke)); + } + + private getItemEffectiveClass(item: PermissionCheckItem): string { + if (item.IsGroup) { + const desc = this.getDescendants(item, true); + const grantCount = count(desc, x => x.GrantRevoke === true || (x.GrantRevoke == null && this.hasByRoleOrImplicitly(x.Key))); + return (grantCount === desc.length || desc.length === 0) ? 'allow' : (grantCount === 0 ? 'deny' : 'partial'); + } + + return (item.GrantRevoke === true || (item.GrantRevoke == null && this.hasByRoleOrImplicitly(item.Key))) ? 'allow' : 'deny'; + } + + protected getColumns(): Column[] { + let columns: Column[] = [{ + name: Texts.Site.UserPermissionDialog.Permission, + field: 'Title', + format: SlickFormatting.treeToggle(() => this.view, x => x.Key, ctx => {ctx.value}), + width: 495, + sortable: false + }, { + name: Texts.Site.UserPermissionDialog.Grant, + field: 'Grant', + format: ctx => , + width: 65, + sortable: false, + headerCssClass: 'align-center', + cssClass: 'align-center' + }]; + + if (this.options.showRevoke) { + columns.push({ + name: Texts.Site.UserPermissionDialog.Revoke, + field: 'Revoke', + format: ctx => , + width: 65, + sortable: false, + headerCssClass: 'align-center', + cssClass: 'align-center' + }); + } + + return columns; + } + + public setItems(items: PermissionCheckItem[]): void { + SlickTreeHelper.setIndents(items, x => x.Key, x => x.ParentKey, false); + this.view.setItems(items, true); + } + + protected onViewSubmit() { + return false; + } + + protected onViewFilter(item: PermissionCheckItem): boolean { + return super.onViewFilter(item) && SlickTreeHelper.filterById(item, this.view, x => x.ParentKey) && + (!this.searchText || (this.matchContains(item) || item.IsGroup && this.getDescendants(item, false).some(x => this.matchContains(x)))); + } + + private matchContains(item: PermissionCheckItem): boolean { + return stripDiacritics(item.Title || '').toLowerCase().indexOf(this.searchText) >= 0; + } + + private getDescendants(item: PermissionCheckItem, excludeGroups: boolean): PermissionCheckItem[] { + const result: PermissionCheckItem[] = []; + const stack = [item]; + while (stack.length > 0) { + let i = stack.pop(); + let children = this.byParentKey[i.Key]; + if (!children) + continue; + + for (let child of children) { + if (!excludeGroups || !child.IsGroup) { + result.push(child); + } + + stack.push(child); + } + } + + return result; + } + + protected onClick(e: Event, row: number, cell: number): void { + super.onClick(e, row, cell); + + if (!Fluent.isDefaultPrevented(e)) + SlickTreeHelper.toggleClick(e, row, cell, this.view, (x: any) => x.Key); + + if (Fluent.isDefaultPrevented(e)) + return; + + const target = Fluent(e.target); + let grant = target.hasClass('grant'); + + if (grant || target.hasClass('revoke')) { + e.preventDefault(); + + let item = this.itemAt(row); + let checkedOrPartial = target.hasClass('checked') || target.hasClass('partial'); + grant = checkedOrPartial ? null : grant !== checkedOrPartial; + + if (item.IsGroup) + this.getDescendants(item, true).forEach(x => x.GrantRevoke = grant); + else + item.GrantRevoke = grant; + + this.slickGrid.invalidate(); + } + } + + private getParentKey(key: string): string { + if (key.charAt(key.length - 1) === ':') + key = key.substring(0, key.length - 1); + + const idx = key.lastIndexOf(':'); + return idx >= 0 ? key.substr(0, idx + 1) : null; + } + + protected getButtons(): ToolButton[] { + return []; + } + + protected createToolbarExtensions(): void { + super.createToolbarExtensions(); + GridUtils.addQuickSearchInputCustom(this.toolbar.domNode, (_, text) => { + this.searchText = stripDiacritics(text?.trim() ?? '').toLowerCase(); + this.view.setItems(this.view.getItems(), true); + }); + } + + private getSortedGroupAndPermissionKeys(titleByKey: Dictionary, then: (result: string[]) => void) { + getRemoteDataAsync(RemoteDataKeys.Administration.PermissionKeys).then((keys: string[]) => { + let titleWithGroup = {}; + for (var s of keys.filter(s => s)) { + if (s.charAt(s.length - 1) == ':') { + s = s.substring(0, s.length - 1); + if (s.length === 0) { + continue; + } + } + + if (titleByKey[s]) { + continue; + } + + titleByKey[s] = tryGetText('Permission.' + s) ?? s; + let parts = s.split(':'); + let group = ''; + let groupTitle = ''; + for (let i = 0; i < parts.length - 1; i++) { + group = group + parts[i] + ':'; + let txt = tryGetText('Permission.' + group); + if (txt == null) { + txt = parts[i]; + } + titleByKey[group] = txt; + groupTitle = groupTitle + titleByKey[group] + ':'; + titleWithGroup[group] = groupTitle; + } + + titleWithGroup[s] = groupTitle + titleByKey[s]; + } + + keys = Object.keys(titleByKey); + keys = keys.sort((x, y) => turkishLocaleCompare(titleWithGroup[x], titleWithGroup[y])); + then(keys); + }); + } + + declare private _value: UserPermissionRow[]; + + get value(): UserPermissionRow[] { + if (!this.view.getItems().length) // not initialized yet + return (this._value || []).map(x => ({ PermissionKey: x.PermissionKey, Granted: x.Granted })); + + return this.view.getItems().filter(item => item.GrantRevoke != null && item.Key.charAt(item.Key.length - 1) != ':') + .map(item => ({ PermissionKey: item.Key, Granted: item.GrantRevoke })); + } + + set value(value: UserPermissionRow[]) { + this._value = (value || []); + this.view.getItems().forEach(x => { x.GrantRevoke = null }); + for (let item of this._value) { + const r = this.view.getItemById(item.PermissionKey); + r && (r.GrantRevoke = item.Granted ?? true); + } + this.setItems(this.getItems()); + } + + get valueAsStrings() { + return this.value.map(x => x.PermissionKey); + } + + set valueAsStrings(value: string[]) { + this.value = (value || []).map(x => ({ PermissionKey: x })); + } + + private rolePermSet: Set = new Set(); + + get rolePermissions(): string[] { + return Array.from(this.rolePermSet.values()); + } + + set rolePermissions(value: string[]) { + this.rolePermSet = new Set(); + value?.forEach(x => this.rolePermSet.add(x)); + } + + private implicitSets: Record> = {}; + + set implicitPermissions(value: Record) { + this.implicitSets = {}; + Object.entries(value || {}).forEach(([ifperm, permarr]) => + permarr?.forEach(perm => (this.implicitSets[ifperm] ||= new Set()).add(perm))); + } +} \ No newline at end of file diff --git a/serene/src/Serene.Web/Modules/Administration/UserPermission/UserPermissionDialog.tsx b/serene/src/Serene.Web/Modules/Administration/UserPermission/UserPermissionDialog.tsx index 724cb3febf..9cb8da3607 100644 --- a/serene/src/Serene.Web/Modules/Administration/UserPermission/UserPermissionDialog.tsx +++ b/serene/src/Serene.Web/Modules/Administration/UserPermission/UserPermissionDialog.tsx @@ -1,6 +1,7 @@ import { getRemoteData, localText, notifySuccess, stringFormat, BaseDialog } from "@serenity-is/corelib"; -import { UserPermissionService } from "../"; +import { UserPermissionService } from "../../ServerTypes/Administration/UserPermissionService"; import { PermissionCheckEditor } from "./PermissionCheckEditor"; +import { RemoteDataKeys } from "../../ServerTypes/RemoteDataKeys"; export class UserPermissionDialog extends BaseDialog { @@ -25,7 +26,7 @@ export class UserPermissionDialog extends BaseDialog { + }, () => { this.dialogClose(); window.setTimeout(() => notifySuccess(localText('Site.UserPermissionDialog.SaveSuccess')), 0); }); diff --git a/serene/src/Serene.Web/Modules/Administration/UserRole/RoleCheckEditor.ts b/serene/src/Serene.Web/Modules/Administration/UserRole/RoleCheckEditor.ts index d94355490b..680d1dfdb8 100644 --- a/serene/src/Serene.Web/Modules/Administration/UserRole/RoleCheckEditor.ts +++ b/serene/src/Serene.Web/Modules/Administration/UserRole/RoleCheckEditor.ts @@ -1,6 +1,5 @@ -import { CheckTreeEditor, CheckTreeItem, Decorators, GridUtils, stripDiacritics } from "@serenity-is/corelib"; -import { isEmptyOrNull } from "@serenity-is/corelib"; -import { RoleRow } from "../"; +import { RoleRow } from "@/ServerTypes/Administration/RoleRow"; +import { CheckTreeEditor, CheckTreeItem, Decorators, GridUtils, isEmptyOrNull, stripDiacritics } from "@serenity-is/corelib"; @Decorators.registerEditor('Serene.Administration.RoleCheckEditor') export class RoleCheckEditor extends CheckTreeEditor, any> { diff --git a/serene/src/Serene.Web/Modules/Administration/UserRole/UserRoleDialog.ts b/serene/src/Serene.Web/Modules/Administration/UserRole/UserRoleDialog.ts index b5a3b4753c..2193ee1f38 100644 --- a/serene/src/Serene.Web/Modules/Administration/UserRole/UserRoleDialog.ts +++ b/serene/src/Serene.Web/Modules/Administration/UserRole/UserRoleDialog.ts @@ -1,10 +1,9 @@ -import { Decorators, TemplatedDialog } from "@serenity-is/corelib"; -import { DialogButton, format, notifySuccess, serviceRequest, localText } from "@serenity-is/corelib"; -import { UserRoleService } from "../"; +import { UserRoleService } from "@/ServerTypes/Administration/UserRoleService"; +import { BaseDialog, Decorators, DialogButton, localText, notifySuccess, serviceRequest, stringFormat } from "@serenity-is/corelib"; import { RoleCheckEditor } from "./RoleCheckEditor"; @Decorators.registerClass() -export class UserRoleDialog extends TemplatedDialog { +export class UserRoleDialog extends BaseDialog { private permissions: RoleCheckEditor; @@ -19,7 +18,7 @@ export class UserRoleDialog extends TemplatedDialog { this.permissions.value = response.Entities.map(x => x.toString()); }); - this.dialogTitle = format(localText('Site.UserRoleDialog.DialogTitle'), this.options.username); + this.dialogTitle = stringFormat(localText('Site.UserRoleDialog.DialogTitle'), this.options.username); } protected getDialogButtons(): DialogButton[] { diff --git a/serene/src/Serene.Web/Modules/Administration/index.ts b/serene/src/Serene.Web/Modules/Administration/index.ts deleted file mode 100644 index a5e6a86668..0000000000 --- a/serene/src/Serene.Web/Modules/Administration/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "../ServerTypes/Administration"; -export * from "../ServerTypes/ScriptUserDefinition"; \ No newline at end of file diff --git a/serene/src/Serene.Web/Modules/Common/Helpers/LanguageList.ts b/serene/src/Serene.Web/Modules/Common/Helpers/LanguageList.ts index 2a797e7238..ab6d158633 100644 --- a/serene/src/Serene.Web/Modules/Common/Helpers/LanguageList.ts +++ b/serene/src/Serene.Web/Modules/Common/Helpers/LanguageList.ts @@ -1,4 +1,4 @@ -import { LanguageRow } from "../../Administration"; +import { LanguageRow } from "@/ServerTypes/Administration/LanguageRow"; export function siteLanguageList() { var result: string[][] = [];