UI обращений, WebSocket и исправление DnD

- Канбан: шире контейнер, колонки minmax(300px), больше высота списков и колонок, брейкпоинт 1100px.
- WebSocket: настраиваемый префикс support_ws_base (occ) и data-support-ws-base; корректная сборка пути /ws/tickets/…
- Устранён ReferenceError: clientChatDndBound до инициализации при открытии создания обращения.
This commit is contained in:
root
2026-05-14 13:24:02 +03:00
parent afec753d7b
commit c5b300ffa2
3 changed files with 55 additions and 30 deletions
+29 -11
View File
@@ -5,6 +5,7 @@
const username = root.dataset.username; const username = root.dataset.username;
const serverAddress = root.dataset.serverAddress; const serverAddress = root.dataset.serverAddress;
const apiBase = root.dataset.supportApiBase; const apiBase = root.dataset.supportApiBase;
const supportWsBaseOverride = (root.dataset.supportWsBase || "").trim();
const RASTER_IMAGE_EXT = new Set(["jpg", "jpeg", "png", "gif", "webp", "bmp", "tif", "tiff", "heic", "heif"]); const RASTER_IMAGE_EXT = new Set(["jpg", "jpeg", "png", "gif", "webp", "bmp", "tif", "tiff", "heic", "heif"]);
const RASTER_IMAGE_MIME = new Set([ const RASTER_IMAGE_MIME = new Set([
@@ -149,10 +150,25 @@
} }
function wsUrlForTicket(ticketNumber) { function wsUrlForTicket(ticketNumber) {
const tail = `/tickets/${encodeURIComponent(ticketNumber)}`;
if (supportWsBaseOverride) {
try {
const raw = supportWsBaseOverride.replace(/\/$/, "");
const u = new URL(raw.includes("://") ? raw : `wss://${raw}`);
const wsProto = u.protocol === "https:" ? "wss:" : u.protocol === "http:" ? "ws:" : u.protocol;
let path = (u.pathname || "").replace(/\/$/, "");
if (path === "" || path === "/") {
path = "/ws";
}
return `${wsProto}//${u.host}${path}${tail}`;
} catch (_) {
return null;
}
}
try { try {
const u = new URL(apiBase); const u = new URL(apiBase);
const wsProto = u.protocol === "https:" ? "wss:" : "ws:"; const wsProto = u.protocol === "https:" ? "wss:" : "ws:";
return `${wsProto}//${u.host}/ws/tickets/${encodeURIComponent(ticketNumber)}`; return `${wsProto}//${u.host}/ws${tail}`;
} catch (_) { } catch (_) {
return null; return null;
} }
@@ -311,25 +327,26 @@
</div> </div>
</div> </div>
<style> <style>
.f7support-wrap { padding: 16px; font-family: sans-serif; max-width: min(1200px, 100%); box-sizing: border-box; } .f7support-wrap { padding: 16px; font-family: sans-serif; max-width: min(1680px, 100%); width: 100%; box-sizing: border-box; }
.f7support-error { color: #b00020; min-height: 1.25em; margin: 8px 0; } .f7support-error { color: #b00020; min-height: 1.25em; margin: 8px 0; }
.f7support-main { margin-top: 12px; } .f7support-main { margin-top: 12px; width: 100%; }
.f7-board { .f7-board {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(300px, 1fr));
gap: 12px; gap: 16px;
margin-top: 12px; margin-top: 12px;
align-items: stretch; align-items: stretch;
width: 100%;
} }
@media (max-width: 900px) { @media (max-width: 1100px) {
.f7-board { grid-template-columns: 1fr; } .f7-board { grid-template-columns: 1fr; }
} }
.f7-column { .f7-column {
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 8px; border-radius: 8px;
background: #fafafa; background: #fafafa;
padding: 10px; padding: 12px;
min-height: 200px; min-height: min(52vh, 560px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -339,8 +356,8 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
flex: 1; flex: 1;
min-height: 160px; min-height: min(42vh, 420px);
max-height: min(56vh, 520px); max-height: min(72vh, 820px);
overflow-y: auto; overflow-y: auto;
} }
.f7-ticket-card { .f7-ticket-card {
@@ -645,6 +662,8 @@
const createModal = document.getElementById("create-ticket-modal"); const createModal = document.getElementById("create-ticket-modal");
const chatModal = document.getElementById("chat-modal"); const chatModal = document.getElementById("chat-modal");
let clientChatDndBound = false;
bindClientChatDnDOnce(); bindClientChatDnDOnce();
function showError(message) { function showError(message) {
@@ -682,7 +701,6 @@
updatePendingFileUi(); updatePendingFileUi();
} }
let clientChatDndBound = false;
function bindClientChatDnDOnce() { function bindClientChatDnDOnce() {
if (clientChatDndBound) return; if (clientChatDndBound) return;
clientChatDndBound = true; clientChatDndBound = true;
+25 -19
View File
@@ -6,20 +6,22 @@ namespace OCA\F7Support\Controller;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IRequest; use OCP\IRequest;
use OCP\IURLGenerator; use OCP\IURLGenerator;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\Util; use OCP\Util;
class PageController extends Controller { class PageController extends Controller {
public function __construct( public function __construct(
string $AppName, string $AppName,
IRequest $request, IRequest $request,
private IUserSession $userSession, private IUserSession $userSession,
private IURLGenerator $urlGenerator private IURLGenerator $urlGenerator,
) { private IConfig $config,
parent::__construct($AppName, $request); ) {
} parent::__construct($AppName, $request);
}
/** /**
* @NoAdminRequired * @NoAdminRequired
@@ -30,19 +32,23 @@ class PageController extends Controller {
$baseUrl = $this->urlGenerator->getBaseUrl(); $baseUrl = $this->urlGenerator->getBaseUrl();
$serverHost = parse_url($baseUrl, PHP_URL_HOST) ?: 'localhost'; $serverHost = parse_url($baseUrl, PHP_URL_HOST) ?: 'localhost';
$supportApiBase = 'https://support.f7cloud.ru'; $supportApiBase = 'https://support.f7cloud.ru';
$supportParts = parse_url($supportApiBase); $supportParts = parse_url($supportApiBase);
$supportApiOrigin = ($supportParts['scheme'] ?? 'https') . '://' . ($supportParts['host'] ?? ''); $supportApiOrigin = ($supportParts['scheme'] ?? 'https') . '://' . ($supportParts['host'] ?? '');
Util::addStyle('f7support', 'f7support'); // Optional full WebSocket URL prefix, e.g. wss://support.f7cloud.ru/api/ws (path before /tickets/{id})
Util::addScript('f7support', 'main'); $supportWsBase = $this->config->getAppValue('f7support', 'support_ws_base', '');
return new TemplateResponse('f7support', 'main', [ Util::addStyle('f7support', 'f7support');
'username' => $user ? $user->getUID() : '', Util::addScript('f7support', 'main');
'serverAddress' => $serverHost,
'supportApiBase' => $supportApiBase, return new TemplateResponse('f7support', 'main', [
'supportApiOrigin' => $supportApiOrigin, 'username' => $user ? $user->getUID() : '',
]); 'serverAddress' => $serverHost,
'supportApiBase' => $supportApiBase,
'supportApiOrigin' => $supportApiOrigin,
'supportWsBase' => $supportWsBase,
]);
} }
} }
+1
View File
@@ -4,5 +4,6 @@
data-username="<?php p($_['username']); ?>" data-username="<?php p($_['username']); ?>"
data-server-address="<?php p($_['serverAddress']); ?>" data-server-address="<?php p($_['serverAddress']); ?>"
data-support-api-base="<?php p($_['supportApiBase']); ?>" data-support-api-base="<?php p($_['supportApiBase']); ?>"
data-support-ws-base="<?php p($_['supportWsBase'] ?? ''); ?>"
data-messages-poll-ms="5000"> data-messages-poll-ms="5000">
</div> </div>