Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions packages/nc-gui/components/cell/ZipcodeRange/Editor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<script lang="ts" setup>
import { formatRange, parseValue, validateRange } from './utils'
import MdiCloseCircle from '~icons/mdi/close-circle'

interface Props {
modelValue?: string | any[]
rowIndex?: number
}

const { modelValue } = defineProps<Props>()

const emit = defineEmits(['update:modelValue'])

const column = inject(ColumnInj)!

const readOnly = inject(ReadonlyInj)!

const isEditable = inject(EditModeInj, ref(false))

const activeCell = inject(ActiveCellInj, ref(false))

const isForm = inject(IsFormInj, ref(false))

const active = computed(() => activeCell.value || isEditable.value || isForm.value)

const rowHeight = inject(RowHeightInj, ref(undefined))

const isOpen = ref(false)

const startZipcode = ref('')
const endZipcode = ref('')
const errorMessage = ref('')

const ranges = computed({
get: () => parseValue(modelValue),
set: (val) => {
emit('update:modelValue', val.length === 0 ? null : JSON.stringify(val))
},
})

const getRandomColor = (index: number) => {
const colors = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2', '#eb2f96']
return colors[index % colors.length]
}

const addRange = () => {
errorMessage.value = ''

const start = startZipcode.value.trim()
const end = endZipcode.value.trim()

const validation = validateRange(start, end)

if (!validation.isValid) {
errorMessage.value = validation.error || 'Invalid range'
return
}

const newRange = {
start,
end: end && end !== start ? end : undefined,
}

ranges.value = [...ranges.value, newRange]
startZipcode.value = ''
endZipcode.value = ''
}

const removeRange = (index: number) => {
const updated = [...ranges.value]
updated.splice(index, 1)
ranges.value = updated
}

useSelectedCellKeydownListener(
activeCell,
(e) => {
switch (e.key) {
case 'Escape':
isOpen.value = false
break
case 'Enter':
if (active.value && !isOpen.value) {
isOpen.value = true
}
break
default:
if (!(e.metaKey || e.ctrlKey || e.altKey) && e.key?.length === 1 && !isDrawerOrModalExist()) {
e.stopPropagation()
isOpen.value = true
}
break
}
},
{
immediate: true,
isGridCell: true,
},
)
</script>

<template>
<div class="nc-cell-field nc-zipcode-range h-full w-full flex items-center" :class="{ 'max-w-full': isForm }">
<div
class="flex flex-wrap flex-1 items-center"
:style="{
'display': '-webkit-box',
'max-width': '100%',
'-webkit-line-clamp': rowHeightTruncateLines(rowHeight, true),
'-webkit-box-orient': 'vertical',
'-webkit-box-align': 'center',
'overflow': 'hidden',
}"
>
<template v-for="(range, index) of ranges" :key="index">
<a-tag
class="rounded-tag max-w-full"
:class="{
'!my-0': !rowHeight || rowHeight === 1,
}"
:color="getRandomColor(index)"
:closable="!readOnly"
@close="removeRange(index)"
>
<span
:style="{
color: getSelectTypeOptionTextColor(getRandomColor(index)),
}"
class="text-small"
>
{{ formatRange(range) }}
</span>
</a-tag>
</template>
</div>

<a-dropdown
v-model:open="isOpen"
:disabled="readOnly || !active"
:trigger="['click']"
placement="bottomLeft"
overlay-class-name="nc-zipcode-range-dropdown"
>
<div v-if="!readOnly && active" class="flex-shrink-0 ml-1">
<NcButton size="xs" type="text">
<GeneralIcon icon="plus" class="w-3 h-3" />
</NcButton>
</div>

<template #overlay>
<div class="p-3 bg-white rounded-lg shadow-lg min-w-[300px]">
<div class="mb-3">
<label class="block text-xs font-medium mb-1">Start Zipcode</label>
<a-input
v-model:value="startZipcode"
placeholder="e.g., 20000"
maxlength="5"
@keyup.enter="addRange"
/>
</div>

<div class="mb-3">
<label class="block text-xs font-medium mb-1">End Zipcode (optional)</label>
<a-input
v-model:value="endZipcode"
placeholder="e.g., 20999"
maxlength="5"
@keyup.enter="addRange"
/>
</div>

