Move import state into ImportPage; share translation list via hook
The session-list import form, preview, and runEpisodeImport now live in ImportPage. useAvailableTranslations fetches the translation list once per visit for both Import and Setup. Drops the never-read importFile state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7bbbe60de
commit
8bb3b104c8
5 files changed
+189
-190
No files matched your search
-174
@@ -48,8 +48,6 @@ import {
|
|||||||
} from './utils/exportBuilders.js';
|
} from './utils/exportBuilders.js';
|
||||||
import {
|
import {
|
||||||
parseCrossRefString,
|
parseCrossRefString,
|
||||||
parseEpisodePassage,
|
|
||||||
parseEpisodeListDocx,
|
|
||||||
} from './utils/importParsers.js';
|
} from './utils/importParsers.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -83,7 +81,6 @@ const PronunciationPage = lazy(() => import('./pages/PronunciationPage.jsx'));
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [availableTranslations, setAvailableTranslations] = useState(['BSB']);
|
|
||||||
const [setup, setSetup] = useState({
|
const [setup, setSetup] = useState({
|
||||||
translation: 'BSB',
|
translation: 'BSB',
|
||||||
book: 'Titus',
|
book: 'Titus',
|
||||||
@@ -138,13 +135,6 @@ const App = () => {
|
|||||||
});
|
});
|
||||||
const [renamingId, setRenamingId] = useState(null);
|
const [renamingId, setRenamingId] = useState(null);
|
||||||
const [renameValue, setRenameValue] = useState('');
|
const [renameValue, setRenameValue] = useState('');
|
||||||
const [importBookAbbrev, setImportBookAbbrev] = useState(bookOptions[0].abbrev);
|
|
||||||
const [importTranslation, setImportTranslation] = useState('BSB');
|
|
||||||
const [importTitle, setImportTitle] = useState('');
|
|
||||||
const [importFile, setImportFile] = useState(null);
|
|
||||||
const [importPreview, setImportPreview] = useState(null); // parsed episode specs, before fetching chapters
|
|
||||||
const [importBusy, setImportBusy] = useState(false);
|
|
||||||
const [importError, setImportError] = useState('');
|
|
||||||
const reader = useBibleReader(currentPage);
|
const reader = useBibleReader(currentPage);
|
||||||
const audio = useAudio(reader.readerBookAbbrev, reader.readerChapter);
|
const audio = useAudio(reader.readerBookAbbrev, reader.readerChapter);
|
||||||
const { authUser, setAuthUser, authServerDown, signIn, logout } = useAuth({
|
const { authUser, setAuthUser, authServerDown, signIn, logout } = useAuth({
|
||||||
@@ -322,17 +312,6 @@ const App = () => {
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [authUser]);
|
}, [authUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('https://bible.helloao.org/api/available_translations.json')
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
|
||||||
setAvailableTranslations(data);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!titleEdited) {
|
if (!titleEdited) {
|
||||||
setSetup((current) => ({
|
setSetup((current) => ({
|
||||||
@@ -1530,151 +1509,9 @@ const App = () => {
|
|||||||
|
|
||||||
const openImportProject = () => {
|
const openImportProject = () => {
|
||||||
setProject(null);
|
setProject(null);
|
||||||
setImportBookAbbrev(bookOptions[0].abbrev);
|
|
||||||
setImportTranslation('BSB');
|
|
||||||
setImportTitle('');
|
|
||||||
setImportFile(null);
|
|
||||||
setImportPreview(null);
|
|
||||||
setImportError('');
|
|
||||||
setCurrentPage('import');
|
setCurrentPage('import');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportFileChange = async (file) => {
|
|
||||||
setImportFile(file);
|
|
||||||
setImportPreview(null);
|
|
||||||
setImportError('');
|
|
||||||
if (!file) return;
|
|
||||||
setImportBusy(true);
|
|
||||||
try {
|
|
||||||
const episodes = await parseEpisodeListDocx(file);
|
|
||||||
if (episodes.length === 0) {
|
|
||||||
throw new Error('No "Session # / Title / Passage" table found in that document.');
|
|
||||||
}
|
|
||||||
const specs = episodes.map((ep) => ({ ...ep, parsed: parseEpisodePassage(ep.passage) }));
|
|
||||||
setImportPreview(specs);
|
|
||||||
} catch (error) {
|
|
||||||
setImportError(error.message || 'Failed to read that document.');
|
|
||||||
} finally {
|
|
||||||
setImportBusy(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const runEpisodeImport = async () => {
|
|
||||||
if (!importPreview || importPreview.length === 0) {
|
|
||||||
setImportError('Choose a .docx file first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setImportBusy(true);
|
|
||||||
setImportError('');
|
|
||||||
try {
|
|
||||||
const selectedBook = bookOptions.find((b) => b.abbrev === importBookAbbrev) ?? bookOptions[0];
|
|
||||||
|
|
||||||
const chapterNumbersNeeded = new Set();
|
|
||||||
importPreview.forEach((spec) => {
|
|
||||||
if (spec.parsed && spec.parsed !== 'invalid') {
|
|
||||||
chapterNumbersNeeded.add(spec.parsed.startChapter);
|
|
||||||
chapterNumbersNeeded.add(spec.parsed.endChapter);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const sortedChapters = Array.from(chapterNumbersNeeded).sort((a, b) => a - b);
|
|
||||||
if (sortedChapters.length === 0) {
|
|
||||||
throw new Error('No verse passages found to import.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const versesByChapter = {};
|
|
||||||
for (const chNum of sortedChapters) {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://bible.helloao.org/api/${importTranslation}/${selectedBook.abbrev}/${chNum}.json`,
|
|
||||||
);
|
|
||||||
if (!response.ok) throw new Error(`Unable to load ${selectedBook.name} ${chNum}.`);
|
|
||||||
const data = await response.json();
|
|
||||||
const verses = parseBibleChapter(data);
|
|
||||||
if (!verses.length) throw new Error(`No verses returned for ${selectedBook.name} ${chNum}.`);
|
|
||||||
versesByChapter[chNum] = verses;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chapterIndexByNum = new Map(sortedChapters.map((n, idx) => [n, idx]));
|
|
||||||
const chapters = sortedChapters.map((chNum) => ({
|
|
||||||
book: selectedBook.name,
|
|
||||||
bookAbbrev: selectedBook.abbrev,
|
|
||||||
chapter: chNum,
|
|
||||||
verses: versesByChapter[chNum],
|
|
||||||
chunks: [],
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Markers (rows with no passage) attach to the nearest upcoming chapter with a
|
|
||||||
// real passage, or fall back to the previous one for trailing markers.
|
|
||||||
const anchorChapterFor = importPreview.map((spec, i) => {
|
|
||||||
if (spec.parsed && spec.parsed !== 'invalid') return spec.parsed.startChapter;
|
|
||||||
for (let j = i + 1; j < importPreview.length; j += 1) {
|
|
||||||
const next = importPreview[j].parsed;
|
|
||||||
if (next && next !== 'invalid') return next.startChapter;
|
|
||||||
}
|
|
||||||
for (let j = i - 1; j >= 0; j -= 1) {
|
|
||||||
const prev = importPreview[j].parsed;
|
|
||||||
if (prev && prev !== 'invalid') return prev.startChapter;
|
|
||||||
}
|
|
||||||
return sortedChapters[0];
|
|
||||||
});
|
|
||||||
|
|
||||||
importPreview.forEach((spec, i) => {
|
|
||||||
if (spec.parsed === 'invalid') return; // skip unparseable rows entirely
|
|
||||||
const chIdx = chapterIndexByNum.get(anchorChapterFor[i]);
|
|
||||||
if (chIdx == null) return;
|
|
||||||
const target = chapters[chIdx];
|
|
||||||
|
|
||||||
const baseChunk = {
|
|
||||||
id: makeId(),
|
|
||||||
observation: '',
|
|
||||||
interpretation: '',
|
|
||||||
application: '',
|
|
||||||
crossReferences: [],
|
|
||||||
greekWords: [],
|
|
||||||
generalNotes: '',
|
|
||||||
episodeNumber: spec.episodeNumber,
|
|
||||||
episodeTitle: spec.title,
|
|
||||||
finalScript: '',
|
|
||||||
tags: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!spec.parsed) {
|
|
||||||
// No passage (e.g. a part intro/review) -> zero-length marker chunk.
|
|
||||||
target.chunks.push({ ...baseChunk, startVerse: 0, endVerse: 0, spilloverEndVerse: null });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { startChapter, startVerse, endChapter, endVerse } = spec.parsed;
|
|
||||||
if (endChapter === startChapter) {
|
|
||||||
target.chunks.push({ ...baseChunk, startVerse, endVerse, spilloverEndVerse: null });
|
|
||||||
} else {
|
|
||||||
const lastVerseOfStartChapter = versesByChapter[startChapter]?.at(-1)?.number ?? endVerse;
|
|
||||||
target.chunks.push({
|
|
||||||
...baseChunk,
|
|
||||||
startVerse,
|
|
||||||
endVerse: lastVerseOfStartChapter,
|
|
||||||
spilloverEndVerse: endVerse,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const newProject = {
|
|
||||||
id: makeId(),
|
|
||||||
title: importTitle.trim() || `${selectedBook.name} Sessions`,
|
|
||||||
translation: importTranslation,
|
|
||||||
lastEdited: Date.now(),
|
|
||||||
selectedChunkId: null,
|
|
||||||
chapters,
|
|
||||||
};
|
|
||||||
setProject(newProject);
|
|
||||||
setActiveChapterIndex(0);
|
|
||||||
setCurrentPage('setup');
|
|
||||||
} catch (error) {
|
|
||||||
setImportError(error.message || 'Import failed.');
|
|
||||||
} finally {
|
|
||||||
setImportBusy(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resumeProject = (id) => {
|
const resumeProject = (id) => {
|
||||||
const loaded = loadProjectById(id);
|
const loaded = loadProjectById(id);
|
||||||
if (!loaded) return;
|
if (!loaded) return;
|
||||||
@@ -1957,21 +1794,10 @@ const deleteProject = (id) => {
|
|||||||
deleteProject,
|
deleteProject,
|
||||||
// Audio (book player on Home + chapter player in the Reader)
|
// Audio (book player on Home + chapter player in the Reader)
|
||||||
...audio,
|
...audio,
|
||||||
// Import
|
|
||||||
importBookAbbrev, setImportBookAbbrev,
|
|
||||||
importTranslation, setImportTranslation,
|
|
||||||
importTitle, setImportTitle,
|
|
||||||
importFile,
|
|
||||||
importPreview,
|
|
||||||
importBusy,
|
|
||||||
importError,
|
|
||||||
handleImportFileChange,
|
|
||||||
runEpisodeImport,
|
|
||||||
// Reader
|
// Reader
|
||||||
...reader,
|
...reader,
|
||||||
copyVerse,
|
copyVerse,
|
||||||
// Setup
|
// Setup
|
||||||
availableTranslations,
|
|
||||||
project, setProject,
|
project, setProject,
|
||||||
setup,
|
setup,
|
||||||
titleEdited, setTitleEdited,
|
titleEdited, setTitleEdited,
|
||||||
|
|||||||
@@ -497,3 +497,18 @@ describe('Admin page', () => {
|
|||||||
expect(screen.queryByRole('button', { name: /admin/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: /admin/i })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Import page (state is local to ImportPage)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
describe('Import page', () => {
|
||||||
|
test('opens from Home, lists fetched translations, and goes back', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
await renderApp();
|
||||||
|
await user.click(screen.getByRole('button', { name: /import session list/i }));
|
||||||
|
expect(await screen.findByRole('heading', { name: /import session list/i })).toBeInTheDocument();
|
||||||
|
expect(await screen.findByRole('option', { name: 'KJV' })).toBeInTheDocument();
|
||||||
|
await user.click(screen.getByRole('button', { name: /back/i }));
|
||||||
|
expect(await screen.findByRole('button', { name: /import session list/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// The list of Bible translations offered by the HelloAO API, fetched once per
|
||||||
|
// visit and shared by every page that needs it (Setup, Import).
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
let cached = null;
|
||||||
|
let pending = null;
|
||||||
|
|
||||||
|
export function useAvailableTranslations() {
|
||||||
|
const [translations, setTranslations] = useState(cached ?? ['BSB']);
|
||||||
|
useEffect(() => {
|
||||||
|
if (cached) return;
|
||||||
|
pending ??= fetch('https://bible.helloao.org/api/available_translations.json')
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) cached = data;
|
||||||
|
return cached;
|
||||||
|
})
|
||||||
|
.catch(() => null)
|
||||||
|
.finally(() => { pending = null; });
|
||||||
|
let active = true;
|
||||||
|
pending.then((list) => { if (active && list) setTranslations(list); });
|
||||||
|
return () => { active = false; };
|
||||||
|
}, []);
|
||||||
|
return translations;
|
||||||
|
}
|
||||||
+147
-15
@@ -1,21 +1,153 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useApp } from '../context/AppContext.js';
|
import { useApp } from '../context/AppContext.js';
|
||||||
import { bookOptions } from '../utils/bibleUtils.jsx';
|
import { useAvailableTranslations } from '../hooks/useAvailableTranslations.js';
|
||||||
|
import { bookOptions, makeId, parseBibleChapter } from '../utils/bibleUtils.jsx';
|
||||||
|
import { parseEpisodeListDocx, parseEpisodePassage } from '../utils/importParsers.js';
|
||||||
|
|
||||||
export default function ImportPage() {
|
export default function ImportPage() {
|
||||||
const {
|
const { authStatus, setCurrentPage, setProject, setActiveChapterIndex } = useApp();
|
||||||
authStatus,
|
const availableTranslations = useAvailableTranslations();
|
||||||
setCurrentPage,
|
const [importBookAbbrev, setImportBookAbbrev] = useState(bookOptions[0].abbrev);
|
||||||
availableTranslations,
|
const [importTranslation, setImportTranslation] = useState('BSB');
|
||||||
importBookAbbrev, setImportBookAbbrev,
|
const [importTitle, setImportTitle] = useState('');
|
||||||
importTranslation, setImportTranslation,
|
const [importPreview, setImportPreview] = useState(null); // parsed episode specs, before fetching chapters
|
||||||
importTitle, setImportTitle,
|
const [importBusy, setImportBusy] = useState(false);
|
||||||
importFile,
|
const [importError, setImportError] = useState('');
|
||||||
importPreview,
|
|
||||||
importBusy,
|
const handleImportFileChange = async (file) => {
|
||||||
importError,
|
setImportPreview(null);
|
||||||
handleImportFileChange,
|
setImportError('');
|
||||||
runEpisodeImport,
|
if (!file) return;
|
||||||
} = useApp();
|
setImportBusy(true);
|
||||||
|
try {
|
||||||
|
const episodes = await parseEpisodeListDocx(file);
|
||||||
|
if (episodes.length === 0) {
|
||||||
|
throw new Error('No "Session # / Title / Passage" table found in that document.');
|
||||||
|
}
|
||||||
|
const specs = episodes.map((ep) => ({ ...ep, parsed: parseEpisodePassage(ep.passage) }));
|
||||||
|
setImportPreview(specs);
|
||||||
|
} catch (error) {
|
||||||
|
setImportError(error.message || 'Failed to read that document.');
|
||||||
|
} finally {
|
||||||
|
setImportBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runEpisodeImport = async () => {
|
||||||
|
if (!importPreview || importPreview.length === 0) {
|
||||||
|
setImportError('Choose a .docx file first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setImportBusy(true);
|
||||||
|
setImportError('');
|
||||||
|
try {
|
||||||
|
const selectedBook = bookOptions.find((b) => b.abbrev === importBookAbbrev) ?? bookOptions[0];
|
||||||
|
|
||||||
|
const chapterNumbersNeeded = new Set();
|
||||||
|
importPreview.forEach((spec) => {
|
||||||
|
if (spec.parsed && spec.parsed !== 'invalid') {
|
||||||
|
chapterNumbersNeeded.add(spec.parsed.startChapter);
|
||||||
|
chapterNumbersNeeded.add(spec.parsed.endChapter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sortedChapters = Array.from(chapterNumbersNeeded).sort((a, b) => a - b);
|
||||||
|
if (sortedChapters.length === 0) {
|
||||||
|
throw new Error('No verse passages found to import.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const versesByChapter = {};
|
||||||
|
for (const chNum of sortedChapters) {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://bible.helloao.org/api/${importTranslation}/${selectedBook.abbrev}/${chNum}.json`,
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new Error(`Unable to load ${selectedBook.name} ${chNum}.`);
|
||||||
|
const data = await response.json();
|
||||||
|
const verses = parseBibleChapter(data);
|
||||||
|
if (!verses.length) throw new Error(`No verses returned for ${selectedBook.name} ${chNum}.`);
|
||||||
|
versesByChapter[chNum] = verses;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chapterIndexByNum = new Map(sortedChapters.map((n, idx) => [n, idx]));
|
||||||
|
const chapters = sortedChapters.map((chNum) => ({
|
||||||
|
book: selectedBook.name,
|
||||||
|
bookAbbrev: selectedBook.abbrev,
|
||||||
|
chapter: chNum,
|
||||||
|
verses: versesByChapter[chNum],
|
||||||
|
chunks: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Markers (rows with no passage) attach to the nearest upcoming chapter with a
|
||||||
|
// real passage, or fall back to the previous one for trailing markers.
|
||||||
|
const anchorChapterFor = importPreview.map((spec, i) => {
|
||||||
|
if (spec.parsed && spec.parsed !== 'invalid') return spec.parsed.startChapter;
|
||||||
|
for (let j = i + 1; j < importPreview.length; j += 1) {
|
||||||
|
const next = importPreview[j].parsed;
|
||||||
|
if (next && next !== 'invalid') return next.startChapter;
|
||||||
|
}
|
||||||
|
for (let j = i - 1; j >= 0; j -= 1) {
|
||||||
|
const prev = importPreview[j].parsed;
|
||||||
|
if (prev && prev !== 'invalid') return prev.startChapter;
|
||||||
|
}
|
||||||
|
return sortedChapters[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
importPreview.forEach((spec, i) => {
|
||||||
|
if (spec.parsed === 'invalid') return; // skip unparseable rows entirely
|
||||||
|
const chIdx = chapterIndexByNum.get(anchorChapterFor[i]);
|
||||||
|
if (chIdx == null) return;
|
||||||
|
const target = chapters[chIdx];
|
||||||
|
|
||||||
|
const baseChunk = {
|
||||||
|
id: makeId(),
|
||||||
|
observation: '',
|
||||||
|
interpretation: '',
|
||||||
|
application: '',
|
||||||
|
crossReferences: [],
|
||||||
|
greekWords: [],
|
||||||
|
generalNotes: '',
|
||||||
|
episodeNumber: spec.episodeNumber,
|
||||||
|
episodeTitle: spec.title,
|
||||||
|
finalScript: '',
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!spec.parsed) {
|
||||||
|
// No passage (e.g. a part intro/review) -> zero-length marker chunk.
|
||||||
|
target.chunks.push({ ...baseChunk, startVerse: 0, endVerse: 0, spilloverEndVerse: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startChapter, startVerse, endChapter, endVerse } = spec.parsed;
|
||||||
|
if (endChapter === startChapter) {
|
||||||
|
target.chunks.push({ ...baseChunk, startVerse, endVerse, spilloverEndVerse: null });
|
||||||
|
} else {
|
||||||
|
const lastVerseOfStartChapter = versesByChapter[startChapter]?.at(-1)?.number ?? endVerse;
|
||||||
|
target.chunks.push({
|
||||||
|
...baseChunk,
|
||||||
|
startVerse,
|
||||||
|
endVerse: lastVerseOfStartChapter,
|
||||||
|
spilloverEndVerse: endVerse,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newProject = {
|
||||||
|
id: makeId(),
|
||||||
|
title: importTitle.trim() || `${selectedBook.name} Sessions`,
|
||||||
|
translation: importTranslation,
|
||||||
|
lastEdited: Date.now(),
|
||||||
|
selectedChunkId: null,
|
||||||
|
chapters,
|
||||||
|
};
|
||||||
|
setProject(newProject);
|
||||||
|
setActiveChapterIndex(0);
|
||||||
|
setCurrentPage('setup');
|
||||||
|
} catch (error) {
|
||||||
|
setImportError(error.message || 'Import failed.');
|
||||||
|
} finally {
|
||||||
|
setImportBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 text-slate-900">
|
<div className="min-h-screen bg-slate-50 text-slate-900">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useApp } from '../context/AppContext.js';
|
import { useApp } from '../context/AppContext.js';
|
||||||
|
import { useAvailableTranslations } from '../hooks/useAvailableTranslations.js';
|
||||||
import { bookOptions, formatChunkReference } from '../utils/bibleUtils.jsx';
|
import { bookOptions, formatChunkReference } from '../utils/bibleUtils.jsx';
|
||||||
|
|
||||||
function SetupForm({ setup, availableTranslations, titleEdited, loadingChapter, errorMessage, hideTitle, onField, onTitleChange, onLoad }) {
|
function SetupForm({ setup, availableTranslations, titleEdited, loadingChapter, errorMessage, hideTitle, onField, onTitleChange, onLoad }) {
|
||||||
@@ -83,7 +84,6 @@ export default function SetupPage() {
|
|||||||
headerButtons,
|
headerButtons,
|
||||||
project,
|
project,
|
||||||
setup,
|
setup,
|
||||||
availableTranslations,
|
|
||||||
titleEdited, setTitleEdited,
|
titleEdited, setTitleEdited,
|
||||||
activeChapterIndex, setActiveChapterIndex,
|
activeChapterIndex, setActiveChapterIndex,
|
||||||
activeChapter,
|
activeChapter,
|
||||||
@@ -111,6 +111,7 @@ export default function SetupPage() {
|
|||||||
deleteChunk,
|
deleteChunk,
|
||||||
updateProject,
|
updateProject,
|
||||||
} = useApp();
|
} = useApp();
|
||||||
|
const availableTranslations = useAvailableTranslations();
|
||||||
|
|
||||||
const chapterTabs = project?.chapters ?? [];
|
const chapterTabs = project?.chapters ?? [];
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user