< Summary

Information
Class: AspNetCoreGeneratedDocument.Areas_Admin_Views_Roles_Pages
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Admin/Views/Roles/Pages.cshtml
Line coverage
0%
Covered lines: 0
Uncovered lines: 14
Coverable lines: 14
Total lines: 203
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 20
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExecuteAsync()0%420200%
<ExecuteAsync()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Admin/Views/Roles/Pages.cshtml

#LineLine coverage
 1@model List<RolePageMatrixRow>
 2@{
 03    var role = (OzonDomains.Models.Authorization.AppRole)ViewBag.Role;
 04    var presetJson = ViewBag.Studio2RolePagePresetsJson as string ?? "{}";
 05    var json = new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web);
 6}
 7
 8<section class="admin-hero">
 9    <h1>Матрица страниц</h1>
 010    <p class="subtitle">Настройка доступа к страницам для роли @role.DisplayName.</p>
 11    <p class="subtitle-help">Изменения сохраняются сразу по строке; галка в заголовке колонки — для всех страниц.</p>
 12</section>
 13
 14<nav class="admin-quick-links">
 015    <a asp-area="Admin" asp-controller="Roles" asp-action="Edit" asp-route-id="@role.Id"><i class="bi bi-arrow-left"></i
 16    <a asp-area="Admin" asp-controller="Pages" asp-action="Index"><i class="bi bi-window-stack"></i>Реестр страниц</a>
 17    <a asp-area="Admin" asp-controller="Users" asp-action="Index"><i class="bi bi-people"></i>Пользователи</a>
 18</nav>
 19
 20<div class="card mb-3">
 21    <div class="card-body d-flex justify-content-between align-items-center">
 22        <div>
 23            <div class="text-muted small">Роль</div>
 024            <h5 class="mb-0">@role.DisplayName <small class="text-muted">(<code>@role.Name</code>)</small></h5>
 25        </div>
 26        <div>
 027            <a asp-action="Edit" asp-route-id="@role.Id" class="btn btn-outline-secondary btn-sm"><i class="bi bi-arrow-
 28        </div>
 29    </div>
 30</div>
 31
 32<div class="admin-toolbar admin-surface">
 33    <div class="admin-matrix-toolbar">
 34        <div class="btn-group btn-group-sm" role="group" aria-label="Пресеты Studio2">
 35            <button type="button" class="btn btn-outline-primary" id="btnPresetOrders" title="Просмотр + создание: весь 
 36            <button type="button" class="btn btn-outline-primary" id="btnPresetTx" title="Просмотр + создание: все типы 
 37            <button type="button" class="btn btn-outline-primary" id="btnPresetBoth" title="Просмотр + создание: заказы 
 38        </div>
 39        <div class="dropdown d-inline-block">
 40            <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" dat
 41            <ul class="dropdown-menu admin-scroll-menu" id="txSinglePresetMenu">
 42                <li><h6 class="dropdown-header">Основные</h6></li>
 43                <li><a class="dropdown-item" href="#" data-tx-preset="txOrderToSupplier">Заказан поставщику</a></li>
 44                <li><a class="dropdown-item" href="#" data-tx-preset="txOrderedToSeller">Заказан реализатору</a></li>
 45                <li><a class="dropdown-item" href="#" data-tx-preset="txShippedBySupplier">Отгружен поставщиком</a></li>
 46                <li><a class="dropdown-item" href="#" data-tx-preset="txShippedToSeller">Отгружен реализатором</a></li>
 47                <li><a class="dropdown-item" href="#" data-tx-preset="txShippedToCommissioner">Отгружен комиссионеру</a>
 48                <li><a class="dropdown-item" href="#" data-tx-preset="txAcceptedToWarehouse">Принят на склад</a></li>
 49                <li><a class="dropdown-item" href="#" data-tx-preset="txShippedToClient">Отгружен клиенту</a></li>
 50                <li><h6 class="dropdown-header">Возвраты</h6></li>
 51                <li><a class="dropdown-item" href="#" data-tx-preset="txCustomerReturn">Возврат покупателя</a></li>
 52                <li><a class="dropdown-item" href="#" data-tx-preset="txReturnToSupplier">Возвращено поставщику</a></li>
 53                <li><a class="dropdown-item" href="#" data-tx-preset="txReturnToWarehouse">Возврат на склад</a></li>
 54                <li><a class="dropdown-item" href="#" data-tx-preset="txReturnTransfer">Перемещение</a></li>
 55                <li><h6 class="dropdown-header">Склад</h6></li>
 56                <li><a class="dropdown-item" href="#" data-tx-preset="txWarehouseChange">Изменение склада</a></li>
 57                <li><a class="dropdown-item" href="#" data-tx-preset="txWarehouseTransfer">Перемещение между складами</a
 58                <li><h6 class="dropdown-header">Отмена</h6></li>
 59                <li><a class="dropdown-item" href="#" data-tx-preset="txToCancellation">К отмене</a></li>
 60                <li><a class="dropdown-item" href="#" data-tx-preset="txCancelled">Отменен</a></li>
 61            </ul>
 62        </div>
 63        <button type="button" class="btn btn-sm btn-outline-success" id="btnCheckAllView">Все «Просмотр»</button>
 64        <button type="button" class="btn btn-sm btn-outline-danger" id="btnClearAll">Очистить</button>
 65        <span class="admin-field-hint ms-2"><i class="bi bi-check-circle"></i> Авто-сохранение по строке</span>
 66    </div>
 67    </div>
 68</div>
 69
 070@await Html.PartialAsync("~/Areas/Studio2/Views/Shared/_BsxDirectoryGrid.cshtml", new BsxDirectoryGridViewModel("admRole
 071    "Строки-разделы — группы меню Studio2. Фильтр колонки права показывает только отмеченные или только снятые."))
 072<script type="application/json" id="admRolePagesData">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model, json))<
 73
 74@section Scripts {
 75    <script>
 76    (function () {
 77        const A = window.AdminGrid, esc = A.esc;
 078        const roleId = @role.Id;
 079        const studio2Presets = @Html.Raw(presetJson);
 80        const COLS = ['canView', 'canCreate', 'canEdit', 'canDelete', 'canExport'];
 081        const saveOneUrl = '@Url.Action("SavePagePermission")?roleId=' + roleId;
 082        const saveBulkUrl = '@Url.Action("SavePagePermissionsBulk")?roleId=' + roleId;
 83        const rows = A.data('admRolePagesData').map(r => Object.assign(r, { id: r.pageId }));
 84        let api = null;
 85
 86        const toast = (title, message, variant) => window.AppToast ? AppToast.show({ title, message, variant: variant ||
 87        const input = r => ({ PageId: r.pageId, CanView: !!r.canView, CanCreate: !!r.canCreate, CanEdit: !!r.canEdit, Ca
 88        const redraw = list => api && api.redrawRows({ rowNodes: list.map(r => api.getRowNode(String(r.id))).filter(Bool
 89        const post = (url, body) => fetch(url, {
 90            method: 'POST', credentials: 'same-origin',
 91            headers: { 'Content-Type': 'application/json', RequestVerificationToken: BsxGrid.token() },
 92            body: JSON.stringify(body)
 93        });
 94        const dataRows = () => rows.filter(r => !r.isSectionHeader);
 95
 96        async function saveOne(row) {
 97            row.__saving = true; redraw([row]);
 98            const resp = await post(saveOneUrl, input(row)).catch(() => null);
 99            row.__saving = false; redraw([row]);
 100            if (!resp || !resp.ok) toast('Ошибка сохранения', 'Не удалось сохранить строку. Повторите или обновите стран
 101        }
 102
 103        /** Один POST вместо сотен — иначе срабатывает rate limit (429). */
 104        async function saveBulk(list, flash) {
 105            if (!list.length) return true;
 106            list.forEach(r => { r.__saving = true; if (flash) r.__flash = true; });
 107            redraw(list);
 108            const resp = await post(saveBulkUrl, list.map(input)).catch(() => null);
 109            list.forEach(r => { r.__saving = false; });
 110            redraw(list);
 111            if (flash) setTimeout(() => { list.forEach(r => { r.__flash = false; }); redraw(list); }, 4500);
 112            if (resp && resp.ok) return true;
 113            toast('Ошибка сохранения', 'Не удалось сохранить (возможен лимит запросов — обновите страницу и повторите).'
 114            return false;
 115        }
 116
 117        function setColumn(field, on) {
 118            const changed = dataRows().filter(r => !!r[field] !== on);
 119            changed.forEach(r => { r[field] = on; });
 120            return saveBulk(changed);
 121        }
 122
 123        async function applyPreset(presetKey, label) {
 124            const codes = studio2Presets && studio2Presets[presetKey];
 125            if (!codes || !codes.length) { toast('Пресет', 'Пресет не найден. Обновите страницу.', 'danger'); return; }
 126            const byCode = new Map(dataRows().map(r => [r.code, r]));
 127            const changed = [], missing = [];
 128            codes.forEach(code => {
 129                const r = byCode.get(code);
 130                if (!r) { missing.push(code); return; }
 131                let dirty = false;
 132                COLS.forEach(c => { const on = c === 'canView' || c === 'canCreate'; if (!!r[c] !== on) { r[c] = on; dir
 133                if (dirty) changed.push(r);
 134            });
 135            if (changed.length) api.ensureNodeVisible(api.getRowNode(String(changed[0].id)), 'middle');
 136            const ok = await saveBulk(changed, true);
 137            if (!ok) return;
 138            let body = changed.length
 139                ? 'Обновлено строк: ' + changed.length + '. Изменённые строки подсвечены на ~4 с.'
 140                : 'Все нужные права «Просмотр» и «Создание» для пресета «' + label + '» уже стоят — строки не трогали.';
 141            if (missing.length) body += ' В реестре не найдено страниц: ' + missing.length + '.';
 142            toast('Пресет: ' + label, body, changed.length ? 'success' : 'info');
 143        }
 144
 145        const onToggleAll = (field, checked) => setColumn(field, checked);
 146        A.grid({
 147            key: 'admRolePages', prefix: 'admRolePages', rows: rows, rowHeight: 44,
 148            columnDefs: [
 149                {
 150                    colId: 'page', field: 'displayName', headerName: 'Страница', minWidth: 320, flex: 3, sortable: false
 151                    getQuickFilterText: p => p.data.displayName + ' ' + p.data.code,
 152                    cellRenderer: p => {
 153                        const r = p.data; if (!r) return '';
 154                        return '<span class="admin-tree-cell" style="padding-left:' + (r.depth * 18) + 'px">' +
 155                            (r.depth > 0 ? '<i class="fas fa-turn-up fa-rotate-90"></i>' : '') +
 156                            (r.icon ? '<i class="' + esc(r.icon) + ' ' + esc(r.accentClass || '') + '" aria-hidden="true
 157                            '<span class="bsx-cell-stack"><b>' + esc(r.displayName) + '</b><span class="admin-code">' + 
 158                    }
 159                },
 160                A.checkboxCol('canView', 'Просмотр', { onToggle: (r, f, v) => { r[f] = v; saveOne(r); }, onToggleAll }),
 161                A.checkboxCol('canCreate', 'Создание', { onToggle: (r, f, v) => { r[f] = v; saveOne(r); }, onToggleAll }
 162                A.checkboxCol('canEdit', 'Редакт.', { onToggle: (r, f, v) => { r[f] = v; saveOne(r); }, onToggleAll }),
 163                A.checkboxCol('canDelete', 'Удаление', { onToggle: (r, f, v) => { r[f] = v; saveOne(r); }, onToggleAll }
 164                A.checkboxCol('canExport', 'Экспорт', { onToggle: (r, f, v) => { r[f] = v; saveOne(r); }, onToggleAll })
 165            ],
 166            rowClassRules: {
 167                'admin-row-saving': p => !!(p.data && p.data.__saving),
 168                'admin-row-flash': p => !!(p.data && p.data.__flash)
 169            },
 170            gridOptions: {
 171                isFullWidthRow: p => !!(p.rowNode.data && p.rowNode.data.isSectionHeader),
 172                fullWidthCellRenderer: p => {
 173                    const r = p.data;
 174                    return '<div class="admin-matrix-section" style="padding-left:' + (12 + r.depth * 18) + 'px">' +
 175                        (r.icon ? '<i class="' + esc(r.icon) + ' ' + esc(r.accentClass || '') + '" aria-hidden="true"></
 176                },
 177                overlayNoRowsTemplate: '<span class="bsx-grid-empty">Нет зарегистрированных страниц. Синхронизируйте рее
 178            },
 179            onReady: a => { api = a; }
 180        });
 181
 182        document.getElementById('btnPresetOrders')?.addEventListener('click', () => applyPreset('ordersMenu', 'Заказы (с
 183        document.getElementById('btnPresetTx')?.addEventListener('click', () => applyPreset('transactionsMenu', 'Все тра
 184        document.getElementById('btnPresetBoth')?.addEventListener('click', () => applyPreset('ordersAndTransactions', '
 185        document.getElementById('txSinglePresetMenu')?.addEventListener('click', e => {
 186            const a = e.target && e.target.closest('a[data-tx-preset]');
 187            if (!a) return;
 188            e.preventDefault();
 189            const dd = document.getElementById('ddTxOne');
 190            if (dd && window.bootstrap) bootstrap.Dropdown.getOrCreateInstance(dd).hide();
 191            applyPreset(a.getAttribute('data-tx-preset'), 'Одна транзакция: ' + (a.textContent.trim() || a.getAttribute(
 192        });
 193        document.getElementById('btnCheckAllView').addEventListener('click', () => setColumn('canView', true));
 194        document.getElementById('btnClearAll').addEventListener('click', async () => {
 195            const approved = await window.adminConfirm({ title: 'Снять все права', message: 'Снять все права страниц для
 196            if (!approved) return;
 197            const changed = dataRows().filter(r => COLS.some(c => r[c]));
 198            changed.forEach(r => COLS.forEach(c => { r[c] = false; }));
 199            if (await saveBulk(changed, true)) toast('Права очищены', 'Сняты все права по страницам этой роли; изменённы
 200        });
 201    })();
 202    </script>
 203}

Methods/Properties

ExecuteAsync()
<ExecuteAsync()