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'
}
}