mirror of https://github.com/nocodb/nocodb
Pranav C
2 weeks ago
committed by
GitHub
45 changed files with 661 additions and 72 deletions
After Width: | Height: | Size: 714 B |
@ -0,0 +1,29 @@
|
||||
<script setup lang="ts"> |
||||
import type { UserType } from 'nocodb-sdk' |
||||
|
||||
const { user } = defineProps<{ |
||||
user: UserType |
||||
}>() |
||||
|
||||
const displayName = computed(() => { |
||||
return user?.display_name?.trim() ? user?.display_name?.trim() : user?.email?.split('@')[0] |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex flex-row items-center gap-x-2 h-12.5 p-2"> |
||||
<GeneralUserIcon |
||||
size="auto" |
||||
:name="user.display_name?.trim() ? user.display_name?.trim() : ''" |
||||
:email="user.email" |
||||
class="!text-[0.65rem]" |
||||
/> |
||||
<div class="flex flex-col justify-center flex-grow"> |
||||
<div class="flex flex-col"> |
||||
<span class="capitalize font-weight-medium">{{ displayName }}</span> |
||||
<span class="text-xs">{{ user.email }}</span> |
||||
</div> |
||||
</div> |
||||
<slot name="append"></slot> |
||||
</div> |
||||
</template> |
@ -0,0 +1,186 @@
|
||||
<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) |
||||
|
||||
onMounted(async () => { |
||||
if (!users.value) { |
||||
await loadUsers() |
||||
} |
||||
}) |
||||
|
||||
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" |
||||
:class="{ |
||||
'shadow-sm': selectedUser && !userSelectMenu, |
||||
}" |
||||
> |
||||
<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> |
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts"> |
||||
import type { TooltipPlacement } from 'ant-design-vue/es/tooltip' |
||||
import type { CSSProperties } from '@vue/runtime-dom' |
||||
|
||||
defineProps<{ |
||||
tooltipStyle?: CSSProperties |
||||
overlayInnerStyle?: CSSProperties |
||||
mouseLeaveDelay?: number |
||||
placement?: TooltipPlacement |
||||
trigger?: 'hover' | 'click' |
||||
message?: string |
||||
enabled?: boolean |
||||
}>() |
||||
</script> |
||||
|
||||
<template> |
||||
<NcTooltip |
||||
:disabled="!enabled" |
||||
:tooltip-style="{ 'min-width': 'max-content' }" |
||||
:overlay-inner-style="{ 'min-width': 'max-content' }" |
||||
:mouse-leave-delay="0.3" |
||||
placement="left" |
||||
trigger="hover" |
||||
> |
||||
<template #title> |
||||
{{ message }} |
||||
</template> |
||||
<slot /> |
||||
</NcTooltip> |
||||
</template> |
@ -0,0 +1,20 @@
|
||||
import type { Knex } from 'knex'; |
||||
import { MetaTable } from '~/utils/globals'; |
||||
|
||||
const up = async (knex: Knex) => { |
||||
await knex.schema.alterTable(MetaTable.COL_BUTTON, (table) => { |
||||
table.string('fk_integration_id', 20); |
||||
table.string('model', 255); |
||||
table.text('output_column_ids'); |
||||
}); |
||||
}; |
||||
|
||||
const down = async (knex: Knex) => { |
||||
await knex.schema.alterTable(MetaTable.COL_BUTTON, (table) => { |
||||
table.dropColumn('fk_integration_id'); |
||||
table.dropColumn('model'); |
||||
table.dropColumn('output_column_ids'); |
||||
}); |
||||
}; |
||||
|
||||
export { up, down }; |
@ -0,0 +1,18 @@
|
||||
import type { Knex } from 'knex'; |
||||
import { MetaTable } from '~/utils/globals'; |
||||
|
||||
const up = async (knex: Knex) => { |
||||
await knex.schema.alterTable(MetaTable.VIEWS, (table) => { |
||||
table.string('created_by', 20).index(); |
||||
table.string('owned_by', 20).index(); |
||||
}); |
||||
}; |
||||
|
||||
const down = async (knex: Knex) => { |
||||
await knex.schema.alterTable(MetaTable.VIEWS, (table) => { |
||||
table.dropColumn('created_by'); |
||||
table.dropColumn('owned_by'); |
||||
}); |
||||
}; |
||||
|
||||
export { up, down }; |
Loading…
Reference in new issue