diff --git a/app/components/Avatar/Avatar.tsx b/app/components/Avatar/Avatar.tsx
index 51bb49c94c..6b11ffadb5 100644
--- a/app/components/Avatar/Avatar.tsx
+++ b/app/components/Avatar/Avatar.tsx
@@ -71,7 +71,7 @@ function Avatar(props: Props) {
) : model ? (
- {model.initial}
+ {model.initial?.toUpperCase()}
) : (
diff --git a/app/components/HoverPreview/HoverPreview.tsx b/app/components/HoverPreview/HoverPreview.tsx
index f0311b3f5d..3be4c32ca1 100644
--- a/app/components/HoverPreview/HoverPreview.tsx
+++ b/app/components/HoverPreview/HoverPreview.tsx
@@ -117,12 +117,31 @@ const HoverPreviewDesktop = observer(
{isVisible ? (
{data.type === UnfurlResourceType.Mention ? (
diff --git a/app/components/HoverPreview/HoverPreviewGroup.tsx b/app/components/HoverPreview/HoverPreviewGroup.tsx
index f62a8cf1f8..7ebfcaecdf 100644
--- a/app/components/HoverPreview/HoverPreviewGroup.tsx
+++ b/app/components/HoverPreview/HoverPreviewGroup.tsx
@@ -1,4 +1,5 @@
import * as React from "react";
+import { useTranslation } from "react-i18next";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { MAX_AVATAR_DISPLAY } from "@shared/constants";
import User from "~/models/User";
@@ -17,21 +18,30 @@ import ErrorBoundary from "../ErrorBoundary";
type Props = Omit;
const HoverPreviewGroup = React.forwardRef(function _HoverPreviewGroup(
- { name, memberCount, users }: Props,
+ { name, description, memberCount, users }: Props,
ref: React.Ref
) {
+ const { t } = useTranslation();
+
return (
- {name}
-
- {memberCount === 1 ? "1 member" : `${memberCount} members`}
-
- {users.length > 0 && (
-
+
+
+ {name}
+
+ {t("{{ count }} members", { count: memberCount })}
+
+
+ {users.length > 0 && (
@@ -46,8 +56,9 @@ const HoverPreviewGroup = React.forwardRef(function _HoverPreviewGroup(
overflow={Math.max(0, memberCount - users.length)}
limit={MAX_AVATAR_DISPLAY}
/>
-
- )}
+ )}
+
+ {description && {description}}
diff --git a/app/models/Group.ts b/app/models/Group.ts
index f8540e8731..be0940b8a2 100644
--- a/app/models/Group.ts
+++ b/app/models/Group.ts
@@ -12,6 +12,10 @@ class Group extends Model implements Searchable {
@observable
name: string;
+ @Field
+ @observable
+ description: string;
+
@observable
externalId: string | undefined;
@@ -33,7 +37,7 @@ class Group extends Model implements Searchable {
@computed
get searchContent(): string[] {
- return [this.name].filter(Boolean);
+ return [this.name, this.description].filter(Boolean);
}
@computed
diff --git a/app/scenes/Settings/components/GroupDialogs.tsx b/app/scenes/Settings/components/GroupDialogs.tsx
index d1b9330e5b..04e18c6629 100644
--- a/app/scenes/Settings/components/GroupDialogs.tsx
+++ b/app/scenes/Settings/components/GroupDialogs.tsx
@@ -27,6 +27,7 @@ import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import { GroupPermission } from "@shared/types";
+import { GroupValidation } from "@shared/validations";
import { EmptySelectValue, Permission } from "~/types";
import GroupUser from "~/models/GroupUser";
import Switch from "~/components/Switch";
@@ -40,6 +41,7 @@ export function CreateGroupDialog() {
const { dialogs, groups } = useStores();
const { t } = useTranslation();
const [name, setName] = React.useState();
+ const [description, setDescription] = React.useState();
const [isSaving, setIsSaving] = React.useState(false);
const handleSubmit = React.useCallback(
@@ -50,6 +52,7 @@ export function CreateGroupDialog() {
const group = new Group(
{
name,
+ description,
},
groups
);
@@ -67,7 +70,7 @@ export function CreateGroupDialog() {
setIsSaving(false);
}
},
- [t, dialogs, groups, name]
+ [t, dialogs, groups, name, description]
);
return (
@@ -79,7 +82,7 @@ export function CreateGroupDialog() {
example.
-
+
+ setDescription(e.target.value)}
+ value={description || ""}
+ maxLength={GroupValidation.maxDescriptionLength}
+ flex
+ />
You’ll be able to add people to the group next.
@@ -104,6 +116,7 @@ export function CreateGroupDialog() {
export function EditGroupDialog({ group, onSubmit }: Props) {
const { t } = useTranslation();
const [name, setName] = React.useState(group.name);
+ const [description, setDescription] = React.useState(group.description || "");
const [disableMentions, setDisableMentions] = React.useState(
group.disableMentions || false
);
@@ -116,6 +129,7 @@ export function EditGroupDialog({ group, onSubmit }: Props) {
try {
await group.save({
name,
+ description,
disableMentions,
});
onSubmit();
@@ -125,7 +139,7 @@ export function EditGroupDialog({ group, onSubmit }: Props) {
setIsSaving(false);
}
},
- [group, onSubmit, name, disableMentions]
+ [group, onSubmit, name, description, disableMentions]
);
const handleNameChange = React.useCallback(
@@ -153,6 +167,15 @@ export function EditGroupDialog({ group, onSubmit }: Props) {
autoFocus
flex
/>
+ setDescription(e.target.value)}
+ value={description}
+ maxLength={GroupValidation.maxDescriptionLength}
+ flex
+ />
group.description || "",
+ component: (group) => (
+
+ {group.description}
+
+ ),
+ width: "2fr",
+ },
{
type: "data",
id: "members",
@@ -97,30 +109,6 @@ export function GroupsTable(props: Props) {
width: "1fr",
sortable: false,
},
- {
- type: "data",
- id: "admins",
- header: t("Admins"),
- accessor: (group) => `${group.memberCount} admins`,
- component: (group) => {
- const users = group.admins.slice(0, MAX_AVATAR_DISPLAY);
-
- if (users.length === 0) {
- return null;
- }
-
- return (
- handleViewMembers(group)}
- width={users.length * AvatarSize.Large}
- >
-
-
- );
- },
- width: "1fr",
- sortable: false,
- },
{
type: "data",
id: "createdAt",
diff --git a/server/migrations/20251029200610-add-description-to-groups.js b/server/migrations/20251029200610-add-description-to-groups.js
new file mode 100644
index 0000000000..ed768e908e
--- /dev/null
+++ b/server/migrations/20251029200610-add-description-to-groups.js
@@ -0,0 +1,15 @@
+"use strict";
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+ async up(queryInterface, Sequelize) {
+ await queryInterface.addColumn("groups", "description", {
+ type: Sequelize.TEXT,
+ allowNull: true,
+ });
+ },
+
+ async down(queryInterface, Sequelize) {
+ await queryInterface.removeColumn("groups", "description");
+ },
+};
diff --git a/server/models/Group.ts b/server/models/Group.ts
index 2d508eb16a..fdba598396 100644
--- a/server/models/Group.ts
+++ b/server/models/Group.ts
@@ -9,6 +9,7 @@ import {
DataType,
Scopes,
} from "sequelize-typescript";
+import { GroupValidation } from "@shared/validations";
import GroupMembership from "./GroupMembership";
import GroupUser from "./GroupUser";
import Team from "./Team";
@@ -65,6 +66,10 @@ class Group extends ParanoidModel<
@Column
name: string;
+ @Length({ min: 0, max: GroupValidation.maxDescriptionLength, msg: `description must be ${GroupValidation.maxDescriptionLength} characters or less` })
+ @Column(DataType.TEXT)
+ description: string;
+
@Column
externalId: string;
diff --git a/server/presenters/group.ts b/server/presenters/group.ts
index 6093674bf3..be7c732dae 100644
--- a/server/presenters/group.ts
+++ b/server/presenters/group.ts
@@ -4,6 +4,7 @@ export default async function presentGroup(group: Group) {
return {
id: group.id,
name: group.name,
+ description: group.description,
externalId: group.externalId,
memberCount: await group.memberCount,
disableMentions: group.disableMentions,
diff --git a/server/presenters/unfurl.ts b/server/presenters/unfurl.ts
index f9a08ff1fe..d332e3cf4e 100644
--- a/server/presenters/unfurl.ts
+++ b/server/presenters/unfurl.ts
@@ -65,6 +65,7 @@ const presentGroup = async (
return {
type: UnfurlResourceType.Group,
name: group.name,
+ description: group.description,
memberCount,
users: (data.users as User[]).map((user) => ({
id: user.id,
diff --git a/server/routes/api/groups/schema.ts b/server/routes/api/groups/schema.ts
index 38bc171973..aee4c07762 100644
--- a/server/routes/api/groups/schema.ts
+++ b/server/routes/api/groups/schema.ts
@@ -1,5 +1,6 @@
import { z } from "zod";
import { GroupPermission } from "@shared/types";
+import { GroupValidation } from "@shared/validations";
import { Group } from "@server/models";
const BaseIdSchema = z.object({
@@ -49,6 +50,8 @@ export const GroupsCreateSchema = z.object({
body: z.object({
/** Group name */
name: z.string(),
+ /** Group description */
+ description: z.string().max(GroupValidation.maxDescriptionLength).optional(),
/** Optionally link this group to an external source. */
externalId: z.string().optional(),
/** Whether mentions are disabled for this group */
@@ -62,6 +65,8 @@ export const GroupsUpdateSchema = z.object({
body: BaseIdSchema.extend({
/** Group name */
name: z.string().optional(),
+ /** Group description */
+ description: z.string().max(GroupValidation.maxDescriptionLength).optional(),
/** Optionally link this group to an external source. */
externalId: z.string().optional(),
/** Whether mentions are disabled for this group */
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 9fdb6d85a6..32a4e8e0b2 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -291,6 +291,8 @@
"Filter options": "Filter options",
"Filter": "Filter",
"No results": "No results",
+ "{{ count }} members": "{{ count }} member",
+ "{{ count }} members_plural": "{{ count }} members",
"{{authorName}} created <3>3>": "{{authorName}} created <3>3>",
"{{authorName}} opened <3>3>": "{{authorName}} opened <3>3>",
"Search emoji": "Search emoji",
@@ -481,8 +483,6 @@
"Image height": "Image height",
"Height": "Height",
"Profile picture": "Profile picture",
- "{{ count }} members": "{{ count }} member",
- "{{ count }} members_plural": "{{ count }} members",
"Create a new doc": "Create a new doc",
"{{ userName }} won't be notified, as they do not have access to this document": "{{ userName }} won't be notified, as they do not have access to this document",
"Members of \"{{ groupName }}\" that have access to this document will be notified": "Members of \"{{ groupName }}\" that have access to this document will be notified",
@@ -998,8 +998,10 @@
"Check server logs for more details.": "Check server logs for more details.",
"{{userName}} requested": "{{userName}} requested",
"Groups are for organizing your team. They work best when centered around a function or a responsibility — Support or Engineering for example.": "Groups are for organizing your team. They work best when centered around a function or a responsibility — Support or Engineering for example.",
+ "Optional": "Optional",
"You’ll be able to add people to the group next.": "You’ll be able to add people to the group next.",
"You can edit the name of this group at any time, however doing so too often might confuse your team mates.": "You can edit the name of this group at any time, however doing so too often might confuse your team mates.",
+ "Description": "Description",
"Disable mentions": "Disable mentions",
"Prevent this group from being mentionable in documents or comments": "Prevent this group from being mentionable in documents or comments",
"Are you sure about that? Deleting the {{groupName}} group will cause its members to lose access to collections and documents that it is associated with.": "Are you sure about that? Deleting the {{groupName}} group will cause its members to lose access to collections and documents that it is associated with.",
@@ -1020,7 +1022,6 @@
"No people left to add": "No people left to add",
"Group admin": "Group admin",
"Member": "Member",
- "Admins": "Admins",
"Date created": "Date created",
"Crop Image": "Crop Image",
"Crop image": "Crop image",
@@ -1050,6 +1051,7 @@
"Domain": "Domain",
"Views": "Views",
"All roles": "All roles",
+ "Admins": "Admins",
"Editors": "Editors",
"All status": "All status",
"Active": "Active",
@@ -1064,7 +1066,6 @@
"The logo is displayed at the top left of the application.": "The logo is displayed at the top left of the application.",
"Workspace logo": "Workspace logo",
"The workspace name, usually the same as your company name.": "The workspace name, usually the same as your company name.",
- "Description": "Description",
"A short description of your workspace.": "A short description of your workspace.",
"Theme": "Theme",
"Customize the interface look and feel.": "Customize the interface look and feel.",
@@ -1222,7 +1223,6 @@
"Deleting this version of the document will permanently and irrevocably remove it from the history.": "Deleting this version of the document will permanently and irrevocably remove it from the history.",
"Format": "Format",
"Add option": "Add option",
- "Optional": "Optional",
"Choose a size for your exported document": "Choose a size for your exported document",
"Revision renamed": "Revision renamed",
"Failed to save revision": "Failed to save revision",
diff --git a/shared/types.ts b/shared/types.ts
index ee48bac5ff..c8889e3bad 100644
--- a/shared/types.ts
+++ b/shared/types.ts
@@ -452,6 +452,8 @@ export type UnfurlResponse = {
type: UnfurlResourceType.Group;
/** Group name */
name: string;
+ /** Group description */
+ description: string | null;
/** Number of members in the group */
memberCount: number;
/** Array of group members (limited to display count) */
diff --git a/shared/validations.ts b/shared/validations.ts
index a0bffa2855..f8ee43a3c8 100644
--- a/shared/validations.ts
+++ b/shared/validations.ts
@@ -54,6 +54,11 @@ export const DocumentValidation = {
maxRecommendedLength: 250000,
};
+export const GroupValidation = {
+ /** The maximum length of the group description */
+ maxDescriptionLength: 2000,
+};
+
export const ImportValidation = {
/** The maximum length of the import name */
maxNameLength: 100,