diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index 7d6a500e..843b10fc 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -120,6 +120,26 @@ internal static string DrawingRelationship(int sheetIndex) internal static string DrawingRelationship(int sheetIndex, string drawingFileName) => $""""""; + internal static string Table(int id, string name, string reference, bool hasHeaderRow, IReadOnlyList columnNames, OpenXmlTableOptions options) + { + static string Flag(bool value) => value ? "1" : "0"; + + var sb = new StringBuilder(); + sb.Append(""""""); + sb.Append($""""""); + if (hasHeaderRow && options.ShowFilterButton) + sb.Append($""""""); + + sb.Append($""""""); + for (var i = 0; i < columnNames.Count; i++) + sb.Append($""""""); + sb.Append(""); + + sb.Append($""""""); + sb.Append("
"); + return sb.ToString(); + } + internal static string TableRelationship(int sheetIndex) => $""""""; diff --git a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs index d5f14f9e..4218d78e 100644 --- a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs @@ -37,6 +37,9 @@ internal static string PaneSelection(string pane, string? activeCell, string? sq "/>"); internal static string StartRow(int rowIndex) => $""; + internal static string StartRow(int rowIndex, double? height) => height is { } ht + ? $"" + : StartRow(rowIndex); internal const string EndRow = ""; internal const string StartCols = ""; @@ -69,4 +72,5 @@ internal static string Cell(string cellReference, string? cellType, string style internal static string Autofilter(string dimensionRef) => $""; internal static string Drawing(int sheetIndex) => $""; + internal static string TableParts(int sheetIndex) => $""; } diff --git a/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs b/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs index a5007bba..91541c2e 100644 --- a/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs +++ b/src/MiniExcel.OpenXml/OpenXmlConfiguration.cs @@ -9,6 +9,12 @@ public class OpenXmlConfiguration : MiniExcelBaseConfiguration public bool FillMergedCells { get; set; } public TableStyles TableStyles { get; set; } = TableStyles.Default; public bool AutoFilter { get; set; } = true; + + /// + /// When set, each sheet's data is written as an Excel table in this style. The sheet-level is then left out. + /// + public OpenXmlTableOptions? Table { get; set; } + public bool RightToLeft { get; set; } = false; public int FreezeRowCount { get; set; } = 1; public int FreezeColumnCount { get; set; } = 0; diff --git a/src/MiniExcel.OpenXml/OpenXmlTableOptions.cs b/src/MiniExcel.OpenXml/OpenXmlTableOptions.cs new file mode 100644 index 00000000..abf74871 --- /dev/null +++ b/src/MiniExcel.OpenXml/OpenXmlTableOptions.cs @@ -0,0 +1,47 @@ +namespace MiniExcelLib.OpenXml; + +/// +/// Exports each sheet's data as an Excel table. Set it on . +/// +/// +/// The table covers the header row and the data, so columns must be adjacent. Combine it with +/// to let the table style alone format the cells. +/// +public class OpenXmlTableOptions +{ + /// + /// The table name. The first sheet's table gets this name and later sheets append their sheet number. + /// Defaults to Table1, Table2 and so on. + /// + public string? Name { get; set; } + + /// + /// A built-in table style such as TableStyleMedium2 or TableStyleLight9. + /// + public string Style { get; set; } = "TableStyleMedium2"; + + /// + /// Shows filter buttons in the header row. + /// + public bool ShowFilterButton { get; set; } = true; + + /// + /// Shades alternate rows. + /// + public bool ShowRowStripes { get; set; } = true; + + /// + /// Shades alternate columns. + /// + public bool ShowColumnStripes { get; set; } + + /// + /// Highlights the first column. + /// + public bool ShowFirstColumn { get; set; } + + /// + /// Highlights the last column. + /// + public bool ShowLastColumn { get; set; } +} diff --git a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs index 7afce315..9115cd11 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs @@ -1,6 +1,6 @@ namespace MiniExcelLib.OpenXml.Styles.Builder; -internal partial class MinimalSheetStyleBuilder(SheetStyleBuilderContext context) : SheetStyleBuilderBase(context) +internal partial class MinimalSheetStyleBuilder(SheetStyleBuilderContext context, OpenXmlStyleOptions? styleOptions = null) : SheetStyleBuilderBase(context) { private static readonly SheetStyleElementInfos GenerateElementInfos = new() { @@ -88,6 +88,8 @@ protected override async Task GenerateCellXfAsync() * * */ + // The second xf is the header's, which takes the alignment of a header style but not its fill + var headerStyle = styleOptions?.HeaderStyle; for (int i = 0; i < 3; i++) { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); @@ -96,6 +98,28 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteAttributeStringAsync(null, "fillId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); + if (i == 1 && headerStyle is not null) + { + /* + * + */ + await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, headerStyle.HorizontalAlignment switch + { + HorizontalCellAlignment.Center => "center", + HorizontalCellAlignment.Right => "right", + _ => "general" + }).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "vertical", null, headerStyle.VerticalAlignment switch + { + VerticalCellAlignment.Top => "top", + VerticalCellAlignment.Center => "center", + _ => "bottom" + }).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, headerStyle.WrapText ? "1" : "0").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + } await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } diff --git a/src/MiniExcel.OpenXml/Styles/OpenXmlHeaderStyle.cs b/src/MiniExcel.OpenXml/Styles/OpenXmlHeaderStyle.cs index 95ea3f01..53478e14 100644 --- a/src/MiniExcel.OpenXml/Styles/OpenXmlHeaderStyle.cs +++ b/src/MiniExcel.OpenXml/Styles/OpenXmlHeaderStyle.cs @@ -23,4 +23,9 @@ public class OpenXmlHeaderStyle /// Vertical alignment /// public VerticalCellAlignment VerticalAlignment { get; set; } = VerticalCellAlignment.Bottom; + + /// + /// The height of the header row in points, or null for the default height + /// + public double? RowHeight { get; set; } } \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs index 635beb12..b0de1bd5 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs @@ -183,7 +183,7 @@ private async Task CopySheetStylesAndGetBuilderUtil oldStylesContext = new SheetStyleBuilderContext(_zipContentsMap, copiedArchive, Utf8WithBom); SheetStyleBuilderBase builder = _configuration.TableStyles switch { - TableStyles.None => new MinimalSheetStyleBuilder(oldStylesContext), + TableStyles.None => new MinimalSheetStyleBuilder(oldStylesContext, _configuration.StyleOptions), TableStyles.Default => new DefaultSheetStyleBuilder(oldStylesContext, _configuration.StyleOptions), _ => throw new InvalidEnumArgumentException(nameof(_configuration.TableStyles), (int)_configuration.TableStyles, typeof(TableStyles)) }; diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs index 29e3aa00..b5aa2837 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs @@ -404,6 +404,39 @@ private string GetDrawingXml(int sheetIndex) return drawing.ToString(); } + private static readonly Regex TableNameRegex = new(@"^[A-Za-z_\][A-Za-z0-9_.]*$"); + + private string? GetTableXml(OpenXmlTableOptions options, List mappings, int maxRowIndex, int maxColumnIndex) + { + // Null mappings are gaps between columns placed by index, which would leave a table column without a header + if (mappings.Any(m => m is null)) + throw new NotSupportedException("A table needs adjacent columns, so it can't be used with column indexes that leave gaps"); + + if (maxColumnIndex == 0 || (!_printHeader && maxRowIndex == 0)) + return null; + + List columnNames = _printHeader + ? [.. mappings.Where(m => !m!.ExcelIgnoreColumn).Select(m => m!.ExcelColumnName ?? "")] + : [.. Enumerable.Range(1, maxColumnIndex).Select(i => $"Column{i}")]; + + if (columnNames.Any(string.IsNullOrEmpty)) + throw new InvalidOperationException("Table column names can't be empty"); + if (columnNames.Distinct(StringComparer.OrdinalIgnoreCase).Count() != columnNames.Count) + throw new InvalidOperationException("Table column names must be unique"); + + var name = options.Name is null ? $"Table{_currentSheetIndex}" + : _currentSheetIndex == 1 ? options.Name + : $"{options.Name}{_currentSheetIndex}"; + if (!TableNameRegex.IsMatch(name)) + throw new ArgumentException($"'{name}' is not a valid table name"); + + // A table needs at least one data row, so a header without data takes in the empty row below it + var lastRow = _printHeader ? Math.Max(maxRowIndex, 2) : maxRowIndex; + var reference = $"A1:{CellReferenceConverter.GetCellFromCoordinates(maxColumnIndex, lastRow)}"; + + return ExcelXml.Table(_currentSheetIndex, name, reference, _printHeader, columnNames, options); + } + private (string WorkbookContents, string WorkbookRelsContents, Dictionary SheetRelsContents) GenerateWorkbookItems() { var workbookContents = new StringBuilder(); @@ -419,7 +452,10 @@ private string GetDrawingXml(int sheetIndex) workbookRelsContents.AppendLine(ExcelXml.WorksheetRelationship(sheetDto)); //TODO: support multiple drawing - sheetsRelsDict.Add(sheetDto.SheetIdx, ExcelXml.DrawingRelationship(sheetId)); + var sheetRels = ExcelXml.DrawingRelationship(sheetId); + if (_tables.ContainsKey(sheetDto.SheetIdx)) + sheetRels += ExcelXml.TableRelationship(sheetDto.SheetIdx); + sheetsRelsDict.Add(sheetDto.SheetIdx, sheetRels); } return (workbookContents.ToString(), workbookRelsContents.ToString(), sheetsRelsDict); diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs index 2c7aeaff..a2ae412b 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs @@ -14,6 +14,9 @@ internal sealed partial class OpenXmlWriter : IMiniExcelWriter private readonly OpenXmlConfiguration _configuration; private readonly List _sheets = []; private readonly List _files = []; + + // Table parts by sheet index, written once all sheets are done + private readonly Dictionary _tables = []; private readonly string _sheetName; private readonly bool _printHeader; @@ -86,6 +89,7 @@ public async Task SaveAsAsync(IProgress? progress = null, Cancellati await GenerateSharedStringsAsync(cancellationToken).ConfigureAwait(false); await GenerateDrawingRelsAsync(cancellationToken).ConfigureAwait(false); await GenerateDrawingsAsync(cancellationToken).ConfigureAwait(false); + await GenerateTablesAsync(cancellationToken).ConfigureAwait(false); await GenerateWorkbookAsync(false, cancellationToken).ConfigureAwait(false); await GenerateContentTypesAsync(cancellationToken).ConfigureAwait(false); @@ -97,6 +101,8 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? { if (!_configuration.FastMode) throw new InvalidOperationException("Insert requires fast mode to be enabled"); + if (_configuration.Table is not null) + throw new NotSupportedException("Tables can't be added when inserting a sheet into an existing workbook"); #if NET10_0_OR_GREATER await using var disposableArchive = _archive.ConfigureAwait(false); @@ -290,7 +296,7 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va var currentRowIndex = 0; if (_printHeader) { - await PrintHeaderAsync(writer, mappings!, cancellationToken).ConfigureAwait(false); + await PrintHeaderAsync(writer, mappings!, _configuration.StyleOptions.HeaderStyle?.RowHeight, cancellationToken).ConfigureAwait(false); currentRowIndex++; } @@ -331,12 +337,19 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va await writer.WriteAsync(WorksheetXml.EndSheetData, cancellationToken).ConfigureAwait(false); - if (_configuration.AutoFilter) + // A sheet can't have its own autofilter on top of a table's + var table = _configuration.Table is { } tableOptions ? GetTableXml(tableOptions, mappings, maxRowIndex, maxColumnIndex) : null; + if (_configuration.AutoFilter && _configuration.Table is null) { await writer.WriteAsync(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex)), cancellationToken).ConfigureAwait(false); } await writer.WriteAsync(WorksheetXml.Drawing(_currentSheetIndex), cancellationToken).ConfigureAwait(false); + if (table is not null) + { + _tables[_currentSheetIndex] = table; + await writer.WriteAsync(WorksheetXml.TableParts(_currentSheetIndex), cancellationToken).ConfigureAwait(false); + } await writer.WriteAsync(WorksheetXml.EndWorksheet, cancellationToken).ConfigureAwait(false); if (_archive.Mode == ZipArchiveMode.Update && dimensionPlaceholderPostition != 0) @@ -414,10 +427,10 @@ private static async Task WriteColumnsWidthsAsync(MiniExcelStreamWriter writer, } [CreateSyncVersion] - private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List mappings, CancellationToken cancellationToken = default) + private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List mappings, double? rowHeight, CancellationToken cancellationToken = default) { const int yIndex = 1; - await writer.WriteAsync(WorksheetXml.StartRow(yIndex), cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(WorksheetXml.StartRow(yIndex, rowHeight), cancellationToken).ConfigureAwait(false); var xIndex = 1; foreach (var map in mappings) @@ -511,7 +524,7 @@ private async Task GetSheetStyleBuilderAsync(CancellationTok { SheetStyleBuilderBase builder = _configuration.TableStyles switch { - TableStyles.None => new MinimalSheetStyleBuilder(_sheetStyleBuilderContext), + TableStyles.None => new MinimalSheetStyleBuilder(_sheetStyleBuilderContext, _configuration.StyleOptions), TableStyles.Default => new DefaultSheetStyleBuilder(_sheetStyleBuilderContext, _configuration.StyleOptions), _ => throw new InvalidEnumArgumentException(nameof(_configuration.TableStyles), (int)_configuration.TableStyles, typeof(TableStyles)) }; @@ -564,6 +577,16 @@ await CreateZipEntryAsync( cancellationToken).ConfigureAwait(false); } + [CreateSyncVersion] + private async Task GenerateTablesAsync(CancellationToken cancellationToken) + { + foreach (var (sheetIndex, table) in _tables) + { + cancellationToken.ThrowIfCancellationRequested(); + await CreateZipEntryAsync(ExcelFileNames.Table(sheetIndex), ExcelContentTypes.Table, table, cancellationToken).ConfigureAwait(false); + } + } + [CreateSyncVersion] private async Task GenerateWorkbookAsync(bool removeOriginalEntry = false, CancellationToken cancellationToken = default) { diff --git a/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs new file mode 100644 index 00000000..541528d8 --- /dev/null +++ b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportAsyncTests.cs @@ -0,0 +1,160 @@ +using ClosedXML.Excel; +using MiniExcelLib.Core.Enums; +using MiniExcelLib.OpenXml.Styles; + +namespace MiniExcelLib.OpenXml.Tests.Tables; + +public class MiniExcelOpenXmlTableExportAsyncTests +{ + private readonly OpenXmlExporter _exporter = MiniExcelV2.Exporters.GetOpenXmlExporter(); + private readonly OpenXmlImporter _importer = MiniExcelV2.Importers.GetOpenXmlImporter(); + + private class Weld + { + public int Id { get; set; } + public string? Name { get; set; } + public double Depth { get; set; } + } + + private class IndexedWeld + { + [MiniExcelColumnIndex(0)] + public int Id { get; set; } + + [MiniExcelColumnIndex(2)] + public double Depth { get; set; } + } + + private static readonly Weld[] Welds = + [ + new() { Id = 1, Name = "first", Depth = 0.1 }, + new() { Id = 2, Name = "second", Depth = 0.2 } + ]; + + private async Task ExportAsync(object value, OpenXmlConfiguration configuration, bool printHeader = true) + { + var stream = new MemoryStream(); + await _exporter.ExportAsync(stream, value, printHeader, configuration: configuration); + stream.Position = 0; + return stream; + } + + [Fact] + public async Task DataIsWrittenAsATableInTheChosenStyleAsync() + { + var configuration = new OpenXmlConfiguration + { + TableStyles = TableStyles.None, + Table = new OpenXmlTableOptions { Name = "Welds", Style = "TableStyleMedium15", ShowFilterButton = false } + }; + + using var stream = await ExportAsync(Welds, configuration); + using var workbook = new XLWorkbook(stream); + var table = workbook.Worksheet(1).Tables.Single(); + + Assert.Equal("Welds", table.Name); + Assert.Equal("A1:C3", table.RangeAddress.ToString()); + Assert.Equal(["Id", "Name", "Depth"], table.Fields.Select(f => f.Name)); + Assert.Equal(XLTableTheme.TableStyleMedium15, table.Theme); + Assert.False(table.ShowAutoFilter); + Assert.True(table.ShowRowStripes); + Assert.Null(workbook.Worksheet(1).AutoFilter.Range); + + stream.Position = 0; + var rows = await _importer.QueryTableAsync(stream, tableName: "Welds").ToListAsync(); + Assert.Equal(2, rows.Count); + Assert.Equal("second", (string)rows[1].Name); + } + + [Fact] + public async Task HeaderStyleAppliesWithoutCellStylesAsync() + { + var configuration = new OpenXmlConfiguration + { + TableStyles = TableStyles.None, + StyleOptions = new OpenXmlStyleOptions + { + HeaderStyle = new OpenXmlHeaderStyle + { + WrapText = true, + HorizontalAlignment = HorizontalCellAlignment.Center, + VerticalAlignment = VerticalCellAlignment.Center, + RowHeight = 45, + } + }, + Table = new OpenXmlTableOptions { Style = "TableStyleMedium15" } + }; + + using var stream = await ExportAsync(Welds, configuration); + using var workbook = new XLWorkbook(stream); + var sheet = workbook.Worksheet(1); + var header = sheet.Cell("A1").Style; + + Assert.True(header.Alignment.WrapText); + Assert.Equal(XLAlignmentHorizontalValues.Center, header.Alignment.Horizontal); + Assert.Equal(XLAlignmentVerticalValues.Center, header.Alignment.Vertical); + Assert.Equal(XLFillPatternValues.None, header.Fill.PatternType); + Assert.Equal(45, sheet.Row(1).Height); + Assert.False(sheet.Cell("A2").Style.Alignment.WrapText); + } + + [Fact] + public async Task EachSheetGetsItsOwnTableAsync() + { + var sheets = new Dictionary { ["First"] = Welds, ["Second"] = Welds.Take(1) }; + + using var stream = await ExportAsync(sheets, new OpenXmlConfiguration { Table = new OpenXmlTableOptions() }); + using var workbook = new XLWorkbook(stream); + + Assert.Equal("Table1", workbook.Worksheet("First").Tables.Single().Name); + Assert.Equal("A1:C3", workbook.Worksheet("First").Tables.Single().RangeAddress.ToString()); + Assert.Equal("Table2", workbook.Worksheet("Second").Tables.Single().Name); + Assert.Equal("A1:C2", workbook.Worksheet("Second").Tables.Single().RangeAddress.ToString()); + Assert.True(workbook.Worksheet("First").Tables.Single().ShowAutoFilter); + } + + [Fact] + public async Task HeaderWithoutDataStillMakesATableAsync() + { + using var stream = await ExportAsync(Array.Empty(), new OpenXmlConfiguration { Table = new OpenXmlTableOptions() }); + using var workbook = new XLWorkbook(stream); + + Assert.Equal("A1:C2", workbook.Worksheet(1).Tables.Single().RangeAddress.ToString()); + } + + [Fact] + public async Task TableWithoutHeaderRowAsync() + { + using var stream = await ExportAsync(Welds, new OpenXmlConfiguration { Table = new OpenXmlTableOptions() }, printHeader: false); + using var workbook = new XLWorkbook(stream); + var table = workbook.Worksheet(1).Tables.Single(); + + Assert.False(table.ShowHeaderRow); + Assert.Equal("A1:C2", table.RangeAddress.ToString()); + } + + [Fact] + public async Task ColumnGapsAreRejectedAsync() + { + IndexedWeld[] rows = [new() { Id = 1, Depth = 0.1 }]; + + await Assert.ThrowsAsync(() => ExportAsync(rows, new OpenXmlConfiguration { Table = new OpenXmlTableOptions() })); + } + + [Fact] + public async Task InvalidTableNameIsRejectedAsync() + { + var configuration = new OpenXmlConfiguration { Table = new OpenXmlTableOptions { Name = "My Welds" } }; + + await Assert.ThrowsAsync(() => ExportAsync(Welds, configuration)); + } + + [Fact] + public async Task InsertingASheetWithATableIsRejectedAsync() + { + using var stream = await ExportAsync(Welds, new OpenXmlConfiguration()); + var configuration = new OpenXmlConfiguration { FastMode = true, Table = new OpenXmlTableOptions() }; + + await Assert.ThrowsAsync(() => _exporter.InsertSheetAsync(stream, Welds, "Second", configuration: configuration)); + } +} diff --git a/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportTests.cs b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportTests.cs new file mode 100644 index 00000000..ac08956b --- /dev/null +++ b/tests/MiniExcel.OpenXml.Tests/Tables/MiniExcelOpenXmlTableExportTests.cs @@ -0,0 +1,25 @@ +using ClosedXML.Excel; + +namespace MiniExcelLib.OpenXml.Tests.Tables; + +public class MiniExcelOpenXmlTableExportTests +{ + private readonly OpenXmlExporter _exporter = MiniExcelV2.Exporters.GetOpenXmlExporter(); + + [Fact] + public void DataIsWrittenAsATable() + { + var rows = new[] { new { Id = 1, Name = "first" }, new { Id = 2, Name = "second" } }; + var configuration = new OpenXmlConfiguration { Table = new OpenXmlTableOptions { Style = "TableStyleLight9" } }; + + var stream = new MemoryStream(); + _exporter.Export(stream, rows, configuration: configuration); + stream.Position = 0; + + using var workbook = new XLWorkbook(stream); + var table = workbook.Worksheet(1).Tables.Single(); + Assert.Equal("Table1", table.Name); + Assert.Equal("A1:B3", table.RangeAddress.ToString()); + Assert.Equal(XLTableTheme.TableStyleLight9, table.Theme); + } +}