Browse Source

Merge branch 'develop' into fix/kanban-share-view

pull/4848/head
Wing-Kam Wong 2 years ago
parent
commit
ba8ff29274
  1. 1
      packages/nc-gui/components.d.ts
  2. 155
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  3. 2
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  4. 10
      packages/nc-gui/composables/useViewData.ts
  5. 19
      packages/noco-docs/content/en/setup-and-usages/account-settings.md
  6. 4
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  7. 17
      tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts
  8. 7
      tests/playwright/tests/columnSingleSelect.spec.ts

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

@ -124,6 +124,7 @@ declare module '@vue/runtime-core' {
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default'] MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default'] MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default'] MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiArrowULeftBottom: typeof import('~icons/mdi/arrow-u-left-bottom')['default']
MdiAt: typeof import('~icons/mdi/at')['default'] MdiAt: typeof import('~icons/mdi/at')['default']
MdiBackburger: typeof import('~icons/mdi/backburger')['default'] MdiBackburger: typeof import('~icons/mdi/backburger')['default']
MdiBookOpenOutline: typeof import('~icons/mdi/book-open-outline')['default'] MdiBookOpenOutline: typeof import('~icons/mdi/book-open-outline')['default']

155
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -3,6 +3,14 @@ import Draggable from 'vuedraggable'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { IsKanbanInj, enumColor, onMounted, useColumnCreateStoreOrThrow, useVModel, watch } from '#imports' import { IsKanbanInj, enumColor, onMounted, useColumnCreateStoreOrThrow, useVModel, watch } from '#imports'
interface Option {
color: string
title: string
id?: string
fk_colum_id?: string
order?: number
}
const props = defineProps<{ const props = defineProps<{
value: any value: any
}>() }>()
@ -13,7 +21,10 @@ const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, isPg, isMysql } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, isPg, isMysql } = useColumnCreateStoreOrThrow()
let options = $ref<any[]>([]) let options = $ref<Option[]>([])
let renderedOptions = $ref<(Option & { status?: 'remove' })[]>([])
let savedDefaultOption = $ref<Option | null>(null)
let savedCdf = $ref<string | null>(null)
const colorMenus = $ref<any>({}) const colorMenus = $ref<any>({})
@ -58,6 +69,9 @@ onMounted(() => {
} }
} }
options = vModel.value.colOptions.options options = vModel.value.colOptions.options
renderedOptions = [...options]
// Support for older options // Support for older options
for (const op of options.filter((el) => el.order === null)) { for (const op of options.filter((el) => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '') op.title = op.title.replace(/^'/, '').replace(/'$/, '')
@ -87,13 +101,6 @@ const optionChanged = (changedId: string) => {
} }
} }
const optionDropped = (changedId: string) => {
if (changedId && changedId === defaultOption.value?.id) {
vModel.value.cdf = null
defaultOption.value = null
}
}
const getNextColor = () => { const getNextColor = () => {
let tempColor = colors[0] let tempColor = colors[0]
if (options.length && options[options.length - 1].color) { if (options.length && options[options.length - 1].color) {
@ -108,13 +115,40 @@ const addNewOption = () => {
title: '', title: '',
color: getNextColor(), color: getNextColor(),
} }
renderedOptions.push(tempOption)
options.push(tempOption) options.push(tempOption)
} }
const removeOption = (index: number) => { const syncOptions = () => {
const optionId = options[index]?.id vModel.value.colOptions.options = renderedOptions.filter((op) => op.status !== 'remove')
options.splice(index, 1) }
optionDropped(optionId)
const removeRenderedOption = (index: number) => {
renderedOptions[index].status = 'remove'
syncOptions()
const optionId = renderedOptions[index]?.id
if (optionId === defaultOption.value?.id) {
savedDefaultOption = { ...defaultOption.value }
savedCdf = vModel.value.cdf
defaultOption.value = null
vModel.value.cdf = null
}
}
const undoRemoveRenderedOption = (index: number) => {
renderedOptions[index].status = undefined
syncOptions()
const optionId = renderedOptions[index]?.id
if (optionId === savedDefaultOption?.id) {
defaultOption.value = { ...savedDefaultOption }
vModel.value.cdf = savedCdf
savedDefaultOption = null
savedCdf = null
}
} }
// focus last created input // focus last created input
@ -128,47 +162,62 @@ watch(inputs, () => {
<template> <template>
<div class="w-full"> <div class="w-full">
<div class="max-h-[250px] overflow-x-auto scrollbar-thin-dull pr-3"> <div class="max-h-[250px] overflow-x-auto scrollbar-thin-dull pr-3">
<Draggable :list="options" item-key="id" handle=".nc-child-draggable-icon"> <Draggable :list="renderedOptions" item-key="id" handle=".nc-child-draggable-icon" @change="syncOptions">
<template #item="{ element, index }"> <template #item="{ element, index }">
<div class="flex py-1 items-center nc-select-option"> <div class="flex p-1 items-center nc-select-option">
<MdiDragVertical <div
v-if="!isKanban" class="flex items-center w-full"
small :data-testid="`select-column-option-${index}`"
class="nc-child-draggable-icon handle" :class="{ removed: element.status === 'remove' }"
:data-testid="`select-option-column-handle-icon-${element.title}`"
/>
<a-dropdown
v-model:visible="colorMenus[index]"
:trigger="['click']"
overlay-class-name="nc-dropdown-select-color-options"
> >
<template #overlay> <MdiDragVertical
<LazyGeneralColorPicker v-if="!isKanban"
v-model="element.color" small
:pick-button="true" class="nc-child-draggable-icon handle"
@update:model-value="colorMenus[index] = false" :data-testid="`select-option-column-handle-icon-${element.title}`"
/>
<a-dropdown
v-model:visible="colorMenus[index]"
:trigger="['click']"
overlay-class-name="nc-dropdown-select-color-options"
>
<template #overlay>
<LazyGeneralColorPicker
v-model="element.color"
:pick-button="true"
@update:model-value="colorMenus[index] = false"
/>
</template>
<MdiArrowDownDropCircle
class="mr-2 text-[1.5em] outline-0 hover:!text-[1.75em]"
:class="{ 'text-[1.75em]': colorMenus[index] }"
:style="{ color: element.color }"
/> />
</template> </a-dropdown>
<MdiArrowDownDropCircle
class="mr-2 text-[1.5em] outline-0 hover:!text-[1.75em]" <a-input
:class="{ 'text-[1.75em]': colorMenus[index] }" ref="inputs"
:style="{ color: element.color }" v-model:value="element.title"
class="caption"
:data-testid="`select-column-option-input-${index}`"
:disabled="element.status === 'remove'"
@keydown.enter.prevent="element.title?.trim() && addNewOption()"
@change="optionChanged(element.id)"
/> />
</a-dropdown> </div>
<a-input
ref="inputs"
v-model:value="element.title"
class="caption"
:data-testid="`select-column-option-input-${index}`"
@keydown.enter.prevent="element.title?.trim() && addNewOption()"
@change="optionChanged(element.id)"
/>
<MdiClose <MdiClose
v-if="element.status !== 'remove'"
class="ml-2 hover:!text-black-500 text-gray-500 cursor-pointer" class="ml-2 hover:!text-black-500 text-gray-500 cursor-pointer"
:data-testid="`select-column-option-remove-${index}`" :data-testid="`select-column-option-remove-${index}`"
@click="removeOption(index)" @click="removeRenderedOption(index)"
/>
<MdiArrowULeftBottom
v-else
class="ml-2 hover:!text-black-500 text-gray-500 cursor-pointer"
:data-testid="`select-column-option-remove-undo-${index}`"
@click="undoRemoveRenderedOption(index)"
/> />
</div> </div>
</template> </template>
@ -186,3 +235,19 @@ watch(inputs, () => {
</a-button> </a-button>
</div> </div>
</template> </template>
<style scoped>
.removed {
position: relative;
}
.removed:after {
position: absolute;
left: 0;
top: 50%;
height: 1px;
background: #ccc;
content: '';
width: calc(100% + 5px);
display: block;
}
</style>

2
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -212,7 +212,7 @@ const iframeCode = computed(() => {
frameborder="0" frameborder="0"
width="100%" width="100%"
height="700" height="700"
style="background: transparent; border: 1px solid #ddd"/>` style="background: transparent; border: 1px solid #ddd"></iframe>`
}) })
const copyIframeCode = async () => { const copyIframeCode = async () => {

10
packages/nc-gui/composables/useViewData.ts

@ -49,7 +49,9 @@ export function useViewData(
const { getMeta } = useMetas() const { getMeta } = useMetas()
const appInfoDefaultLimit = appInfo.defaultLimit || 25 const appInfoDefaultLimit = appInfo.defaultLimit || 25
const _paginationData = ref<PaginatedType>({ page: 1, pageSize: appInfoDefaultLimit }) const _paginationData = ref<PaginatedType>({ page: 1, pageSize: appInfoDefaultLimit })
const aggCommentCount = ref<{ row_id: string; count: number }[]>([]) const aggCommentCount = ref<{ row_id: string; count: number }[]>([])
const galleryData = ref<GalleryType>() const galleryData = ref<GalleryType>()
@ -64,7 +66,7 @@ export function useViewData(
const { project, isSharedBase } = useProject() const { project, isSharedBase } = useProject()
const { fetchSharedViewData, paginationData: sharedPaginationData } = useSharedView() const { sharedView, fetchSharedViewData, paginationData: sharedPaginationData } = useSharedView()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -203,8 +205,10 @@ export function useViewData(
} }
async function loadGalleryData() { async function loadGalleryData() {
if (!viewMeta?.value?.id || isPublic.value) return if (!viewMeta?.value?.id) return
galleryData.value = await $api.dbView.galleryRead(viewMeta.value.id) galleryData.value = isPublic.value
? (sharedView.value?.view as GalleryType)
: await $api.dbView.galleryRead(viewMeta.value.id)
} }
async function insertRow( async function insertRow(

19
packages/noco-docs/content/en/setup-and-usages/account-settings.md

@ -27,14 +27,25 @@ If you are a super admin, you can also manage all user roles in organization lev
## User Management ## User Management
Super-admin has new privelege to do user management at root-level. Permissions within NocoDB are divided into two levels: Organisation level and Project level.
- `org-level-creator` - this user can create a new project and access any invited project. ### Organisation Level Permissions:
- `Org Level Creator`: Allows users to create new projects and access invited projects.
- `org-level-viewer` - this user can't create a new project but they can access any invited project. - `Org Level Viewer`: Allows users to access invited projects but does not permit the creation of new projects.
![image](https://user-images.githubusercontent.com/35857179/203261168-5ba75f9c-476e-4fe7-ace4-f81051f42773.png) ![image](https://user-images.githubusercontent.com/35857179/203261168-5ba75f9c-476e-4fe7-ace4-f81051f42773.png)
### Project Level Permissions:
- `Owner`: The user who created the project. A project can have only one owner. The owner persists until the project exists and the role is non-transferable. The owner has access to carry out any operations within the project, including deleting it.
- `Creator`: Has access to carry out any operations within the project except deleting the project and removing the "Owner."
- `Editor`: Can modify data but cannot modify the schema (add/remove columns, tables, users, and such).
- `Commenter`: Can neither modify data nor schema, can only see data and can mark row-level comments.
- `Viewer`: Can only see data.
Additional access details for project level permissions can be found [here](https://docs.nocodb.com/setup-and-usages/team-and-auth#advanced-options--configurations).
Please note that the above-mentioned Project Level Permissions are additional to the already defined Organisation Level Permissions.
In addition to the previously defined permissions, NocoDB also includes the role of "Super Admin." The "Super Admin" is the first user to sign up on this NocoDB installation. An organisation can have only one "Super Admin" and this role is non-transferable. The "Super Admin" will have the equivalent permissions of an "Org Level Creator" and "Owner" for all projects within the organisation.
## Enable / Disable Signup ## Enable / Disable Signup
Signup without an invitation is disabled by default and can be managed from UI by a super admin. Signup without an invitation is disabled by default and can be managed from UI by a super admin.

4
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -384,7 +384,7 @@ export class MssqlUi {
return ''; return '';
case 'varchar': case 'varchar':
return ''; return 255;
default: default:
return ''; return '';
@ -1095,7 +1095,7 @@ export class MssqlUi {
colProp.dt = 'varchar'; colProp.dt = 'varchar';
break; break;
case 'Date': case 'Date':
colProp.dt = 'varchar'; colProp.dt = 'date';
break; break;
case 'Year': case 'Year':

17
tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts

@ -1,5 +1,6 @@
import { ColumnPageObject } from '.'; import { ColumnPageObject } from '.';
import BasePage from '../../../Base'; import BasePage from '../../../Base';
import { expect } from '@playwright/test';
export class SelectOptionColumnPageObject extends BasePage { export class SelectOptionColumnPageObject extends BasePage {
readonly column: ColumnPageObject; readonly column: ColumnPageObject;
@ -49,6 +50,22 @@ export class SelectOptionColumnPageObject extends BasePage {
await this.column.get().locator(`svg[data-testid="select-column-option-remove-${index}"]`).click(); await this.column.get().locator(`svg[data-testid="select-column-option-remove-${index}"]`).click();
await expect(this.column.get().getByTestId(`select-column-option-${index}`)).toHaveClass(/removed/);
await this.column.save({ isUpdated: true });
}
async deleteOptionWithUndo({ columnTitle, index }: { index: number; columnTitle: string }) {
await this.column.openEdit({ title: columnTitle });
await this.column.get().locator(`svg[data-testid="select-column-option-remove-${index}"]`).click();
await expect(this.column.get().getByTestId(`select-column-option-${index}`)).toHaveClass(/removed/);
await this.column.get().locator(`svg[data-testid="select-column-option-remove-undo-${index}"]`).click();
await expect(this.column.get().getByTestId(`select-column-option-${index}`)).not.toHaveClass(/removed/);
await this.column.save({ isUpdated: true }); await this.column.save({ isUpdated: true });
} }

7
tests/playwright/tests/columnSingleSelect.spec.ts

@ -53,6 +53,13 @@ test.describe('Single select', () => {
await grid.cell.selectOption.select({ index: 0, columnHeader: 'SingleSelect', option: 'Option 3' }); await grid.cell.selectOption.select({ index: 0, columnHeader: 'SingleSelect', option: 'Option 3' });
await grid.cell.selectOption.verify({ index: 0, columnHeader: 'SingleSelect', option: 'Option 3' }); await grid.cell.selectOption.verify({ index: 0, columnHeader: 'SingleSelect', option: 'Option 3' });
await grid.column.selectOption.deleteOptionWithUndo({ index: 0, columnTitle: 'SingleSelect' });
await grid.cell.selectOption.verifyOptions({
index: 0,
columnHeader: 'SingleSelect',
options: ['Option 1', 'Option 2', 'Option 3'],
});
await grid.column.selectOption.deleteOption({ index: 2, columnTitle: 'SingleSelect' }); await grid.column.selectOption.deleteOption({ index: 2, columnTitle: 'SingleSelect' });
await grid.cell.selectOption.verifyNoOptionsSelected({ index: 0, columnHeader: 'SingleSelect' }); await grid.cell.selectOption.verifyNoOptionsSelected({ index: 0, columnHeader: 'SingleSelect' });

Loading…
Cancel
Save