Skip to content

Commit 8fd1420

Browse files
author
Jiuling.Lei
committed
feature-org-not-admin-can-delete-user
1 parent 92faf1e commit 8fd1420

File tree

5 files changed

+239
-13
lines changed

5 files changed

+239
-13
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<template>
2+
<el-dialog
3+
v-model="visible"
4+
width="400px"
5+
:close-on-click-modal="!isLoading"
6+
:close-on-press-escape="!isLoading"
7+
:show-close="false"
8+
@update:model-value="handleDialogClose"
9+
class="leave-org-dialog"
10+
>
11+
<div class="p-[30px]">
12+
<!-- Header with icon and close button -->
13+
<div class="flex items-start justify-between mb-6">
14+
<div class="flex-shrink-0 w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
15+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
16+
<path d="M16 18H22M12 15.5H7.5C6.10444 15.5 5.40665 15.5 4.83886 15.6722C3.56045 16.06 2.56004 17.0605 2.17224 18.3389C2 18.9067 2 19.6044 2 21M14.5 7.5C14.5 9.98528 12.4853 12 10 12C7.51472 12 5.5 9.98528 5.5 7.5C5.5 5.01472 7.51472 3 10 3C12.4853 3 14.5 5.01472 14.5 7.5Z" stroke="#D92D20" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
17+
</svg>
18+
</div>
19+
<button
20+
@click="handleCancel"
21+
:disabled="isLoading"
22+
class="text-gray-400 hover:text-gray-600 transition-colors"
23+
>
24+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
25+
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
26+
</svg>
27+
</button>
28+
</div>
29+
30+
<!-- Content -->
31+
<div class="mb-8">
32+
<h3 class="text-lg font-semibold text-gray-900 mb-3">{{ $t('organization.leaveOrgTitle') }}</h3>
33+
<p class="text-gray-600 leading-relaxed">{{ $t('organization.leaveOrgContent') }}</p>
34+
</div>
35+
36+
<!-- Footer buttons -->
37+
<div class="flex gap-3">
38+
<button
39+
@click="handleCancel"
40+
:disabled="isLoading"
41+
class="flex-1 h-[44px] px-[10px] bg-white border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors font-medium"
42+
>
43+
{{ $t('organization.cancel') }}
44+
</button>
45+
<button
46+
@click="handleLeave"
47+
:disabled="isLoading"
48+
class="flex-1 h-[44px] px-[10px] text-white rounded-md font-medium transition-colors flex items-center justify-center"
49+
:class="isLoading ? 'bg-gray-400 cursor-not-allowed' : 'bg-[#D92D20] hover:bg-[#B91C1C]'"
50+
>
51+
<svg v-if="isLoading" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
52+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
53+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
54+
</svg>
55+
{{ $t('organization.leaveOrgButton') }}
56+
</button>
57+
</div>
58+
</div>
59+
</el-dialog>
60+
</template>
61+
62+
<script setup>
63+
import { ref, computed } from 'vue'
64+
import { useI18n } from 'vue-i18n'
65+
import { ElMessage } from 'element-plus'
66+
import useFetchApi from '../../packs/useFetchApi'
67+
import useUserStore from '../../stores/UserStore'
68+
69+
const props = defineProps({
70+
modelValue: {
71+
type: Boolean,
72+
default: false
73+
},
74+
organizationName: {
75+
type: String,
76+
required: true
77+
},
78+
userRole: {
79+
type: String,
80+
default: ''
81+
}
82+
})
83+
84+
const emit = defineEmits(['update:modelValue', 'success'])
85+
86+
const { t } = useI18n()
87+
const userStore = useUserStore()
88+
const isLoading = ref(false)
89+
90+
const visible = computed({
91+
get: () => props.modelValue,
92+
set: (value) => emit('update:modelValue', value)
93+
})
94+
95+
const handleDialogClose = (value) => {
96+
if (!isLoading.value) {
97+
emit('update:modelValue', value)
98+
}
99+
}
100+
101+
const handleCancel = () => {
102+
if (!isLoading.value) {
103+
emit('update:modelValue', false)
104+
}
105+
}
106+
107+
const handleLeave = async () => {
108+
if (isLoading.value) return
109+
110+
isLoading.value = true
111+
112+
try {
113+
const { response, error } = await useFetchApi(`/organization/${props.organizationName}/members/${userStore.username}`, { body: JSON.stringify({role: props.userRole}) })
114+
.delete()
115+
.json()
116+
117+
if (error.value) {
118+
ElMessage.error(t('organization.leaveOrgFailure'))
119+
} else if (response.value.ok) {
120+
// Update organizations in store before redirect
121+
await userStore.fetchOrgs()
122+
ElMessage.success(t('organization.leaveOrgSuccess'))
123+
124+
// Redirect to profile page
125+
window.location.href = `/profile/${userStore.username}`
126+
} else {
127+
ElMessage.error(t('organization.leaveOrgFailure'))
128+
}
129+
} catch (error) {
130+
console.error('Leave organization error:', error)
131+
ElMessage.error(t('organization.leaveOrgNetworkError'))
132+
} finally {
133+
isLoading.value = false
134+
}
135+
}
136+
</script>
137+
138+
<style>
139+
.el-dialog.leave-org-dialog {
140+
padding: 0 !important;
141+
}
142+
143+
.el-dialog.leave-org-dialog .el-dialog__header {
144+
padding: 0 !important;
145+
}
146+
147+
.el-dialog.leave-org-dialog .el-dialog__body {
148+
padding: 0 !important;
149+
}
150+
</style>

