From 65d2b6916fc1e92c278a782640fff8b408e5c37b Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 30 Jan 2025 11:10:44 +1100 Subject: [PATCH 01/15] Add sql migrations for the initial setup of context widgets, and for the syncing fo chats and custom views to the context menu. Still requires a step to reorder widgets based on a groups contents --- apps/backend/api/models/Group.js | 107 +++++++-------- ...130104013_setup-initial-context-widgets.js | 81 +++++++++++ ...s-and-custom-views-have-context-widgets.js | 129 ++++++++++++++++++ 3 files changed, 263 insertions(+), 54 deletions(-) create mode 100644 apps/backend/migrations/20250130104013_setup-initial-context-widgets.js create mode 100644 apps/backend/migrations/20250130104100_ensure-chats-and-custom-views-have-context-widgets.js diff --git a/apps/backend/api/models/Group.js b/apps/backend/api/models/Group.js index dd779127a..beedb80c4 100644 --- a/apps/backend/api/models/Group.js +++ b/apps/backend/api/models/Group.js @@ -511,7 +511,7 @@ module.exports = bookshelf.Model.extend(merge({ // Get home tag id for the home chat const homeTag = await Tag.where({ name: 'home' }).fetch({ transacting: trx }) - // Create hearth widget as child of home + // Create home chat widget as child of home await ContextWidget.forge({ group_id: this.id, type: 'chat', @@ -557,43 +557,42 @@ module.exports = bookshelf.Model.extend(merge({ )) }, - // This is idempotent async transitionToNewMenu (existingTrx) { const doWork = async (trx) => { // Get all widgets for this group const widgets = await ContextWidget.where({ group_id: this.id }).fetchAll({ transacting: trx }) - const chatsWidget = widgets.find(w => w.get('view') === 'chats') + // const chatsWidget = widgets.find(w => w.get('view') === 'chats') const autoAddWidget = widgets.find(w => w.get('type') === 'auto-view') - const chatsWidgetId = chatsWidget?.get('id') + // const chatsWidgetId = chatsWidget?.get('id') const autoAddWidgetId = autoAddWidget?.get('id') - if (!chatsWidget?.get('auto_added')) { - // TODO CONTEXT: port this section to the new chat model - const chatPostResults = await bookshelf.knex.raw(` - SELECT DISTINCT t.id as tag_id, t.name, gt.visibility - FROM posts p - JOIN groups_posts gp ON gp.post_id = p.id - JOIN posts_tags pt ON pt.post_id = p.id - JOIN tags t ON t.id = pt.tag_id - JOIN groups_tags gt ON gt.tag_id = t.id AND gt.group_id = gp.group_id - WHERE gp.group_id = ? AND p.type = 'chat' - `, [this.id], { transacting: trx }) - - const groupChats = chatPostResults.rows.filter(tag => tag.name !== 'general') - - if (groupChats.length > 0) { - await Promise.all(groupChats.map(chat => - ContextWidget.create({ - group_id: this.id, - title: chat.name, - type: 'chat', - parent_id: chat.visibility === 2 ? chatsWidgetId : null, - view_chat_id: chat.tag_id, - addToEnd: (chat.visibility === 2) - }, { transacting: trx }) - )) - } - } + // if (!chatsWidget?.get('auto_added')) { + // // TODO CONTEXT: port this section to the new chat model + // const chatPostResults = await bookshelf.knex.raw(` + // SELECT DISTINCT t.id as tag_id, t.name, gt.visibility + // FROM posts p + // JOIN groups_posts gp ON gp.post_id = p.id + // JOIN posts_tags pt ON pt.post_id = p.id + // JOIN tags t ON t.id = pt.tag_id + // JOIN groups_tags gt ON gt.tag_id = t.id AND gt.group_id = gp.group_id + // WHERE gp.group_id = ? AND p.type = 'chat' + // `, [this.id], { transacting: trx }) + + // const groupChats = chatPostResults.rows.filter(tag => tag.name !== 'general' || tag.name !== 'home') + + // if (groupChats.length > 0) { + // await Promise.all(groupChats.map(chat => + // ContextWidget.create({ + // group_id: this.id, + // title: chat.name, + // type: 'chat', + // parent_id: chat.visibility === 2 ? chatsWidgetId : null, + // view_chat_id: chat.tag_id, + // addToEnd: (chat.visibility === 2) + // }, { transacting: trx }) + // )) + // } + // } const askOfferWidget = widgets.find(w => w.get('view') === 'ask-and-offer') if (askOfferWidget && !askOfferWidget.get('auto_added')) { @@ -707,29 +706,29 @@ module.exports = bookshelf.Model.extend(merge({ } } - const customViews = await bookshelf.knex('custom_views') - .where({ group_id: this.id }) - .whereNotExists(function() { - this.select('*') - .from('context_widgets') - .whereRaw('context_widgets.custom_view_id = custom_views.id') - .andWhere('auto_added', true) - }) - if (customViews.length > 0) { - const customViewsWidget = widgets.find(w => w.get('type') === 'custom-views') - if (customViewsWidget) { - await Promise.all(customViews.map(view => - ContextWidget.create({ - group_id: this.id, - custom_view_id: view.id, - parent_id: customViewsWidget.get('id'), - auto_added: true, - addToEnd: true - }, { transacting: trx }) - )) - } - } - } + // const customViews = await bookshelf.knex('custom_views') + // .where({ group_id: this.id }) + // .whereNotExists(function() { + // this.select('*') + // .from('context_widgets') + // .whereRaw('context_widgets.custom_view_id = custom_views.id') + // .andWhere('auto_added', true) + // }) + // if (customViews.length > 0) { + // const customViewsWidget = widgets.find(w => w.get('type') === 'custom-views') + // if (customViewsWidget) { + // await Promise.all(customViews.map(view => + // ContextWidget.create({ + // group_id: this.id, + // custom_view_id: view.id, + // parent_id: customViewsWidget.get('id'), + // auto_added: true, + // addToEnd: true + // }, { transacting: trx }) + // )) + // } + // } + // } if (existingTrx) { return doWork(existingTrx) diff --git a/apps/backend/migrations/20250130104013_setup-initial-context-widgets.js b/apps/backend/migrations/20250130104013_setup-initial-context-widgets.js new file mode 100644 index 000000000..88399465d --- /dev/null +++ b/apps/backend/migrations/20250130104013_setup-initial-context-widgets.js @@ -0,0 +1,81 @@ +exports.up = function(knex) { + return knex.raw(` + DO $$ + DECLARE + group_record RECORD; + BEGIN + FOR group_record IN SELECT id FROM groups WHERE active = true + LOOP + WITH home_widget AS ( + INSERT INTO context_widgets ( + group_id, type, title, "order", created_at, updated_at + ) + SELECT + group_record.id, 'home', 'widget-home', 1, NOW(), NOW() + WHERE NOT EXISTS ( + SELECT 1 FROM context_widgets + WHERE group_id = group_record.id AND type = 'home' + ) + RETURNING id + ), + home_chat_widget AS ( + INSERT INTO context_widgets ( + group_id, type, view_chat_id, parent_id, "order", created_at, updated_at + ) + SELECT + group_record.id, + 'chat', + (SELECT id FROM tags WHERE name = 'home'), + (SELECT id FROM context_widgets WHERE group_id = group_record.id AND type = 'home'), + 1, + NOW(), + NOW() + WHERE NOT EXISTS ( + SELECT 1 FROM context_widgets + WHERE group_id = group_record.id + AND type = 'chat' + AND parent_id = (SELECT id FROM context_widgets WHERE group_id = group_record.id AND type = 'home') + ) + ), + ordered_widgets AS ( + INSERT INTO context_widgets ( + group_id, title, type, view, "order", visibility, created_at, updated_at + ) + SELECT v.* FROM (VALUES + (group_record.id, 'widget-chats', 'chats', NULL, 2, NULL, NOW(), NOW()), + (group_record.id, 'widget-auto-view', 'auto-view', NULL, 3, NULL, NOW(), NOW()), + (group_record.id, 'widget-members', 'members', 'members', 4, NULL, NOW(), NOW()), + (group_record.id, 'widget-setup', 'setup', NULL, 5, 'admin', NOW(), NOW()), + (group_record.id, 'widget-custom-views', 'custom-views', NULL, 6, NULL, NOW(), NOW()) + ) AS v(group_id, title, type, view, "order", visibility, created_at, updated_at) + WHERE NOT EXISTS ( + SELECT 1 FROM context_widgets w + WHERE w.group_id = v.group_id AND w.title = v.title + ) + ) + INSERT INTO context_widgets ( + group_id, title, type, view, created_at, updated_at + ) + SELECT v.* FROM (VALUES + (group_record.id, 'widget-discussions', NULL, 'discussions', NOW(), NOW()), + (group_record.id, 'widget-ask-and-offer', NULL, 'ask-and-offer', NOW(), NOW()), + (group_record.id, 'widget-stream', NULL, 'stream', NOW(), NOW()), + (group_record.id, 'widget-events', 'events', 'events', NOW(), NOW()), + (group_record.id, 'widget-projects', 'projects', 'projects', NOW(), NOW()), + (group_record.id, 'widget-groups', 'groups', 'groups', NOW(), NOW()), + (group_record.id, 'widget-decisions', 'decisions', 'decisions', NOW(), NOW()), + (group_record.id, 'widget-about', 'about', 'about', NOW(), NOW()), + (group_record.id, 'widget-map', 'map', 'map', NOW(), NOW()) + ) AS v(group_id, title, type, view, created_at, updated_at) + WHERE NOT EXISTS ( + SELECT 1 FROM context_widgets w + WHERE w.group_id = v.group_id AND w.title = v.title + ); + END LOOP; + END $$; + `); +}; + +exports.down = function(knex) { + return Promise.resolve(); +}; diff --git a/apps/backend/migrations/20250130104100_ensure-chats-and-custom-views-have-context-widgets.js b/apps/backend/migrations/20250130104100_ensure-chats-and-custom-views-have-context-widgets.js new file mode 100644 index 000000000..25c5a2574 --- /dev/null +++ b/apps/backend/migrations/20250130104100_ensure-chats-and-custom-views-have-context-widgets.js @@ -0,0 +1,129 @@ +exports.up = function(knex) { + + // This call is idempotent; you can run it many times and it will just sync chats/custcomes up to their needed widgets. + return knex.raw(` + WITH RECURSIVE all_groups AS ( + SELECT id FROM groups WHERE active = true + ), + all_groups_with_widgets AS ( + SELECT + g.id as group_id, + EXISTS ( + SELECT 1 + FROM context_widgets cw + WHERE cw.group_id = g.id + ) as has_widgets + FROM all_groups g + ), + group_existing_widgets AS ( + SELECT + g.id as group_id, + cw.id as widget_id, + cw.type, + CASE + WHEN cw.type = 'chats' THEN 'chats' + WHEN cw.type = 'custom-views' THEN 'custom_views' + END as parent_type + FROM all_groups g + LEFT JOIN context_widgets cw ON cw.group_id = g.id + WHERE cw.type IN ('chats', 'custom-views') + ), + new_chat_widgets AS ( + SELECT DISTINCT + gp.group_id, + t.id as tag_id, + t.name, + gt.visibility, + EXISTS ( + SELECT 1 + FROM context_widgets + WHERE group_id = gp.group_id + AND type = 'chat' + AND view_chat_id = t.id + ) as has_widget + FROM all_groups g + JOIN groups_posts gp ON gp.group_id = g.id + JOIN posts p ON p.id = gp.post_id + JOIN posts_tags pt ON pt.post_id = p.id + JOIN tags t ON t.id = pt.tag_id + JOIN groups_tags gt ON gt.tag_id = t.id AND gt.group_id = gp.group_id + WHERE p.type = 'chat' + AND t.name != 'general' + ), + new_custom_view_widgets AS ( + SELECT + cv.group_id, + cv.id as custom_view_id, + EXISTS ( + SELECT 1 + FROM context_widgets + WHERE group_id = cv.group_id + AND custom_view_id = cv.id + ) as has_widget + FROM custom_views cv + JOIN all_groups g ON g.id = cv.group_id + ) + + INSERT INTO context_widgets ( + group_id, + title, + type, + parent_id, + view_chat_id, + custom_view_id, + auto_added, + created_at, + updated_at + ) + -- Insert chat widgets + SELECT + ncw.group_id, + ncw.name, + 'chat'::text, + CASE + WHEN ncw.visibility = 2 THEN ( + SELECT widget_id + FROM group_existing_widgets + WHERE group_id = ncw.group_id + AND parent_type = 'chats' + LIMIT 1 + ) + ELSE NULL + END, + ncw.tag_id, + NULL, + true, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + FROM new_chat_widgets ncw + WHERE NOT ncw.has_widget + + UNION ALL + + -- Insert custom view widgets + SELECT + ncvw.group_id, + NULL, + NULL, + ( + SELECT widget_id + FROM group_existing_widgets + WHERE group_id = ncvw.group_id + AND parent_type = 'custom_views' + LIMIT 1 + ), + NULL, + ncvw.custom_view_id, + true, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + FROM new_custom_view_widgets ncvw + WHERE NOT ncvw.has_widget + `); +}; + +exports.down = function(knex) { + // Since this migration only adds widgets where they're missing, + // and doesn't modify existing ones, we don't need a down migration + return Promise.resolve(); +}; From d32bdfa1ce3a713b3bc764add60ea3b261eebed4 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 30 Jan 2025 12:26:12 +1100 Subject: [PATCH 02/15] Missing bracket --- apps/backend/api/models/Group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/api/models/Group.js b/apps/backend/api/models/Group.js index beedb80c4..f7162946c 100644 --- a/apps/backend/api/models/Group.js +++ b/apps/backend/api/models/Group.js @@ -728,7 +728,7 @@ module.exports = bookshelf.Model.extend(merge({ // )) // } // } - // } + } if (existingTrx) { return doWork(existingTrx) From 7657bf0a3dbbb6c2d739d32ffdc892ce4d40e17c Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 30 Jan 2025 12:26:50 +1100 Subject: [PATCH 03/15] update-widgets-based-on-group-content --- ...0_update-widgets-based-on-group-content.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js diff --git a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js new file mode 100644 index 000000000..ae50f93e3 --- /dev/null +++ b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js @@ -0,0 +1,24 @@ + +exports.up = async function(knex) { + + // Find all groups that don't have context widgets + const groups = await knex.raw(` + SELECT g.id + FROM groups g + `) + const groupIds = groups.rows.map(row => row.id) + console.log('Number of updated groups', groupIds.length) + // Process each group in its own transaction + return Promise.all(groupIds.map(async groupId => { + return bookshelf.transaction(async trx => { + const group = await Group.find(groupId) + if (group) { + await group.transitionToNewMenu(trx) + } + }) + })) +}; + +exports.down = function(knex) { + +}; From d92645825a77fb2ee532b7b28d0c5516e4321bbf Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 30 Jan 2025 13:16:48 +1100 Subject: [PATCH 04/15] Init models so Group methods actually work --- .../20250130121530_update-widgets-based-on-group-content.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js index ae50f93e3..6eb4891be 100644 --- a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js +++ b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js @@ -1,5 +1,9 @@ +/* globals Group */ +require("@babel/register") +const models = require('../api/models') exports.up = async function(knex) { + models.init() // Find all groups that don't have context widgets const groups = await knex.raw(` From aec22ca2e50c1d974f4947841bf604cab5fc28a0 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 30 Jan 2025 11:10:44 +1100 Subject: [PATCH 05/15] Add sql migrations for the initial setup of context widgets, and for the syncing fo chats and custom views to the context menu. Still requires a step to reorder widgets based on a groups contents --- apps/backend/api/models/Group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/api/models/Group.js b/apps/backend/api/models/Group.js index bcdeba7a8..325b6aa19 100644 --- a/apps/backend/api/models/Group.js +++ b/apps/backend/api/models/Group.js @@ -735,7 +735,7 @@ module.exports = bookshelf.Model.extend(merge({ // )) // } // } - } + // } if (existingTrx) { return doWork(existingTrx) From b9db274a78e915b46c10e01517c3cbdf98e8103a Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 30 Jan 2025 12:26:50 +1100 Subject: [PATCH 06/15] update-widgets-based-on-group-content --- ...0_update-widgets-based-on-group-content.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js diff --git a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js new file mode 100644 index 000000000..ae50f93e3 --- /dev/null +++ b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js @@ -0,0 +1,24 @@ + +exports.up = async function(knex) { + + // Find all groups that don't have context widgets + const groups = await knex.raw(` + SELECT g.id + FROM groups g + `) + const groupIds = groups.rows.map(row => row.id) + console.log('Number of updated groups', groupIds.length) + // Process each group in its own transaction + return Promise.all(groupIds.map(async groupId => { + return bookshelf.transaction(async trx => { + const group = await Group.find(groupId) + if (group) { + await group.transitionToNewMenu(trx) + } + }) + })) +}; + +exports.down = function(knex) { + +}; From c84aab81266ea801c25b458603fffda5ba5e165c Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 30 Jan 2025 13:16:48 +1100 Subject: [PATCH 07/15] Init models so Group methods actually work --- .../20250130121530_update-widgets-based-on-group-content.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js index ae50f93e3..6eb4891be 100644 --- a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js +++ b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js @@ -1,5 +1,9 @@ +/* globals Group */ +require("@babel/register") +const models = require('../api/models') exports.up = async function(knex) { + models.init() // Find all groups that don't have context widgets const groups = await knex.raw(` From 1a9187b012375ba25dcd36bd04175d81d11fb7f6 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Wed, 29 Jan 2025 22:23:27 -0800 Subject: [PATCH 08/15] Make sure groups have a home tag when transitioning menu --- apps/backend/api/models/Group.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/backend/api/models/Group.js b/apps/backend/api/models/Group.js index 325b6aa19..e92cdc4dd 100644 --- a/apps/backend/api/models/Group.js +++ b/apps/backend/api/models/Group.js @@ -511,6 +511,11 @@ module.exports = bookshelf.Model.extend(merge({ // Get home tag id for the home chat const homeTag = await Tag.where({ name: 'home' }).fetch({ transacting: trx }) + // XXX: make sure there is a home tag for every group + const homeGroupTag = await GroupTag.where({ group_id: this.id, tag_id: homeTag.id }).fetch({ transacting: trx }) + if (!homeGroupTag) { + await GroupTag.create({ group_id: this.id, tag_id: homeTag.id, user_id: this.get('created_by_id'), is_default: true }, { transacting: trx }) + } // XXX: make sure there is a home tag for every group const homeGroupTag = await GroupTag.where({ group_id: this.id, tag_id: homeTag.id }).fetch({ transacting: trx }) From 4c833e0bd5bd35412f6b31238d6e2bf1673ca77b Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Fri, 31 Jan 2025 15:06:37 -0800 Subject: [PATCH 09/15] Fix duplicate code --- apps/backend/api/models/Group.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/backend/api/models/Group.js b/apps/backend/api/models/Group.js index e92cdc4dd..325b6aa19 100644 --- a/apps/backend/api/models/Group.js +++ b/apps/backend/api/models/Group.js @@ -511,11 +511,6 @@ module.exports = bookshelf.Model.extend(merge({ // Get home tag id for the home chat const homeTag = await Tag.where({ name: 'home' }).fetch({ transacting: trx }) - // XXX: make sure there is a home tag for every group - const homeGroupTag = await GroupTag.where({ group_id: this.id, tag_id: homeTag.id }).fetch({ transacting: trx }) - if (!homeGroupTag) { - await GroupTag.create({ group_id: this.id, tag_id: homeTag.id, user_id: this.get('created_by_id'), is_default: true }, { transacting: trx }) - } // XXX: make sure there is a home tag for every group const homeGroupTag = await GroupTag.where({ group_id: this.id, tag_id: homeTag.id }).fetch({ transacting: trx }) From 1ae890d2a95d25369af6ea461126591ef0f61b87 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Fri, 31 Jan 2025 15:07:22 -0800 Subject: [PATCH 10/15] Fix bug --- apps/backend/api/models/Group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/api/models/Group.js b/apps/backend/api/models/Group.js index 325b6aa19..bcdeba7a8 100644 --- a/apps/backend/api/models/Group.js +++ b/apps/backend/api/models/Group.js @@ -735,7 +735,7 @@ module.exports = bookshelf.Model.extend(merge({ // )) // } // } - // } + } if (existingTrx) { return doWork(existingTrx) From cbca1d7021cd64ad8b886f20dc8c24b67ab64cbe Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Fri, 31 Jan 2025 15:24:45 -0800 Subject: [PATCH 11/15] Comment --- .../20250130121530_update-widgets-based-on-group-content.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js index 6eb4891be..658db0a24 100644 --- a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js +++ b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js @@ -5,7 +5,7 @@ const models = require('../api/models') exports.up = async function(knex) { models.init() - // Find all groups that don't have context widgets + // Update all groups to have the correct widgets based on their content const groups = await knex.raw(` SELECT g.id FROM groups g @@ -24,5 +24,5 @@ exports.up = async function(knex) { }; exports.down = function(knex) { - + }; From 2ec28f9f563fc4603b35680085e5b207d4432210 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Sun, 2 Feb 2025 14:54:49 -0800 Subject: [PATCH 12/15] Improve migration to setup common views widget for all groups Add Stream and Resources and Discussion widgets too --- apps/backend/api/models/Group.js | 1 + ...0_update-widgets-based-on-group-content.js | 145 ++++++++++++++++-- apps/mobile/locales/en.json | 3 +- apps/mobile/locales/es.json | 3 +- apps/web/public/locales/en.json | 1 + apps/web/public/locales/es.json | 1 + .../StreamViewControls/StreamViewControls.js | 2 +- .../AuthLayoutRouter/AuthLayoutRouter.js | 4 +- .../src/store/reducers/ormReducer/index.js | 4 + packages/shared/src/ViewHelpers.js | 11 +- 10 files changed, 151 insertions(+), 24 deletions(-) diff --git a/apps/backend/api/models/Group.js b/apps/backend/api/models/Group.js index bcdeba7a8..7f8b40d62 100644 --- a/apps/backend/api/models/Group.js +++ b/apps/backend/api/models/Group.js @@ -544,6 +544,7 @@ module.exports = bookshelf.Model.extend(merge({ { title: 'widget-ask-and-offer', view: 'ask-and-offer' }, { title: 'widget-stream', view: 'stream' }, { title: 'widget-events', type: 'events', view: 'events' }, + { title: 'widget-resources', type: 'resources', view: 'resources' }, { title: 'widget-projects', type: 'projects', view: 'projects' }, { title: 'widget-groups', type: 'groups', view: 'groups' }, { title: 'widget-decisions', type: 'decisions', view: 'decisions' }, diff --git a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js index 658db0a24..d7f506137 100644 --- a/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js +++ b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js @@ -3,26 +3,137 @@ require("@babel/register") const models = require('../api/models') exports.up = async function(knex) { - models.init() + // Fetch all group IDs + const groups = await knex('groups').select('id'); + const groupIds = groups.map(group => parseInt(group.id)); - // Update all groups to have the correct widgets based on their content - const groups = await knex.raw(` - SELECT g.id - FROM groups g - `) - const groupIds = groups.rows.map(row => row.id) - console.log('Number of updated groups', groupIds.length) // Process each group in its own transaction - return Promise.all(groupIds.map(async groupId => { - return bookshelf.transaction(async trx => { - const group = await Group.find(groupId) - if (group) { - await group.transitionToNewMenu(trx) - } - }) - })) + for (const groupId of groupIds) { + console.log('Processing group', groupId,) + const trx = await knex.transaction(); + + try { + await trx.raw(` + INSERT INTO context_widgets ( + group_id, title, type, view, created_at, updated_at + ) + SELECT v.* FROM (VALUES + (CAST(? AS bigint), 'widget-resources', 'resources', 'resources', NOW(), NOW()) + ) AS v(group_id, title, type, view, created_at, updated_at) + WHERE NOT EXISTS ( + SELECT 1 FROM context_widgets w + WHERE w.group_id = ? AND w.title = 'widget-resources' + ) + RETURNING id + `, [groupId, groupId]) + + await trx.raw(` + WITH widgets AS ( + SELECT * FROM context_widgets WHERE group_id = ? + ), + auto_add_widget AS ( + SELECT id FROM context_widgets WHERE type = 'auto-view' and group_id = ? LIMIT 1 + ), + update_stream AS ( + UPDATE context_widgets + SET parent_id = (SELECT id FROM auto_add_widget), "order" = 1 + WHERE (view = 'stream' AND group_id = ?) + ), + has_discussions AS ( + SELECT 1 FROM posts + JOIN groups_posts ON groups_posts.post_id = posts.id + WHERE groups_posts.group_id = ? AND posts.type IN ('discussion') LIMIT 1 + ), + update_discussions AS ( + UPDATE context_widgets + SET parent_id = (SELECT id FROM auto_add_widget), "order" = 2 + WHERE (view = 'discussions' AND group_id = ? AND auto_added = FALSE) AND EXISTS (SELECT 1 FROM has_discussions) + ), + has_events AS ( + SELECT 1 FROM posts + JOIN groups_posts ON groups_posts.post_id = posts.id + WHERE groups_posts.group_id = ? AND posts.type = 'event' LIMIT 1 + ), + update_events AS ( + UPDATE context_widgets + SET parent_id = (SELECT id FROM auto_add_widget), "order" = 3 + WHERE (type = 'events' AND group_id = ? AND auto_added = FALSE) AND EXISTS (SELECT 1 FROM has_events) + ), + has_projects AS ( + SELECT 1 FROM posts + JOIN groups_posts ON groups_posts.post_id = posts.id + WHERE groups_posts.group_id = ? AND posts.type = 'project' LIMIT 1 + ), + update_projects AS ( + UPDATE context_widgets + SET parent_id = (SELECT id FROM auto_add_widget), "order" = 4 + WHERE (type = 'projects' and group_id = ? AND auto_added = FALSE) AND EXISTS (SELECT 1 FROM has_projects) + ), + has_asks_offers AS ( + SELECT 1 FROM posts + JOIN groups_posts ON groups_posts.post_id = posts.id + WHERE groups_posts.group_id = ? AND posts.type IN ('request', 'offer') LIMIT 1 + ), + update_ask_offer AS ( + UPDATE context_widgets + SET parent_id = (SELECT id FROM auto_add_widget), "order" = 5 + WHERE (view = 'ask-and-offer' AND group_id = ? AND auto_added = FALSE) AND EXISTS (SELECT 1 FROM has_asks_offers) + ), + has_resources AS ( + SELECT 1 FROM posts + JOIN groups_posts ON groups_posts.post_id = posts.id + WHERE groups_posts.group_id = ? AND posts.type = 'resource' LIMIT 1 + ), + update_resources AS ( + UPDATE context_widgets + SET parent_id = (SELECT id FROM auto_add_widget), "order" = 6 + WHERE (view = 'resources' AND group_id = ? AND auto_added = FALSE) AND EXISTS (SELECT 1 FROM has_resources) + ), + has_proposals AS ( + SELECT 1 FROM posts + JOIN groups_posts ON groups_posts.post_id = posts.id + WHERE groups_posts.group_id = ? AND posts.type = 'proposal' LIMIT 1 + ), + has_moderation AS ( + SELECT 1 FROM moderation_actions WHERE group_id = ? LIMIT 1 + ), + update_decisions AS ( + UPDATE context_widgets + SET parent_id = (SELECT id FROM auto_add_widget), "order" = 7 + WHERE (type = 'decisions' and group_id = ? AND auto_added = FALSE) AND (EXISTS (SELECT 1 FROM has_proposals) OR EXISTS (SELECT 1 FROM has_moderation)) + ), + has_location_posts AS ( + SELECT 1 FROM posts + JOIN groups_posts ON groups_posts.post_id = posts.id + WHERE groups_posts.group_id = ? AND posts.location_id IS NOT NULL LIMIT 1 + ), + has_members_with_location AS ( + SELECT 1 FROM users + JOIN group_memberships ON users.id = group_memberships.user_id + WHERE group_memberships.group_id = ? AND users.location_id IS NOT NULL LIMIT 1 + ), + update_map AS ( + UPDATE context_widgets + SET parent_id = (SELECT id FROM auto_add_widget), "order" = 8 + WHERE (type = 'map' and group_id = ? AND auto_added = FALSE) AND (EXISTS (SELECT 1 FROM has_location_posts) OR EXISTS (SELECT 1 FROM has_members_with_location)) + ), + has_related_groups AS ( + SELECT 1 FROM group_relationships + WHERE parent_group_id = ? OR child_group_id = ? LIMIT 1 + ) + UPDATE context_widgets + SET parent_id = (SELECT id FROM auto_add_widget), "order" = 9 + WHERE (type = 'groups' and group_id = ? AND auto_added = FALSE) AND EXISTS (SELECT 1 FROM has_related_groups); + `, [groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId, groupId]); + + await trx.commit(); + } catch (error) { + await trx.rollback(); + throw error; + } + } }; exports.down = function(knex) { - + // Implement the down migration if necessary }; diff --git a/apps/mobile/locales/en.json b/apps/mobile/locales/en.json index 41759c6ee..b5c3889e7 100644 --- a/apps/mobile/locales/en.json +++ b/apps/mobile/locales/en.json @@ -352,6 +352,7 @@ "widget-public-groups": "Group Explorer", "widget-public-map": "Public Map", "widget-public-stream": "Public Stream", + "widget-resources": "Resources", "widget-setup": "Setup", "widget-stream": "Stream", "Write a comment": "Write a comment...", @@ -410,4 +411,4 @@ "welcome page backup text": "Please take a moment to explore and see what’s alive in our group. \n \n Introduce yourself by clicking Create, and posting a Discussion to share who you are and what you brings you here. \n \n Don’t forget to fill our your profile, so likeminded folks can connect with you", "{{childGroupsLength}} Group(s) are a part of {{currentGroupName}}": "{{childGroupsLength}} Group(s) are a part of {{currentGroupName}}", "{{currentGroupName}} is a part of {{parentGroupsLength}} Group(s)": "{{currentGroupName}} is a part of {{parentGroupsLength}} Group(s)" -} \ No newline at end of file +} diff --git a/apps/mobile/locales/es.json b/apps/mobile/locales/es.json index 6af0fbb08..bfcc60e21 100644 --- a/apps/mobile/locales/es.json +++ b/apps/mobile/locales/es.json @@ -405,9 +405,10 @@ "widget-public-groups": "Explorador de Grupos", "widget-public-map": "Mapa Público", "widget-public-stream": "Flujo Público", + "widget-resources": "Recursos", "widget-setup": "Configuración", "widget-stream": "Flujo", "welcome page backup text": "Tómese un momento para explorar y ver qué hay vivo en nuestro grupo.\n \n Preséntate haciendo clic en Crear y publicando una discusión para compartir quién eres y qué te trae aquí.\n \n No olvide completar nuestro perfil para que personas con ideas afines puedan conectarse con usted.", "{{childGroupsLength}} Group(s) are a part of {{currentGroupName}}": "Los grupos {{childGroupsLength}} son parte de {{currentGroupName}}", "{{currentGroupName}} is a part of {{parentGroupsLength}} Group(s)": "{{currentGroupName}} es parte de {{parentGroupsLength}} grupo(s)" -} \ No newline at end of file +} diff --git a/apps/web/public/locales/en.json b/apps/web/public/locales/en.json index eec54f8cf..95bceed0c 100644 --- a/apps/web/public/locales/en.json +++ b/apps/web/public/locales/en.json @@ -1168,6 +1168,7 @@ "widget-public-groups": "Group Explorer", "widget-public-map": "Public Map", "widget-public-stream": "Public Stream", + "widget-resources": "Resources", "widget-setup": "Finish setting up your group", "widget-stream": "Stream", "with this link": "with this link", diff --git a/apps/web/public/locales/es.json b/apps/web/public/locales/es.json index 1d7a5111a..96af8e503 100644 --- a/apps/web/public/locales/es.json +++ b/apps/web/public/locales/es.json @@ -1167,6 +1167,7 @@ "widget-public-groups": "Explorador de Grupos", "widget-public-map": "Mapa Público", "widget-public-stream": "Flujo Público", + "widget-resources": "Recursos", "widget-setup": "Configuración", "widget-stream": "Flujo", "with this link": "con este enlace", diff --git a/apps/web/src/components/StreamViewControls/StreamViewControls.js b/apps/web/src/components/StreamViewControls/StreamViewControls.js index a138ba418..bacecf696 100644 --- a/apps/web/src/components/StreamViewControls/StreamViewControls.js +++ b/apps/web/src/components/StreamViewControls/StreamViewControls.js @@ -158,7 +158,7 @@ const StreamViewControls = ({ {view === 'events' && timeframeDropdown} {view !== 'events' && makeDropdown(sortBy, customViewType === 'collection' ? COLLECTION_SORT_OPTIONS : STREAM_SORT_OPTIONS, changeSort, t)} - {!['events', 'projects', 'decisions', 'ask-and-offer'].includes(view) && postTypeFilterDropdown} + {!['events', 'projects', 'decisions', 'ask-and-offer', 'resources', 'discussions'].includes(view) && postTypeFilterDropdown} {view === 'decisions' && decisionViewDropdown} diff --git a/apps/web/src/routes/AuthLayoutRouter/AuthLayoutRouter.js b/apps/web/src/routes/AuthLayoutRouter/AuthLayoutRouter.js index 470d6d474..00d469091 100644 --- a/apps/web/src/routes/AuthLayoutRouter/AuthLayoutRouter.js +++ b/apps/web/src/routes/AuthLayoutRouter/AuthLayoutRouter.js @@ -385,9 +385,11 @@ export default function AuthLayoutRouter (props) { } /> } /> } /> + } /> + } /> + } /> } /> } /> - } /> } /> } /> } /> diff --git a/apps/web/src/store/reducers/ormReducer/index.js b/apps/web/src/store/reducers/ormReducer/index.js index c04a838f5..2fdd00582 100644 --- a/apps/web/src/store/reducers/ormReducer/index.js +++ b/apps/web/src/store/reducers/ormReducer/index.js @@ -283,12 +283,16 @@ export default function ormReducer (state = orm.getEmptyState(), action) { if (postType === 'request' || postType === 'offer') { widgetToMove = allWidgets.find(w => w.view === 'ask-and-offer') + } else if (postType === 'discussion') { + widgetToMove = allWidgets.find(w => w.view === 'discussions') } else if (postType === 'project') { widgetToMove = allWidgets.find(w => w.view === 'projects') } else if (postType === 'proposal') { widgetToMove = allWidgets.find(w => w.view === 'decisions') } else if (postType === 'event') { widgetToMove = allWidgets.find(w => w.view === 'event') + } else if (postType === 'resource') { + widgetToMove = allWidgets.find(w => w.view === 'resources') } if (widgetToMove && !widgetToMove.autoAdded) { diff --git a/packages/shared/src/ViewHelpers.js b/packages/shared/src/ViewHelpers.js index 59c53bba6..068be7e74 100644 --- a/packages/shared/src/ViewHelpers.js +++ b/packages/shared/src/ViewHelpers.js @@ -46,10 +46,15 @@ export const COMMON_VIEWS = { postTypes: ['project'], defaultSortBy: 'created' }, + resources: { + name: 'Resources', + icon: 'Document', + defaultViewMode: 'grid', + postTypes: ['resource'], + defaultSortBy: 'created' + }, stream: { name: 'Stream', - icon: 'Stream', - defaultViewMode: 'cards', - defaultSortBy: 'created' + icon: 'Stream' } } From 9d6f5dceb54c0230bb1f4246d9660fd5d8015c6e Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Sun, 2 Feb 2025 14:55:22 -0800 Subject: [PATCH 13/15] Use the default view settings for each common view Instead of one shared settings for all streams. --- apps/web/src/routes/Stream/Stream.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/routes/Stream/Stream.js b/apps/web/src/routes/Stream/Stream.js index 92f0152e7..4d489656a 100644 --- a/apps/web/src/routes/Stream/Stream.js +++ b/apps/web/src/routes/Stream/Stream.js @@ -82,10 +82,10 @@ export default function Stream (props) { const topicLoading = useSelector(state => isPendingFor([FETCH_TOPIC, FETCH_GROUP_TOPIC], state)) - const defaultSortBy = get('settings.streamSortBy', currentUser) || systemView?.defaultSortBy || 'created' - const defaultViewMode = get('settings.streamViewMode', currentUser) || systemView?.defaultViewMode || 'cards' - const defaultPostType = get('settings.streamPostType', currentUser) || undefined - const defaultChildPostInclusion = get('settings.streamChildPosts', currentUser) || 'yes' + const defaultSortBy = systemView?.defaultSortBy || get('settings.streamSortBy', currentUser) || 'created' + const defaultViewMode = systemView?.defaultViewMode || get('settings.streamViewMode', currentUser) || 'cards' + const defaultPostType = systemView?.defaultPostType || get('settings.streamPostType', currentUser) || undefined + const defaultChildPostInclusion = get('settings.streamChildPosts', currentUser) || systemView?.defaultChildPostInclusion || 'yes' const querystringParams = getQuerystringParam(['s', 't', 'v', 'c', 'search', 'timeframe'], location) From ea9f9d2ed188062776845ec6870e3b4a77b48b8f Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 3 Feb 2025 17:24:30 +1100 Subject: [PATCH 14/15] Settle the ordering of all auto-view children --- ...0250203164236_settle-order-of-auto-view.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 apps/backend/migrations/20250203164236_settle-order-of-auto-view.js diff --git a/apps/backend/migrations/20250203164236_settle-order-of-auto-view.js b/apps/backend/migrations/20250203164236_settle-order-of-auto-view.js new file mode 100644 index 000000000..2eda6401b --- /dev/null +++ b/apps/backend/migrations/20250203164236_settle-order-of-auto-view.js @@ -0,0 +1,43 @@ +exports.up = async function(knex) { + // Fetch all group IDs + const groups = await knex('groups').select('id'); + const groupIds = groups.map(group => parseInt(group.id)) + + // Process each group in its own transaction + for (const groupId of groupIds) { + console.log('Settling auto-view order for group', groupId,) + const trx = await knex.transaction() + + const autoViewWidget = await trx('context_widgets') + .where({ + type: 'auto-view', + group_id: groupId + }) + .first(); + + try { + await trx.raw(` + WITH numbered_widgets AS ( + SELECT + id, + ROW_NUMBER() OVER (ORDER BY "order" ASC) as new_order + FROM context_widgets + WHERE parent_id = ? + ) + UPDATE context_widgets + SET "order" = numbered_widgets.new_order + FROM numbered_widgets + WHERE context_widgets.id = numbered_widgets.id + `, [autoViewWidget.id]); + + await trx.commit(); + } catch (error) { + await trx.rollback(); + throw error; + } + } +}; + +exports.down = function(knex) { + // Implement the down migration if necessary +}; From 23eeebaae226e04aa0b5e4721b0997f8a07f0768 Mon Sep 17 00:00:00 2001 From: Tibet Sprague Date: Mon, 3 Feb 2025 08:59:03 -0800 Subject: [PATCH 15/15] Only settle widget order for active groups --- .../20250203164236_settle-order-of-auto-view.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/backend/migrations/20250203164236_settle-order-of-auto-view.js b/apps/backend/migrations/20250203164236_settle-order-of-auto-view.js index 2eda6401b..1898c6b3a 100644 --- a/apps/backend/migrations/20250203164236_settle-order-of-auto-view.js +++ b/apps/backend/migrations/20250203164236_settle-order-of-auto-view.js @@ -1,13 +1,13 @@ exports.up = async function(knex) { // Fetch all group IDs - const groups = await knex('groups').select('id'); + const groups = await knex('groups').where('active', true).select('id'); const groupIds = groups.map(group => parseInt(group.id)) // Process each group in its own transaction for (const groupId of groupIds) { console.log('Settling auto-view order for group', groupId,) const trx = await knex.transaction() - + const autoViewWidget = await trx('context_widgets') .where({ type: 'auto-view', @@ -18,7 +18,7 @@ exports.up = async function(knex) { try { await trx.raw(` WITH numbered_widgets AS ( - SELECT + SELECT id, ROW_NUMBER() OVER (ORDER BY "order" ASC) as new_order FROM context_widgets @@ -29,7 +29,7 @@ exports.up = async function(knex) { FROM numbered_widgets WHERE context_widgets.id = numbered_widgets.id `, [autoViewWidget.id]); - + await trx.commit(); } catch (error) { await trx.rollback();