mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
CBL Import (#1834)
* Wrote my own step tracker and added a prev button. Works up to first conflict flow. * Everything but final import is hooked up in the UI. Polish still needed, but getting there. * Making more progress in the CBL import flow. * Ready for the last step * Cleaned up some logic to prepare for the last step and reset * Users like order to be starting at 1 * Fixed a few bugs around cbl import * CBL import is ready for some basic testing * Added a reading list hook on side nav * Fixed up unit tests * Added icons and color to the import flow * Tweaked some phrasing * Hooked up a loading variable but disabled the component as it didn't look good. * Styling it up * changed an icon to better fit --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
57de661d71
commit
d88a4d5d0c
@ -961,322 +961,382 @@ public class ReadingListServiceTests
|
|||||||
Assert.Single(userWithList.ReadingLists);
|
Assert.Single(userWithList.ReadingLists);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
//
|
|
||||||
// #region CreateReadingListFromCBL
|
#region ValidateCBL
|
||||||
//
|
|
||||||
// private static CblReadingList LoadCblFromPath(string path)
|
[Fact]
|
||||||
// {
|
public async Task ValidateCblFile_ShouldFail_UserHasAccessToNoSeries()
|
||||||
// var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ReadingListService/");
|
{
|
||||||
//
|
await ResetDb();
|
||||||
// var reader = new System.Xml.Serialization.XmlSerializer(typeof(CblReadingList));
|
var cblReadingList = LoadCblFromPath("Fables.cbl");
|
||||||
// using var file = new StreamReader(Path.Join(testDirectory, path));
|
|
||||||
// var cblReadingList = (CblReadingList) reader.Deserialize(file);
|
// Mock up our series
|
||||||
// file.Close();
|
var fablesSeries = DbFactory.Series("Fables");
|
||||||
// return cblReadingList;
|
var fables2Series = DbFactory.Series("Fables: The Last Castle");
|
||||||
// }
|
|
||||||
//
|
fablesSeries.Volumes.Add(new Volume()
|
||||||
// [Fact]
|
{
|
||||||
// public async Task CreateReadingListFromCBL_ShouldCreateList()
|
Number = 1,
|
||||||
// {
|
Name = "2002",
|
||||||
// await ResetDb();
|
Chapters = new List<Chapter>()
|
||||||
// var cblReadingList = LoadCblFromPath("Fables.cbl");
|
{
|
||||||
//
|
EntityFactory.CreateChapter("1", false),
|
||||||
// // Mock up our series
|
EntityFactory.CreateChapter("2", false),
|
||||||
// var fablesSeries = DbFactory.Series("Fables");
|
EntityFactory.CreateChapter("3", false),
|
||||||
// var fables2Series = DbFactory.Series("Fables: The Last Castle");
|
|
||||||
//
|
}
|
||||||
// fablesSeries.Volumes.Add(new Volume()
|
});
|
||||||
// {
|
fables2Series.Volumes.Add(new Volume()
|
||||||
// Number = 1,
|
{
|
||||||
// Name = "2002",
|
Number = 1,
|
||||||
// Chapters = new List<Chapter>()
|
Name = "2003",
|
||||||
// {
|
Chapters = new List<Chapter>()
|
||||||
// EntityFactory.CreateChapter("1", false),
|
{
|
||||||
// EntityFactory.CreateChapter("2", false),
|
EntityFactory.CreateChapter("1", false),
|
||||||
// EntityFactory.CreateChapter("3", false),
|
EntityFactory.CreateChapter("2", false),
|
||||||
//
|
EntityFactory.CreateChapter("3", false),
|
||||||
// }
|
|
||||||
// });
|
}
|
||||||
// fables2Series.Volumes.Add(new Volume()
|
});
|
||||||
// {
|
|
||||||
// Number = 1,
|
_context.AppUser.Add(new AppUser()
|
||||||
// Name = "2003",
|
{
|
||||||
// Chapters = new List<Chapter>()
|
UserName = "majora2007",
|
||||||
// {
|
ReadingLists = new List<ReadingList>(),
|
||||||
// EntityFactory.CreateChapter("1", false),
|
Libraries = new List<Library>(),
|
||||||
// EntityFactory.CreateChapter("2", false),
|
});
|
||||||
// EntityFactory.CreateChapter("3", false),
|
|
||||||
//
|
_context.Library.Add(new Library()
|
||||||
// }
|
{
|
||||||
// });
|
Name = "Test Lib 2",
|
||||||
//
|
Type = LibraryType.Book,
|
||||||
// _context.AppUser.Add(new AppUser()
|
Series = new List<Series>()
|
||||||
// {
|
{
|
||||||
// UserName = "majora2007",
|
fablesSeries,
|
||||||
// ReadingLists = new List<ReadingList>(),
|
fables2Series,
|
||||||
// Libraries = new List<Library>()
|
},
|
||||||
// {
|
});
|
||||||
// new Library()
|
|
||||||
// {
|
await _unitOfWork.CommitAsync();
|
||||||
// Name = "Test LIb",
|
|
||||||
// Type = LibraryType.Book,
|
var importSummary = await _readingListService.ValidateCblFile(1, cblReadingList);
|
||||||
// Series = new List<Series>()
|
|
||||||
// {
|
Assert.Equal(CblImportResult.Fail, importSummary.Success);
|
||||||
// fablesSeries,
|
Assert.NotEmpty(importSummary.Results);
|
||||||
// fables2Series
|
}
|
||||||
// },
|
|
||||||
// },
|
[Fact]
|
||||||
// },
|
public async Task ValidateCblFile_ShouldFail_ServerHasNoSeries()
|
||||||
// });
|
{
|
||||||
// await _unitOfWork.CommitAsync();
|
await ResetDb();
|
||||||
//
|
var cblReadingList = LoadCblFromPath("Fables.cbl");
|
||||||
// var importSummary = await _readingListService.CreateReadingListFromCbl(1, cblReadingList);
|
|
||||||
//
|
// Mock up our series
|
||||||
// Assert.Equal(CblImportResult.Partial, importSummary.Success);
|
var fablesSeries = DbFactory.Series("Fablesa");
|
||||||
// Assert.NotEmpty(importSummary.Results);
|
var fables2Series = DbFactory.Series("Fablesa: The Last Castle");
|
||||||
//
|
|
||||||
// var createdList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1);
|
fablesSeries.Volumes.Add(new Volume()
|
||||||
//
|
{
|
||||||
// Assert.NotNull(createdList);
|
Number = 1,
|
||||||
// Assert.Equal("Fables", createdList.Title);
|
Name = "2002",
|
||||||
//
|
Chapters = new List<Chapter>()
|
||||||
// Assert.Equal(4, createdList.Items.Count);
|
{
|
||||||
// Assert.Equal(1, createdList.Items.First(item => item.Order == 0).ChapterId);
|
EntityFactory.CreateChapter("1", false),
|
||||||
// Assert.Equal(2, createdList.Items.First(item => item.Order == 1).ChapterId);
|
EntityFactory.CreateChapter("2", false),
|
||||||
// Assert.Equal(3, createdList.Items.First(item => item.Order == 2).ChapterId);
|
EntityFactory.CreateChapter("3", false),
|
||||||
// Assert.Equal(4, createdList.Items.First(item => item.Order == 3).ChapterId);
|
|
||||||
// }
|
}
|
||||||
//
|
});
|
||||||
// [Fact]
|
fables2Series.Volumes.Add(new Volume()
|
||||||
// public async Task CreateReadingListFromCBL_ShouldCreateList_ButOnlyIncludeSeriesThatUserHasAccessTo()
|
{
|
||||||
// {
|
Number = 1,
|
||||||
// await ResetDb();
|
Name = "2003",
|
||||||
// var cblReadingList = LoadCblFromPath("Fables.cbl");
|
Chapters = new List<Chapter>()
|
||||||
//
|
{
|
||||||
// // Mock up our series
|
EntityFactory.CreateChapter("1", false),
|
||||||
// var fablesSeries = DbFactory.Series("Fables");
|
EntityFactory.CreateChapter("2", false),
|
||||||
// var fables2Series = DbFactory.Series("Fables: The Last Castle");
|
EntityFactory.CreateChapter("3", false),
|
||||||
//
|
|
||||||
// fablesSeries.Volumes.Add(new Volume()
|
}
|
||||||
// {
|
});
|
||||||
// Number = 1,
|
|
||||||
// Name = "2002",
|
_context.AppUser.Add(new AppUser()
|
||||||
// Chapters = new List<Chapter>()
|
{
|
||||||
// {
|
UserName = "majora2007",
|
||||||
// EntityFactory.CreateChapter("1", false),
|
ReadingLists = new List<ReadingList>(),
|
||||||
// EntityFactory.CreateChapter("2", false),
|
Libraries = new List<Library>(),
|
||||||
// EntityFactory.CreateChapter("3", false),
|
});
|
||||||
//
|
|
||||||
// }
|
_context.Library.Add(new Library()
|
||||||
// });
|
{
|
||||||
// fables2Series.Volumes.Add(new Volume()
|
Name = "Test Lib 2",
|
||||||
// {
|
Type = LibraryType.Book,
|
||||||
// Number = 1,
|
Series = new List<Series>()
|
||||||
// Name = "2003",
|
{
|
||||||
// Chapters = new List<Chapter>()
|
fablesSeries,
|
||||||
// {
|
fables2Series,
|
||||||
// EntityFactory.CreateChapter("1", false),
|
},
|
||||||
// EntityFactory.CreateChapter("2", false),
|
});
|
||||||
// EntityFactory.CreateChapter("3", false),
|
|
||||||
//
|
await _unitOfWork.CommitAsync();
|
||||||
// }
|
|
||||||
// });
|
var importSummary = await _readingListService.ValidateCblFile(1, cblReadingList);
|
||||||
//
|
|
||||||
// _context.AppUser.Add(new AppUser()
|
Assert.Equal(CblImportResult.Fail, importSummary.Success);
|
||||||
// {
|
Assert.NotEmpty(importSummary.Results);
|
||||||
// UserName = "majora2007",
|
}
|
||||||
// ReadingLists = new List<ReadingList>(),
|
|
||||||
// Libraries = new List<Library>()
|
#endregion
|
||||||
// {
|
|
||||||
// new Library()
|
#region CreateReadingListFromCBL
|
||||||
// {
|
|
||||||
// Name = "Test LIb",
|
private static CblReadingList LoadCblFromPath(string path)
|
||||||
// Type = LibraryType.Book,
|
{
|
||||||
// Series = new List<Series>()
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ReadingListService/");
|
||||||
// {
|
|
||||||
// fablesSeries,
|
var reader = new System.Xml.Serialization.XmlSerializer(typeof(CblReadingList));
|
||||||
// },
|
using var file = new StreamReader(Path.Join(testDirectory, path));
|
||||||
// },
|
var cblReadingList = (CblReadingList) reader.Deserialize(file);
|
||||||
// },
|
file.Close();
|
||||||
// });
|
return cblReadingList;
|
||||||
//
|
}
|
||||||
// _context.Library.Add(new Library()
|
|
||||||
// {
|
[Fact]
|
||||||
// Name = "Test Lib 2",
|
public async Task CreateReadingListFromCBL_ShouldCreateList()
|
||||||
// Type = LibraryType.Book,
|
{
|
||||||
// Series = new List<Series>()
|
await ResetDb();
|
||||||
// {
|
var cblReadingList = LoadCblFromPath("Fables.cbl");
|
||||||
// fables2Series,
|
|
||||||
// },
|
// Mock up our series
|
||||||
// });
|
var fablesSeries = DbFactory.Series("Fables");
|
||||||
//
|
var fables2Series = DbFactory.Series("Fables: The Last Castle");
|
||||||
// await _unitOfWork.CommitAsync();
|
|
||||||
//
|
fablesSeries.Volumes.Add(new Volume()
|
||||||
// var importSummary = await _readingListService.CreateReadingListFromCbl(1, cblReadingList);
|
{
|
||||||
//
|
Number = 1,
|
||||||
// Assert.Equal(CblImportResult.Partial, importSummary.Success);
|
Name = "2002",
|
||||||
// Assert.NotEmpty(importSummary.Results);
|
Chapters = new List<Chapter>()
|
||||||
//
|
{
|
||||||
// var createdList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1);
|
EntityFactory.CreateChapter("1", false),
|
||||||
//
|
EntityFactory.CreateChapter("2", false),
|
||||||
// Assert.NotNull(createdList);
|
EntityFactory.CreateChapter("3", false),
|
||||||
// Assert.Equal("Fables", createdList.Title);
|
|
||||||
//
|
}
|
||||||
// Assert.Equal(3, createdList.Items.Count);
|
});
|
||||||
// Assert.Equal(1, createdList.Items.First(item => item.Order == 0).ChapterId);
|
fables2Series.Volumes.Add(new Volume()
|
||||||
// Assert.Equal(2, createdList.Items.First(item => item.Order == 1).ChapterId);
|
{
|
||||||
// Assert.Equal(3, createdList.Items.First(item => item.Order == 2).ChapterId);
|
Number = 1,
|
||||||
// Assert.NotNull(importSummary.Results.SingleOrDefault(r => r.Series == "Fables: The Last Castle"
|
Name = "2003",
|
||||||
// && r.Reason == CblImportReason.SeriesMissing));
|
Chapters = new List<Chapter>()
|
||||||
// }
|
{
|
||||||
//
|
EntityFactory.CreateChapter("1", false),
|
||||||
// [Fact]
|
EntityFactory.CreateChapter("2", false),
|
||||||
// public async Task CreateReadingListFromCBL_ShouldFail_UserHasAccessToNoSeries()
|
EntityFactory.CreateChapter("3", false),
|
||||||
// {
|
|
||||||
// await ResetDb();
|
}
|
||||||
// var cblReadingList = LoadCblFromPath("Fables.cbl");
|
});
|
||||||
//
|
|
||||||
// // Mock up our series
|
_context.AppUser.Add(new AppUser()
|
||||||
// var fablesSeries = DbFactory.Series("Fables");
|
{
|
||||||
// var fables2Series = DbFactory.Series("Fables: The Last Castle");
|
UserName = "majora2007",
|
||||||
//
|
ReadingLists = new List<ReadingList>(),
|
||||||
// fablesSeries.Volumes.Add(new Volume()
|
Libraries = new List<Library>()
|
||||||
// {
|
{
|
||||||
// Number = 1,
|
new Library()
|
||||||
// Name = "2002",
|
{
|
||||||
// Chapters = new List<Chapter>()
|
Name = "Test LIb",
|
||||||
// {
|
Type = LibraryType.Book,
|
||||||
// EntityFactory.CreateChapter("1", false),
|
Series = new List<Series>()
|
||||||
// EntityFactory.CreateChapter("2", false),
|
{
|
||||||
// EntityFactory.CreateChapter("3", false),
|
fablesSeries,
|
||||||
//
|
fables2Series
|
||||||
// }
|
},
|
||||||
// });
|
},
|
||||||
// fables2Series.Volumes.Add(new Volume()
|
},
|
||||||
// {
|
});
|
||||||
// Number = 1,
|
await _unitOfWork.CommitAsync();
|
||||||
// Name = "2003",
|
|
||||||
// Chapters = new List<Chapter>()
|
var importSummary = await _readingListService.CreateReadingListFromCbl(1, cblReadingList);
|
||||||
// {
|
|
||||||
// EntityFactory.CreateChapter("1", false),
|
Assert.Equal(CblImportResult.Partial, importSummary.Success);
|
||||||
// EntityFactory.CreateChapter("2", false),
|
Assert.NotEmpty(importSummary.Results);
|
||||||
// EntityFactory.CreateChapter("3", false),
|
|
||||||
//
|
var createdList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1);
|
||||||
// }
|
|
||||||
// });
|
Assert.NotNull(createdList);
|
||||||
//
|
Assert.Equal("Fables", createdList.Title);
|
||||||
// _context.AppUser.Add(new AppUser()
|
|
||||||
// {
|
Assert.Equal(4, createdList.Items.Count);
|
||||||
// UserName = "majora2007",
|
Assert.Equal(1, createdList.Items.First(item => item.Order == 0).ChapterId);
|
||||||
// ReadingLists = new List<ReadingList>(),
|
Assert.Equal(2, createdList.Items.First(item => item.Order == 1).ChapterId);
|
||||||
// Libraries = new List<Library>(),
|
Assert.Equal(3, createdList.Items.First(item => item.Order == 2).ChapterId);
|
||||||
// });
|
Assert.Equal(4, createdList.Items.First(item => item.Order == 3).ChapterId);
|
||||||
//
|
}
|
||||||
// _context.Library.Add(new Library()
|
|
||||||
// {
|
[Fact]
|
||||||
// Name = "Test Lib 2",
|
public async Task CreateReadingListFromCBL_ShouldCreateList_ButOnlyIncludeSeriesThatUserHasAccessTo()
|
||||||
// Type = LibraryType.Book,
|
{
|
||||||
// Series = new List<Series>()
|
await ResetDb();
|
||||||
// {
|
var cblReadingList = LoadCblFromPath("Fables.cbl");
|
||||||
// fablesSeries,
|
|
||||||
// fables2Series,
|
// Mock up our series
|
||||||
// },
|
var fablesSeries = DbFactory.Series("Fables");
|
||||||
// });
|
var fables2Series = DbFactory.Series("Fables: The Last Castle");
|
||||||
//
|
|
||||||
// await _unitOfWork.CommitAsync();
|
fablesSeries.Volumes.Add(new Volume()
|
||||||
//
|
{
|
||||||
// var importSummary = await _readingListService.CreateReadingListFromCbl(1, cblReadingList);
|
Number = 1,
|
||||||
//
|
Name = "2002",
|
||||||
// Assert.Equal(CblImportResult.Fail, importSummary.Success);
|
Chapters = new List<Chapter>()
|
||||||
// Assert.NotEmpty(importSummary.Results);
|
{
|
||||||
//
|
EntityFactory.CreateChapter("1", false),
|
||||||
// var createdList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1);
|
EntityFactory.CreateChapter("2", false),
|
||||||
//
|
EntityFactory.CreateChapter("3", false),
|
||||||
// Assert.Null(createdList);
|
|
||||||
// }
|
}
|
||||||
//
|
});
|
||||||
//
|
fables2Series.Volumes.Add(new Volume()
|
||||||
// [Fact]
|
{
|
||||||
// public async Task CreateReadingListFromCBL_ShouldUpdateAnExistingList()
|
Number = 1,
|
||||||
// {
|
Name = "2003",
|
||||||
// await ResetDb();
|
Chapters = new List<Chapter>()
|
||||||
// var cblReadingList = LoadCblFromPath("Fables.cbl");
|
{
|
||||||
//
|
EntityFactory.CreateChapter("1", false),
|
||||||
// // Mock up our series
|
EntityFactory.CreateChapter("2", false),
|
||||||
// var fablesSeries = DbFactory.Series("Fables");
|
EntityFactory.CreateChapter("3", false),
|
||||||
// var fables2Series = DbFactory.Series("Fables: The Last Castle");
|
|
||||||
//
|
}
|
||||||
// fablesSeries.Volumes.Add(new Volume()
|
});
|
||||||
// {
|
|
||||||
// Number = 1,
|
_context.AppUser.Add(new AppUser()
|
||||||
// Name = "2002",
|
{
|
||||||
// Chapters = new List<Chapter>()
|
UserName = "majora2007",
|
||||||
// {
|
ReadingLists = new List<ReadingList>(),
|
||||||
// EntityFactory.CreateChapter("1", false),
|
Libraries = new List<Library>()
|
||||||
// EntityFactory.CreateChapter("2", false),
|
{
|
||||||
// EntityFactory.CreateChapter("3", false),
|
new Library()
|
||||||
//
|
{
|
||||||
// }
|
Name = "Test LIb",
|
||||||
// });
|
Type = LibraryType.Book,
|
||||||
// fables2Series.Volumes.Add(new Volume()
|
Series = new List<Series>()
|
||||||
// {
|
{
|
||||||
// Number = 1,
|
fablesSeries,
|
||||||
// Name = "2003",
|
},
|
||||||
// Chapters = new List<Chapter>()
|
},
|
||||||
// {
|
},
|
||||||
// EntityFactory.CreateChapter("1", false),
|
});
|
||||||
// EntityFactory.CreateChapter("2", false),
|
|
||||||
// EntityFactory.CreateChapter("3", false),
|
_context.Library.Add(new Library()
|
||||||
//
|
{
|
||||||
// }
|
Name = "Test Lib 2",
|
||||||
// });
|
Type = LibraryType.Book,
|
||||||
//
|
Series = new List<Series>()
|
||||||
// _context.AppUser.Add(new AppUser()
|
{
|
||||||
// {
|
fables2Series,
|
||||||
// UserName = "majora2007",
|
},
|
||||||
// ReadingLists = new List<ReadingList>(),
|
});
|
||||||
// Libraries = new List<Library>()
|
|
||||||
// {
|
await _unitOfWork.CommitAsync();
|
||||||
// new Library()
|
|
||||||
// {
|
var importSummary = await _readingListService.CreateReadingListFromCbl(1, cblReadingList);
|
||||||
// Name = "Test LIb",
|
|
||||||
// Type = LibraryType.Book,
|
Assert.Equal(CblImportResult.Partial, importSummary.Success);
|
||||||
// Series = new List<Series>()
|
Assert.NotEmpty(importSummary.Results);
|
||||||
// {
|
|
||||||
// fablesSeries,
|
var createdList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1);
|
||||||
// fables2Series
|
|
||||||
// },
|
Assert.NotNull(createdList);
|
||||||
// },
|
Assert.Equal("Fables", createdList.Title);
|
||||||
// },
|
|
||||||
// });
|
Assert.Equal(3, createdList.Items.Count);
|
||||||
//
|
Assert.Equal(1, createdList.Items.First(item => item.Order == 0).ChapterId);
|
||||||
// await _unitOfWork.CommitAsync();
|
Assert.Equal(2, createdList.Items.First(item => item.Order == 1).ChapterId);
|
||||||
//
|
Assert.Equal(3, createdList.Items.First(item => item.Order == 2).ChapterId);
|
||||||
// // Create a reading list named Fables and add 2 chapters to it
|
Assert.NotNull(importSummary.Results.SingleOrDefault(r => r.Series == "Fables: The Last Castle"
|
||||||
// var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.ReadingLists);
|
&& r.Reason == CblImportReason.SeriesMissing));
|
||||||
// var readingList = await _readingListService.CreateReadingListForUser(user, "Fables");
|
}
|
||||||
// Assert.True(await _readingListService.AddChaptersToReadingList(1, new List<int>() {1, 3}, readingList));
|
|
||||||
// Assert.Equal(2, readingList.Items.Count);
|
[Fact]
|
||||||
//
|
public async Task CreateReadingListFromCBL_ShouldUpdateAnExistingList()
|
||||||
// // Attempt to import a Cbl with same reading list name
|
{
|
||||||
// var importSummary = await _readingListService.CreateReadingListFromCbl(1, cblReadingList);
|
await ResetDb();
|
||||||
//
|
var cblReadingList = LoadCblFromPath("Fables.cbl");
|
||||||
// Assert.Equal(CblImportResult.Partial, importSummary.Success);
|
|
||||||
// Assert.NotEmpty(importSummary.Results);
|
// Mock up our series
|
||||||
//
|
var fablesSeries = DbFactory.Series("Fables");
|
||||||
// var createdList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1);
|
var fables2Series = DbFactory.Series("Fables: The Last Castle");
|
||||||
//
|
|
||||||
// Assert.NotNull(createdList);
|
fablesSeries.Volumes.Add(new Volume()
|
||||||
// Assert.Equal("Fables", createdList.Title);
|
{
|
||||||
//
|
Number = 1,
|
||||||
// Assert.Equal(4, createdList.Items.Count);
|
Name = "2002",
|
||||||
// Assert.Equal(4, importSummary.SuccessfulInserts.Count);
|
Chapters = new List<Chapter>()
|
||||||
//
|
{
|
||||||
// Assert.Equal(1, createdList.Items.First(item => item.Order == 0).ChapterId);
|
EntityFactory.CreateChapter("1", false),
|
||||||
// Assert.Equal(3, createdList.Items.First(item => item.Order == 1).ChapterId); // we inserted 3 first
|
EntityFactory.CreateChapter("2", false),
|
||||||
// Assert.Equal(2, createdList.Items.First(item => item.Order == 2).ChapterId);
|
EntityFactory.CreateChapter("3", false),
|
||||||
// Assert.Equal(4, createdList.Items.First(item => item.Order == 3).ChapterId);
|
|
||||||
// }
|
}
|
||||||
// #endregion
|
});
|
||||||
//
|
fables2Series.Volumes.Add(new Volume()
|
||||||
|
{
|
||||||
|
Number = 1,
|
||||||
|
Name = "2003",
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false),
|
||||||
|
EntityFactory.CreateChapter("2", false),
|
||||||
|
EntityFactory.CreateChapter("3", false),
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
ReadingLists = new List<ReadingList>(),
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
new Library()
|
||||||
|
{
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
fablesSeries,
|
||||||
|
fables2Series
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
// Create a reading list named Fables and add 2 chapters to it
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.ReadingLists);
|
||||||
|
var readingList = await _readingListService.CreateReadingListForUser(user, "Fables");
|
||||||
|
Assert.True(await _readingListService.AddChaptersToReadingList(1, new List<int>() {1, 3}, readingList));
|
||||||
|
Assert.Equal(2, readingList.Items.Count);
|
||||||
|
|
||||||
|
// Attempt to import a Cbl with same reading list name
|
||||||
|
var importSummary = await _readingListService.CreateReadingListFromCbl(1, cblReadingList);
|
||||||
|
|
||||||
|
Assert.Equal(CblImportResult.Partial, importSummary.Success);
|
||||||
|
Assert.NotEmpty(importSummary.Results);
|
||||||
|
|
||||||
|
var createdList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1);
|
||||||
|
|
||||||
|
Assert.NotNull(createdList);
|
||||||
|
Assert.Equal("Fables", createdList.Title);
|
||||||
|
|
||||||
|
Assert.Equal(4, createdList.Items.Count);
|
||||||
|
Assert.Equal(4, importSummary.SuccessfulInserts.Count);
|
||||||
|
|
||||||
|
Assert.Equal(1, createdList.Items.First(item => item.Order == 0).ChapterId);
|
||||||
|
Assert.Equal(3, createdList.Items.First(item => item.Order == 1).ChapterId); // we inserted 3 first
|
||||||
|
Assert.Equal(2, createdList.Items.First(item => item.Order == 2).ChapterId);
|
||||||
|
Assert.Equal(4, createdList.Items.First(item => item.Order == 3).ChapterId);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
68
API/Controllers/CBLController.cs
Normal file
68
API/Controllers/CBLController.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.ReadingLists;
|
||||||
|
using API.DTOs.ReadingLists.CBL;
|
||||||
|
using API.Extensions;
|
||||||
|
using API.Services;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responsible for the CBL import flow
|
||||||
|
/// </summary>
|
||||||
|
public class CblController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IReadingListService _readingListService;
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
|
|
||||||
|
public CblController(IReadingListService readingListService, IDirectoryService directoryService)
|
||||||
|
{
|
||||||
|
_readingListService = readingListService;
|
||||||
|
_directoryService = directoryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The first step in a cbl import. This validates the cbl file that if an import occured, would it be successful.
|
||||||
|
/// If this returns errors, the cbl will always be rejected by Kavita.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="file">FormBody with parameter name of cbl</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("validate")]
|
||||||
|
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl([FromForm(Name = "cbl")] IFormFile file)
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
var cbl = await SaveAndLoadCblFile(userId, file);
|
||||||
|
|
||||||
|
var importSummary = await _readingListService.ValidateCblFile(userId, cbl);
|
||||||
|
return Ok(importSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs the actual import (assuming dryRun = false)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="file">FormBody with parameter name of cbl</param>
|
||||||
|
/// <param name="dryRun">If true, will only emulate the import but not perform. This should be done to preview what will happen</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("import")]
|
||||||
|
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file, [FromForm(Name = "dryRun")] bool dryRun = false)
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
var cbl = await SaveAndLoadCblFile(userId, file);
|
||||||
|
|
||||||
|
return Ok(await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CblReadingList> SaveAndLoadCblFile(int userId, IFormFile file)
|
||||||
|
{
|
||||||
|
var filename = Path.GetRandomFileName();
|
||||||
|
var outputFile = Path.Join(_directoryService.TempDirectory, filename);
|
||||||
|
await using var stream = System.IO.File.Create(outputFile);
|
||||||
|
await file.CopyToAsync(stream);
|
||||||
|
stream.Close();
|
||||||
|
return ReadingListService.LoadCblFromPath(outputFile);
|
||||||
|
}
|
||||||
|
}
|
@ -484,22 +484,4 @@ public class ReadingListController : BaseApiController
|
|||||||
if (string.IsNullOrEmpty(name)) return true;
|
if (string.IsNullOrEmpty(name)) return true;
|
||||||
return Ok(await _unitOfWork.ReadingListRepository.ReadingListExists(name));
|
return Ok(await _unitOfWork.ReadingListRepository.ReadingListExists(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// [HttpPost("import-cbl")]
|
|
||||||
// public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file, [FromForm(Name = "dryRun")] bool dryRun = false)
|
|
||||||
// {
|
|
||||||
// var userId = User.GetUserId();
|
|
||||||
// var filename = Path.GetRandomFileName();
|
|
||||||
// var outputFile = Path.Join(_directoryService.TempDirectory, filename);
|
|
||||||
//
|
|
||||||
// await using var stream = System.IO.File.Create(outputFile);
|
|
||||||
// await file.CopyToAsync(stream);
|
|
||||||
// stream.Close();
|
|
||||||
// var cbl = ReadingListService.LoadCblFromPath(outputFile);
|
|
||||||
//
|
|
||||||
// // We need to pass the temp file back
|
|
||||||
//
|
|
||||||
// var importSummary = await _readingListService.ValidateCblFile(userId, cbl);
|
|
||||||
// return importSummary.Results.Any() ? Ok(importSummary) : Ok(await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun));
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using API.DTOs.ReadingLists.CBL;
|
|
||||||
|
|
||||||
namespace API.DTOs.ReadingLists;
|
namespace API.DTOs.ReadingLists.CBL;
|
||||||
|
|
||||||
public enum CblImportResult {
|
public enum CblImportResult {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -64,10 +63,19 @@ public enum CblImportReason
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("All Chapters Missing")]
|
[Description("All Chapters Missing")]
|
||||||
AllChapterMissing = 7,
|
AllChapterMissing = 7,
|
||||||
|
/// <summary>
|
||||||
|
/// The Chapter was imported
|
||||||
|
/// </summary>
|
||||||
|
[Description("Success")]
|
||||||
|
Success = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CblBookResult
|
public class CblBookResult
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order in the CBL
|
||||||
|
/// </summary>
|
||||||
|
public int Order { get; set; }
|
||||||
public string Series { get; set; }
|
public string Series { get; set; }
|
||||||
public string Volume { get; set; }
|
public string Volume { get; set; }
|
||||||
public string Number { get; set; }
|
public string Number { get; set; }
|
||||||
@ -95,10 +103,5 @@ public class CblImportSummaryDto
|
|||||||
public ICollection<CblBookResult> Results { get; set; }
|
public ICollection<CblBookResult> Results { get; set; }
|
||||||
public CblImportResult Success { get; set; }
|
public CblImportResult Success { get; set; }
|
||||||
public ICollection<CblBookResult> SuccessfulInserts { get; set; }
|
public ICollection<CblBookResult> SuccessfulInserts { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// A list of Series that are within the CBL but map to multiple libraries within Kavita
|
|
||||||
/// </summary>
|
|
||||||
public IList<SeriesDto> Conflicts { get; set; }
|
|
||||||
public IList<CblConflictQuestion> Conflicts2 { get; set; }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ public interface ISeriesRepository
|
|||||||
Task<bool> IsSeriesInWantToRead(int userId, int seriesId);
|
Task<bool> IsSeriesInWantToRead(int userId, int seriesId);
|
||||||
Task<Series> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
|
Task<Series> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
|
||||||
|
|
||||||
Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IEnumerable<string> normalizedNames,
|
Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> normalizedNames,
|
||||||
int userId, SeriesIncludes includes = SeriesIncludes.None);
|
int userId, SeriesIncludes includes = SeriesIncludes.None);
|
||||||
Task<IEnumerable<SeriesDto>> GetAllSeriesDtosByNameAsync(IEnumerable<string> normalizedNames,
|
Task<IEnumerable<SeriesDto>> GetAllSeriesDtosByNameAsync(IEnumerable<string> normalizedNames,
|
||||||
int userId, SeriesIncludes includes = SeriesIncludes.None);
|
int userId, SeriesIncludes includes = SeriesIncludes.None);
|
||||||
@ -1213,14 +1213,14 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IEnumerable<string> normalizedNames,
|
public async Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> normalizedNames,
|
||||||
int userId, SeriesIncludes includes = SeriesIncludes.None)
|
int userId, SeriesIncludes includes = SeriesIncludes.None)
|
||||||
{
|
{
|
||||||
var libraryIds = _context.Library.GetUserLibraries(userId);
|
var libraryIds = _context.Library.GetUserLibraries(userId);
|
||||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
|
||||||
return await _context.Series
|
return await _context.Series
|
||||||
.Where(s => normalizedNames.Contains(s.NormalizedName))
|
.Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName))
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||||
.RestrictAgainstAgeRestriction(userRating)
|
.RestrictAgainstAgeRestriction(userRating)
|
||||||
.Includes(includes)
|
.Includes(includes)
|
||||||
|
@ -355,13 +355,20 @@ public class ReadingListService : IReadingListService
|
|||||||
CblName = cblReading.Name,
|
CblName = cblReading.Name,
|
||||||
Success = CblImportResult.Success,
|
Success = CblImportResult.Success,
|
||||||
Results = new List<CblBookResult>(),
|
Results = new List<CblBookResult>(),
|
||||||
SuccessfulInserts = new List<CblBookResult>(),
|
SuccessfulInserts = new List<CblBookResult>()
|
||||||
Conflicts = new List<SeriesDto>(),
|
|
||||||
Conflicts2 = new List<CblConflictQuestion>()
|
|
||||||
};
|
};
|
||||||
if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl;
|
if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl;
|
||||||
|
|
||||||
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct();
|
// Is there another reading list with the same name?
|
||||||
|
if (await _unitOfWork.ReadingListRepository.ReadingListExists(cblReading.Name))
|
||||||
|
{
|
||||||
|
importSummary.Results.Add(new CblBookResult()
|
||||||
|
{
|
||||||
|
Reason = CblImportReason.NameConflict
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
|
||||||
var userSeries =
|
var userSeries =
|
||||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||||
if (!userSeries.Any())
|
if (!userSeries.Any())
|
||||||
@ -421,10 +428,11 @@ public class ReadingListService : IReadingListService
|
|||||||
SuccessfulInserts = new List<CblBookResult>()
|
SuccessfulInserts = new List<CblBookResult>()
|
||||||
};
|
};
|
||||||
|
|
||||||
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct();
|
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
|
||||||
var userSeries =
|
var userSeries =
|
||||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||||
var allSeries = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.Name));
|
var allSeries = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.Name));
|
||||||
|
var allSeriesLocalized = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.LocalizedName));
|
||||||
|
|
||||||
var readingListNameNormalized = Tasks.Scanner.Parser.Parser.Normalize(cblReading.Name);
|
var readingListNameNormalized = Tasks.Scanner.Parser.Parser.Normalize(cblReading.Name);
|
||||||
// Get all the user's reading lists
|
// Get all the user's reading lists
|
||||||
@ -452,38 +460,52 @@ public class ReadingListService : IReadingListService
|
|||||||
foreach (var (book, i) in cblReading.Books.Book.Select((value, i) => ( value, i )))
|
foreach (var (book, i) in cblReading.Books.Book.Select((value, i) => ( value, i )))
|
||||||
{
|
{
|
||||||
var normalizedSeries = Tasks.Scanner.Parser.Parser.Normalize(book.Series);
|
var normalizedSeries = Tasks.Scanner.Parser.Parser.Normalize(book.Series);
|
||||||
if (!allSeries.TryGetValue(normalizedSeries, out var bookSeries))
|
if (!allSeries.TryGetValue(normalizedSeries, out var bookSeries) && !allSeriesLocalized.TryGetValue(normalizedSeries, out bookSeries))
|
||||||
{
|
{
|
||||||
importSummary.Results.Add(new CblBookResult(book)
|
importSummary.Results.Add(new CblBookResult(book)
|
||||||
{
|
{
|
||||||
Reason = CblImportReason.SeriesMissing
|
Reason = CblImportReason.SeriesMissing,
|
||||||
|
Order = i
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Prioritize lookup by Volume then Chapter, but allow fallback to just Chapter
|
// Prioritize lookup by Volume then Chapter, but allow fallback to just Chapter
|
||||||
var matchingVolume = bookSeries.Volumes.FirstOrDefault(v => book.Volume == v.Name) ?? bookSeries.Volumes.FirstOrDefault(v => v.Number == 0);
|
var bookVolume = string.IsNullOrEmpty(book.Volume)
|
||||||
|
? Tasks.Scanner.Parser.Parser.DefaultVolume
|
||||||
|
: book.Volume;
|
||||||
|
var matchingVolume = bookSeries.Volumes.FirstOrDefault(v => bookVolume == v.Name) ?? bookSeries.Volumes.FirstOrDefault(v => v.Number == 0);
|
||||||
if (matchingVolume == null)
|
if (matchingVolume == null)
|
||||||
{
|
{
|
||||||
importSummary.Results.Add(new CblBookResult(book)
|
importSummary.Results.Add(new CblBookResult(book)
|
||||||
{
|
{
|
||||||
Reason = CblImportReason.VolumeMissing
|
Reason = CblImportReason.VolumeMissing,
|
||||||
|
Order = i
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Number == book.Number);
|
// We need to handle chapter 0 or empty string when it's just a volume
|
||||||
|
var bookNumber = string.IsNullOrEmpty(book.Number)
|
||||||
|
? Tasks.Scanner.Parser.Parser.DefaultChapter
|
||||||
|
: book.Number;
|
||||||
|
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Number == bookNumber);
|
||||||
if (chapter == null)
|
if (chapter == null)
|
||||||
{
|
{
|
||||||
importSummary.Results.Add(new CblBookResult(book)
|
importSummary.Results.Add(new CblBookResult(book)
|
||||||
{
|
{
|
||||||
Reason = CblImportReason.ChapterMissing
|
Reason = CblImportReason.ChapterMissing,
|
||||||
|
Order = i
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// See if a matching item already exists
|
// See if a matching item already exists
|
||||||
ExistsOrAddReadingListItem(readingList, bookSeries.Id, matchingVolume.Id, chapter.Id);
|
ExistsOrAddReadingListItem(readingList, bookSeries.Id, matchingVolume.Id, chapter.Id);
|
||||||
importSummary.SuccessfulInserts.Add(new CblBookResult(book));
|
importSummary.SuccessfulInserts.Add(new CblBookResult(book)
|
||||||
|
{
|
||||||
|
Reason = CblImportReason.Success,
|
||||||
|
Order = i
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (importSummary.SuccessfulInserts.Count != cblReading.Books.Book.Count || importSummary.Results.Count > 0)
|
if (importSummary.SuccessfulInserts.Count != cblReading.Books.Book.Count || importSummary.Results.Count > 0)
|
||||||
@ -491,9 +513,14 @@ public class ReadingListService : IReadingListService
|
|||||||
importSummary.Success = CblImportResult.Partial;
|
importSummary.Success = CblImportResult.Partial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (importSummary.SuccessfulInserts.Count == 0 && importSummary.Results.Count == cblReading.Books.Book.Count)
|
||||||
|
{
|
||||||
|
importSummary.Success = CblImportResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
await CalculateReadingListAgeRating(readingList);
|
await CalculateReadingListAgeRating(readingList);
|
||||||
|
|
||||||
if (!dryRun) return importSummary;
|
if (dryRun) return importSummary;
|
||||||
|
|
||||||
if (!_unitOfWork.HasChanges()) return importSummary;
|
if (!_unitOfWork.HasChanges()) return importSummary;
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
@ -90,4 +90,4 @@ img {
|
|||||||
0px 0px calc(0.5px*3.14) 0.3px rgb(0 0 0 / 43%),
|
0px 0px calc(0.5px*3.14) 0.3px rgb(0 0 0 / 43%),
|
||||||
0px 0px 1px 0.5px rgb(0 0 0 / 43%);
|
0px 0px 1px 0.5px rgb(0 0 0 / 43%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CblImportReason } from "./cbl-import-reason.enum";
|
import { CblImportReason } from "./cbl-import-reason.enum";
|
||||||
|
|
||||||
export interface CblBookResult {
|
export interface CblBookResult {
|
||||||
|
order: number;
|
||||||
series: string;
|
series: string;
|
||||||
volume: string;
|
volume: string;
|
||||||
number: string;
|
number: string;
|
||||||
|
@ -7,4 +7,5 @@ export enum CblImportReason {
|
|||||||
EmptyFile = 5,
|
EmptyFile = 5,
|
||||||
SeriesCollision = 6,
|
SeriesCollision = 6,
|
||||||
AllChapterMissing = 7,
|
AllChapterMissing = 7,
|
||||||
|
Success = 8
|
||||||
}
|
}
|
@ -12,7 +12,4 @@ export interface CblImportSummary {
|
|||||||
results: Array<CblBookResult>;
|
results: Array<CblBookResult>;
|
||||||
success: CblImportResult;
|
success: CblImportResult;
|
||||||
successfulInserts: Array<CblBookResult>;
|
successfulInserts: Array<CblBookResult>;
|
||||||
conflicts: Array<Series>;
|
|
||||||
conflicts2: Array<CblConflictQuestion>;
|
|
||||||
|
|
||||||
}
|
}
|
@ -95,7 +95,11 @@ export class ReadingListService {
|
|||||||
return this.httpClient.get<boolean>(this.baseUrl + 'readinglist/name-exists?name=' + name);
|
return this.httpClient.get<boolean>(this.baseUrl + 'readinglist/name-exists?name=' + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateCbl(form: FormData) {
|
||||||
|
return this.httpClient.post<CblImportSummary>(this.baseUrl + 'cbl/validate', form);
|
||||||
|
}
|
||||||
|
|
||||||
importCbl(form: FormData) {
|
importCbl(form: FormData) {
|
||||||
return this.httpClient.post<CblImportSummary>(this.baseUrl + 'readinglist/import-cbl', form);
|
return this.httpClient.post<CblImportSummary>(this.baseUrl + 'cbl/import', form);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,10 +198,10 @@ $action-bar-height: 38px;
|
|||||||
height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2
|
height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2
|
||||||
}
|
}
|
||||||
|
|
||||||
&.immersive {
|
// &.immersive {
|
||||||
// Note: I removed this for bug: https://github.com/Kareadita/Kavita/issues/1726
|
// // Note: I removed this for bug: https://github.com/Kareadita/Kavita/issues/1726
|
||||||
//height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
|
// //height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
|
||||||
}
|
// }
|
||||||
|
|
||||||
a, :link {
|
a, :link {
|
||||||
color: var(--brtheme-link-text-color);
|
color: var(--brtheme-link-text-color);
|
||||||
|
@ -28,7 +28,7 @@ export class ReadingListsComponent implements OnInit {
|
|||||||
isAdmin: boolean = false;
|
isAdmin: boolean = false;
|
||||||
jumpbarKeys: Array<JumpKey> = [];
|
jumpbarKeys: Array<JumpKey> = [];
|
||||||
actions: {[key: number]: Array<ActionItem<ReadingList>>} = {};
|
actions: {[key: number]: Array<ActionItem<ReadingList>>} = {};
|
||||||
globalActions: Array<ActionItem<any>> = []; //[{action: Action.Import, title: 'Import CBL', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}]
|
globalActions: Array<ActionItem<any>> = [{action: Action.Import, title: 'Import CBL', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
|
||||||
|
|
||||||
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||||
private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService,
|
private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService,
|
||||||
@ -63,6 +63,7 @@ export class ReadingListsComponent implements OnInit {
|
|||||||
importCbl() {
|
importCbl() {
|
||||||
const ref = this.ngbModal.open(ImportCblModalComponent, {size: 'xl'});
|
const ref = this.ngbModal.open(ImportCblModalComponent, {size: 'xl'});
|
||||||
ref.closed.subscribe(result => this.loadPage());
|
ref.closed.subscribe(result => this.loadPage());
|
||||||
|
ref.dismissed.subscribe(_ => this.loadPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReadingListActionCallback(action: ActionItem<ReadingList>, readingList: ReadingList) {
|
handleReadingListActionCallback(action: ActionItem<ReadingList>, readingList: ReadingList) {
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
<div class="card card-timeline px-2 mb-3 border-none">
|
||||||
|
<ul class="bs4-order-tracking">
|
||||||
|
<ng-container *ngFor="let step of steps">
|
||||||
|
<li class="step" [ngClass]="{'active': step.index === currentStep}">
|
||||||
|
<div><i class="{{step.icon}}"></i></div>
|
||||||
|
{{step.title}}
|
||||||
|
</li>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</div>
|
@ -0,0 +1,80 @@
|
|||||||
|
|
||||||
|
.bs4-order-tracking {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #878788;
|
||||||
|
padding-left: 0px;
|
||||||
|
margin-top: 30px
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li {
|
||||||
|
list-style-type: none;
|
||||||
|
font-size: 17px;
|
||||||
|
width: 25%;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #878788;
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li:first-child:before {
|
||||||
|
margin-left: 15px !important;
|
||||||
|
padding-left: 11px !important;
|
||||||
|
text-align: left !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li:last-child:before {
|
||||||
|
margin-right: 5px !important;
|
||||||
|
padding-right: 11px !important;
|
||||||
|
text-align: right !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li>div {
|
||||||
|
color: #fff;
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 50px;
|
||||||
|
display: block;
|
||||||
|
font-size: 20px;
|
||||||
|
background: #878788;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li:after {
|
||||||
|
content: '';
|
||||||
|
width: 150%;
|
||||||
|
height: 2px;
|
||||||
|
background: #878788;
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
right: 0%;
|
||||||
|
top: 25px;
|
||||||
|
z-index: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li:first-child:after {
|
||||||
|
left: 50%
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li:last-child:after {
|
||||||
|
left: 0% !important;
|
||||||
|
width: 0% !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li.active {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li.active>div {
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs4-order-tracking li.active:after {
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-timeline {
|
||||||
|
z-index: 0
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Component, Input, ChangeDetectionStrategy, OnInit, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
||||||
|
|
||||||
|
|
||||||
|
export interface TimelineStep {
|
||||||
|
title: string;
|
||||||
|
active: boolean;
|
||||||
|
icon: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-step-tracker',
|
||||||
|
templateUrl: './step-tracker.component.html',
|
||||||
|
styleUrls: ['./step-tracker.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class StepTrackerComponent {
|
||||||
|
@Input() steps: Array<TimelineStep> = [];
|
||||||
|
@Input() currentStep: number = 0;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private readonly cdRef: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
}
|
@ -1,47 +1,62 @@
|
|||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">CBL Import: {{currentStep.title}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">CBL Import</h4>
|
||||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
<div class="modal-body scrollable-modal">
|
||||||
|
<div class="row g-0" style="min-width: 135px;">
|
||||||
<div class="row g-0" *ngIf="currentStep.index === 0">
|
<app-step-tracker [steps]="steps" [currentStep]="currentStepIndex"></app-step-tracker>
|
||||||
<p>Import a .cbl file as a reading list</p>
|
|
||||||
<form [formGroup]="uploadForm" enctype="multipart/form-data">
|
|
||||||
<file-upload formControlName="files"></file-upload>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="currentStep.index === 1">
|
|
||||||
<div class="row g-0">
|
|
||||||
<ng-container *ngIf="validateSummary; else noValidateIssues">
|
|
||||||
<h5>There are issues with the CBL that will prevent an import. Correct these issues then try again.</h5>
|
|
||||||
<ol class="list-group list-group-flush" >
|
|
||||||
<li class="list-group-item no-hover" *ngFor="let result of validateSummary.results">
|
|
||||||
{{result | cblConflictReason}}
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #noValidateIssues>No issues found with CBL, press next.</ng-template>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="currentStep.index === 2 && dryRunSummary">
|
<!-- This is going to need to have a fixed height with a scrollbar-->
|
||||||
<div class="row g-0">
|
<div>
|
||||||
<h5>This is a dry run and shows what will happen if you press Next</h5>
|
<div class="row g-0" *ngIf="currentStepIndex === Step.Import">
|
||||||
<h6>The import was a {{dryRunSummary.success}}</h6>
|
<p>To get started, import a .cbl file. Kavita will perform multiple checks before importing. Some steps will block moving forward due to issues with the file.</p>
|
||||||
<ul class="list-group list-group-flush" *ngIf="dryRunSummary">
|
<form [formGroup]="uploadForm" enctype="multipart/form-data">
|
||||||
<li class="list-group-item no-hover" *ngFor="let result of dryRunSummary.results">
|
<file-upload formControlName="files"></file-upload>
|
||||||
{{result | cblConflictReason}}
|
</form>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="list-group list-group-flush" *ngIf="dryRunSummary">
|
|
||||||
<li class="list-group-item no-hover" *ngFor="let result of dryRunSummary.successfulInserts">
|
|
||||||
{{result | cblConflictReason}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
|
||||||
|
<ng-container *ngIf="currentStepIndex === Step.Validate">
|
||||||
|
<div class="row g-0">
|
||||||
|
<ng-container *ngIf="validateSummary">
|
||||||
|
<ng-container *ngIf="validateSummary.results.length > 0; else noValidateIssues">
|
||||||
|
<h5>There are issues with the CBL that will prevent an import. Correct these issues then try again.</h5>
|
||||||
|
<ol class="list-group list-group-numbered list-group-flush" >
|
||||||
|
<li class="list-group-item no-hover" *ngFor="let result of validateSummary.results"
|
||||||
|
[innerHTML]="result | cblConflictReason | safeHtml">
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #noValidateIssues>
|
||||||
|
No issues found with CBL, press next.
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="currentStepIndex === Step.DryRun && dryRunSummary">
|
||||||
|
<div class="row g-0">
|
||||||
|
<h5>This is a dry run and shows what will happen if you press Next</h5>
|
||||||
|
<h6>The import was a {{dryRunSummary.success | cblImportResult}}</h6>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item no-hover" *ngFor="let result of dryRunResults"
|
||||||
|
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="currentStepIndex === Step.Finalize && finalizeSummary && dryRunSummary">
|
||||||
|
<div class="row g-0">
|
||||||
|
<h5>{{finalizeSummary.success | cblImportResult }} on {{dryRunSummary.cblName}} Import</h5>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item no-hover" *ngFor="let result of finalizeResults"
|
||||||
|
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}">
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -49,7 +64,8 @@
|
|||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<a class="btn btn-icon" href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/reading-lists#creating-a-reading-list-via-cbl" target="_blank" rel="noopener noreferrer">Help</a>
|
<a class="btn btn-icon" href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/reading-lists#creating-a-reading-list-via-cbl" target="_blank" rel="noopener noreferrer">Help</a>
|
||||||
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
||||||
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">Next</button>
|
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">Prev</button>
|
||||||
|
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{NextButtonLabel}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,41 @@
|
|||||||
.file-input {
|
.file-input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep .file-info {
|
||||||
|
width: 83%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .file-buttons {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
file-upload {
|
||||||
|
background: none;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .upload-input {
|
||||||
|
color: var(--input-text-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep file-upload-list-item {
|
||||||
|
color: var(--input-text-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .remove-btn {
|
||||||
|
background: #C0392B;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--input-text-color) !important;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 3px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .reading-list-success--item {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .reading-list-fail--item {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
@ -2,9 +2,13 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, View
|
|||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { FileUploadValidators } from '@iplab/ngx-file-upload';
|
import { FileUploadValidators } from '@iplab/ngx-file-upload';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
|
import { CblBookResult } from 'src/app/_models/reading-list/cbl/cbl-book-result';
|
||||||
|
import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-result.enum';
|
||||||
import { CblImportSummary } from 'src/app/_models/reading-list/cbl/cbl-import-summary';
|
import { CblImportSummary } from 'src/app/_models/reading-list/cbl/cbl-import-summary';
|
||||||
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
import { ReadingListService } from 'src/app/_services/reading-list.service';
|
||||||
|
import { TimelineStep } from '../../_components/step-tracker/step-tracker.component';
|
||||||
|
|
||||||
enum Step {
|
enum Step {
|
||||||
Import = 0,
|
Import = 0,
|
||||||
@ -35,59 +39,103 @@ export class ImportCblModalComponent {
|
|||||||
importSummaries: Array<CblImportSummary> = [];
|
importSummaries: Array<CblImportSummary> = [];
|
||||||
validateSummary: CblImportSummary | undefined;
|
validateSummary: CblImportSummary | undefined;
|
||||||
dryRunSummary: CblImportSummary | undefined;
|
dryRunSummary: CblImportSummary | undefined;
|
||||||
|
dryRunResults: Array<CblBookResult> = [];
|
||||||
|
finalizeSummary: CblImportSummary | undefined;
|
||||||
|
finalizeResults: Array<CblBookResult> = [];
|
||||||
|
|
||||||
steps = [
|
isLoading: boolean = false;
|
||||||
{title: 'Import CBL', index: Step.Import},
|
|
||||||
{title: 'Validate File', index: Step.Validate},
|
steps: Array<TimelineStep> = [
|
||||||
{title: 'Dry Run', index: Step.DryRun},
|
{title: 'Import CBL', index: Step.Import, active: true, icon: 'fa-solid fa-file-arrow-up'},
|
||||||
{title: 'Final Import', index: Step.Finalize},
|
{title: 'Validate File', index: Step.Validate, active: false, icon: 'fa-solid fa-spell-check'},
|
||||||
|
{title: 'Dry Run', index: Step.DryRun, active: false, icon: 'fa-solid fa-gears'},
|
||||||
|
{title: 'Final Import', index: Step.Finalize, active: false, icon: 'fa-solid fa-floppy-disk'},
|
||||||
];
|
];
|
||||||
currentStep = this.steps[0];
|
currentStepIndex = this.steps[0].index;
|
||||||
|
|
||||||
get Breakpoint() { return Breakpoint; }
|
get Breakpoint() { return Breakpoint; }
|
||||||
get Step() { return Step; }
|
get Step() { return Step; }
|
||||||
|
|
||||||
|
get NextButtonLabel() {
|
||||||
|
switch(this.currentStepIndex) {
|
||||||
|
case Step.DryRun:
|
||||||
|
return 'Import';
|
||||||
|
case Step.Finalize:
|
||||||
|
return 'Restart'
|
||||||
|
default:
|
||||||
|
return 'Next';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
|
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
|
||||||
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {}
|
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef,
|
||||||
|
private toastr: ToastrService) {}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.ngModal.close();
|
this.ngModal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
nextStep() {
|
nextStep() {
|
||||||
|
if (this.currentStepIndex === Step.Import && !this.isFileSelected()) return;
|
||||||
|
if (this.currentStepIndex === Step.Validate && this.validateSummary && this.validateSummary.results.length > 0) return;
|
||||||
|
|
||||||
if (this.currentStep.index >= Step.Finalize) return;
|
this.isLoading = true;
|
||||||
if (this.currentStep.index === Step.Import && !this.isFileSelected()) return;
|
switch (this.currentStepIndex) {
|
||||||
if (this.currentStep.index === Step.Validate && this.validateSummary && this.validateSummary.results.length > 0) return;
|
|
||||||
|
|
||||||
switch (this.currentStep.index) {
|
|
||||||
case Step.Import:
|
case Step.Import:
|
||||||
this.importFile();
|
this.importFile();
|
||||||
break;
|
break;
|
||||||
case Step.Validate:
|
case Step.Validate:
|
||||||
|
this.import(true);
|
||||||
break;
|
break;
|
||||||
case Step.DryRun:
|
case Step.DryRun:
|
||||||
|
this.import(false);
|
||||||
break;
|
break;
|
||||||
case Step.Finalize:
|
case Step.Finalize:
|
||||||
// Clear the models and allow user to do another import
|
// Clear the models and allow user to do another import
|
||||||
|
this.uploadForm.get('files')?.setValue(undefined);
|
||||||
|
this.currentStepIndex = Step.Import;
|
||||||
|
this.validateSummary = undefined;
|
||||||
|
this.dryRunSummary = undefined;
|
||||||
|
this.dryRunResults = [];
|
||||||
|
this.finalizeSummary = undefined;
|
||||||
|
this.finalizeResults = [];
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevStep() {
|
||||||
|
if (this.currentStepIndex === Step.Import) return;
|
||||||
|
this.currentStepIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
canMoveToNextStep() {
|
canMoveToNextStep() {
|
||||||
switch (this.currentStep.index) {
|
switch (this.currentStepIndex) {
|
||||||
case Step.Import:
|
case Step.Import:
|
||||||
return this.isFileSelected();
|
return this.isFileSelected();
|
||||||
case Step.Validate:
|
case Step.Validate:
|
||||||
return this.validateSummary && this.validateSummary.results.length > 0;
|
return this.validateSummary && this.validateSummary.results.length === 0;
|
||||||
case Step.DryRun:
|
case Step.DryRun:
|
||||||
return true;
|
return this.dryRunSummary?.success != CblImportResult.Fail;
|
||||||
case Step.Finalize:
|
case Step.Finalize:
|
||||||
return true;
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canMoveToPrevStep() {
|
||||||
|
switch (this.currentStepIndex) {
|
||||||
|
case Step.Import:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
isFileSelected() {
|
isFileSelected() {
|
||||||
const files = this.uploadForm.get('files')?.value;
|
const files = this.uploadForm.get('files')?.value;
|
||||||
if (files) return files.length > 0;
|
if (files) return files.length > 0;
|
||||||
@ -98,42 +146,42 @@ export class ImportCblModalComponent {
|
|||||||
const files = this.uploadForm.get('files')?.value;
|
const files = this.uploadForm.get('files')?.value;
|
||||||
if (!files) return;
|
if (!files) return;
|
||||||
|
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('cbl', files[0]);
|
formData.append('cbl', files[0]);
|
||||||
formData.append('dryRun', (this.currentStep.index !== Step.Finalize) + '');
|
this.readingListService.validateCbl(formData).subscribe(res => {
|
||||||
this.readingListService.importCbl(formData).subscribe(res => {
|
if (this.currentStepIndex === Step.Import) {
|
||||||
console.log('Result: ', res);
|
|
||||||
if (this.currentStep.index === Step.Import) {
|
|
||||||
this.validateSummary = res;
|
this.validateSummary = res;
|
||||||
}
|
}
|
||||||
if (this.currentStep.index === Step.DryRun) {
|
|
||||||
this.dryRunSummary = res;
|
|
||||||
}
|
|
||||||
this.importSummaries.push(res);
|
this.importSummaries.push(res);
|
||||||
this.currentStep.index++;
|
this.currentStepIndex++;
|
||||||
|
this.isLoading = false;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// onFileSelected(event: any) {
|
import(dryRun: boolean = false) {
|
||||||
// console.log('event: ', event);
|
const files = this.uploadForm.get('files')?.value;
|
||||||
// if (!(event.target as HTMLInputElement).files === null || (event.target as HTMLInputElement).files?.length === 0) return;
|
if (!files) return;
|
||||||
|
|
||||||
// const file = (event.target as HTMLInputElement).files![0];
|
const formData = new FormData();
|
||||||
|
formData.append('cbl', files[0]);
|
||||||
|
formData.append('dryRun', dryRun + '');
|
||||||
|
this.readingListService.importCbl(formData).subscribe(res => {
|
||||||
|
// Our step when calling is always one behind
|
||||||
|
if (dryRun) {
|
||||||
|
this.dryRunSummary = res;
|
||||||
|
this.dryRunResults = [...res.successfulInserts, ...res.results].sort((a, b) => a.order - b.order);
|
||||||
|
} else {
|
||||||
|
this.finalizeSummary = res;
|
||||||
|
this.finalizeResults = [...res.successfulInserts, ...res.results].sort((a, b) => a.order - b.order);
|
||||||
|
this.toastr.success('Reading List imported');
|
||||||
|
}
|
||||||
|
|
||||||
// if (file) {
|
this.isLoading = false;
|
||||||
|
this.currentStepIndex++;
|
||||||
// //this.fileName = file.name;
|
this.cdRef.markForCheck();
|
||||||
|
});
|
||||||
// const formData = new FormData();
|
}
|
||||||
|
|
||||||
// formData.append("cbl", file);
|
|
||||||
|
|
||||||
// this.readingListService.importCbl(formData).subscribe(res => {
|
|
||||||
// this.importSummaries.push(res);
|
|
||||||
// this.cdRef.markForCheck();
|
|
||||||
// });
|
|
||||||
// this.fileUpload.value = '';
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@ -2,31 +2,34 @@ import { Pipe, PipeTransform } from '@angular/core';
|
|||||||
import { CblBookResult } from 'src/app/_models/reading-list/cbl/cbl-book-result';
|
import { CblBookResult } from 'src/app/_models/reading-list/cbl/cbl-book-result';
|
||||||
import { CblImportReason } from 'src/app/_models/reading-list/cbl/cbl-import-reason.enum';
|
import { CblImportReason } from 'src/app/_models/reading-list/cbl/cbl-import-reason.enum';
|
||||||
|
|
||||||
|
const failIcon = '<i aria-hidden="true" class="reading-list-fail--item fa-solid fa-circle-xmark me-1"></i>';
|
||||||
|
const successIcon = '<i aria-hidden="true" class="reading-list-success--item fa-solid fa-circle-check me-1"></i>';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'cblConflictReason'
|
name: 'cblConflictReason'
|
||||||
})
|
})
|
||||||
export class CblConflictReasonPipe implements PipeTransform {
|
export class CblConflictReasonPipe implements PipeTransform {
|
||||||
|
|
||||||
transform(result: CblBookResult): string {
|
transform(result: CblBookResult): string {
|
||||||
if (result.reason === undefined)
|
|
||||||
return result.series + ' volume ' + result.volume + ' number ' + result.number + ' mapped successfully';
|
|
||||||
switch (result.reason) {
|
switch (result.reason) {
|
||||||
case CblImportReason.AllSeriesMissing:
|
case CblImportReason.AllSeriesMissing:
|
||||||
return 'Your account is missing access to all series in the list or Kavita does not have anything present in the list.';
|
return failIcon + 'Your account is missing access to all series in the list or Kavita does not have anything present in the list.';
|
||||||
case CblImportReason.ChapterMissing:
|
case CblImportReason.ChapterMissing:
|
||||||
return 'Chapter ' + result.number + ' is missing from Kavita. This item will be skipped.';
|
return failIcon + result.series + ': ' + 'Chapter ' + result.number + ' is missing from Kavita. This item will be skipped.';
|
||||||
case CblImportReason.EmptyFile:
|
case CblImportReason.EmptyFile:
|
||||||
return 'The Cbl file is empty, nothing to be done.';
|
return failIcon + 'The cbl file is empty, nothing to be done.';
|
||||||
case CblImportReason.NameConflict:
|
case CblImportReason.NameConflict:
|
||||||
return 'A reading list already exists on your account that matches the Cbl file.';
|
return failIcon + 'A reading list already exists on your account that matches the cbl file.';
|
||||||
case CblImportReason.SeriesCollision:
|
case CblImportReason.SeriesCollision:
|
||||||
return 'The series, ' + result.series + ', collides with another series of the same name in another library.';
|
return failIcon + 'The series, ' + result.series + ', collides with another series of the same name in another library.';
|
||||||
case CblImportReason.SeriesMissing:
|
case CblImportReason.SeriesMissing:
|
||||||
return 'The series, ' + result.series + ', is missing from Kavita or your account does not have permission. All items with this series will be skipped from import.';
|
return failIcon + 'The series, ' + result.series + ', is missing from Kavita or your account does not have permission. All items with this series will be skipped from import.';
|
||||||
case CblImportReason.VolumeMissing:
|
case CblImportReason.VolumeMissing:
|
||||||
return 'Volume ' + result.volume + ' is missing from Kavita. All items with this volume number will be skipped.';
|
return failIcon + result.series + ': ' + 'Volume ' + result.volume + ' is missing from Kavita. All items with this volume number will be skipped.';
|
||||||
case CblImportReason.AllChapterMissing:
|
case CblImportReason.AllChapterMissing:
|
||||||
return 'All chapters cannot be matched to Chapters in Kavita.';
|
return failIcon + 'All chapters cannot be matched to Chapters in Kavita.';
|
||||||
|
case CblImportReason.Success:
|
||||||
|
return successIcon + result.series + ' volume ' + result.volume + ' number ' + result.number + ' mapped successfully';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
UI/Web/src/app/reading-list/_pipes/cbl-import-result.pipe.ts
Normal file
19
UI/Web/src/app/reading-list/_pipes/cbl-import-result.pipe.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-result.enum';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'cblImportResult'
|
||||||
|
})
|
||||||
|
export class CblImportResultPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(result: CblImportResult): string {
|
||||||
|
switch (result) {
|
||||||
|
case CblImportResult.Success:
|
||||||
|
return 'Success';
|
||||||
|
case CblImportResult.Partial:
|
||||||
|
return 'Partial Success';
|
||||||
|
case CblImportResult.Fail:
|
||||||
|
return 'Failure';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,8 @@ import { ReadingListsComponent } from './_components/reading-lists/reading-lists
|
|||||||
import { ImportCblModalComponent } from './_modals/import-cbl-modal/import-cbl-modal.component';
|
import { ImportCblModalComponent } from './_modals/import-cbl-modal/import-cbl-modal.component';
|
||||||
import { FileUploadModule } from '@iplab/ngx-file-upload';
|
import { FileUploadModule } from '@iplab/ngx-file-upload';
|
||||||
import { CblConflictReasonPipe } from './_pipes/cbl-conflict-reason.pipe';
|
import { CblConflictReasonPipe } from './_pipes/cbl-conflict-reason.pipe';
|
||||||
|
import { StepTrackerComponent } from './_components/step-tracker/step-tracker.component';
|
||||||
|
import { CblImportResultPipe } from './_pipes/cbl-import-result.pipe';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -28,6 +29,8 @@ import { CblConflictReasonPipe } from './_pipes/cbl-conflict-reason.pipe';
|
|||||||
ReadingListItemComponent,
|
ReadingListItemComponent,
|
||||||
ImportCblModalComponent,
|
ImportCblModalComponent,
|
||||||
CblConflictReasonPipe,
|
CblConflictReasonPipe,
|
||||||
|
StepTrackerComponent,
|
||||||
|
CblImportResultPipe,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -10,7 +10,11 @@
|
|||||||
<app-side-nav-item icon="fa-home" title="Home" link="/libraries/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-home" title="Home" link="/libraries/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-star" title="Want To Read" link="/want-to-read/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-star" title="Want To Read" link="/want-to-read/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-list" title="Collections" link="/collections/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-list" title="Collections" link="/collections/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-list-ol" title="Reading Lists" link="/lists/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-list-ol" title="Reading Lists" link="/lists/">
|
||||||
|
<ng-container actions>
|
||||||
|
<app-card-actionables [actions]="readingListActions" labelBy="Reading Lists" iconClass="fa-ellipsis-v" (actionHandler)="importCbl()"></app-card-actionables>
|
||||||
|
</ng-container>
|
||||||
|
</app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-bookmark" title="Bookmarks" link="/bookmarks/"></app-side-nav-item>
|
<app-side-nav-item icon="fa-bookmark" title="Bookmarks" link="/bookmarks/"></app-side-nav-item>
|
||||||
<app-side-nav-item icon="fa-regular fa-rectangle-list" title="All Series" link="/all-series/" *ngIf="libraries.length > 0"></app-side-nav-item>
|
<app-side-nav-item icon="fa-regular fa-rectangle-list" title="All Series" link="/all-series/" *ngIf="libraries.length > 0"></app-side-nav-item>
|
||||||
<div class="mb-2 mt-3 ms-2 me-2" *ngIf="libraries.length > 10 && (navService?.sideNavCollapsed$ | async) === false">
|
<div class="mb-2 mt-3 ms-2 me-2" *ngIf="libraries.length > 10 && (navService?.sideNavCollapsed$ | async) === false">
|
||||||
|
@ -3,6 +3,8 @@ import { NavigationEnd, Router } from '@angular/router';
|
|||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { filter, map, shareReplay, take, takeUntil } from 'rxjs/operators';
|
import { filter, map, shareReplay, take, takeUntil } from 'rxjs/operators';
|
||||||
|
import { ImportCblModalComponent } from 'src/app/reading-list/_modals/import-cbl-modal/import-cbl-modal.component';
|
||||||
|
import { ReadingList } from 'src/app/_models/reading-list';
|
||||||
import { ImageService } from 'src/app/_services/image.service';
|
import { ImageService } from 'src/app/_services/image.service';
|
||||||
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||||
import { Breakpoint, UtilityService } from '../../../shared/_services/utility.service';
|
import { Breakpoint, UtilityService } from '../../../shared/_services/utility.service';
|
||||||
@ -23,6 +25,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
libraries: Library[] = [];
|
libraries: Library[] = [];
|
||||||
actions: ActionItem<Library>[] = [];
|
actions: ActionItem<Library>[] = [];
|
||||||
|
readingListActions = [{action: Action.Import, title: 'Import CBL', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
|
||||||
|
|
||||||
filterQuery: string = '';
|
filterQuery: string = '';
|
||||||
filterLibrary = (library: Library) => {
|
filterLibrary = (library: Library) => {
|
||||||
@ -36,7 +39,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||||||
public utilityService: UtilityService, private messageHub: MessageHubService,
|
public utilityService: UtilityService, private messageHub: MessageHubService,
|
||||||
private actionFactoryService: ActionFactoryService, private actionService: ActionService,
|
private actionFactoryService: ActionFactoryService, private actionService: ActionService,
|
||||||
public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef,
|
public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef,
|
||||||
private modalService: NgbModal, private imageService: ImageService) {
|
private ngbModal: NgbModal, private imageService: ImageService) {
|
||||||
|
|
||||||
this.router.events.pipe(
|
this.router.events.pipe(
|
||||||
filter(event => event instanceof NavigationEnd),
|
filter(event => event instanceof NavigationEnd),
|
||||||
@ -97,6 +100,9 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
importCbl() {
|
||||||
|
const ref = this.ngbModal.open(ImportCblModalComponent, {size: 'xl'});
|
||||||
|
}
|
||||||
|
|
||||||
performAction(action: ActionItem<Library>, library: Library) {
|
performAction(action: ActionItem<Library>, library: Library) {
|
||||||
if (typeof action.callback === 'function') {
|
if (typeof action.callback === 'function') {
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "es2020",
|
"module": "ES2022",
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2022",
|
"ES2022",
|
||||||
|
263
openapi.json
263
openapi.json
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.1.2"
|
"version": "0.7.1.4"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@ -1024,6 +1024,185 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/Cbl/validate": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Cbl"
|
||||||
|
],
|
||||||
|
"summary": "The first step in a cbl import. This validates the cbl file that if an import occured, would it be successful.\r\nIf this returns errors, the cbl will always be rejected by Kavita.",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"multipart/form-data": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ContentType": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ContentDisposition": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Headers": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Length": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"FileName": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"encoding": {
|
||||||
|
"ContentType": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"ContentDisposition": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"Headers": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"Length": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"FileName": {
|
||||||
|
"style": "form"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CblImportSummaryDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CblImportSummaryDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"text/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CblImportSummaryDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/Cbl/import": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Cbl"
|
||||||
|
],
|
||||||
|
"summary": "Performs the actual import (assuming dryRun = false)",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"multipart/form-data": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ContentType": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ContentDisposition": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Headers": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Length": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"FileName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"dryRun": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"encoding": {
|
||||||
|
"ContentType": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"ContentDisposition": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"Headers": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"Length": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"FileName": {
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
"dryRun": {
|
||||||
|
"style": "form"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CblImportSummaryDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CblImportSummaryDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"text/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CblImportSummaryDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/Collection": {
|
"/api/Collection": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -9910,6 +10089,84 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"CblBookResult": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"order": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Order in the CBL",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"series": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"$ref": "#/components/schemas/CblImportReason"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"CblImportReason": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"CblImportResult": {
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"CblImportSummaryDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cblName": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/CblBookResult"
|
||||||
|
},
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"$ref": "#/components/schemas/CblImportResult"
|
||||||
|
},
|
||||||
|
"successfulInserts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/CblBookResult"
|
||||||
|
},
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Represents the summary from the Import of a given CBL"
|
||||||
|
},
|
||||||
"Chapter": {
|
"Chapter": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -14973,6 +15230,10 @@
|
|||||||
"name": "Account",
|
"name": "Account",
|
||||||
"description": "All Account matters"
|
"description": "All Account matters"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Cbl",
|
||||||
|
"description": "Responsible for the CBL import flow"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Collection",
|
"name": "Collection",
|
||||||
"description": "APIs for Collections"
|
"description": "APIs for Collections"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user