feat(admin-ui): bulk advanced selections + copy/paste (#4568)
* wip: bulk editor copy/paste * feat: exit edit mode with "enter" press * wip: arrow navigation + onEnter * wip: 2D select + arrow navigation * feat: arrow navigation and multiselect, tabs navigation and multiselect * fix: region cols offset * feat: 2d copy * feat: 2d paste * fix: trailing tab * fix: borders * feat: ensure consistent copy order * fix: off by one col, pass `cmd` keypress * feat: `cmd` select * refactor: cleanup 1 * refactor: cleanup 2, utils * fix: copy paste * fix: copy paste indicator * fix: reduce dashed border size * fix: issue with leading empty cell * feat: cp support 2 formats of content, notification on copy, remove dashed box * fix: last empty cell case * feat: buffer content edge cases * refactor: remove log * feat: past fill selected area * feat: simplify copy-paste * fix: throw error if textual cell is in the buffer * Create eighty-zebras-grow.md --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
1 parent
0887e5007f
commit
4a448b68fd
4 files changed
+432
-59
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/admin-ui": patch
|
||||||
|
"@medusajs/admin": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(admin-ui): bulk advanced selections + copy/paste
|
||||||
+29
-6
@@ -37,6 +37,12 @@ type CurrencyCellProps = {
|
|||||||
isRangeEnd: boolean
|
isRangeEnd: boolean
|
||||||
isInRange: boolean
|
isInRange: boolean
|
||||||
|
|
||||||
|
isRangeStartCol: boolean
|
||||||
|
isRangeEndCol: boolean
|
||||||
|
isInRangeCol: boolean
|
||||||
|
|
||||||
|
onColumnOver: (currencyOrRegion: string) => void
|
||||||
|
|
||||||
onDragFillStart: (
|
onDragFillStart: (
|
||||||
variantId: string,
|
variantId: string,
|
||||||
currencyCode?: string,
|
currencyCode?: string,
|
||||||
@@ -79,6 +85,7 @@ function CurrencyCell(props: CurrencyCellProps) {
|
|||||||
isInRange,
|
isInRange,
|
||||||
isRangeStart,
|
isRangeStart,
|
||||||
isRangeEnd,
|
isRangeEnd,
|
||||||
|
onColumnOver,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const ref = useRef()
|
const ref = useRef()
|
||||||
@@ -135,6 +142,15 @@ function CurrencyCell(props: CurrencyCellProps) {
|
|||||||
* If we use set timout it will work as expected.
|
* If we use set timout it will work as expected.
|
||||||
*/
|
*/
|
||||||
setTimeout(() => ref.current.focus())
|
setTimeout(() => ref.current.focus())
|
||||||
|
|
||||||
|
const onEnter = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
ref.current.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keypress", onEnter)
|
||||||
|
return () => document.removeEventListener("keypress", onEnter)
|
||||||
} else {
|
} else {
|
||||||
// Format value back after edit
|
// Format value back after edit
|
||||||
setLocalValue({
|
setLocalValue({
|
||||||
@@ -179,14 +195,21 @@ function CurrencyCell(props: CurrencyCellProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
|
onMouseOver={() => onColumnOver(currencyCode || region)}
|
||||||
onMouseDown={onCellMouseDown}
|
onMouseDown={onCellMouseDown}
|
||||||
className={clsx("relative cursor-pointer pr-2 pl-4", {
|
className={clsx("relative cursor-pointer border pr-2 pl-4", {
|
||||||
border: !isInRange,
|
|
||||||
"bg-blue-100": isSelected && !isAnchor,
|
"bg-blue-100": isSelected && !isAnchor,
|
||||||
"border-x border-double border-blue-400": isInRange,
|
|
||||||
"border-t border-blue-400": isRangeStart,
|
|
||||||
"border-b border-blue-400": isRangeEnd,
|
|
||||||
})}
|
})}
|
||||||
|
style={{
|
||||||
|
borderTop:
|
||||||
|
isRangeStart && props.isInRangeCol ? "1px double #3B82F6" : "",
|
||||||
|
borderBottom:
|
||||||
|
isRangeEnd && props.isInRangeCol ? "1px double #3B82F6" : "",
|
||||||
|
borderLeft:
|
||||||
|
props.isRangeStartCol && isInRange ? "1px double #3B82F6" : "",
|
||||||
|
borderRight:
|
||||||
|
props.isRangeEndCol && isInRange ? "1px double #3B82F6" : "",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="text-gray-400">{currencyMeta?.symbol_native}</span>
|
<span className="text-gray-400">{currencyMeta?.symbol_native}</span>
|
||||||
@@ -206,7 +229,7 @@ function CurrencyCell(props: CurrencyCellProps) {
|
|||||||
decimalSeparator="."
|
decimalSeparator="."
|
||||||
placeholder="-"
|
placeholder="-"
|
||||||
/>
|
/>
|
||||||
{isRangeEnd && !isEditable && (
|
{isRangeEnd && props.isRangeEndCol && !isEditable && (
|
||||||
<div
|
<div
|
||||||
style={{ bottom: -4, right: -4, zIndex: 9999 }}
|
style={{ bottom: -4, right: -4, zIndex: 9999 }}
|
||||||
onMouseDown={onFillIndicatorMouseDown}
|
onMouseDown={onFillIndicatorMouseDown}
|
||||||
|
|||||||
+386
-53
@@ -2,11 +2,24 @@ import React, { useEffect, useMemo, useRef, useState } from "react"
|
|||||||
import { useAdminRegions, useAdminStore } from "medusa-react"
|
import { useAdminRegions, useAdminStore } from "medusa-react"
|
||||||
import { Product } from "@medusajs/client-types"
|
import { Product } from "@medusajs/client-types"
|
||||||
|
|
||||||
import { getCurrencyPricesOnly, getRegionPricesOnly } from "./utils"
|
import {
|
||||||
import CurrencyCell from "./currency-cell"
|
getCurrencyPricesOnly,
|
||||||
|
getRegionPricesOnly,
|
||||||
|
mod,
|
||||||
|
isText,
|
||||||
|
} from "./utils"
|
||||||
import IconBuildingTax from "../../../fundamentals/icons/building-tax-icon"
|
import IconBuildingTax from "../../../fundamentals/icons/building-tax-icon"
|
||||||
import { currencies as CURRENCY_MAP } from "../../../../utils/currencies"
|
import { currencies as CURRENCY_MAP } from "../../../../utils/currencies"
|
||||||
import Tooltip from "../../../atoms/tooltip"
|
import Tooltip from "../../../atoms/tooltip"
|
||||||
|
import CurrencyCell from "./currency-cell"
|
||||||
|
import useNotification from "../../../../hooks/use-notification"
|
||||||
|
|
||||||
|
enum ArrowMove {
|
||||||
|
UP,
|
||||||
|
DOWN,
|
||||||
|
LEFT,
|
||||||
|
RIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
type EditPricesTableProps = {
|
type EditPricesTableProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -21,9 +34,9 @@ type EditPricesTableProps = {
|
|||||||
let anchorVariant: string | undefined
|
let anchorVariant: string | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* During drag move keep info which column is active one.
|
* Currency or region column that is origin of the drag move.
|
||||||
*/
|
*/
|
||||||
let activeCurrencyOrRegion: string | undefined = undefined
|
let anchorCurrencyOrRegion: string | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pointer for displaying highlight rectangle range.
|
* Pointer for displaying highlight rectangle range.
|
||||||
@@ -32,6 +45,10 @@ let startIndex: number | undefined
|
|||||||
let endIndex: number | undefined
|
let endIndex: number | undefined
|
||||||
let anchorIndex: number | undefined
|
let anchorIndex: number | undefined
|
||||||
|
|
||||||
|
let startIndexCol: number | undefined
|
||||||
|
let endIndexCol: number | undefined
|
||||||
|
let anchorIndexCol: number | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temp. variable for persisting previous "editedPrices" state before editing,
|
* Temp. variable for persisting previous "editedPrices" state before editing,
|
||||||
* so we can undo changes.
|
* so we can undo changes.
|
||||||
@@ -61,8 +78,12 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
limit: 1000,
|
limit: 1000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const notification = useNotification()
|
||||||
|
|
||||||
const initialPricesSet = useRef(false)
|
const initialPricesSet = useRef(false)
|
||||||
|
|
||||||
|
const columns = [...props.currencies, ...props.regions]
|
||||||
|
|
||||||
const [isDragFill, setIsDragFill] = useState(false)
|
const [isDragFill, setIsDragFill] = useState(false)
|
||||||
const [isDrag, setIsDrag] = useState(false)
|
const [isDrag, setIsDrag] = useState(false)
|
||||||
const [editedPrices, setEditedPrices] = useState<
|
const [editedPrices, setEditedPrices] = useState<
|
||||||
@@ -78,7 +99,7 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
region?: string,
|
region?: string,
|
||||||
override?: boolean
|
override?: boolean
|
||||||
) => {
|
) => {
|
||||||
if ((currencyCode || region) !== activeCurrencyOrRegion) {
|
if ((currencyCode || region) !== anchorCurrencyOrRegion) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,14 +132,169 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
startIndex = undefined
|
startIndex = undefined
|
||||||
endIndex = undefined
|
endIndex = undefined
|
||||||
|
|
||||||
|
anchorIndexCol = undefined
|
||||||
|
startIndexCol = undefined
|
||||||
|
endIndexCol = undefined
|
||||||
|
|
||||||
anchorVariant = undefined
|
anchorVariant = undefined
|
||||||
activeCurrencyOrRegion = undefined
|
anchorCurrencyOrRegion = undefined
|
||||||
|
|
||||||
// warning state updates in event handlers will be batched together so if there is another
|
// warning state updates in event handlers will be batched together so if there is another
|
||||||
// `setSelectedCells` (or `resetSelection`) call in the same event handler, only last state will apply
|
// `setSelectedCells` (or `resetSelection`) call in the same event handler, only last state will apply
|
||||||
setSelectedCells({})
|
setSelectedCells({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moveAnchor = (
|
||||||
|
direction: ArrowMove,
|
||||||
|
isShift: boolean,
|
||||||
|
isCmd: boolean
|
||||||
|
) => {
|
||||||
|
if (!anchorIndex) {
|
||||||
|
setSelectedCells({ [getKey(variantIds[0], columns[0])]: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === ArrowMove.DOWN) {
|
||||||
|
if (!isShift) {
|
||||||
|
let ind = variantIds.findIndex((v) => v === anchorVariant)
|
||||||
|
ind = mod(ind + 1, variantIds.length)
|
||||||
|
const nextVariant = variantIds[ind]
|
||||||
|
|
||||||
|
anchorVariant = nextVariant
|
||||||
|
anchorIndex = ind
|
||||||
|
startIndex = ind
|
||||||
|
endIndex = ind
|
||||||
|
|
||||||
|
startIndexCol = anchorIndexCol
|
||||||
|
endIndexCol = anchorIndexCol
|
||||||
|
|
||||||
|
setSelectedCells({
|
||||||
|
[getKey(nextVariant, anchorCurrencyOrRegion)]: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (isCmd) {
|
||||||
|
startIndex = anchorIndex
|
||||||
|
endIndex = variantIds.length - 1
|
||||||
|
} else {
|
||||||
|
if (anchorIndex === startIndex) {
|
||||||
|
endIndex = Math.min(endIndex + 1, variantIds.length - 1)
|
||||||
|
} else {
|
||||||
|
startIndex = Math.min(startIndex + 1, variantIds.length - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectionRangeChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === ArrowMove.UP) {
|
||||||
|
if (!isShift) {
|
||||||
|
let ind = variantIds.findIndex((v) => v === anchorVariant)
|
||||||
|
ind = mod(ind - 1, variantIds.length)
|
||||||
|
const nextVariant = variantIds[ind]
|
||||||
|
anchorVariant = nextVariant
|
||||||
|
|
||||||
|
anchorIndex = ind
|
||||||
|
startIndex = ind
|
||||||
|
endIndex = ind
|
||||||
|
|
||||||
|
startIndexCol = anchorIndexCol
|
||||||
|
endIndexCol = anchorIndexCol
|
||||||
|
|
||||||
|
setSelectedCells({
|
||||||
|
[getKey(nextVariant, anchorCurrencyOrRegion)]: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (isCmd) {
|
||||||
|
endIndex = anchorIndex
|
||||||
|
startIndex = 0
|
||||||
|
} else {
|
||||||
|
if (anchorIndex === startIndex) {
|
||||||
|
if (startIndex === endIndex) {
|
||||||
|
startIndex = Math.max(startIndex - 1, 0)
|
||||||
|
} else {
|
||||||
|
endIndex = Math.max(endIndex - 1, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startIndex = Math.max(startIndex - 1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectionRangeChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === ArrowMove.LEFT) {
|
||||||
|
if (!isShift) {
|
||||||
|
let ind = columns.findIndex((v) => v === anchorCurrencyOrRegion)
|
||||||
|
ind = mod(ind - 1, columns.length)
|
||||||
|
const nextCol = columns[ind]
|
||||||
|
|
||||||
|
anchorCurrencyOrRegion = nextCol
|
||||||
|
|
||||||
|
anchorIndexCol = ind
|
||||||
|
startIndexCol = ind
|
||||||
|
endIndexCol = ind
|
||||||
|
|
||||||
|
startIndex = anchorIndex
|
||||||
|
endIndex = anchorIndex
|
||||||
|
|
||||||
|
setSelectedCells({
|
||||||
|
[getKey(anchorVariant, nextCol)]: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (isCmd) {
|
||||||
|
endIndexCol = anchorIndexCol
|
||||||
|
startIndexCol = 0
|
||||||
|
} else {
|
||||||
|
if (anchorIndexCol === startIndexCol) {
|
||||||
|
if (startIndexCol === endIndexCol) {
|
||||||
|
startIndexCol = Math.max(startIndexCol - 1, 0)
|
||||||
|
} else {
|
||||||
|
endIndexCol = Math.max(endIndexCol - 1, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startIndexCol = Math.max(startIndexCol - 1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectionRangeChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === ArrowMove.RIGHT) {
|
||||||
|
if (!isShift) {
|
||||||
|
let ind = columns.findIndex((v) => v === anchorCurrencyOrRegion)
|
||||||
|
ind = mod(ind + 1, columns.length)
|
||||||
|
const nextCol = columns[ind]
|
||||||
|
|
||||||
|
anchorCurrencyOrRegion = nextCol
|
||||||
|
|
||||||
|
anchorIndexCol = ind
|
||||||
|
startIndexCol = ind
|
||||||
|
endIndexCol = ind
|
||||||
|
|
||||||
|
startIndex = anchorIndex
|
||||||
|
endIndex = anchorIndex
|
||||||
|
|
||||||
|
setSelectedCells({
|
||||||
|
[getKey(anchorVariant, nextCol)]: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (isCmd) {
|
||||||
|
startIndexCol = anchorIndexCol
|
||||||
|
endIndexCol = columns.length - 1
|
||||||
|
} else {
|
||||||
|
if (anchorIndexCol === startIndexCol) {
|
||||||
|
endIndexCol = Math.min(endIndexCol + 1, columns.length - 1)
|
||||||
|
} else {
|
||||||
|
startIndexCol = Math.min(startIndexCol + 1, columns.length - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSelectionRangeChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ==================== HANDLERS ====================
|
* ==================== HANDLERS ====================
|
||||||
*/
|
*/
|
||||||
@@ -138,10 +314,37 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
endIndex = anchorIndex
|
endIndex = anchorIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSelectionRangeChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onColumnOver = (currencyOrRegion: string) => {
|
||||||
|
if (!(isDragFill || isDrag)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndexCol = columns.findIndex((v) => v === currencyOrRegion)
|
||||||
|
|
||||||
|
if (currentIndexCol > anchorIndexCol) {
|
||||||
|
endIndexCol = currentIndexCol
|
||||||
|
startIndexCol = anchorIndexCol
|
||||||
|
} else {
|
||||||
|
startIndexCol = currentIndexCol
|
||||||
|
endIndexCol = anchorIndexCol
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectionRangeChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectionRangeChange = () => {
|
||||||
|
const selectedColumns = columns.slice(startIndexCol, endIndexCol + 1)
|
||||||
const selectedVariants = variantIds.slice(startIndex, endIndex + 1)
|
const selectedVariants = variantIds.slice(startIndex, endIndex + 1)
|
||||||
|
|
||||||
const keys = selectedVariants.map((vId) =>
|
const keys = []
|
||||||
getKey(vId, activeCurrencyOrRegion)
|
|
||||||
|
selectedVariants.forEach((vId) =>
|
||||||
|
selectedColumns.forEach((c) => {
|
||||||
|
keys.push(getKey(vId, c))
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const nextSelection = { ...selectedCells }
|
const nextSelection = { ...selectedCells }
|
||||||
@@ -149,7 +352,7 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
|
|
||||||
Object.keys(nextSelection).forEach((k) => {
|
Object.keys(nextSelection).forEach((k) => {
|
||||||
// deselect case
|
// deselect case
|
||||||
if (k.split("-")[1] === activeCurrencyOrRegion && !keys.includes(k)) {
|
if (!keys.includes(k)) {
|
||||||
delete nextSelection[k] // remove selection
|
delete nextSelection[k] // remove selection
|
||||||
nextPrices[k] = prevPriceState[k] // ...and reset price of that cell to the previous state
|
nextPrices[k] = prevPriceState[k] // ...and reset price of that cell to the previous state
|
||||||
}
|
}
|
||||||
@@ -161,7 +364,7 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
|
|
||||||
if (isDragFill) {
|
if (isDragFill) {
|
||||||
nextPrices[k] =
|
nextPrices[k] =
|
||||||
editedPrices[getKey(anchorVariant, activeCurrencyOrRegion)]
|
editedPrices[getKey(anchorVariant, anchorCurrencyOrRegion)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -184,14 +387,16 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
// set variant row anchors
|
// set variant row anchors
|
||||||
anchorVariant = variantId
|
anchorVariant = variantId
|
||||||
anchorIndex = variantIds.findIndex((v) => v === anchorVariant)
|
anchorIndex = variantIds.findIndex((v) => v === anchorVariant)
|
||||||
|
startIndex = anchorIndex
|
||||||
|
endIndex = startIndex
|
||||||
|
|
||||||
|
anchorCurrencyOrRegion = currencyCode || regionId
|
||||||
|
anchorIndexCol = columns.findIndex((v) => v === anchorCurrencyOrRegion)
|
||||||
|
startIndexCol = anchorIndexCol
|
||||||
|
endIndexCol = anchorIndexCol
|
||||||
|
|
||||||
activeCurrencyOrRegion = currencyCode || regionId
|
|
||||||
setSelectedCells({ [getKey(variantId, currencyCode || regionId)]: true })
|
setSelectedCells({ [getKey(variantId, currencyCode || regionId)]: true })
|
||||||
setIsDrag(true)
|
setIsDrag(true)
|
||||||
|
|
||||||
startIndex = props.product.variants!.findIndex((v) => v.id === variantId)
|
|
||||||
endIndex = startIndex
|
|
||||||
anchorIndex = startIndex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInputChange = (
|
const onInputChange = (
|
||||||
@@ -288,6 +493,69 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
setEditedPrices(next)
|
setEditedPrices(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === "Tab") {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
if (anchorIndexCol === 0) {
|
||||||
|
moveAnchor(ArrowMove.UP, false)
|
||||||
|
}
|
||||||
|
moveAnchor(ArrowMove.LEFT, false)
|
||||||
|
} else {
|
||||||
|
if (anchorIndexCol === columns.length - 1) {
|
||||||
|
moveAnchor(ArrowMove.DOWN, false)
|
||||||
|
}
|
||||||
|
moveAnchor(ArrowMove.RIGHT, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 38) {
|
||||||
|
moveAnchor(ArrowMove.UP, e.shiftKey, e.metaKey || e.ctrlKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 40) {
|
||||||
|
moveAnchor(ArrowMove.DOWN, e.shiftKey, e.metaKey || e.ctrlKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 37) {
|
||||||
|
moveAnchor(ArrowMove.LEFT, e.shiftKey, e.metaKey || e.ctrlKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 39) {
|
||||||
|
moveAnchor(ArrowMove.RIGHT, e.shiftKey, e.metaKey || e.ctrlKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.keyCode === 67) {
|
||||||
|
let ret = ""
|
||||||
|
const variants = {}
|
||||||
|
const columns = {}
|
||||||
|
|
||||||
|
Object.keys(selectedCells).forEach((k) => {
|
||||||
|
const [r, c] = k.split("-")
|
||||||
|
variants[r] = true
|
||||||
|
columns[c] = true
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.keys(variants)
|
||||||
|
.sort((v1, v2) => variantIds.indexOf(v1) - variantIds.indexOf(v2))
|
||||||
|
.forEach((k) => {
|
||||||
|
Object.keys(columns).forEach((c) => {
|
||||||
|
const price = editedPrices[getKey(k, c)]
|
||||||
|
|
||||||
|
ret += (!isNaN(price) ? price : "") + "\t"
|
||||||
|
})
|
||||||
|
|
||||||
|
ret = ret.slice(0, -1)
|
||||||
|
ret += `\n`
|
||||||
|
})
|
||||||
|
|
||||||
|
ret = ret.slice(0, -1)
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(ret)
|
||||||
|
notification("Success", "Copied to clipboard", "success")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo last selection change (or delete) on CMD/CTR + Z
|
* Undo last selection change (or delete) on CMD/CTR + Z
|
||||||
*/
|
*/
|
||||||
@@ -301,14 +569,74 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onPaste = (event: ClipboardEvent) => {
|
||||||
|
const paste = (event.clipboardData || window.clipboardData).getData(
|
||||||
|
"text"
|
||||||
|
)
|
||||||
|
|
||||||
|
const rows = paste.split("\n").map((r) => r.split("\t"))
|
||||||
|
|
||||||
|
// single cell click -> determine from the content
|
||||||
|
if (
|
||||||
|
typeof anchorIndex === "number" &&
|
||||||
|
typeof anchorIndexCol === "number"
|
||||||
|
) {
|
||||||
|
const _edited = { ...editedPrices }
|
||||||
|
|
||||||
|
const isRange = startIndex !== endIndex || startIndexCol !== endIndexCol
|
||||||
|
|
||||||
|
// if only anchor is clicked past selected, if range is selected fill and repeat until selected area is filled
|
||||||
|
const iBoundary = !isRange ? rows.length - 1 : endIndex - startIndex
|
||||||
|
|
||||||
|
for (let i = 0; i <= iBoundary; i++) {
|
||||||
|
if (i >= variantIds.length) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = rows[i % rows.length]
|
||||||
|
|
||||||
|
// if only anchor is clicked past selected, if range is selected fill and repeat until selected area is filled
|
||||||
|
const jBoundary = !isRange
|
||||||
|
? parts.length - 1
|
||||||
|
: endIndexCol - startIndexCol
|
||||||
|
|
||||||
|
for (let j = 0; j <= jBoundary; j++) {
|
||||||
|
if (j >= columns.length) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isText(parts[j % parts.length])) {
|
||||||
|
notification(
|
||||||
|
"Error",
|
||||||
|
"Invalid data - copied cells contain text",
|
||||||
|
"error"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = parseFloat(parts[j % parts.length])
|
||||||
|
|
||||||
|
_edited[
|
||||||
|
getKey(variantIds[startIndex + i], columns[startIndexCol + j])
|
||||||
|
] = !isNaN(amount) ? amount : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditedPrices(_edited)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("mousedown", down)
|
document.addEventListener("mousedown", down)
|
||||||
document.addEventListener("mouseup", up)
|
document.addEventListener("mouseup", up)
|
||||||
document.addEventListener("keydown", onKeyDown)
|
document.addEventListener("keydown", onKeyDown)
|
||||||
|
|
||||||
|
document.addEventListener("paste", onPaste)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", down)
|
document.removeEventListener("mousedown", down)
|
||||||
document.removeEventListener("mouseup", up)
|
document.removeEventListener("mouseup", up)
|
||||||
document.addEventListener("keydown", onKeyDown)
|
document.removeEventListener("keydown", onKeyDown)
|
||||||
|
document.removeEventListener("paste", onPaste)
|
||||||
}
|
}
|
||||||
}, [selectedCells])
|
}, [selectedCells])
|
||||||
|
|
||||||
@@ -443,7 +771,7 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
{variant.title} {variant.sku && `∙ ${variant.sku}`}
|
{variant.title} {variant.sku && `∙ ${variant.sku}`}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{props.currencies.map((c) => {
|
{props.currencies.map((c, indexCol) => {
|
||||||
return (
|
return (
|
||||||
<CurrencyCell
|
<CurrencyCell
|
||||||
key={variant.id + c}
|
key={variant.id + c}
|
||||||
@@ -454,48 +782,53 @@ function EditPricesTable(props: EditPricesTableProps) {
|
|||||||
onInputChange={onInputChange}
|
onInputChange={onInputChange}
|
||||||
onMouseCellClick={onMouseCellClick}
|
onMouseCellClick={onMouseCellClick}
|
||||||
onDragFillStart={onDragFillStart}
|
onDragFillStart={onDragFillStart}
|
||||||
isAnchor={anchorIndex === index}
|
onColumnOver={onColumnOver}
|
||||||
isRangeStart={
|
isAnchor={
|
||||||
activeCurrencyOrRegion === c && startIndex === index
|
anchorIndex === index && anchorIndexCol === indexCol
|
||||||
}
|
}
|
||||||
isRangeEnd={
|
isRangeStart={startIndex === index}
|
||||||
activeCurrencyOrRegion === c && index === endIndex
|
isRangeEnd={index === endIndex}
|
||||||
}
|
isInRange={index >= startIndex && index <= endIndex}
|
||||||
isInRange={
|
isRangeStartCol={startIndexCol === indexCol}
|
||||||
activeCurrencyOrRegion === c &&
|
isRangeEndCol={indexCol === endIndexCol}
|
||||||
index >= startIndex &&
|
isInRangeCol={
|
||||||
index <= endIndex
|
indexCol >= startIndexCol && indexCol <= endIndexCol
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{props.regions.map((r) => (
|
{props.regions.map((r, indexCol) => {
|
||||||
<CurrencyCell
|
indexCol = props.currencies.length + indexCol
|
||||||
key={variant.id + r}
|
return (
|
||||||
region={r!}
|
<CurrencyCell
|
||||||
variant={variant}
|
key={variant.id + r}
|
||||||
isSelected={selectedCells[getKey(variant.id, undefined, r)]}
|
region={r!}
|
||||||
editedAmount={
|
variant={variant}
|
||||||
editedPrices[getKey(variant.id, undefined, r)]
|
isSelected={
|
||||||
}
|
selectedCells[getKey(variant.id, undefined, r)]
|
||||||
onInputChange={onInputChange}
|
}
|
||||||
onMouseCellClick={onMouseCellClick}
|
editedAmount={
|
||||||
onDragFillStart={onDragFillStart}
|
editedPrices[getKey(variant.id, undefined, r)]
|
||||||
isAnchor={anchorIndex === index}
|
}
|
||||||
isRangeStart={
|
onInputChange={onInputChange}
|
||||||
activeCurrencyOrRegion === r && startIndex === index
|
onMouseCellClick={onMouseCellClick}
|
||||||
}
|
onDragFillStart={onDragFillStart}
|
||||||
isRangeEnd={
|
onColumnOver={onColumnOver}
|
||||||
activeCurrencyOrRegion === r && index === endIndex
|
isAnchor={
|
||||||
}
|
anchorIndex === index && anchorIndexCol === indexCol
|
||||||
isInRange={
|
}
|
||||||
activeCurrencyOrRegion === r &&
|
isRangeStart={startIndex === index}
|
||||||
index >= startIndex &&
|
isRangeEnd={index === endIndex}
|
||||||
index <= endIndex
|
isInRange={index >= startIndex && index <= endIndex}
|
||||||
}
|
isRangeStartCol={startIndexCol === indexCol}
|
||||||
/>
|
isRangeEndCol={indexCol === endIndexCol}
|
||||||
))}
|
isInRangeCol={
|
||||||
|
indexCol >= startIndexCol && indexCol <= endIndexCol
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
+11
@@ -71,3 +71,14 @@ export function getRegionPricesOnly(prices: MoneyAmount[]) {
|
|||||||
return !(price.price_list_id || price.min_quantity || price.max_quantity)
|
return !(price.price_list_id || price.min_quantity || price.max_quantity)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modulo operation
|
||||||
|
*/
|
||||||
|
export function mod(n: number, m: number) {
|
||||||
|
return ((n % m) + m) % m
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isText = (v: string) => {
|
||||||
|
return v !== "" && isNaN(Number(v))
|
||||||
|
}
|
||||||
Reference in new issue
Block a user