Перейти к содержимому
FastOffice

Разрешить изменение диапазонов

Шесть намерений из десяти, что есть у операции: пароль уходит за шов и не возвращается — обратно приходит только способ защиты.

шесть намерений из десяти, что есть у операции: пароль уходит за шов и не возвращается — обратно приходит только способ защиты
открыть в новой вкладке ↗Работающий пример. Ниже — его код целиком, файл за файлом.

В рамке выше работает тот самый проект, чей код лежит ниже, — собранный, но не переписанный: те же файлы, только пропущенные через сборку. Считает в нём не ядро, а дублёр транспорта, поэтому числа в ячейках ничего не значат: смотреть нужно на код.

Страница, которую открывает браузер.

index.html
<!doctype html>
<!--
Разрешить изменение диапазонов — НАСТОЯЩИЙ МАЛЕНЬКИЙ ПРОЕКТ.
Не «страница витрины», а тот проект, который встраивающий копирует к себе
целиком. Галерея примеров ЗАПУСКАЕТ эти файлы и ПОКАЗЫВАЕТ их же.
-->
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Разрешить изменение диапазонов — @f1</title>
</head>
<body>
<div data-example="protected-ranges" class="flex h-screen w-screen flex-col bg-base-100 text-base-content">
<header class="border-b border-base-300 px-4 py-2">
<div class="text-sm font-semibold">Разрешить изменение диапазонов</div>
<div class="text-xs opacity-70">
Права на диапазон живут на операции <code>protected_ranges_permissions</code>: перечислить,
создать, изменить, удалить, снять замок на сеанс и вернуть его. Пароль уходит ЗА ШОВ и
обратно не приходит — возвращается только СПОСОБ защиты. Своего списка закрытых мест у
оболочки нет: он принадлежит листу.
</div>
</header>
<section class="m-4 flex flex-col gap-3 rounded-card border border-base-300 p-4" data-ranges-panel>
<!-- Подписи полей — Excel для macOS, окно «Разрешить изменение
диапазонов» (те же строки ресурсов, что у нашего окна:
`strman_0014.strings`, раздел «Новый диапазон»). -->
<div class="flex flex-wrap items-end gap-2">
<label class="form-control">
<span class="label-text text-xs">Название диапазона:</span>
<input data-ranges-name value="Оклады" class="input input-sm input-bordered w-40" />
</label>
<label class="form-control">
<span class="label-text text-xs">Диапазон ячеек:</span>
<input data-ranges-sqref value="C2:C4" class="input input-sm input-bordered w-32" />
</label>
<label class="form-control">
<span class="label-text text-xs">Пароль диапазона:</span>
<input data-ranges-password type="password" value="секрет" class="input input-sm input-bordered w-32" />
</label>
<button data-ranges-create class="btn btn-sm btn-primary">Создать</button>
<button data-ranges-unlock class="btn btn-sm">Снять замок</button>
<button data-ranges-lock class="btn btn-sm">Вернуть замок</button>
<button data-ranges-delete class="btn btn-sm">Удалить</button>
</div>
<div data-ranges-answer class="text-xs opacity-60">ещё не спрашивали</div>
<div data-ranges-list class="flex flex-col gap-1 text-xs"></div>
<p class="text-xs opacity-60">
Подсветки закрытых мест на самом ЛИСТЕ здесь нет: она принадлежит сетке, и её срез ещё не
сделан. Нарисовать её страницей значило бы показать несделанное сделанным.
</p>
</section>
<div id="app" class="flex min-h-0 flex-1 flex-col"></div>
</div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

Точка входа: здесь лист создаётся и сюда же приходят события.

