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