frontend/src/components/organizations/OrganizationDetail.vue

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,31 @@
9090
/>
9191

9292
<div class="mt-[16px] flex flex-wrap gap-[8px]">
93-
<a v-for="user in membersList" :href="`/profile/${user.username}`">
94-
<el-tooltip placement="bottom" effect="light">
95-
<div class="flex flex-col items-center">
96-
<img :src="user.avatar || 'https://cdn.casbin.org/img/casbin.svg'" class="h-[52px] w-[52px] rounded-[50%] border p-[2px]" />
97-
</div>
98-
<template #content>
99-
<span class="text-xs py-[8px] px-[12px]">{{ user.nickname || user.name }} ( {{ user.role }} )</span>
100-
</template>
101-
</el-tooltip>
102-
</a>
93+
<div v-for="user in membersList" class="flex flex-col items-center gap-2">
94+
<a :href="`/profile/${user.username}`">
95+
<el-tooltip placement="bottom" effect="light">
96+
<div class="flex flex-col items-center">
97+
<img :src="user.avatar || 'https://cdn.casbin.org/img/casbin.svg'" class="h-[52px] w-[52px] rounded-[50%] border p-[2px]" />
98+
</div>
99+
<template #content>
100+
<span class="text-xs py-[8px] px-[12px]">{{ user.nickname || user.name }} ( {{ user.role }} )</span>
101+
</template>
102+
</el-tooltip>
103+
</a>
104+
</div>
105+
</div>
106+
107+
<!-- Leave Organization Button - only show for current user if they are a member and not admin/super_admin -->
108+
<div v-if="userStore.isLoggedIn && role && role !== 'admin' && role !== 'super_admin'" class="mt-4">
109+
<button
110+
@click="showLeaveOrgDialog = true"
111+
class="px-2 py-1.5 text-sm text-black bg-white hover:bg-gray-200 rounded-md border border-gray-300 transition-colors duration-200 flex items-center gap-2"
112+
>
113+
<svg width="14" height="14" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
114+
<path d="M8.25 8L10.75 10.5M10.75 8L8.25 10.5M7.75 1.64538C8.48296 1.94207 9 2.66066 9 3.5C9 4.33934 8.48296 5.05793 7.75 5.35462M6 7.5H4C3.06812 7.5 2.60218 7.5 2.23463 7.65224C1.74458 7.85523 1.35523 8.24458 1.15224 8.73463C1 9.10218 1 9.56812 1 10.5M6.75 3.5C6.75 4.60457 5.85457 5.5 4.75 5.5C3.64543 5.5 2.75 4.60457 2.75 3.5C2.75 2.39543 3.64543 1.5 4.75 1.5C5.85457 1.5 6.75 2.39543 6.75 3.5Z" stroke="#344054" stroke-linecap="round" stroke-linejoin="round"/>
115+
</svg>
116+
{{ $t('organization.leaveOrg') }}
117+
</button>
103118
</div>
104119
</div>
105120
<div class="grow px-[20px] py-[36px] sm:py-0">
@@ -113,13 +128,22 @@
113128
:loading="isDataLoading"
114129
:text="$t('organization.loading')"
115130
/>
131+
132+
<!-- Leave Organization Dialog -->
133+
<LeaveOrganizationDialog
134+
v-model="showLeaveOrgDialog"
135+
:organization-name="organizationData.name"
136+
:user-role="currentUserRoleFromMembers"
137+
@success="handleLeaveSuccess"
138+
/>
116139
</template>
117140

