Browse Source

fix(gui-v2): circular component imports replaced with async imports and typehack

pull/3158/head
braks 2 years ago
parent
commit
1c79b77c64
  1. 14
      packages/nc-gui-v2/components/dashboard/settings/app-store/AppInstall.vue
  2. 6
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  3. 34
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  4. 4
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  5. 38
      packages/nc-gui-v2/components/smartsheet/VirtualCell.vue
  6. 47
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  7. 9
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  8. 4
      packages/nc-gui-v2/components/virtual-cell/Formula.vue
  9. 27
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  10. 3
      packages/nc-gui-v2/components/virtual-cell/Lookup.vue
  11. 24
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  12. 2
      packages/nc-gui-v2/components/virtual-cell/Rollup.vue
  13. 19
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  14. 21
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  15. 19
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue

14
packages/nc-gui-v2/components/dashboard/settings/app-store/AppInstall.vue

@ -1,11 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import type { PluginType } from 'nocodb-sdk'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import CloseIcon from '~icons/material-symbols/close-rounded'
import MdiPlusIcon from '~icons/mdi/plus'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { extractSdkResponseErrorMsg, ref, useNuxtApp } from '#imports'
interface Props {
id: string
@ -139,7 +135,7 @@ onMounted(async () => {
class="mr-1 flex items-center justify-center"
:class="[plugin.title === 'SES' ? 'p-2 bg-[#242f3e]' : '']"
>
<img :src="`/${plugin.logo}`" class="h-6" />
<img :alt="plugin.title || 'plugin'" :src="`/${plugin.logo}`" class="h-6" />
</div>
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span>
@ -147,7 +143,7 @@ onMounted(async () => {
<div class="absolute -right-2 -top-0.5">
<a-button type="text" class="!rounded-md mr-1" @click="emits('close')">
<template #icon>
<CloseIcon class="flex mx-auto" />
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>
</a-button>
</div>
@ -194,7 +190,7 @@ onMounted(async () => {
v-if="itemIndex !== 0 && columnIndex === plugin.formDetails.items.length - 1"
class="absolute flex flex-col justify-start mt-2 -right-6 top-0"
>
<MdiDeleteOutlineIcon class="hover:text-red-400 cursor-pointer" @click="deleteFormRow(itemIndex)" />
<MdiDeleteOutline class="hover:text-red-400 cursor-pointer" @click="deleteFormRow(itemIndex)" />
</div>
</a-form-item>
</td>
@ -205,7 +201,7 @@ onMounted(async () => {
<td :colspan="plugin.formDetails.items.length" class="text-center">
<a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addSetting">
<template #icon>
<MdiPlusIcon class="flex mx-auto" />
<MdiPlus class="flex mx-auto" />
</template>
</a-button>
</td>

6
packages/nc-gui-v2/components/smartsheet-header/Cell.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { inject, toRef } from 'vue'
import { ColumnInj, IsFormInj } from '~/context'
import { ColumnInj, IsFormInj, inject, provide, ref, toRef, useUIPermission } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; required?: boolean; hideMenu?: boolean }>()
const hideMenu = toRef(props, 'hideMenu')
@ -24,7 +24,7 @@ function onVisibleChange() {
</script>
<template>
<div class="flex align-center w-full text-xs font-weight-regular">
<div class="flex items-center w-full text-xs text-normal">
<SmartsheetHeaderCellIcon v-if="column" />
<span v-if="column" class="name" style="white-space: nowrap" :title="column.title">{{ column.title }}</span>
<span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>

34
packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue

@ -1,8 +1,19 @@
<script setup lang="ts">
import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import { ColumnInj, IsFormInj, MetaInj } from '~/context'
import { provide, toRef, useMetas } from '#imports'
import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import {
ColumnInj,
IsFormInj,
MetaInj,
computed,
inject,
provide,
ref,
toRef,
useMetas,
useUIPermission,
useVirtualCell,
} from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required?: boolean }>()
@ -48,12 +59,10 @@ const relatedTableTitle = $computed(() => relatedTableMeta?.title)
const childColumn = $computed(() => {
if (relatedTableMeta?.columns) {
if (isRollup.value) {
const ch = relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as RollupType).fk_rollup_column_id)
return ch
return relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as RollupType).fk_rollup_column_id)
}
if (isLookup.value) {
const ch = relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as LookupType).fk_lookup_column_id)
return ch
return relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as LookupType).fk_lookup_column_id)
}
}
return ''
@ -92,11 +101,7 @@ function onVisibleChange() {
</script>
<template>
<div class="d-flex align-center w-full text-xs font-weight-regular">
<!-- <v-tooltip bottom>
<template #activator="{ on }">
todo: bring tooltip
-->
<div class="flex items-center w-full text-xs text-normal">
<SmartsheetHeaderVirtualCellIcon v-if="column" />
<a-tooltip placement="bottom">
@ -105,13 +110,10 @@ function onVisibleChange() {
</template>
<span class="name" style="white-space: nowrap" :title="column.title"> {{ column.title }}</span>
</a-tooltip>
<span v-if="column.rqd || required" class="text-red-500">&nbsp;*</span>
<!-- <span class="caption" v-html="tooltipMsg" /> -->
<span v-if="column.rqd || required" class="text-red-500">&nbsp;*</span>
<!-- </v-tooltip> -->
<template v-if="!hideMenu">
<v-spacer />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
</template>

4
packages/nc-gui-v2/components/smartsheet/Cell.vue

@ -1,9 +1,7 @@
<script setup lang="ts">
import { UITypes } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import { provide, toRef } from 'vue'
import { computed, useColumn, useDebounceFn, useVModel } from '#imports'
import { ActiveCellInj, ColumnInj, EditModeInj } from '~/context'
import { ActiveCellInj, ColumnInj, EditModeInj, computed, provide, toRef, useColumn, useDebounceFn, useVModel } from '#imports'
import { NavigateDir } from '~/lib'
interface Props {

38
packages/nc-gui-v2/components/smartsheet/VirtualCell.vue

@ -1,10 +1,27 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { provide, toRef, useVirtualCell } from '#imports'
import { ActiveCellInj, CellValueInj, ColumnInj, RowInj, provide, toRef, useVirtualCell } from '#imports'
import type { Row } from '~/composables'
import { ActiveCellInj, CellValueInj, ColumnInj, RowInj } from '~/context'
import { NavigateDir } from '~/lib'
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'navigate'])
const HasMany = defineAsyncComponent(() => import('../virtual-cell/HasMany.vue'))
const ManyToMany = defineAsyncComponent(() => import('../virtual-cell/ManyToMany.vue'))
const BelongsTo = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
const Rollup = defineAsyncComponent(() => import('../virtual-cell/HasMany.vue'))
const Formula = defineAsyncComponent(() => import('../virtual-cell/ManyToMany.vue'))
const Count = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
const Lookup = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
interface Props {
column: ColumnType
modelValue: any
@ -12,9 +29,6 @@ interface Props {
active?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'navigate'])
const column = toRef(props, 'column')
const active = toRef(props, 'active', false)
const row = toRef(props, 'row')
@ -33,12 +47,12 @@ const { isLookup, isBt, isRollup, isMm, isHm, isFormula, isCount } = useVirtualC
@keydown.stop.enter.exact="emit('navigate', NavigateDir.NEXT)"
@keydown.stop.shift.enter.exact="emit('navigate', NavigateDir.PREV)"
>
<VirtualCellHasMany v-if="isHm" />
<VirtualCellManyToMany v-else-if="isMm" />
<VirtualCellBelongsTo v-else-if="isBt" />
<VirtualCellRollup v-else-if="isRollup" />
<VirtualCellFormula v-else-if="isFormula" />
<VirtualCellCount v-else-if="isCount" />
<VirtualCellLookup v-else-if="isLookup" />
<HasMany v-if="isHm" />
<ManyToMany v-else-if="isMm" />
<BelongsTo v-else-if="isBt" />
<Rollup v-else-if="isRollup" />
<Formula v-else-if="isFormula" />
<Count v-else-if="isCount" />
<Lookup v-else-if="isLookup" />
</div>
</template>

47
packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue

@ -1,25 +1,32 @@
<script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import Cell from '../Cell.vue'
import VirtualCell from '../VirtualCell.vue'
import Comments from './Comments.vue'
import Header from './Header.vue'
import {
FieldsInj,
IsFormInj,
MetaInj,
NOCO,
computedInject,
extractPkFromRow,
provide,
ref,
toRef,
useNuxtApp,
useProject,
useProvideExpandedFormStore,
useProvideSmartsheetStore,
useVModel,
watch,
} from '#imports'
import { NOCO } from '~/lib'
import { extractPkFromRow } from '~/utils'
import type { Row } from '~/composables'
import { FieldsInj, IsFormInj, MetaInj } from '~/context'
interface Props {
modelValue: string | null
modelValue?: boolean
row: Row
state?: Record<string, any> | null
meta: TableType
@ -28,9 +35,13 @@ interface Props {
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const row = toRef(props, 'row')
const state = toRef(props, 'state')
const meta = toRef(props, 'meta')
const fields = computedInject(FieldsInj, (_fields) => {
@ -45,25 +56,26 @@ provide(MetaInj, meta)
const { commentsDrawer, changedColumns, state: rowState } = useProvideExpandedFormStore(meta, row)
const { $api } = useNuxtApp()
if (props.loadRow) {
const { project } = useProject()
row.value.row = await $api.dbTableRow.read(
NOCO,
project.value.id as string,
meta.value.title,
extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]),
)
row.value.oldRow = { ...row.value.row }
row.value.rowMeta = {}
}
useProvideSmartsheetStore(ref({}) as any, meta)
useProvideSmartsheetStore(ref({}) as Ref<ViewType>, meta)
provide(IsFormInj, ref(true))
// accept as a prop
// const row: Row = { row: {}, rowMeta: {}, oldRow: {} }
watch(
state,
() => {
@ -76,7 +88,15 @@ watch(
{ immediate: true },
)
const isExpanded = useVModel(props, 'modelValue', emits)
const isExpanded = useVModel(props, 'modelValue', emits, {
defaultValue: false,
})
</script>
<script lang="ts">
export default {
name: 'ExpandedForm',
}
</script>
<template>
@ -86,13 +106,14 @@ const isExpanded = useVModel(props, 'modelValue', emits)
<div class="flex h-full nc-form-wrapper items-stretch min-h-[70vh]">
<div class="flex-grow overflow-auto scrollbar-thin-primary">
<div class="w-[500px] mx-auto">
<div v-for="col in fields" :key="col.title" class="mt-2 py-2" :class="`nc-expand-col-${col.title}`">
<div v-for="col of fields" :key="col.title" class="mt-2 py-2" :class="`nc-expand-col-${col.title}`">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
<div class="!bg-white rounded px-1 min-h-[35px] flex align-center mt-2">
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<SmartsheetCell
<VirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<Cell
v-else
v-model="row.row[col.title]"
:column="col"

9
packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue

@ -1,8 +1,6 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListItems from './components/ListItems.vue'
import {
ActiveCellInj,
CellValueInj,
@ -10,6 +8,7 @@ import {
EditModeInj,
ReloadViewDataHookInj,
RowInj,
defineAsyncComponent,
inject,
ref,
useProvideLTARStore,
@ -18,6 +17,10 @@ import {
import MdiArrowExpand from '~icons/mdi/arrow-expand'
import MdiPlus from '~icons/mdi/plus'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'))
const column = inject(ColumnInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)!
@ -65,7 +68,7 @@ const unlinkRef = async (rec: Record<string, any>) => {
<template>
<div class="flex w-full chips-wrapper align-center" :class="{ active }">
<div class="chips d-flex align-center flex-grow">
<template v-if="value">
<template v-if="value && relatedTablePrimaryValueProp">
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
</template>
</div>

4
packages/nc-gui-v2/components/virtual-cell/Formula.vue

@ -1,7 +1,5 @@
<script lang="ts" setup>
import { computed, inject, ref, useProject } from '#imports'
import { CellValueInj, ColumnInj } from '~/context'
import { handleTZ, replaceUrlsWithLink } from '~/utils'
import { CellValueInj, ColumnInj, computed, handleTZ, inject, ref, replaceUrlsWithLink, useProject } from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck
const column: any = inject(ColumnInj)

27
packages/nc-gui-v2/components/virtual-cell/HasMany.vue

@ -1,11 +1,26 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, EditModeInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
import {
CellValueInj,
ColumnInj,
EditModeInj,
IsFormInj,
ReloadViewDataHookInj,
RowInj,
computed,
defineAsyncComponent,
inject,
ref,
useProvideLTARStore,
useSmartsheetRowStoreOrThrow,
} from '#imports'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'))
const ListChildItems = defineAsyncComponent(() => import('./components/ListChildItems.vue'))
const column = inject(ColumnInj)!
@ -85,7 +100,9 @@ const unlinkRef = async (rec: Record<string, any>) => {
/>
</div>
</template>
<ListItems v-model="listItemsDlg" />
<ListChildItems
v-model="childListDlg"
@attach-record="

3
packages/nc-gui-v2/components/virtual-cell/Lookup.vue

@ -2,8 +2,7 @@
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, MetaInj, ReadonlyInj } from '~/context'
import { computed, inject, provide, useColumn, useMetas } from '#imports'
import { CellValueInj, ColumnInj, MetaInj, ReadonlyInj, computed, inject, provide, useColumn, useMetas } from '#imports'
const { metas, getMeta } = useMetas()

24
packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue

@ -1,11 +1,25 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, EditModeInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
import {
CellValueInj,
ColumnInj,
EditModeInj,
IsFormInj,
ReloadViewDataHookInj,
RowInj,
computed,
inject,
ref,
useProvideLTARStore,
useSmartsheetRowStoreOrThrow,
} from '#imports'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'))
const ListChildItems = defineAsyncComponent(() => import('./components/ListChildItems.vue'))
const column = inject(ColumnInj)!

2
packages/nc-gui-v2/components/virtual-cell/Rollup.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { CellValueInj } from '~/context'
import { CellValueInj, inject } from '#imports'
const value = inject(CellValueInj)
</script>

19
packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { inject, ref, useLTARStoreOrThrow } from '#imports'
import { ActiveCellInj, EditModeInj, IsFormInj } from '~/context'
<script lang="ts" setup>
import { ActiveCellInj, EditModeInj, IsFormInj, defineAsyncComponent, inject, ref, useLTARStoreOrThrow } from '#imports'
interface Props {
value?: string | number | boolean
@ -11,6 +10,8 @@ const { value, item } = defineProps<Props>()
const emit = defineEmits(['unlink'])
const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/expanded-form/index.vue'))
const { relatedTableMeta } = useLTARStoreOrThrow()!
const editEnabled = inject(EditModeInj)!
@ -22,6 +23,12 @@ const isForm = inject(IsFormInj)!
const expandedFormDlg = ref(false)
</script>
<script lang="ts">
export default {
name: 'ItemChip',
}
</script>
<template>
<div
class="group py-1 px-2 flex align-center gap-1 bg-gray-200/50 hover:bg-gray-200 rounded-[20px]"
@ -34,14 +41,16 @@ const expandedFormDlg = ref(false)
<MdiCloseThick class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click.stop="emit('unlink')" />
</div>
<SmartsheetExpandedForm
v-if="expandedFormDlg && editEnabled"
<Suspense>
<ExpandedForm
v-if="editEnabled"
v-model="expandedFormDlg"
:row="{ row: item }"
:meta="relatedTableMeta"
load-row
use-meta-fields
/>
</Suspense>
</div>
</template>

21
packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue

@ -1,12 +1,23 @@
<script lang="ts" setup>
import { Empty, Modal } from 'ant-design-vue'
import type { ColumnType } from 'nocodb-sdk'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel, watch } from '#imports'
import { ColumnInj, EditModeInj, IsFormInj } from '~/context'
import {
ColumnInj,
EditModeInj,
IsFormInj,
computed,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useVModel,
watch,
} from '#imports'
const props = defineProps<{ modelValue?: boolean }>()
const emit = defineEmits(['update:modelValue', 'attachRecord'])
const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/expanded-form/index.vue'))
const vModel = useVModel(props, 'modelValue', emit)
const isForm = inject(IsFormInj, ref(false))
@ -120,14 +131,16 @@ const expandedFormRow = ref()
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
<SmartsheetExpandedForm
v-if="expandedFormDlg && expandedFormRow"
<Suspense>
<ExpandedForm
v-if="expandedFormRow"
v-model="expandedFormDlg"
:row="{ row: expandedFormRow }"
:meta="relatedTableMeta"
load-row
use-meta-fields
/>
</Suspense>
</component>
</template>

19
packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue

@ -2,13 +2,24 @@
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { Empty } from 'ant-design-vue'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel } from '#imports'
import { ColumnInj } from '~/context'
import {
ColumnInj,
computed,
defineAsyncComponent,
inject,
ref,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useVModel,
watch,
} from '#imports'
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits(['update:modelValue', 'addNewRecord'])
const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/expanded-form/index.vue'))
const vModel = useVModel(props, 'modelValue', emit)
const column = inject(ColumnInj)
@ -118,7 +129,8 @@ const newRowState = computed(() => {
</template>
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<SmartsheetExpandedForm
<Suspense>
<ExpandedForm
v-if="expandedFormDlg"
v-model="expandedFormDlg"
:meta="relatedTableMeta"
@ -126,6 +138,7 @@ const newRowState = computed(() => {
:state="newRowState"
use-meta-fields
/>
</Suspense>
</div>
</a-modal>
</template>

Loading…
Cancel
Save