Browse Source

Merge branch 'develop' into refactor/webhooks

pull/5349/head
Wing-Kam Wong 2 years ago
parent
commit
4607875bdd
  1. 1
      packages/nc-gui/components.d.ts
  2. 195
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  3. 7
      packages/nc-gui/composables/useExpandedFormStore.ts
  4. 1
      packages/nc-gui/lang/en.json
  5. 34
      packages/nocodb-sdk/src/lib/Api.ts
  6. 15
      packages/nocodb/src/lib/controllers/audit.ctl.ts
  7. 16
      packages/nocodb/src/lib/models/Audit.ts
  8. 21
      packages/nocodb/src/lib/services/audit.svc.ts
  9. 269
      packages/nocodb/src/schema/swagger.json
  10. 3
      tests/playwright/pages/ProjectsPage/index.ts
  11. 21
      tests/playwright/tests/projectOperations.spec.ts

1
packages/nc-gui/components.d.ts vendored

@ -137,6 +137,7 @@ declare module '@vue/runtime-core' {
MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default'] MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default'] MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default'] MdiChat: typeof import('~icons/mdi/chat')['default']
MdiChatProcessingOutline: typeof import('~icons/mdi/chat-processing-outline')['default']
MdiCheck: typeof import('~icons/mdi/check')['default'] MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default'] MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']