src/main.ts
import { createSheetsCore } from "@f1/preset-sheets-core";
import type { ProtectedRange, ProtectedRangesResult } from "@f1/facade";
import { EXECUTOR, transport } from "./book.js";
import { hostAsks } from "./host.js";
import "./styles.css";
/**
* ПРАВА НА ДИАПАЗОН ЧЕРЕЗ SDK — ШЕСТЬ НАМЕРЕНИЙ ОДНОЙ ОПЕРАЦИИ.
*
* Ответ на чужую страницу «Hide Permission Background Shadow»: у нас закрытые
* места объявляются и разбираются по-настоящему, а вот подсветки их фона на
* листе нет — она принадлежит сетке, и это сказано на самой странице, а не
* замолчано.
*
* ЧТО ЗДЕСЬ ГЛАВНОЕ И ЧЕГО НЕ ВИДНО В ЧУЖИХ ПРИМЕРАХ:
*
* · ПАРОЛЬ УХОДИТ ЗА ШОВ И НЕ ВОЗВРАЩАЕТСЯ. Обратно приходит `accessKind` —
* способ защиты, — и `unlocked`. Держать пароль в оболочке значило бы завести
* вторую правду о защите ровно там, где она опаснее всего;
* · НОМЕР СОСТОЯНИЯ БЕРЁТСЯ ИЗ ОТВЕТА и уходит ВНУТРИ намерения: правка по
* устаревшему списку отвергается ядром, и это защита от «двое правят одно».
* Фасад ведёт этот номер сам — здесь его нет ни в одной строке;
* · ОТКАЗ — ЗНАЧЕНИЕ, а не исключение: имя занято, пароль не тот, листа нет.
* Код ядра показывается человеку рядом со словами.
*/
const { locale } = hostAsks();
const name = document.querySelector<HTMLInputElement>("[data-ranges-name]")!;
const sqref = document.querySelector<HTMLInputElement>("[data-ranges-sqref]")!;
const password = document.querySelector<HTMLInputElement>("[data-ranges-password]")!;
const answer = document.querySelector<HTMLElement>("[data-ranges-answer]")!;
const list = document.querySelector<HTMLElement>("[data-ranges-list]")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });
const workbook = core.f1.getActiveWorkbook();
const sheetKey = workbook.getActiveSheet()!.getName();
let current: ProtectedRange | null = null;
const ACCESS: Readonly<Record<string, string>> = {
open: "Изменение диапазона без пароля",
password: "Защищён паролем",
domain_permission: "Права домена",
irm: "Управление правами (IRM)",
};
function show(said: ProtectedRangesResult, what: string): void {
if (!said.ok) {
answer.className = "text-xs text-error";
answer.setAttribute("data-ranges-refusal", said.code ?? "refused");
answer.textContent = `${what}: отказ — ${said.reason}${said.code ? ` (${said.code})` : ""}`;
return;
}
answer.removeAttribute("data-ranges-refusal");
answer.className = "text-xs opacity-70";
answer.setAttribute("data-ranges-status", said.status);
answer.textContent =
`${what} — сделано ${EXECUTOR}; ответ «${said.status}», диапазонов ${said.entries.length}, ` +
`открыто на сеанс ${said.unlockedCount}, номер состояния ${said.revision}.`;
current = said.entries.at(-1) ?? null;
list.replaceChildren(
...said.entries.map((entry) => {
const row = document.createElement("div");
row.setAttribute("data-ranges-item", entry.name);
row.className = "rounded border border-base-300 px-2 py-1";
row.textContent =
`${entry.name} · ${entry.sqref} · ${ACCESS[entry.accessKind] ?? entry.accessKind}` +
`${entry.unlocked ? " · замок снят на сеанс" : " · заперт"}`;
return row;
}),
);
/* ПУСТОЙ СПИСОК — УТВЕРЖДЕНИЕ ЯДРА, а не молчание страницы. */
if (!said.entries.length) {
const empty = document.createElement("div");
empty.setAttribute("data-ranges-empty", "");
empty.className = "opacity-60";
empty.textContent = "На этом листе нет диапазонов, изменение которых разрешено паролем.";
list.replaceChildren(empty);
}
}
document.querySelector("[data-ranges-create]")!.addEventListener("click", () => {
void workbook
.createProtectedRangeAsync(sheetKey, {
name: name.value,
/* Ссылка уходит КАК ЕЁ НАПИСАЛ человек: разбирает её ядро, не мы. */
sqref: sqref.value,
...(password.value ? { password: password.value } : {}),
})
.then((said) => show(said, "Создать"));
});
document.querySelector("[data-ranges-unlock]")!.addEventListener("click", () => {
if (!current) return;
void workbook
.unlockProtectedRangeAsync(sheetKey, current.id, password.value)
.then((said) => show(said, "Снять замок"));
});
document.querySelector("[data-ranges-lock]")!.addEventListener("click", () => {
if (!current) return;
void workbook.lockProtectedRangeAsync(sheetKey, current.id).then((said) => show(said, "Вернуть замок"));
});
document.querySelector("[data-ranges-delete]")!.addEventListener("click", () => {
if (!current) return;
void workbook.deleteProtectedRangeAsync(sheetKey, current.id).then((said) => show(said, "Удалить"));
});
/* ПЕРВЫЙ ВОПРОС — ИМЕННО ВОПРОС: `list` книгу не меняет и не запирается даже
в режиме чтения (иначе окно, объясняющее отказ, само отказывало бы). */
void core.f1
.loadAsync()
.then(() => workbook.getProtectedRangesAsync(sheetKey).then((said) => show(said, "Список диапазонов")));
import.meta.hot?.dispose(() => core.destroy());

Книга примера: данные, с которыми лист открывается.

src/book.ts
import { createTransport, transportExecutorName, transportSettingFromSearch } from "@f1/contract";
/**
* КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА.
*
* В книге есть колонка, которую и закрывают в жизни: зарплаты. Права на
* диапазон при этом принадлежат ЛИСТУ, то есть ядру, — в оболочке второго
* списка нет и быть не может.
*
* ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ: умолчание — заглушка,
* `?core=http://127.0.0.1:5507` уводит на живое ядро. Код примера не меняется.
*/
const TABLE: readonly (readonly string[])[] = [
["Сотрудник", "Отдел", "Оклад"],
["Иванова", "Продажи", "180 000"],
["Петров", "Логистика", "145 000"],
["Сидорова", "Финансы", "210 000"],
];
const setting = transportSettingFromSearch(globalThis.location?.search ?? "", {
sheets: ["Лист1"],
usedRange: { rowEnd: 3, colEnd: 2 },
cellAt: (row, col) => TABLE[row]?.[col],
});
export const transport = createTransport(setting);
/**
* КТО РЕШАЕТ, ЧТО ЗАКРЫТО, — и почему это написано рядом с транспортом. Пароль
* остаётся у ядра: обратно приходит только СПОСОБ защиты. Подпись под ответом
* обязана называть исполнителя правдиво и на заглушке, и на живом ядре.
*/
export const EXECUTOR = transportExecutorName(setting);

Сторона хозяина страницы: что приложение отвечает листу и что от него получает.

src/host.ts
import { applyLocaleAttributes, resolveLocale, setLocale, type Locale } from "@f1/i18n";
/**
* ЧТО ХОЗЯИН СТРАНИЦЫ ДАЁТ ЛИСТУ — тема и язык.
*
* У встраивающего это уже есть своё: тему он знает из настроек, язык — из
* профиля пользователя. Здесь и то и другое читается из адреса
* (`?theme=dark&locale=ar`) ровно затем, чтобы на любое состояние можно было
* дать ссылку и чтобы его проверял обход: настройка, доступная только щелчком,
* недоступна ни ссылке, ни автоматической проверке.
*
* ПРИЗНАКИ СТАВЯТСЯ НА КОРЕНЬ ДОКУМЕНТА — потому что это делает ХОЗЯИН, а не
* лист. Лист читает тему оттуда, где стоит (`:host-context`), а язык получает
* СВОЙСТВОМ сборки: два листа на одной странице вправе говорить на разном.
*
* НАПРАВЛЕНИЕ ПИСЬМА — ИНТЕРФЕЙСА, А НЕ ДОКУМЕНТА. `dir="rtl"` зеркалит хром
* страницы; «Лист справа налево» из Excel — свойство КНИГИ, и сетка от
* арабского языка интерфейса не переворачивается. Это не пропуск, а решение
* (`locale.ts`).
*/
export function hostAsks(): { readonly theme: "light" | "dark"; readonly locale: Locale } {
const params = new URLSearchParams(location.search);
const theme = params.get("theme") === "dark" ? "dark" : "light";
const locale = resolveLocale(params.get("locale") ?? undefined, "ru");
document.documentElement.setAttribute("data-theme", theme);
/* Язык — один на процесс: словарь модульный, и ставит его хозяин страницы. */
setLocale(locale);
applyLocaleAttributes(document.documentElement, locale);
return { theme, locale };
}

Оформление страницы вокруг листа.

src/styles.css
/* ЛИСТ СТИЛЕЙ НАБОРА КОМПОНЕНТОВ — ЕДИНСТВЕННАЯ СТРОКА, КОТОРУЮ НЕЛЬЗЯ ЗАБЫТЬ.
Собственное оформление лист приносит в теневые корни сам, но утилиты
раскладки, которыми он держит свои органы, компилируются ЭТИМ листом. Без
него страница не просто выглядит иначе — вкладка ЗАВИСАЕТ (бесконечный цикл
замера у сетки без размеров). У потребителя вне репозитория это
`@fastoffices/formula-kit` из его node_modules. */
@import "@fastoffices/formula-kit"; /* см. примечание выше */
html,
body {
height: 100%;
margin: 0;
}

Из каких пакетов собран пример.

package.json
{
"name": "f1-example-protected-ranges",
"private": true,
"type": "module",
"description": "Разрешить изменение диапазонов: шесть намерений одной операции, а пароль остаётся у ядра.",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"@f1/preset-sheets-core": "*",
"@f1/facade": "*",
"@f1/contract": "*",
"@f1/i18n": "*"
},
"devDependencies": {
"vite": "^7.0.0"
}
}

Ассистент документации

Ответ собран из документации и может быть неточным — сверяйтесь с источниками.