Browse Source

chore: update dependencies

0xJacky 2 years ago
parent
commit
813b94baae

+ 4 - 3
frontend/components.d.ts

@@ -58,11 +58,12 @@ declare module '@vue/runtime-core' {
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     RouterView: typeof import('vue-router')['RouterView']
     SetLanguage: typeof import('./src/components/SetLanguage/SetLanguage.vue')['default']
     SetLanguage: typeof import('./src/components/SetLanguage/SetLanguage.vue')['default']
+    StdBatchEdit: typeof import('./src/components/StdDataDisplay/StdBatchEdit.vue')['default']
     StdCurd: typeof import('./src/components/StdDataDisplay/StdCurd.vue')['default']
     StdCurd: typeof import('./src/components/StdDataDisplay/StdCurd.vue')['default']
     StdPagination: typeof import('./src/components/StdDataDisplay/StdPagination.vue')['default']
     StdPagination: typeof import('./src/components/StdDataDisplay/StdPagination.vue')['default']
-    StdPassword: typeof import('./src/components/StdDataEntry/compontents/StdPassword.vue')['default']
-    StdSelect: typeof import('./src/components/StdDataEntry/compontents/StdSelect.vue')['default']
-    StdSelector: typeof import('./src/components/StdDataEntry/compontents/StdSelector.vue')['default']
+    StdPassword: typeof import('./src/components/StdDataEntry/components/StdPassword.vue')['default']
+    StdSelect: typeof import('./src/components/StdDataEntry/components/StdSelect.vue')['default']
+    StdSelector: typeof import('./src/components/StdDataEntry/components/StdSelector.vue')['default']
     StdTable: typeof import('./src/components/StdDataDisplay/StdTable.vue')['default']
     StdTable: typeof import('./src/components/StdDataDisplay/StdTable.vue')['default']
   }
   }
 }
 }

+ 17 - 16
frontend/package.json

@@ -12,34 +12,35 @@
     },
     },
     "dependencies": {
     "dependencies": {
         "@ant-design/icons-vue": "^6.1.0",
         "@ant-design/icons-vue": "^6.1.0",
-        "ant-design-vue": "^3.2.13",
-        "apexcharts": "^3.35.4",
-        "axios": "^0.27.2",
-        "dayjs": "^1.11.4",
-        "lodash": "^4.17.21",
+        "ant-design-vue": "^3.2.15",
+        "apexcharts": "^3.36.3",
+        "axios": "^1.1.3",
+        "dayjs": "^1.11.6",
         "pinia": "^2.0.23",
         "pinia": "^2.0.23",
-        "pinia-plugin-persistedstate": "^1.6.3",
+        "pinia-plugin-persistedstate": "^2.3.0",
         "reconnecting-websocket": "^4.4.0",
         "reconnecting-websocket": "^4.4.0",
         "vite-plugin-build-id": "^0.2.2",
         "vite-plugin-build-id": "^0.2.2",
-        "vue": "^3.2.41",
+        "vue": "^3.2.45",
         "vue-router": "4",
         "vue-router": "4",
         "vue3-ace-editor": "^2.2.2",
         "vue3-ace-editor": "^2.2.2",
         "vue3-apexcharts": "^1.4.1",
         "vue3-apexcharts": "^1.4.1",
         "vue3-gettext": "^2.3.4",
         "vue3-gettext": "^2.3.4",
-        "xterm": "^4.19.0",
-        "xterm-addon-attach": "^0.6.0",
-        "xterm-addon-fit": "^0.5.0"
+        "xterm": "^5.0.0",
+        "xterm-addon-attach": "^0.7.0",
+        "xterm-addon-fit": "^0.6.0",
+        "@types/lodash": "^4.14.188",
+        "vuedraggable": "^4.1.0",
+        "@types/sortablejs": "^1.15.0"
     },
     },
     "devDependencies": {
     "devDependencies": {
-        "@types/lodash": "^4.14.182",
-        "@vitejs/plugin-vue": "^3.0.0",
-        "@vitejs/plugin-vue-jsx": "^2.0.0",
+        "@vitejs/plugin-vue": "^3.2.0",
+        "@vitejs/plugin-vue-jsx": "^2.1.1",
         "@zougt/vite-plugin-theme-preprocessor": "^1.4.5",
         "@zougt/vite-plugin-theme-preprocessor": "^1.4.5",
         "less": "^4.1.3",
         "less": "^4.1.3",
         "typescript": "^4.6.4",
         "typescript": "^4.6.4",
-        "unplugin-vue-components": "^0.21.2",
-        "vite": "^3.0.0",
+        "unplugin-vue-components": "^0.22.9",
+        "vite": "^3.2.3",
         "vite-plugin-html": "^3.2.0",
         "vite-plugin-html": "^3.2.0",
-        "vue-tsc": "^0.38.4"
+        "vue-tsc": "^1.0.9"
     }
     }
 }
 }

+ 77 - 0
frontend/src/components/StdDataDisplay/StdBatchEdit.vue

@@ -0,0 +1,77 @@
+<script setup lang="ts">
+import {reactive, ref} from 'vue'
+import gettext from '@/gettext'
+
+const {$gettext} = gettext
+
+import StdDataEntry from '@/components/StdDataEntry'
+import {message} from 'ant-design-vue'
+
+const emit = defineEmits(['onSave'])
+
+const props = defineProps(['api', 'beforeSave'])
+
+const batchColumns = ref([])
+
+const visible = ref(false)
+
+const selectedRowKeys = ref([])
+
+function showModal(c: any, rowKeys: any) {
+    visible.value = true
+    selectedRowKeys.value = rowKeys
+    batchColumns.value = c
+}
+
+defineExpose({
+    showModal
+})
+
+const data = reactive({})
+const error = reactive({})
+const loading = ref(false)
+
+async function ok() {
+    loading.value = true
+
+    await props.beforeSave?.()
+
+    await props.api(selectedRowKeys.value, data).then(async () => {
+        message.success($gettext('Save successfully'))
+        emit('onSave')
+    }).catch((e: any) => {
+        message.error(e?.message ?? $gettext('Server error'))
+    }).finally(() => {
+        loading.value = false
+    })
+}
+</script>
+
+<template>
+    <a-modal
+            class="std-curd-edit-modal"
+            :mask="false"
+            :title="$gettext('Batch Modify')"
+            v-model:visible="visible"
+            :cancel-text="$gettext('Cancel')"
+            :ok-text="$gettext('OK')"
+            @ok="ok"
+            :confirm-loading="loading"
+            :width="600"
+            destroyOnClose
+    >
+
+        <std-data-entry
+                ref="std_data_entry"
+                :data-list="batchColumns"
+                v-model:data-source="data"
+                :error="error"
+        />
+
+        <slot name="extra"/>
+    </a-modal>
+</template>
+
+<style scoped>
+
+</style>

+ 11 - 5
frontend/src/components/StdDataDisplay/StdCurd.vue

@@ -55,17 +55,18 @@ const props = defineProps({
     modalWidth: {
     modalWidth: {
         type: Number,
         type: Number,
         default: 600
         default: 600
-    }
+    },
+    useSortable: Boolean
 })
 })
 
 
 const visible = ref(false)
 const visible = ref(false)
 const update = ref(0)
 const update = ref(0)
 const data: any = reactive({id: null})
 const data: any = reactive({id: null})
 const error: any = reactive({})
 const error: any = reactive({})
-const selected = reactive([])
+const selected = ref([])
 
 
 function onSelect(keys: any) {
 function onSelect(keys: any) {
-    selected.concat(...keys)
+    selected.value = keys
 }
 }
 
 
 function editableColumns() {
 function editableColumns() {
@@ -83,6 +84,11 @@ function add() {
     visible.value = true
     visible.value = true
 }
 }
 
 
+defineExpose({
+    add,
+    data
+})
+
 const table = ref(null)
 const table = ref(null)
 
 
 interface Table {
 interface Table {
@@ -125,6 +131,7 @@ function edit(id: any) {
     })
     })
 }
 }
 
 
+const selectedRowKeys = ref([])
 </script>
 </script>
 
 
 <template>
 <template>
@@ -136,12 +143,11 @@ function edit(id: any) {
 
 
             <std-table
             <std-table
                     ref="table"
                     ref="table"
+                    v-model:selected-row-keys="selectedRowKeys"
                     v-bind="props"
                     v-bind="props"
                     @clickEdit="edit"
                     @clickEdit="edit"
                     @selected="onSelect"
                     @selected="onSelect"
                     :key="update"
                     :key="update"
-                    :get_params="get_params"
-                    :exportCsv="exportCsv"
             >
             >
                 <template v-slot:actions="slotProps">
                 <template v-slot:actions="slotProps">
                     <slot name="actions" :actions="slotProps.record"/>
                     <slot name="actions" :actions="slotProps.record"/>

+ 4 - 4
frontend/src/components/StdDataDisplay/StdPagination.vue

@@ -2,11 +2,11 @@
 import {useGettext} from 'vue3-gettext'
 import {useGettext} from 'vue3-gettext'
 
 
 const {pagination, size} = defineProps(['pagination', 'size'])
 const {pagination, size} = defineProps(['pagination', 'size'])
-const emit = defineEmits(['changePage'])
+const emit = defineEmits(['change'])
 const {$gettext} = useGettext()
 const {$gettext} = useGettext()
 
 
-function changePage(num: number) {
-    emit('changePage', num)
+function change(num: number, pageSize: number) {
+    emit('change', num, pageSize)
 }
 }
 </script>
 </script>
 
 
@@ -17,7 +17,7 @@ function changePage(num: number) {
                 :pageSize="pagination.per_page"
                 :pageSize="pagination.per_page"
                 :size="size"
                 :size="size"
                 :total="pagination.total"
                 :total="pagination.total"
-                @change="changePage"
+                @change="change"
         />
         />
     </div>
     </div>
 </template>
 </template>

+ 181 - 12
frontend/src/components/StdDataDisplay/StdTable.vue

@@ -7,10 +7,13 @@ import {useRoute, useRouter} from 'vue-router'
 import {message} from 'ant-design-vue'
 import {message} from 'ant-design-vue'
 import {downloadCsv} from '@/lib/helper'
 import {downloadCsv} from '@/lib/helper'
 import dayjs from 'dayjs'
 import dayjs from 'dayjs'
+import Sortable from 'sortablejs'
+import {HolderOutlined} from '@ant-design/icons-vue'
+import {toRaw} from '@vue/reactivity'
 
 
 const {$gettext, interpolate} = gettext
 const {$gettext, interpolate} = gettext
 
 
-const emit = defineEmits(['onSelected', 'onSelectedRecord', 'clickEdit', 'update:selectedRowKeys'])
+const emit = defineEmits(['onSelected', 'onSelectedRecord', 'clickEdit', 'update:selectedRowKeys', 'clickBatchModify'])
 
 
 const props = defineProps({
 const props = defineProps({
     api: Object,
     api: Object,
@@ -71,11 +74,14 @@ const props = defineProps({
     size: String,
     size: String,
     selectedRowKeys: {
     selectedRowKeys: {
         type: Array
         type: Array
-    }
+    },
+    useSortable: Boolean
 })
 })
 
 
+const data_source: any = ref([])
+const expand_keys_list: any = ref([])
+const rows_key_index_map: any = ref({})
 
 
-const data_source = ref([])
 const loading = ref(true)
 const loading = ref(true)
 const pagination = reactive({
 const pagination = reactive({
     total: 1,
     total: 1,
@@ -83,6 +89,7 @@ const pagination = reactive({
     current_page: 1,
     current_page: 1,
     total_pages: 1
     total_pages: 1
 })
 })
+
 const route = useRoute()
 const route = useRoute()
 const params = reactive({
 const params = reactive({
     ...props.get_params
     ...props.get_params
@@ -102,12 +109,17 @@ const selectedRowKeysBuffer = computed({
 
 
 const searchColumns = getSearchColumns()
 const searchColumns = getSearchColumns()
 const pithyColumns = getPithyColumns()
 const pithyColumns = getPithyColumns()
+const batchColumns = getBatchEditColumns()
 
 
 onMounted(() => {
 onMounted(() => {
     if (!props.disable_query_params) {
     if (!props.disable_query_params) {
         Object.assign(params, route.query)
         Object.assign(params, route.query)
     }
     }
     get_list()
     get_list()
+
+    if (props.useSortable) {
+        initSortable()
+    }
 })
 })
 
 
 defineExpose({
 defineExpose({
@@ -123,13 +135,29 @@ function destroy(id: any) {
     })
     })
 }
 }
 
 
-function get_list(page_num = null) {
+function get_list(page_num = null, page_size = 20) {
     loading.value = true
     loading.value = true
     if (page_num) {
     if (page_num) {
         params['page'] = page_num
         params['page'] = page_num
+        params['page_size'] = page_size
     }
     }
-    props.api!.get_list(params).then((r: any) => {
+    props.api!.get_list(params).then(async (r: any) => {
         data_source.value = r.data
         data_source.value = r.data
+        rows_key_index_map.value = {}
+        if (props.useSortable) {
+            function buildIndexMap(data: any, level: number = 0, index: number = 0, total: number[] = []) {
+                if (data && data.length > 0) {
+                    data.forEach((v: any) => {
+                        v.level = level
+                        let current_index = [...total, index++]
+                        rows_key_index_map.value[v.id] = current_index
+                        if (v.children) buildIndexMap(v.children, level + 1, 0, current_index)
+                    })
+                }
+            }
+
+            buildIndexMap(r.data)
+        }
 
 
         if (r.pagination !== undefined) {
         if (r.pagination !== undefined) {
             Object.assign(pagination, r.pagination)
             Object.assign(pagination, r.pagination)
@@ -159,6 +187,10 @@ function stdChange(pagination: any, filters: any, sorter: any) {
     }
     }
 }
 }
 
 
+function expandedTable(keys: any) {
+    expand_keys_list.value = keys
+}
+
 function getSearchColumns() {
 function getSearchColumns() {
     let searchColumns: any = []
     let searchColumns: any = []
     props.columns!.forEach((column: any) => {
     props.columns!.forEach((column: any) => {
@@ -169,6 +201,16 @@ function getSearchColumns() {
     return searchColumns
     return searchColumns
 }
 }
 
 
+function getBatchEditColumns() {
+    let batch: any = []
+    props.columns!.forEach((column: any) => {
+        if (column.batch) {
+            batch.push(column)
+        }
+    })
+    return batch
+}
+
 function getPithyColumns() {
 function getPithyColumns() {
     if (props.pithy) {
     if (props.pithy) {
         return props.columns!.filter((c: any, index: any, columns: any) => {
         return props.columns!.filter((c: any, index: any, columns: any) => {
@@ -187,7 +229,6 @@ function checked(c: any) {
 const crossPageSelect: any = {}
 const crossPageSelect: any = {}
 
 
 async function onSelectChange(_selectedRowKeys: any) {
 async function onSelectChange(_selectedRowKeys: any) {
-
     const page = params.page || 1
     const page = params.page || 1
 
 
     crossPageSelect[page] = await _selectedRowKeys
     crossPageSelect[page] = await _selectedRowKeys
@@ -231,10 +272,10 @@ watch(params, () => {
 })
 })
 
 
 const rowSelection = computed(() => {
 const rowSelection = computed(() => {
-    if (props.selectionType) {
+    if (batchColumns.length > 0 || props.selectionType) {
         return {
         return {
             selectedRowKeys: selectedRowKeysBuffer.value, onChange: onSelectChange,
             selectedRowKeys: selectedRowKeysBuffer.value, onChange: onSelectChange,
-            onSelect: onSelect, type: props.selectionType
+            onSelect: onSelect, type: batchColumns.length > 0 ? 'checkbox' : props.selectionType
         }
         }
     } else {
     } else {
         return null
         return null
@@ -316,6 +357,112 @@ async function export_csv() {
     downloadCsv(header, data,
     downloadCsv(header, data,
             `${$gettext('Export')}-${dayjs().format('YYYYMMDDHHmmss')}.csv`)
             `${$gettext('Export')}-${dayjs().format('YYYYMMDDHHmmss')}.csv`)
 }
 }
+
+const hasSelectedRow = computed(() => {
+    return batchColumns.length > 0 && selectedRowKeysBuffer.value.length > 0
+})
+
+function click_batch_edit() {
+    emit('clickBatchModify', batchColumns, selectedRowKeysBuffer.value)
+}
+
+function getLeastIndex(index: number) {
+    return index >= 1 ? index : 1
+}
+
+function getTargetData(data: any, indexList: number[]): any {
+    let target: any = {children: data}
+    indexList.forEach((index: number) => {
+        target.children[index].parent = target
+        target = target.children[index]
+    })
+    return target
+}
+
+function initSortable() {
+    const table: any = document.querySelector('#std-table tbody')
+    new Sortable(table, {
+        handle: '.ant-table-drag-icon',
+        animation: 150,
+        sort: true,
+        forceFallback: true,
+        setData: function (dataTransfer) {
+            dataTransfer.setData('Text', '')
+        },
+        onStart({item}) {
+            let targetRowKey = Number(item.dataset.rowKey)
+            if (targetRowKey) {
+                expand_keys_list.value = expand_keys_list.value.filter((item: number) => item !== targetRowKey)
+            }
+        },
+        onMove({dragged, related}) {
+            const oldRow: number[] = rows_key_index_map.value?.[Number(dragged.dataset.rowKey)]
+            const newRow: number[] = rows_key_index_map.value?.[Number(related.dataset.rowKey)]
+            if (oldRow.length !== newRow.length || oldRow[oldRow.length - 2] != newRow[newRow.length - 2]) {
+                return false
+            }
+        },
+        async onEnd({item, newIndex, oldIndex}) {
+            if (newIndex === oldIndex) return
+
+            const indexDelta: number = Number(oldIndex) - Number(newIndex)
+            const direction: number = indexDelta > 0 ? +1 : -1
+
+            let rowIndex: number[] = rows_key_index_map.value?.[Number(item.dataset.rowKey)]
+            const newRow = getTargetData(data_source.value, rowIndex)
+            const newRowParent = newRow.parent
+            const level: number = newRow.level
+
+            let currentRowIndex: number[] = [...rows_key_index_map.value?.
+                    [Number(table.children[Number(newIndex) + direction].dataset.rowKey)]]
+            let currentRow: any = getTargetData(data_source.value, currentRowIndex)
+            // Reset parent
+            currentRow.parent = newRow.parent = null
+            newRowParent.children.splice(rowIndex[level], 1)
+            newRowParent.children.splice(currentRowIndex[level], 0, toRaw(newRow))
+
+            let changeIds: number[] = []
+
+            function processChanges(row: any, children: boolean = false, newIndex: number | undefined = undefined) {
+                // Build changes ID list expect new row
+                if (children || newIndex === undefined) changeIds.push(row.id)
+
+                if (newIndex !== undefined)
+                    rows_key_index_map.value[row.id][level] = newIndex
+                else if (children)
+                    rows_key_index_map.value[row.id][level] += direction
+
+                row.parent = null
+                if (row.children) {
+                    row.children.forEach((v: any) => processChanges(v, true, newIndex))
+                }
+            }
+
+            // Replace row index for new row
+            processChanges(newRow, false, currentRowIndex[level])
+            // Rebuild row index maps for changes row
+            for (let i = Number(oldIndex); i != newIndex; i -= direction) {
+                let rowIndex: number[] = rows_key_index_map.value?.[table.children[i].dataset.rowKey]
+                rowIndex[level] += direction
+                processChanges(getTargetData(data_source.value, rowIndex))
+            }
+            console.log('Change row id', newRow.id, 'order', newRow.id, '=>', currentRow.id, ', direction: ', direction,
+                    ', changes IDs:', changeIds)
+
+            props.api!.update_order({
+                target_id: newRow.id,
+                direction: direction,
+                affected_ids: changeIds
+            }).then(() => {
+                message.success($gettext('Updated successfully'))
+            }).catch((e: any) => {
+                message.error(e?.message ?? $gettext('Server error'))
+            })
+        }
+    })
+}
+
+
 </script>
 </script>
 
 
 <template>
 <template>
@@ -327,13 +474,16 @@ async function export_csv() {
                 layout="inline"
                 layout="inline"
         >
         >
             <template #action>
             <template #action>
-                <a-space class="reset-btn">
+                <a-space class="action-btn">
                     <a-button v-if="exportCsv" @click="export_csv" type="primary" ghost>
                     <a-button v-if="exportCsv" @click="export_csv" type="primary" ghost>
                         {{ $gettext('Export') }}
                         {{ $gettext('Export') }}
                     </a-button>
                     </a-button>
                     <a-button @click="reset_search">
                     <a-button @click="reset_search">
                         {{ $gettext('Reset') }}
                         {{ $gettext('Reset') }}
                     </a-button>
                     </a-button>
+                    <a-button v-if="hasSelectedRow" @click="click_batch_edit">
+                        {{ $gettext('Batch Modify') }}
+                    </a-button>
                 </a-space>
                 </a-space>
             </template>
             </template>
         </std-data-entry>
         </std-data-entry>
@@ -347,10 +497,17 @@ async function export_csv() {
                 @change="stdChange"
                 @change="stdChange"
                 :scroll="{ x: scrollX }"
                 :scroll="{ x: scrollX }"
                 :size="size"
                 :size="size"
+                id="std-table"
+                @expandedRowsChange="expandedTable"
+                :expandedRowKeys="expand_keys_list"
         >
         >
             <template
             <template
                     v-slot:bodyCell="{text, record, index, column}"
                     v-slot:bodyCell="{text, record, index, column}"
             >
             >
+                <template v-if="column.handle === true">
+                    <span class="ant-table-drag-icon"><HolderOutlined/></span>
+                    {{ text }}
+                </template>
                 <template v-if="column.dataIndex === 'action'">
                 <template v-if="column.dataIndex === 'action'">
                     <a v-if="props.editable" @click="$emit('clickEdit', record[props.rowKey], record)">
                     <a v-if="props.editable" @click="$emit('clickEdit', record[props.rowKey], record)">
                         {{ props.edit_text || $gettext('Modify') }}
                         {{ props.edit_text || $gettext('Modify') }}
@@ -361,7 +518,7 @@ async function export_csv() {
                         <a-popconfirm
                         <a-popconfirm
                                 :cancelText="$gettext('No')"
                                 :cancelText="$gettext('No')"
                                 :okText="$gettext('OK')"
                                 :okText="$gettext('OK')"
-                                :title="$gettext('Are you sure you want to delete ?')"
+                                :title="$gettext('Are you sure you want to delete?')"
                                 @confirm="destroy(record[rowKey])">
                                 @confirm="destroy(record[rowKey])">
                             <a v-translate>Delete</a>
                             <a v-translate>Delete</a>
                         </a-popconfirm>
                         </a-popconfirm>
@@ -369,7 +526,7 @@ async function export_csv() {
                 </template>
                 </template>
             </template>
             </template>
         </a-table>
         </a-table>
-        <std-pagination :size="size" :pagination="pagination" @changePage="get_list"/>
+        <std-pagination :size="size" :pagination="pagination" @change="get_list"/>
     </div>
     </div>
 </template>
 </template>
 
 
@@ -396,7 +553,7 @@ async function export_csv() {
     }
     }
 }
 }
 
 
-.reset-btn {
+.action-btn {
     // min-height: 50px;
     // min-height: 50px;
     height: 100%;
     height: 100%;
     display: flex;
     display: flex;
@@ -407,3 +564,15 @@ async function export_csv() {
     margin-bottom: 10px;
     margin-bottom: 10px;
 }
 }
 </style>
 </style>
+
+<style lang="less">
+.ant-table-drag-icon {
+    float: left;
+    margin-right: 16px;
+    cursor: grab;
+}
+
+.sortable-ghost *, .sortable-chosen * {
+    cursor: grabbing !important;
+}
+</style>

+ 1 - 0
frontend/src/components/StdDataDisplay/StdTableTransformer.tsx

@@ -2,6 +2,7 @@
 import dayjs from 'dayjs'
 import dayjs from 'dayjs'
 
 
 export interface customRender {
 export interface customRender {
+    value: any
     text: any
     text: any
     record: any
     record: any
     index: any
     index: any

+ 1 - 1
frontend/src/components/StdDataEntry/StdDataEntry.tsx

@@ -11,7 +11,7 @@ export default defineComponent({
             props.dataList.forEach((v: any) => {
             props.dataList.forEach((v: any) => {
                 if (v.edit.type) {
                 if (v.edit.type) {
                     template.push(
                     template.push(
-                        <FormItem label={v.title()}>
+                        <FormItem label={v.title()} extra={v.extra}>
                             {v.edit.type(v.edit, props.dataSource, v.dataIndex)}
                             {v.edit.type(v.edit, props.dataSource, v.dataIndex)}
                         </FormItem>
                         </FormItem>
                     )
                     )

+ 51 - 0
frontend/src/components/StdDataEntry/components/StdPassword.vue

@@ -0,0 +1,51 @@
+<script setup lang="ts">
+import {computed, ref} from 'vue'
+
+const props = defineProps(['value', 'generate', 'placeholder'])
+const emit = defineEmits(['update:value'])
+
+const M_value = computed({
+    get() {
+        return props.value
+    },
+    set(v) {
+        emit('update:value', v)
+    }
+})
+const visibility = ref(false)
+
+function handle_generate() {
+    visibility.value = true
+    M_value.value = 'xxxx'
+
+    const chars = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+    const passwordLength = 12
+    let password = ''
+    for (let i = 0; i <= passwordLength; i++) {
+        const randomNumber = Math.floor(Math.random() * chars.length)
+        password += chars.substring(randomNumber, randomNumber + 1)
+    }
+
+    M_value.value = password
+
+}
+</script>
+
+<template>
+    <a-input-group compact>
+        <a-input-password
+                v-if="!visibility"
+                :class="{compact: generate}"
+                v-model:value="M_value" :placeholoder="placeholder"/>
+        <a-input v-else :class="{compact: generate}" v-model:value="M_value" :placeholoder="placeholder"/>
+        <a-button @click="handle_generate" v-if="generate" type="primary">
+            <translate>Generate</translate>
+        </a-button>
+    </a-input-group>
+</template>
+
+<style scoped>
+.compact {
+    width: calc(100% - 91px)
+}
+</style>

+ 45 - 0
frontend/src/components/StdDataEntry/components/StdSelect.vue

@@ -0,0 +1,45 @@
+<script setup lang="ts">
+import {computed, ref} from 'vue'
+import {SelectProps} from 'ant-design-vue'
+
+const props = defineProps(['value', 'mask'])
+const emit = defineEmits(['update:value'])
+
+const options = computed(() => {
+    const _options = ref<SelectProps['options']>([])
+
+    for (const [key, value] of Object.entries(props.mask)) {
+        const v = value as any
+        _options.value!.push({label: v?.(), value: key})
+    }
+
+    return _options
+})
+
+const _value = computed({
+    get() {
+        let v
+
+        if (typeof props.mask?.[props.value] === 'function') {
+            v = props.mask[props.value]()
+        } else if (typeof props.mask?.[props.value] === 'string') {
+            v = props.mask[props.value]
+        } else {
+            v = props.value
+        }
+        return v
+    },
+    set(v) {
+        emit('update:value', v)
+    }
+})
+</script>
+
+<template>
+    <a-select v-model:value="_value"
+              :options="options.value" style="min-width: 180px"/>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 146 - 0
frontend/src/components/StdDataEntry/components/StdSelector.vue

@@ -0,0 +1,146 @@
+<script setup lang="ts">
+import {computed, onMounted, reactive, ref, watch} from 'vue'
+import StdTable from '@/components/StdDataDisplay/StdTable.vue'
+import gettext from '@/gettext'
+
+const {$gettext} = gettext
+const props = defineProps(['selectedKey', 'value', 'recordValueIndex',
+    'selectionType', 'api', 'columns', 'data_key',
+    'disable_search', 'get_params', 'description'])
+const emit = defineEmits(['update:selectedKey', 'changeSelect'])
+const visible = ref(false)
+const M_value = ref('')
+
+onMounted(() => {
+    init()
+})
+
+const selected = ref([])
+
+const record: any = reactive({})
+
+function init() {
+    if (props.selectedKey && !props.value && props.selectionType === 'radio') {
+        props.api.get(props.selectedKey).then((r: any) => {
+            Object.assign(record, r)
+            M_value.value = r[props.recordValueIndex]
+        })
+    }
+}
+
+function show() {
+    visible.value = true
+}
+
+function onSelect(_selected: any) {
+    selected.value = _selected
+}
+
+function onSelectedRecord(r: any) {
+    Object.assign(record, r)
+}
+
+function ok() {
+    visible.value = false
+    if (props.selectionType == 'radio') {
+        emit('update:selectedKey', selected.value[0])
+    } else {
+        emit('update:selectedKey', selected.value)
+    }
+    M_value.value = record[props.recordValueIndex]
+    emit('changeSelect', record)
+}
+
+watch(props, () => {
+    if (!props?.selectedKey) {
+        M_value.value = ''
+    } else if (props.value) {
+        M_value.value = props.value
+    } else {
+        init()
+    }
+})
+
+const _selectedKey = computed({
+    get() {
+        return props.selectedKey
+    },
+    set(v) {
+        emit('update:selectedKey', v)
+    }
+})
+</script>
+
+<template>
+    <div class="std-selector-container">
+        <div class="std-selector" @click="show()">
+            <a-input v-model="_selectedKey" disabled hidden/>
+            <div class="value">
+                {{ M_value }}
+            </div>
+            <a-modal
+                    :mask="false"
+                    :visible="visible"
+                    :cancel-text="$gettext('Cancel')"
+                    :ok-text="$gettext('OK')"
+                    :title="$gettext('Selector')"
+                    @cancel="visible=false"
+                    @ok="ok()"
+                    :width="800"
+                    destroyOnClose
+            >
+                {{ description }}
+                <std-table
+                        :api="api"
+                        :columns="columns"
+                        :data_key="data_key"
+                        :disable_search="disable_search"
+                        :pithy="true"
+                        :get_params="get_params"
+                        :selectionType="selectionType"
+                        :disable_query_params="true"
+                        @onSelected="onSelect"
+                        @onSelectedRecord="onSelectedRecord"
+                />
+            </a-modal>
+        </div>
+    </div>
+</template>
+
+<style lang="less" scoped>
+.std-selector-container {
+    height: 39.9px;
+    display: flex;
+    align-items: flex-start;
+
+    .std-selector {
+        box-sizing: border-box;
+        font-variant: tabular-nums;
+        list-style: none;
+        font-feature-settings: 'tnum';
+        height: 32px;
+        padding: 4px 11px;
+        color: rgba(0, 0, 0, 0.85);
+        font-size: 14px;
+        line-height: 1.5;
+        background-color: #fff;
+        background-image: none;
+        border: 1px solid #d9d9d9;
+        border-radius: 4px;
+        transition: all 0.3s;
+        margin: 0 10px 0 0;
+        cursor: pointer;
+        min-width: 180px;
+
+        @media (prefers-color-scheme: dark) {
+            background-color: #1e1f20;
+            border: 1px solid #666666;
+            color: rgba(255, 255, 255, 0.99);
+        }
+
+        .value {
+
+        }
+    }
+}
+</style>

+ 21 - 5
frontend/src/components/StdDataEntry/index.tsx

@@ -1,9 +1,9 @@
 import StdDataEntry from './StdDataEntry.js'
 import StdDataEntry from './StdDataEntry.js'
 import {h} from 'vue'
 import {h} from 'vue'
-import {Input, Textarea, InputPassword} from 'ant-design-vue'
-import StdSelector from './compontents/StdSelector.vue'
-import StdSelect from './compontents/StdSelect.vue'
-import StdPassword from './compontents/StdPassword.vue'
+import {Input, Textarea, InputPassword, InputNumber} from 'ant-design-vue'
+import StdSelector from './components/StdSelector.vue'
+import StdSelect from './components/StdSelect.vue'
+import StdPassword from './components/StdPassword.vue'
 
 
 interface IEdit {
 interface IEdit {
     type: Function
     type: Function
@@ -20,6 +20,9 @@ interface IEdit {
     get_params: Object,
     get_params: Object,
     description: string
     description: string
     generate: boolean
     generate: boolean
+    min: number
+    max: number,
+    extra: string
 }
 }
 
 
 function fn(obj: Object, desc: any) {
 function fn(obj: Object, desc: any) {
@@ -55,6 +58,18 @@ function input(edit: IEdit, dataSource: any, dataIndex: any) {
     })
     })
 }
 }
 
 
+function inputNumber(edit: IEdit, dataSource: any, dataIndex: any) {
+    return h(InputNumber, {
+        placeholder: edit.placeholder?.() ?? '',
+        min: edit.min,
+        max: edit.max,
+        value: dataSource?.[dataIndex],
+        'onUpdate:value': value => {
+            dataSource[dataIndex] = value
+        }
+    })
+}
+
 function textarea(edit: IEdit, dataSource: any, dataIndex: any) {
 function textarea(edit: IEdit, dataSource: any, dataIndex: any) {
     return h(Textarea, {
     return h(Textarea, {
         placeholder: edit.placeholder?.() ?? '',
         placeholder: edit.placeholder?.() ?? '',
@@ -101,7 +116,8 @@ export {
     textarea,
     textarea,
     select,
     select,
     selector,
     selector,
-    password
+    password,
+    inputNumber
 }
 }
 
 
 export default StdDataEntry
 export default StdDataEntry

File diff suppressed because it is too large
+ 438 - 208
frontend/yarn.lock


Some files were not shown because too many files changed in this diff