diff --git a/hub/procedures/channel/channel_export_count.sql b/hub/procedures/channel/channel_export_count.sql index 9da85791..5e0250af 100644 --- a/hub/procedures/channel/channel_export_count.sql +++ b/hub/procedures/channel/channel_export_count.sql @@ -2,27 +2,49 @@ DELIMITER $ -- ========================================================= -- channel_export_count --- Fast COUNT of hub team-chat messages (file_thread_id IS NULL) --- for the given uid and optional date range. Used by the --- 10k guard in channel.export before gathering begins. +-- Fast COUNT of team-chat messages (file_thread_id IS NULL) +-- for the given uid, optional date range, and folder subtree. +-- Used by the 10k guard in channel.export and the message +-- count shown on the export modal's folder card. -- Date bounds arrive as VARCHAR: the mariadb layer converts a -- JS null param to '' (empty string), which an INT(11) IN-param -- rejects under strict SQL mode. Accept text, normalise '' / NULL -- to a NULL bound (no filter) and CAST numeric strings to epoch. +-- Folder scope mirrors channel_export_messages: only messages +-- whose metadata._scope_nid falls inside the subtree rooted at +-- _root_nid are counted; legacy rows (no _scope_nid) always +-- count. _root_nid NULL/''/'0' = hub root (whole hub). -- READ-ONLY: no UPDATE / INSERT. -- ========================================================= DROP PROCEDURE IF EXISTS `channel_export_count`$ CREATE PROCEDURE `channel_export_count`( IN _uid VARCHAR(16), + IN _root_nid VARCHAR(16), IN _date_start VARCHAR(20), IN _date_end VARCHAR(20) ) BEGIN + DECLARE _root VARCHAR(16) DEFAULT NULL; DECLARE ds BIGINT DEFAULT NULL; DECLARE de BIGINT DEFAULT NULL; IF _date_start IS NOT NULL AND _date_start <> '' THEN SET ds = CAST(_date_start AS UNSIGNED); END IF; IF _date_end IS NOT NULL AND _date_end <> '' THEN SET de = CAST(_date_end AS UNSIGNED); END IF; + IF _root_nid IS NOT NULL AND _root_nid <> '' AND _root_nid <> '0' THEN + SELECT id INTO _root FROM media WHERE id = _root_nid LIMIT 1; + END IF; + IF _root IS NULL THEN + SELECT id INTO _root FROM media WHERE parent_id = '0' LIMIT 1; + END IF; + + WITH RECURSIVE subtree AS ( + SELECT m.id FROM media m WHERE m.id = _root + UNION ALL + SELECT c.id + FROM media c + INNER JOIN subtree s ON c.parent_id = s.id + WHERE c.mimetype = 'folder' AND c.status = 'active' + ) SELECT COUNT(*) AS message_count FROM channel c WHERE @@ -32,7 +54,11 @@ BEGIN WHERE uid = _uid AND ref_sys_id = c.sys_id ) AND (ds IS NULL OR c.ctime >= ds) - AND (de IS NULL OR c.ctime <= de); + AND (de IS NULL OR c.ctime <= de) + AND ( + read_json_object(c.metadata, '_scope_nid') = '' + OR read_json_object(c.metadata, '_scope_nid') IN (SELECT id FROM subtree) + ); END $ DELIMITER ; diff --git a/hub/procedures/channel/channel_export_file_thread_list.sql b/hub/procedures/channel/channel_export_file_thread_list.sql index 1c3987a5..7d5958ac 100644 --- a/hub/procedures/channel/channel_export_file_thread_list.sql +++ b/hub/procedures/channel/channel_export_file_thread_list.sql @@ -2,24 +2,55 @@ DELIMITER $ -- ========================================================= -- channel_export_file_thread_list --- Hub-wide list of active file threads with resolved filename. --- Used by channel.export_scope to enumerate all file threads --- the user may include in an export. Single-hub DB context. +-- List active file threads for chat export, restricted to +-- files living inside the folder subtree rooted at _root_nid +-- (current membership follows media.parent_id, matching +-- channel_file_thread_list_by_folder). _root_nid NULL/''/'0' +-- = hub root (whole hub). +-- Also returns the file's current folder (folder_nid + +-- folder_name) so export sections can label threads with +-- their location. Only active files — trashed files keep +-- their thread row but must not appear in the export scope. +-- Used by channel.export_scope and the export gather. -- No pagination — export scope UI renders the full list once. +-- READ-ONLY. -- ========================================================= DROP PROCEDURE IF EXISTS `channel_export_file_thread_list`$ CREATE PROCEDURE `channel_export_file_thread_list`( - IN _uid VARCHAR(16) + IN _uid VARCHAR(16), + IN _root_nid VARCHAR(16) ) BEGIN + DECLARE _root VARCHAR(16) DEFAULT NULL; + + IF _root_nid IS NOT NULL AND _root_nid <> '' AND _root_nid <> '0' THEN + SELECT id INTO _root FROM media WHERE id = _root_nid LIMIT 1; + END IF; + IF _root IS NULL THEN + SELECT id INTO _root FROM media WHERE parent_id = '0' LIMIT 1; + END IF; + + WITH RECURSIVE subtree AS ( + SELECT m.id FROM media m WHERE m.id = _root + UNION ALL + SELECT c.id + FROM media c + INNER JOIN subtree s ON c.parent_id = s.id + WHERE c.mimetype = 'folder' AND c.status = 'active' + ) SELECT ft.root_message_id AS file_thread_id, ft.file_nid, m.user_filename AS filename, + m.parent_id AS folder_nid, + f.user_filename AS folder_name, ft.reply_count FROM file_thread ft INNER JOIN media m ON m.id = ft.file_nid + LEFT JOIN media f ON f.id = m.parent_id WHERE ft.status = 'active' + AND m.status = 'active' + AND m.parent_id IN (SELECT id FROM subtree) ORDER BY ft.mtime DESC; END $ diff --git a/hub/procedures/channel/channel_export_file_thread_messages.sql b/hub/procedures/channel/channel_export_file_thread_messages.sql index a461b392..ba618463 100644 --- a/hub/procedures/channel/channel_export_file_thread_messages.sql +++ b/hub/procedures/channel/channel_export_file_thread_messages.sql @@ -47,7 +47,7 @@ BEGIN c.metadata, COALESCE(d.firstname, du.name, '') AS firstname, COALESCE(d.lastname, '') AS lastname, - COALESCE(CONCAT(d.firstname, ' ', d.lastname), du.name, '') AS fullname, + TRIM(COALESCE(NULLIF(CONCAT_WS(' ', d.firstname, d.lastname), ''), du.name, '')) AS fullname, IFNULL(read_json_object(c.metadata, 'message_type'), 'chat') AS message_type, read_json_object(c.metadata, 'call_status') AS call_status FROM ( diff --git a/hub/procedures/channel/channel_export_folder_tree.sql b/hub/procedures/channel/channel_export_folder_tree.sql new file mode 100644 index 00000000..2c7fccc4 --- /dev/null +++ b/hub/procedures/channel/channel_export_folder_tree.sql @@ -0,0 +1,63 @@ +DELIMITER $ + +-- ========================================================= +-- channel_export_folder_tree +-- Folder subtree for chat export, rooted at _root_nid. +-- Chat export groups messages into one section per folder +-- (messages carry their folder in metadata._scope_nid), so the +-- export needs the list of folders under the exported folder. +-- +-- _root_nid: the folder the export modal was opened in. NULL / +-- '' / '0' resolves to the hub root node (parent_id='0'). +-- _hub_id: used only to resolve the display name of the hub +-- root, whose media row has an empty user_filename — the +-- name lives in yp.hub (name, falling back to hubname). +-- +-- System folders (__chat__, __trash__, __upload__) never host +-- user chat and are pruned together with their subtrees. +-- READ-ONLY. +-- ========================================================= +DROP PROCEDURE IF EXISTS `channel_export_folder_tree`$ +CREATE PROCEDURE `channel_export_folder_tree`( + IN _hub_id VARCHAR(16), + IN _root_nid VARCHAR(16) +) +BEGIN + DECLARE _root VARCHAR(16) DEFAULT NULL; + + IF _root_nid IS NOT NULL AND _root_nid <> '' AND _root_nid <> '0' THEN + SELECT id INTO _root FROM media WHERE id = _root_nid LIMIT 1; + END IF; + IF _root IS NULL THEN + SELECT id INTO _root FROM media WHERE parent_id = '0' LIMIT 1; + END IF; + + WITH RECURSIVE subtree AS ( + SELECT m.id, m.parent_id, m.user_filename, 0 AS depth + FROM media m + WHERE m.id = _root + UNION ALL + SELECT c.id, c.parent_id, c.user_filename, s.depth + 1 + FROM media c + INNER JOIN subtree s ON c.parent_id = s.id + WHERE c.mimetype = 'folder' + AND c.status = 'active' + AND c.user_filename NOT IN ('__chat__', '__trash__', '__upload__') + ) + SELECT + s.id, + s.parent_id, + s.depth, + CASE + WHEN s.depth = 0 AND (s.user_filename IS NULL OR s.user_filename = '') + THEN ( + SELECT COALESCE(NULLIF(h.name, ''), NULLIF(h.hubname, ''), _hub_id) + FROM yp.hub h WHERE h.id = _hub_id LIMIT 1 + ) + ELSE s.user_filename + END AS name + FROM subtree s + ORDER BY s.depth ASC, s.user_filename ASC; +END $ + +DELIMITER ; diff --git a/hub/procedures/channel/channel_export_messages.sql b/hub/procedures/channel/channel_export_messages.sql index 6ee20681..f1cf7c48 100644 --- a/hub/procedures/channel/channel_export_messages.sql +++ b/hub/procedures/channel/channel_export_messages.sql @@ -11,12 +11,22 @@ DELIMITER $ -- full history when client omits dates). Bounds arrive as -- VARCHAR because the mariadb layer sends '' for a JS null -- param, which an INT(11) IN-param rejects in strict mode. --- 3. ORDER BY ctime ASC (export order, oldest first). --- 4. No _sort_by / _order params — always ascending ctime. +-- 3. ORDER BY sys_id ASC (export order, oldest first). +-- 4. No _sort_by / _order params — always ascending. +-- 5. Folder-subtree scope: general-chat messages carry their +-- folder in metadata._scope_nid; only rows whose folder is +-- inside the subtree rooted at _root_nid are returned. +-- Legacy rows (no _scope_nid — the live UI shows them in +-- every folder context) are always included; the service +-- groups them into the export-root section. +-- _root_nid NULL/''/'0' = hub root (whole hub). +-- 6. scope_nid exposed as a column so the service can group +-- messages into one section per folder. -- ========================================================= DROP PROCEDURE IF EXISTS `channel_export_messages`$ CREATE PROCEDURE `channel_export_messages`( IN _uid VARCHAR(16), + IN _root_nid VARCHAR(16), IN _date_start VARCHAR(20), IN _date_end VARCHAR(20), IN _page TINYINT(4) @@ -24,12 +34,28 @@ CREATE PROCEDURE `channel_export_messages`( BEGIN DECLARE _range BIGINT; DECLARE _offset BIGINT; + DECLARE _root VARCHAR(16) DEFAULT NULL; DECLARE ds BIGINT DEFAULT NULL; DECLARE de BIGINT DEFAULT NULL; IF _date_start IS NOT NULL AND _date_start <> '' THEN SET ds = CAST(_date_start AS UNSIGNED); END IF; IF _date_end IS NOT NULL AND _date_end <> '' THEN SET de = CAST(_date_end AS UNSIGNED); END IF; CALL pageToLimits(_page, _offset, _range); + IF _root_nid IS NOT NULL AND _root_nid <> '' AND _root_nid <> '0' THEN + SELECT id INTO _root FROM media WHERE id = _root_nid LIMIT 1; + END IF; + IF _root IS NULL THEN + SELECT id INTO _root FROM media WHERE parent_id = '0' LIMIT 1; + END IF; + + WITH RECURSIVE subtree AS ( + SELECT m.id FROM media m WHERE m.id = _root + UNION ALL + SELECT c.id + FROM media c + INNER JOIN subtree s ON c.parent_id = s.id + WHERE c.mimetype = 'folder' AND c.status = 'active' + ) SELECT _page AS `page`, c.sys_id, @@ -45,12 +71,13 @@ BEGIN c.status, c.ctime, c.metadata, + read_json_object(c.metadata, '_scope_nid') AS scope_nid, IFNULL(read_json_object(c.metadata, 'message_type'), 'chat') AS message_type, COALESCE(d.firstname, du.name, '') AS firstname, COALESCE(d.lastname, '') AS lastname, - COALESCE(CONCAT(d.firstname, ' ', d.lastname), du.name, '') AS fullname + TRIM(COALESCE(NULLIF(CONCAT_WS(' ', d.firstname, d.lastname), ''), du.name, '')) AS fullname FROM ( - SELECT sys_id FROM channel c + SELECT c.sys_id FROM channel c WHERE NOT EXISTS ( SELECT 1 FROM delete_channel @@ -59,6 +86,10 @@ BEGIN AND c.file_thread_id IS NULL AND (ds IS NULL OR c.ctime >= ds) AND (de IS NULL OR c.ctime <= de) + AND ( + read_json_object(c.metadata, '_scope_nid') = '' + OR read_json_object(c.metadata, '_scope_nid') IN (SELECT id FROM subtree) + ) ORDER BY c.sys_id ASC LIMIT _offset, _range ) s