mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
6.0 KiB
218 lines
6.0 KiB
<script lang="ts" setup> |
|
import { ProjectRoles, ViewLockType } from 'nocodb-sdk' |
|
import UserItem from './UserItem.vue' |
|
const props = defineProps<Props>() |
|
|
|
const emits = defineEmits<Emits>() |
|
|
|
const { loadUsers, users } = useManageUsers() |
|
|
|
interface Props { |
|
modelValue: boolean |
|
view?: Record<string, any> |
|
} |
|
|
|
interface Emits { |
|
(event: 'update:modelValue', data: boolean): void |
|
} |
|
|
|
const vModel = useVModel(props, 'modelValue', emits) |
|
|
|
const { t } = useI18n() |
|
|
|
const useForm = Form.useForm |
|
const validators = computed(() => { |
|
return { |
|
emails: [ |
|
{ |
|
validator: (rule: any, value: string, callback: (errMsg?: string) => void) => { |
|
if (!value || value.length === 0) { |
|
callback(t('msg.error.signUpRules.emailRequired')) |
|
return |
|
} |
|
const invalidEmails = (value || '').split(/\s*,\s*/).filter((e: string) => !validateEmail(e)) |
|
if (invalidEmails.length > 0) { |
|
callback( |
|
`${ |
|
invalidEmails.length > 1 ? t('msg.error.signUpRules.invalidEmails') : t('msg.error.signUpRules.invalidEmail') |
|
} ${invalidEmails.join(', ')} `, |
|
) |
|
} else { |
|
callback() |
|
} |
|
}, |
|
}, |
|
], |
|
} |
|
}) |
|
|
|
const invitationUsersData = ref({}) |
|
|
|
const { validateInfos } = useForm(invitationUsersData, validators) |
|
|
|
onMounted(async () => { |
|
if (!users.value) { |
|
await loadUsers() |
|
} |
|
}) |
|
|
|
watch( |
|
() => validateInfos.emails.validateStatus, |
|
() => { |
|
invitationValid.value = validateInfos.emails.validateStatus === 'success' && invitationUsersData.emails?.length !== 0 |
|
}, |
|
) |
|
|
|
const basesStore = useBases() |
|
const viewsStore = useViewsStore() |
|
|
|
const { basesUser } = storeToRefs(basesStore) |
|
|
|
const searchQuery = ref('') |
|
const selectedUser = ref() |
|
const userSelectMenu = ref(false) |
|
|
|
const currentOwner = computed(() => { |
|
return ( |
|
(props.view && basesUser.value.get(props.view.base_id)?.find((u) => u.id === props.view.owned_by)) || { |
|
id: props.view.owned_by, |
|
display_name: 'Unknown User', |
|
} |
|
) |
|
}) |
|
|
|
const filterdBaseUsers = computed(() => { |
|
let users = props.view.base_id ? basesUser.value.get(props.view.base_id) || [] : [] |
|
if (searchQuery.value) { |
|
const keyword = searchQuery.value.toLowerCase() |
|
users = users.filter((u) => { |
|
return u.display_name?.toLowerCase().includes(keyword) || u.email.toLowerCase().includes(keyword) |
|
}) |
|
} |
|
|
|
// exclude current owner from the list |
|
return users.filter((u) => u.id !== currentOwner.value?.id && u.base_roles !== ProjectRoles.NO_ACCESS) |
|
}) |
|
|
|
const { api, isLoading } = useApi() |
|
|
|
const assignView = async () => { |
|
try { |
|
if (!selectedUser.value) return |
|
await api.dbView.update(props.view.id, { |
|
owned_by: selectedUser.value.id, |
|
}) |
|
vModel.value = false |
|
message.success('View reassigned successfully') |
|
|
|
// if personal view then redirect to default view and reload view list |
|
if (props.view.lock_type === ViewLockType.Personal) { |
|
// then reload the view list |
|
viewsStore |
|
.loadViews({ |
|
ignoreLoading: true, |
|
tableId: props.view.fk_model_id, |
|
force: true, |
|
}) |
|
.catch(() => { |
|
// ignore |
|
}) |
|
} |
|
} catch (e) { |
|
await message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
} |
|
|
|
const selectUser = (user) => { |
|
selectedUser.value = user |
|
userSelectMenu.value = false |
|
} |
|
|
|
const inputEl = (el: HTMLInputElement) => { |
|
setTimeout(() => el?.focus(), 100) |
|
} |
|
</script> |
|
|
|
<template> |
|
<NcModal v-model:visible="vModel" wrap-class-name="nc-modal-re-assign" width="448px"> |
|
<div class="mb-5"> |
|
<div class="flex text-base font-bold mb-2">Re-assign this view</div> |
|
<div class="flex">Once reassigned, current owner will no longer be able to edit the view configuration.</div> |
|
</div> |
|
|
|
<div class="mb-5"> |
|
<div class="mb-1">Current owner</div> |
|
<UserItem :user="currentOwner" class="bg-gray-100 rounded-lg" /> |
|
</div> |
|
<div class="mb-5"> |
|
<div class="mb-1">New owner</div> |
|
<div class="rounded-lg border-1"> |
|
<UserItem |
|
v-if="selectedUser && !userSelectMenu" |
|
:user="selectedUser" |
|
class="cursor-pointer" |
|
@click="userSelectMenu = true" |
|
> |
|
<template #append> |
|
<GeneralIcon icon="arrowDown" class="text-gray-500" /> |
|
</template> |
|
</UserItem> |
|
|
|
<div v-else class="flex flex-row items-center gap-x-2 h-12.5 p-2 nc-list-user-item"> |
|
<GeneralIcon icon="search" class="text-gray-500 ml-2" /> |
|
<input |
|
:ref="inputEl" |
|
v-model="searchQuery" |
|
placeholder="Search User to assign..." |
|
class="border-0 px-2.5 outline-none nc-search-input" |
|
/> |
|
</div> |
|
|
|
<div v-if="!selectedUser || userSelectMenu" class="max-h-65 overflow-auto"> |
|
<UserItem |
|
v-for="user of filterdBaseUsers" |
|
:key="user.id" |
|
class="cursor-pointer hover:(bg-gray-100) nc-list-user-item" |
|
:class="{ 'bg-gray-100': selectedUser === user }" |
|
:user="user" |
|
@click="selectUser(user)" |
|
> |
|
</UserItem> |
|
</div> |
|
|
|
<div v-if="!filterdBaseUsers?.length" class="h-12.5 p-2 text-gray-400 text-sm flex items-center justify-center"> |
|
No base users found |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="flex justify-end"> |
|
<div class="flex gap-2"> |
|
<NcButton size="small" type="secondary" @click="vModel = false"> {{ $t('labels.cancel') }} </NcButton> |
|
<NcButton |
|
size="small" |
|
type="primary" |
|
class="nc-invite-btn" |
|
:disabled="!selectedUser" |
|
:loading="isLoading" |
|
@click="assignView" |
|
> |
|
{{ $t('activity.assignView') }} |
|
</NcButton> |
|
</div> |
|
</div> |
|
</NcModal> |
|
</template> |
|
|
|
<style scoped lang="scss"> |
|
.nc-modal-re-assign { |
|
.nc-search-input::placeholder { |
|
@apply text-gray-400; |
|
} |
|
|
|
.nc-list-user-item:not(:last-of-type) { |
|
border-bottom: 1px solid; |
|
border-color: inherit; |
|
} |
|
} |
|
</style>
|
|
|