195
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -1,14 +1,108 @@
<script setup lang="ts"> <script setup lang="ts">
import { enumColor, ref, timeAgo, useExpandedFormStoreOrThrow, watch } from '#imports' import type { VNodeRef } from '@vue/runtime-core'
import type { AuditType } from 'nocodb-sdk'
import { enumColor, ref, timeAgo, useCopy, useExpandedFormStoreOrThrow, useGlobal, useI18n, watch } from '#imports'
const { loadCommentsAndLogs, commentsAndLogs, isCommentsLoading, commentsOnly, saveComment, isYou, comment } = const { loadCommentsAndLogs, commentsAndLogs, isCommentsLoading, commentsOnly, saveComment, isYou, comment, updateComment } =
useExpandedFormStoreOrThrow() useExpandedFormStoreOrThrow()
const commentsWrapperEl = ref<HTMLDivElement>() const commentsWrapperEl = ref<HTMLDivElement>()
await loadCommentsAndLogs() await loadCommentsAndLogs()
const showborder = ref(false) const showBorder = ref(false)
const { copy } = useCopy()
const { t } = useI18n()
const { user } = useGlobal()
const { isUIAllowed } = useUIPermission()
const hasEditPermission = $computed(() => isUIAllowed('commentEditable'))
// currently, edit option is disable on purpose
// since the current update wouldn't keep track of the previous values
// need history of edit feature in order to enable it back
const disableEditOption = ref(true)
let editLog = $ref<AuditType>()
let isEditing = $ref<boolean>(false)
const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
onKeyEsc(event)
} else if (event.key === 'Enter') {
onKeyEnter(event)
}
}
function onKeyEnter(event: KeyboardEvent) {
event.stopImmediatePropagation()
event.preventDefault()
onEditComment()
}
function onKeyEsc(event: KeyboardEvent) {
event.stopImmediatePropagation()
event.preventDefault()
onCancel()
}
async function onEditComment() {
if (!isEditing || !editLog) return
await updateComment(editLog.id!, {
description: editLog.description,
})
onStopEdit()
}
function onCancel() {
if (!isEditing) return
editLog = undefined
onStopEdit()
}
function onStopEdit() {
isEditing = false
editLog = undefined
}
onKeyStroke('Enter', (event) => {
if (isEditing) {
onKeyEnter(event)
}
})
const _contextMenu = ref(false)
const contextMenu = computed({
get: () => _contextMenu.value,
set: (val) => {
if (hasEditPermission) {
_contextMenu.value = val
}
},
})
async function copyComment(val: string) {
if (!val) return
try {
await copy(val)
message.success(t('msg.success.commentCopied'))
} catch (e: any) {
message.error(e.message)
}
}
function editComment(log: AuditType) {
editLog = log
isEditing = true
}
watch( watch(
commentsAndLogs, commentsAndLogs,
@ -25,32 +119,68 @@ watch(
<template> <template>
<div class="h-full flex flex-col w-full bg-[#eceff1] p-2"> <div class="h-full flex flex-col w-full bg-[#eceff1] p-2">
<div ref="commentsWrapperEl" class="flex-1 min-h-[100px] overflow-y-auto scrollbar-thin-dull p-2 space-y-2"> <div ref="commentsWrapperEl" class="flex-1 min-h-[100px] overflow-y-auto scrollbar-thin-dull p-2 space-y-2">
<a-skeleton v-if="isCommentsLoading && !commentsAndLogs" type="list-item-avatar-two-line@8" /> <a-skeleton v-if="isCommentsLoading" type="list-item-avatar-two-line@8" />
<template v-else-if="commentsAndLogs.length === 0">
<template v-else> <div class="flex flex-col text-center justify-center h-full">
<div v-for="log of commentsAndLogs" :key="log.id" class="flex gap-1 text-xs"> <div class="text-center text-3xl">
<MdiAccountCircle class="row-span-2" :class="isYou(log.user) ? 'text-pink-300' : 'text-blue-300 '" /> <MdiChatProcessingOutline />
<div class="flex-1">
<p class="mb-1 caption edited-text text-[10px] text-gray-500">
{{ isYou(log.user) ? 'You' : log.user == null ? 'Shared base' : log.user }}
{{ log.op_type === 'COMMENT' ? 'commented' : log.op_sub_type === 'INSERT' ? 'created' : 'edited' }}
</p>
<p
v-if="log.op_type === 'COMMENT'"
class="block caption my-2 nc-chip w-full min-h-20px p-2 rounded"
:style="{ backgroundColor: enumColor.light[2] }"
>
{{ log.description }}
</p>
<p v-else v-dompurify-html="log.details" class="caption my-3" style="word-break: break-all" />
<p class="time text-right text-[10px] mb-0 mt-1 text-gray-500">
{{ timeAgo(log.created_at) }}
</p>
</div> </div>
<div class="font-bold text-center my-1">Start a conversation</div>
<div>NocoDB allows you to inquire, monitor progress updates, and collaborate with your team members.</div>
</div>
</template>
<template v-else>
<div v-for="(log, idx) of commentsAndLogs" :key="log.id">
<a-dropdown :trigger="['contextmenu']" :overlay-class-name="`nc-dropdown-comment-context-menu-${idx}`">
<div class="flex gap-1 text-xs">
<MdiAccountCircle class="row-span-2" :class="isYou(log.user) ? 'text-pink-300' : 'text-blue-300 '" />
<div class="flex-1">
<p class="mb-1 caption edited-text text-[10px] text-gray-500">
{{ isYou(log.user) ? 'You' : log.user == null ? 'Shared base' : log.user }}
{{ log.op_type === 'COMMENT' ? 'commented' : log.op_sub_type === 'INSERT' ? 'created' : 'edited' }}
</p>
<div v-if="log.op_type === 'COMMENT'">
<a-input
v-if="log.id === editLog?.id"
:ref="focusInput"
v-model:value="editLog.description"
@blur="onCancel"
@keydown.stop="onKeyDown($event)"
/>
<p
v-else
class="block caption my-2 nc-chip w-full min-h-20px p-2 rounded"
:style="{ backgroundColor: enumColor.light[2] }"
>
{{ log.description }}
</p>
</div>
<p v-else v-dompurify-html="log.details" class="caption my-3" style="word-break: break-all" />
<p class="time text-right text-[10px] mb-0 mt-1 text-gray-500">
{{ timeAgo(log.created_at) }}
</p>
</div>
</div>
<template #overlay>
<a-menu v-if="log.op_type === 'COMMENT'" @click="contextMenu = false">
<a-menu-item key="copy-comment" @click="copyComment(log.description)">
<div v-e="['a:comment:copy']" class="nc-project-menu-item">
{{ t('general.copy') }}
</div>
</a-menu-item>
<a-menu-item v-if="log.user === user.email && !disableEditOption" key="edit-comment" @click="editComment(log)">
<div v-e="['a:comment:edit']" class="nc-project-menu-item">
{{ t('general.edit') }}
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div> </div>
</template> </template>
</div> </div>
@ -59,10 +189,9 @@ watch(
<div class="p-0"> <div class="p-0">
<div class="flex justify-center"> <div class="flex justify-center">
<!-- Comments only --> <!-- Comments only -->
<a-checkbox v-model:checked="commentsOnly" v-e="['c:row-expand:comment-only']" @change="loadCommentsAndLogs"> <a-checkbox v-model:checked="commentsOnly" v-e="['c:row-expand:comment-only']" @change="loadCommentsAndLogs">
{{ $t('labels.commentsOnly') }} {{ $t('labels.commentsOnly') }}
<span class="text-[11px] text-gray-500" /> <span class="text-[11px] text-gray-500" />
</a-checkbox> </a-checkbox>
</div> </div>
@ -72,9 +201,9 @@ watch(
v-model:value="comment" v-model:value="comment"
class="!text-xs nc-comment-box" class="!text-xs nc-comment-box"
ghost ghost
:class="{ focus: showborder }" :class="{ focus: showBorder }"
@focusin="showborder = true" @focusin="showBorder = true"
@focusout="showborder = false" @focusout="showBorder = false"
@keyup.enter.prevent="saveComment" @keyup.enter.prevent="saveComment"
> >
<template #addonBefore> <template #addonBefore>

7
packages/nc-gui/composables/useExpandedFormStore.ts

@ -1,5 +1,5 @@
import { UITypes, ViewTypes } from 'nocodb-sdk' import { UITypes, ViewTypes } from 'nocodb-sdk'
import type { ColumnType, TableType } from 'nocodb-sdk' import type { AuditType, ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { import {
@ -229,6 +229,10 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}) })
} }
const updateComment = async (auditId: string, audit: Partial<AuditType>) => {
return await $api.utils.commentUpdate(auditId, audit)
}
return { return {
...rowStore, ...rowStore,
commentsOnly, commentsOnly,
@ -247,6 +251,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
loadRow, loadRow,
primaryKey, primaryKey,
saveRowAndStay, saveRowAndStay,
updateComment,
} }
}, 'expanded-form-store') }, 'expanded-form-store')

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

