Browse Source

[feat] Support Multi-Version Workflows (#11990)

Co-authored-by: hobo.l <hobo.l@binance.com>
Co-authored-by: crazywoola <427733928@qq.com>
Warren Chen 3 months ago
parent
commit
901028f1e8

+ 28 - 2
api/controllers/console/app/workflow.py

@@ -2,7 +2,7 @@ import json
 import logging
 
 from flask import abort, request
-from flask_restful import Resource, marshal_with, reqparse  # type: ignore
+from flask_restful import Resource, inputs, marshal_with, reqparse  # type: ignore
 from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
 
 import services
@@ -14,7 +14,7 @@ from controllers.console.wraps import account_initialization_required, setup_req
 from core.app.apps.base_app_queue_manager import AppQueueManager
 from core.app.entities.app_invoke_entities import InvokeFrom
 from factories import variable_factory
-from fields.workflow_fields import workflow_fields
+from fields.workflow_fields import workflow_fields, workflow_pagination_fields
 from fields.workflow_run_fields import workflow_run_node_execution_fields
 from libs import helper
 from libs.helper import TimestampField, uuid_value
@@ -440,6 +440,31 @@ class WorkflowConfigApi(Resource):
         }
 
 
+class PublishedAllWorkflowApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
+    @marshal_with(workflow_pagination_fields)
+    def get(self, app_model: App):
+        """
+        Get published workflows
+        """
+        if not current_user.is_editor:
+            raise Forbidden()
+
+        parser = reqparse.RequestParser()
+        parser.add_argument("page", type=inputs.int_range(1, 99999), required=False, default=1, location="args")
+        parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
+        args = parser.parse_args()
+        page = args.get("page")
+        limit = args.get("limit")
+        workflow_service = WorkflowService()
+        workflows, has_more = workflow_service.get_all_published_workflow(app_model=app_model, page=page, limit=limit)
+
+        return {"items": workflows, "page": page, "limit": limit, "has_more": has_more}
+
+
 api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft")
 api.add_resource(WorkflowConfigApi, "/apps/<uuid:app_id>/workflows/draft/config")
 api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
@@ -454,6 +479,7 @@ api.add_resource(
     WorkflowDraftRunIterationNodeApi, "/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run"
 )
 api.add_resource(PublishedWorkflowApi, "/apps/<uuid:app_id>/workflows/publish")