118141
<script setup>
119-
import { ref, onMounted, watch } from 'vue'
142+
import { ref, onMounted, watch, computed } from 'vue'
120143
import InviteMember from './InviteMember.vue'
121144
import ProfileRepoList from '../shared/ProfileRepoList.vue'
122145
import LoadingSpinner from '../shared/LoadingSpinner.vue'
146+
import LeaveOrganizationDialog from './LeaveOrganizationDialog.vue'
123147
import useFetchApi from '../../packs/useFetchApi'
124148
import { ElMessage } from 'element-plus'
125149
import useUserStore from '../../stores/UserStore'
@@ -139,11 +163,19 @@
139163
const verifiedStatus = ref('')
140164
const verifiedReason = ref('')
141165
const isDataLoading = ref(false)
166+
const showLeaveOrgDialog = ref(false)
142167
143168
const userStore = useUserStore()
144169
145170
const role = ref('')
146171
172+
// 计算当前用户在成员列表中的角色
173+
const currentUserRoleFromMembers = computed(() => {
174+
if (!userStore.username || !membersList.value.length) return ''
175+
const currentUser = membersList.value.find(user => user.username === userStore.username)
176+
return currentUser?.role || ''
177+
})
178+
147179
const fetchOrgDetail = async () => {
148180
if (isDataLoading.value) {
149181
return false
@@ -198,6 +230,25 @@
198230
}
199231
}
200232
233+
const getOrganizationVerify = async () => {
234+
try {
235+
const { data, error } = await useFetchApi(`/organization/verify/${props.name}`).get().json()
236+
if (error.value) {
237+
ElMessage.warning(error.value.msg)
238+
}
239+
240+
verifiedStatus.value = data.value?.data?.status
241+
verifiedReason.value = data.value?.data?.reason
242+
} catch (error) {
243+
console.error(error)
244+
}
245+
}
246+
247+
const handleLeaveSuccess = () => {
248+
// Refresh member list after successful leave
249+
fetchOrgMemberList()
250+
}
251+
201252
watch(
202253
() => userStore.isLoggedIn,
203254
() => {

frontend/src/locales/en_js/organization.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,13 @@ export const organization = {
6565
prompt: "New Prompt",
6666
collection: "New Collection",
6767
mcp: "New MCP",
68-
}
68+
},
69+
leaveOrg: "Leave Organization",
70+
leaveOrgTitle: "Leave this Organization?",
71+
leaveOrgContent: "After leaving, you will lose access to all resources, projects, and permissions under this organization. To rejoin, please contact an organization admin to invite you again.",
72+
leaveOrgButton: "Leave Organization",
73+
cancel: "Cancel",
74+
leaveOrgSuccess: "You have successfully left the organization.",
75+
leaveOrgFailure: "Failed to leave organization, please try again later.",
76+
leaveOrgNetworkError: "Network error, failed to leave, please try again later."
6977
}

frontend/src/locales/zh_js/organization.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,13 @@ export const organization = {
6565
prompt: "新建提示词库",
6666
collection: "新建合集",
6767
mcp: "新建MCP",
68-
}
68+
},
69+
leaveOrg: "退出组织",
70+
leaveOrgTitle: "确认退出组织?",
71+
leaveOrgContent: "离开后,你将无法再访问该组织的资源和协作内容。如果未来需要回到组织,请联系管理员重新添加。",
72+
leaveOrgButton: "退出组织",
73+
cancel: "取消",
74+
leaveOrgSuccess: "你已成功退出该组织。",
75+
leaveOrgFailure: "退出组织失败,请稍后重试。",
76+
leaveOrgNetworkError: "网络异常,退出失败,请稍后重试。"
6977
}

frontend/src/stores/UserStore.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ const useUserStore = defineStore('User', () => {
6363
initialized.value = initializedData
6464
}
6565

66+
async function fetchOrgs() {
67+
if (!username.value) return
68+
const { data } = await useFetchApi(`/user/${username.value}`).json()
69+
if (data.value) {
70+
orgs.value = data.value.data?.orgs ? data.value.data?.orgs : []
71+
}
72+
}
73+
6674
async function fetchUserInfo() {
6775
if (!uuid.value) return
6876
const { data, error } = await useFetchApi(`/user/${uuid.value}?type=uuid`).json()
@@ -99,6 +107,7 @@ const useUserStore = defineStore('User', () => {
99107
clearStore,
100108
updateInitalized,
101109
fetchUserInfo,
110+
fetchOrgs,
102111
timestamp
103112
}
104113
}, {

0 commit comments

Comments
 (0)