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/20250130104013_setup-initial-context-widgets.js b/apps/backend/migrations/20250130104013_setup-initial-context-widgets.js index ea9dbc344..cd6b2b99d 100644 --- a/apps/backend/migrations/20250130104013_setup-initial-context-widgets.js +++ b/apps/backend/migrations/20250130104013_setup-initial-context-widgets.js @@ -1,5 +1,4 @@ exports.up = function(knex) { - console.log('Setting up initial context widgets') return knex.raw(` DO $$ DECLARE @@ -11,10 +10,10 @@ exports.up = function(knex) { INSERT INTO context_widgets ( group_id, type, title, "order", created_at, updated_at ) - SELECT + SELECT group_record.id, 'home', 'widget-home', 1, NOW(), NOW() WHERE NOT EXISTS ( - SELECT 1 FROM context_widgets + SELECT 1 FROM context_widgets WHERE group_id = group_record.id AND type = 'home' ) RETURNING id 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 index 99219ffa8..619c9ef32 100644 --- 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 @@ -1,16 +1,15 @@ exports.up = function(knex) { - console.log('Ensuring chats and custom views have context widgets') // 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 + SELECT g.id as group_id, EXISTS ( - SELECT 1 - FROM context_widgets cw + SELECT 1 + FROM context_widgets cw WHERE cw.group_id = g.id ) as has_widgets FROM all_groups g @@ -29,16 +28,16 @@ exports.up = function(knex) { WHERE cw.type IN ('chats', 'custom-views') ), new_chat_widgets AS ( - SELECT DISTINCT + 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' + 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 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..d7f506137 --- /dev/null +++ b/apps/backend/migrations/20250130121530_update-widgets-based-on-group-content.js @@ -0,0 +1,139 @@ +/* globals Group */ +require("@babel/register") +const models = require('../api/models') + +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('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/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..1898c6b3a --- /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').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', + 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 +}; 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/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) 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' } }