@ -779,6 +779,7 @@
"userDeletedFromProject": "Successfully deleted user from project", "userDeletedFromProject": "Successfully deleted user from project",
"inviteEmailSent": "Invite Email sent successfully", "inviteEmailSent": "Invite Email sent successfully",
"inviteURLCopied": "Invite URL copied to clipboard", "inviteURLCopied": "Invite URL copied to clipboard",
"commentCopied": "Comment copied to clipboard",
"passwordResetURLCopied": "Password reset URL copied to clipboard", "passwordResetURLCopied": "Password reset URL copied to clipboard",
"shareableURLCopied": "Copied shareable base URL to clipboard!", "shareableURLCopied": "Copied shareable base URL to clipboard!",
"embeddableHTMLCodeCopied": "Copied embeddable HTML code!", "embeddableHTMLCodeCopied": "Copied embeddable HTML code!",

34
packages/nocodb-sdk/src/lib/Api.ts

@ -499,6 +499,17 @@ export interface CommentReqType {
row_id: string; row_id: string;
} }
/**
* Model for Comment Update Request
*/
export interface CommentUpdateReqType {
/**
* Description for the target row
* @example This is the comment for the row
*/
description?: string;
}
/** /**
* Model for Filter * Model for Filter
*/ */
@ -8105,6 +8116,29 @@ export class Api<
}), }),
/** /**
* @description Update comment in Audit
*
* @tags Utils
* @name CommentUpdate
* @summary Update Comment in Audit
* @request PATCH:/api/v1/db/meta/audits/{auditId}/comment
* @response `200` `number` OK
*/
commentUpdate: (
auditId: string,
data: CommentUpdateReqType,
params: RequestParams = {}
) =>
this.request<number, any>({
path: `/api/v1/db/meta/audits/${auditId}/comment`,
method: 'PATCH',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
/**
* @description Return the number of comments in the given query. * @description Return the number of comments in the given query.
* *
* @tags Utils * @tags Utils

15
packages/nocodb/src/lib/controllers/audit.ctl.ts

@ -30,6 +30,16 @@ export async function commentList(req: Request<any, any, any>, res) {
); );
} }
export async function commentUpdate(req, res) {
res.json(
await auditService.commentUpdate({
auditId: req.params.auditId,
userEmail: req?.session?.passport?.user.email,
body: req.body,
})
);
}
export async function auditList(req: Request, res: Response) { export async function auditList(req: Request, res: Response) {
res.json( res.json(
new PagedResponseImpl( new PagedResponseImpl(
@ -66,6 +76,11 @@ router.post(
ncMetaAclMw(commentRow, 'commentRow') ncMetaAclMw(commentRow, 'commentRow')
); );
router.patch(
'/api/v1/db/meta/audits/:auditId/comment',
ncMetaAclMw(commentUpdate, 'commentUpdate')
);
router.post( router.post(
'/api/v1/db/meta/audits/rows/:rowId/update', '/api/v1/db/meta/audits/rows/:rowId/update',
ncMetaAclMw(auditRowUpdate, 'auditRowUpdate') ncMetaAclMw(auditRowUpdate, 'auditRowUpdate')

16
packages/nocodb/src/lib/models/Audit.ts

@ -157,6 +157,7 @@ export default class Audit implements AuditType {
offset, offset,
}); });
} }
static async projectAuditCount(projectId: string): Promise<number> { static async projectAuditCount(projectId: string): Promise<number> {
return ( return (
await Noco.ncMeta await Noco.ncMeta
@ -172,4 +173,19 @@ export default class Audit implements AuditType {
fk_model_id, fk_model_id,
}); });
} }
static async commentUpdate(
auditId: string,
audit: Partial<AuditType>,
ncMeta = Noco.ncMeta
) {
const updateObj = extractProps(audit, ['description']);
return await ncMeta.metaUpdate(
null,
null,
MetaTable.AUDIT,
updateObj,
auditId
);
}
} }

21
packages/nocodb/src/lib/services/audit.svc.ts

@ -3,7 +3,8 @@ import DOMPurify from 'isomorphic-dompurify';
import { validatePayload } from '../meta/api/helpers'; import { validatePayload } from '../meta/api/helpers';
import Audit from '../models/Audit'; import Audit from '../models/Audit';
import Model from '../models/Model'; import Model from '../models/Model';
import type { AuditRowUpdateReqType } from 'nocodb-sdk'; import { NcError } from '../meta/helpers/catchError';
import type { AuditRowUpdateReqType, CommentUpdateReqType } from 'nocodb-sdk';
export async function commentRow(param: { export async function commentRow(param: {
rowId: string; rowId: string;
@ -62,3 +63,21 @@ export async function commentsCount(param: {
ids: param.ids as string[], ids: param.ids as string[],
}); });
} }
export async function commentUpdate(param: {
auditId: string;
userEmail: string;
body: CommentUpdateReqType;
}) {
validatePayload(
'swagger.json#/components/schemas/CommentUpdateReq',
param.body
);
const log = await Audit.get(param.auditId);
if (log.user !== param.userEmail) {
NcError.unauthorized('Unauthorized access');
}
return await Audit.commentUpdate(param.auditId, param.body);
}

269
packages/nocodb/src/schema/swagger.json

@ -11523,6 +11523,69 @@
] ]
} }
}, },
"/api/v1/db/meta/audits/{auditId}/comment": {
"parameters": [
{
"schema": {
"type": "string",
"example": "adt_zlskd6rlf3liay"
},
"name": "auditId",
"in": "path",
"required": true,
"description": "Audit ID"
},
{
"name": "xc-auth",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "Auth Token is a JWT Token generated based on the logged-in user. By default, the token is only valid for 10 hours. However, you can change the value by defining it using environment variable NC_JWT_EXPIRES_IN."
}
],
"patch": {
"summary": "Update Comment in Audit",
"operationId": "utils-comment-update",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "number",
"example": 1
},
"examples": {
"Example 1": {
"value": 1
}
}
}
}
}
},
"tags": ["Utils"],
"description": "Update comment in Audit",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommentUpdateReq"
},
"examples": {
"Example 1": {
"value": {
"description": "This is the comment for the row"
}
}
}
}
}
}
}
},
"/api/v1/db/meta/audits/comments/count": { "/api/v1/db/meta/audits/comments/count": {
"parameters": [ "parameters": [
{ {
@ -13583,7 +13646,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "q8h78qib3l9ko" "id": "zc9ztw0ih4ttt"
} }
}, },
"ApiTokenReq": { "ApiTokenReq": {
@ -13604,13 +13667,13 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "insn8kf1n4s6k" "id": "t9dpbaw4dhpx3"
} }
}, },
"ApiTokenList": { "ApiTokenList": {
"description": "Model for API Token List", "description": "Model for API Token List",
"x-stoplight": { "x-stoplight": {
"id": "r482re16lknrx" "id": "dgoo5jfkvypld"
}, },
"examples": [ "examples": [
{ {
@ -13715,7 +13778,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "g4rbip7xwyo6c" "id": "9uff8of377ouf"
} }
}, },
"AttachmentReq": { "AttachmentReq": {
@ -13753,7 +13816,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "13qyneoxa9jal" "id": "7mvkmys63d0bx"
} }
}, },
"Audit": { "Audit": {
@ -13878,7 +13941,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "l0ptb1ze54xw8" "id": "dz9hjvx07jaje"
} }
}, },
"AuditRowUpdateReq": { "AuditRowUpdateReq": {
@ -13918,7 +13981,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "56u0jsmndirwt" "id": "t4l2bqjgukcr1"
} }
}, },
"Base": { "Base": {
@ -13995,7 +14058,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "af5ft42zw0uds" "id": "l5nhyvoa0dfag"
} }
}, },
"BaseList": { "BaseList": {
@ -14097,7 +14160,7 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "ixksiq5jlmtt8" "id": "pn96b18y3k0je"
} }
}, },
"BaseReq": { "BaseReq": {
@ -14153,7 +14216,7 @@
"title": "Base Request", "title": "Base Request",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "noplj8yr34xzd" "id": "gebxq2b5pp1a3"
} }
}, },
"Bool": { "Bool": {
@ -14175,7 +14238,7 @@
], ],
"title": "Bool Model", "title": "Bool Model",
"x-stoplight": { "x-stoplight": {
"id": "1mdcm71g7euvt" "id": "6qmxbfclxa8em"
} }
}, },
"Column": { "Column": {
@ -14463,7 +14526,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "jcj9uwgl3rnum" "id": "0nszsk1q6done"
} }
}, },
"ColumnList": { "ColumnList": {
@ -14571,7 +14634,7 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "vs478sn3czrc2" "id": "fjqsqgw1ahlon"
} }
}, },
"ColumnReq": { "ColumnReq": {
@ -14663,7 +14726,7 @@
"title": "Column Request Model", "title": "Column Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "gz9531uzue95g" "id": "fw6an7vvhpqfu"
} }
}, },
"CommentReq": { "CommentReq": {
@ -14696,7 +14759,27 @@
}, },
"required": ["fk_model_id", "row_id"], "required": ["fk_model_id", "row_id"],
"x-stoplight": { "x-stoplight": {
"id": "bsshnshrcy8f7" "id": "k7vnf7oc33p6m"
}
},
"CommentUpdateReq": {
"description": "Model for Comment Update Request",
"x-stoplight": {
"id": "wan84coiq9lyn"
},
"examples": [
{
"description": "This is the comment for the row"
}
],
"title": "Comment Update Request Model",
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "Description for the target row",
"example": "This is the comment for the row"
}
} }
}, },
"Filter": { "Filter": {
@ -14855,7 +14938,7 @@
"title": "Filter Model", "title": "Filter Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "ugwuw0aemt5y2" "id": "goytqs4ua69w0"
} }
}, },
"FilterList": { "FilterList": {
@ -14944,7 +15027,7 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "5wly9iyeixwxs" "id": "wzp2nwu7k19uj"
} }
}, },
"FilterReq": { "FilterReq": {
@ -14962,7 +15045,7 @@
"title": "Filter Request Model", "title": "Filter Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "3rypg89wx0qp1" "id": "viq9ex4d7k8k2"
}, },
"properties": { "properties": {
"comparison_op": { "comparison_op": {
@ -15183,7 +15266,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "luvu108gux43l" "id": "bjh5qbmqyzy1k"
} }
}, },
"FormUpdateReq": { "FormUpdateReq": {
@ -15254,7 +15337,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "j9hvt9lds2o4q" "id": "76m5jsz93rjsc"
} }
}, },
"FormColumn": { "FormColumn": {
@ -15348,7 +15431,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "uga344v068snf" "id": "liiv7vd1hgeqf"
} }
}, },
"FormColumnReq": { "FormColumnReq": {
@ -15415,7 +15498,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "7x7zkufy7a93t" "id": "21k1kvmq14s9a"
} }
}, },
"Formula": { "Formula": {
@ -15456,7 +15539,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "3h68x9a08nvlu" "id": "8ahp8va0147v1"
} }
}, },
"FormulaColumnReq": { "FormulaColumnReq": {
@ -15493,7 +15576,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "rd9929r4qa3c0" "id": "s3c9yjip3dank"
} }
}, },
"Gallery": { "Gallery": {
@ -15587,7 +15670,7 @@
"title": "Gallery Model", "title": "Gallery Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "at47gz5aqeuv5" "id": "e1o8q9fieshbc"
} }
}, },
"GalleryColumn": { "GalleryColumn": {
@ -15622,13 +15705,13 @@
"title": "Gallery Column Model", "title": "Gallery Column Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "4mx8vfd16y55g" "id": "llcstnvsjdgz0"
} }
}, },
"GalleryUpdateReq": { "GalleryUpdateReq": {
"description": "Model for Gallery View Update Request", "description": "Model for Gallery View Update Request",
"x-stoplight": { "x-stoplight": {
"id": "uiz5gs88mmt1a" "id": "5cxhum5vh2nr2"
}, },
"examples": [ "examples": [
{ {
@ -15681,7 +15764,7 @@
"title": "Geo Location Model", "title": "Geo Location Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "cphi852bw9h6n" "id": "hwuzujovr9qwx"
} }
}, },
"Grid": { "Grid": {
@ -15791,7 +15874,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "b84k9hlbnco1o" "id": "xntep33dod0ys"
} }
}, },
"GridColumn": { "GridColumn": {
@ -15875,7 +15958,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "r2fo58lue2zws" "id": "2go3w0igl2243"
} }
}, },
"GridColumnReq": { "GridColumnReq": {
@ -15913,13 +15996,13 @@
"title": "Grid Column Request Model", "title": "Grid Column Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "1viyr55m2bzy3" "id": "2j3uyoabzmsrz"
} }
}, },
"GridUpdateReq": { "GridUpdateReq": {
"description": "Model for Grid View Update", "description": "Model for Grid View Update",
"x-stoplight": { "x-stoplight": {
"id": "rmiv1e1jrzj71" "id": "2q9ya4zmyr5x7"
}, },
"examples": [ "examples": [
{ {
@ -16039,13 +16122,13 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "0rlu88fc7ctax" "id": "q8cxkpronjmyx"
} }
}, },
"HookReq": { "HookReq": {
"description": "Model for Hook", "description": "Model for Hook",
"x-stoplight": { "x-stoplight": {
"id": "yogkf6qrh2el9" "id": "4zlzrppfjxrqb"
}, },
"examples": [ "examples": [
{ {
@ -16223,7 +16306,7 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "xhy5z4cp53e19" "id": "2tsq8h6hjctju"
} }
}, },
"HookLog": { "HookLog": {
@ -16304,7 +16387,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "uwhlr759v7ftb" "id": "t3hlkw7zdqpom"
} }
}, },
"HookTestReq": { "HookTestReq": {
@ -16358,7 +16441,7 @@
}, },
"required": ["hook", "payload"], "required": ["hook", "payload"],
"x-stoplight": { "x-stoplight": {
"id": "7fzbka372uhws" "id": "p2t60nmuy6x26"
} }
}, },
"Id": { "Id": {
@ -16369,7 +16452,7 @@
"title": "ID Model", "title": "ID Model",
"type": "string", "type": "string",
"x-stoplight": { "x-stoplight": {
"id": "q0pbjcp2vwe2s" "id": "srvzhcm5j7d65"
} }
}, },
"Kanban": { "Kanban": {
@ -16436,7 +16519,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "y0wib3uswcjv3" "id": "1jq82nu2uugxr"
} }
}, },
"KanbanColumn": { "KanbanColumn": {
@ -16509,7 +16592,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "jq9x7ac9cr56v" "id": "ipigcz5c7b5qy"
} }
}, },
"KanbanUpdateReq": { "KanbanUpdateReq": {
@ -16586,7 +16669,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "ds5bt314hgogd" "id": "ufp4dyqfvlk6a"
} }
}, },
"LicenseReq": { "LicenseReq": {
@ -16608,7 +16691,7 @@
"title": "License Key Request Model", "title": "License Key Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "ydf14317b0zdt" "id": "2hys6jqfbam2w"
} }
}, },
"LinkToAnotherColumnReq": { "LinkToAnotherColumnReq": {
@ -16657,7 +16740,7 @@
"title": "LinkToAnotherColumn Request Model", "title": "LinkToAnotherColumn Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "72l8lzwwxd36o" "id": "nx1h0s2wseid0"
} }
}, },
"LinkToAnotherRecord": { "LinkToAnotherRecord": {
@ -16733,7 +16816,7 @@
"title": "LinkToAnotherRecord Model", "title": "LinkToAnotherRecord Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "dyp9tqjqy0vjn" "id": "845623nb0zp0w"
} }
}, },
"Lookup": { "Lookup": {
@ -16773,7 +16856,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "oin5vuleupbau" "id": "22i9gmsgqt79n"
} }
}, },
"LookupColumnReq": { "LookupColumnReq": {
@ -16810,7 +16893,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "pas3o57wv14yl" "id": "dhmss9g7ljbw7"
} }
}, },
"Map": { "Map": {
@ -16887,13 +16970,13 @@
"title": "Map Model", "title": "Map Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "g54ldc4p8ukcb" "id": "2gkbn859ho3ct"
} }
}, },
"MapUpdateReq": { "MapUpdateReq": {
"description": "Model for Map", "description": "Model for Map",
"x-stoplight": { "x-stoplight": {
"id": "np6n7vwe0bpbc" "id": "u7xywz40dryws"
}, },
"examples": [ "examples": [
{ {
@ -16968,7 +17051,7 @@
"title": "Map Column Model", "title": "Map Column Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "jwohh924xkrki" "id": "ftaul6uruvc4z"
} }
}, },
"Meta": { "Meta": {
@ -16987,7 +17070,7 @@
], ],
"title": "Meta Model", "title": "Meta Model",
"x-stoplight": { "x-stoplight": {
"id": "3bua8s1l4ia18" "id": "pqxyr0shwtrp5"
} }
}, },
"ModelRoleVisibility": { "ModelRoleVisibility": {
@ -17030,7 +17113,7 @@
"title": "ModelRoleVisibility Model", "title": "ModelRoleVisibility Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "qd2r14lgbgil5" "id": "d3j1x4pulo4ng"
} }
}, },
"NormalColumnRequest": { "NormalColumnRequest": {
@ -17209,7 +17292,7 @@
"type": "object", "type": "object",
"required": ["column_name"], "required": ["column_name"],
"x-stoplight": { "x-stoplight": {
"id": "lbdjjckrjmr9a" "id": "uzaqliigop3nw"
} }
}, },
"OrgUserReq": { "OrgUserReq": {
@ -17234,7 +17317,7 @@
"title": "Organisation User Request Model", "title": "Organisation User Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "odog9hoqfpbii" "id": "pk8a8itnqcis1"
} }
}, },
"Paginated": { "Paginated": {
@ -17276,7 +17359,7 @@
"title": "Paginated Model", "title": "Paginated Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "x9joizn5gys68" "id": "a5zhehpll01cm"
} }
}, },
"Password": { "Password": {
@ -17287,7 +17370,7 @@
"title": "Password Model", "title": "Password Model",
"type": "string", "type": "string",
"x-stoplight": { "x-stoplight": {
"id": "bn4nxlclrh5pl" "id": "mleq9lljoh76x"
} }
}, },
"PasswordChangeReq": { "PasswordChangeReq": {
@ -17311,7 +17394,7 @@
"title": "Password Change Request Model", "title": "Password Change Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "0bt6ueal1r1ai" "id": "8a21c5za82w87"
} }
}, },
"PasswordForgotReq": { "PasswordForgotReq": {
@ -17332,7 +17415,7 @@
"title": "Password Forgot Request Model", "title": "Password Forgot Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "ucprs67a4yk4e" "id": "ggt9lpf2gz2va"
} }
}, },
"PasswordResetReq": { "PasswordResetReq": {
@ -17354,7 +17437,7 @@
"title": "Password Reset Request Model", "title": "Password Reset Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "kgww333t4d6oi" "id": "f63x3tpb8ti9r"
} }
}, },
"Plugin": { "Plugin": {
@ -17471,7 +17554,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "yddgye9ktoowm" "id": "0atoct4o6csg1"
} }
}, },
"PluginReq": { "PluginReq": {
@ -17495,7 +17578,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "8n2qkaa4mmbwk" "id": "w6jwfvgpbhmt0"
} }
}, },
"PluginTestReq": { "PluginTestReq": {
@ -17537,7 +17620,7 @@
}, },
"required": ["title", "input", "category"], "required": ["title", "input", "category"],
"x-stoplight": { "x-stoplight": {
"id": "xburi6p29sjxt" "id": "yye6yekmme06k"
} }
}, },
"Project": { "Project": {
@ -17627,7 +17710,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "mws3mrzua4dgn" "id": "xzqakuahxm8d1"
} }
}, },
"ProjectList": { "ProjectList": {
@ -17723,7 +17806,7 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "xyl4jpahycz9z" "id": "rb5o2yaf8ue64"
} }
}, },
"ProjectReq": { "ProjectReq": {
@ -17776,13 +17859,13 @@
"title": "Project Request Model", "title": "Project Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "q30kb0cjsqejc" "id": "km2fj1ebsroys"
} }
}, },
"ProjectUpdateReq": { "ProjectUpdateReq": {
"description": "Model for Project Update Request", "description": "Model for Project Update Request",
"x-stoplight": { "x-stoplight": {
"id": "c3vcm2zuncet0" "id": "ou8zri28ak9yl"
}, },
"examples": [ "examples": [
{ {
@ -17847,7 +17930,7 @@
}, },
"required": ["email", "roles"], "required": ["email", "roles"],
"x-stoplight": { "x-stoplight": {
"id": "9t06rq4mfxpsi" "id": "9pgemwq7iirkp"
} }
}, },
"Rollup": { "Rollup": {
@ -17896,7 +17979,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "vx9w2nz7au6ra" "id": "rq1o1yy2htfv0"
} }
}, },
"RollupColumnReq": { "RollupColumnReq": {
@ -17947,7 +18030,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "4dhjg4qtqyxgt" "id": "8yc4ashgpj37h"
} }
}, },
"SelectOption": { "SelectOption": {
@ -17989,7 +18072,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "phvjpv4bedapu" "id": "sc5hl2s3rm8x7"
} }
}, },
"SelectOptions": { "SelectOptions": {
@ -18020,7 +18103,7 @@
}, },
"required": ["options"], "required": ["options"],
"x-stoplight": { "x-stoplight": {
"id": "kjpgrmx40vtbs" "id": "r6d5rziqt288w"
} }
}, },
"SharedBaseReq": { "SharedBaseReq": {
@ -18048,7 +18131,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "oinjv6cau9px7" "id": "ybt81tfrpfvd2"
} }
}, },
"SharedView": { "SharedView": {
@ -18086,7 +18169,7 @@
} }
], ],
"x-stoplight": { "x-stoplight": {
"id": "80fgtmpmd1hxi" "id": "9wxqjviu1tfd4"
} }
}, },
"SharedViewList": { "SharedViewList": {
@ -18180,7 +18263,7 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "4nhixasc8ncwz" "id": "jtlp6vswbc96e"
} }
}, },
"SharedViewReq": { "SharedViewReq": {
@ -18204,7 +18287,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "bnn8fqklepl7j" "id": "zja4lmiol065l"
} }
}, },
"SignInReq": { "SignInReq": {
@ -18230,7 +18313,7 @@
"title": "Signin Request Model", "title": "Signin Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "u8vbpvxi9gs2z" "id": "2m1y6fuop0adc"
} }
}, },
"SignUpReq": { "SignUpReq": {
@ -18289,7 +18372,7 @@
}, },
"required": ["email", "password"], "required": ["email", "password"],
"x-stoplight": { "x-stoplight": {
"id": "tw6fyw78ih8dx" "id": "t7stkblz9wllz"
} }
}, },
"Sort": { "Sort": {
@ -18342,7 +18425,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "9vo9f9qec4rwf" "id": "tlcmtmjzcyhm6"
} }
}, },
"SortList": { "SortList": {
@ -18423,7 +18506,7 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "wwcmyc9odjaxt" "id": "7j3nbohh43tyy"
} }
}, },
"SortReq": { "SortReq": {
@ -18449,7 +18532,7 @@
} }
}, },
"x-stoplight": { "x-stoplight": {
"id": "g320wvsh90rcp" "id": "shzdd8k3xswo9"
} }
}, },
"StringOrNull": { "StringOrNull": {
@ -18466,7 +18549,7 @@
], ],
"title": "StringOrNull Model", "title": "StringOrNull Model",
"x-stoplight": { "x-stoplight": {
"id": "n57pb7pryq2zm" "id": "1lecfrpmnndhi"
} }
}, },
"Table": { "Table": {
@ -18895,7 +18978,7 @@
}, },
"required": ["table_name", "title"], "required": ["table_name", "title"],
"x-stoplight": { "x-stoplight": {
"id": "jnbkhtbvk6np6" "id": "986a3bp33neka"
} }
}, },
"TableList": { "TableList": {
@ -19025,7 +19108,7 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "dki89c4lat66i" "id": "gemdr6sjuj4gl"
} }
}, },
"TableReq": { "TableReq": {
@ -19166,7 +19249,7 @@
"title": "Table Request Model", "title": "Table Request Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "rx8c69o5719vx" "id": "3i0r55tix0aso"
} }
}, },
"User": { "User": {
@ -19218,7 +19301,7 @@
}, },
"required": ["email", "email_verified", "firstname", "id", "lastname"], "required": ["email", "email_verified", "firstname", "id", "lastname"],
"x-stoplight": { "x-stoplight": {
"id": "cls6abm42ey04" "id": "2uxbpdjvs379s"
} }
}, },
"UserInfo": { "UserInfo": {
@ -19262,7 +19345,7 @@
"title": "User Info Model", "title": "User Info Model",
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "tvl09i4n62jnc" "id": "uatcf3jcnna1t"
} }
}, },
"UserList": { "UserList": {
@ -19343,7 +19426,7 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "kxaws22o5xk74" "id": "ygjybmp21hdgp"
} }
}, },
"View": { "View": {
@ -19457,7 +19540,7 @@
}, },
"required": ["fk_model_id", "show", "title", "type"], "required": ["fk_model_id", "show", "title", "type"],
"x-stoplight": { "x-stoplight": {
"id": "t38y987y9rgv5" "id": "66ki9gaxq3kwg"
} }
}, },
"ViewList": { "ViewList": {
@ -19585,13 +19668,13 @@
}, },
"required": ["list", "pageInfo"], "required": ["list", "pageInfo"],
"x-stoplight": { "x-stoplight": {
"id": "kjdjlqsioesz5" "id": "2iat5dhhl4hoa"
} }
}, },
"ViewCreateReq": { "ViewCreateReq": {
"type": "object", "type": "object",
"x-stoplight": { "x-stoplight": {
"id": "cuum3lv94tb57" "id": "venaxkw5uod3n"
}, },
"title": "ViewCreateReq", "title": "ViewCreateReq",
"description": "Model for View Create Request", "description": "Model for View Create Request",
@ -19660,7 +19743,7 @@
"ViewUpdateReq": { "ViewUpdateReq": {
"description": "Model for View Update Request", "description": "Model for View Update Request",
"x-stoplight": { "x-stoplight": {
"id": "zlgpq6n2pg2rj" "id": "bo1mzzdpk82z0"
}, },
"examples": [ "examples": [
{ {
@ -19725,7 +19808,7 @@
"ViewColumnUpdateReq": { "ViewColumnUpdateReq": {
"description": "Model for View Column Update Request", "description": "Model for View Column Update Request",
"x-stoplight": { "x-stoplight": {
"id": "zt66lex00hzth" "id": "jmhr4prd02ssy"
}, },
"examples": [ "examples": [
{ {
@ -19754,7 +19837,7 @@
"ViewColumnReq": { "ViewColumnReq": {
"description": "Model for View Column Request", "description": "Model for View Column Request",
"x-stoplight": { "x-stoplight": {
"id": "chpsc3sgvhehw" "id": "05tr5rxotp432"
}, },
"examples": [ "examples": [
{ {
@ -19845,7 +19928,7 @@
"title": "Visibility Rule Request Model", "title": "Visibility Rule Request Model",
"type": "array", "type": "array",
"x-stoplight": { "x-stoplight": {
"id": "bmgsfkt8woxh5" "id": "c6q9v9rtiduxg"
} }
} }
}, },

3
tests/playwright/pages/ProjectsPage/index.ts

@ -147,6 +147,9 @@ export class ProjectsPage extends BasePage {
}); });
await projRow.locator('.nc-action-btn').nth(0).click(); await projRow.locator('.nc-action-btn').nth(0).click();
// there is a flicker; add delay to avoid flakiness
await this.rootPage.waitForTimeout(1000);
await project.locator('input.nc-metadb-project-name').fill(newTitle); await project.locator('input.nc-metadb-project-name').fill(newTitle);
// press enter to save // press enter to save
const submitAction = () => project.locator('input.nc-metadb-project-name').press('Enter'); const submitAction = () => project.locator('input.nc-metadb-project-name').press('Enter');

21
tests/playwright/tests/projectOperations.spec.ts

@ -3,11 +3,13 @@ import { DashboardPage } from '../pages/Dashboard';
import setup from '../setup'; import setup from '../setup';
import { ToolbarPage } from '../pages/Dashboard/common/Toolbar'; import { ToolbarPage } from '../pages/Dashboard/common/Toolbar';
import { ProjectsPage } from '../pages/ProjectsPage'; import { ProjectsPage } from '../pages/ProjectsPage';
import { Api } from 'nocodb-sdk';
test.describe('Project operations', () => { test.describe('Project operations', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;
let toolbar: ToolbarPage; let toolbar: ToolbarPage;
let context: any; let context: any;
let api: Api<any>;
let projectPage: ProjectsPage; let projectPage: ProjectsPage;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@ -15,9 +17,28 @@ test.describe('Project operations', () => {
dashboard = new DashboardPage(page, context.project); dashboard = new DashboardPage(page, context.project);
projectPage = new ProjectsPage(page); projectPage = new ProjectsPage(page);
toolbar = dashboard.grid.toolbar; toolbar = dashboard.grid.toolbar;
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': context.token,
},
});
}); });
test('rename, delete', async () => { test('rename, delete', async () => {
// if project already exists, delete it
try {
const projectList = await api.project.list();
const project = projectList.list.find((p: any) => p.title === 'project-firstName');
if (project) {
await api.project.delete(project.id);
console.log('deleted project: ', project.id);
}
} catch (e) {
console.log('Error: ', e);
}
await dashboard.clickHome(); await dashboard.clickHome();
await projectPage.createProject({ name: 'project-firstName', withoutPrefix: true }); await projectPage.createProject({ name: 'project-firstName', withoutPrefix: true });
await dashboard.clickHome(); await dashboard.clickHome();

Loading…
Cancel
Save