<div v-if="errorMessage" class="mb-3 text-xs text-red-500">
{{ errorMessage }}
</div>

<div class="flex justify-end gap-2">
<NcButton size="small" type="secondary" @click="isOpen = false">
Cancel
</NcButton>
<NcButton size="small" type="primary" @click="addRange">
Add Range
</NcButton>
</div>
</div>
</template>
</a-dropdown>
</div>
</template>

<style scoped lang="scss">
.rounded-tag {
@apply py-[0.5px] px-2 rounded-[12px];
}

:deep(.ant-tag) {
@apply "rounded-tag" my-[1px];
}

:deep(.ant-tag-close-icon) {
@apply "text-slate-500";
}
</style>
99 changes: 99 additions & 0 deletions packages/nc-gui/components/cell/ZipcodeRange/Readonly.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script lang="ts" setup>
import { formatRange, parseValue } from './utils'

interface Props {
modelValue?: string | any[]
rowIndex?: number
}

const { modelValue } = defineProps<Props>()

const column = inject(ColumnInj)!

const isForm = inject(IsFormInj, ref(false))

const rowHeight = inject(RowHeightInj, ref(undefined))

const isKanban = inject(IsKanbanInj, ref(false))

const extensionConfig = inject(ExtensionConfigInj, ref({ isPageDesignerPreviewPanel: false }))

const ranges = computed(() => parseValue(modelValue))

const getRandomColor = (index: number) => {
const colors = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2', '#eb2f96']
return colors[index % colors.length]
}
</script>

<template>
<div class="nc-cell-field nc-zipcode-range h-full w-full flex items-center read-only" :class="{ 'max-w-full': isForm }">
<div
class="flex flex-wrap"
:class="{
'flex-col items-start gap-2': extensionConfig?.widget?.displayAs === 'List',
}"
:style="
extensionConfig?.widget?.displayAs !== 'List'
? {
'display': '-webkit-box',
'max-width': '100%',
'-webkit-line-clamp': rowHeightTruncateLines(rowHeight, true),
'-webkit-box-orient': 'vertical',
'-webkit-box-align': 'center',
'overflow': 'hidden',
}
: {}
"
>
<template v-for="(range, index) of ranges" :key="index">
<a-tag
class="rounded-tag max-w-full"
:class="{
'!my-0': !rowHeight || rowHeight === 1,
}"
:color="getRandomColor(index)"
>
<span
:style="{
color: getSelectTypeOptionTextColor(getRandomColor(index)),
}"
:class="{ 'text-sm': isKanban, 'text-small': !isKanban }"
>
<NcTooltip class="truncate max-w-full" show-on-truncate-only>
<template #title>
{{ formatRange(range) }}
</template>
<span
class="text-ellipsis overflow-hidden"
:style="{
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ formatRange(range) }}
</span>
</NcTooltip>
</span>
</a-tag>
</template>
</div>
</div>
</template>

<style scoped lang="scss">
.read-only {
.ms-close-icon {
display: none;
}
}

.rounded-tag {
@apply py-[0.5px] px-2 rounded-[12px];
}

:deep(.ant-tag) {
@apply "rounded-tag" my-[1px];
}
</style>
40 changes: 40 additions & 0 deletions packages/nc-gui/components/cell/ZipcodeRange/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script lang="ts" setup>
interface Props {
modelValue?: string | any[]
rowIndex?: number
showReadonlyField?: boolean
}

const props = defineProps<Props>()

const emit = defineEmits(['update:modelValue'])

const vModel = useVModel(props, 'modelValue', emit)

const column = inject(ColumnInj)!

const active = inject(ActiveCellInj, ref(false))

const readOnly = inject(ReadonlyInj, ref(false))

const isForm = inject(IsFormInj, ref(false))

const isEditColumn = inject(EditColumnInj, ref(false))

const showReadonlyField = computed(() => {
return props.showReadonlyField ?? (readOnly.value || !(active.value || isEditColumn.value || isForm.value))
})
</script>

<template>
<LazyCellZipcodeRangeReadonly
v-if="showReadonlyField"
:model-value="vModel"
:row-index="rowIndex"
/>
<LazyCellZipcodeRangeEditor
v-else
v-model="vModel"
:row-index="rowIndex"
/>
</template>
Loading