Browse Source

feat: personal view

pull/9807/head
Pranav C 2 weeks ago
parent
commit
b6e9ec649f
  1. 10
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  2. 3
      packages/nc-gui/lang/en.json
  3. 8
      packages/nc-gui/lib/enums.ts
  4. 6
      packages/nocodb-sdk/src/lib/enums.ts
  5. 6
      packages/nocodb/src/controllers/meta-diffs.controller.ts
  6. 17
      packages/nocodb/src/helpers/populateMeta.ts
  7. 4
      packages/nocodb/src/meta/migrations/v2/nc_066_personal_view.ts
  8. 5
      packages/nocodb/src/models/Model.ts
  9. 10
      packages/nocodb/src/models/View.ts
  10. 2
      packages/nocodb/src/services/bases.service.ts
  11. 2
      packages/nocodb/src/services/calendars.service.ts
  12. 2
      packages/nocodb/src/services/columns.service.ts
  13. 2
      packages/nocodb/src/services/forms.service.ts
  14. 2
      packages/nocodb/src/services/galleries.service.ts
  15. 2
      packages/nocodb/src/services/grids.service.ts
  16. 2
      packages/nocodb/src/services/kanbans.service.ts
  17. 2
      packages/nocodb/src/services/maps.service.ts
  18. 26
      packages/nocodb/src/services/meta-diffs.service.ts
  19. 2
      packages/nocodb/src/services/sources.service.ts
  20. 12
      packages/nocodb/src/services/tables.service.ts
  21. 65
      packages/nocodb/src/services/views.service.ts

10
packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue

