Skip to content
Open
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
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repos:
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg|frontend/src2/types/emojis.ts"
- id: check-merge-conflict
- id: check-ast
- id: check-json
Expand All @@ -33,6 +33,7 @@ repos:
rev: v3.1.0
hooks:
- id: prettier
exclude: "frontend/src2/types/emojis.ts"
types_or: [javascript, vue, scss]

ci:
Expand Down
52 changes: 36 additions & 16 deletions frontend/src2/charts/components/NumberChart.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue'
import { formatNumber, getShortNumber } from '../../helpers'
import { formatNumber, getShortNumber } from '../../helpers/index'
import { NumberChartConfig, NumberColumnOptions } from '../../types/chart.types'
import { iconMap } from '../../types/iconMap'
import { QueryResult, QueryResultColumn, QueryResultRow } from '../../types/query.types'
import Sparkline from './Sparkline.vue'

Expand Down Expand Up @@ -46,16 +47,18 @@ const cards = computed(() => {
const delta = config.value.negative_is_better
? previousValue - currentValue
: currentValue - previousValue
const percentDelta = (delta / Math.abs(previousValue)) * 100
const percentDelta = (delta / Math.abs(previousValue || 1)) * 100

const prefix = getNumberOption(idx, 'prefix')
const suffix = getNumberOption(idx, 'suffix')
const decimal = getNumberOption(idx, 'decimal')
const color = getNumberOption(idx, 'color')
const shorten_numbers = getNumberOption(idx, 'shorten_numbers')
const prefix = (getNumberOption(idx, 'prefix') as string) || ''
const suffix = (getNumberOption(idx, 'suffix') as string) || ''
const decimal = getNumberOption(idx, 'decimal') as number
const color = getNumberOption(idx, 'color') as string
const icon = getNumberOption(idx, 'icon') as string
const shorten_numbers = getNumberOption(idx, 'shorten_numbers') as boolean

return {
measure_name,
icon,
values: numberValues,
currentValue: getFormattedValue(currentValue, decimal, shorten_numbers),
previousValue: getFormattedValue(previousValue, decimal, shorten_numbers),
Expand All @@ -76,14 +79,19 @@ const getFormattedValue = (value: number, decimal?: number, shorten_numbers?: bo
return formatNumber(value, decimal)
}

function getNumberOption(index: number, option: keyof NumberColumnOptions) {
const numberOption = config.value.number_column_options?.[index]?.[option] as any
return numberOption === undefined ? config.value[option] : numberOption
function getNumberOption<T = string | number | boolean>(
index: number,
option: keyof NumberColumnOptions,
): T | undefined {
const numberOption = config.value.number_column_options?.[index]?.[option]
if (numberOption !== undefined) return numberOption as T
const globalOption = (config.value as any)[option]
return globalOption !== undefined ? globalOption : undefined
}

function onDoubleClick(measure_name: string) {
const column = props.result.columns.find((c) => c.name === measure_name)
const row = props.result.formattedRows.at(-1)
const row = props.result.formattedRows[props.result.formattedRows.length - 1]
if (column && row) {
emit('drillDown', column, row)
}
Expand All @@ -105,16 +113,28 @@ function onDoubleClick(measure_name: string) {
prefix,
suffix,
color,
icon,
} in cards"
:key="measure_name"
class="flex max-h-[140px] items-center gap-2 overflow-hidden rounded bg-white px-6 pt-5 shadow cursor-pointer"
:class="config.comparison ? 'pb-6' : 'pb-3'"
@dblclick="onDoubleClick(measure_name)"
>
<div class="flex w-full flex-col">
<span class="truncate text-sm font-medium">
{{ measure_name }}
</span>
<div class="flex items-center gap-2">
<component
v-if="icon && iconMap[icon as keyof typeof iconMap]"
:is="iconMap[icon as keyof typeof iconMap]"
class="h-6 w-6 flex-shrink-0 fill-current stroke-0"
:style="{ color }"
/>
<span v-else-if="icon" class="text-xl leading-none flex-shrink-0">{{
icon
}}</span>
<span class="truncate text-sm font-medium">
{{ measure_name }}
</span>
</div>
<div
class="flex-1 flex-shrink-0 truncate text-[24px] font-semibold leading-10"
:style="color && typeof color === 'string' ? { color: color } : {}"
Expand All @@ -130,8 +150,8 @@ function onDoubleClick(measure_name: string) {
? 'text-red-500'
: 'text-green-500'
: delta >= 0
? 'text-green-500'
: 'text-red-500',
? 'text-green-500'
: 'text-red-500',
]"
>
<span class="">
Expand Down
23 changes: 18 additions & 5 deletions frontend/src2/charts/components/NumberChartConfigForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { computed, watchEffect } from 'vue'
import DraggableList from '../../components/DraggableList.vue'
import InlineFormControlLabel from '../../components/InlineFormControlLabel.vue'
import { FIELDTYPES } from '../../helpers/constants'
import { NumberChartConfig, NumberColumnOptions } from '../../types/chart.types'
import { ICONS, NumberChartConfig, NumberColumnOptions } from '../../types/chart.types'
import { ColumnOption, Dimension, DimensionOption, MeasureOption } from '../../types/query.types'
import CollapsibleSection from './CollapsibleSection.vue'
import DimensionPicker from './DimensionPicker.vue'
import MeasurePicker from './MeasurePicker.vue'
import IconPicker from '../../components/IconPicker.vue'
import { FormControl } from 'frappe-ui'

const props = defineProps<{
dimensions: DimensionOption[]
Expand Down Expand Up @@ -46,19 +48,19 @@ function addNumberColumn() {
if (!config.value.number_columns) {
config.value.number_columns = []
}
config.value.number_columns.push({} as MeasureOption)
config.value.number_columns!.push({} as MeasureOption)
}

const updateColor = debounce((color: string) => {
config.value.sparkline_color = color
}, 500)

function getNumberOption(index: number, option: keyof NumberColumnOptions) {
return config.value.number_column_options[index]?.[option]
return config.value.number_column_options[index]?.[option] as any
}
function setNumberOption(index: number, option: keyof NumberColumnOptions, value: any) {
if (!config.value.number_column_options[index]) {
config.value.number_column_options[index] = {} as NumberColumnOptions
config.value.number_column_options![index] = {} as NumberColumnOptions
}
config.value.number_column_options[index][option] = value
}
Expand Down Expand Up @@ -117,6 +119,17 @@ function setNumberOption(index: number, option: keyof NumberColumnOptions, value
/>
</InlineFormControlLabel>

<InlineFormControlLabel label="Icon">
<IconPicker
:model-value="getNumberOption(index, 'icon')"
@update:model-value="
(val: unknown) =>
setNumberOption(index, 'icon', val as string)
"
class="!mb-0"
/>
</InlineFormControlLabel>

<Toggle
label="Show short numbers"
:modelValue="getNumberOption(index, 'shorten_numbers')"
Expand All @@ -130,7 +143,7 @@ function setNumberOption(index: number, option: keyof NumberColumnOptions, value
</DraggableList>
<button
class="mt-1.5 text-left text-xs text-gray-600 hover:underline"
@click="config.number_columns.push({} as any)"
@click="addNumberColumn()"
>
+ Add column
</button>
Expand Down
105 changes: 105 additions & 0 deletions frontend/src2/components/IconPicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { X } from 'lucide-vue-next'
import { Button, FormControl } from 'frappe-ui'
import { EMOJIS } from '../types/emojis'
import { IconName } from '../types/chart.types'
import { iconMap } from '../types/iconMap'

const model = defineModel<string>({ default: '' })

const emit = defineEmits<{
'update:modelValue': [value: string]
}>()

const searchQuery = ref('')

const filteredIcons = computed(() => {
if (!searchQuery.value) return EMOJIS
return EMOJIS.filter((icon) => icon.includes(searchQuery.value))
})

const selectIcon = (icon: IconName) => {
model.value = icon
emit('update:modelValue', icon)
searchQuery.value = ''
}

const clearIcon = () => {
model.value = ''
emit('update:modelValue', '')
}
</script>

<template>
<div class="relative">
<Popover class="w-full" placement="bottom-start">
<template #target="{ togglePopover }">
<Button
variant="outline"
class="h-9 w-full justify-center text-center font-normal text-sm"
@click="togglePopover"
>
<component
v-if="model && iconMap[model as keyof typeof iconMap]"
:is="iconMap[model as keyof typeof iconMap]"
class="h-4 w-4 fill-current stroke-0"
/>
<span v-else-if="model" class="text-lg leading-none">
{{ model }}
</span>
<span v-else class="truncate text-gray-400">Select Icon</span>
</Button>
</template>

<template #body="{ togglePopover }">
<div class="w-80 z-50 shadow-xl border border-gray-200 rounded-lg bg-white">
<div class="border-b border-gray-200 p-3 rounded-t-lg">
<FormControl
v-model="searchQuery"
placeholder="Search icons..."
class="w-full border border-gray-200"
/>
</div>

<div class="max-h-[300px] overflow-y-auto p-3">
<Button
v-for="icon in filteredIcons"
:key="icon"
size="sm"
variant="ghost"
class="mx-0.5 my-0.5 h-12 w-12 p-0 justify-center hover:bg-gray-100"
:class="model === icon ? 'bg-gray-100 ring-1 ring-gray-300' : ''"
@click="
selectIcon(icon)
togglePopover(false)
"
>
<component
v-if="iconMap[icon as keyof typeof iconMap]"
:is="iconMap[icon as keyof typeof iconMap]"
class="h-5 w-5 fill-current stroke-0"
/>
<span v-else class="text-lg leading-none">{{ icon }}</span>
</Button>
</div>

<div class="flex items-center p-3 border-t border-gray-200 rounded-b-lg">
<Button
variant="outline"
class="gap-2"
size="sm"
@click="
clearIcon()
togglePopover(false)
"
>
<X class="h-4 w-4" />
Clear
</Button>
</div>
</div>
</template>
</Popover>
</div>
</template>
8 changes: 6 additions & 2 deletions frontend/src2/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ body {
opacity: 0;
}

.lucide {
stroke-width: 1.5;
@layer utilities {
.lucide {
stroke-width: 2;
color: theme('colors.foreground');
fill: none;
}
}
41 changes: 40 additions & 1 deletion frontend/src2/types/chart.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,46 @@ export type NumberColumnOptions = {
prefix?: string
suffix?: string
color?: string
}
icon?: string
}

import { EMOJIS } from './emojis'

export const ICONS = [
'users',
'dollar-sign',
'users-up',
'trending-up',
'activity',
'bar-chart-3',
'pie-chart',
'target',
'award',
'star',
'heart',
'zap',
'globe',
'home',
'box',
'truck',
'phone',
'mail',
'calendar',
'clock',
'percentage',
'arrow-up-circle',
'arrow-down-circle',
'check-circle',
'alert-triangle',
'none'
] as const

export const ALL_ICONS = [...ICONS, ...EMOJIS] as const

export type IconName = string

export { iconMap } from './iconMap'


export type DonutChartConfig = {
label_column: Dimension
Expand Down
10 changes: 10 additions & 0 deletions frontend/src2/types/emojis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const EMOJIS = [
'📊', '📈', '📉', '📋', '💰', '💵', '💱', '⭐', '✅', '❌', '⚠️', '🚀', '🔥', '💎',
'👥', '👤', '🏠', '🌍', '📱', '💻', '⚙️', '🔧', '📧', '📅', '🕐', '📊', '🎯',
'📈', '📉', '💹', '📊', '💼', '🏢', '🏭', '🛒', '🛍️', '💳', '💳', '💰',
'📈', '📉', '🎲', '🎯', '🏆', '🥇', '💯', '🔝', '📈', '📉', '➡️', '⬆️', '⬇️',
'✅', '❌', '⚠️', 'ℹ️', '🔄', '⏳', '✅'
] as const;

export type EmojiName = (typeof EMOJIS)[number];

32 changes: 32 additions & 0 deletions frontend/src2/types/iconMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Users, DollarSign, TrendingUp, Activity, BarChart3, PieChart, Target, Award, Star, Heart, Zap, Globe, Home, Box, Truck, Phone, Mail, Calendar, Clock, Percent, ArrowUpCircle, ArrowDownCircle, CheckCircle, AlertTriangle } from 'lucide-vue-next'
import type { IconName } from './chart.types'

export const iconMap: Record<IconName, any> = {
'users': Users,
'dollar-sign': DollarSign,
'users-up': Users,
'trending-up': TrendingUp,
'activity': Activity,
'bar-chart-3': BarChart3,
'pie-chart': PieChart,
'target': Target,
'award': Award,
'star': Star,
'heart': Heart,
'zap': Zap,
'globe': Globe,
'home': Home,
'box': Box,
'truck': Truck,
'phone': Phone,
'mail': Mail,
'calendar': Calendar,
'clock': Clock,
'percentage': Percent,
'arrow-up-circle': ArrowUpCircle,
'arrow-down-circle': ArrowDownCircle,
'check-circle': CheckCircle,
'alert-triangle': AlertTriangle,
none: undefined
}

Loading