This commit is contained in:
seb
2026-10-04 08:18:36 +02:00
parent f2fce9097b
commit 4edd417dab
19 changed files with 141 additions and 220 deletions

View File

@@ -5,7 +5,7 @@
*/ */
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path'; import { resolve, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath } from 'node:url';
import decodeAvif, { init as initAvifDecode } from '@jsquash/avif/decode.js'; import decodeAvif, { init as initAvifDecode } from '@jsquash/avif/decode.js';
import encodeAvif, { init as initAvifEncode } from '@jsquash/avif/encode.js'; import encodeAvif, { init as initAvifEncode } from '@jsquash/avif/encode.js';
import encodeJpeg, { init as initJpegEncode } from '@jsquash/jpeg/encode.js'; import encodeJpeg, { init as initJpegEncode } from '@jsquash/jpeg/encode.js';

View File

@@ -798,7 +798,7 @@ export class StorageManager extends ApiClient {
return requireArray(data.orders, 'orders'); return requireArray(data.orders, 'orders');
} }
async doSearch({ target, filter, offset, maxCount, onUpdate }) { async doSearch({ target, onUpdate }) {
const articles = await this.articleIndexPromise; const articles = await this.articleIndexPromise;
const query = target.trim().toLowerCase(); const query = target.trim().toLowerCase();
const matches = articles.filter( const matches = articles.filter(

View File

@@ -5,7 +5,6 @@ import companyAutomat from './companyAutomat';
import { import {
parseCategoryTreeResponse, parseCategoryTreeResponse,
normalizeCategoryNode, normalizeCategoryNode,
getCategoryBreadcrumbs,
getCategoryChildren, getCategoryChildren,
cleanCategorySlugPath, cleanCategorySlugPath,
resolveCategoryIdFromSlugPath, resolveCategoryIdFromSlugPath,
@@ -94,7 +93,7 @@ function refreshLoadedCategoryViewChildren(categories) {
* Get or create a per-route category view automat for the given slug and categoryId. * Get or create a per-route category view automat for the given slug and categoryId.
* (Backward compatibility helper) * (Backward compatibility helper)
*/ */
export function getOrCreateCategoryAutomat({ slugPath, categoryId }) { export function getOrCreateCategoryAutomat({ slugPath }) {
const slug = cleanCategorySlugPath(slugPath); const slug = cleanCategorySlugPath(slugPath);
return categoryViews.key(slug); return categoryViews.key(slug);
} }

View File

@@ -3,7 +3,6 @@ import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Tab from '@mui/material/Tab'; import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs'; import Tabs from '@mui/material/Tabs';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import CategoryIcon from '@mui/icons-material/Category'; import CategoryIcon from '@mui/icons-material/Category';
import HistoryIcon from '@mui/icons-material/History'; import HistoryIcon from '@mui/icons-material/History';
import LabelIcon from '@mui/icons-material/Label'; import LabelIcon from '@mui/icons-material/Label';

View File

@@ -45,7 +45,7 @@ export default class CategoryPickerDialog extends PureComponent {
componentDidMount() { componentDidMount() {
if (this.props.categories === undefined || this.props.rootCategoryId === undefined) { if (this.props.categories === undefined || this.props.rootCategoryId === undefined) {
this._unsub = categoryAutomat.subscribe((s) => { this._unsub = categoryAutomat.subscribe((s) => {
this.setState((prev) => ({ this.setState(() => ({
categories: this.props.categories ?? s.categories, categories: this.props.categories ?? s.categories,
rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId, rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId,
})); }));
@@ -132,7 +132,6 @@ export default class CategoryPickerDialog extends PureComponent {
const dialogTitle = title || (mode === 'single' ? 'Kategorie auswählen' : 'Kategorie zuweisen'); const dialogTitle = title || (mode === 'single' ? 'Kategorie auswählen' : 'Kategorie zuweisen');
const actionLabel = confirmLabel || (mode === 'single' ? 'Auswählen' : 'Zuweisen'); const actionLabel = confirmLabel || (mode === 'single' ? 'Auswählen' : 'Zuweisen');
const selectedNode = categories && selectedId != null ? categories[String(selectedId)] : null;
const isAlreadyAssigned = const isAlreadyAssigned =
mode !== 'single' && currentCategoryIds.some((id) => String(id) === String(selectedId)); mode !== 'single' && currentCategoryIds.some((id) => String(id) === String(selectedId));
const isSelectedDisabled = const isSelectedDisabled =

View File

@@ -366,22 +366,20 @@ export default class ArticleViewLists extends PureComponent {
const count = Math.max(1, Number(stuecklisteCount) || 1); const count = Math.max(1, Number(stuecklisteCount) || 1);
const existingIndex = currentItems.findIndex((i) => i.sku === stuecklisteTargetArticle.sku); const existingIndex = currentItems.findIndex((i) => i.sku === stuecklisteTargetArticle.sku);
let nextItems = []; const nextItems =
if (existingIndex >= 0) { existingIndex >= 0
nextItems = currentItems.map((item, idx) => ? currentItems.map((item, idx) =>
idx === existingIndex ? { ...item, count: item.count + count } : item idx === existingIndex ? { ...item, count: item.count + count } : item
); )
} else { : [
nextItems = [ ...currentItems,
...currentItems, {
{ articleId: stuecklisteTargetArticle.id,
articleId: stuecklisteTargetArticle.id, sku: stuecklisteTargetArticle.sku,
sku: stuecklisteTargetArticle.sku, count,
count, sort: currentItems.length,
sort: currentItems.length, },
}, ];
];
}
this.setState({ busy: true }); this.setState({ busy: true });
try { try {
@@ -492,25 +490,23 @@ export default class ArticleViewLists extends PureComponent {
const count = Math.max(1, Number(optionCount) || 1); const count = Math.max(1, Number(optionCount) || 1);
const existingIndex = currentOptions.findIndex((o) => o.sku === optionTargetArticle.sku); const existingIndex = currentOptions.findIndex((o) => o.sku === optionTargetArticle.sku);
let nextOptions = []; const nextOptions =
if (existingIndex >= 0) { existingIndex >= 0
nextOptions = currentOptions.map((opt, idx) => ? currentOptions.map((opt, idx) =>
idx === existingIndex idx === existingIndex
? { ...opt, count: opt.count + count, mandated: Boolean(optionMandated) } ? { ...opt, count: opt.count + count, mandated: Boolean(optionMandated) }
: opt : opt
); )
} else { : [
nextOptions = [ ...currentOptions,
...currentOptions, {
{ articleId: optionTargetArticle.id,
articleId: optionTargetArticle.id, sku: optionTargetArticle.sku,
sku: optionTargetArticle.sku, count,
count, sort: currentOptions.length,
sort: currentOptions.length, mandated: Boolean(optionMandated),
mandated: Boolean(optionMandated), },
}, ];
];
}
this.setState({ busy: true }); this.setState({ busy: true });
try { try {
@@ -1363,15 +1359,9 @@ export default class ArticleViewLists extends PureComponent {
busy, busy,
activeCreatorMode, activeCreatorMode,
activeListTab, activeListTab,
selectedAttributeId,
selectedChildValueId, selectedChildValueId,
newChildText, newChildText,
isCustomChildValue, isCustomChildValue,
stuecklisteTargetArticle,
stuecklisteCount,
optionTargetArticle,
optionCount,
optionMandated,
removeTargetChild, removeTargetChild,
confirmClearType, confirmClearType,
} = this.state; } = this.state;

View File

@@ -184,13 +184,14 @@ export default class ArticleAttributesTab extends PureComponent {
return; return;
} }
let nextValues = []; const nextValues = isExclusive
if (isExclusive) { ? [
const others = assignedAttributes.filter((a) => a.attributeId !== targetAttr.id); ...assignedAttributes
nextValues = [...others.map((a) => a.attributeValueId), targetValId]; .filter((a) => a.attributeId !== targetAttr.id)
} else { .map((a) => a.attributeValueId),
nextValues = [...assignedAttributes.map((a) => a.attributeValueId), targetValId]; targetValId,
} ]
: [...assignedAttributes.map((a) => a.attributeValueId), targetValId];
this.setState({ saving: true }); this.setState({ saving: true });
try { try {
@@ -222,19 +223,13 @@ export default class ArticleAttributesTab extends PureComponent {
return; return;
} }
} }
let nextProps = []; const sourceProps = isExclusive
if (isExclusive) { ? assignedProperties.filter((p) => p.propertyId !== propId)
const existing = assignedProperties.filter((p) => p.propertyId !== propId); : assignedProperties;
nextProps = [ const nextProps = [
...existing.map((p) => ({ propertyId: p.propertyId, value: p.value, sort: p.sort })), ...sourceProps.map((p) => ({ propertyId: p.propertyId, value: p.value, sort: p.sort })),
{ propertyId: propId, value: val }, { propertyId: propId, value: val },
]; ];
} else {
nextProps = [
...assignedProperties.map((p) => ({ propertyId: p.propertyId, value: p.value, sort: p.sort })),
{ propertyId: propId, value: val },
];
}
this.setState({ saving: true }); this.setState({ saving: true });
try { try {

View File

@@ -43,7 +43,7 @@ function getMatchingAndAncestorIds(categories, query, rootCategoryId, excludeIds
if (!categories || !query.trim()) return matches; if (!categories || !query.trim()) return matches;
const q = query.trim().toLowerCase(); const q = query.trim().toLowerCase();
for (const [idStr, node] of Object.entries(categories)) { for (const node of Object.values(categories)) {
if (excludeIds.some((id) => String(id) === String(node.id))) continue; if (excludeIds.some((id) => String(id) === String(node.id))) continue;
const label = categoryDisplayName(node.name, node.id, rootCategoryId).toLowerCase(); const label = categoryDisplayName(node.name, node.id, rootCategoryId).toLowerCase();
if (label.includes(q)) { if (label.includes(q)) {
@@ -580,7 +580,6 @@ export default class CategoryTree extends PureComponent {
addCategoryName, addCategoryName,
isSubmitting, isSubmitting,
renameDialogOpen, renameDialogOpen,
renameNode,
renameValue, renameValue,
deleteDialogOpen, deleteDialogOpen,
deleteNode, deleteNode,

View File

@@ -2,7 +2,6 @@ import {
ROOT_CATEGORY_NAME, ROOT_CATEGORY_NAME,
categorySlug, categorySlug,
getCategoryBreadcrumbs, getCategoryBreadcrumbs,
getCategoryChildren,
} from '../../utils/categoryUtils'; } from '../../utils/categoryUtils';
export { ROOT_CATEGORY_NAME, categorySlug }; export { ROOT_CATEGORY_NAME, categorySlug };

View File

@@ -9,7 +9,6 @@ import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import FormControl from '@mui/material/FormControl'; import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel'; import InputLabel from '@mui/material/InputLabel';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import SendRoundedIcon from '@mui/icons-material/SendRounded'; import SendRoundedIcon from '@mui/icons-material/SendRounded';
@@ -231,7 +230,6 @@ export default class ChatDrawer extends PureComponent {
const { const {
conversations, conversations,
selectedConversationId, selectedConversationId,
sseStatus,
loadingConversations, loadingConversations,
} = this.state; } = this.state;
@@ -249,21 +247,8 @@ export default class ChatDrawer extends PureComponent {
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box <AutoAwesomeRoundedIcon sx={{ fontSize: 22, color: 'primary.main' }} />
sx={{
width: 32,
height: 32,
borderRadius: 2,
bgcolor: 'primary.main',
color: 'primary.contrastText',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<AutoAwesomeRoundedIcon sx={{ fontSize: 18 }} />
</Box>
<Typography variant="subtitle1" fontWeight={700}> <Typography variant="subtitle1" fontWeight={700}>
KI-Assistent KI-Assistent
</Typography> </Typography>
@@ -275,80 +260,82 @@ export default class ChatDrawer extends PureComponent {
</Box> </Box>
{/* Conversation Selector Bar */} {/* Conversation Selector Bar */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> {conversations.length > 0 && (
<FormControl fullWidth size="small"> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<InputLabel id="chat-conversation-label">Konversation</InputLabel> <FormControl fullWidth size="small">
<Select <InputLabel id="chat-conversation-label">Konversation</InputLabel>
labelId="chat-conversation-label" <Select
value={selectedConversationId || ''} labelId="chat-conversation-label"
label="Konversation" value={selectedConversationId || ''}
onChange={this.handleSelectConversation} label="Konversation"
disabled={loadingConversations || conversations.length === 0} onChange={this.handleSelectConversation}
sx={{ borderRadius: 2 }} disabled={loadingConversations || conversations.length === 0}
renderValue={(selectedId) => { sx={{ borderRadius: 2 }}
const conv = conversations.find((c) => c.id === selectedId); renderValue={(selectedId) => {
if (!conv) return ''; const conv = conversations.find((c) => c.id === selectedId);
return ( if (!conv) return '';
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, overflow: 'hidden' }}> return (
<Typography variant="body2" fontWeight={600} noWrap> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, overflow: 'hidden' }}>
#{conv.id} <Typography variant="body2" fontWeight={600} noWrap>
</Typography> #{conv.id}
<Typography variant="caption" color="text.secondary" noWrap> </Typography>
{conv.model || 'Standard'} <Typography variant="caption" color="text.secondary" noWrap>
</Typography> {conv.model || 'Standard'}
</Box> </Typography>
); </Box>
);
}}
>
{conversations.map((c) => (
<MenuItem key={c.id} value={c.id}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%', overflow: 'hidden' }}>
<Typography variant="body2" fontWeight={600} noWrap>
#{c.id}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ flexGrow: 1 }}>
{c.model || 'Standard'}
</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
<IconButton
size="small"
color="error"
disabled={!selectedConversationId}
onClick={(e) => this.handleDeleteConversation(e, selectedConversationId)}
aria-label="Konversation löschen"
sx={{
borderRadius: 2,
border: 1,
borderColor: 'divider',
p: 0.9,
flexShrink: 0,
}} }}
> >
{conversations.map((c) => ( <DeleteOutlineRoundedIcon fontSize="small" />
<MenuItem key={c.id} value={c.id}> </IconButton>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%', overflow: 'hidden' }}>
<Typography variant="body2" fontWeight={600} noWrap>
#{c.id}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ flexGrow: 1 }}>
{c.model || 'Standard'}
</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
<IconButton <IconButton
size="small" size="small"
color="error" color="primary"
disabled={!selectedConversationId} disabled={!selectedConversationId}
onClick={(e) => this.handleDeleteConversation(e, selectedConversationId)} onClick={this.handleNewChat}
aria-label="Konversation löschen" aria-label="Neuer Chat"
sx={{ sx={{
borderRadius: 2, borderRadius: 2,
border: 1, border: 1,
borderColor: 'divider', borderColor: 'divider',
p: 0.9, p: 0.9,
flexShrink: 0, flexShrink: 0,
}} }}
> >
<DeleteOutlineRoundedIcon fontSize="small" /> <AddRoundedIcon fontSize="small" />
</IconButton> </IconButton>
</Box>
<IconButton )}
size="small"
color="primary"
disabled={!selectedConversationId}
onClick={this.handleNewChat}
aria-label="Neuer Chat"
sx={{
borderRadius: 2,
border: 1,
borderColor: 'divider',
p: 0.9,
flexShrink: 0,
}}
>
<AddRoundedIcon fontSize="small" />
</IconButton>
</Box>
</Box> </Box>
); );
} }
@@ -362,7 +349,6 @@ export default class ChatDrawer extends PureComponent {
messages, messages,
streamingText, streamingText,
loadingConversations, loadingConversations,
loadingMessages,
model, model,
} = this.state; } = this.state;
@@ -391,20 +377,7 @@ export default class ChatDrawer extends PureComponent {
gap: 2, gap: 2,
}} }}
> >
<Box <AutoAwesomeRoundedIcon sx={{ fontSize: 44, color: 'primary.main', opacity: 0.85 }} />
sx={{
width: 56,
height: 56,
borderRadius: '50%',
bgcolor: 'primary.light',
color: 'primary.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<AutoAwesomeRoundedIcon sx={{ fontSize: 32 }} />
</Box>
<Typography variant="h6" fontWeight={700}> <Typography variant="h6" fontWeight={700}>
Willkommen im Chat Willkommen im Chat
</Typography> </Typography>

View File

@@ -177,14 +177,17 @@ function MessageRow({ msg, showTime = true }) {
bodyLength = textContent.length; bodyLength = textContent.length;
} }
let displayText = ''; const isToolAnswer =
if (resObj && ('answer' in resObj || 'content' in resObj) && ('tool_call_id' in resObj) && !('status' in resObj)) { Boolean(resObj) &&
displayText = resObj.answer || resObj.content || ''; ('answer' in resObj || 'content' in resObj) &&
} else { 'tool_call_id' in resObj &&
displayText = statusText !('status' in resObj);
const displayText = isToolAnswer
? resObj.answer || resObj.content || ''
: statusText
? `${statusText}${bodyLength != null ? ` · ${bodyLength} byte` : ''}` ? `${statusText}${bodyLength != null ? ` · ${bodyLength} byte` : ''}`
: (bodyLength != null ? `${bodyLength} byte` : String(textContent)); : (bodyLength != null ? `${bodyLength} byte` : String(textContent));
}
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'flex-start', py: 0.35, px: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-start', py: 0.35, px: 2 }}>

View File

@@ -68,7 +68,7 @@ export class Attachment extends PureComponent {
} }
}; };
handleTileClick = (e) => { handleTileClick = () => {
const { attachment, onClick } = this.props; const { attachment, onClick } = this.props;
if (onClick && attachment) { if (onClick && attachment) {
onClick(attachment); onClick(attachment);

View File

@@ -1,12 +1,9 @@
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import Typography from '@mui/material/Typography';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import BackendError from '../BackendError'; import BackendError from '../BackendError';
import { PageTitleRight } from '../PageLayout'; import { PanelTileGrid } from 'dashgui/panels';
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import { StorageManagerContext } from '../../api/storageManagerContext'; import { StorageManagerContext } from '../../api/storageManagerContext';
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape'; import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
import { backendErrorFrom } from '../../utils/backendError'; import { backendErrorFrom } from '../../utils/backendError';
@@ -435,7 +432,7 @@ class ManifestForm extends PureComponent {
} }
render() { render() {
const { kind, mode } = this.props; const { kind } = this.props;
const cfg = this.cfg; const cfg = this.cfg;
const { const {
status, status,

View File

@@ -85,7 +85,6 @@ class ManifestHeader extends PureComponent {
saving, saving,
pdfUploading, pdfUploading,
attachments, attachments,
lines,
onPdfUpload, onPdfUpload,
onPdfRescan, onPdfRescan,
onPdfDelete, onPdfDelete,

View File

@@ -24,7 +24,6 @@ class ManifestLines extends PureComponent {
onAddLine, onAddLine,
onRemoveLine, onRemoveLine,
onRemoveAllLines, onRemoveAllLines,
onSave,
columns = LINE_COLUMNS, columns = LINE_COLUMNS,
supplierArticlesMap = {}, supplierArticlesMap = {},
supplierArticlesByLineKey = {}, supplierArticlesByLineKey = {},

View File

@@ -275,31 +275,6 @@ function fileToBase64(file) {
}); });
} }
/**
* Convert base64 string to Blob
* @param {string} base64
* @param {string} mimeType
* @returns {Blob}
*/
function base64ToBlob(base64, mimeType) {
const clean = base64.includes(',') ? base64.split(',')[1] : base64;
const byteCharacters = atob(clean);
const byteArrays = [];
const sliceSize = 512;
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: mimeType });
}
/** /**
* Restore attachments from API response * Restore attachments from API response
* @param {Array<{id: number, fileId?: number, manifestId?: number, mimetype: string, name: string, thumbavif?: any, size: number, created?: string, meta?: object}>} apiFiles * @param {Array<{id: number, fileId?: number, manifestId?: number, mimetype: string, name: string, thumbavif?: any, size: number, created?: string, meta?: object}>} apiFiles

View File

@@ -105,7 +105,7 @@ class KundeListePage extends PureComponent {
} }
render() { render() {
const { status, error, entities, groupsById, busyId } = this.state; const { status, error, entities, busyId } = this.state;
if (status === 'failed') { if (status === 'failed') {
return <BackendError error={error} onRetry={this.handleRetry} />; return <BackendError error={error} onRetry={this.handleRetry} />;

View File

@@ -21,7 +21,6 @@ import { enqueueSnackbar } from 'notistack';
import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import BackendError from '../../components/BackendError'; import BackendError from '../../components/BackendError';
import withNavigate from '../../components/withNavigate'; import withNavigate from '../../components/withNavigate';
import { PageTitleRight } from '../../components/PageLayout';
import { import {
ManifestHeader, ManifestHeader,
ManifestLines, ManifestLines,

View File

@@ -16,10 +16,7 @@
* only as a de-duplication / reference key, not as the SKU. * only as a de-duplication / reference key, not as the SKU.
*/ */
import { import { parseImportCsv } from './importArtikelCsv';
parseImportCsv,
parseCategoryPath,
} from './importArtikelCsv';
import { article, category, brand } from '../api'; import { article, category, brand } from '../api';
import { import {
requireObject, requireObject,
@@ -27,7 +24,6 @@ import {
} from './apiShape'; } from './apiShape';
function logImport(message) { function logImport(message) {
// eslint-disable-next-line no-console
console.log(`[import] ${message}`); console.log(`[import] ${message}`);
} }