+api.add_resource(PublishedAllWorkflowApi, "/apps/<uuid:app_id>/workflows")
 api.add_resource(DefaultBlockConfigsApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
 api.add_resource(
     DefaultBlockConfigApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>"

+ 8 - 0
api/fields/workflow_fields.py

@@ -45,6 +45,7 @@ workflow_fields = {
     "graph": fields.Raw(attribute="graph_dict"),
     "features": fields.Raw(attribute="features_dict"),
     "hash": fields.String(attribute="unique_hash"),
+    "version": fields.String(attribute="version"),
     "created_by": fields.Nested(simple_account_fields, attribute="created_by_account"),
     "created_at": TimestampField,
     "updated_by": fields.Nested(simple_account_fields, attribute="updated_by_account", allow_null=True),
@@ -61,3 +62,10 @@ workflow_partial_fields = {
     "updated_by": fields.String,
     "updated_at": TimestampField,
 }
+
+workflow_pagination_fields = {
+    "items": fields.List(fields.Nested(workflow_fields), attribute="items"),
+    "page": fields.Integer,
+    "limit": fields.Integer(attribute="limit"),
+    "has_more": fields.Boolean(attribute="has_more"),
+}

+ 24 - 0
api/services/workflow_service.py

@@ -5,6 +5,8 @@ from datetime import UTC, datetime
 from typing import Any, Optional, cast
 from uuid import uuid4
 
+from sqlalchemy import desc
+
 from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager
 from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager
 from core.model_runtime.utils.encoders import jsonable_encoder
@@ -76,6 +78,28 @@ class WorkflowService:
 
         return workflow
 
+    def get_all_published_workflow(self, app_model: App, page: int, limit: int) -> tuple[list[Workflow], bool]:
+        """
+        Get published workflow with pagination
+        """
+        if not app_model.workflow_id:
+            return [], False
+
+        workflows = (
+            db.session.query(Workflow)
+            .filter(Workflow.app_id == app_model.id)
+            .order_by(desc(Workflow.version))
+            .offset((page - 1) * limit)
+            .limit(limit + 1)
+            .all()
+        )
+
+        has_more = len(workflows) > limit
+        if has_more:
+            workflows = workflows[:-1]
+
+        return workflows, has_more
+
     def sync_draft_workflow(
         self,
         *,

+ 29 - 20
web/app/components/workflow/header/index.tsx

@@ -20,6 +20,7 @@ import type { StartNodeType } from '../nodes/start/types'
 import {
   useChecklistBeforePublish,
   useIsChatMode,
+  useNodesInteractions,
   useNodesReadOnly,
   useNodesSyncDraft,
   useWorkflowMode,
@@ -35,6 +36,7 @@ import RestoringTitle from './restoring-title'
 import ViewHistory from './view-history'
 import ChatVariableButton from './chat-variable-button'
 import EnvButton from './env-button'
+import VersionHistoryModal from './version-history-modal'
 import Button from '@/app/components/base/button'
 import { useStore as useAppStore } from '@/app/components/app/store'
 import { publishWorkflow } from '@/service/workflow'
@@ -49,11 +51,13 @@ const Header: FC = () => {
   const appID = appDetail?.id
   const isChatMode = useIsChatMode()
   const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
+  const { handleNodeSelect } = useNodesInteractions()
   const publishedAt = useStore(s => s.publishedAt)
   const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
   const toolPublished = useStore(s => s.toolPublished)
   const nodes = useNodes<StartNodeType>()
   const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
+  const selectedNode = nodes.find(node => node.data.selected)
   const startVariables = startNode?.data.variables
   const fileSettings = useFeatures(s => s.features.file)
   const variables = useMemo(() => {
@@ -76,7 +80,6 @@ const Header: FC = () => {
   const {
     handleLoadBackupDraft,
     handleBackupDraft,
-    handleRestoreFromPublishedWorkflow,
   } = useWorkflowRun()
   const { handleCheckBeforePublish } = useChecklistBeforePublish()
   const { handleSyncWorkflowDraft } = useNodesSyncDraft()
@@ -126,8 +129,10 @@ const Header: FC = () => {
   const onStartRestoring = useCallback(() => {
     workflowStore.setState({ isRestoring: true })
     handleBackupDraft()
-    handleRestoreFromPublishedWorkflow()
-  }, [handleBackupDraft, handleRestoreFromPublishedWorkflow, workflowStore])
+    // clear right panel
+    if (selectedNode)
+      handleNodeSelect(selectedNode.id, true)
+  }, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode])
 
   const onPublisherToggle = useCallback((state: boolean) => {
     if (state)
@@ -209,23 +214,27 @@ const Header: FC = () => {
       }
       {
         restoring && (
-          <div className='flex items-center space-x-2'>
-            <Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
-              <RiApps2AddLine className='w-4 h-4 mr-1 text-components-button-secondary-text' />
-              {t('workflow.common.features')}
-            </Button>
-            <Divider type='vertical' className='h-3.5 mx-auto' />
-            <Button
-              onClick={handleCancelRestore}
-            >
-              {t('common.operation.cancel')}
-            </Button>
-            <Button
-              onClick={handleRestore}
-              variant='primary'
-            >
-              {t('workflow.common.restore')}
-            </Button>
+          <div className='flex flex-col mt-auto'>
+            <div className='flex items-center justify-end my-4'>
+              <Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
+                <RiApps2AddLine className='w-4 h-4 mr-1 text-components-button-secondary-text' />
+                {t('workflow.common.features')}
+              </Button>
+              <div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
+              <Button
+                className='mr-2'
+                onClick={handleCancelRestore}
+              >
+                {t('common.operation.cancel')}
+              </Button>
+              <Button
+                onClick={handleRestore}
+                variant='primary'
+              >
+                {t('workflow.common.restore')}
+              </Button>
+            </div>
+            <VersionHistoryModal />
           </div>
         )
       }

+ 66 - 0
web/app/components/workflow/header/version-history-item.tsx

@@ -0,0 +1,66 @@
+import React from 'react'
+import dayjs from 'dayjs'
+import { useTranslation } from 'react-i18next'
+import { WorkflowVersion } from '../types'
+import cn from '@/utils/classnames'
+import type { VersionHistory } from '@/types/workflow'
+
+type VersionHistoryItemProps = {
+  item: VersionHistory
+  selectedVersion: string
+  onClick: (item: VersionHistory) => void
+  curIdx: number
+  page: number
+}
+
+const formatVersion = (version: string, curIdx: number, page: number): string => {
+  if (curIdx === 0 && page === 1)
+    return WorkflowVersion.Draft
+  if (curIdx === 1 && page === 1)
+    return WorkflowVersion.Latest
+  try {
+    const date = new Date(version)
+    if (isNaN(date.getTime()))
+      return version
+
+    // format as YYYY-MM-DD HH:mm:ss
+    return date.toISOString().slice(0, 19).replace('T', ' ')
+  }
+  catch {
+    return version
+  }
+}
+
+const VersionHistoryItem: React.FC<VersionHistoryItemProps> = ({ item, selectedVersion, onClick, curIdx, page }) => {
+  const { t } = useTranslation()
+  const formatTime = (time: number) => dayjs.unix(time).format('YYYY-MM-DD HH:mm:ss')
+  const formattedVersion = formatVersion(item.version, curIdx, page)
+  const renderVersionLabel = (version: string) => (
+    (version === WorkflowVersion.Draft || version === WorkflowVersion.Latest)
+      ? (
+        <div className="shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate">
+          {version}
+        </div>
+      )
+      : null
+  )
+
+  return (
+    <div
+      className={cn(
+        'flex items-center p-2 h-12 text-xs font-medium text-gray-700 justify-between',
+        formattedVersion === selectedVersion ? '' : 'hover:bg-gray-100',
+        formattedVersion === WorkflowVersion.Draft ? 'cursor-not-allowed' : 'cursor-pointer',
+      )}
+      onClick={() => item.version !== WorkflowVersion.Draft && onClick(item)}
+    >
+      <div className='flex flex-col gap-1 py-2'>
+        <span className="text-left">{formatTime(formattedVersion === WorkflowVersion.Draft ? item.updated_at : item.created_at)}</span>
+        <span className="text-left">{t('workflow.panel.createdBy')}  {item.created_by.name}</span>
+      </div>
+      {renderVersionLabel(formattedVersion)}
+    </div>
+  )
+}
+
+export default React.memo(VersionHistoryItem)

+ 89 - 0
web/app/components/workflow/header/version-history-modal.tsx

@@ -0,0 +1,89 @@
+'use client'
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import useSWR from 'swr'
+import { useWorkflowRun } from '../hooks'
+import VersionHistoryItem from './version-history-item'
+import type { VersionHistory } from '@/types/workflow'
+import { useStore as useAppStore } from '@/app/components/app/store'
+import { fetchPublishedAllWorkflow } from '@/service/workflow'
+import Loading from '@/app/components/base/loading'
+import Button from '@/app/components/base/button'
+
+const limit = 10
+
+const VersionHistoryModal = () => {
+  const [selectedVersion, setSelectedVersion] = useState('draft')
+  const [page, setPage] = useState(1)
+  const { handleRestoreFromPublishedWorkflow } = useWorkflowRun()
+  const appDetail = useAppStore.getState().appDetail
+  const { t } = useTranslation()
+  const {
+    data: versionHistory,
+    isLoading,
+  } = useSWR(
+    `/apps/${appDetail?.id}/workflows?page=${page}&limit=${limit}`,
+    fetchPublishedAllWorkflow,
+  )
+
+  const handleVersionClick = (item: VersionHistory) => {
+    if (item.version !== selectedVersion) {
+      setSelectedVersion(item.version)
+      handleRestoreFromPublishedWorkflow(item)
+    }
+  }
+
+  const handleNextPage = () => {
+    if (versionHistory?.has_more)
+      setPage(page => page + 1)
+  }
+
+  return (
+    <div className='w-[336px] bg-white rounded-2xl border-[0.5px] border-gray-200 shadow-xl p-2'>
+      <div className="max-h-[400px] overflow-auto">
+        {(isLoading && page) === 1
+          ? (
+            <div className='flex items-center justify-center h-10'>
+              <Loading/>
+            </div>
+          )
+          : (
+            <>
+              {versionHistory?.items?.map((item, idx) => (
+                <VersionHistoryItem
+                  key={item.version}
+                  item={item}
+                  selectedVersion={selectedVersion}
+                  onClick={handleVersionClick}
+                  curIdx={idx}
+                  page={page}
+                />
+              ))}
+              {isLoading && page > 1 && (
+                <div className='flex items-center justify-center h-10'>
+                  <Loading/>
+                </div>
+              )}
+              {!isLoading && versionHistory?.has_more && (
+                <div className='flex items-center justify-center h-10 mt-2'>
+                  <Button
+                    className='text-sm'
+                    onClick={handleNextPage}
+                  >
+                    {t('workflow.common.loadMore')}
+                  </Button>
+                </div>
+              )}
+              {!isLoading && !versionHistory?.items?.length && (
+                <div className='flex items-center justify-center h-10 text-gray-500'>
+                  {t('workflow.common.noHistory')}
+                </div>
+              )}
+            </>
+          )}
+      </div>
+    </div>
+  )
+}
+
+export default React.memo(VersionHistoryModal)

+ 1 - 1
web/app/components/workflow/header/view-history.tsx

@@ -202,7 +202,7 @@ const ViewHistory = ({
                             {`Test ${isChatMode ? 'Chat' : 'Run'}#${item.sequence_number}`}
                           </div>
                           <div className='flex items-center text-xs text-gray-500 leading-[18px]'>
-                            {item.created_by_account.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)}
+                            {item.created_by_account?.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)}
                           </div>
                         </div>
                       </div>

+ 14 - 23
web/app/components/workflow/hooks/use-workflow-run.ts

@@ -18,17 +18,14 @@ import { useWorkflowUpdate } from './use-workflow-interactions'
 import { useStore as useAppStore } from '@/app/components/app/store'
 import type { IOtherOptions } from '@/service/base'
 import { ssePost } from '@/service/base'
-import {
-  fetchPublishedWorkflow,
-  stopWorkflowRun,
-} from '@/service/workflow'
+import { stopWorkflowRun } from '@/service/workflow'
 import { useFeaturesStore } from '@/app/components/base/features/hooks'
 import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
 import {
   getFilesInLogs,
 } from '@/app/components/base/file-uploader/utils'
 import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
-import type { NodeTracing } from '@/types/workflow'
+import type { NodeTracing, VersionHistory } from '@/types/workflow'
 
 export const useWorkflowRun = () => {
   const store = useStoreApi()
@@ -754,24 +751,18 @@ export const useWorkflowRun = () => {
     stopWorkflowRun(`/apps/${appId}/workflow-runs/tasks/${taskId}/stop`)
   }, [])
 
-  const handleRestoreFromPublishedWorkflow = useCallback(async () => {
-    const appDetail = useAppStore.getState().appDetail
-    const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
-
-    if (publishedWorkflow) {
-      const nodes = publishedWorkflow.graph.nodes
-      const edges = publishedWorkflow.graph.edges
-      const viewport = publishedWorkflow.graph.viewport!
-
-      handleUpdateWorkflowCanvas({
-        nodes,
-        edges,
-        viewport,
-      })
-      featuresStore?.setState({ features: publishedWorkflow.features })
-      workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
-      workflowStore.getState().setEnvironmentVariables(publishedWorkflow.environment_variables || [])
-    }
+  const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
+    const nodes = publishedWorkflow.graph.nodes.map(node => ({ ...node, selected: false, data: { ...node.data, selected: false } }))
+    const edges = publishedWorkflow.graph.edges
+    const viewport = publishedWorkflow.graph.viewport!
+    handleUpdateWorkflowCanvas({
+      nodes,
+      edges,
+      viewport,
+    })
+    featuresStore?.setState({ features: publishedWorkflow.features })
+    workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
+    workflowStore.getState().setEnvironmentVariables(publishedWorkflow.environment_variables || [])
   }, [featuresStore, handleUpdateWorkflowCanvas, workflowStore])
 
   return {

+ 5 - 1
web/app/components/workflow/store.ts

@@ -21,7 +21,7 @@ import type {
   WorkflowRunningData,
 } from './types'
 import { WorkflowContext } from './context'
-import type { NodeTracing } from '@/types/workflow'
+import type { NodeTracing, VersionHistory } from '@/types/workflow'
 
 // #TODO chatVar#
 // const MOCK_DATA = [
@@ -171,6 +171,8 @@ type Shape = {
   setIterTimes: (iterTimes: number) => void
   iterParallelLogMap: Map<string, Map<string, NodeTracing[]>>
   setIterParallelLogMap: (iterParallelLogMap: Map<string, Map<string, NodeTracing[]>>) => void
+  versionHistory: VersionHistory[]
+  setVersionHistory: (versionHistory: VersionHistory[]) => void
 }
 
 export const createWorkflowStore = () => {
@@ -291,6 +293,8 @@ export const createWorkflowStore = () => {
     iterParallelLogMap: new Map<string, Map<string, NodeTracing[]>>(),
     setIterParallelLogMap: iterParallelLogMap => set(() => ({ iterParallelLogMap })),
 
+    versionHistory: [],
+    setVersionHistory: versionHistory => set(() => ({ versionHistory })),
   }))
 }
 

+ 5 - 0
web/app/components/workflow/types.ts

@@ -289,6 +289,11 @@ export enum WorkflowRunningStatus {
   Stopped = 'stopped',
 }
 
+export enum WorkflowVersion {
+  Draft = 'draft',
+  Latest = 'latest',
+}
+
 export enum NodeRunningStatus {
   NotStart = 'not-start',
   Waiting = 'waiting',

+ 2 - 0
web/i18n/zh-Hans/workflow.ts

@@ -104,6 +104,8 @@ const translation = {
     onFailure: '异常时',
     addFailureBranch: '添加异常分支',
     openInExplore: '在“探索”中打开',
+    loadMore: '加载更多',
+    noHistory: '没有历史版本',
   },
   env: {
     envPanelTitle: '环境变量',

+ 13 - 2
web/service/workflow.ts

@@ -4,6 +4,7 @@ import type { CommonResponse } from '@/models/common'
 import type {
   ChatRunHistoryResponse,
   ConversationVariableResponse,
+  FetchWorkflowDraftPageResponse,
   FetchWorkflowDraftResponse,
   NodesDefaultConfigsResponse,
   WorkflowRunHistoryResponse,
@@ -14,7 +15,10 @@ export const fetchWorkflowDraft = (url: string) => {
   return get(url, {}, { silent: true }) as Promise<FetchWorkflowDraftResponse>
 }
 
-export const syncWorkflowDraft = ({ url, params }: { url: string; params: Pick<FetchWorkflowDraftResponse, 'graph' | 'features' | 'environment_variables' | 'conversation_variables'> }) => {
+export const syncWorkflowDraft = ({ url, params }: {
+  url: string
+  params: Pick<FetchWorkflowDraftResponse, 'graph' | 'features' | 'environment_variables' | 'conversation_variables'>
+}) => {
   return post<CommonResponse & { updated_at: number; hash: string }>(url, { body: params }, { silent: true })
 }
 
@@ -46,6 +50,10 @@ export const fetchPublishedWorkflow: Fetcher<FetchWorkflowDraftResponse, string>
   return get<FetchWorkflowDraftResponse>(url)
 }
 
+export const fetchPublishedAllWorkflow: Fetcher<FetchWorkflowDraftPageResponse, string> = (url) => {
+  return get<FetchWorkflowDraftPageResponse>(url)
+}
+
 export const stopWorkflowRun = (url: string) => {
   return post<CommonResponse>(url)
 }
@@ -61,6 +69,9 @@ export const updateWorkflowDraftFromDSL = (appId: string, data: string) => {
   return post<FetchWorkflowDraftResponse>(`apps/${appId}/workflows/draft/import`, { body: { data } })
 }
 
-export const fetchCurrentValueOfConversationVariable: Fetcher<ConversationVariableResponse, { url: string; params: { conversation_id: string } }> = ({ url, params }) => {
+export const fetchCurrentValueOfConversationVariable: Fetcher<ConversationVariableResponse, {
+  url: string
+  params: { conversation_id: string }
+}> = ({ url, params }) => {
   return get<ConversationVariableResponse>(url, { params })
 }

+ 10 - 7
web/types/workflow.ts

@@ -1,11 +1,5 @@
 import type { Viewport } from 'reactflow'
-import type {
-  BlockEnum,
-  ConversationVariable,
-  Edge,
-  EnvironmentVariable,
-  Node,
-} from '@/app/components/workflow/types'
+import type { BlockEnum, ConversationVariable, Edge, EnvironmentVariable, Node } from '@/app/components/workflow/types'
 import type { TransferMethod } from '@/types/app'
 import type { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
 
@@ -79,6 +73,15 @@ export type FetchWorkflowDraftResponse = {
   tool_published: boolean
   environment_variables?: EnvironmentVariable[]
   conversation_variables?: ConversationVariable[]
+  version: string
+}
+
+export type VersionHistory = FetchWorkflowDraftResponse
+
+export type FetchWorkflowDraftPageResponse = {
+  items: VersionHistory[]
+  has_more: boolean
+  page: number
 }
 
 export type NodeTracingListResponse = {

+ 7 - 2
web/utils/var.ts

@@ -1,11 +1,16 @@
 import { MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config'
-import { CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT } from '@/app/components/base/prompt-editor/constants'
+import {
+  CONTEXT_PLACEHOLDER_TEXT,
+  HISTORY_PLACEHOLDER_TEXT,
+  PRE_PROMPT_PLACEHOLDER_TEXT,
+  QUERY_PLACEHOLDER_TEXT,
+} from '@/app/components/base/prompt-editor/constants'
 import { InputVarType } from '@/app/components/workflow/types'
 
 const otherAllowedRegex = /^[a-zA-Z0-9_]+$/
 
 export const getNewVar = (key: string, type: string) => {
-  const { max_length, ...rest } = VAR_ITEM_TEMPLATE
+  const { ...rest } = VAR_ITEM_TEMPLATE
   if (type !== 'string') {
     return {
       ...rest,