mirror of https://github.com/nocodb/nocodb
flisowna
2 years ago
150 changed files with 3787 additions and 885 deletions
@ -0,0 +1,194 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import type { ColumnType } from 'nocodb-sdk' |
||||||
|
import { |
||||||
|
ColumnInj, |
||||||
|
EditModeInj, |
||||||
|
ReadonlyInj, |
||||||
|
computed, |
||||||
|
isBoolean, |
||||||
|
isCurrency, |
||||||
|
isDate, |
||||||
|
isDateTime, |
||||||
|
isDecimal, |
||||||
|
isDuration, |
||||||
|
isFloat, |
||||||
|
isInt, |
||||||
|
isMultiSelect, |
||||||
|
isPercent, |
||||||
|
isRating, |
||||||
|
isSingleSelect, |
||||||
|
isTextArea, |
||||||
|
isTime, |
||||||
|
isYear, |
||||||
|
provide, |
||||||
|
ref, |
||||||
|
toRef, |
||||||
|
useProject, |
||||||
|
} from '#imports' |
||||||
|
import type { Filter } from '~/lib' |
||||||
|
import SingleSelect from '~/components/cell/SingleSelect.vue' |
||||||
|
import MultiSelect from '~/components/cell/MultiSelect.vue' |
||||||
|
import DatePicker from '~/components/cell/DatePicker.vue' |
||||||
|
import YearPicker from '~/components/cell/YearPicker.vue' |
||||||
|
import DateTimePicker from '~/components/cell/DateTimePicker.vue' |
||||||
|
import TimePicker from '~/components/cell/TimePicker.vue' |
||||||
|
import Rating from '~/components/cell/Rating.vue' |
||||||
|
import Duration from '~/components/cell/Duration.vue' |
||||||
|
import Percent from '~/components/cell/Percent.vue' |
||||||
|
import Currency from '~/components/cell/Currency.vue' |
||||||
|
import Decimal from '~/components/cell/Decimal.vue' |
||||||
|
import Integer from '~/components/cell/Integer.vue' |
||||||
|
import Float from '~/components/cell/Float.vue' |
||||||
|
import Text from '~/components/cell/Text.vue' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
column: ColumnType |
||||||
|
filter: Filter |
||||||
|
} |
||||||
|
|
||||||
|
interface Emits { |
||||||
|
(event: 'updateFilterValue', model: any): void |
||||||
|
} |
||||||
|
|
||||||
|
const props = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits<Emits>() |
||||||
|
|
||||||
|
const column = toRef(props, 'column') |
||||||
|
|
||||||
|
const editEnabled = ref(true) |
||||||
|
|
||||||
|
provide(ColumnInj, column) |
||||||
|
|
||||||
|
provide(EditModeInj, readonly(editEnabled)) |
||||||
|
|
||||||
|
provide(ReadonlyInj, ref(false)) |
||||||
|
|
||||||
|
const checkTypeFunctions = { |
||||||
|
isSingleSelect, |
||||||
|
isMultiSelect, |
||||||
|
isDate, |
||||||
|
isYear, |
||||||
|
isDateTime, |
||||||
|
isTime, |
||||||
|
isRating, |
||||||
|
isDuration, |
||||||
|
isPercent, |
||||||
|
isCurrency, |
||||||
|
isDecimal, |
||||||
|
isInt, |
||||||
|
isFloat, |
||||||
|
isTextArea, |
||||||
|
} |
||||||
|
|
||||||
|
type FilterType = keyof typeof checkTypeFunctions |
||||||
|
|
||||||
|
const { sqlUi } = $(useProject()) |
||||||
|
|
||||||
|
const abstractType = $computed(() => (column.value?.dt && sqlUi ? sqlUi.getAbstractType(column.value) : null)) |
||||||
|
|
||||||
|
const checkType = (filterType: FilterType) => { |
||||||
|
const checkTypeFunction = checkTypeFunctions[filterType] |
||||||
|
|
||||||
|
if (!column.value || !checkTypeFunction) { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return checkTypeFunction(column.value, abstractType) |
||||||
|
} |
||||||
|
|
||||||
|
const filterInput = computed({ |
||||||
|
get: () => { |
||||||
|
return props.filter.value |
||||||
|
}, |
||||||
|
set: (value) => { |
||||||
|
emit('updateFilterValue', value) |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
const booleanOptions = [ |
||||||
|
{ value: true, label: 'true' }, |
||||||
|
{ value: false, label: 'false' }, |
||||||
|
{ value: null, label: 'unset' }, |
||||||
|
] |
||||||
|
|
||||||
|
const componentMap: Partial<Record<FilterType, any>> = $computed(() => { |
||||||
|
return { |
||||||
|
// use MultiSelect for SingleSelect columns for anyof / nanyof filters |
||||||
|
isSingleSelect: ['anyof', 'nanyof'].includes(props.filter.comparison_op!) ? MultiSelect : SingleSelect, |
||||||
|
isMultiSelect: MultiSelect, |
||||||
|
isDate: DatePicker, |
||||||
|
isYear: YearPicker, |
||||||
|
isDateTime: DateTimePicker, |
||||||
|
isTime: TimePicker, |
||||||
|
isRating: Rating, |
||||||
|
isDuration: Duration, |
||||||
|
isPercent: Percent, |
||||||
|
isCurrency: Currency, |
||||||
|
isDecimal: Decimal, |
||||||
|
isInt: Integer, |
||||||
|
isFloat: Float, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const filterType = $computed(() => { |
||||||
|
return Object.keys(componentMap).find((key) => checkType(key as FilterType)) |
||||||
|
}) |
||||||
|
|
||||||
|
const componentProps = $computed(() => { |
||||||
|
switch (filterType) { |
||||||
|
case 'isSingleSelect': |
||||||
|
case 'isMultiSelect': { |
||||||
|
return { disableOptionCreation: true } |
||||||
|
} |
||||||
|
case 'isPercent': |
||||||
|
case 'isDecimal': |
||||||
|
case 'isFloat': |
||||||
|
case 'isInt': { |
||||||
|
return { class: 'h-32px' } |
||||||
|
} |
||||||
|
case 'isDuration': { |
||||||
|
return { showValidationError: false } |
||||||
|
} |
||||||
|
default: { |
||||||
|
return {} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const hasExtraPadding = $computed(() => { |
||||||
|
return ( |
||||||
|
column.value && |
||||||
|
(isInt(column.value, abstractType) || |
||||||
|
isDate(column.value, abstractType) || |
||||||
|
isDateTime(column.value, abstractType) || |
||||||
|
isTime(column.value, abstractType) || |
||||||
|
isYear(column.value, abstractType)) |
||||||
|
) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<a-select |
||||||
|
v-if="column && isBoolean(column, abstractType)" |
||||||
|
v-model:value="filterInput" |
||||||
|
:disabled="filter.readOnly" |
||||||
|
:options="booleanOptions" |
||||||
|
/> |
||||||
|
<div |
||||||
|
v-else |
||||||
|
class="bg-white border-1 flex min-w-120px max-w-170px min-h-32px h-full" |
||||||
|
:class="{ 'px-2': hasExtraPadding }" |
||||||
|
@mouseup.stop |
||||||
|
> |
||||||
|
<component |
||||||
|
:is="filterType ? componentMap[filterType] : Text" |
||||||
|
v-model="filterInput" |
||||||
|
:disabled="filter.readOnly" |
||||||
|
placeholder="Enter a value" |
||||||
|
:column="column" |
||||||
|
class="flex" |
||||||
|
v-bind="componentProps" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</template> |
@ -1,40 +1,42 @@ |
|||||||
--- |
--- |
||||||
title: "Primary Value" |
title: "Display Value" |
||||||
description: "Understanding Primary Value in NocoDB!" |
description: "Understanding Display Value in NocoDB!" |
||||||
position: 580 |
position: 580 |
||||||
category: "Product" |
category: "Product" |
||||||
menuTitle: "Primary Value" |
menuTitle: "Display Value" |
||||||
--- |
--- |
||||||
|
|
||||||
## What is a Primary Value ? |
## What is a Display Value ? |
||||||
- Primary value as the name stands is the primary or main value within a row of a table that you generally associate that row with. |
|
||||||
|
- Display Value as the name stands is the primary or main value within a row of a table that you generally associate that row with. |
||||||
- It should be usually associated with a column which is uniquely identifiable. However, this uniqueness is not enforced at the database level. |
- It should be usually associated with a column which is uniquely identifiable. However, this uniqueness is not enforced at the database level. |
||||||
|
- Before v0.105.0, Display Value was known as Primary Value. |
||||||
|
|
||||||
## What is the use of Primary Value ? |
## What is the use of Display Value ? |
||||||
- Within a spreadsheet, primary value are always highlighted so that it is easier to recognise what row we are in. |
- Within a spreadsheet, Display Value are always highlighted so that it is easier to recognise what row we are in. |
||||||
- And when LinkToAnotherRecord is created between two tables - it is the primary value that appears in LinkToAnotheRecord column. |
- And when LinkToAnotherRecord is created between two tables - it is the Display Value that appears in LinkToAnotheRecord column. |
||||||
|
|
||||||
#### Example : Primary Value highlighted in Actor table |
#### Example : Display Value highlighted in Actor table |
||||||
<img width="646" alt="image" src="https://user-images.githubusercontent.com/35857179/189114321-58ebaa16-20e2-4615-abda-39417a5df5bf.png"> |
<img width="646" alt="image" src="https://user-images.githubusercontent.com/35857179/189114321-58ebaa16-20e2-4615-abda-39417a5df5bf.png"> |
||||||
|
|
||||||
#### Example : Primary Value highlighted in Film table |
#### Example : Display Value highlighted in Film table |
||||||
<img width="643" alt="image" src="https://user-images.githubusercontent.com/35857179/189114462-a7fef0e2-f9ac-4943-98d5-fee9f60a4ab5.png"> |
<img width="643" alt="image" src="https://user-images.githubusercontent.com/35857179/189114462-a7fef0e2-f9ac-4943-98d5-fee9f60a4ab5.png"> |
||||||
|
|
||||||
#### Example : Primary Value associated when LinkToAnotherRecord is created |
#### Example : Display Value associated when LinkToAnotherRecord is created |
||||||
<img width="311" alt="image" src="https://user-images.githubusercontent.com/35857179/189114548-193acc4d-f714-4204-a560-97668db7884c.png"> |
<img width="311" alt="image" src="https://user-images.githubusercontent.com/35857179/189114548-193acc4d-f714-4204-a560-97668db7884c.png"> |
||||||
|
|
||||||
## How to set Primary Value ? |
## How to set Display Value ? |
||||||
|
|
||||||
Click down arrow in the target column. Click `Set as Primary Value`. |
Click down arrow in the target column. Click `Set as Display Value`. |
||||||
|
|
||||||
<img width="251" alt="image" src="https://user-images.githubusercontent.com/35857179/189114857-b452aa6b-5cdb-4a74-9980-cb839d7d15fd.png"> |
![image](https://user-images.githubusercontent.com/35857179/219339727-dee5fdea-6db7-4a06-9e48-df7113cc63b1.png) |
||||||
|
|
||||||
|
|
||||||
## How is Primary Value identfied for existing database tables ? |
## How is Display Value identfied for existing database tables ? |
||||||
|
|
||||||
- It is usually the first column after the primary key which is not a number. |
- It is usually the first column after the primary key which is not a number. |
||||||
- If there is no column which is not a number then the column adjacent to primary key is chosen. |
- If there is no column which is not a number then the column adjacent to primary key is chosen. |
||||||
|
|
||||||
## Can I change the Primary Value to another column within tables ? |
## Can I change the Display Value to another column within tables ? |
||||||
|
|
||||||
- Yes, you can use the same way mentioned above to set Primary Value. |
- Yes, you can use the same way mentioned above to set Display Value. |
@ -0,0 +1,209 @@ |
|||||||
|
import { customAlphabet } from 'nanoid'; |
||||||
|
import { |
||||||
|
ColumnReqType, |
||||||
|
LinkToAnotherRecordType, |
||||||
|
LookupColumnReqType, |
||||||
|
RelationTypes, |
||||||
|
RollupColumnReqType, |
||||||
|
TableType, |
||||||
|
UITypes, |
||||||
|
} from 'nocodb-sdk'; |
||||||
|
import Column from '../../../models/Column'; |
||||||
|
import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; |
||||||
|
import LookupColumn from '../../../models/LookupColumn'; |
||||||
|
import Model from '../../../models/Model'; |
||||||
|
import { getUniqueColumnAliasName } from '../../helpers/getUniqueName'; |
||||||
|
import validateParams from '../../helpers/validateParams'; |
||||||
|
|
||||||
|
export const randomID = customAlphabet( |
||||||
|
'1234567890abcdefghijklmnopqrstuvwxyz_', |
||||||
|
10 |
||||||
|
); |
||||||
|
|
||||||
|
export async function createHmAndBtColumn( |
||||||
|
child: Model, |
||||||
|
parent: Model, |
||||||
|
childColumn: Column, |
||||||
|
type?: RelationTypes, |
||||||
|
alias?: string, |
||||||
|
fkColName?: string, |
||||||
|
virtual = false, |
||||||
|
isSystemCol = false |
||||||
|
) { |
||||||
|
// save bt column
|
||||||
|
{ |
||||||
|
const title = getUniqueColumnAliasName( |
||||||
|
await child.getColumns(), |
||||||
|
type === 'bt' ? alias : `${parent.title}` |
||||||
|
); |
||||||
|
await Column.insert<LinkToAnotherRecordColumn>({ |
||||||
|
title, |
||||||
|
|
||||||
|
fk_model_id: child.id, |
||||||
|
// ref_db_alias
|
||||||
|
uidt: UITypes.LinkToAnotherRecord, |
||||||
|
type: 'bt', |
||||||
|
// db_type:
|
||||||
|
|
||||||
|
fk_child_column_id: childColumn.id, |
||||||
|
fk_parent_column_id: parent.primaryKey.id, |
||||||
|
fk_related_model_id: parent.id, |
||||||
|
virtual, |
||||||
|
system: isSystemCol, |
||||||
|
fk_col_name: fkColName, |
||||||
|
fk_index_name: fkColName, |
||||||
|
}); |
||||||
|
} |
||||||
|
// save hm column
|
||||||
|
{ |
||||||
|
const title = getUniqueColumnAliasName( |
||||||
|
await parent.getColumns(), |
||||||
|
type === 'hm' ? alias : `${child.title} List` |
||||||
|
); |
||||||
|
await Column.insert({ |
||||||
|
title, |
||||||
|
fk_model_id: parent.id, |
||||||
|
uidt: UITypes.LinkToAnotherRecord, |
||||||
|
type: 'hm', |
||||||
|
fk_child_column_id: childColumn.id, |
||||||
|
fk_parent_column_id: parent.primaryKey.id, |
||||||
|
fk_related_model_id: child.id, |
||||||
|
virtual, |
||||||
|
system: isSystemCol, |
||||||
|
fk_col_name: fkColName, |
||||||
|
fk_index_name: fkColName, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function validateRollupPayload( |
||||||
|
payload: ColumnReqType & { uidt: UITypes } |
||||||
|
) { |
||||||
|
validateParams( |
||||||
|
[ |
||||||
|
'title', |
||||||
|
'fk_relation_column_id', |
||||||
|
'fk_rollup_column_id', |
||||||
|
'rollup_function', |
||||||
|
], |
||||||
|
payload |
||||||
|
); |
||||||
|
|
||||||
|
const relation = await ( |
||||||
|
await Column.get({ |
||||||
|
colId: (payload as RollupColumnReqType).fk_relation_column_id, |
||||||
|
}) |
||||||
|
).getColOptions<LinkToAnotherRecordType>(); |
||||||
|
|
||||||
|
if (!relation) { |
||||||
|
throw new Error('Relation column not found'); |
||||||
|
} |
||||||
|
|
||||||
|
let relatedColumn: Column; |
||||||
|
switch (relation.type) { |
||||||
|
case 'hm': |
||||||
|
relatedColumn = await Column.get({ |
||||||
|
colId: relation.fk_child_column_id, |
||||||
|
}); |
||||||
|
break; |
||||||
|
case 'mm': |
||||||
|
case 'bt': |
||||||
|
relatedColumn = await Column.get({ |
||||||
|
colId: relation.fk_parent_column_id, |
||||||
|
}); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
const relatedTable = await relatedColumn.getModel(); |
||||||
|
if ( |
||||||
|
!(await relatedTable.getColumns()).find( |
||||||
|
(c) => c.id === (payload as RollupColumnReqType).fk_rollup_column_id |
||||||
|
) |
||||||
|
) |
||||||
|
throw new Error('Rollup column not found in related table'); |
||||||
|
} |
||||||
|
|
||||||
|
export async function validateLookupPayload( |
||||||
|
payload: ColumnReqType & { uidt: UITypes }, |
||||||
|
columnId?: string |
||||||
|
) { |
||||||
|
validateParams( |
||||||
|
['title', 'fk_relation_column_id', 'fk_lookup_column_id'], |
||||||
|
payload |
||||||
|
); |
||||||
|
|
||||||
|
// check for circular reference
|
||||||
|
if (columnId) { |
||||||
|
let lkCol: LookupColumn | LookupColumnReqType = |
||||||
|
payload as LookupColumnReqType; |
||||||
|
while (lkCol) { |
||||||
|
// check if lookup column is same as column itself
|
||||||
|
if (columnId === lkCol.fk_lookup_column_id) |
||||||
|
throw new Error('Circular lookup reference not allowed'); |
||||||
|
lkCol = await Column.get({ colId: lkCol.fk_lookup_column_id }).then( |
||||||
|
(c: Column) => { |
||||||
|
if (c.uidt === 'Lookup') { |
||||||
|
return c.getColOptions<LookupColumn>(); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const relation = await ( |
||||||
|
await Column.get({ |
||||||
|
colId: (payload as LookupColumnReqType).fk_relation_column_id, |
||||||
|
}) |
||||||
|
).getColOptions<LinkToAnotherRecordType>(); |
||||||
|
|
||||||
|
if (!relation) { |
||||||
|
throw new Error('Relation column not found'); |
||||||
|
} |
||||||
|
|
||||||
|
let relatedColumn: Column; |
||||||
|
switch (relation.type) { |
||||||
|
case 'hm': |
||||||
|
relatedColumn = await Column.get({ |
||||||
|
colId: relation.fk_child_column_id, |
||||||
|
}); |
||||||
|
break; |
||||||
|
case 'mm': |
||||||
|
case 'bt': |
||||||
|
relatedColumn = await Column.get({ |
||||||
|
colId: relation.fk_parent_column_id, |
||||||
|
}); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
const relatedTable = await relatedColumn.getModel(); |
||||||
|
if ( |
||||||
|
!(await relatedTable.getColumns()).find( |
||||||
|
(c) => c.id === (payload as LookupColumnReqType).fk_lookup_column_id |
||||||
|
) |
||||||
|
) |
||||||
|
throw new Error('Lookup column not found in related table'); |
||||||
|
} |
||||||
|
|
||||||
|
export const validateRequiredField = ( |
||||||
|
payload: Record<string, any>, |
||||||
|
requiredProps: string[] |
||||||
|
) => { |
||||||
|
return requiredProps.every( |
||||||
|
(prop) => |
||||||
|
prop in payload && payload[prop] !== undefined && payload[prop] !== null |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
// generate unique foreign key constraint name for foreign key
|
||||||
|
export const generateFkName = (parent: TableType, child: TableType) => { |
||||||
|
// generate a unique constraint name by taking first 10 chars of parent and child table name (by replacing all non word chars with _)
|
||||||
|
// and appending a random string of 15 chars maximum length.
|
||||||
|
// In database constraint name can be upto 64 chars and here we are generating a name of maximum 40 chars
|
||||||
|
const constraintName = `fk_${parent.table_name |
||||||
|
.replace(/\W+/g, '_') |
||||||
|
.slice(0, 10)}_${child.table_name |
||||||
|
.replace(/\W+/g, '_') |
||||||
|
.slice(0, 10)}_${randomID(15)}`;
|
||||||
|
return constraintName; |
||||||
|
}; |
@ -1,3 +1,4 @@ |
|||||||
import { populateMeta } from './populateMeta'; |
import { populateMeta } from './populateMeta'; |
||||||
|
export * from './columnHelpers'; |
||||||
|
|
||||||
export { populateMeta }; |
export { populateMeta }; |
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue