frappe-ui 0.1.98 → 0.1.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/components/FormControl.vue +5 -1
- package/src/components/{ListView.story.vue → ListView/ListView.story.vue} +11 -12
- package/src/components/ListView/ListView.vue +7 -0
- package/src/components/Tabs/TabList.vue +81 -0
- package/src/components/Tabs/TabPanel.vue +17 -0
- package/src/components/Tabs/Tabs.story.md +97 -0
- package/src/components/{Tabs.story.vue → Tabs/Tabs.story.vue} +34 -9
- package/src/components/Tabs/Tabs.vue +52 -0
- package/src/components/TextInput.vue +2 -0
- package/src/data-fetching/docStore.ts +7 -6
- package/src/data-fetching/useCall/useCall.test.ts +1 -1
- package/src/data-fetching/useCall/useCall.ts +12 -6
- package/src/data-fetching/useDoc/useDoc.ts +52 -8
- package/src/data-fetching/useDoctype/useDoctype.ts +51 -1
- package/src/data-fetching/useList/listStore.ts +7 -0
- package/src/data-fetching/useList/types.ts +9 -9
- package/src/data-fetching/useList/useList.test.ts +3 -1
- package/src/data-fetching/useList/useList.ts +147 -42
- package/src/data-fetching/useNewDoc/useNewDoc.ts +17 -1
- package/src/data-fetching/utils.ts +3 -3
- package/src/index.js +3 -1
- package/src/mocks/handlers.ts +1 -4
- package/src/components/Tabs.story.md +0 -15
- package/src/components/Tabs.vue +0 -110
- /package/src/components/{ListView.story.md → ListView/ListView.story.md} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.101",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"typescript": "^5.0.2"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"vue": ">=3.
|
|
63
|
+
"vue": ">=3.5.0",
|
|
64
64
|
"vue-router": "^4.1.6"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
@@ -29,7 +29,11 @@
|
|
|
29
29
|
:id="id"
|
|
30
30
|
v-bind="{ ...controlAttrs, size }"
|
|
31
31
|
/>
|
|
32
|
-
<TextInput
|
|
32
|
+
<TextInput
|
|
33
|
+
v-else
|
|
34
|
+
:id="id"
|
|
35
|
+
v-bind="{ ...controlAttrs, type, size, required }"
|
|
36
|
+
>
|
|
33
37
|
<template #prefix v-if="$slots.prefix">
|
|
34
38
|
<slot name="prefix" />
|
|
35
39
|
</template>
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { reactive, h, ref } from 'vue'
|
|
3
|
-
import Avatar from '
|
|
4
|
-
import Badge from '
|
|
5
|
-
import { Button } from '
|
|
6
|
-
import FeatherIcon from '
|
|
7
|
-
import ListHeader from './
|
|
8
|
-
import ListHeaderItem from './
|
|
9
|
-
import ListRow from './
|
|
10
|
-
import ListRowItem from './
|
|
11
|
-
import ListRows from './
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import ListView from './ListView/ListView.vue'
|
|
3
|
+
import Avatar from '../Avatar.vue'
|
|
4
|
+
import Badge from '../Badge.vue'
|
|
5
|
+
import { Button } from '../Button'
|
|
6
|
+
import FeatherIcon from '../FeatherIcon.vue'
|
|
7
|
+
import ListHeader from './ListHeader.vue'
|
|
8
|
+
import ListHeaderItem from './ListHeaderItem.vue'
|
|
9
|
+
import ListRow from './ListRow.vue'
|
|
10
|
+
import ListRowItem from './ListRowItem.vue'
|
|
11
|
+
import ListRows from './ListRows.vue'
|
|
12
|
+
import ListSelectBanner from './ListSelectBanner.vue'
|
|
13
|
+
import ListView from './ListView.vue'
|
|
15
14
|
|
|
16
15
|
const state = reactive({
|
|
17
16
|
selectable: true,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TabList
|
|
3
|
+
class="relative flex"
|
|
4
|
+
:class="
|
|
5
|
+
vertical
|
|
6
|
+
? 'flex-col border-r overflow-y-auto'
|
|
7
|
+
: 'gap-7.5 border-b overflow-x-auto items-center px-5'
|
|
8
|
+
"
|
|
9
|
+
>
|
|
10
|
+
<Tab
|
|
11
|
+
ref="tabRef"
|
|
12
|
+
as="template"
|
|
13
|
+
v-for="(tab, i) in tabs"
|
|
14
|
+
:key="i"
|
|
15
|
+
v-slot="{ selected }"
|
|
16
|
+
class="focus:outline-none focus:transition-none"
|
|
17
|
+
>
|
|
18
|
+
<slot v-bind="{ tab, selected }">
|
|
19
|
+
<button
|
|
20
|
+
class="flex items-center gap-1.5 text-base text-ink-gray-5 duration-300 ease-in-out hover:text-ink-gray-9"
|
|
21
|
+
:class="[
|
|
22
|
+
selected ? 'text-ink-gray-9' : '',
|
|
23
|
+
vertical
|
|
24
|
+
? 'py-2.5 px-4 border-r border-transparent hover:border-outline-gray-3'
|
|
25
|
+
: 'py-3 border-b border-transparent hover:border-outline-gray-3',
|
|
26
|
+
]"
|
|
27
|
+
>
|
|
28
|
+
<component v-if="tab.icon" :is="tab.icon" class="size-4" />
|
|
29
|
+
{{ tab.label }}
|
|
30
|
+
</button>
|
|
31
|
+
</slot>
|
|
32
|
+
</Tab>
|
|
33
|
+
<div
|
|
34
|
+
ref="indicator"
|
|
35
|
+
class="tab-indicator absolute bg-surface-gray-7"
|
|
36
|
+
:class="[vertical ? 'right-0 w-px' : 'bottom-0 h-px', transitionClass]"
|
|
37
|
+
/>
|
|
38
|
+
</TabList>
|
|
39
|
+
</template>
|
|
40
|
+
<script setup>
|
|
41
|
+
import { TabList, Tab } from '@headlessui/vue'
|
|
42
|
+
import { ref, watch, computed, onMounted, nextTick, inject } from 'vue'
|
|
43
|
+
|
|
44
|
+
const tabIndex = inject('tabIndex')
|
|
45
|
+
const tabs = inject('tabs')
|
|
46
|
+
const vertical = inject('vertical')
|
|
47
|
+
|
|
48
|
+
const tabRef = ref([])
|
|
49
|
+
const indicator = ref(null)
|
|
50
|
+
const tabsLength = computed(() => tabs.value?.length)
|
|
51
|
+
|
|
52
|
+
const transitionClass = ref('')
|
|
53
|
+
|
|
54
|
+
function moveIndicator(index) {
|
|
55
|
+
if (index >= tabsLength.value) {
|
|
56
|
+
index = tabsLength.value - 1
|
|
57
|
+
}
|
|
58
|
+
const selectedTab = tabRef.value[index].el
|
|
59
|
+
if (vertical) {
|
|
60
|
+
indicator.value.style.height = `${selectedTab.offsetHeight}px`
|
|
61
|
+
indicator.value.style.top = `${selectedTab.offsetTop}px`
|
|
62
|
+
} else {
|
|
63
|
+
indicator.value.style.width = `${selectedTab.offsetWidth}px`
|
|
64
|
+
indicator.value.style.left = `${selectedTab.offsetLeft}px`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
watch(tabIndex, (index) => {
|
|
69
|
+
if (index >= tabsLength.value) {
|
|
70
|
+
tabIndex.value = tabsLength.value - 1
|
|
71
|
+
}
|
|
72
|
+
transitionClass.value = 'transition-all duration-300 ease-in-out'
|
|
73
|
+
nextTick(() => moveIndicator(index))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
onMounted(() => {
|
|
77
|
+
nextTick(() => moveIndicator(tabIndex.value))
|
|
78
|
+
// Fix for indicator not moving on initial load
|
|
79
|
+
setTimeout(() => moveIndicator(tabIndex.value), 100)
|
|
80
|
+
})
|
|
81
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TabPanels class="flex flex-1 overflow-hidden">
|
|
3
|
+
<TabPanel
|
|
4
|
+
class="flex flex-1 flex-col overflow-y-auto focus:outline-none"
|
|
5
|
+
v-for="(tab, i) in tabs"
|
|
6
|
+
:key="i"
|
|
7
|
+
>
|
|
8
|
+
<slot v-bind="{ tab }" />
|
|
9
|
+
</TabPanel>
|
|
10
|
+
</TabPanels>
|
|
11
|
+
</template>
|
|
12
|
+
<script setup>
|
|
13
|
+
import { TabPanels, TabPanel } from '@headlessui/vue'
|
|
14
|
+
import { inject } from 'vue'
|
|
15
|
+
|
|
16
|
+
const tabs = inject('tabs')
|
|
17
|
+
</script>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
## Props
|
|
2
|
+
|
|
3
|
+
### tabs
|
|
4
|
+
|
|
5
|
+
It is an array of objects which contains the following attributes:
|
|
6
|
+
|
|
7
|
+
1. `label` is the name of the tab, it is required.
|
|
8
|
+
2. `icon` is the icon to be shown in the tab, it accept component and it is
|
|
9
|
+
optional.
|
|
10
|
+
3. You can add more attributes which can be used for custom rendering in the tab
|
|
11
|
+
header or content.
|
|
12
|
+
|
|
13
|
+
### v-model
|
|
14
|
+
|
|
15
|
+
It is used to set the active tab or change the active tab. It is required.
|
|
16
|
+
|
|
17
|
+
### vertical
|
|
18
|
+
|
|
19
|
+
It is used to show the tabs vertically. It is optional.
|
|
20
|
+
|
|
21
|
+
### as
|
|
22
|
+
|
|
23
|
+
You can set it to `div` to wrap tabs in a `div`. It can be any valid HTML tag.
|
|
24
|
+
This is useful to control the layout of the tabs. It is optional.
|
|
25
|
+
|
|
26
|
+
1. `as="div"` or any valid HTML tag
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<div>
|
|
30
|
+
<!-- container div -->
|
|
31
|
+
<div>
|
|
32
|
+
<div active>Tab 1</div>
|
|
33
|
+
<div>Tab 2</div>
|
|
34
|
+
<div>Tab 3</div>
|
|
35
|
+
</div>
|
|
36
|
+
<div>
|
|
37
|
+
<div active>Content 1</div>
|
|
38
|
+
<div>Content 2</div>
|
|
39
|
+
<div>Content 3</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
2. `as` is not set
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<div>
|
|
48
|
+
<div active>Tab 1</div>
|
|
49
|
+
<div>Tab 2</div>
|
|
50
|
+
<div>Tab 3</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div>
|
|
53
|
+
<div active>Content 1</div>
|
|
54
|
+
<div>Content 2</div>
|
|
55
|
+
<div>Content 3</div>
|
|
56
|
+
</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Slots
|
|
60
|
+
|
|
61
|
+
1. **tab-item:** You can use this slot to render custom tab items. It is
|
|
62
|
+
optional.
|
|
63
|
+
2. **tab-panel:** You can use this slot to render custom tab panels. It is
|
|
64
|
+
required. Example:
|
|
65
|
+
|
|
66
|
+
```vue
|
|
67
|
+
<Tabs v-model="tabIndex" :tabs="tabs">
|
|
68
|
+
<template #tab-item="{ tab, selected }">
|
|
69
|
+
<div :class="{ 'text-gray-900 font-semibold': selected }">
|
|
70
|
+
<span>{{ tab.label }}</span>
|
|
71
|
+
<span>{{ tab.icon }}</span>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
<template #tab-panel="{ tab }">
|
|
75
|
+
<div>{{ tab.content }}</div>
|
|
76
|
+
</template>
|
|
77
|
+
</Tabs>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Layout Customization
|
|
81
|
+
|
|
82
|
+
You can customize the layout of the tabs by using `<TabList />` and `<TabPanels />`
|
|
83
|
+
components.
|
|
84
|
+
|
|
85
|
+
```vue
|
|
86
|
+
<Tabs v-model="tabIndex" :tabs="tabs">
|
|
87
|
+
<TabList v-slot="{ tab, selected }">
|
|
88
|
+
<div :class="{ 'text-gray-900 font-semibold': selected }">
|
|
89
|
+
<span>{{ tab.label }}</span>
|
|
90
|
+
<span>{{ tab.icon }}</span>
|
|
91
|
+
</div>
|
|
92
|
+
</TabList>
|
|
93
|
+
<TabPanel v-slot="{ tab }">
|
|
94
|
+
<div>{{ tab.content }}</div>
|
|
95
|
+
</TabPanel>
|
|
96
|
+
</Tabs>
|
|
97
|
+
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { h, reactive } from 'vue'
|
|
3
3
|
import Tabs from './Tabs.vue'
|
|
4
|
-
import FeatherIcon from '
|
|
4
|
+
import FeatherIcon from '../FeatherIcon.vue'
|
|
5
5
|
const state = reactive({
|
|
6
6
|
index: 0,
|
|
7
7
|
tabs_without_icon: [
|
|
@@ -48,20 +48,45 @@ const state = reactive({
|
|
|
48
48
|
<Story :layout="{ type: 'grid', width: '80%' }">
|
|
49
49
|
<Variant title="Without Icon">
|
|
50
50
|
<Tabs
|
|
51
|
-
|
|
51
|
+
as="div"
|
|
52
|
+
class="border rounded"
|
|
52
53
|
v-model="state.index"
|
|
53
54
|
:tabs="state.tabs_without_icon"
|
|
54
55
|
>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
<template #tab-panel="{ tab }">
|
|
57
|
+
<div class="p-5">
|
|
58
|
+
{{ tab.content }}
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
58
61
|
</Tabs>
|
|
59
62
|
</Variant>
|
|
60
63
|
<Variant title="With Icon">
|
|
61
|
-
<Tabs
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
<Tabs
|
|
65
|
+
as="div"
|
|
66
|
+
class="border rounded"
|
|
67
|
+
v-model="state.index"
|
|
68
|
+
:tabs="state.tabs_with_icon"
|
|
69
|
+
>
|
|
70
|
+
<template #tab-panel="{ tab }">
|
|
71
|
+
<div class="p-5">
|
|
72
|
+
{{ tab.content }}
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
</Tabs>
|
|
76
|
+
</Variant>
|
|
77
|
+
<Variant title="Vertical Tabs">
|
|
78
|
+
<Tabs
|
|
79
|
+
as="div"
|
|
80
|
+
class="border rounded"
|
|
81
|
+
v-model="state.index"
|
|
82
|
+
:tabs="state.tabs_with_icon"
|
|
83
|
+
vertical
|
|
84
|
+
>
|
|
85
|
+
<template #tab-panel="{ tab }">
|
|
86
|
+
<div class="p-5">
|
|
87
|
+
{{ tab.content }}
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
65
90
|
</Tabs>
|
|
66
91
|
</Variant>
|
|
67
92
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TabGroup
|
|
3
|
+
v-bind="
|
|
4
|
+
as !== 'template'
|
|
5
|
+
? {
|
|
6
|
+
as,
|
|
7
|
+
class: ['flex flex-1 overflow-hidden', vertical ? '' : 'flex-col '],
|
|
8
|
+
}
|
|
9
|
+
: {}
|
|
10
|
+
"
|
|
11
|
+
:defaultIndex="tabIndex"
|
|
12
|
+
:selectedIndex="tabIndex"
|
|
13
|
+
@change="(idx) => (tabIndex = idx)"
|
|
14
|
+
>
|
|
15
|
+
<slot>
|
|
16
|
+
<TabList v-slot="{ tab, selected }">
|
|
17
|
+
<slot name="tab-item" v-bind="{ tab, selected }" />
|
|
18
|
+
</TabList>
|
|
19
|
+
<TabPanel v-slot="{ tab }">
|
|
20
|
+
<slot name="tab-panel" v-bind="{ tab }" />
|
|
21
|
+
</TabPanel>
|
|
22
|
+
</slot>
|
|
23
|
+
</TabGroup>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup>
|
|
27
|
+
import TabList from './TabList.vue'
|
|
28
|
+
import TabPanel from './TabPanel.vue'
|
|
29
|
+
import { TabGroup } from '@headlessui/vue'
|
|
30
|
+
import { provide } from 'vue'
|
|
31
|
+
|
|
32
|
+
const props = defineProps({
|
|
33
|
+
as: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: 'template',
|
|
36
|
+
},
|
|
37
|
+
tabs: {
|
|
38
|
+
type: Array,
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
vertical: {
|
|
42
|
+
type: Boolean,
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const tabIndex = defineModel()
|
|
48
|
+
|
|
49
|
+
provide('tabIndex', tabIndex)
|
|
50
|
+
provide('tabs', props.tabs)
|
|
51
|
+
provide('vertical', props.vertical)
|
|
52
|
+
</script>
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
:disabled="disabled"
|
|
23
23
|
:id="id"
|
|
24
24
|
:value="modelValue"
|
|
25
|
+
:required="required"
|
|
25
26
|
@input="handleChange"
|
|
26
27
|
@change="handleChange"
|
|
27
28
|
v-bind="attrsWithoutClassStyle"
|
|
@@ -53,6 +54,7 @@ interface TextInputProps {
|
|
|
53
54
|
id?: string
|
|
54
55
|
modelValue?: string | number
|
|
55
56
|
debounce?: number
|
|
57
|
+
required?: boolean
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
defineOptions({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ref, ref, MaybeRefOrGetter, toValue } from 'vue'
|
|
2
2
|
import { idbStore } from './idbStore'
|
|
3
3
|
|
|
4
4
|
type Doc = {
|
|
@@ -49,11 +49,8 @@ class DocStore {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
getDoc(
|
|
53
|
-
|
|
54
|
-
name: string | Ref<string> | ComputedRef<string>,
|
|
55
|
-
): Ref<Doc | null> {
|
|
56
|
-
const nameStr = unref(name)
|
|
52
|
+
getDoc(doctype: string, name: MaybeRefOrGetter<string>): Ref<Doc | null> {
|
|
53
|
+
const nameStr = toValue(name)
|
|
57
54
|
if (!doctype || !nameStr) {
|
|
58
55
|
throw new Error('doctype and name are required')
|
|
59
56
|
}
|
|
@@ -114,6 +111,10 @@ class DocStore {
|
|
|
114
111
|
await this.cleanup(key)
|
|
115
112
|
}
|
|
116
113
|
|
|
114
|
+
removeDoc(doctype: string, name: string) {
|
|
115
|
+
return this.invalidateDoc(doctype, name)
|
|
116
|
+
}
|
|
117
|
+
|
|
117
118
|
private getKey(doctype: string, name: string): DocKey {
|
|
118
119
|
return `${doctype.trim()}/${name.trim()}` as DocKey
|
|
119
120
|
}
|
|
@@ -88,11 +88,15 @@ export function useCall<TResponse, TParams extends BasicParams = undefined>(
|
|
|
88
88
|
canAbort,
|
|
89
89
|
aborted,
|
|
90
90
|
abort,
|
|
91
|
-
execute,
|
|
91
|
+
execute: _execute,
|
|
92
92
|
onFetchResponse,
|
|
93
93
|
onFetchError,
|
|
94
94
|
} = result
|
|
95
95
|
|
|
96
|
+
function execute(): Promise<TResponse | null> {
|
|
97
|
+
return _execute().then((r) => data.value)
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
onFetchResponse(() => {
|
|
97
101
|
resolve()
|
|
98
102
|
promise.value = makePromise()
|
|
@@ -107,7 +111,9 @@ export function useCall<TResponse, TParams extends BasicParams = undefined>(
|
|
|
107
111
|
if (beforeSubmit) {
|
|
108
112
|
beforeSubmit(params)
|
|
109
113
|
}
|
|
110
|
-
|
|
114
|
+
if (params != null) {
|
|
115
|
+
submitParams.value = params
|
|
116
|
+
}
|
|
111
117
|
if (!refetch) {
|
|
112
118
|
return execute()
|
|
113
119
|
}
|
|
@@ -122,14 +128,14 @@ export function useCall<TResponse, TParams extends BasicParams = undefined>(
|
|
|
122
128
|
|
|
123
129
|
const _data = computed(() => {
|
|
124
130
|
if (normalizedCacheKey && (out.loading || !out.isFinished)) {
|
|
125
|
-
let
|
|
131
|
+
let cachedData = cachedResponse.value as TResponse
|
|
126
132
|
if (transform) {
|
|
127
|
-
let returnValue = transform(
|
|
133
|
+
let returnValue = transform(cachedData)
|
|
128
134
|
if (returnValue !== undefined) {
|
|
129
|
-
|
|
135
|
+
cachedData = returnValue
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
|
-
return
|
|
138
|
+
return cachedData
|
|
133
139
|
}
|
|
134
140
|
return data.value
|
|
135
141
|
})
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
reactive,
|
|
4
|
+
readonly,
|
|
5
|
+
Ref,
|
|
6
|
+
MaybeRefOrGetter,
|
|
7
|
+
toValue,
|
|
8
|
+
} from 'vue'
|
|
2
9
|
import { UseFetchOptions } from '@vueuse/core'
|
|
3
10
|
import { useFrappeFetch } from '../useFrappeFetch'
|
|
4
11
|
import { useCall } from '../useCall/useCall'
|
|
@@ -24,7 +31,7 @@ interface DocMethodOption<T = any>
|
|
|
24
31
|
|
|
25
32
|
interface UseDocOptions {
|
|
26
33
|
doctype: string
|
|
27
|
-
name:
|
|
34
|
+
name: MaybeRefOrGetter<string>
|
|
28
35
|
baseUrl?: string
|
|
29
36
|
methods?: Record<string, string | DocMethodOption>
|
|
30
37
|
immediate?: boolean
|
|
@@ -42,15 +49,28 @@ export function useDoc<TDoc extends { name: string }, TMethods = {}>(
|
|
|
42
49
|
} = options
|
|
43
50
|
|
|
44
51
|
const url = computed(
|
|
45
|
-
() => `${baseUrl}/api/v2/document/${doctype}/${
|
|
52
|
+
() => `${baseUrl}/api/v2/document/${doctype}/${toValue(name)}`,
|
|
46
53
|
)
|
|
47
54
|
|
|
55
|
+
type SuccessCallback = (doc: TDoc) => void
|
|
56
|
+
const successCallbacks: SuccessCallback[] = []
|
|
57
|
+
const triggerSuccessCallbacks = (doc: TDoc) => {
|
|
58
|
+
for (let cb of successCallbacks) {
|
|
59
|
+
try {
|
|
60
|
+
cb(doc)
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error('Error in onSuccess hook:', e)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
const fetchOptions: UseFetchOptions = {
|
|
49
68
|
immediate,
|
|
50
69
|
refetch: true,
|
|
51
70
|
afterFetch(ctx) {
|
|
52
71
|
docStore.setDoc({ doctype, ...ctx.data })
|
|
53
72
|
listStore.updateRow(doctype, ctx.data)
|
|
73
|
+
triggerSuccessCallbacks(ctx.data)
|
|
54
74
|
return ctx
|
|
55
75
|
},
|
|
56
76
|
}
|
|
@@ -80,13 +100,13 @@ export function useDoc<TDoc extends { name: string }, TMethods = {}>(
|
|
|
80
100
|
|
|
81
101
|
let callOptions: UseCallOptions = {
|
|
82
102
|
immediate: false,
|
|
83
|
-
refetch:
|
|
103
|
+
refetch: false,
|
|
84
104
|
method: 'POST',
|
|
85
105
|
...option,
|
|
86
106
|
baseUrl,
|
|
87
107
|
url: computed(
|
|
88
108
|
() =>
|
|
89
|
-
`/api/v2/document/${doctype}/${
|
|
109
|
+
`/api/v2/document/${doctype}/${toValue(name)}/method/${option.name}`,
|
|
90
110
|
),
|
|
91
111
|
}
|
|
92
112
|
|
|
@@ -95,17 +115,30 @@ export function useDoc<TDoc extends { name: string }, TMethods = {}>(
|
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
let setValue = useCall<TDoc, Partial<TDoc>>({
|
|
98
|
-
|
|
99
|
-
refetch: false,
|
|
118
|
+
url: computed(() => `/api/v2/document/${doctype}/${toValue(name)}`),
|
|
100
119
|
method: 'PUT',
|
|
101
120
|
baseUrl,
|
|
102
|
-
|
|
121
|
+
immediate: false,
|
|
122
|
+
refetch: false,
|
|
103
123
|
onSuccess(data) {
|
|
104
124
|
docStore.setDoc({ doctype, ...data })
|
|
105
125
|
listStore.updateRow(doctype, data)
|
|
106
126
|
},
|
|
107
127
|
})
|
|
108
128
|
|
|
129
|
+
type DeleteResponse = 'ok'
|
|
130
|
+
const delete_ = useCall<DeleteResponse>({
|
|
131
|
+
url: computed(() => `/api/v2/document/${doctype}/${toValue(name)}`),
|
|
132
|
+
method: 'DELETE',
|
|
133
|
+
baseUrl,
|
|
134
|
+
immediate: false,
|
|
135
|
+
refetch: false,
|
|
136
|
+
onSuccess() {
|
|
137
|
+
docStore.removeDoc(doctype, toValue(name))
|
|
138
|
+
listStore.removeRow(doctype, toValue(name))
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
|
|
109
142
|
const doc = docStore.getDoc(doctype, name) as Ref<TDoc | null>
|
|
110
143
|
|
|
111
144
|
let out = reactive({
|
|
@@ -121,6 +154,17 @@ export function useDoc<TDoc extends { name: string }, TMethods = {}>(
|
|
|
121
154
|
reload: execute,
|
|
122
155
|
abort,
|
|
123
156
|
setValue,
|
|
157
|
+
delete: delete_,
|
|
158
|
+
onSuccess: (callback: SuccessCallback) => {
|
|
159
|
+
successCallbacks.push(callback)
|
|
160
|
+
return () => {
|
|
161
|
+
// unsubscribe function
|
|
162
|
+
const index = successCallbacks.indexOf(callback)
|
|
163
|
+
if (index > -1) {
|
|
164
|
+
successCallbacks.splice(index, 1)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
124
168
|
...docMethods,
|
|
125
169
|
})
|
|
126
170
|
|
|
@@ -13,14 +13,16 @@ export function useDoctype<T>(
|
|
|
13
13
|
) {
|
|
14
14
|
const insert = useInsert<T>(doctype, options)
|
|
15
15
|
const delete_ = useDelete(doctype, options)
|
|
16
|
-
const runDocMethod = useRunDocMethod(doctype, options)
|
|
17
16
|
const setValue = useSetValue<T>(doctype, options)
|
|
17
|
+
const runDocMethod = useRunDocMethod(doctype, options)
|
|
18
|
+
const runMethod = useRunMethod(doctype, options)
|
|
18
19
|
|
|
19
20
|
return reactive({
|
|
20
21
|
insert,
|
|
21
22
|
delete: delete_,
|
|
22
23
|
setValue,
|
|
23
24
|
runDocMethod,
|
|
25
|
+
runMethod,
|
|
24
26
|
})
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -107,6 +109,54 @@ function useRunDocMethod(doctype: string, options: UseDoctypeOptions = {}) {
|
|
|
107
109
|
} as RunDocMethodReturnValue)
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
function useRunMethod(doctype: string, options: UseDoctypeOptions = {}) {
|
|
113
|
+
let { baseUrl = '' } = options
|
|
114
|
+
let url = ref(`/api/v2/method/${doctype}/<method>`)
|
|
115
|
+
|
|
116
|
+
interface RunMethodParams {
|
|
117
|
+
method: string
|
|
118
|
+
validate?: () => string | void
|
|
119
|
+
params?: Record<string, any>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type RunMethodReturnValue = ReturnType<typeof useCall> & {
|
|
123
|
+
submit: (params: RunMethodParams) => Promise<any>
|
|
124
|
+
isLoading: (method: string) => boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let runMethod = useCall<any, RunMethodParams['params']>({
|
|
128
|
+
url,
|
|
129
|
+
method: 'POST',
|
|
130
|
+
immediate: false,
|
|
131
|
+
baseUrl,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
let validateError = ref<Error | null>(null)
|
|
135
|
+
|
|
136
|
+
return reactive({
|
|
137
|
+
...runMethod,
|
|
138
|
+
error: computed(() => validateError.value || runMethod.error),
|
|
139
|
+
submit: ({ method, validate, params }: RunMethodParams) => {
|
|
140
|
+
url.value = `/api/v2/method/${doctype}/${method}`
|
|
141
|
+
if (validate) {
|
|
142
|
+
const errorMessage = validate()
|
|
143
|
+
if (errorMessage) {
|
|
144
|
+
validateError.value = new Error(errorMessage)
|
|
145
|
+
return Promise.reject(validateError.value)
|
|
146
|
+
} else {
|
|
147
|
+
validateError.value = null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return runMethod.submit(params)
|
|
151
|
+
},
|
|
152
|
+
isLoading: (method: string) => {
|
|
153
|
+
return (
|
|
154
|
+
runMethod.loading && url.value === `/api/v2/method/${doctype}/${method}`
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
} as RunMethodReturnValue)
|
|
158
|
+
}
|
|
159
|
+
|
|
110
160
|
function useSetValue<T>(doctype: string, options: UseDoctypeOptions = {}) {
|
|
111
161
|
let { baseUrl = '' } = options
|
|
112
162
|
let url = ref(`/api/v2/document/${doctype}/<name>`)
|
|
@@ -30,6 +30,13 @@ class ListStore {
|
|
|
30
30
|
})
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
removeRow(doctype: string, name: string) {
|
|
34
|
+
this.ensureList(doctype)
|
|
35
|
+
this.byDocType[doctype].forEach((list) => {
|
|
36
|
+
list.removeRow(name)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
ensureList(docType: string) {
|
|
34
41
|
if (!this.byDocType[docType]) {
|
|
35
42
|
this.byDocType[docType] = []
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { Reactive, Ref } from 'vue'
|
|
1
|
+
import { MaybeRefOrGetter, Reactive, Ref } from 'vue'
|
|
2
2
|
import { CacheKey } from '../useCall/types'
|
|
3
3
|
|
|
4
4
|
export type Field = string
|
|
5
|
+
export type LinkField = `${Field}.${Field}` | `${Field}.${Field} as ${string}`
|
|
6
|
+
export type FieldWithAlias = `${Field} as ${string}`
|
|
5
7
|
|
|
6
8
|
export type ChildTableField = {
|
|
7
9
|
[key: string]: Field[]
|
|
@@ -13,7 +15,7 @@ export type FilterValue =
|
|
|
13
15
|
| boolean
|
|
14
16
|
| [string, string | number | boolean | Ref<string | number | boolean>]
|
|
15
17
|
|
|
16
|
-
export interface
|
|
18
|
+
export interface Filters {
|
|
17
19
|
[key: Field]: FilterValue
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -25,9 +27,9 @@ export type OrderBy =
|
|
|
25
27
|
|
|
26
28
|
export interface UseListOptions<T> {
|
|
27
29
|
doctype: string
|
|
28
|
-
fields?: Array<keyof T | ChildTableField>
|
|
29
|
-
filters?:
|
|
30
|
-
orderBy?: OrderBy
|
|
30
|
+
fields?: Array<keyof T | ChildTableField | LinkField | FieldWithAlias | '*'>
|
|
31
|
+
filters?: MaybeRefOrGetter<Filters>
|
|
32
|
+
orderBy?: MaybeRefOrGetter<OrderBy>
|
|
31
33
|
start?: number
|
|
32
34
|
limit?: number
|
|
33
35
|
groupBy?: Field
|
|
@@ -38,12 +40,10 @@ export interface UseListOptions<T> {
|
|
|
38
40
|
immediate?: boolean
|
|
39
41
|
refetch?: boolean
|
|
40
42
|
baseUrl?: string
|
|
43
|
+
url?: `/${string}`
|
|
41
44
|
transform?: (data: T[]) => T[]
|
|
42
45
|
onSuccess?: (data: T[]) => void
|
|
43
46
|
onError?: (error: Error) => void
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
export
|
|
47
|
-
result: T[]
|
|
48
|
-
has_next_page: boolean
|
|
49
|
-
}
|
|
49
|
+
export type UseListResponse<T> = T[]
|
|
@@ -27,7 +27,7 @@ describe('useList', () => {
|
|
|
27
27
|
// Verify initial state
|
|
28
28
|
expect(users.data).toBe(null)
|
|
29
29
|
expect(users.error).toBe(null)
|
|
30
|
-
expect(users.hasNextPage).toBe(
|
|
30
|
+
expect(users.hasNextPage).toBe(true)
|
|
31
31
|
expect(typeof users.fetch).toBe('function')
|
|
32
32
|
|
|
33
33
|
// fetch
|
|
@@ -64,6 +64,8 @@ describe('useList', () => {
|
|
|
64
64
|
expect(users.start).toBe(2)
|
|
65
65
|
expect(users.hasPreviousPage).toBe(true)
|
|
66
66
|
expect(users.data).toStrictEqual([
|
|
67
|
+
{ name: 'User1', email: 'user1@example.com' },
|
|
68
|
+
{ name: 'User2', email: 'user2@example.com' },
|
|
67
69
|
{ name: 'User3', email: 'user3@example.com' },
|
|
68
70
|
{ name: 'User4', email: 'user4@example.com' },
|
|
69
71
|
])
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
MaybeRefOrGetter,
|
|
4
|
+
reactive,
|
|
5
|
+
readonly,
|
|
6
|
+
Ref,
|
|
7
|
+
ref,
|
|
8
|
+
toValue,
|
|
9
|
+
} from 'vue'
|
|
2
10
|
import {
|
|
3
11
|
AfterFetchContext,
|
|
4
12
|
OnFetchErrorContext,
|
|
@@ -10,6 +18,7 @@ import { parseFilters, makeGetParams, normalizeCacheKey } from '../utils'
|
|
|
10
18
|
import { UseListOptions, UseListResponse } from './types'
|
|
11
19
|
import { idbStore } from '../idbStore'
|
|
12
20
|
import { listStore } from './listStore'
|
|
21
|
+
import { docStore } from '../docStore'
|
|
13
22
|
|
|
14
23
|
export function useList<T extends { name: string }>(
|
|
15
24
|
options: UseListOptions<T>,
|
|
@@ -29,34 +38,47 @@ export function useList<T extends { name: string }>(
|
|
|
29
38
|
refetch = true,
|
|
30
39
|
cacheKey,
|
|
31
40
|
baseUrl = '',
|
|
41
|
+
url = '',
|
|
32
42
|
transform,
|
|
33
43
|
} = options
|
|
34
44
|
|
|
35
45
|
const _start = ref(start || 0)
|
|
36
46
|
const _limit = ref(limit || 20)
|
|
37
47
|
|
|
38
|
-
const
|
|
39
|
-
const parsedFilters = parseFilters(filters
|
|
48
|
+
const _url = computed(() => {
|
|
49
|
+
const parsedFilters = parseFilters(filters ? toValue(filters) : {})
|
|
50
|
+
const _fields = fields ? toValue(fields) : []
|
|
40
51
|
const params = makeGetParams({
|
|
41
|
-
fields:
|
|
52
|
+
fields: _fields.length ? JSON.stringify(_fields) : null,
|
|
42
53
|
filters: parsedFilters ? JSON.stringify(parsedFilters) : null,
|
|
43
|
-
order_by: orderBy,
|
|
54
|
+
order_by: toValue(orderBy),
|
|
44
55
|
start: _start.value,
|
|
45
56
|
limit: _limit.value,
|
|
46
57
|
group_by: groupBy,
|
|
47
58
|
parent: parent,
|
|
48
59
|
debug: debug,
|
|
49
60
|
})
|
|
61
|
+
if (url) {
|
|
62
|
+
return `${baseUrl}${url}?${params}`
|
|
63
|
+
}
|
|
50
64
|
return `${baseUrl}/api/v2/document/${doctype}?${params}`
|
|
51
65
|
})
|
|
52
66
|
|
|
67
|
+
const allData: Ref<T[] | null> = ref(null)
|
|
68
|
+
const hasNextPage = ref(true)
|
|
69
|
+
const hasPreviousPage = computed(() => _start.value > 0)
|
|
70
|
+
|
|
53
71
|
const fetchOptions: UseFetchOptions = {
|
|
54
72
|
immediate,
|
|
55
73
|
refetch,
|
|
56
|
-
initialData: initialData
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
initialData: initialData || null,
|
|
75
|
+
afterFetch: handleAfterFetch<T>({
|
|
76
|
+
...options,
|
|
77
|
+
allData,
|
|
78
|
+
_start,
|
|
79
|
+
_limit,
|
|
80
|
+
hasNextPage,
|
|
81
|
+
}),
|
|
60
82
|
onFetchError: handleFetchError<T>(options),
|
|
61
83
|
}
|
|
62
84
|
|
|
@@ -69,7 +91,7 @@ export function useList<T extends { name: string }>(
|
|
|
69
91
|
aborted,
|
|
70
92
|
abort,
|
|
71
93
|
execute,
|
|
72
|
-
} = useFrappeFetch<UseListResponse<T>>(
|
|
94
|
+
} = useFrappeFetch<UseListResponse<T>>(_url, fetchOptions).get()
|
|
73
95
|
|
|
74
96
|
let normalizedCacheKey = normalizeCacheKey(cacheKey, 'useList')
|
|
75
97
|
let cachedResponse = ref<UseListResponse<T> | null>(null)
|
|
@@ -79,24 +101,16 @@ export function useList<T extends { name: string }>(
|
|
|
79
101
|
let data = cachedResponse.value
|
|
80
102
|
if (data) {
|
|
81
103
|
if (transform) {
|
|
82
|
-
let returnValue = transform(data
|
|
104
|
+
let returnValue = transform(data as T[])
|
|
83
105
|
if (returnValue !== undefined) {
|
|
84
106
|
return returnValue
|
|
85
107
|
}
|
|
86
108
|
}
|
|
87
|
-
return data
|
|
109
|
+
return data
|
|
88
110
|
}
|
|
89
111
|
}
|
|
90
|
-
return
|
|
91
|
-
})
|
|
92
|
-
const hasNextPage = computed(() => {
|
|
93
|
-
if (normalizedCacheKey && (out.loading || !out.isFinished)) {
|
|
94
|
-
let data = cachedResponse.value
|
|
95
|
-
return data?.has_next_page ?? false
|
|
96
|
-
}
|
|
97
|
-
return data.value?.has_next_page ?? false
|
|
112
|
+
return allData.value
|
|
98
113
|
})
|
|
99
|
-
const hasPreviousPage = computed(() => _start.value > 0)
|
|
100
114
|
|
|
101
115
|
if (normalizedCacheKey) {
|
|
102
116
|
idbStore.get(normalizedCacheKey).then((data) => {
|
|
@@ -119,24 +133,31 @@ export function useList<T extends { name: string }>(
|
|
|
119
133
|
type PartialDoc = Partial<T extends { name: string } ? T : { name: string }>
|
|
120
134
|
|
|
121
135
|
const updateRow = (doc: PartialDoc) => {
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
136
|
+
if (allData.value == null) return
|
|
137
|
+
let changed = false
|
|
138
|
+
for (let row of allData.value) {
|
|
139
|
+
if (doc.name && doc.name === row.name) {
|
|
140
|
+
for (let key in doc) {
|
|
141
|
+
if (key in row) {
|
|
142
|
+
row[key] = doc[key]
|
|
143
|
+
changed = true
|
|
131
144
|
}
|
|
132
|
-
break
|
|
133
145
|
}
|
|
134
|
-
|
|
135
|
-
if (changed) {
|
|
136
|
-
data.value.result = [...data.value.result]
|
|
137
|
-
triggerRef(data)
|
|
146
|
+
break
|
|
138
147
|
}
|
|
139
148
|
}
|
|
149
|
+
if (changed) {
|
|
150
|
+
allData.value = [...allData.value]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const removeRow = (name: string) => {
|
|
155
|
+
if (allData.value == null) return
|
|
156
|
+
const index = allData.value.findIndex((row) => row.name === name)
|
|
157
|
+
if (index > -1) {
|
|
158
|
+
allData.value.splice(index, 1)
|
|
159
|
+
allData.value = [...allData.value]
|
|
160
|
+
}
|
|
140
161
|
}
|
|
141
162
|
|
|
142
163
|
const insert = useCall<T, Partial<T>>({
|
|
@@ -149,6 +170,26 @@ export function useList<T extends { name: string }>(
|
|
|
149
170
|
},
|
|
150
171
|
})
|
|
151
172
|
|
|
173
|
+
const setValueUrl = ref(`/api/v2/document/${doctype}/<name>`)
|
|
174
|
+
|
|
175
|
+
const setValue = useCall<T, Partial<T>>({
|
|
176
|
+
url: setValueUrl,
|
|
177
|
+
method: 'PUT',
|
|
178
|
+
baseUrl,
|
|
179
|
+
immediate: false,
|
|
180
|
+
refetch: false,
|
|
181
|
+
beforeSubmit(params) {
|
|
182
|
+
if (params?.name) {
|
|
183
|
+
setValueUrl.value = `/api/v2/document/${doctype}/${params.name}`
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
onSuccess(data) {
|
|
187
|
+
docStore.setDoc({ doctype, ...data })
|
|
188
|
+
listStore.updateRow(doctype, data)
|
|
189
|
+
if (refetch) execute()
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
|
|
152
193
|
let deleteUrl = ref(`/api/v2/document/${doctype}/<name>`)
|
|
153
194
|
type DeleteResponse = 'ok'
|
|
154
195
|
type DeleteParams = { name: string }
|
|
@@ -167,9 +208,45 @@ export function useList<T extends { name: string }>(
|
|
|
167
208
|
},
|
|
168
209
|
})
|
|
169
210
|
|
|
211
|
+
function useEdit(name: MaybeRefOrGetter<string>) {
|
|
212
|
+
if (!allData.value) {
|
|
213
|
+
throw new Error('Data not found')
|
|
214
|
+
}
|
|
215
|
+
let row = allData.value.find((row) => row.name === toValue(name))
|
|
216
|
+
if (!row) {
|
|
217
|
+
throw new Error(`Couldn't find row with name ${toValue(name)}`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let originalRow = JSON.parse(JSON.stringify(row))
|
|
221
|
+
let doc = reactive(row)
|
|
222
|
+
|
|
223
|
+
const setValue = useCall<T, Partial<T>>({
|
|
224
|
+
url: `/api/v2/document/${doctype}/${toValue(name)}`,
|
|
225
|
+
method: 'PUT',
|
|
226
|
+
baseUrl,
|
|
227
|
+
immediate: false,
|
|
228
|
+
refetch: false,
|
|
229
|
+
onSuccess(data) {
|
|
230
|
+
docStore.setDoc({ doctype, ...data })
|
|
231
|
+
listStore.updateRow(doctype, data)
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
doc,
|
|
237
|
+
reset: () => {
|
|
238
|
+
for (let key in originalRow) {
|
|
239
|
+
doc[key] = originalRow[key]
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
setValue,
|
|
243
|
+
update: () => setValue.submit(doc),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
170
247
|
let out = reactive({
|
|
171
248
|
data: result,
|
|
172
|
-
hasNextPage,
|
|
249
|
+
hasNextPage: readonly(hasNextPage),
|
|
173
250
|
hasPreviousPage,
|
|
174
251
|
start: readonly(_start),
|
|
175
252
|
limit: readonly(_limit),
|
|
@@ -179,7 +256,7 @@ export function useList<T extends { name: string }>(
|
|
|
179
256
|
isFinished,
|
|
180
257
|
canAbort,
|
|
181
258
|
aborted,
|
|
182
|
-
url,
|
|
259
|
+
url: _url,
|
|
183
260
|
abort,
|
|
184
261
|
next,
|
|
185
262
|
previous,
|
|
@@ -187,8 +264,11 @@ export function useList<T extends { name: string }>(
|
|
|
187
264
|
fetch: execute,
|
|
188
265
|
reload: execute,
|
|
189
266
|
updateRow,
|
|
267
|
+
removeRow,
|
|
190
268
|
insert,
|
|
269
|
+
setValue,
|
|
191
270
|
delete: delete_,
|
|
271
|
+
edit: useEdit,
|
|
192
272
|
})
|
|
193
273
|
|
|
194
274
|
listStore.addList(doctype, out)
|
|
@@ -196,19 +276,44 @@ export function useList<T extends { name: string }>(
|
|
|
196
276
|
return out
|
|
197
277
|
}
|
|
198
278
|
|
|
199
|
-
function handleAfterFetch<T>({
|
|
279
|
+
function handleAfterFetch<T extends { name: string }>({
|
|
200
280
|
transform,
|
|
201
281
|
onSuccess,
|
|
202
282
|
cacheKey,
|
|
203
|
-
|
|
283
|
+
allData,
|
|
284
|
+
_start,
|
|
285
|
+
_limit,
|
|
286
|
+
hasNextPage,
|
|
287
|
+
}: UseListOptions<T> & {
|
|
288
|
+
allData: Ref<T[] | null>
|
|
289
|
+
_start: Ref<number>
|
|
290
|
+
_limit: Ref<number>
|
|
291
|
+
hasNextPage: Ref<boolean>
|
|
292
|
+
}) {
|
|
204
293
|
return function (ctx: AfterFetchContext) {
|
|
294
|
+
let resultData = ctx.data
|
|
295
|
+
if (resultData[0]?.name) {
|
|
296
|
+
resultData = resultData.map((item) => ({
|
|
297
|
+
...item,
|
|
298
|
+
name: String(item.name),
|
|
299
|
+
}))
|
|
300
|
+
}
|
|
301
|
+
hasNextPage.value = resultData.length < _limit.value ? false : true
|
|
302
|
+
|
|
205
303
|
if (transform) {
|
|
206
|
-
const returnValue = transform(
|
|
304
|
+
const returnValue = transform(resultData)
|
|
207
305
|
if (Array.isArray(returnValue)) {
|
|
208
|
-
|
|
306
|
+
resultData = returnValue
|
|
209
307
|
}
|
|
210
308
|
}
|
|
211
309
|
|
|
310
|
+
if (_start.value === 0) {
|
|
311
|
+
allData.value = resultData as T[]
|
|
312
|
+
} else {
|
|
313
|
+
allData.value = [...(allData.value || []), ...resultData]
|
|
314
|
+
}
|
|
315
|
+
ctx.data = allData.value
|
|
316
|
+
|
|
212
317
|
let normalizedCacheKey = normalizeCacheKey(cacheKey, 'useList')
|
|
213
318
|
if (normalizedCacheKey) {
|
|
214
319
|
idbStore.set(normalizedCacheKey, ctx.data)
|
|
@@ -216,7 +321,7 @@ function handleAfterFetch<T>({
|
|
|
216
321
|
|
|
217
322
|
if (onSuccess) {
|
|
218
323
|
try {
|
|
219
|
-
onSuccess(
|
|
324
|
+
onSuccess(allData.value)
|
|
220
325
|
} catch (e) {
|
|
221
326
|
console.error('Error in onSuccess hook:', e)
|
|
222
327
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { reactive, unref } from 'vue'
|
|
2
2
|
import { useCall } from '../useCall/useCall'
|
|
3
3
|
import { UseCallOptions } from '../useCall/types'
|
|
4
|
+
import { docStore } from '../docStore'
|
|
4
5
|
|
|
5
6
|
type UseNewDocOptions = Omit<
|
|
6
7
|
UseCallOptions,
|
|
@@ -18,7 +19,11 @@ export function useNewDoc<T extends object>(
|
|
|
18
19
|
) {
|
|
19
20
|
let doc = reactive<NewDoc<T>>(initialValues)
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
type DocResponse = T & {
|
|
23
|
+
name: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const out = useCall<DocResponse>({
|
|
22
27
|
url: `/api/v2/document/${doctype}`,
|
|
23
28
|
method: 'POST',
|
|
24
29
|
params() {
|
|
@@ -34,8 +39,19 @@ export function useNewDoc<T extends object>(
|
|
|
34
39
|
...options,
|
|
35
40
|
})
|
|
36
41
|
|
|
42
|
+
function submit() {
|
|
43
|
+
return out
|
|
44
|
+
.submit()
|
|
45
|
+
.then((doc) =>
|
|
46
|
+
docStore
|
|
47
|
+
.setDoc({ doctype, ...(doc as DocResponse) })
|
|
48
|
+
.then(() => docStore.getDoc(doctype, doc.name.toString()).value as T),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
return reactive({
|
|
38
53
|
...out,
|
|
54
|
+
submit,
|
|
39
55
|
doc,
|
|
40
56
|
})
|
|
41
57
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MaybeRef, unref } from 'vue'
|
|
2
|
-
import {
|
|
2
|
+
import { Filters } from './useList/types'
|
|
3
3
|
|
|
4
4
|
export function makeGetParams(params: Record<string, any>) {
|
|
5
5
|
let url = new URLSearchParams()
|
|
@@ -16,8 +16,8 @@ export function isEmptyObject(obj: any) {
|
|
|
16
16
|
return Object.keys(obj).length === 0 && obj.constructor === Object
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function parseFilters(filters:
|
|
20
|
-
let parsedFilters:
|
|
19
|
+
export function parseFilters(filters: Filters): Filters | null {
|
|
20
|
+
let parsedFilters: Filters = {}
|
|
21
21
|
for (let key in filters) {
|
|
22
22
|
let value = filters[key]
|
|
23
23
|
if (Array.isArray(value)) {
|
package/src/index.js
CHANGED
|
@@ -32,7 +32,9 @@ export { default as Select } from './components/Select.vue'
|
|
|
32
32
|
export { default as Spinner } from './components/Spinner.vue'
|
|
33
33
|
export { default as Switch } from './components/Switch.vue'
|
|
34
34
|
export { default as TabButtons } from './components/TabButtons.vue'
|
|
35
|
-
export { default as Tabs } from './components/Tabs.vue'
|
|
35
|
+
export { default as Tabs } from './components/Tabs/Tabs.vue'
|
|
36
|
+
export { default as TabList } from './components/Tabs/TabList.vue'
|
|
37
|
+
export { default as TabPanel } from './components/Tabs/TabPanel.vue'
|
|
36
38
|
export { default as TextInput } from './components/TextInput.vue'
|
|
37
39
|
export { default as Textarea } from './components/Textarea.vue'
|
|
38
40
|
export {
|
package/src/mocks/handlers.ts
CHANGED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
## Props
|
|
2
|
-
|
|
3
|
-
### Tabs
|
|
4
|
-
|
|
5
|
-
It is an array of objects which contains the following attributes:
|
|
6
|
-
|
|
7
|
-
1. `label` is the name of the tab, it is required.
|
|
8
|
-
2. `icon` is the icon to be shown in the tab, it accept component and it is
|
|
9
|
-
optional.
|
|
10
|
-
3. You can add more attributes which can be used for custom rendering in the tab
|
|
11
|
-
header or content.
|
|
12
|
-
|
|
13
|
-
## v-model
|
|
14
|
-
|
|
15
|
-
It is used to set the active tab or change the active tab. It is required.
|
package/src/components/Tabs.vue
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<TabGroup
|
|
3
|
-
as="div"
|
|
4
|
-
class="flex flex-1 flex-col overflow-y-hidden"
|
|
5
|
-
:style="`height: calc(100vh - ${tabListRef?.$el.offsetTop}px)`"
|
|
6
|
-
:defaultIndex="changedIndex"
|
|
7
|
-
:selectedIndex="changedIndex"
|
|
8
|
-
@change="(idx) => (changedIndex = idx)"
|
|
9
|
-
>
|
|
10
|
-
<TabList
|
|
11
|
-
ref="tabListRef"
|
|
12
|
-
class="relative flex items-center gap-7.5 overflow-x-auto border-b px-5"
|
|
13
|
-
:class="tablistClass"
|
|
14
|
-
>
|
|
15
|
-
<Tab
|
|
16
|
-
ref="tabRef"
|
|
17
|
-
as="template"
|
|
18
|
-
v-for="(tab, i) in tabs"
|
|
19
|
-
:key="i"
|
|
20
|
-
v-slot="{ selected }"
|
|
21
|
-
class="focus:outline-none focus:transition-none"
|
|
22
|
-
>
|
|
23
|
-
<slot name="tab" v-bind="{ tab, selected }">
|
|
24
|
-
<button
|
|
25
|
-
class="flex items-center gap-1.5 border-b border-transparent py-3 text-base text-ink-gray-5 duration-300 ease-in-out hover:border-outline-gray-3 hover:text-ink-gray-9"
|
|
26
|
-
:class="{ 'text-ink-gray-9': selected }"
|
|
27
|
-
>
|
|
28
|
-
<component v-if="tab.icon" :is="tab.icon" class="size-4" />
|
|
29
|
-
{{ tab.label }}
|
|
30
|
-
</button>
|
|
31
|
-
</slot>
|
|
32
|
-
</Tab>
|
|
33
|
-
<div
|
|
34
|
-
ref="indicator"
|
|
35
|
-
class="tab-indicator absolute bottom-0 h-px bg-surface-gray-7"
|
|
36
|
-
:class="transitionClass"
|
|
37
|
-
/>
|
|
38
|
-
</TabList>
|
|
39
|
-
<TabPanels class="flex flex-1 overflow-hidden" :class="tabPanelClass">
|
|
40
|
-
<TabPanel
|
|
41
|
-
class="flex flex-1 flex-col overflow-y-auto focus:outline-none"
|
|
42
|
-
v-for="(tab, i) in tabs"
|
|
43
|
-
:key="i"
|
|
44
|
-
>
|
|
45
|
-
<slot v-bind="{ tab }" />
|
|
46
|
-
</TabPanel>
|
|
47
|
-
</TabPanels>
|
|
48
|
-
</TabGroup>
|
|
49
|
-
</template>
|
|
50
|
-
|
|
51
|
-
<script setup>
|
|
52
|
-
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
|
53
|
-
import { ref, watch, computed, onMounted, nextTick } from 'vue'
|
|
54
|
-
|
|
55
|
-
const props = defineProps({
|
|
56
|
-
tabs: {
|
|
57
|
-
type: Array,
|
|
58
|
-
required: true,
|
|
59
|
-
},
|
|
60
|
-
modelValue: {
|
|
61
|
-
type: Number,
|
|
62
|
-
default: 0,
|
|
63
|
-
},
|
|
64
|
-
tablistClass: {
|
|
65
|
-
type: String,
|
|
66
|
-
default: '',
|
|
67
|
-
},
|
|
68
|
-
tabPanelClass: {
|
|
69
|
-
type: String,
|
|
70
|
-
default: '',
|
|
71
|
-
},
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const emit = defineEmits(['update:modelValue'])
|
|
75
|
-
|
|
76
|
-
const changedIndex = computed({
|
|
77
|
-
get: () => props.modelValue,
|
|
78
|
-
set: (index) => emit('update:modelValue', index),
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
const tabListRef = ref(null)
|
|
82
|
-
const tabRef = ref([])
|
|
83
|
-
const indicator = ref(null)
|
|
84
|
-
const tabsLength = computed(() => props.tabs?.length)
|
|
85
|
-
|
|
86
|
-
const transitionClass = ref('')
|
|
87
|
-
|
|
88
|
-
function moveIndicator(index) {
|
|
89
|
-
if (index >= tabsLength.value) {
|
|
90
|
-
index = tabsLength.value - 1
|
|
91
|
-
}
|
|
92
|
-
const selectedTab = tabRef.value[index].el
|
|
93
|
-
indicator.value.style.width = `${selectedTab.offsetWidth}px`
|
|
94
|
-
indicator.value.style.left = `${selectedTab.offsetLeft}px`
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
watch(changedIndex, (index) => {
|
|
98
|
-
if (index >= tabsLength.value) {
|
|
99
|
-
changedIndex.value = tabsLength.value - 1
|
|
100
|
-
}
|
|
101
|
-
transitionClass.value = 'transition-all duration-300 ease-in-out'
|
|
102
|
-
nextTick(() => moveIndicator(index))
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
onMounted(() => {
|
|
106
|
-
nextTick(() => moveIndicator(changedIndex.value))
|
|
107
|
-
// Fix for indicator not moving on initial load
|
|
108
|
-
setTimeout(() => moveIndicator(changedIndex.value), 100)
|
|
109
|
-
})
|
|
110
|
-
</script>
|
|
File without changes
|