|
|
|
<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.roles !== ProjectRoles.NO_ACCESS && u.roles !== ProjectRoles.VIEWER,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
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>
|