@ -70,9 +70,9 @@ async function changeLockType(type: LockType) {
if (!view.value) return
if (type === 'personal') {
// Coming soon
return message.info(t('msg.toast.futureRelease'))
// if default view block the change since it's not allowed
if (type === 'personal' && view.value.is_default) {
return message.info(t('msg.toast.notAllowedToChangeDefaultView'))
}
try {
view.value.lock_type = type
@ -306,6 +306,10 @@ const onDelete = async () => {
<LazySmartsheetToolbarLockType :type="LockType.Collaborative" @click="changeLockType(LockType.Collaborative)" />
</a-menu-item>
<a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction max-w-[100px]">
<LazySmartsheetToolbarLockType :type="LockType.Personal" @click="changeLockType(LockType.Personal)" />
</a-menu-item>
<a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction">
<LazySmartsheetToolbarLockType :type="LockType.Locked" @click="changeLockType(LockType.Locked)" />
</a-menu-item>

3
packages/nc-gui/lang/en.json

@ -1806,7 +1806,8 @@
"formEmailSMTP": "Please activate SMTP plugin in App store for enabling email notification",
"collabView": "Successfully Switched to collaborative view",
"lockedView": "Successfully Switched to locked view",
"futureRelease": "Coming soon!"
"futureRelease": "Coming soon!",
"notAllowedToChangeDefaultView": "You are not allowed to change the default view"
},
"success": {
"licenseKeyUpdated": "License Key Updated",

8
packages/nc-gui/lib/enums.ts

@ -1,3 +1,5 @@
import { ViewLockType } from 'nocodb-sdk'
export { ClientType, IntegrationCategoryType, SyncDataType } from 'nocodb-sdk'
export enum Language {
@ -50,11 +52,7 @@ export enum NavigateDir {
PREV,
}
export enum LockType {
Personal = 'personal',
Locked = 'locked',
Collaborative = 'collaborative',
}
export { ViewLockType as LockType }
export enum TabType {
TABLE = 'table',

6
packages/nocodb-sdk/src/lib/enums.ts

@ -434,3 +434,9 @@ export enum IntegrationCategoryType {
STORAGE = 'storage',
OTHERS = 'others',
}
export enum ViewLockType {
Personal = 'personal',
Locked = 'locked',
Collaborative = 'collaborative',
}

6
packages/nocodb/src/controllers/meta-diffs.controller.ts

@ -1,10 +1,10 @@
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common';
import { GlobalGuard } from '~/guards/global/global.guard';
import { MetaDiffsService } from '~/services/meta-diffs.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
import { TenantContext } from '~/decorators/tenant-context.decorator';
import { NcContext } from '~/interface/config';
import { NcContext, NcRequest } from '~/interface/config';
@Controller()
@UseGuards(MetaApiLimiterGuard, GlobalGuard)
@ -32,10 +32,12 @@ export class MetaDiffsController {
@TenantContext() context: NcContext,
@Param('baseId') baseId: string,
@Param('sourceId') sourceId: string,
@Req() req: NcRequest,
) {
return await this.metaDiffsService.baseMetaDiff(context, {
sourceId,
baseId,
user: req.user,
});
}
}

17
packages/nocodb/src/helpers/populateMeta.ts

@ -3,6 +3,7 @@ import { isVirtualCol, RelationTypes } from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection';
import { isLinksOrLTAR } from 'nocodb-sdk';
import { getUniqueColumnAliasName, getUniqueColumnName } from './getUniqueName';
import type { UserType } from 'nocodb-sdk';
import type { RollupColumn } from '~/models';
import type LinkToAnotherRecordColumn from '~/models/LinkToAnotherRecordColumn';
import type Source from '~/models/Source';
@ -205,9 +206,17 @@ export async function extractAndGenerateManyToManyRelations(
export async function populateMeta(
context: NcContext,
source: Source,
base: Base,
logger?: (message: string) => void,
{
source,
base,
logger,
user,
}: {
source: Source;
base: Base;
logger?: (message: string) => void;
user: UserType;
},
): Promise<any> {
const info = {
type: 'rest',
@ -347,6 +356,7 @@ export async function populateMeta(
title: table.title,
type: table.type || 'table',
order: table.order,
user_id: user.id,
},
);
@ -489,6 +499,7 @@ export async function populateMeta(
// todo: sanitize
type: ModelTypes.VIEW,
order: table.order,
user_id: user.id,
},
);

4
packages/nocodb/src/meta/migrations/v2/nc_066_personal_view.ts

@ -3,15 +3,15 @@ import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.VIEWS, (table) => {
table.boolean('is_personal').defaultTo(false);
table.string('created_by', 20).index();
table.string('owned_by', 20).index();
});
};
const down = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.SOURCES, (table) => {
table.dropColumn('is_personal');
table.dropColumn('created_by');
table.dropColumn('owned_by');
});
};

5
packages/nocodb/src/models/Model.ts

@ -140,6 +140,7 @@ export default class Model implements TableType {
mm?: BoolType;
type?: ModelTypes;
source_id?: string;
user_id: string;
},
ncMeta = Noco.ncMeta,
) {
@ -199,6 +200,8 @@ export default class Model implements TableType {
type: ViewTypes.GRID,
base_id: baseId,
source_id: sourceId,
created_by: model.user_id,
owned_by: model.user_id,
},
{
getColumns: async () => insertedColumns,
@ -1154,7 +1157,7 @@ export default class Model implements TableType {
context: NcContext,
{
modelId,
userId
userId,
}: {
modelId: string;
userId?: string;

10
packages/nocodb/src/models/View.ts

@ -74,7 +74,7 @@ export default class View implements ViewType {
type: ViewTypes;
lock_type?: ViewType['lock_type'];
created_by?: string;
is_personal?: boolean;
owned_by?: string;
fk_model_id: string;
model?: Model;
@ -1263,7 +1263,10 @@ export default class View implements ViewType {
password?: string;
uuid?: string;
meta?: any;
owned_by?: string;
created_by?: string;
},
includeCreatedByAndUpdateBy = false,
ncMeta = Noco.ncMeta,
) {
const updateObj = extractProps(body, [
@ -1275,6 +1278,7 @@ export default class View implements ViewType {
'password',
'meta',
'uuid',
...(includeCreatedByAndUpdateBy ? ['owned_by', 'created_by'] : [])
]);
const oldView = await this.get(context, viewId, ncMeta);
@ -1986,6 +1990,8 @@ export default class View implements ViewType {
copy_from_id?: string;
fk_grp_col_id?: string;
calendar_range?: Partial<CalendarRange>[];
created_by: string;
owned_by: string;
},
model: {
getColumns: (context: NcContext, ncMeta?) => Promise<Column[]>;
@ -2002,6 +2008,8 @@ export default class View implements ViewType {
'base_id',
'source_id',
'meta',
'created_by',
'owned_by',
]);
if (!insertObj.order) {

2
packages/nocodb/src/services/bases.service.ts

@ -282,7 +282,7 @@ export class BasesService {
// populate metadata if existing table
for (const source of await base.getSources()) {
if (process.env.NC_CLOUD !== 'true' && !base.is_meta) {
const info = await populateMeta(context, source, base);
const info = await populateMeta(context, {source, base, user: param.user});
this.appHooksService.emit(AppEvents.APIS_CREATED, {
info,

2
packages/nocodb/src/services/calendars.service.ts

@ -45,6 +45,8 @@ export class CalendarsService {
type: ViewTypes.CALENDAR,
base_id: model.base_id,
source_id: model.source_id,
created_by: param.user.id,
owned_by: param.user.id,
},
model,
);

2
packages/nocodb/src/services/columns.service.ts

@ -3067,6 +3067,7 @@ export class ColumnsService {
base: Base;
reuse?: ReusableParams;
colExtra?: any;
user: UserType;
},
) {
validateParams(['parentId', 'childId', 'type'], param.column);
@ -3369,6 +3370,7 @@ export class ColumnsService {
// todo: sanitize
mm: true,
columns: associateTableCols,
user_id: param.user.id,
},
);

2
packages/nocodb/src/services/forms.service.ts

@ -53,6 +53,8 @@ export class FormsService {
type: ViewTypes.FORM,
base_id: model.base_id,
source_id: model.source_id,
created_by: param.user.id,
owned_by: param.user.id,
},
model,
);

2
packages/nocodb/src/services/galleries.service.ts

@ -47,6 +47,8 @@ export class GalleriesService {
type: ViewTypes.GALLERY,
base_id: model.base_id,
source_id: model.source_id,
created_by: param.user.id,
owned_by: param.user.id,
},
model,
);

2
packages/nocodb/src/services/grids.service.ts

@ -37,6 +37,8 @@ export class GridsService {
type: ViewTypes.GRID,
base_id: model.base_id,
source_id: model.source_id,
created_by: param.req?.user.id,
owned_by: param.req?.user.id,
},
model,
);

2
packages/nocodb/src/services/kanbans.service.ts

@ -46,6 +46,8 @@ export class KanbansService {
type: ViewTypes.KANBAN,
base_id: model.base_id,
source_id: model.source_id,
owned_by: param.user.id,
created_by: param.user.id,
},
model,
);

2
packages/nocodb/src/services/maps.service.ts

@ -42,6 +42,8 @@ export class MapsService {
type: ViewTypes.MAP,
base_id: model.base_id,
source_id: model.source_id,
created_by: param.user.id,
owned_by: param.user.id,
},
model,
);

26
packages/nocodb/src/services/meta-diffs.service.ts

@ -8,6 +8,7 @@ import {
UITypes,
} from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection';
import type { UserType } from 'nocodb-sdk';
import type { LinksColumn, LinkToAnotherRecordColumn } from '~/models';
import type { NcContext } from '~/interface/config';
import { AppHooksService } from '~/services/app-hooks/app-hooks.service';
@ -647,7 +648,7 @@ export class MetaDiffsService {
async baseMetaDiff(
context: NcContext,
param: { baseId: string; sourceId: string },
param: { baseId: string; sourceId: string; user: UserType },
) {
const base = await Base.getWithInfo(context, param.baseId);
const source = await Source.get(context, param.sourceId);
@ -662,9 +663,17 @@ export class MetaDiffsService {
async syncBaseMeta(
context: NcContext,
base: Base,
source: Source,
{
base,
source,
throwOnFail = false,
user,
}: {
base: Base;
source: Source;
throwOnFail?: boolean;
user: UserType;
},
) {
if (source.is_meta) {
if (throwOnFail) NcError.badRequest('Cannot sync meta source');
@ -711,6 +720,7 @@ export class MetaDiffsService {
source,
),
type: ModelTypes.TABLE,
user_id: user.id,
});
for (const column of columns) {
@ -738,6 +748,7 @@ export class MetaDiffsService {
table_name: table_name,
title: getTableNameAlias(table_name, base.prefix, source),
type: ModelTypes.VIEW,
user_id: user.id,
});
for (const column of columns) {
@ -909,7 +920,7 @@ export class MetaDiffsService {
async metaDiffSync(context: NcContext, param: { baseId: string; req: any }) {
const base = await Base.getWithInfo(context, param.baseId);
for (const source of base.sources) {
await this.syncBaseMeta(context, base, source);
await this.syncBaseMeta(context, { base, source, user: param.req.user });
}
this.appHooksService.emit(AppEvents.META_DIFF_SYNC, {
@ -931,7 +942,12 @@ export class MetaDiffsService {
const base = await Base.getWithInfo(context, param.baseId);
const source = await Source.get(context, param.sourceId);
await this.syncBaseMeta(context, base, source, true);
await this.syncBaseMeta(context, {
base,
source,
throwOnFail: true,
user: param.req.user,
});
this.appHooksService.emit(AppEvents.META_DIFF_SYNC, {
base,

2
packages/nocodb/src/services/sources.service.ts

@ -173,7 +173,7 @@ export class SourcesService {
param.logger?.('Populating meta');
const info = await populateMeta(context, source, base, param.logger);
const info = await populateMeta(context, {source, base, logger:param.logger, user: param.req.user});
await populateRollupColumnAndHideLTAR(context, source, base);

12
packages/nocodb/src/services/tables.service.ts

@ -10,7 +10,9 @@ import {
ProjectRoles,
RelationTypes,
UITypes,
ViewLockType,
} from 'nocodb-sdk';
import { LockType } from 'nc-gui/lib/enums';
import { MetaDiffsService } from './meta-diffs.service';
import { ColumnsService } from './columns.service';
import type {
@ -353,9 +355,13 @@ export class TablesService {
);
//await View.list(param.tableId)
table.views = viewList.filter((table: any) => {
return Object.keys(param.user?.roles).some(
(role) => param.user?.roles[role] && !table.disabled[role],
table.views = viewList.filter((view: any) => {
return (
Object.keys(param.user?.roles).some(
(role) => param.user?.roles[role] && !view.disabled[role],
) &&
(view.lock_type !== ViewLockType.Locked ||
view.fk_owned_by === param.user.id)
);
});

65
packages/nocodb/src/services/views.service.ts

@ -10,6 +10,7 @@ import { AppHooksService } from '~/services/app-hooks/app-hooks.service';
import { validatePayload } from '~/helpers';
import { NcError } from '~/helpers/catchError';
import { Model, ModelRoleVisibility, View } from '~/models';
import {WorkspaceUser} from "~/ee/models";
// todo: move
async function xcVisibilityMetaGet(
@ -136,18 +137,72 @@ export class ViewsService {
'swagger.json#/components/schemas/ViewUpdateReq',
param.view,
);
const oldView = await View.get(context, param.viewId);
const view = await View.get(context, param.viewId);
if (!view) {
if (!oldView) {
NcError.viewNotFound(param.viewId);
}
const result = await View.update(context, param.viewId, param.view);
let ownedBy = oldView.owned_by;
let createdBy = oldView.created_by;
let includeCreatedByAndUpdateBy = false;
// check if the lock_type changing to `personal` and only allow if user is the owner
// if the owned_by is not the same as the user, then throw error
// if owned_by is empty, then only allow owner of project to change
if (
param.view.lock_type === 'personal' &&
param.view.lock_type !== oldView.lock_type
) {
// if owned_by is not empty then check if the user is the owner of the project
if (ownedBy && ownedBy !== param.user.id) {
NcError.unauthorized('Only owner can change to personal view');
}
// if empty then check if current user is the owner of the project then allow and update the owned_by
if (!ownedBy && (param.user as any).base_roles?.[ProjectRoles.OWNER]) {
includeCreatedByAndUpdateBy = true;
ownedBy = param.user.id;
if (!createdBy) {
createdBy = param.user.id;
}
} else if (!ownedBy) {
// todo: move to catchError
NcError.unauthorized('Only owner can change to personal view');
}
}
if(ownedBy && param.view.owned_by && param.user.id === ownedBy) {
ownedBy = param.view.owned_by
// verify if the new owned_by is a valid user who have access to the base/workspace
// if not then throw error
const baseUser = await BaseUser.get(context,param.view.owned_by, context.base_id);
if(!baseUser){
NcError.badRequest('Invalid user');
}
// // todo: ee only
// if(!baseUser) {
// const workspace = await WorkspaceUser
// }
}
const result = await View.update(
context,
param.viewId,
{
...param.view,
owned_by: ownedBy,
created_by: createdBy,
},
includeCreatedByAndUpdateBy,
);
this.appHooksService.emit(AppEvents.VIEW_UPDATE, {
view: {
...view,
...oldView,
...param.view,
},
user: param.user,

Loading…
Cancel
Save