Added better listing and listing varient support.
This commit is contained in:
parent
8e393e229f
commit
17da8a4407
@ -1,16 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.58577,0,0,0.58577,29.000019,28.999974)">
|
||||
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
|
||||
<g transform="matrix(0.570497,0,0,0.570497,16.522802,13.725853)">
|
||||
<path d="M6.499,56.236L41.124,56.236C44.294,56.236 46.809,54.281 46.809,50.825C46.809,47.443 44.391,45.464 41.124,45.464L18.117,45.464L18.117,45.062C22.049,43.001 23.774,38.335 23.774,33.596C23.774,32.751 23.669,32.057 23.534,31.399L37.509,31.399C39.584,31.399 41.06,30.032 41.06,28.125C41.06,26.241 39.584,24.905 37.509,24.905L22.134,24.905C21.788,23.604 21.265,21.585 21.265,19.395C21.265,13.243 26.441,10.758 32.681,10.758C34.774,10.758 36.433,10.942 37.882,11.247C38.957,11.394 40.282,11.543 41.544,11.543C44.007,11.543 46.13,10.334 46.13,7.354C46.13,5.297 45.211,3.871 43.447,2.745C39.934,0.628 34.458,0.378 30.367,0.378C18.274,0.378 7.903,5.899 7.903,17.414C7.903,19.396 8.212,21.38 9.112,24.905L3.574,24.905C1.5,24.905 0,26.241 0,28.125C0,30.071 1.514,31.399 3.574,31.399L10.467,31.399C10.73,32.486 10.797,33.332 10.797,34.157C10.797,38.814 8.74,42.769 5.065,44.806C3.057,46.118 0.808,47.931 0.808,50.875C0.808,54.298 3.219,56.236 6.499,56.236Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,11.133834,5.394156)">
|
||||
<rect x="0" y="0" width="74.451" height="58.962" style="fill-opacity:0;"/>
|
||||
<g transform="matrix(0.700206,0,0,0.700206,-5.133834,5.956054)">
|
||||
<path d="M28.563,58.962L10.588,58.962C3.679,58.962 0,55.326 0,48.481L0,10.522C0,3.667 3.679,0.02 10.588,0.02L63.676,0.02C70.606,0.02 74.264,3.667 74.264,10.522L74.264,20.933C74.047,20.926 73.827,20.922 73.604,20.922L67.305,20.922L67.305,11.251C67.305,8.378 65.813,6.979 63.097,6.979L11.167,6.979C8.429,6.979 6.959,8.378 6.959,11.251L6.959,47.743C6.959,50.615 8.429,52.003 11.167,52.003L28.563,52.003L28.563,58.962ZM28.857,31.097L16.175,31.097C14.981,31.097 14.107,30.204 14.107,29.051C14.107,27.929 14.981,27.055 16.175,27.055L30.296,27.055C29.634,28.235 29.144,29.582 28.857,31.097ZM16.175,19.207C14.981,19.207 14.107,18.313 14.107,17.138C14.107,16.016 14.981,15.164 16.175,15.164L58.13,15.164C59.295,15.164 60.157,16.016 60.157,17.138C60.157,18.313 59.295,19.207 58.13,19.207L16.175,19.207Z"/>
|
||||
<g>
|
||||
<path d="M12.892,61L51.107,61C57.723,61 60.999,57.755 60.999,51.265L60.999,12.766C60.999,6.276 57.723,3 51.107,3L12.892,3C6.308,3 3,6.276 3,12.766L3,51.265C3,57.755 6.308,61 12.892,61ZM12.955,55.928C9.805,55.928 8.072,54.258 8.072,50.982L8.072,13.05C8.072,9.774 9.805,8.072 12.955,8.072L51.044,8.072C54.163,8.072 55.927,9.773 55.927,13.05L55.927,50.982C55.927,54.258 54.163,55.928 51.044,55.928L12.955,55.928Z" style="fill-rule:nonzero;"/>
|
||||
<g transform="matrix(0.630094,0,0,0.630094,12.050043,9.166267)">
|
||||
<path d="M14.688,30.601L14.688,17.094C14.688,15.264 15.627,13.647 17.191,12.746L29.089,5.879C30.674,4.944 32.523,4.944 34.111,5.878L46.001,12.745C47.566,13.647 48.504,15.264 48.504,17.094L48.504,30.521L60.25,37.305C61.815,38.206 62.754,39.824 62.754,41.653L62.754,55.39C62.754,57.22 61.813,58.84 60.251,59.729L48.363,66.604C46.769,67.533 44.919,67.532 43.339,66.605L31.662,59.857L19.995,66.604C18.401,67.533 16.552,67.532 14.971,66.605L3.077,59.732C1.511,58.84 0.57,57.22 0.57,55.39L0.57,41.653C0.57,39.824 1.509,38.206 3.073,37.306L14.688,30.601ZM22.461,16.011L31.554,21.217L40.687,15.981L32,10.966L31.986,10.958C31.74,10.809 31.453,10.809 31.207,10.958L31.193,10.966L22.461,16.011ZM28.869,35.565L28.869,25.991L20.135,20.989L20.135,30.103C20.135,30.312 20.215,30.501 20.356,30.648L28.869,35.565ZM43.118,60.173L43.118,50.551L34.386,45.55L34.386,54.723C34.406,54.993 34.565,55.222 34.804,55.364L43.118,60.173ZM20.205,60.177L28.527,55.364C28.782,55.213 28.931,54.957 28.931,54.662L28.931,45.503L20.205,50.498L20.205,60.177ZM14.751,60.174L14.751,50.551L6.017,45.549L6.017,54.662C6.017,54.958 6.179,55.212 6.437,55.364L14.751,60.174ZM34.324,35.617L42.645,30.805C42.901,30.653 43.05,30.398 43.05,30.103L43.05,20.943L34.324,25.938L34.324,35.617ZM36.709,40.57L45.803,45.777L54.936,40.54L46.249,35.526L46.234,35.517C45.989,35.368 45.702,35.368 45.457,35.517L45.442,35.526L36.709,40.57ZM48.573,60.177L56.895,55.364C57.15,55.213 57.299,54.957 57.299,54.662L57.299,45.503L48.573,50.498L48.573,60.177ZM8.341,40.57L17.436,45.777L26.526,40.564L17.621,35.418L17.618,35.417C17.44,35.381 17.256,35.417 17.09,35.517L17.075,35.526L8.341,40.57Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.6 KiB |
19
assets/icons/listingvarienticon.svg
Normal file
19
assets/icons/listingvarienticon.svg
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g>
|
||||
<path d="M34.148,61L12.892,61C6.308,61 3,57.755 3,51.265L3,12.766C3,6.276 6.308,3 12.892,3L51.107,3C57.723,3 60.999,6.276 60.999,12.766L60.999,34.148C59.513,32.753 57.802,31.593 55.927,30.73L55.927,13.05C55.927,9.773 54.163,8.072 51.044,8.072L12.955,8.072C9.805,8.072 8.072,9.774 8.072,13.05L8.072,50.982C8.072,54.258 9.805,55.928 12.955,55.928L30.73,55.928C31.593,57.803 32.752,59.514 34.148,61ZM29.009,48.611L24.649,51.133C23.644,51.718 22.48,51.718 21.483,51.134L13.989,46.803C13.002,46.241 12.409,45.22 12.409,44.067L12.409,35.412C12.409,34.259 13.001,33.24 13.987,32.672L21.305,28.448L21.305,19.937C21.305,18.784 21.897,17.765 22.882,17.197L30.379,12.87C31.377,12.281 32.542,12.281 33.543,12.87L41.035,17.197C42.021,17.765 42.612,18.784 42.612,19.937L42.612,28.397L44.285,29.364C37.862,30.639 32.6,35.156 30.28,41.135L30.28,37.837L24.781,40.985L24.781,47.084L29.331,44.452C29.113,45.602 28.999,46.788 28.999,48C28.999,48.205 29.003,48.408 29.009,48.611ZM33.677,31.608L38.92,28.576C39.082,28.481 39.176,28.32 39.176,28.134L39.176,22.362L33.677,25.51L33.677,31.608ZM26.202,19.254L31.932,22.535L37.687,19.236L32.213,16.076L32.204,16.071C32.049,15.977 31.868,15.977 31.713,16.071L31.705,16.076L26.202,19.254ZM17.306,34.729L23.036,38.01L28.764,34.726L23.153,31.483L23.151,31.483C23.039,31.46 22.923,31.482 22.818,31.546L22.809,31.551L17.306,34.729ZM30.241,31.576L30.241,25.543L24.737,22.391L24.737,28.134C24.737,28.266 24.788,28.384 24.876,28.477L30.241,31.576ZM21.345,47.081L21.345,41.018L15.841,37.866L15.841,43.608C15.841,43.795 15.944,43.955 16.106,44.051L21.345,47.081Z"/>
|
||||
<g transform="matrix(1.077085,0,0,1.077085,-2.466684,-4.933344)">
|
||||
<path d="M46.854,34.29C55.053,34.29 61.709,40.946 61.709,49.145C61.709,57.344 55.053,64 46.854,64C38.656,64 32,57.344 32,49.145C32,40.946 38.656,34.29 46.854,34.29ZM46.854,38.468C40.962,38.468 36.177,43.252 36.177,49.145C36.177,55.038 40.962,59.822 46.854,59.822C52.747,59.822 57.531,55.038 57.531,49.145C57.531,43.252 52.747,38.468 46.854,38.468Z"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-1.649781,-7.506927)">
|
||||
<circle cx="49.627" cy="50.064" r="3.017"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-6.45365,0.751087)">
|
||||
<circle cx="49.627" cy="50.064" r="3.017"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,3.071774,0.733775)">
|
||||
<circle cx="49.627" cy="50.064" r="3.017"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@ -30,6 +30,7 @@ import { ElectronProvider } from './components/Dashboard/context/ElectronContext
|
||||
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
|
||||
import AuthCallback from './components/App/AuthCallback.jsx'
|
||||
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
|
||||
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
|
||||
|
||||
import {
|
||||
ProductionRoutes,
|
||||
@ -101,6 +102,10 @@ const AppContent = () => {
|
||||
path='/auth/callback'
|
||||
element={<AuthCallback />}
|
||||
/>
|
||||
<Route
|
||||
path='/auth/marketplace/callback'
|
||||
element={<MarketplaceAuthCallback />}
|
||||
/>
|
||||
<Route
|
||||
path='/email/notification'
|
||||
element={<EmailNotificationTemplate />}
|
||||
|
||||
@ -0,0 +1,253 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Space, Flex, Modal } from 'antd'
|
||||
import loglevel from 'loglevel'
|
||||
import config from '../../../../config'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import ViewButton from '../../common/ViewButton'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import ObjectForm from '../../common/ObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import PublishListingVarient from './PublishListingVarient.jsx'
|
||||
import UnpublishListingVarient from './UnpublishListingVarient.jsx'
|
||||
import { Card } from 'antd'
|
||||
|
||||
const log = loglevel.getLogger('ListingVarientInfo')
|
||||
log.setLevel(config.logLevel)
|
||||
|
||||
const ListingVarientInfo = () => {
|
||||
const location = useLocation()
|
||||
const objectFormRef = useRef(null)
|
||||
const actionHandlerRef = useRef(null)
|
||||
const listingVarientId = new URLSearchParams(location.search).get(
|
||||
'listingVarientId'
|
||||
)
|
||||
const [publishOpen, setPublishOpen] = useState(false)
|
||||
const [unpublishOpen, setUnpublishOpen] = useState(false)
|
||||
const [collapseState, updateCollapseState] = useCollapseState(
|
||||
'ListingVarientInfo',
|
||||
{
|
||||
info: true,
|
||||
notes: true,
|
||||
auditLogs: true
|
||||
}
|
||||
)
|
||||
const [objectFormState, setEditFormState] = useState({
|
||||
isEditing: false,
|
||||
editLoading: false,
|
||||
formValid: false,
|
||||
lock: null,
|
||||
loading: false,
|
||||
objectData: {}
|
||||
})
|
||||
|
||||
const actions = {
|
||||
reload: () => {
|
||||
objectFormRef?.current?.handleFetchObject?.()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
objectFormRef?.current?.startEditing?.()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
objectFormRef?.current?.cancelEditing?.()
|
||||
return true
|
||||
},
|
||||
finishEdit: () => {
|
||||
objectFormRef?.current?.handleUpdate?.()
|
||||
return true
|
||||
},
|
||||
delete: () => {
|
||||
objectFormRef?.current?.handleDelete?.()
|
||||
return true
|
||||
},
|
||||
publish: () => {
|
||||
setPublishOpen(true)
|
||||
return true
|
||||
},
|
||||
unpublish: () => {
|
||||
setUnpublishOpen(true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ maxHeight: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<ObjectActions
|
||||
type='listingVarient'
|
||||
id={listingVarientId}
|
||||
disabled={objectFormState.loading}
|
||||
objectData={objectFormState.objectData}
|
||||
/>
|
||||
<ViewButton
|
||||
disabled={objectFormState.loading}
|
||||
items={[
|
||||
{ key: 'info', label: 'Listing Varient Information' },
|
||||
{ key: 'notes', label: 'Notes' },
|
||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||
]}
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='listingVarient'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='listingVarient'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
</Space>
|
||||
<LockIndicator lock={objectFormState.lock} />
|
||||
</Space>
|
||||
<Space>
|
||||
<EditButtons
|
||||
isEditing={objectFormState.isEditing}
|
||||
handleUpdate={() => {
|
||||
actionHandlerRef.current.callAction('finishEdit')
|
||||
}}
|
||||
cancelEditing={() => {
|
||||
actionHandlerRef.current.callAction('cancelEdit')
|
||||
}}
|
||||
startEditing={() => {
|
||||
actionHandlerRef.current.callAction('edit')
|
||||
}}
|
||||
editLoading={objectFormState.editLoading}
|
||||
formValid={objectFormState.formValid}
|
||||
disabled={objectFormState.lock?.locked || objectFormState.loading}
|
||||
loading={objectFormState.editLoading}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
<ScrollBox>
|
||||
<Flex vertical gap={'large'}>
|
||||
<ActionHandler
|
||||
actions={actions}
|
||||
loading={objectFormState.loading}
|
||||
ref={actionHandlerRef}
|
||||
>
|
||||
<ObjectForm
|
||||
id={listingVarientId}
|
||||
type='listingVarient'
|
||||
style={{ height: '100%' }}
|
||||
ref={objectFormRef}
|
||||
onStateChange={(state) => {
|
||||
setEditFormState((prev) => ({ ...prev, ...state }))
|
||||
}}
|
||||
>
|
||||
{({ loading, isEditing, objectData }) => (
|
||||
<Flex vertical gap={'large'}>
|
||||
<InfoCollapse
|
||||
title='Listing Varient Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('info', expanded)
|
||||
}
|
||||
collapseKey='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
isEditing={isEditing}
|
||||
type='listingVarient'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
)}
|
||||
</ObjectForm>
|
||||
</ActionHandler>
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
||||
collapseKey='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={listingVarientId} type='listingVarient' />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
collapseKey='auditLogs'
|
||||
>
|
||||
{objectFormState.loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': listingVarientId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</ScrollBox>
|
||||
</Flex>
|
||||
<Modal
|
||||
open={publishOpen}
|
||||
onCancel={() => setPublishOpen(false)}
|
||||
width={515}
|
||||
footer={null}
|
||||
destroyOnHidden={true}
|
||||
centered={true}
|
||||
>
|
||||
<PublishListingVarient
|
||||
objectData={objectFormState.objectData}
|
||||
onOk={() => {
|
||||
setPublishOpen(false)
|
||||
actions.reload()
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={unpublishOpen}
|
||||
onCancel={() => setUnpublishOpen(false)}
|
||||
width={515}
|
||||
footer={null}
|
||||
destroyOnHidden={true}
|
||||
centered={true}
|
||||
>
|
||||
<UnpublishListingVarient
|
||||
objectData={objectFormState.objectData}
|
||||
onOk={() => {
|
||||
setUnpublishOpen(false)
|
||||
actions.reload()
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListingVarientInfo
|
||||
@ -0,0 +1,87 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import NewObjectForm from '../../common/NewObjectForm'
|
||||
import WizardView from '../../common/WizardView'
|
||||
|
||||
const NewListingVarient = ({ onOk, defaultValues }) => {
|
||||
return (
|
||||
<NewObjectForm
|
||||
type={'listingVarient'}
|
||||
defaultValues={{ status: 'draft', ...defaultValues }}
|
||||
>
|
||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||
const steps = [
|
||||
{
|
||||
title: 'Required',
|
||||
key: 'required',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='listingVarient'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={true}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Optional',
|
||||
key: 'optional',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='listingVarient'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={false}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Summary',
|
||||
key: 'summary',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='listingVarient'
|
||||
column={1}
|
||||
bordered={false}
|
||||
visibleProperties={{
|
||||
_id: false,
|
||||
createdAt: false,
|
||||
updatedAt: false,
|
||||
lastSyncedAt: false
|
||||
}}
|
||||
isEditing={false}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
return (
|
||||
<WizardView
|
||||
steps={steps}
|
||||
loading={submitLoading}
|
||||
formValid={formValid}
|
||||
title='New Listing Varient'
|
||||
onSubmit={async () => {
|
||||
const result = await handleSubmit()
|
||||
if (result) {
|
||||
onOk()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</NewObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
NewListingVarient.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
reset: PropTypes.bool,
|
||||
defaultValues: PropTypes.object
|
||||
}
|
||||
|
||||
export default NewListingVarient
|
||||
@ -0,0 +1,54 @@
|
||||
import { useState, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import { message } from 'antd'
|
||||
import MessageDialogView from '../../common/MessageDialogView.jsx'
|
||||
|
||||
const PublishListingVarient = ({ onOk, objectData }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { sendObjectFunction } = useContext(ApiServerContext)
|
||||
|
||||
const handlePublish = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await sendObjectFunction(
|
||||
objectData._id,
|
||||
'ListingVarient',
|
||||
'publish'
|
||||
)
|
||||
if (result) {
|
||||
message.success('Published successfully')
|
||||
onOk(result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error publishing listing variant:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const ref =
|
||||
objectData?.title ||
|
||||
objectData?._reference ||
|
||||
objectData?.name ||
|
||||
objectData?._id
|
||||
|
||||
return (
|
||||
<MessageDialogView
|
||||
title='Publish this listing variant on the marketplace?'
|
||||
description={`Publishes this SKU on the connected marketplace so the item goes live when a draft listing exists (eBay).${
|
||||
ref ? ` (variant: ${ref})` : ''
|
||||
}`}
|
||||
onOk={handlePublish}
|
||||
okText='Publish'
|
||||
okLoading={loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
PublishListingVarient.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
objectData: PropTypes.object
|
||||
}
|
||||
|
||||
export default PublishListingVarient
|
||||
@ -0,0 +1,54 @@
|
||||
import { useState, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import { message } from 'antd'
|
||||
import MessageDialogView from '../../common/MessageDialogView.jsx'
|
||||
|
||||
const UnpublishListingVarient = ({ onOk, objectData }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { sendObjectFunction } = useContext(ApiServerContext)
|
||||
|
||||
const handleUnpublish = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await sendObjectFunction(
|
||||
objectData._id,
|
||||
'ListingVarient',
|
||||
'unpublish'
|
||||
)
|
||||
if (result) {
|
||||
message.success('Unpublished successfully')
|
||||
onOk(result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error unpublishing listing variant:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const ref =
|
||||
objectData?.title ||
|
||||
objectData?._reference ||
|
||||
objectData?.name ||
|
||||
objectData?._id
|
||||
|
||||
return (
|
||||
<MessageDialogView
|
||||
title="Withdraw this variant from the marketplace?"
|
||||
description={`Ends the live marketplace listing for this SKU where applicable (eBay).${
|
||||
ref ? ` (variant: ${ref})` : ''
|
||||
}`}
|
||||
onOk={handleUnpublish}
|
||||
okText='Unpublish'
|
||||
okLoading={loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
UnpublishListingVarient.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
objectData: PropTypes.object
|
||||
}
|
||||
|
||||
export default UnpublishListingVarient
|
||||
107
src/components/Dashboard/Sales/Listings.jsx
Normal file
107
src/components/Dashboard/Sales/Listings.jsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
|
||||
import NewListing from './Listings/NewListing'
|
||||
import ObjectTable from '../common/ObjectTable'
|
||||
import PlusIcon from '../../Icons/PlusIcon'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||
import ObjectTableViewButton from '../common/ObjectTableViewButton'
|
||||
import FilterSidebarButton from '../common/FilterSidebarButton'
|
||||
import useViewMode from '../hooks/useViewMode'
|
||||
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
|
||||
import ColumnViewButton from '../common/ColumnViewButton'
|
||||
import ExportListButton from '../common/ExportListButton'
|
||||
|
||||
const Listings = () => {
|
||||
const [newListingOpen, setNewListingOpen] = useState(false)
|
||||
const tableRef = useRef()
|
||||
|
||||
const [viewMode, setViewMode] = useViewMode('listings')
|
||||
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
useColumnVisibility('listings')
|
||||
|
||||
const [showFilterSidebar, setShowFilterSidebar] =
|
||||
useFilterSidebarVisibility('Listings')
|
||||
|
||||
const actionItems = {
|
||||
items: [
|
||||
{
|
||||
label: 'New Listing',
|
||||
key: 'newListing',
|
||||
icon: <PlusIcon />
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: 'Reload List',
|
||||
key: 'reloadList',
|
||||
icon: <ReloadIcon />
|
||||
}
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'reloadList') {
|
||||
tableRef.current?.reload()
|
||||
} else if (key === 'newListing') {
|
||||
setNewListingOpen(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex vertical={'true'} gap='large' className='h-100'>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='small'>
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
<ColumnViewButton
|
||||
type='listing'
|
||||
loading={false}
|
||||
visibleState={columnVisibility}
|
||||
updateVisibleState={setColumnVisibility}
|
||||
/>
|
||||
<ExportListButton objectType='listing' />
|
||||
</Space>
|
||||
<Space>
|
||||
<FilterSidebarButton
|
||||
active={showFilterSidebar}
|
||||
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
|
||||
/>
|
||||
<ObjectTableViewButton
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
<ObjectTable
|
||||
ref={tableRef}
|
||||
visibleColumns={columnVisibility}
|
||||
type='listing'
|
||||
cards={viewMode === 'cards'}
|
||||
showFilterSidebar={showFilterSidebar}
|
||||
/>
|
||||
</Flex>
|
||||
<Modal
|
||||
open={newListingOpen}
|
||||
styles={{ content: { paddingBottom: '24px' } }}
|
||||
footer={null}
|
||||
width={800}
|
||||
onCancel={() => {
|
||||
setNewListingOpen(false)
|
||||
}}
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
<NewListing
|
||||
onOk={() => {
|
||||
setNewListingOpen(false)
|
||||
tableRef.current?.reload()
|
||||
}}
|
||||
reset={newListingOpen}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Listings
|
||||
298
src/components/Dashboard/Sales/Listings/ListingInfo.jsx
Normal file
298
src/components/Dashboard/Sales/Listings/ListingInfo.jsx
Normal file
@ -0,0 +1,298 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Space, Flex, Modal } from 'antd'
|
||||
import loglevel from 'loglevel'
|
||||
import config from '../../../../config'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
import NotesPanel from '../../common/NotesPanel'
|
||||
import InfoCollapse from '../../common/InfoCollapse'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import ViewButton from '../../common/ViewButton'
|
||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||
import ListingVarientIcon from '../../../Icons/ListingVarientIcon.jsx'
|
||||
import ObjectForm from '../../common/ObjectForm'
|
||||
import EditButtons from '../../common/EditButtons'
|
||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import NewListingVarient from '../ListingVarients/NewListingVarient.jsx'
|
||||
import PublishListing from './PublishListing.jsx'
|
||||
import UnpublishListing from './UnpublishListing.jsx'
|
||||
import { Card } from 'antd'
|
||||
|
||||
const log = loglevel.getLogger('ListingInfo')
|
||||
log.setLevel(config.logLevel)
|
||||
|
||||
const ListingInfo = () => {
|
||||
const location = useLocation()
|
||||
const objectFormRef = useRef(null)
|
||||
const listingVarientsTableRef = useRef(null)
|
||||
const actionHandlerRef = useRef(null)
|
||||
const listingId = new URLSearchParams(location.search).get('listingId')
|
||||
const [newListingVarientOpen, setNewListingVarientOpen] = useState(false)
|
||||
const [publishListingOpen, setPublishListingOpen] = useState(false)
|
||||
const [unpublishListingOpen, setUnpublishListingOpen] = useState(false)
|
||||
const [collapseState, updateCollapseState] = useCollapseState('ListingInfo', {
|
||||
info: true,
|
||||
listingVarients: true,
|
||||
notes: true,
|
||||
auditLogs: true
|
||||
})
|
||||
const [objectFormState, setEditFormState] = useState({
|
||||
isEditing: false,
|
||||
editLoading: false,
|
||||
formValid: false,
|
||||
lock: null,
|
||||
loading: false,
|
||||
objectData: {}
|
||||
})
|
||||
|
||||
const actions = {
|
||||
reload: () => {
|
||||
objectFormRef?.current?.handleFetchObject?.()
|
||||
return true
|
||||
},
|
||||
edit: () => {
|
||||
objectFormRef?.current?.startEditing?.()
|
||||
return false
|
||||
},
|
||||
cancelEdit: () => {
|
||||
objectFormRef?.current?.cancelEditing?.()
|
||||
return true
|
||||
},
|
||||
finishEdit: () => {
|
||||
objectFormRef?.current?.handleUpdate?.()
|
||||
return true
|
||||
},
|
||||
delete: () => {
|
||||
objectFormRef?.current?.handleDelete?.()
|
||||
return true
|
||||
},
|
||||
newListingVarient: () => {
|
||||
setNewListingVarientOpen(true)
|
||||
return true
|
||||
},
|
||||
publish: () => {
|
||||
setPublishListingOpen(true)
|
||||
return true
|
||||
},
|
||||
unpublish: () => {
|
||||
setUnpublishListingOpen(true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
gap='large'
|
||||
vertical='true'
|
||||
style={{ maxHeight: '100%', minHeight: 0 }}
|
||||
>
|
||||
<Flex justify={'space-between'}>
|
||||
<Space size='middle'>
|
||||
<Space size='small'>
|
||||
<ObjectActions
|
||||
type='listing'
|
||||
id={listingId}
|
||||
disabled={objectFormState.loading}
|
||||
objectData={objectFormState.objectData}
|
||||
/>
|
||||
<ViewButton
|
||||
disabled={objectFormState.loading}
|
||||
items={[
|
||||
{ key: 'info', label: 'Listing Information' },
|
||||
{ key: 'listingVarients', label: 'Listing Varients' },
|
||||
{ key: 'notes', label: 'Notes' },
|
||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||
]}
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
<UserNotifierToggle
|
||||
type='listing'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
<DocumentPrintButton
|
||||
type='listing'
|
||||
objectData={objectFormState.objectData}
|
||||
disabled={objectFormState.loading}
|
||||
/>
|
||||
</Space>
|
||||
<LockIndicator lock={objectFormState.lock} />
|
||||
</Space>
|
||||
<Space>
|
||||
<EditButtons
|
||||
isEditing={objectFormState.isEditing}
|
||||
handleUpdate={() => {
|
||||
actionHandlerRef.current.callAction('finishEdit')
|
||||
}}
|
||||
cancelEditing={() => {
|
||||
actionHandlerRef.current.callAction('cancelEdit')
|
||||
}}
|
||||
startEditing={() => {
|
||||
actionHandlerRef.current.callAction('edit')
|
||||
}}
|
||||
editLoading={objectFormState.editLoading}
|
||||
formValid={objectFormState.formValid}
|
||||
disabled={objectFormState.lock?.locked || objectFormState.loading}
|
||||
loading={objectFormState.editLoading}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
<ScrollBox>
|
||||
<Flex vertical gap={'large'}>
|
||||
<ActionHandler
|
||||
actions={actions}
|
||||
loading={objectFormState.loading}
|
||||
ref={actionHandlerRef}
|
||||
>
|
||||
<ObjectForm
|
||||
id={listingId}
|
||||
type='listing'
|
||||
style={{ height: '100%' }}
|
||||
ref={objectFormRef}
|
||||
onStateChange={(state) => {
|
||||
setEditFormState((prev) => ({ ...prev, ...state }))
|
||||
}}
|
||||
>
|
||||
{({ loading, isEditing, objectData }) => (
|
||||
<Flex vertical gap={'large'}>
|
||||
<InfoCollapse
|
||||
title='Listing Information'
|
||||
icon={<InfoCircleIcon />}
|
||||
active={collapseState.info}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('info', expanded)
|
||||
}
|
||||
collapseKey='info'
|
||||
>
|
||||
<ObjectInfo
|
||||
loading={loading}
|
||||
isEditing={isEditing}
|
||||
type='listing'
|
||||
objectData={objectData}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Listing Varients'
|
||||
icon={<ListingVarientIcon />}
|
||||
active={collapseState.listingVarients}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('listingVarients', expanded)
|
||||
}
|
||||
collapseKey='listingVarients'
|
||||
>
|
||||
<ObjectTable
|
||||
type='listingVarient'
|
||||
masterFilter={{
|
||||
'listing._id': listingId
|
||||
}}
|
||||
visibleColumns={{ listing: false }}
|
||||
ref={listingVarientsTableRef}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
)}
|
||||
</ObjectForm>
|
||||
</ActionHandler>
|
||||
<InfoCollapse
|
||||
title='Notes'
|
||||
icon={<NoteIcon />}
|
||||
active={collapseState.notes}
|
||||
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
||||
collapseKey='notes'
|
||||
>
|
||||
<Card>
|
||||
<NotesPanel _id={listingId} type='listing' />
|
||||
</Card>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Audit Logs'
|
||||
icon={<AuditLogIcon />}
|
||||
active={collapseState.auditLogs}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('auditLogs', expanded)
|
||||
}
|
||||
collapseKey='auditLogs'
|
||||
>
|
||||
{objectFormState.loading ? (
|
||||
<InfoCollapsePlaceholder />
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='auditLog'
|
||||
masterFilter={{ 'parent._id': listingId }}
|
||||
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||
/>
|
||||
)}
|
||||
</InfoCollapse>
|
||||
</Flex>
|
||||
</ScrollBox>
|
||||
</Flex>
|
||||
<Modal
|
||||
open={newListingVarientOpen}
|
||||
onCancel={() => {
|
||||
setNewListingVarientOpen(false)
|
||||
}}
|
||||
width={800}
|
||||
footer={null}
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
<NewListingVarient
|
||||
onOk={() => {
|
||||
setNewListingVarientOpen(false)
|
||||
listingVarientsTableRef.current?.reload()
|
||||
}}
|
||||
reset={newListingVarientOpen}
|
||||
defaultValues={{
|
||||
listing: { _id: listingId }
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={publishListingOpen}
|
||||
onCancel={() => setPublishListingOpen(false)}
|
||||
width={515}
|
||||
footer={null}
|
||||
destroyOnHidden={true}
|
||||
centered={true}
|
||||
>
|
||||
<PublishListing
|
||||
objectData={objectFormState.objectData}
|
||||
onOk={() => {
|
||||
setPublishListingOpen(false)
|
||||
actions.reload()
|
||||
listingVarientsTableRef.current?.reload?.()
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={unpublishListingOpen}
|
||||
onCancel={() => setUnpublishListingOpen(false)}
|
||||
width={515}
|
||||
footer={null}
|
||||
destroyOnHidden={true}
|
||||
centered={true}
|
||||
>
|
||||
<UnpublishListing
|
||||
objectData={objectFormState.objectData}
|
||||
onOk={() => {
|
||||
setUnpublishListingOpen(false)
|
||||
actions.reload()
|
||||
listingVarientsTableRef.current?.reload?.()
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListingInfo
|
||||
87
src/components/Dashboard/Sales/Listings/NewListing.jsx
Normal file
87
src/components/Dashboard/Sales/Listings/NewListing.jsx
Normal file
@ -0,0 +1,87 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import NewObjectForm from '../../common/NewObjectForm'
|
||||
import WizardView from '../../common/WizardView'
|
||||
|
||||
const NewListing = ({ onOk, defaultValues }) => {
|
||||
return (
|
||||
<NewObjectForm
|
||||
type={'listing'}
|
||||
defaultValues={{ status: 'draft', ...defaultValues }}
|
||||
>
|
||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||
const steps = [
|
||||
{
|
||||
title: 'Required',
|
||||
key: 'required',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='listing'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={true}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Optional',
|
||||
key: 'optional',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='listing'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={false}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Summary',
|
||||
key: 'summary',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='listing'
|
||||
column={1}
|
||||
bordered={false}
|
||||
visibleProperties={{
|
||||
_id: false,
|
||||
createdAt: false,
|
||||
updatedAt: false,
|
||||
lastSyncedAt: false
|
||||
}}
|
||||
isEditing={false}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
return (
|
||||
<WizardView
|
||||
steps={steps}
|
||||
loading={submitLoading}
|
||||
formValid={formValid}
|
||||
title='New Listing'
|
||||
onSubmit={async () => {
|
||||
const result = await handleSubmit()
|
||||
if (result) {
|
||||
onOk()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</NewObjectForm>
|
||||
)
|
||||
}
|
||||
|
||||
NewListing.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
reset: PropTypes.bool,
|
||||
defaultValues: PropTypes.object
|
||||
}
|
||||
|
||||
export default NewListing
|
||||
50
src/components/Dashboard/Sales/Listings/PublishListing.jsx
Normal file
50
src/components/Dashboard/Sales/Listings/PublishListing.jsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { useState, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import { message } from 'antd'
|
||||
import MessageDialogView from '../../common/MessageDialogView.jsx'
|
||||
|
||||
const PublishListing = ({ onOk, objectData }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { sendObjectFunction } = useContext(ApiServerContext)
|
||||
|
||||
const handlePublish = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await sendObjectFunction(objectData._id, 'Listing', 'publish')
|
||||
if (result) {
|
||||
message.success('Published successfully')
|
||||
onOk(result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error publishing listing:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const ref =
|
||||
objectData?.title ||
|
||||
objectData?._reference ||
|
||||
objectData?.name ||
|
||||
objectData?._id
|
||||
|
||||
return (
|
||||
<MessageDialogView
|
||||
title='Publish this listing on the marketplace?'
|
||||
description={`Each variant with a SKU will be published on the connected marketplace. Variants that are already active are skipped.${
|
||||
ref ? ` (listing: ${ref})` : ''
|
||||
}`}
|
||||
onOk={handlePublish}
|
||||
okText='Publish'
|
||||
okLoading={loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
PublishListing.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
objectData: PropTypes.object
|
||||
}
|
||||
|
||||
export default PublishListing
|
||||
50
src/components/Dashboard/Sales/Listings/UnpublishListing.jsx
Normal file
50
src/components/Dashboard/Sales/Listings/UnpublishListing.jsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { useState, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import { message } from 'antd'
|
||||
import MessageDialogView from '../../common/MessageDialogView.jsx'
|
||||
|
||||
const UnpublishListing = ({ onOk, objectData }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { sendObjectFunction } = useContext(ApiServerContext)
|
||||
|
||||
const handleUnpublish = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await sendObjectFunction(objectData._id, 'Listing', 'unpublish')
|
||||
if (result) {
|
||||
message.success('Unpublished successfully')
|
||||
onOk(result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error unpublishing listing:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const ref =
|
||||
objectData?.title ||
|
||||
objectData?._reference ||
|
||||
objectData?.name ||
|
||||
objectData?._id
|
||||
|
||||
return (
|
||||
<MessageDialogView
|
||||
title='Unpublish all variants for this listing?'
|
||||
description={`Ends live marketplace listings for every active variant. On eBay the item is withdrawn and can be published again later.${
|
||||
ref ? ` (listing: ${ref})` : ''
|
||||
}`}
|
||||
onOk={handleUnpublish}
|
||||
okText='Unpublish'
|
||||
okLoading={loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
UnpublishListing.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
objectData: PropTypes.object
|
||||
}
|
||||
|
||||
export default UnpublishListing
|
||||
@ -0,0 +1,68 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Result, Typography, Flex, Button } from 'antd'
|
||||
import CopyButton from '../../common/CopyButton'
|
||||
import MarketplaceIcon from '../../../Icons/MarketplaceIcon'
|
||||
import { getMarketplaceCallbackUrl } from './authUtils'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const ConfigureMarketplace = ({
|
||||
onConnect,
|
||||
isConnected = false,
|
||||
loading = false,
|
||||
disabled = false
|
||||
}) => {
|
||||
const callbackUrl = getMarketplaceCallbackUrl()
|
||||
|
||||
return (
|
||||
<Flex vertical align='center'>
|
||||
<Result
|
||||
title='Marketplace Connection'
|
||||
subTitle={
|
||||
<Text>
|
||||
Use this callback URL for TikTok Shop redirects and as the eBay
|
||||
RuName accept URL target. Click Connect to authorize your
|
||||
marketplace account.
|
||||
</Text>
|
||||
}
|
||||
icon={<MarketplaceIcon />}
|
||||
>
|
||||
<Flex
|
||||
vertical
|
||||
gap='middle'
|
||||
align='center'
|
||||
style={{ minWidth: '395px' }}
|
||||
>
|
||||
<Flex justify='center'>
|
||||
<Flex gap='small' align='center' justify='center'>
|
||||
<CopyButton size='default' text={callbackUrl} />
|
||||
<Text code style={{ fontSize: '14px', wordBreak: 'break-all' }}>
|
||||
{callbackUrl}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex justify='center'>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={onConnect}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
key='connect'
|
||||
>
|
||||
{isConnected ? 'Reconnect' : 'Connect'}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Result>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
ConfigureMarketplace.propTypes = {
|
||||
onConnect: PropTypes.func,
|
||||
isConnected: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
export default ConfigureMarketplace
|
||||
@ -0,0 +1,186 @@
|
||||
import { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { Alert, Button, Card, Flex, message } from 'antd'
|
||||
import CheckIcon from '../../../Icons/CheckIcon'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import {
|
||||
clearMarketplaceAuthState,
|
||||
parseMarketplaceAuthState,
|
||||
readMarketplaceAuthState
|
||||
} from './authUtils'
|
||||
import { AuthContext } from '../../context/AuthContext'
|
||||
import AuthParticles from '../../../App/AppParticles'
|
||||
import FarmControlLogo from '../../../Logos/FarmControlLogo'
|
||||
import ExclamationOctagonIcon from '../../../Icons/ExclamationOctagonIcon'
|
||||
import ArrowLeftIcon from '../../../Icons/ArrowLeftIcon'
|
||||
|
||||
const MarketplaceAuthCallback = () => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { token } = useContext(AuthContext)
|
||||
const { sendObjectFunction, connected } = useContext(ApiServerContext)
|
||||
const [status, setStatus] = useState('loading')
|
||||
const [redirectLoading, setRedirectLoading] = useState(false)
|
||||
const [resultMessage, setResultMessage] = useState('')
|
||||
|
||||
const params = useMemo(
|
||||
() => new URLSearchParams(location.search),
|
||||
[location.search]
|
||||
)
|
||||
const rawState = params.get('state') || ''
|
||||
const parsedState = parseMarketplaceAuthState(rawState)
|
||||
const storedState = readMarketplaceAuthState(rawState)
|
||||
const marketplaceId =
|
||||
params.get('marketplaceId') ||
|
||||
parsedState?.marketplaceId ||
|
||||
storedState?.marketplaceId ||
|
||||
''
|
||||
const returnTo =
|
||||
parsedState?.returnTo ||
|
||||
storedState?.returnTo ||
|
||||
(marketplaceId
|
||||
? `/dashboard/sales/marketplaces/info?marketplaceId=${marketplaceId}`
|
||||
: '/dashboard/sales/marketplaces')
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || token == null || !token) {
|
||||
return
|
||||
}
|
||||
|
||||
const error = params.get('error') || params.get('error_description')
|
||||
const code = params.get('code') || params.get('auth_code')
|
||||
|
||||
if (error) {
|
||||
setStatus('error')
|
||||
setResultMessage(error)
|
||||
if (rawState) {
|
||||
clearMarketplaceAuthState(rawState)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!marketplaceId) {
|
||||
setStatus('error')
|
||||
setResultMessage('Could not determine which marketplace to connect.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
setStatus('error')
|
||||
setResultMessage('Authorization code was not returned by the provider.')
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const exchangeCode = async () => {
|
||||
try {
|
||||
const result = await sendObjectFunction(
|
||||
marketplaceId,
|
||||
'Marketplace',
|
||||
'auth/exchange',
|
||||
{
|
||||
code,
|
||||
state: rawState
|
||||
}
|
||||
)
|
||||
if (!result?.success) {
|
||||
throw new Error(
|
||||
result?.error || 'Failed to complete marketplace authorization.'
|
||||
)
|
||||
}
|
||||
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
setStatus('success')
|
||||
setResultMessage('Marketplace authorization completed successfully.')
|
||||
message.success('Marketplace connected')
|
||||
if (rawState) {
|
||||
clearMarketplaceAuthState(rawState)
|
||||
}
|
||||
} catch (error) {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error('Marketplace auth callback failed:', error)
|
||||
setStatus('error')
|
||||
setResultMessage(
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
'Failed to complete marketplace authorization.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
exchangeCode()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [marketplaceId, params, rawState, sendObjectFunction, token, connected])
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthParticles />
|
||||
<Flex
|
||||
align='center'
|
||||
justify='center'
|
||||
vertical
|
||||
style={{ height: '100vh' }}
|
||||
gap='large'
|
||||
>
|
||||
<Card style={{ borderRadius: 20 }}>
|
||||
<Flex vertical align='center'>
|
||||
<FarmControlLogo style={{ fontSize: '500px', height: '40px' }} />
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
{status != 'error' && status != 'success' && (
|
||||
<Alert
|
||||
message='Completing provider authorization...'
|
||||
icon={<LoadingOutlined />}
|
||||
type='info'
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<Alert
|
||||
message={resultMessage}
|
||||
icon={<CheckIcon />}
|
||||
type='success'
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<Alert
|
||||
message={resultMessage}
|
||||
icon={<ExclamationOctagonIcon />}
|
||||
type='error'
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
<Flex gap='middle'>
|
||||
<Button
|
||||
icon={<ArrowLeftIcon />}
|
||||
onClick={() => {
|
||||
setRedirectLoading(true)
|
||||
navigate(returnTo)
|
||||
}}
|
||||
loading={redirectLoading}
|
||||
disabled={status === 'loading' || redirectLoading}
|
||||
size='large'
|
||||
></Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarketplaceAuthCallback
|
||||
@ -1,6 +1,6 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useContext, useRef, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Space, Flex } from 'antd'
|
||||
import { Space, Flex, Modal, message } from 'antd'
|
||||
import loglevel from 'loglevel'
|
||||
import config from '../../../../config'
|
||||
import useCollapseState from '../../hooks/useCollapseState'
|
||||
@ -22,6 +22,15 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||
import { Card } from 'antd'
|
||||
import SyncListings from './SyncListings.jsx'
|
||||
import SyncOrders from './SyncOrders.jsx'
|
||||
import ConfigureMarketplace from './ConfigureMarketplace.jsx'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext.jsx'
|
||||
import { ElectronContext } from '../../context/ElectronContext.jsx'
|
||||
import {
|
||||
buildMarketplaceAuthState,
|
||||
storeMarketplaceAuthState
|
||||
} from './authUtils.js'
|
||||
|
||||
const log = loglevel.getLogger('MarketplaceInfo')
|
||||
log.setLevel(config.logLevel)
|
||||
@ -30,12 +39,20 @@ const MarketplaceInfo = () => {
|
||||
const location = useLocation()
|
||||
const objectFormRef = useRef(null)
|
||||
const actionHandlerRef = useRef(null)
|
||||
const marketplaceId = new URLSearchParams(location.search).get('marketplaceId')
|
||||
const [collapseState, updateCollapseState] = useCollapseState('MarketplaceInfo', {
|
||||
const { getMarketplaceAuthUrl, refreshMarketplaceAuth } =
|
||||
useContext(ApiServerContext)
|
||||
const { openExternalUrl } = useContext(ElectronContext)
|
||||
const marketplaceId = new URLSearchParams(location.search).get(
|
||||
'marketplaceId'
|
||||
)
|
||||
const [collapseState, updateCollapseState] = useCollapseState(
|
||||
'MarketplaceInfo',
|
||||
{
|
||||
info: true,
|
||||
notes: true,
|
||||
auditLogs: true
|
||||
})
|
||||
}
|
||||
)
|
||||
const [objectFormState, setEditFormState] = useState({
|
||||
isEditing: false,
|
||||
editLoading: false,
|
||||
@ -44,6 +61,55 @@ const MarketplaceInfo = () => {
|
||||
loading: false,
|
||||
objectData: {}
|
||||
})
|
||||
const [syncListingsOpen, setSyncListingsOpen] = useState(false)
|
||||
const [syncOrdersOpen, setSyncOrdersOpen] = useState(false)
|
||||
const [configureModalOpen, setConfigureModalOpen] = useState(false)
|
||||
|
||||
const startAuthorization = async () => {
|
||||
const objectData = objectFormState.objectData
|
||||
if (!objectData?._id) return
|
||||
|
||||
if (objectFormState.isEditing) {
|
||||
message.warning('Save marketplace changes before starting authorization')
|
||||
return
|
||||
}
|
||||
|
||||
const returnTo = `/dashboard/sales/marketplaces/info?marketplaceId=${objectData._id}`
|
||||
const state = buildMarketplaceAuthState({
|
||||
marketplaceId: objectData._id,
|
||||
returnTo
|
||||
})
|
||||
|
||||
storeMarketplaceAuthState(state, {
|
||||
marketplaceId: objectData._id,
|
||||
returnTo
|
||||
})
|
||||
|
||||
const result = await getMarketplaceAuthUrl(objectData._id, state)
|
||||
|
||||
if (!result?.url) {
|
||||
message.error('Authorization URL was not returned')
|
||||
return
|
||||
}
|
||||
|
||||
const openedExternally = openExternalUrl(result.url)
|
||||
if (!openedExternally) {
|
||||
window.location.assign(result.url)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshToken = async () => {
|
||||
const objectData = objectFormState.objectData
|
||||
if (!objectData?._id) return
|
||||
|
||||
const result = await refreshMarketplaceAuth(objectData._id)
|
||||
if (!result?.success) {
|
||||
message.error(result?.error || 'Token refresh failed')
|
||||
return
|
||||
}
|
||||
message.success('Marketplace token refreshed')
|
||||
objectFormRef?.current?.handleFetchObject?.()
|
||||
}
|
||||
|
||||
const actions = {
|
||||
reload: () => {
|
||||
@ -65,6 +131,30 @@ const MarketplaceInfo = () => {
|
||||
delete: () => {
|
||||
objectFormRef?.current?.handleDelete?.()
|
||||
return true
|
||||
},
|
||||
syncListings: () => {
|
||||
setSyncListingsOpen(true)
|
||||
return true
|
||||
},
|
||||
syncOrders: () => {
|
||||
setSyncOrdersOpen(true)
|
||||
return true
|
||||
},
|
||||
configure: () => {
|
||||
setConfigureModalOpen(true)
|
||||
return true
|
||||
},
|
||||
connect: () => {
|
||||
startAuthorization()
|
||||
return true
|
||||
},
|
||||
reconnect: () => {
|
||||
startAuthorization()
|
||||
return true
|
||||
},
|
||||
refreshToken: () => {
|
||||
handleRefreshToken()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,6 +244,7 @@ const MarketplaceInfo = () => {
|
||||
loading={loading}
|
||||
isEditing={isEditing}
|
||||
type='marketplace'
|
||||
labelWidth={215}
|
||||
objectData={objectData}
|
||||
/>
|
||||
)}
|
||||
@ -193,6 +284,62 @@ const MarketplaceInfo = () => {
|
||||
</Flex>
|
||||
</ScrollBox>
|
||||
</Flex>
|
||||
<Modal
|
||||
open={syncListingsOpen}
|
||||
onCancel={() => setSyncListingsOpen(false)}
|
||||
width={500}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
centered
|
||||
>
|
||||
<SyncListings
|
||||
onOk={() => {
|
||||
setSyncListingsOpen(false)
|
||||
actions.reload()
|
||||
}}
|
||||
objectData={objectFormState.objectData}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={syncOrdersOpen}
|
||||
onCancel={() => setSyncOrdersOpen(false)}
|
||||
width={500}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
centered
|
||||
>
|
||||
<SyncOrders
|
||||
onOk={() => {
|
||||
setSyncOrdersOpen(false)
|
||||
actions.reload()
|
||||
}}
|
||||
objectData={objectFormState.objectData}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={configureModalOpen}
|
||||
onCancel={() => setConfigureModalOpen(false)}
|
||||
width={650}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
centered
|
||||
>
|
||||
<ConfigureMarketplace
|
||||
onConnect={() => {
|
||||
startAuthorization()
|
||||
setConfigureModalOpen(false)
|
||||
}}
|
||||
isConnected={
|
||||
!!(
|
||||
objectFormState.objectData?.config?.refreshToken ||
|
||||
objectFormState.objectData?.config?.accessToken ||
|
||||
objectFormState.objectData?.config?.shopCipher
|
||||
)
|
||||
}
|
||||
loading={objectFormState.loading}
|
||||
disabled={objectFormState.isEditing}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,12 +2,21 @@ import PropTypes from 'prop-types'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import NewObjectForm from '../../common/NewObjectForm'
|
||||
import WizardView from '../../common/WizardView'
|
||||
import { getMarketplaceCallbackUrl } from './authUtils'
|
||||
|
||||
const NewMarketplace = ({ onOk, defaultValues }) => {
|
||||
const callbackUrl = getMarketplaceCallbackUrl()
|
||||
|
||||
return (
|
||||
<NewObjectForm
|
||||
type={'marketplace'}
|
||||
defaultValues={{ active: true, ...defaultValues }}
|
||||
defaultValues={{
|
||||
active: true,
|
||||
config: {
|
||||
redirectUri: callbackUrl
|
||||
},
|
||||
...defaultValues
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||
const steps = [
|
||||
@ -60,14 +69,23 @@ const NewMarketplace = ({ onOk, defaultValues }) => {
|
||||
_id: false,
|
||||
createdAt: false,
|
||||
updatedAt: false,
|
||||
'config.appId': false,
|
||||
'config.certId': false,
|
||||
'config.devId': false,
|
||||
'config.userToken': false,
|
||||
authConnected: false,
|
||||
'config.clientId': false,
|
||||
'config.clientSecret': false,
|
||||
'config.ruName': false,
|
||||
'config.marketplaceId': false,
|
||||
'config.sandbox': false,
|
||||
'config.verificationToken': false,
|
||||
'config.redirectUri': false,
|
||||
'config.accessToken': false,
|
||||
'config.refreshToken': false,
|
||||
'config.refreshTokenExpiresAt': false,
|
||||
'config.accessTokenExpiresAt': false,
|
||||
'config.lastTokenRefreshAt': false,
|
||||
connectedAt: false,
|
||||
'config.appKey': false,
|
||||
'config.appSecret': false
|
||||
'config.appSecret': false,
|
||||
'config.shopCipher': false
|
||||
}}
|
||||
isEditing={false}
|
||||
objectData={objectData}
|
||||
|
||||
46
src/components/Dashboard/Sales/Marketplaces/SyncListings.jsx
Normal file
46
src/components/Dashboard/Sales/Marketplaces/SyncListings.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useState, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import { message } from 'antd'
|
||||
import MessageDialogView from '../../common/MessageDialogView.jsx'
|
||||
|
||||
const SyncListings = ({ onOk, objectData }) => {
|
||||
const [syncLoading, setSyncLoading] = useState(false)
|
||||
const { sendObjectFunction } = useContext(ApiServerContext)
|
||||
|
||||
const handleSync = async () => {
|
||||
setSyncLoading(true)
|
||||
try {
|
||||
const result = await sendObjectFunction(
|
||||
objectData._id,
|
||||
'Marketplace',
|
||||
'sync/items'
|
||||
)
|
||||
if (result) {
|
||||
message.success('Listings synced successfully')
|
||||
onOk(result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing listings:', error)
|
||||
} finally {
|
||||
setSyncLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageDialogView
|
||||
title='Sync listings from marketplace?'
|
||||
description={`This will sync product listings from ${objectData?.name || objectData?._reference || objectData?._id} to your local database.`}
|
||||
onOk={handleSync}
|
||||
okText='Sync'
|
||||
okLoading={syncLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
SyncListings.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
objectData: PropTypes.object
|
||||
}
|
||||
|
||||
export default SyncListings
|
||||
46
src/components/Dashboard/Sales/Marketplaces/SyncOrders.jsx
Normal file
46
src/components/Dashboard/Sales/Marketplaces/SyncOrders.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useState, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||
import { message } from 'antd'
|
||||
import MessageDialogView from '../../common/MessageDialogView.jsx'
|
||||
|
||||
const SyncOrders = ({ onOk, objectData }) => {
|
||||
const [syncLoading, setSyncLoading] = useState(false)
|
||||
const { sendObjectFunction } = useContext(ApiServerContext)
|
||||
|
||||
const handleSync = async () => {
|
||||
setSyncLoading(true)
|
||||
try {
|
||||
const result = await sendObjectFunction(
|
||||
objectData._id,
|
||||
'Marketplace',
|
||||
'sync/orders'
|
||||
)
|
||||
if (result) {
|
||||
message.success('Orders synced successfully')
|
||||
onOk(result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing orders:', error)
|
||||
} finally {
|
||||
setSyncLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageDialogView
|
||||
title='Sync orders from marketplace?'
|
||||
description={`This will sync orders from ${objectData?.name || objectData?._reference || objectData?._id} to your local database.`}
|
||||
onOk={handleSync}
|
||||
okText='Sync'
|
||||
okLoading={syncLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
SyncOrders.propTypes = {
|
||||
onOk: PropTypes.func.isRequired,
|
||||
objectData: PropTypes.object
|
||||
}
|
||||
|
||||
export default SyncOrders
|
||||
68
src/components/Dashboard/Sales/Marketplaces/authUtils.js
Normal file
68
src/components/Dashboard/Sales/Marketplaces/authUtils.js
Normal file
@ -0,0 +1,68 @@
|
||||
export const MARKETPLACE_AUTH_STATE_KEY = 'marketplace-auth-state'
|
||||
|
||||
export function buildMarketplaceAuthState({ marketplaceId, returnTo } = {}) {
|
||||
return btoa(
|
||||
JSON.stringify({
|
||||
marketplaceId,
|
||||
returnTo
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function parseMarketplaceAuthState(rawState) {
|
||||
if (!rawState) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(atob(rawState))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getMarketplaceCallbackUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `${window.location.origin}/auth/marketplace/callback`
|
||||
}
|
||||
|
||||
export function storeMarketplaceAuthState(rawState, data) {
|
||||
if (typeof window === 'undefined' || !rawState) {
|
||||
return
|
||||
}
|
||||
|
||||
window.sessionStorage.setItem(
|
||||
`${MARKETPLACE_AUTH_STATE_KEY}:${rawState}`,
|
||||
JSON.stringify(data)
|
||||
)
|
||||
}
|
||||
|
||||
export function readMarketplaceAuthState(rawState) {
|
||||
if (typeof window === 'undefined' || !rawState) {
|
||||
return null
|
||||
}
|
||||
|
||||
const stored = window.sessionStorage.getItem(
|
||||
`${MARKETPLACE_AUTH_STATE_KEY}:${rawState}`
|
||||
)
|
||||
if (!stored) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function clearMarketplaceAuthState(rawState) {
|
||||
if (typeof window === 'undefined' || !rawState) {
|
||||
return
|
||||
}
|
||||
|
||||
window.sessionStorage.removeItem(`${MARKETPLACE_AUTH_STATE_KEY}:${rawState}`)
|
||||
}
|
||||
@ -44,4 +44,3 @@ CancelSalesOrder.propTypes = {
|
||||
}
|
||||
|
||||
export default CancelSalesOrder
|
||||
|
||||
|
||||
@ -44,4 +44,3 @@ ConfirmSalesOrder.propTypes = {
|
||||
}
|
||||
|
||||
export default ConfirmSalesOrder
|
||||
|
||||
|
||||
@ -44,4 +44,3 @@ PostSalesOrder.propTypes = {
|
||||
}
|
||||
|
||||
export default PostSalesOrder
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import ClientIcon from '../../Icons/ClientIcon'
|
||||
import SalesIcon from '../../Icons/SalesIcon'
|
||||
import SalesOrderIcon from '../../Icons/SalesOrderIcon'
|
||||
import MarketplaceIcon from '../../Icons/MarketplaceIcon'
|
||||
import ListingIcon from '../../Icons/ListingIcon'
|
||||
|
||||
const items = [
|
||||
{
|
||||
@ -30,6 +31,12 @@ const items = [
|
||||
label: 'Marketplaces',
|
||||
icon: <MarketplaceIcon />,
|
||||
path: '/dashboard/sales/marketplaces'
|
||||
},
|
||||
{
|
||||
key: 'listings',
|
||||
label: 'Listings',
|
||||
icon: <ListingIcon />,
|
||||
path: '/dashboard/sales/listings'
|
||||
}
|
||||
]
|
||||
|
||||
@ -37,7 +44,9 @@ const routeKeyMap = {
|
||||
'/dashboard/sales/overview': 'overview',
|
||||
'/dashboard/sales/clients': 'clients',
|
||||
'/dashboard/sales/salesorders': 'salesorders',
|
||||
'/dashboard/sales/marketplaces': 'marketplaces'
|
||||
'/dashboard/sales/marketplaces': 'marketplaces',
|
||||
'/dashboard/sales/listings': 'listings',
|
||||
'/dashboard/sales/listingvarients': 'listings'
|
||||
}
|
||||
|
||||
const SalesSidebar = (props) => {
|
||||
|
||||
@ -143,6 +143,10 @@ const ObjectProperty = ({
|
||||
if (name?.includes('.')) {
|
||||
formItemName = name ? name.split('.') : undefined
|
||||
}
|
||||
// For state type with options (editable), form field is state.type
|
||||
if (type === 'state' && options?.length && !readOnly) {
|
||||
formItemName = ['state', 'type']
|
||||
}
|
||||
|
||||
var textParams = { style: { whiteSpace: 'nowrap', minWidth: '0' } }
|
||||
|
||||
@ -662,6 +666,18 @@ const ObjectProperty = ({
|
||||
{...inputProps}
|
||||
/>
|
||||
)
|
||||
case 'state':
|
||||
if (options?.length && !readOnly) {
|
||||
return (
|
||||
<CustomSelect
|
||||
placeholder={'Select a ' + label.toLowerCase() + '...'}
|
||||
options={Array.isArray(options) ? options : []}
|
||||
{...inputProps}
|
||||
{...(useFormItem ? {} : { value: value?.type })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
case 'priceMode':
|
||||
return (
|
||||
<Select
|
||||
|
||||
@ -80,6 +80,30 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
|
||||
status = 'default'
|
||||
text = 'Draft'
|
||||
break
|
||||
case 'active':
|
||||
status = 'success'
|
||||
text = 'Active'
|
||||
break
|
||||
case 'inactive':
|
||||
status = 'default'
|
||||
text = 'Inactive'
|
||||
break
|
||||
case 'deleted':
|
||||
status = 'error'
|
||||
text = 'Deleted'
|
||||
break
|
||||
case 'suspended':
|
||||
status = 'warning'
|
||||
text = 'Suspended'
|
||||
break
|
||||
case 'syncing':
|
||||
status = 'processing'
|
||||
text = 'Syncing'
|
||||
break
|
||||
case 'disconnected':
|
||||
status = 'default'
|
||||
text = 'Disconnected'
|
||||
break
|
||||
case 'failed':
|
||||
status = 'error'
|
||||
text = 'Failed'
|
||||
|
||||
@ -30,10 +30,7 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Text
|
||||
style={{ marginRight: 8, minWidth: 0, flex: 1, width: 0 }}
|
||||
ellipsis
|
||||
>
|
||||
<Text style={{ marginRight: 8, minWidth: 0 }} ellipsis>
|
||||
{url}
|
||||
</Text>
|
||||
<Tooltip title='Open URL' arrow={false}>
|
||||
|
||||
@ -939,6 +939,37 @@ const ApiServerProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getObjectFunction = async (id, type, functionName, params = {}) => {
|
||||
const url = `${config.backendUrl}/${type.toLowerCase()}s/${id}/${functionName}`
|
||||
logger.debug(`Fetching object function ${functionName} for ${id} at ${url}`)
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
params,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
return response.data
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showError(err, () => {
|
||||
getObjectFunction(id, type, functionName, params)
|
||||
})
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const getMarketplaceAuthUrl = async (marketplaceId, state) => {
|
||||
return getObjectFunction(marketplaceId, 'Marketplace', 'auth/url', {
|
||||
state
|
||||
})
|
||||
}
|
||||
|
||||
const refreshMarketplaceAuth = async (marketplaceId) => {
|
||||
return sendObjectFunction(marketplaceId, 'Marketplace', 'auth/refresh')
|
||||
}
|
||||
|
||||
// Export list data to CSV and download
|
||||
const exportToCsv = async (objectType) => {
|
||||
try {
|
||||
@ -1551,6 +1582,7 @@ const ApiServerProvider = ({ children }) => {
|
||||
updateObject,
|
||||
updateMultipleObjects,
|
||||
createObject,
|
||||
getObjectFunction,
|
||||
sendObjectFunction,
|
||||
deleteObject,
|
||||
subscribeToObjectUpdates,
|
||||
@ -1590,7 +1622,9 @@ const ApiServerProvider = ({ children }) => {
|
||||
deleteNotificationApi,
|
||||
deleteAllNotificationsApi,
|
||||
registerNotificationListener,
|
||||
unregisterNotificationListener
|
||||
unregisterNotificationListener,
|
||||
getMarketplaceAuthUrl,
|
||||
refreshMarketplaceAuth
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
|
||||
6
src/components/Icons/ListingIcon.jsx
Normal file
6
src/components/Icons/ListingIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
||||
import Icon from '@ant-design/icons'
|
||||
import CustomIconSvg from '../../../assets/icons/listingicon.svg?react'
|
||||
|
||||
const ListingIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||
|
||||
export default ListingIcon
|
||||
8
src/components/Icons/ListingVarientIcon.jsx
Normal file
8
src/components/Icons/ListingVarientIcon.jsx
Normal file
@ -0,0 +1,8 @@
|
||||
import Icon from '@ant-design/icons'
|
||||
import CustomIconSvg from '../../../assets/icons/listingvarienticon.svg?react'
|
||||
|
||||
const ListingVarientIcon = (props) => (
|
||||
<Icon component={CustomIconSvg} {...props} />
|
||||
)
|
||||
|
||||
export default ListingVarientIcon
|
||||
@ -40,6 +40,8 @@ import { Payment } from './models/Payment.js'
|
||||
import { Client } from './models/Client.js'
|
||||
import { SalesOrder } from './models/SalesOrder.js'
|
||||
import { Marketplace } from './models/Marketplace.js'
|
||||
import { Listing } from './models/Listing.js'
|
||||
import { ListingVarient } from './models/ListingVarient.js'
|
||||
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
|
||||
|
||||
export const objectModels = [
|
||||
@ -84,7 +86,9 @@ export const objectModels = [
|
||||
Payment,
|
||||
Client,
|
||||
SalesOrder,
|
||||
Marketplace
|
||||
Marketplace,
|
||||
Listing,
|
||||
ListingVarient
|
||||
]
|
||||
|
||||
// Re-export individual models for direct access
|
||||
@ -130,7 +134,9 @@ export {
|
||||
Payment,
|
||||
Client,
|
||||
SalesOrder,
|
||||
Marketplace
|
||||
Marketplace,
|
||||
Listing,
|
||||
ListingVarient
|
||||
}
|
||||
|
||||
export function getModelByName(name, ignoreCase = false) {
|
||||
|
||||
241
src/database/models/Listing.js
Normal file
241
src/database/models/Listing.js
Normal file
@ -0,0 +1,241 @@
|
||||
import ListingIcon from '../../components/Icons/ListingIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import BinIcon from '../../components/Icons/BinIcon'
|
||||
import PlusIcon from '../../components/Icons/PlusIcon'
|
||||
|
||||
export const Listing = {
|
||||
name: 'listing',
|
||||
label: 'Listing',
|
||||
prefix: 'LST',
|
||||
icon: ListingIcon,
|
||||
actions: [
|
||||
{
|
||||
name: 'info',
|
||||
label: 'Info',
|
||||
default: true,
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
url: (_id) => `/dashboard/sales/listings/info?listingId=${_id}`
|
||||
},
|
||||
{
|
||||
name: 'reload',
|
||||
label: 'Reload',
|
||||
icon: ReloadIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listings/info?listingId=${_id}&action=reload`
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
label: 'Edit',
|
||||
row: true,
|
||||
icon: EditIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listings/info?listingId=${_id}&action=edit`,
|
||||
visible: (objectData) => {
|
||||
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'finishEdit',
|
||||
label: 'Save Edits',
|
||||
icon: CheckIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listings/info?listingId=${_id}&action=finishEdit`,
|
||||
visible: (objectData) => {
|
||||
return objectData?._isEditing && objectData?._isEditing == true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cancelEdit',
|
||||
label: 'Cancel Edits',
|
||||
icon: XMarkIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listings/info?listingId=${_id}&action=cancelEdit`,
|
||||
visible: (objectData) => {
|
||||
return objectData?._isEditing && objectData?._isEditing == true
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'newListingVarient',
|
||||
label: 'New Listing Varient',
|
||||
type: 'button',
|
||||
icon: PlusIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listings/info?listingId=${_id}&action=newListingVarient`
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'publish',
|
||||
label: 'Publish',
|
||||
type: 'button',
|
||||
icon: CheckIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listings/info?listingId=${_id}&action=publish`,
|
||||
visible: (objectData) =>
|
||||
objectData?.state?.type === 'draft' ||
|
||||
objectData?.state?.type === 'inactive',
|
||||
disabled: (objectData) => objectData?.state?.type === 'syncing'
|
||||
},
|
||||
{
|
||||
name: 'unpublish',
|
||||
label: 'Unpublish',
|
||||
type: 'button',
|
||||
icon: XMarkIcon,
|
||||
danger: true,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listings/info?listingId=${_id}&action=unpublish`,
|
||||
visible: (objectData) => objectData?.state?.type === 'active',
|
||||
disabled: (objectData) => objectData?.state?.type === 'syncing'
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'delete',
|
||||
label: 'Delete',
|
||||
icon: BinIcon,
|
||||
danger: true,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listings/info?listingId=${_id}&action=delete`
|
||||
}
|
||||
],
|
||||
columns: [
|
||||
'_reference',
|
||||
'title',
|
||||
'product',
|
||||
'marketplace',
|
||||
'state',
|
||||
'price',
|
||||
'currency',
|
||||
'lastSyncedAt',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
],
|
||||
filters: [
|
||||
'title',
|
||||
'_id',
|
||||
'product',
|
||||
'marketplace',
|
||||
'state',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
],
|
||||
sorters: [
|
||||
'title',
|
||||
'state',
|
||||
'price',
|
||||
'lastSyncedAt',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_id'
|
||||
],
|
||||
group: ['marketplace', 'product'],
|
||||
properties: [
|
||||
{
|
||||
name: '_id',
|
||||
label: 'ID',
|
||||
columnFixed: 'left',
|
||||
type: 'id',
|
||||
objectType: 'listing',
|
||||
showCopy: true,
|
||||
columnWidth: 140
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
label: 'Created At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
{
|
||||
name: '_reference',
|
||||
label: 'Reference',
|
||||
type: 'reference',
|
||||
columnFixed: 'left',
|
||||
objectType: 'listing',
|
||||
showCopy: true,
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'updatedAt',
|
||||
label: 'Updated At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
columnFixed: 'left',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 250
|
||||
},
|
||||
{
|
||||
name: 'lastSyncedAt',
|
||||
label: 'Last Synced',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
{
|
||||
name: 'product',
|
||||
label: 'Product',
|
||||
type: 'object',
|
||||
objectType: 'product',
|
||||
showHyperlink: true,
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'marketplace',
|
||||
label: 'Marketplace',
|
||||
type: 'object',
|
||||
objectType: 'marketplace',
|
||||
showHyperlink: true,
|
||||
readOnly: false,
|
||||
required: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
label: 'State',
|
||||
type: 'state',
|
||||
objectType: 'listing',
|
||||
readOnly: true,
|
||||
columnWidth: 130
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
label: 'Price',
|
||||
type: 'number',
|
||||
prefix: '£',
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 120
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
label: 'Currency',
|
||||
type: 'text',
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 100
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
label: 'URL',
|
||||
type: 'url',
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 250
|
||||
}
|
||||
]
|
||||
}
|
||||
243
src/database/models/ListingVarient.js
Normal file
243
src/database/models/ListingVarient.js
Normal file
@ -0,0 +1,243 @@
|
||||
import ListingVarientIcon from '../../components/Icons/ListingVarientIcon'
|
||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||
import EditIcon from '../../components/Icons/EditIcon'
|
||||
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import BinIcon from '../../components/Icons/BinIcon'
|
||||
|
||||
export const ListingVarient = {
|
||||
name: 'listingVarient',
|
||||
label: 'Listing Varient',
|
||||
prefix: 'LVR',
|
||||
icon: ListingVarientIcon,
|
||||
actions: [
|
||||
{
|
||||
name: 'info',
|
||||
label: 'Info',
|
||||
default: true,
|
||||
row: true,
|
||||
icon: InfoCircleIcon,
|
||||
url: (_id) => `/dashboard/sales/listingvarients/info?listingVarientId=${_id}`
|
||||
},
|
||||
{
|
||||
name: 'reload',
|
||||
label: 'Reload',
|
||||
icon: ReloadIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=reload`
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
label: 'Edit',
|
||||
row: true,
|
||||
icon: EditIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=edit`,
|
||||
visible: (objectData) => {
|
||||
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'finishEdit',
|
||||
label: 'Save Edits',
|
||||
icon: CheckIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=finishEdit`,
|
||||
visible: (objectData) => {
|
||||
return objectData?._isEditing && objectData?._isEditing == true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cancelEdit',
|
||||
label: 'Cancel Edits',
|
||||
icon: XMarkIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=cancelEdit`,
|
||||
visible: (objectData) => {
|
||||
return objectData?._isEditing && objectData?._isEditing == true
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'publish',
|
||||
label: 'Publish',
|
||||
type: 'button',
|
||||
icon: CheckIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=publish`,
|
||||
visible: (objectData) =>
|
||||
objectData?.state?.type === 'draft' ||
|
||||
objectData?.state?.type === 'inactive',
|
||||
disabled: (objectData) => objectData?.state?.type === 'syncing'
|
||||
},
|
||||
{
|
||||
name: 'unpublish',
|
||||
label: 'Unpublish',
|
||||
type: 'button',
|
||||
icon: XMarkIcon,
|
||||
danger: true,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=unpublish`,
|
||||
visible: (objectData) => objectData?.state?.type === 'active',
|
||||
disabled: (objectData) => objectData?.state?.type === 'syncing'
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'delete',
|
||||
label: 'Delete',
|
||||
icon: BinIcon,
|
||||
danger: true,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/listingvarients/info?listingVarientId=${_id}&action=delete`
|
||||
}
|
||||
],
|
||||
columns: [
|
||||
'_reference',
|
||||
'listing',
|
||||
'product',
|
||||
'productSku',
|
||||
'state',
|
||||
'price',
|
||||
'currency',
|
||||
'lastSyncedAt',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
],
|
||||
filters: [
|
||||
'_id',
|
||||
'listing',
|
||||
'product',
|
||||
'productSku',
|
||||
'state',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
],
|
||||
sorters: [
|
||||
'state',
|
||||
'price',
|
||||
'lastSyncedAt',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_id'
|
||||
],
|
||||
group: ['listing', 'state'],
|
||||
properties: [
|
||||
{
|
||||
name: '_id',
|
||||
label: 'ID',
|
||||
columnFixed: 'left',
|
||||
type: 'id',
|
||||
objectType: 'listingVarient',
|
||||
showCopy: true,
|
||||
columnWidth: 140
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
label: 'Created At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
{
|
||||
name: '_reference',
|
||||
label: 'Reference',
|
||||
type: 'reference',
|
||||
columnFixed: 'left',
|
||||
objectType: 'listingVarient',
|
||||
showCopy: true,
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: 'updatedAt',
|
||||
label: 'Updated At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
{
|
||||
name: 'listing',
|
||||
label: 'Listing',
|
||||
type: 'object',
|
||||
objectType: 'listing',
|
||||
showHyperlink: true,
|
||||
readOnly: false,
|
||||
required: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'product',
|
||||
label: 'Product',
|
||||
type: 'object',
|
||||
objectType: 'product',
|
||||
showHyperlink: true,
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'productSku',
|
||||
label: 'Product SKU',
|
||||
type: 'object',
|
||||
objectType: 'productSku',
|
||||
showHyperlink: true,
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
label: 'State',
|
||||
type: 'state',
|
||||
objectType: 'listingVarient',
|
||||
readOnly: true,
|
||||
columnWidth: 130
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
label: 'Price',
|
||||
type: 'number',
|
||||
prefix: '£',
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 120
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
label: 'Currency',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 100
|
||||
},
|
||||
{
|
||||
name: 'priceTaxRate',
|
||||
label: 'Tax Rate',
|
||||
type: 'object',
|
||||
objectType: 'taxRate',
|
||||
showHyperlink: true,
|
||||
required: false,
|
||||
columnWidth: 150
|
||||
},
|
||||
{
|
||||
name: 'priceWithTax',
|
||||
label: 'Price w/ Tax',
|
||||
type: 'number',
|
||||
prefix: '£',
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 130
|
||||
},
|
||||
{
|
||||
name: 'lastSyncedAt',
|
||||
label: 'Last Synced',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 175
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -4,6 +4,7 @@ import EditIcon from '../../components/Icons/EditIcon'
|
||||
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||
import LinkIcon from '../../components/Icons/LinkIcon'
|
||||
import BinIcon from '../../components/Icons/BinIcon'
|
||||
|
||||
export const Marketplace = {
|
||||
@ -59,6 +60,67 @@ export const Marketplace = {
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'syncListings',
|
||||
label: 'Sync Listings',
|
||||
type: 'button',
|
||||
icon: ReloadIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=syncListings`,
|
||||
disabled: (objectData) => {
|
||||
return objectData?.state?.type != 'ready'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'syncOrders',
|
||||
label: 'Sync Orders',
|
||||
type: 'button',
|
||||
icon: ReloadIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=syncOrders`,
|
||||
disabled: (objectData) => {
|
||||
return objectData?.state?.type != 'ready'
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'connect',
|
||||
label: 'Connect',
|
||||
type: 'button',
|
||||
icon: LinkIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=connect`,
|
||||
disabled: (objectData) => {
|
||||
return (
|
||||
objectData?.state?.type == 'ready' ||
|
||||
objectData?.state?.type == 'connecting' ||
|
||||
objectData?.state?.type == 'syncing'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'reconnect',
|
||||
label: 'Reconnect',
|
||||
type: 'button',
|
||||
icon: LinkIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=reconnect`,
|
||||
disabled: (objectData) => {
|
||||
return objectData?.state?.type != 'ready'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'refreshToken',
|
||||
label: 'Refresh Token',
|
||||
type: 'button',
|
||||
icon: ReloadIcon,
|
||||
url: (_id) =>
|
||||
`/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=refreshToken`,
|
||||
disabled: (objectData) => {
|
||||
return objectData?.state?.type != 'ready'
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'delete',
|
||||
label: 'Delete',
|
||||
@ -72,12 +134,34 @@ export const Marketplace = {
|
||||
'_reference',
|
||||
'name',
|
||||
'provider',
|
||||
'authConnected',
|
||||
'state',
|
||||
'connected',
|
||||
'config.accessTokenExpiresAt',
|
||||
'active',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
],
|
||||
filters: ['name', '_id', 'provider', 'active', 'createdAt', 'updatedAt'],
|
||||
sorters: ['name', 'provider', 'active', 'createdAt', 'updatedAt', '_id'],
|
||||
filters: [
|
||||
'name',
|
||||
'_id',
|
||||
'provider',
|
||||
'active',
|
||||
'connected',
|
||||
'state',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
],
|
||||
sorters: [
|
||||
'name',
|
||||
'provider',
|
||||
'active',
|
||||
'connected',
|
||||
'state',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_id'
|
||||
],
|
||||
group: ['provider'],
|
||||
properties: [
|
||||
{
|
||||
@ -121,12 +205,12 @@ export const Marketplace = {
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
label: 'Active',
|
||||
type: 'bool',
|
||||
readOnly: false,
|
||||
required: true,
|
||||
columnWidth: 125
|
||||
name: 'connectedAt',
|
||||
label: 'Connected At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
showSince: true,
|
||||
columnWidth: 175
|
||||
},
|
||||
{
|
||||
name: 'provider',
|
||||
@ -140,10 +224,67 @@ export const Marketplace = {
|
||||
],
|
||||
columnWidth: 150
|
||||
},
|
||||
{
|
||||
name: 'config.accessTokenExpiresAt',
|
||||
label: 'Access Token Expires At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
columnWidth: 215
|
||||
},
|
||||
|
||||
{
|
||||
name: 'config.appId',
|
||||
label: 'App ID',
|
||||
name: 'state',
|
||||
label: 'State',
|
||||
type: 'state',
|
||||
objectType: 'marketplace',
|
||||
readOnly: true,
|
||||
columnWidth: 130
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
label: 'Active',
|
||||
type: 'bool',
|
||||
readOnly: false,
|
||||
required: true,
|
||||
columnWidth: 125
|
||||
},
|
||||
{
|
||||
name: 'connected',
|
||||
label: 'Connected',
|
||||
type: 'bool',
|
||||
readOnly: true,
|
||||
columnWidth: 125
|
||||
},
|
||||
{
|
||||
name: 'config.refreshTokenExpiresAt',
|
||||
label: 'Refresh Token Expires At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
showSince: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
{
|
||||
name: 'config.clientId',
|
||||
label: 'Client ID',
|
||||
type: 'secret',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'config.lastTokenRefreshAt',
|
||||
label: 'Last Token Refresh At',
|
||||
type: 'dateTime',
|
||||
readOnly: true,
|
||||
showSince: true,
|
||||
columnWidth: 200
|
||||
},
|
||||
|
||||
{
|
||||
name: 'config.clientSecret',
|
||||
label: 'Client Secret',
|
||||
type: 'secret',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
@ -151,49 +292,67 @@ export const Marketplace = {
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.certId',
|
||||
label: 'Cert ID',
|
||||
type: 'secret',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.devId',
|
||||
label: 'Dev ID',
|
||||
type: 'secret',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.userToken',
|
||||
label: 'User Token',
|
||||
type: 'secret',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.siteId',
|
||||
label: 'Site ID',
|
||||
name: 'config.ruName',
|
||||
label: 'RuName',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.marketplaceId',
|
||||
label: 'Marketplace ID',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 160,
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.sandbox',
|
||||
label: 'Sandbox',
|
||||
type: 'bool',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 120,
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.accessToken',
|
||||
label: 'Access Token',
|
||||
name: 'config.verificationToken',
|
||||
label: 'Verification Token',
|
||||
type: 'secret',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'etsy'
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.refreshToken',
|
||||
label: 'Refresh Token',
|
||||
type: 'secret',
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.accessToken',
|
||||
label: 'Access Token',
|
||||
type: 'secret',
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'ebay'
|
||||
},
|
||||
{
|
||||
name: 'config.redirectUri',
|
||||
label: 'Redirect URI',
|
||||
type: 'url',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 280,
|
||||
visible: (objectData) => objectData?.provider === 'tiktokShop'
|
||||
},
|
||||
{
|
||||
name: 'config.refreshToken',
|
||||
@ -202,6 +361,12 @@ export const Marketplace = {
|
||||
readOnly: false,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
value: (objectData) => {
|
||||
if (objectData?.config?.refreshToken) {
|
||||
return '••••••••••••••••••••••••••••••••••••'
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
visible: (objectData) => objectData?.provider === 'etsy'
|
||||
},
|
||||
{
|
||||
@ -213,6 +378,22 @@ export const Marketplace = {
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'etsy'
|
||||
},
|
||||
{
|
||||
name: 'config.accessToken',
|
||||
label: 'Access Token',
|
||||
type: 'secret',
|
||||
readOnly: (objectData) => objectData?.provider === 'tiktokShop',
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
value: (objectData) => {
|
||||
if (objectData?.config?.accessToken) {
|
||||
return '••••••••••••••••••••••••••••••••••••'
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
visible: (objectData) =>
|
||||
objectData?.provider === 'etsy' || objectData?.provider === 'tiktokShop'
|
||||
},
|
||||
{
|
||||
name: 'config.appKey',
|
||||
label: 'App Key',
|
||||
@ -235,10 +416,45 @@ export const Marketplace = {
|
||||
name: 'config.shopCipher',
|
||||
label: 'Shop Cipher',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'tiktokShop'
|
||||
},
|
||||
{
|
||||
name: 'config.shopName',
|
||||
label: 'Shop Name',
|
||||
type: 'text',
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 200,
|
||||
visible: (objectData) => objectData?.provider === 'tiktokShop'
|
||||
},
|
||||
{
|
||||
name: 'config.shopRegion',
|
||||
label: 'Shop Region',
|
||||
type: 'text',
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 140,
|
||||
visible: (objectData) => objectData?.provider === 'tiktokShop'
|
||||
},
|
||||
{
|
||||
name: 'config.callbackUrl',
|
||||
label: 'Callback URL',
|
||||
type: 'url',
|
||||
readOnly: true,
|
||||
required: false,
|
||||
columnWidth: 280,
|
||||
visible: (objectData) =>
|
||||
objectData?.provider === 'ebay' ||
|
||||
objectData?.provider === 'tiktokShop',
|
||||
value: () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return undefined
|
||||
}
|
||||
return `${window.location.origin}/auth/marketplace/callback`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { lazy } from 'react'
|
||||
import { Route } from 'react-router-dom'
|
||||
|
||||
const Clients = lazy(
|
||||
() => import('../components/Dashboard/Sales/Clients.jsx')
|
||||
)
|
||||
const Clients = lazy(() => import('../components/Dashboard/Sales/Clients.jsx'))
|
||||
const ClientInfo = lazy(
|
||||
() => import('../components/Dashboard/Sales/Clients/ClientInfo.jsx')
|
||||
)
|
||||
@ -22,32 +20,56 @@ const Marketplaces = lazy(
|
||||
const MarketplaceInfo = lazy(
|
||||
() => import('../components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx')
|
||||
)
|
||||
const Listings = lazy(
|
||||
() => import('../components/Dashboard/Sales/Listings.jsx')
|
||||
)
|
||||
const ListingInfo = lazy(
|
||||
() => import('../components/Dashboard/Sales/Listings/ListingInfo.jsx')
|
||||
)
|
||||
const ListingVarientInfo = lazy(
|
||||
() =>
|
||||
import('../components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx')
|
||||
)
|
||||
|
||||
const SalesRoutes = [
|
||||
<Route
|
||||
key='overview'
|
||||
path='sales/overview'
|
||||
element={<SalesOverview />}
|
||||
/>,
|
||||
<Route key='overview' path='sales/overview' element={<SalesOverview />} />,
|
||||
<Route key='clients' path='sales/clients' element={<Clients />} />,
|
||||
<Route
|
||||
key='clients-info'
|
||||
path='sales/clients/info'
|
||||
element={<ClientInfo />}
|
||||
/>,
|
||||
<Route key='salesorders' path='sales/salesorders' element={<SalesOrders />} />,
|
||||
<Route
|
||||
key='salesorders'
|
||||
path='sales/salesorders'
|
||||
element={<SalesOrders />}
|
||||
/>,
|
||||
<Route
|
||||
key='salesorders-info'
|
||||
path='sales/salesorders/info'
|
||||
element={<SalesOrderInfo />}
|
||||
/>,
|
||||
<Route key='marketplaces' path='sales/marketplaces' element={<Marketplaces />} />,
|
||||
<Route
|
||||
key='marketplaces'
|
||||
path='sales/marketplaces'
|
||||
element={<Marketplaces />}
|
||||
/>,
|
||||
<Route
|
||||
key='marketplaces-info'
|
||||
path='sales/marketplaces/info'
|
||||
element={<MarketplaceInfo />}
|
||||
/>,
|
||||
<Route key='listings' path='sales/listings' element={<Listings />} />,
|
||||
<Route
|
||||
key='listings-info'
|
||||
path='sales/listings/info'
|
||||
element={<ListingInfo />}
|
||||
/>,
|
||||
<Route
|
||||
key='listingvarients-info'
|
||||
path='sales/listingvarients/info'
|
||||
element={<ListingVarientInfo />}
|
||||
/>
|
||||
]
|
||||
|
||||
export default SalesRoutes
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user