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);
+ }
+}