feat(organization): enhance organization selection and sharing UI

- Updated organization selection dropdowns to include new SVG icons for better visual representation.
- Improved organization option layout with additional metadata display, including member count and share count.
- Enhanced share item display with organization icons and improved action buttons for better user interaction.
- Adjusted styles for organization-related components to improve usability and aesthetics.

This update enhances the user experience by providing clearer organization visuals and more intuitive sharing options.
This commit is contained in:
wizardchen
2026-02-02 22:45:13 +08:00
committed by lyingbug
parent d854083c70
commit ffbb8e1b02
7 changed files with 383 additions and 169 deletions
@@ -18,6 +18,8 @@
v-model="shareForm.organization_id"
:placeholder="$t('organization.share.selectOrgPlaceholder')"
:loading="loadingOrgs"
class="org-select-dropdown"
:popup-props="{ overlayClassName: 'org-select-dropdown-popup' }"
>
<t-option
v-for="org in availableOrganizations"
@@ -26,25 +28,29 @@
:label="org.name"
>
<div class="org-option-content">
<div class="org-option-header">
<span class="org-option-name">{{ org.name }}</span>
<t-tag v-if="org.is_owner" theme="primary" size="small" variant="light">
{{ $t('organization.owner') }}
</t-tag>
<t-tag v-else-if="org.my_role" :theme="org.my_role === 'admin' ? 'warning' : 'default'" size="small" variant="light">
{{ $t(`organization.role.${org.my_role}`) }}
</t-tag>
<div class="org-option-icon-wrap">
<img src="@/assets/img/organization.svg" class="org-option-icon" alt="" aria-hidden="true" />
</div>
<div v-if="org.description" class="org-option-desc">{{ org.description }}</div>
<div class="org-option-meta">
<span class="org-meta-item">
<t-icon name="usergroup" size="14px" />
{{ org.member_count || 0 }} {{ $t('organization.members') }}
</span>
<span v-if="org.share_count !== undefined" class="org-meta-item">
<t-icon name="share" size="14px" />
{{ org.share_count }} {{ $t('organization.share.sharedKBs') }}
</span>
<div class="org-option-body">
<div class="org-option-header">
<span class="org-option-name">{{ org.name }}</span>
<t-tag v-if="org.is_owner" theme="primary" size="small" variant="light">
{{ $t('organization.owner') }}
</t-tag>
<t-tag v-else-if="org.my_role" :theme="org.my_role === 'admin' ? 'warning' : 'default'" size="small" variant="light">
{{ $t(`organization.role.${org.my_role}`) }}
</t-tag>
</div>
<div class="org-option-meta">
<span class="org-meta-item">
<t-icon name="usergroup" size="12px" />
{{ org.member_count || 0 }} {{ $t('organization.members') }}
</span>
<span v-if="org.share_count !== undefined" class="org-meta-item">
<t-icon name="share" size="12px" />
{{ org.share_count }} {{ $t('organization.share.sharedKBs') }}
</span>
</div>
</div>
</div>
</t-option>
@@ -90,19 +96,27 @@
<div v-else class="share-items">
<div v-for="share in shares" :key="share.id" class="share-item">
<div class="share-info">
<img src="@/assets/img/organization.svg" class="share-org-icon" alt="" aria-hidden="true" />
<span class="share-org-name">{{ share.organization_name }}</span>
<t-tag :theme="share.permission === 'editor' ? 'warning' : 'default'" size="small">
{{ share.permission === 'editor' ? $t('organization.share.permissionEditable') : $t('organization.share.permissionReadonly') }}
</t-tag>
</div>
<t-button
variant="text"
theme="danger"
size="small"
@click="handleUnshare(share)"
>
<t-icon name="close" />
</t-button>
<div class="share-actions">
<t-tooltip :content="$t('organization.settings.editTitle')" placement="top">
<t-button variant="text" theme="default" size="small" @click="handleGoToOrgSettings(share.organization_id)">
<t-icon name="setting" />
</t-button>
</t-tooltip>
<t-button
variant="text"
theme="danger"
size="small"
@click="handleUnshare(share)"
>
<t-icon name="close" />
</t-button>
</div>
</div>
</div>
</div>
@@ -113,11 +127,13 @@
import { ref, computed, watch } from 'vue'
import { MessagePlugin } from 'tdesign-vue-next'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useOrganizationStore } from '@/stores/organization'
import { shareKnowledgeBase, listKBShares, removeShare } from '@/api/organization'
import type { KnowledgeBaseShare } from '@/api/organization'
const { t } = useI18n()
const router = useRouter()
const orgStore = useOrganizationStore()
interface Props {
@@ -241,6 +257,16 @@ async function handleUnshare(share: KnowledgeBaseShare) {
function handleClose() {
emit('update:visible', false)
}
// Navigate to organization settings
function handleGoToOrgSettings(orgId: string) {
router.push({
path: '/platform/organizations',
query: { orgId }
})
// 关闭当前弹窗
emit('update:visible', false)
}
</script>
<style lang="less" scoped>
@@ -294,101 +320,150 @@ function handleClose() {
.share-items {
display: flex;
flex-direction: column;
gap: 8px;
gap: 10px;
max-height: 280px;
overflow-y: auto;
}
.share-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
padding: 14px 16px;
background: var(--td-bg-color-container-hover);
border-radius: 6px;
border: 1px solid var(--td-component-stroke);
border-radius: 8px;
transition: background 0.2s, border-color 0.2s;
}
.share-item:hover {
background: var(--td-bg-color-container-active);
border-color: var(--td-component-stroke);
}
.share-info {
display: flex;
align-items: center;
gap: 12px;
gap: 10px;
}
.share-org-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.share-org-name {
font-weight: 500;
}
// Custom option styles for organization select
.share-actions {
display: flex;
align-items: center;
gap: 6px;
}
// Custom option styles for organization select (compact)
:deep(.t-select-option) {
height: auto;
align-items: flex-start;
padding-top: 8px;
padding-bottom: 8px;
align-items: center;
padding: 6px 12px;
border-radius: 4px;
margin: 1px 6px;
transition: background 0.15s ease;
}
:deep(.t-select-option:hover),
:deep(.t-select-option.t-is-selected) {
background: var(--td-bg-color-container-hover);
}
:deep(.t-select-option__content) {
white-space: normal;
width: 100%;
}
.org-option-content {
padding: 2px 0;
min-width: 280px;
display: flex;
align-items: center;
gap: 10px;
padding: 0;
min-width: 260px;
width: 100%;
}
.org-option-icon-wrap {
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 6px;
background: rgba(0, 82, 217, 0.06);
display: flex;
align-items: center;
justify-content: center;
}
.org-option-icon {
width: 16px;
height: 16px;
}
.org-option-body {
flex: 1;
min-width: 0;
}
.org-option-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
gap: 6px;
margin-bottom: 2px;
.org-option-name {
font-family: "PingFang SC";
font-size: 14px;
font-size: 13px;
font-weight: 500;
color: #000000e6;
color: var(--td-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.org-option-desc {
font-family: "PingFang SC";
font-size: 12px;
color: #00000066;
line-height: 18px;
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.org-option-meta {
display: flex;
align-items: center;
gap: 16px;
gap: 12px;
font-family: "PingFang SC";
font-size: 12px;
color: #00000066;
font-size: 11px;
color: var(--td-text-color-placeholder);
.org-meta-item {
display: flex;
align-items: center;
gap: 4px;
gap: 3px;
}
}
</style>
<style lang="less">
// Global styles for select dropdown (outside scoped)
.t-select__dropdown .t-select-option {
height: auto;
align-items: flex-start;
padding-top: 8px;
padding-bottom: 8px;
// Global styles for organization select dropdown (compact)
.org-select-dropdown-popup.t-select__dropdown {
padding: 4px 0;
max-height: 320px;
overflow-y: auto;
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.t-select__dropdown .t-select-option__content {
white-space: normal;
.org-select-dropdown-popup .t-select-option {
height: auto;
align-items: center;
padding: 6px 12px;
border-radius: 4px;
margin: 1px 6px;
}
.org-select-dropdown-popup .t-select-option__content {
width: 100%;
}
</style>
@@ -248,11 +248,11 @@
</div>
<div class="bottom-right">
<t-tooltip :content="kb.org_name" placement="top">
<div class="org-source">
<t-icon name="usergroup" size="12px" />
<span>{{ kb.org_name }}</span>
</div>
</t-tooltip>
<div class="org-source">
<img src="@/assets/img/organization.svg" class="org-source-icon" alt="" aria-hidden="true" />
<span>{{ kb.org_name }}</span>
</div>
</t-tooltip>
</div>
</div>
</div>
@@ -342,7 +342,7 @@
</div>
</t-tooltip>
<!-- 共享状态图标 -->
<t-tooltip v-if="kb.share_count > 0" :content="$t('knowledgeList.sharedToOrgs', { count: kb.share_count })" placement="top">
<t-tooltip v-if="(kb.share_count ?? 0) > 0" :content="$t('knowledgeList.sharedToOrgs', { count: kb.share_count ?? 0 })" placement="top">
<div class="feature-badge shared">
<t-icon name="share" size="14px" />
</div>
@@ -416,7 +416,7 @@
<div class="bottom-right">
<t-tooltip :content="shared.org_name" placement="top">
<div class="org-source">
<t-icon name="usergroup" size="12px" />
<img src="@/assets/img/organization.svg" class="org-source-icon" alt="" aria-hidden="true" />
<span>{{ shared.org_name }}</span>
</div>
</t-tooltip>
@@ -551,8 +551,8 @@
</div>
<div class="form-item read-only-row">
<label class="form-label">{{ t('knowledgeList.detail.sourceOrg') }}</label>
<div class="form-value-row">
<t-icon name="usergroup" size="16px" class="form-value-icon" />
<div class="form-value-row form-value-org">
<img src="@/assets/img/organization.svg" class="form-value-org-icon" alt="" aria-hidden="true" />
<span>{{ currentSharedKb.org_name }}</span>
</div>
</div>
@@ -1224,11 +1224,11 @@ const handleUploadFinishedEvent = (event: Event) => {
}
}
// 来源组织
// 来源组织(空间图标)
.org-source {
display: flex;
align-items: center;
gap: 4px;
gap: 6px;
padding: 2px 8px;
background: rgba(0, 0, 0, 0.04);
border-radius: 4px;
@@ -1242,6 +1242,12 @@ const handleUploadFinishedEvent = (event: Event) => {
white-space: nowrap;
}
.org-source-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.t-icon {
color: #999;
flex-shrink: 0;
@@ -1517,6 +1523,16 @@ const handleUploadFinishedEvent = (event: Event) => {
color: #00000099;
}
.form-value-org {
gap: 8px;
}
.form-value-org-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.permission-list {
margin-top: 20px;
display: flex;
@@ -14,7 +14,8 @@
v-model="selectedOrgId"
:placeholder="$t('organization.share.selectOrgPlaceholder')"
:loading="loadingOrgs"
class="org-select"
class="org-select org-select-dropdown"
:popup-props="{ overlayClassName: 'org-select-dropdown-popup' }"
>
<t-option
v-for="org in availableOrganizations"
@@ -23,25 +24,29 @@
:label="org.name"
>
<div class="org-option-content">
<div class="org-option-header">
<span class="org-option-name">{{ org.name }}</span>
<t-tag v-if="org.is_owner" theme="primary" size="small" variant="light">
{{ $t('organization.owner') }}
</t-tag>
<t-tag v-else-if="org.my_role" :theme="org.my_role === 'admin' ? 'warning' : 'default'" size="small" variant="light">
{{ $t(`organization.role.${org.my_role}`) }}
</t-tag>
<div class="org-option-icon-wrap">
<img src="@/assets/img/organization.svg" class="org-option-icon" alt="" aria-hidden="true" />
</div>
<div v-if="org.description" class="org-option-desc">{{ org.description }}</div>
<div class="org-option-meta">
<span class="org-meta-item">
<t-icon name="usergroup" size="14px" />
{{ org.member_count || 0 }} {{ $t('organization.members') }}
</span>
<span v-if="org.share_count !== undefined" class="org-meta-item">
<t-icon name="share" size="14px" />
{{ org.share_count }} {{ $t('organization.share.sharedKBs') }}
</span>
<div class="org-option-body">
<div class="org-option-header">
<span class="org-option-name">{{ org.name }}</span>
<t-tag v-if="org.is_owner" theme="primary" size="small" variant="light">
{{ $t('organization.owner') }}
</t-tag>
<t-tag v-else-if="org.my_role" :theme="org.my_role === 'admin' ? 'warning' : 'default'" size="small" variant="light">
{{ $t(`organization.role.${org.my_role}`) }}
</t-tag>
</div>
<div class="org-option-meta">
<span class="org-meta-item">
<t-icon name="usergroup" size="12px" />
{{ org.member_count || 0 }} {{ $t('organization.members') }}
</span>
<span v-if="org.share_count !== undefined" class="org-meta-item">
<t-icon name="share" size="12px" />
{{ org.share_count }} {{ $t('organization.share.sharedKBs') }}
</span>
</div>
</div>
</div>
</t-option>
@@ -87,7 +92,7 @@
<div v-for="share in shares" :key="share.id" class="share-item">
<div class="share-info">
<div class="share-org">
<t-icon name="usergroup" class="org-icon" />
<img src="@/assets/img/organization-green.svg" class="org-icon" alt="" aria-hidden="true" />
<span class="org-name">{{ share.organization_name }}</span>
</div>
<t-tag
@@ -99,6 +104,11 @@
</t-tag>
</div>
<div class="share-actions">
<t-tooltip :content="$t('organization.settings.editTitle')" placement="top">
<t-button variant="text" theme="default" size="small" @click="handleGoToOrgSettings(share.organization_id)">
<t-icon name="setting" />
</t-button>
</t-tooltip>
<t-select
:value="share.permission"
size="small"
@@ -137,11 +147,13 @@
import { ref, computed, watch, onMounted } from 'vue'
import { MessagePlugin } from 'tdesign-vue-next'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useOrganizationStore } from '@/stores/organization'
import { shareKnowledgeBase, listKBShares, removeShare, updateSharePermission } from '@/api/organization'
import type { KnowledgeBaseShare } from '@/api/organization'
const { t } = useI18n()
const router = useRouter()
const orgStore = useOrganizationStore()
interface Props {
@@ -258,6 +270,14 @@ async function handleUnshare(share: KnowledgeBaseShare) {
}
}
// Navigate to organization settings
function handleGoToOrgSettings(orgId: string) {
router.push({
path: '/platform/organizations',
query: { orgId }
})
}
// Watch for kbId changes
watch(() => props.kbId, async (newKbId) => {
if (newKbId) {
@@ -323,14 +343,16 @@ onMounted(async () => {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
.org-select {
flex: 1;
min-width: 200px;
min-width: 240px;
}
.permission-select {
width: 120px;
flex-shrink: 0;
}
}
@@ -390,20 +412,24 @@ onMounted(async () => {
.shares-list {
display: flex;
flex-direction: column;
gap: 8px;
gap: 10px;
max-height: 320px;
overflow-y: auto;
}
.share-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
padding: 14px 16px;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 8px;
transition: background 0.2s ease;
transition: background 0.2s ease, border-color 0.2s ease;
&:hover {
background: #f5f5f5;
border-color: #e8e8e8;
}
}
@@ -419,7 +445,9 @@ onMounted(async () => {
gap: 8px;
.org-icon {
font-size: 16px;
width: 18px;
height: 18px;
flex-shrink: 0;
color: #0052d9;
}
@@ -434,7 +462,7 @@ onMounted(async () => {
.share-actions {
display: flex;
align-items: center;
gap: 8px;
gap: 6px;
.permission-change-select {
width: 100px;
@@ -472,79 +500,107 @@ onMounted(async () => {
}
}
// Custom option styles for organization select
// Custom option styles for organization select (compact)
:deep(.t-select-option) {
height: auto;
align-items: flex-start;
padding-top: 8px;
padding-bottom: 8px;
align-items: center;
padding: 6px 12px;
border-radius: 4px;
margin: 1px 6px;
transition: background 0.15s ease;
}
:deep(.t-select-option:hover),
:deep(.t-select-option.t-is-selected) {
background: #f0f7ff;
}
:deep(.t-select-option__content) {
white-space: normal;
width: 100%;
}
.org-option-content {
padding: 2px 0;
min-width: 280px;
display: flex;
align-items: center;
gap: 10px;
padding: 0;
min-width: 260px;
width: 100%;
}
.org-option-icon-wrap {
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 6px;
background: rgba(0, 82, 217, 0.06);
display: flex;
align-items: center;
justify-content: center;
}
.org-option-icon {
width: 16px;
height: 16px;
}
.org-option-body {
flex: 1;
min-width: 0;
}
.org-option-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
gap: 6px;
margin-bottom: 2px;
.org-option-name {
font-family: "PingFang SC";
font-size: 14px;
font-size: 13px;
font-weight: 500;
color: #000000e6;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.org-option-desc {
font-family: "PingFang SC";
font-size: 12px;
color: #00000066;
line-height: 18px;
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.org-option-meta {
display: flex;
align-items: center;
gap: 16px;
gap: 12px;
font-family: "PingFang SC";
font-size: 12px;
color: #00000066;
font-size: 11px;
color: #00000099;
.org-meta-item {
display: flex;
align-items: center;
gap: 4px;
gap: 3px;
}
}
</style>
<style lang="less">
// Global styles for select dropdown (outside scoped)
.t-select__dropdown .t-select-option {
height: auto;
align-items: flex-start;
padding-top: 8px;
padding-bottom: 8px;
// Global styles for organization select dropdown (compact)
.org-select-dropdown-popup.t-select__dropdown {
padding: 4px 0;
max-height: 320px;
overflow-y: auto;
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.t-select__dropdown .t-select-option__content {
white-space: normal;
.org-select-dropdown-popup .t-select-option {
height: auto;
align-items: center;
padding: 6px 12px;
border-radius: 4px;
margin: 1px 6px;
}
.org-select-dropdown-popup .t-select-option__content {
width: 100%;
}
</style>
@@ -432,7 +432,15 @@
</div>
<div class="preview-detail-title-block">
<h2 class="preview-detail-name">{{ invitePreviewData.name }}</h2>
<p class="preview-detail-id">{{ $t('organization.join.spaceId') }}: {{ invitePreviewData.id }}</p>
<div class="preview-detail-id-row">
<span class="preview-detail-id-label">{{ $t('organization.join.spaceId') }}</span>
<span class="preview-detail-id-value">{{ shortPreviewSpaceId }}</span>
<t-tooltip :content="$t('common.copy')">
<t-button variant="text" size="small" class="preview-detail-id-copy" @click="copyPreviewSpaceId">
<t-icon name="file-copy" />
</t-button>
</t-tooltip>
</div>
</div>
</div>
</div>
@@ -605,6 +613,13 @@ const inviteBodyWrapStyle = computed(() => {
return { maxHeight: `${px}px`, minHeight: `${px}px` }
})
// 预览中空间 ID 的简短显示(前 8 位 + …)
const shortPreviewSpaceId = computed(() => {
const id = invitePreviewData.value?.id
if (!id) return ''
return id.length > 8 ? `${id.slice(0, 8)}…` : id
})
// 根据当前 body 内容更新高度(用于过渡动画)
function updateInviteBodyHeight() {
const el = inviteBodyInnerRef.value
@@ -1090,6 +1105,16 @@ function viewOrganizationFromPreview() {
showSettingsModal.value = true
}
// 复制预览中的空间 ID
function copyPreviewSpaceId() {
if (!invitePreviewData.value?.id) return
navigator.clipboard.writeText(invitePreviewData.value.id).then(() => {
MessagePlugin.success(t('common.copied'))
}).catch(() => {
MessagePlugin.error('复制失败')
})
}
// 从搜索列表加入空间(通过空间 ID,无需邀请码)- 在预览确认后调用
async function joinBySearchOrg() {
if (!invitePreviewData.value || invitePreviewData.value.is_already_member) return
@@ -1134,6 +1159,18 @@ onMounted(async () => {
if (code) {
await handleInvitePreview(code)
}
// 检查 URL 中是否有 orgId,如果有则打开空间设置
const orgId = route.query.orgId as string
if (orgId) {
settingsOrgId.value = orgId
settingsMode.value = 'edit'
showSettingsModal.value = true
// 清除 URL 中的 orgId 参数,避免刷新时重复打开
const newQuery = { ...route.query }
delete newQuery.orgId
router.replace({ path: route.path, query: newQuery })
}
})
onUnmounted(() => {
@@ -2407,12 +2444,33 @@ onUnmounted(() => {
letter-spacing: -0.02em;
}
.preview-detail-id {
.preview-detail-id-row {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
font-size: 12px;
color: #94a3b8;
margin: 0;
font-family: "PingFang SC", system-ui, sans-serif;
word-break: break-all;
}
.preview-detail-id-label {
flex-shrink: 0;
}
.preview-detail-id-value {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 11px;
letter-spacing: 0.02em;
color: #64748b;
}
.preview-detail-id-copy {
padding: 2px;
color: #94a3b8;
}
.preview-detail-id-copy:hover {
color: #0f172a;
}
.preview-detail-content {
@@ -2571,7 +2629,6 @@ onUnmounted(() => {
.invite-preview-footer {
padding: 20px 24px;
border-top: 1px solid #e2e8f0;
background: #f8fafc;
display: flex;
justify-content: flex-end;
gap: 12px;
@@ -492,9 +492,8 @@
class="shared-item"
@click="handleShareClick(share)"
>
<div :class="['shared-icon', share.knowledge_base_type === 'faq' ? 'type-faq' : 'type-document']">
<t-icon :name="share.knowledge_base_type === 'faq' ? 'chat-bubble-help' : 'folder'" size="16px" />
<span class="badge-count">{{ share.knowledge_base_type === 'faq' ? (share.chunk_count || 0) : (share.knowledge_count || 0) }}</span>
<div class="shared-icon shared-icon-org">
<img src="@/assets/img/organization.svg" class="org-icon-img" alt="" aria-hidden="true" />
</div>
<div class="shared-info">
<span class="shared-name">{{ share.knowledge_base_name }}</span>
@@ -2258,6 +2257,17 @@ watch(currentSection, (section) => {
color: #0052d9;
}
&.shared-icon-org {
background: rgba(0, 82, 217, 0.06);
color: #0052d9;
}
.org-icon-img {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.badge-count {
font-size: 12px;
font-weight: 500;
+9 -9
View File
@@ -240,15 +240,15 @@ func (s *organizationService) SearchSearchableOrganizations(ctx context.Context,
items := make([]types.SearchableOrganizationItem, 0, len(orgs))
for _, org := range orgs {
items = append(items, types.SearchableOrganizationItem{
ID: org.ID,
Name: org.Name,
Description: org.Description,
Avatar: org.Avatar,
MemberCount: int(memberCounts[org.ID]),
MemberLimit: org.MemberLimit,
ShareCount: int(shareCounts[org.ID]),
IsAlreadyMember: memberOrgIDs[org.ID],
RequireApproval: org.RequireApproval,
ID: org.ID,
Name: org.Name,
Description: org.Description,
Avatar: org.Avatar,
MemberCount: int(memberCounts[org.ID]),
MemberLimit: org.MemberLimit,
ShareCount: int(shareCounts[org.ID]),
IsAlreadyMember: memberOrgIDs[org.ID],
RequireApproval: org.RequireApproval,
})
}
return &types.ListSearchableOrganizationsResponse{
+14 -14
View File
@@ -229,9 +229,9 @@ type UpdateOrganizationRequest struct {
Description *string `json:"description" binding:"omitempty,max=1000"`
Avatar *string `json:"avatar" binding:"omitempty,max=512"` // optional avatar URL
RequireApproval *bool `json:"require_approval"`
Searchable *bool `json:"searchable"` // open for search so others can discover and join
Searchable *bool `json:"searchable"` // open for search so others can discover and join
InviteCodeValidityDays *int `json:"invite_code_validity_days"` // 0=never, 1, 7, 30
MemberLimit *int `json:"member_limit"` // max members; 0=unlimited
MemberLimit *int `json:"member_limit"` // max members; 0=unlimited
}
// AddMemberRequest represents a request to add a member to an organization
@@ -350,28 +350,28 @@ type ListOrganizationsResponse struct {
// SearchableOrganizationItem is a searchable org item for discovery (no invite code)
type SearchableOrganizationItem struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Avatar string `json:"avatar,omitempty"`
MemberCount int `json:"member_count"`
MemberLimit int `json:"member_limit"` // 0 = unlimited
ShareCount int `json:"share_count"`
IsAlreadyMember bool `json:"is_already_member"`
RequireApproval bool `json:"require_approval"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Avatar string `json:"avatar,omitempty"`
MemberCount int `json:"member_count"`
MemberLimit int `json:"member_limit"` // 0 = unlimited
ShareCount int `json:"share_count"`
IsAlreadyMember bool `json:"is_already_member"`
RequireApproval bool `json:"require_approval"`
}
// ListSearchableOrganizationsResponse is the response for searching discoverable organizations
type ListSearchableOrganizationsResponse struct {
Organizations []SearchableOrganizationItem `json:"organizations"`
Total int64 `json:"total"`
Total int64 `json:"total"`
}
// JoinByOrganizationIDRequest is used to join a searchable organization by ID (no invite code)
type JoinByOrganizationIDRequest struct {
OrganizationID string `json:"organization_id" binding:"required"`
Message string `json:"message" binding:"max=500"` // Optional message for join request
Role OrgMemberRole `json:"role"` // Optional: requested role (admin/editor/viewer); default viewer
Message string `json:"message" binding:"max=500"` // Optional message for join request
Role OrgMemberRole `json:"role"` // Optional: requested role (admin/editor/viewer); default viewer
}
// JoinRequestResponse represents a join request in API responses