From c1fa0b0e72a46b358b61ee28204469ed8afea611 Mon Sep 17 00:00:00 2001 From: Felix-CodingClimber Date: Wed, 27 Mar 2024 05:20:20 +0100 Subject: [PATCH] Started adding StringDiff. Added ReadOnlyCrudService. Added InfoDialog. --- src/DotNetElements.Core/Core/Repository.cs | 2 +- .../DiffBuilder/InlineDiffBuilder.cs | 82 +++++ .../StringDiff/DiffBuilder/Model/DiffPiece.cs | 76 +++++ .../DiffBuilder/Model/InlineDiffModel.cs | 10 + .../DiffBuilder/Model/SideBySideDiffModel.cs | 16 + .../DiffBuilder/SideBySideDiffBuilder.cs | 123 ++++++++ .../Internal/Chunkers/DelimiterChunker.cs | 70 +++++ .../Internal/Chunkers/LineChunker.cs | 11 + .../Internal/Chunkers/WordChunker.cs | 8 + .../Core/StringDiff/Internal/Differ.cs | 294 ++++++++++++++++++ .../Core/StringDiff/Internal/IChunker.cs | 12 + .../StringDiff/Internal/Models/DiffBlock.cs | 10 + .../StringDiff/Internal/Models/DiffResult.cs | 9 + .../Internal/Models/EditLengthResult.cs | 12 + .../Internal/Models/ModificationData.cs | 17 + .../Core/ThrowHelper/ThrowHelper.cs | 58 ---- .../Core/ThrowHelper/ThrowIf.cs | 68 ++++ src/DotNetElements.Web.Blazor/CrudService.cs | 113 +------ .../CrudServiceBase.cs | 71 +++++ .../CrudTableActionsButton.razor | 13 + .../CrudTableActionsCell.razor | 24 +- .../DialogeServiceExtensions.cs | 61 +++- .../DiffViewer/DiffViewer.razor | 45 +++ ...ingLinear.razor => DneLoadingLinear.razor} | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 50 +++ .../InfoDialog.razor | 13 + .../InfoDialog.razor.cs | 15 + .../ReadOnlyCrudService.cs | 69 ++++ 28 files changed, 1163 insertions(+), 191 deletions(-) create mode 100644 src/DotNetElements.Core/Core/StringDiff/DiffBuilder/InlineDiffBuilder.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/DiffPiece.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/InlineDiffModel.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/SideBySideDiffModel.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/DiffBuilder/SideBySideDiffBuilder.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/DelimiterChunker.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/LineChunker.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/WordChunker.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/Internal/Differ.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/Internal/IChunker.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/Internal/Models/DiffBlock.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/Internal/Models/DiffResult.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/Internal/Models/EditLengthResult.cs create mode 100644 src/DotNetElements.Core/Core/StringDiff/Internal/Models/ModificationData.cs delete mode 100644 src/DotNetElements.Core/Core/ThrowHelper/ThrowHelper.cs create mode 100644 src/DotNetElements.Core/Core/ThrowHelper/ThrowIf.cs create mode 100644 src/DotNetElements.Web.Blazor/CrudServiceBase.cs create mode 100644 src/DotNetElements.Web.Blazor/CrudTableActionsButton.razor create mode 100644 src/DotNetElements.Web.Blazor/DiffViewer/DiffViewer.razor rename src/DotNetElements.Web.Blazor/{CardLoadingLinear.razor => DneLoadingLinear.razor} (85%) create mode 100644 src/DotNetElements.Web.Blazor/InfoDialog.razor create mode 100644 src/DotNetElements.Web.Blazor/InfoDialog.razor.cs create mode 100644 src/DotNetElements.Web.Blazor/ReadOnlyCrudService.cs diff --git a/src/DotNetElements.Core/Core/Repository.cs b/src/DotNetElements.Core/Core/Repository.cs index 97583e1..d872a39 100644 --- a/src/DotNetElements.Core/Core/Repository.cs +++ b/src/DotNetElements.Core/Core/Repository.cs @@ -100,7 +100,7 @@ public virtual async Task> CreateOrUpdateAsync(TKey id, public virtual async Task> UpdateAsync(TKey id, TFrom from) where TFrom : notnull { - ThrowHelper.ThrowIfDefault(id); + ThrowIf.Default(id); IQueryable query = LoadRelatedEntitiesOnUpdate(Entities); diff --git a/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/InlineDiffBuilder.cs b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/InlineDiffBuilder.cs new file mode 100644 index 0000000..374d668 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/InlineDiffBuilder.cs @@ -0,0 +1,82 @@ +namespace DotNetElements.Core.StringDiff; + +public class InlineDiffBuilder +{ + private readonly bool ignoreWhiteSpace; + private readonly bool ignoreCase; + + private readonly LineChunker chunker; + + /// + /// Creates a new instance of a + /// + /// if ignore the white space; otherwise, . + /// if case-insensitive; otherwise, . + public InlineDiffBuilder(bool ignoreWhiteSpace = true, bool ignoreCase = false) + { + this.ignoreWhiteSpace = ignoreWhiteSpace; + this.ignoreCase = ignoreCase; + + chunker = new LineChunker(); + } + + /// + /// Gets the inline textual diffs. + /// + /// The differ instance. + /// The old text to diff. + /// The new text. + /// if ignore the white space; otherwise, . + /// if case-insensitive; otherwise, . + /// The chunker. + /// The diffs result. + public InlineDiffModel Diff(string oldText, string newText) + { + ArgumentNullException.ThrowIfNull(oldText); + ArgumentNullException.ThrowIfNull(newText); + + InlineDiffModel model = new InlineDiffModel(); + DiffResult diffResult = Differ.CreateDiffs(oldText, newText, ignoreWhiteSpace, ignoreCase, chunker); + BuildDiffPieces(diffResult, model.Lines); + + return model; + } + + private static void BuildDiffPieces(DiffResult diffResult, List pieces) + { + int bPos = 0; + + foreach (var diffBlock in diffResult.DiffBlocks) + { + for (; bPos < diffBlock.InsertStartB; bPos++) + pieces.Add(new DiffPiece(diffResult.PiecesNew[bPos], ChangeType.Unchanged, bPos + 1)); + + int i = 0; + for (; i < Math.Min(diffBlock.DeleteCountA, diffBlock.InsertCountB); i++) + pieces.Add(new DiffPiece(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted)); + + for (i = 0; i < Math.Min(diffBlock.DeleteCountA, diffBlock.InsertCountB); i++) + { + pieces.Add(new DiffPiece(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1)); + bPos++; + } + + if (diffBlock.DeleteCountA > diffBlock.InsertCountB) + { + for (; i < diffBlock.DeleteCountA; i++) + pieces.Add(new DiffPiece(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted)); + } + else + { + for (; i < diffBlock.InsertCountB; i++) + { + pieces.Add(new DiffPiece(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1)); + bPos++; + } + } + } + + for (; bPos < diffResult.PiecesNew.Length; bPos++) + pieces.Add(new DiffPiece(diffResult.PiecesNew[bPos], ChangeType.Unchanged, bPos + 1)); + } +} diff --git a/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/DiffPiece.cs b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/DiffPiece.cs new file mode 100644 index 0000000..509d1f3 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/DiffPiece.cs @@ -0,0 +1,76 @@ +namespace DotNetElements.Core.StringDiff; + +public enum ChangeType +{ + Unchanged, + Deleted, + Inserted, + Imaginary, + Modified +} + +public class DiffPiece : IEquatable +{ + public ChangeType Type { get; set; } + public int? Position { get; set; } + public string? Text { get; set; } + public List SubPieces { get; set; } = []; + + public DiffPiece(string? text, ChangeType type, int? position = null) + { + Text = text; + Position = position; + Type = type; + } + + public DiffPiece() : this(null, ChangeType.Imaginary) + { + } + + public override bool Equals(object? obj) + { + return Equals(obj as DiffPiece); + } + + public bool Equals(DiffPiece? other) + { + return other != null + && Type == other.Type + && EqualityComparer.Default.Equals(Position, other.Position) + && Text == other.Text + && SubPiecesEqual(other); + } + + public override int GetHashCode() + { + ArgumentNullException.ThrowIfNull(Position); + ArgumentNullException.ThrowIfNull(Text); + ArgumentNullException.ThrowIfNull(SubPieces); + + var hashCode = 1688038063; + hashCode = hashCode * -1521134295 + Type.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Position); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Text); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SubPieces.Count); + return hashCode; + } + + private bool SubPiecesEqual(DiffPiece other) + { + if (SubPieces is null) + return other.SubPieces is null; + else if (other.SubPieces is null) + return false; + + if (SubPieces.Count != other.SubPieces.Count) + return false; + + for (int i = 0; i < SubPieces.Count; i++) + { + if (!Equals(SubPieces[i], other.SubPieces[i])) + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/InlineDiffModel.cs b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/InlineDiffModel.cs new file mode 100644 index 0000000..1c990d6 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/InlineDiffModel.cs @@ -0,0 +1,10 @@ +namespace DotNetElements.Core.StringDiff; + +public record class InlineDiffModel(List Lines) +{ + public bool HasDifferences => Lines.Any(x => x.Type != ChangeType.Unchanged); + + public InlineDiffModel() : this([]) + { + } +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/SideBySideDiffModel.cs b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/SideBySideDiffModel.cs new file mode 100644 index 0000000..e78fc14 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/SideBySideDiffModel.cs @@ -0,0 +1,16 @@ +namespace DotNetElements.Core.StringDiff; + +/// +/// A model which represents differences between to texts to be shown side by side +/// +public class SideBySideDiffModel +{ + public InlineDiffModel OldText { get; } + public InlineDiffModel NewText { get; } + + public SideBySideDiffModel() + { + OldText = new InlineDiffModel(); + NewText = new InlineDiffModel(); + } +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/SideBySideDiffBuilder.cs b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/SideBySideDiffBuilder.cs new file mode 100644 index 0000000..f10643c --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/DiffBuilder/SideBySideDiffBuilder.cs @@ -0,0 +1,123 @@ +namespace DotNetElements.Core.StringDiff; + +public class SideBySideDiffBuilder +{ + private delegate ChangeType PieceBuilder(string oldText, string newText, List oldPieces, List newPieces, bool ignoreWhitespace, bool ignoreCase); + + private readonly bool ignoreWhiteSpace; + private readonly bool ignoreCase; + + private readonly LineChunker lineChunker; + private readonly WordChunker wordChunker; + + /// + /// Creates a new instance of a + /// + /// if ignore the white space; otherwise, . + /// if case-insensitive; otherwise, . + public SideBySideDiffBuilder(bool ignoreWhiteSpace = false, bool ignoreCase = false) + { + this.ignoreWhiteSpace = ignoreWhiteSpace; + this.ignoreCase = ignoreCase; + + lineChunker = new LineChunker(); + wordChunker = new WordChunker(); + } + + /// + /// Gets the side-by-side textual diffs. + /// + /// The old text to diff. + /// The new text. + /// The diffs result. + public SideBySideDiffModel Diff(string oldText, string newText) + { + ArgumentNullException.ThrowIfNull(oldText); + ArgumentNullException.ThrowIfNull(newText); + + SideBySideDiffModel model = new(); + DiffResult diffResult = Differ.CreateDiffs(oldText, newText, ignoreWhiteSpace, ignoreCase, lineChunker); + BuildDiffPieces(diffResult, model.OldText.Lines, model.NewText.Lines, BuildWordDiffPieces, ignoreWhiteSpace, ignoreCase); + + return model; + } + + private ChangeType BuildWordDiffPieces(string oldText, string newText, List oldPieces, List newPieces, bool ignoreWhiteSpace, bool ignoreCase) + { + DiffResult diffResult = Differ.CreateDiffs(oldText, newText, ignoreWhiteSpace: ignoreWhiteSpace, ignoreCase, wordChunker); + + return BuildDiffPieces(diffResult, oldPieces, newPieces, subPieceBuilder: null, ignoreWhiteSpace, ignoreCase); + } + + private static ChangeType BuildDiffPieces(DiffResult diffResult, List oldPieces, List newPieces, PieceBuilder? subPieceBuilder, bool ignoreWhiteSpace, bool ignoreCase) + { + int aPos = 0; + int bPos = 0; + + ChangeType changeSummary = ChangeType.Unchanged; + + foreach (DiffBlock diffBlock in diffResult.DiffBlocks) + { + while (bPos < diffBlock.InsertStartB && aPos < diffBlock.DeleteStartA) + { + oldPieces.Add(new DiffPiece(diffResult.PiecesOld[aPos], ChangeType.Unchanged, aPos + 1)); + newPieces.Add(new DiffPiece(diffResult.PiecesNew[bPos], ChangeType.Unchanged, bPos + 1)); + aPos++; + bPos++; + } + + int i = 0; + for (; i < Math.Min(diffBlock.DeleteCountA, diffBlock.InsertCountB); i++) + { + DiffPiece oldPiece = new(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted, aPos + 1); + DiffPiece newPiece = new(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1); + + if (subPieceBuilder is not null) + { + ChangeType subChangeSummary = subPieceBuilder(diffResult.PiecesOld[aPos], diffResult.PiecesNew[bPos], oldPiece.SubPieces, newPiece.SubPieces, ignoreWhiteSpace, ignoreCase); + newPiece.Type = oldPiece.Type = subChangeSummary; + } + + oldPieces.Add(oldPiece); + newPieces.Add(newPiece); + aPos++; + bPos++; + } + + if (diffBlock.DeleteCountA > diffBlock.InsertCountB) + { + for (; i < diffBlock.DeleteCountA; i++) + { + oldPieces.Add(new DiffPiece(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted, aPos + 1)); + newPieces.Add(new DiffPiece()); + aPos++; + } + } + else + { + for (; i < diffBlock.InsertCountB; i++) + { + newPieces.Add(new DiffPiece(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1)); + oldPieces.Add(new DiffPiece()); + bPos++; + } + } + } + + while (bPos < diffResult.PiecesNew.Length && aPos < diffResult.PiecesOld.Length) + { + oldPieces.Add(new DiffPiece(diffResult.PiecesOld[aPos], ChangeType.Unchanged, aPos + 1)); + newPieces.Add(new DiffPiece(diffResult.PiecesNew[bPos], ChangeType.Unchanged, bPos + 1)); + aPos++; + bPos++; + } + + // Consider the whole diff as "modified" if we found any change, otherwise we consider it unchanged + if (oldPieces.Any(x => x.Type is ChangeType.Modified or ChangeType.Inserted or ChangeType.Deleted)) + changeSummary = ChangeType.Modified; + else if (newPieces.Any(x => x.Type is ChangeType.Modified or ChangeType.Inserted or ChangeType.Deleted)) + changeSummary = ChangeType.Modified; + + return changeSummary; + } +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/DelimiterChunker.cs b/src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/DelimiterChunker.cs new file mode 100644 index 0000000..1224de0 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/DelimiterChunker.cs @@ -0,0 +1,70 @@ +namespace DotNetElements.Core.StringDiff; + +internal class DelimiterChunker : IChunker +{ + private readonly char[] delimiters; + + public DelimiterChunker(char[] delimiters) + { + ThrowIf.CollectionIsNullOrEmpty(delimiters); + + this.delimiters = delimiters; + } + + public string[] Chunk(string str) + { + List list = []; + int begin = 0; + bool processingDelimiter = false; + int delimiterBegin = 0; + + for (int i = 0; i < str.Length; i++) + { + if (Array.IndexOf(delimiters, str[i]) != -1) + { + if (i >= str.Length - 1) + { + if (processingDelimiter) + { + list.Add(str[delimiterBegin..(i + 1)]); + } + else + { + list.Add(str[begin..i]); + list.Add(str.Substring(i, 1)); + } + } + else + { + if (!processingDelimiter) + { + // Add everything up to this delimiter as the next chunk (if there is anything) + if (i - begin > 0) + list.Add(str[begin..i]); + + processingDelimiter = true; + delimiterBegin = i; + } + } + + begin = i + 1; + } + else + { + if (processingDelimiter) + { + if (i - delimiterBegin > 0) + list.Add(str[delimiterBegin..i]); + + processingDelimiter = false; + } + + // If we are at the end, add the remaining as the last chunk + if (i >= str.Length - 1) + list.Add(str[begin..(i + 1)]); + } + } + + return [.. list]; + } +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/LineChunker.cs b/src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/LineChunker.cs new file mode 100644 index 0000000..5cac525 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/LineChunker.cs @@ -0,0 +1,11 @@ +namespace DotNetElements.Core.StringDiff; + +internal class LineChunker : IChunker +{ + private static readonly string[] LineSeparators = ["\r\n", "\r", "\n"]; + + public string[] Chunk(string text) + { + return text.Split(LineSeparators, StringSplitOptions.None); + } +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/WordChunker.cs b/src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/WordChunker.cs new file mode 100644 index 0000000..a673795 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/WordChunker.cs @@ -0,0 +1,8 @@ +namespace DotNetElements.Core.StringDiff; + +internal class WordChunker : DelimiterChunker, IChunker +{ + private static readonly char[] WordSeparators = [' ', '\t', '.', '(', ')', '{', '}', ',', '!', '?', ';']; + + public WordChunker() : base(WordSeparators) { } +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/Internal/Differ.cs b/src/DotNetElements.Core/Core/StringDiff/Internal/Differ.cs new file mode 100644 index 0000000..1322910 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/Internal/Differ.cs @@ -0,0 +1,294 @@ +using System.Diagnostics; + +namespace DotNetElements.Core.StringDiff; + +internal static class Differ +{ + public static DiffResult CreateDiffs(string oldText, string newText, bool ignoreWhiteSpace, bool ignoreCase, IChunker chunker) + { + ArgumentNullException.ThrowIfNull(oldText); + ArgumentNullException.ThrowIfNull(newText); + ArgumentNullException.ThrowIfNull(chunker); + + Dictionary pieceHash = new(ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); + List lineDiffs = []; + + ModificationData modOld = new(oldText); + ModificationData modNew = new(newText); + + BuildPieceHashes(pieceHash, modOld, ignoreWhiteSpace, chunker); + BuildPieceHashes(pieceHash, modNew, ignoreWhiteSpace, chunker); + + BuildModificationData(modOld, modNew); + + int piecesALength = modOld.HashedPieces.Length; + int piecesBLength = modNew.HashedPieces.Length; + int posA = 0; + int posB = 0; + + do + { + while (posA < piecesALength + && posB < piecesBLength + && !modOld.Modifications[posA] + && !modNew.Modifications[posB]) + { + posA++; + posB++; + } + + int beginA = posA; + int beginB = posB; + for (; posA < piecesALength && modOld.Modifications[posA]; posA++) + ; + + for (; posB < piecesBLength && modNew.Modifications[posB]; posB++) + ; + + int deleteCount = posA - beginA; + int insertCount = posB - beginB; + + if (deleteCount > 0 || insertCount > 0) + lineDiffs.Add(new DiffBlock(beginA, deleteCount, beginB, insertCount)); + + } while (posA < piecesALength && posB < piecesBLength); + + return new DiffResult(modOld.Pieces, modNew.Pieces, lineDiffs); + } + + private static EditLengthResult CalculateEditLength(int[] a, int startA, int endA, int[] b, int startB, int endB, int[] forwardDiagonal, int[] reverseDiagonal) + { + ArgumentNullException.ThrowIfNull(a); + ArgumentNullException.ThrowIfNull(b); + + if (a.Length == 0 && b.Length == 0) + return new EditLengthResult(); + + int n = endA - startA; + int m = endB - startB; + int max = m + n + 1; + int half = max / 2; + int delta = n - m; + bool deltaEven = delta % 2 == 0; + forwardDiagonal[1 + half] = 0; + reverseDiagonal[1 + half] = n + 1; + + Log("Comparing strings"); + Log("\t{0} of length {1}", a, a.Length); + Log("\t{0} of length {1}", b, b.Length); + + for (int d = 0; d <= half; d++) + { + Log("\nSearching for a {0}-Path", d); + // forward D-path + Log("\tSearching for forward path"); + Edit lastEdit; + for (int k = -d; k <= d; k += 2) + { + Log("\n\t\tSearching diagonal {0}", k); + int kIndex = k + half; + int x, y; + if (k == -d || (k != d && forwardDiagonal[kIndex - 1] < forwardDiagonal[kIndex + 1])) + { + x = forwardDiagonal[kIndex + 1]; // y up move down from previous diagonal + lastEdit = Edit.InsertDown; + Log("\t\tMoved down from diagonal {0} at ({1},{2}) to ", k + 1, x, (x - (k + 1))); + } + else + { + x = forwardDiagonal[kIndex - 1] + 1; // x up move right from previous diagonal + lastEdit = Edit.DeleteRight; + Log("\t\tMoved right from diagonal {0} at ({1},{2}) to ", k - 1, x - 1, (x - 1 - (k - 1))); + } + + y = x - k; + int startX = x; + int startY = y; + Log("({0},{1})", x, y); + while (x < n && y < m && a[x + startA] == b[y + startB]) + { + x += 1; + y += 1; + } + + Log("\t\tFollowed snake to ({0},{1})", x, y); + + forwardDiagonal[kIndex] = x; + + if (!deltaEven && k - delta >= -d + 1 && k - delta <= d - 1) + { + int revKIndex = (k - delta) + half; + int revX = reverseDiagonal[revKIndex]; + int revY = revX - k; + if (revX <= x && revY <= y) + { + return new EditLengthResult + { + EditLength = 2 * d - 1, + StartX = startX + startA, + StartY = startY + startB, + EndX = x + startA, + EndY = y + startB, + LastEdit = lastEdit + }; + } + } + } + + // reverse D-path + Log("\n\tSearching for a reverse path"); + for (int k = -d; k <= d; k += 2) + { + Log("\n\t\tSearching diagonal {0} ({1})", k, k + delta); + int kIndex = k + half; + int x, y; + if (k == -d || (k != d && reverseDiagonal[kIndex + 1] <= reverseDiagonal[kIndex - 1])) + { + x = reverseDiagonal[kIndex + 1] - 1; // move left from k+1 diagonal + lastEdit = Edit.DeleteLeft; + Log("\t\tMoved left from diagonal {0} at ({1},{2}) to ", k + 1, x + 1, ((x + 1) - (k + 1 + delta))); + } + else + { + x = reverseDiagonal[kIndex - 1]; //move up from k-1 diagonal + lastEdit = Edit.InsertUp; + Log("\t\tMoved up from diagonal {0} at ({1},{2}) to ", k - 1, x, (x - (k - 1 + delta))); + } + + y = x - (k + delta); + + int endX = x; + int endY = y; + + Log("({0},{1})", x, y); + while (x > 0 && y > 0 && a[startA + x - 1] == b[startB + y - 1]) + { + x -= 1; + y -= 1; + } + + Log("\t\tFollowed snake to ({0},{1})", x, y); + reverseDiagonal[kIndex] = x; + + if (deltaEven && k + delta >= -d && k + delta <= d) + { + int forKIndex = (k + delta) + half; + int forX = forwardDiagonal[forKIndex]; + int forY = forX - (k + delta); + if (forX >= x && forY >= y) + { + return new EditLengthResult + { + EditLength = 2 * d, + StartX = x + startA, + StartY = y + startB, + EndX = endX + startA, + EndY = endY + startB, + LastEdit = lastEdit + }; + } + } + } + } + + throw new Exception("Should never get here"); + } + + private static void BuildModificationData(ModificationData a, ModificationData b) + { + int n = a.HashedPieces.Length; + int m = b.HashedPieces.Length; + int max = m + n + 1; + var forwardDiagonal = new int[max + 1]; + var reverseDiagonal = new int[max + 1]; + + BuildModificationData(a, 0, n, b, 0, m, forwardDiagonal, reverseDiagonal); + } + + private static void BuildModificationData( + ModificationData A, + int startA, + int endA, + ModificationData B, + int startB, + int endB, + int[] forwardDiagonal, + int[] reverseDiagonal) + { + while (startA < endA && startB < endB && A.HashedPieces[startA].Equals(B.HashedPieces[startB])) + { + startA++; + startB++; + } + + while (startA < endA && startB < endB && A.HashedPieces[endA - 1].Equals(B.HashedPieces[endB - 1])) + { + endA--; + endB--; + } + + int aLength = endA - startA; + int bLength = endB - startB; + if (aLength > 0 && bLength > 0) + { + EditLengthResult result = CalculateEditLength(A.HashedPieces, startA, endA, B.HashedPieces, startB, endB, forwardDiagonal, reverseDiagonal); + if (result.EditLength <= 0) + return; + + if (result.LastEdit == Edit.DeleteRight && result.StartX - 1 > startA) + A.Modifications[--result.StartX] = true; + else if (result.LastEdit == Edit.InsertDown && result.StartY - 1 > startB) + B.Modifications[--result.StartY] = true; + else if (result.LastEdit == Edit.DeleteLeft && result.EndX < endA) + A.Modifications[result.EndX++] = true; + else if (result.LastEdit == Edit.InsertUp && result.EndY < endB) + B.Modifications[result.EndY++] = true; + + BuildModificationData(A, startA, result.StartX, B, startB, result.StartY, forwardDiagonal, reverseDiagonal); + BuildModificationData(A, result.EndX, endA, B, result.EndY, endB, forwardDiagonal, reverseDiagonal); + } + else if (aLength > 0) + { + for (int i = startA; i < endA; i++) + A.Modifications[i] = true; + } + else if (bLength > 0) + { + for (int i = startB; i < endB; i++) + B.Modifications[i] = true; + } + } + + private static void BuildPieceHashes(Dictionary pieceHash, ModificationData data, bool ignoreWhitespace, IChunker chunker) + { + string[] pieces = string.IsNullOrEmpty(data.RawData) ? [] : chunker.Chunk(data.RawData); + data.Pieces = pieces; + + int numPieces = pieces.Length; + data.HashedPieces = new int[numPieces]; + data.Modifications = new bool[numPieces]; + + for (int i = 0; i < numPieces; i++) + { + string piece = pieces[i]; + if (ignoreWhitespace) + piece = piece.Trim(); + + if (pieceHash.TryGetValue(piece, out int value)) + { + data.HashedPieces[i] = value; + } + else + { + data.HashedPieces[i] = pieceHash.Count; + pieceHash[piece] = pieceHash.Count; + } + } + } + + [Conditional("Debug")] + private static void Log(string format, params object[] args) + { + Debug.WriteLine(string.Format(format, args)); + } +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/Internal/IChunker.cs b/src/DotNetElements.Core/Core/StringDiff/Internal/IChunker.cs new file mode 100644 index 0000000..6db8b99 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/Internal/IChunker.cs @@ -0,0 +1,12 @@ +namespace DotNetElements.Core.StringDiff; + +/// +/// Responsible for how to turn the document into pieces +/// +internal interface IChunker +{ + /// + /// Divide text into sub-parts + /// + string[] Chunk(string text); +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/Internal/Models/DiffBlock.cs b/src/DotNetElements.Core/Core/StringDiff/Internal/Models/DiffBlock.cs new file mode 100644 index 0000000..4fbc8f7 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/Internal/Models/DiffBlock.cs @@ -0,0 +1,10 @@ +namespace DotNetElements.Core.StringDiff; + +/// +/// A block of consecutive edits from A and/or B +/// +/// Position where deletions in A begin +/// The number of deletions in A +/// Position where insertion in B begin +/// The number of insertions in B +internal record struct DiffBlock(int DeleteStartA, int DeleteCountA, int InsertStartB, int InsertCountB); \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/Internal/Models/DiffResult.cs b/src/DotNetElements.Core/Core/StringDiff/Internal/Models/DiffResult.cs new file mode 100644 index 0000000..1b44562 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/Internal/Models/DiffResult.cs @@ -0,0 +1,9 @@ +namespace DotNetElements.Core.StringDiff; + +/// +/// The result of diffing two pieces of text +/// +/// The chunked pieces of the old text +/// The chunked pieces of the new text +/// A collection of DiffBlocks which details deletions and insertions +internal record struct DiffResult(string[] PiecesOld, string[] PiecesNew, IList DiffBlocks); \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/Internal/Models/EditLengthResult.cs b/src/DotNetElements.Core/Core/StringDiff/Internal/Models/EditLengthResult.cs new file mode 100644 index 0000000..e748313 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/Internal/Models/EditLengthResult.cs @@ -0,0 +1,12 @@ +namespace DotNetElements.Core.StringDiff; + +internal enum Edit +{ + None, + DeleteRight, + DeleteLeft, + InsertDown, + InsertUp +} + +internal record struct EditLengthResult(int EditLength, int StartX, int EndX, int StartY, int EndY, Edit LastEdit); \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/StringDiff/Internal/Models/ModificationData.cs b/src/DotNetElements.Core/Core/StringDiff/Internal/Models/ModificationData.cs new file mode 100644 index 0000000..3b765e9 --- /dev/null +++ b/src/DotNetElements.Core/Core/StringDiff/Internal/Models/ModificationData.cs @@ -0,0 +1,17 @@ +namespace DotNetElements.Core.StringDiff; + +internal class ModificationData +{ + public int[] HashedPieces { get; set; } = []; + + public string RawData { get; } + + public bool[] Modifications { get; set; } = []; + + public string[] Pieces { get; set; } = []; + + public ModificationData(string rawData) + { + RawData = rawData; + } +} \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/ThrowHelper/ThrowHelper.cs b/src/DotNetElements.Core/Core/ThrowHelper/ThrowHelper.cs deleted file mode 100644 index 567e225..0000000 --- a/src/DotNetElements.Core/Core/ThrowHelper/ThrowHelper.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace DotNetElements.Core; - -public static class ThrowHelper -{ - /// - /// Throws an if is null or . - /// - /// The guid to validate as non-null and non Guid.Empty - /// The name of the parameter with which corresponds. - [DebuggerHidden, StackTraceHidden] - public static void ThrowIfNullOrDefault([NotNull] Guid? guid, [CallerArgumentExpression(nameof(guid))] string? paramName = null) - { - if (guid is null || guid == Guid.Empty) - { - ThrowArgumentException(paramName); - } - } - - /// - /// Throws an if is . - /// - /// The guid to validate as non Guid.Empty - /// The name of the parameter with which corresponds. - [DebuggerHidden, StackTraceHidden] - public static void ThrowIfDefault([NotNull] Guid guid, [CallerArgumentExpression(nameof(guid))] string? paramName = null) - { - if (guid == Guid.Empty) - { - ThrowArgumentException(paramName); - } - } - - /// - /// Throws an if is default. - /// - /// The arguments to validate as non-default - /// The name of the parameter with which corresponds. - [DebuggerHidden, StackTraceHidden] - public static void ThrowIfDefault([NotNull] T arguments, [CallerArgumentExpression(nameof(arguments))] string? paramName = null) - where T : IEquatable - { - if (arguments.Equals(default(T))) - { - ThrowArgumentException(paramName); - } - } - - [DoesNotReturn] - [DebuggerHidden, StackTraceHidden] - private static void ThrowArgumentException(string? paramName) => throw new ArgumentException(paramName); - - [DoesNotReturn] - private static void ThrowArgumentNullException(string? paramName) => throw new ArgumentNullException(paramName); -} diff --git a/src/DotNetElements.Core/Core/ThrowHelper/ThrowIf.cs b/src/DotNetElements.Core/Core/ThrowHelper/ThrowIf.cs new file mode 100644 index 0000000..2b562e2 --- /dev/null +++ b/src/DotNetElements.Core/Core/ThrowHelper/ThrowIf.cs @@ -0,0 +1,68 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace DotNetElements.Core; + +public static class ThrowIf +{ + /// + /// Throws an if is null or . + /// + /// The guid to validate as non-null and non Guid.Empty + /// The name of the parameter with which corresponds. + [DebuggerHidden, StackTraceHidden] + public static void NullOrDefault([NotNull] Guid? guid, [CallerArgumentExpression(nameof(guid))] string? paramName = null) + { + if (guid is null || guid == Guid.Empty) + ThrowArgumentException(paramName); + } + + /// + /// Throws an if is . + /// + /// The guid to validate as non Guid.Empty + /// The name of the parameter with which corresponds. + [DebuggerHidden, StackTraceHidden] + public static void Default([NotNull] Guid guid, [CallerArgumentExpression(nameof(guid))] string? paramName = null) + { + if (guid == Guid.Empty) + ThrowArgumentException(paramName); + } + + /// + /// Throws an if is default. + /// + /// The argument to validate as non-default + /// The name of the parameter with which corresponds. + [DebuggerHidden, StackTraceHidden] + public static void Default([NotNull] T argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + where T : IEquatable + { + if (argument.Equals(default(T))) + ThrowArgumentException(paramName); + } + + /// + /// Throws an if is . + /// Throws an if is empty. + /// + /// Type of the items in the collection. + /// Collection to validate as non null or empty. + /// The name of the parameter with which corresponds. + /// + public static void CollectionIsNullOrEmpty([NotNull] ICollection? collection, [CallerArgumentExpression(nameof(collection))] string? paramName = null) + { + ArgumentNullException.ThrowIfNull(collection, paramName); + + if (collection.Count == 0) + throw new ArgumentException("The collection is empty...", paramName); + } + + [DoesNotReturn] + [DebuggerHidden, StackTraceHidden] + private static void ThrowArgumentException(string? paramName) => throw new ArgumentException(paramName); + + [DoesNotReturn] + private static void ThrowArgumentNullException(string? paramName) => throw new ArgumentNullException(paramName); +} diff --git a/src/DotNetElements.Web.Blazor/CrudService.cs b/src/DotNetElements.Web.Blazor/CrudService.cs index e165b5d..2606df2 100644 --- a/src/DotNetElements.Web.Blazor/CrudService.cs +++ b/src/DotNetElements.Web.Blazor/CrudService.cs @@ -1,6 +1,6 @@ namespace DotNetElements.Web.Blazor; -public interface ICrudService +public interface ICrudService : IReadOnlyCrudService where TKey : notnull, IEquatable where TModel : IModel where TDetails : ModelDetails @@ -8,29 +8,17 @@ public interface ICrudService { Task> CreateEntryAsync(TEditModel editModel); Task DeleteEntryAsync(TModel model); - Task> GetEntryByIdAsync(TKey id); - Task>> GetAllEntriesReadOnlyAsync(); - Task>> GetAllEntriesAsync(); - Task>>> GetAllEntriesWithDetailsReadOnlyAsync(); - Task>>> GetAllEntriesWithDetailsAsync(); - Task GetEntryDetailsAsync(ModelWithDetails modelWithDetails); Task> UpdateEntryAsync(TEditModel editModel); } -public class CrudService : ICrudService where TKey : notnull, IEquatable +public class CrudService : ReadOnlyCrudService, ICrudService + where TKey : notnull, IEquatable where TModel : IModel where TDetails : ModelDetails where TEditModel : IMapFromModel, ICreateNew { - protected readonly ISnackbar Snackbar; - protected readonly HttpClient HttpClient; - protected readonly CrudOptions Options; - - public CrudService(ISnackbar snackbar, HttpClient httpClient, CrudOptions options) + public CrudService(ISnackbar snackbar, HttpClient httpClient, CrudOptions options) : base(snackbar, httpClient, options) { - Snackbar = snackbar; - HttpClient = httpClient; - Options = options; } public virtual async Task> CreateEntryAsync(TEditModel editModel) @@ -89,97 +77,4 @@ public virtual async Task DeleteEntryAsync(TModel model) return result; } - - public virtual async Task> GetEntryByIdAsync(TKey id) - { - Result result = await HttpClient.GetFromJsonWithResultAsync(Options.GetByIdEndpoint(id)); - - // todo add logging - // todo wrap Snackbar call in bool option NotifyUser - // todo add function OnDeleteSuccess - if (result.IsFail) - { - Snackbar.Add("Failed to fetch entry from server", Severity.Error); - } - - return result; - } - - public virtual async Task GetEntryDetailsAsync(ModelWithDetails modelWithDetails) - { - Result detailsResult = await HttpClient.GetFromJsonWithResultAsync(Options.GetDetailsEndpoint(modelWithDetails.Value.Id.ToString())); - - // todo add logging - // todo wrap Snackbar call in bool option NotifyUser - // todo add function OnDeleteSuccess - if (detailsResult.IsFail) - { - Snackbar.Add($"Failed to fetch details.\n{detailsResult.ErrorMessage}", Severity.Error); - return detailsResult; - } - - modelWithDetails.Details = detailsResult.Value; - - return detailsResult; - } - - public virtual async Task>> GetAllEntriesReadOnlyAsync() - { - Result> result = await HttpClient.GetFromJsonWithResultAsync>(Options.GetAllEndpoint); - - // todo add logging - // todo wrap Snackbar call in bool option NotifyUser - // todo add function OnDeleteSuccess - if (result.IsFail) - { - Snackbar.Add("Failed to fetch entries from server", Severity.Error); - } - - return result; - } - - public virtual async Task>> GetAllEntriesAsync() - { - Result> result = await HttpClient.GetFromJsonWithResultAsync>(Options.GetAllEndpoint); - - // todo add logging - // todo wrap Snackbar call in bool option NotifyUser - // todo add function OnDeleteSuccess - if (result.IsFail) - { - Snackbar.Add("Failed to fetch entries from server", Severity.Error); - } - - return result; - } - - public virtual async Task>>> GetAllEntriesWithDetailsReadOnlyAsync() - { - Result>> result = await HttpClient.GetModelWithDetailsListFromJsonAsync(Options.GetAllEndpoint); - - // todo add logging - // todo wrap Snackbar call in bool option NotifyUser - // todo add function OnDeleteSuccess - if (result.IsFail) - { - Snackbar.Add("Failed to fetch entries from server", Severity.Error); - } - - return Result.Ok(result.Value as IReadOnlyList>); - } - - public virtual async Task>>> GetAllEntriesWithDetailsAsync() - { - Result>> result = await HttpClient.GetModelWithDetailsListFromJsonAsync(Options.GetAllEndpoint); - - // todo add logging - // todo wrap Snackbar call in bool option NotifyUser - // todo add function OnDeleteSuccess - if (result.IsFail) - { - Snackbar.Add("Failed to fetch entries from server", Severity.Error); - } - - return result; - } } diff --git a/src/DotNetElements.Web.Blazor/CrudServiceBase.cs b/src/DotNetElements.Web.Blazor/CrudServiceBase.cs new file mode 100644 index 0000000..a3471ad --- /dev/null +++ b/src/DotNetElements.Web.Blazor/CrudServiceBase.cs @@ -0,0 +1,71 @@ +namespace DotNetElements.Web.Blazor; + +public interface ICrudServiceBase + where TKey : notnull, IEquatable + where TModel : IModel +{ + Task> GetEntryByIdAsync(TKey id); + Task>> GetAllEntriesReadOnlyAsync(); + Task>> GetAllEntriesAsync(); +} + +public class CrudServiceBase : ICrudServiceBase + where TKey : notnull, IEquatable + where TModel : IModel +{ + protected readonly ISnackbar Snackbar; + protected readonly HttpClient HttpClient; + protected readonly CrudOptions Options; + + public CrudServiceBase(ISnackbar snackbar, HttpClient httpClient, CrudOptions options) + { + Snackbar = snackbar; + HttpClient = httpClient; + Options = options; + } + + public virtual async Task> GetEntryByIdAsync(TKey id) + { + Result result = await HttpClient.GetFromJsonWithResultAsync(Options.GetByIdEndpoint(id)); + + // todo add logging + // todo wrap Snackbar call in bool option NotifyUser + // todo add function OnDeleteSuccess + if (result.IsFail) + { + Snackbar.Add("Failed to fetch entry from server", Severity.Error); + } + + return result; + } + + public virtual async Task>> GetAllEntriesReadOnlyAsync() + { + Result> result = await HttpClient.GetFromJsonWithResultAsync>(Options.GetAllEndpoint); + + // todo add logging + // todo wrap Snackbar call in bool option NotifyUser + // todo add function OnDeleteSuccess + if (result.IsFail) + { + Snackbar.Add("Failed to fetch entries from server", Severity.Error); + } + + return result; + } + + public virtual async Task>> GetAllEntriesAsync() + { + Result> result = await HttpClient.GetFromJsonWithResultAsync>(Options.GetAllEndpoint); + + // todo add logging + // todo wrap Snackbar call in bool option NotifyUser + // todo add function OnDeleteSuccess + if (result.IsFail) + { + Snackbar.Add("Failed to fetch entries from server", Severity.Error); + } + + return result; + } +} diff --git a/src/DotNetElements.Web.Blazor/CrudTableActionsButton.razor b/src/DotNetElements.Web.Blazor/CrudTableActionsButton.razor new file mode 100644 index 0000000..031178f --- /dev/null +++ b/src/DotNetElements.Web.Blazor/CrudTableActionsButton.razor @@ -0,0 +1,13 @@ +@inherits MudIconButton + +@{ + base.BuildRenderTree(__builder); +} + +@code +{ + public CrudTableActionsButton() + { + Class = "pa-1"; + } +} \ No newline at end of file diff --git a/src/DotNetElements.Web.Blazor/CrudTableActionsCell.razor b/src/DotNetElements.Web.Blazor/CrudTableActionsCell.razor index 0767de2..51d7024 100644 --- a/src/DotNetElements.Web.Blazor/CrudTableActionsCell.razor +++ b/src/DotNetElements.Web.Blazor/CrudTableActionsCell.razor @@ -6,18 +6,20 @@ @if (SimpleTable) { - - - - + + + + + @ChildContent } else { - - - - + + + + + @ChildContent } @@ -37,4 +39,10 @@ else [Parameter] public bool SimpleTable { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public int Width { get; set; } = 140; } \ No newline at end of file diff --git a/src/DotNetElements.Web.Blazor/DialogeServiceExtensions.cs b/src/DotNetElements.Web.Blazor/DialogeServiceExtensions.cs index 82c69a9..9490c04 100644 --- a/src/DotNetElements.Web.Blazor/DialogeServiceExtensions.cs +++ b/src/DotNetElements.Web.Blazor/DialogeServiceExtensions.cs @@ -1,18 +1,51 @@ -namespace DotNetElements.Web.Blazor; +using System.Linq.Expressions; + +namespace DotNetElements.Web.Blazor; public static class DialogeServiceExtensions { - public static async Task ShowDeleteDialogAsync(this IDialogService dialogService, string title, string itemValue, string itemLabel) - { - var dialogParameters = new DialogParameters - { - { x => x.ItemValue, itemValue }, - { x => x.ItemLabel, itemLabel } - }; - - IDialogReference dialog = await dialogService.ShowAsync(title, dialogParameters); - DialogResult result = await dialog.Result; - - return result.Canceled ? Result.Fail("Canceled by user") : Result.Ok(); - } + private static DialogOptions DefaultInfoDialogOptions => new() + { + CloseOnEscapeKey = true, + DisableBackdropClick = false, + }; + + public static async Task ShowDeleteDialogAsync(this IDialogService dialogService, string title, string itemValue, string itemLabel) + { + var dialogParameters = new DialogParameters + { + { x => x.ItemValue, itemValue }, + { x => x.ItemLabel, itemLabel } + }; + + IDialogReference dialog = await dialogService.ShowAsync(title, dialogParameters); + DialogResult result = await dialog.Result; + + return result.Canceled ? Result.Fail("Canceled by user") : Result.Ok(); + } + + public static async Task ShowInfoDialogAsync(this IDialogService dialogService, string title, Expression> parameterPropertyExpression, TParam parameterValue, MaxWidth maxWidth = MaxWidth.Medium, bool fullWidth = true) + where TDialog : InfoDialog + { + DialogParameters dialogParameters = new() + { + { parameterPropertyExpression, parameterValue } + }; + + DialogOptions options = DefaultInfoDialogOptions; + options.MaxWidth = maxWidth; + options.FullWidth = fullWidth; + + await dialogService.ShowAsync(title, dialogParameters, options); + } + + public static async Task ShowInfoDialogAsync(this IDialogService dialogService, string title, MaxWidth maxWidth = MaxWidth.Medium, bool fullWidth = true, DialogParameters? parameters = null) + where TDialog : InfoDialog + { + DialogOptions options = DefaultInfoDialogOptions; + options.MaxWidth = maxWidth; + options.FullWidth = fullWidth; + + await dialogService.ShowAsync(title, parameters, options); + } } diff --git a/src/DotNetElements.Web.Blazor/DiffViewer/DiffViewer.razor b/src/DotNetElements.Web.Blazor/DiffViewer/DiffViewer.razor new file mode 100644 index 0000000..4ad9e7f --- /dev/null +++ b/src/DotNetElements.Web.Blazor/DiffViewer/DiffViewer.razor @@ -0,0 +1,45 @@ +@using DotNetElements.Core.StringDiff + +@if (Diff?.HasDifferences is true) +{ + + + @foreach (DiffPiece diffLine in Diff.Lines) + { + + + + + } +
+ @(diffLine.Position?.ToString() ?? " "); + + + @diffLine.Text + +
+} +else +{ + No differences found +} + +@code +{ + [Parameter, EditorRequired] + public InlineDiffModel Diff { get; set; } = default!; + + // todo tmp + private string GetDiffLineStyle(DiffPiece diffLine) + { + return diffLine.Type switch + { + ChangeType.Unchanged => "", + ChangeType.Deleted => "background-color: red;", + ChangeType.Inserted => "background-color: green;", + ChangeType.Imaginary => "background-color: gray;", + ChangeType.Modified => "background-color: yellow;", + _ => throw new NotImplementedException(nameof(diffLine.Type)), + }; + } +} diff --git a/src/DotNetElements.Web.Blazor/CardLoadingLinear.razor b/src/DotNetElements.Web.Blazor/DneLoadingLinear.razor similarity index 85% rename from src/DotNetElements.Web.Blazor/CardLoadingLinear.razor rename to src/DotNetElements.Web.Blazor/DneLoadingLinear.razor index f9104e7..86b0182 100644 --- a/src/DotNetElements.Web.Blazor/CardLoadingLinear.razor +++ b/src/DotNetElements.Web.Blazor/DneLoadingLinear.razor @@ -6,7 +6,7 @@ @code { - public CardLoadingLinear() + public DneLoadingLinear() { Color = Color.Primary; Indeterminate = true; diff --git a/src/DotNetElements.Web.Blazor/Extensions/ServiceCollectionExtensions.cs b/src/DotNetElements.Web.Blazor/Extensions/ServiceCollectionExtensions.cs index 7f002cb..a2bba27 100644 --- a/src/DotNetElements.Web.Blazor/Extensions/ServiceCollectionExtensions.cs +++ b/src/DotNetElements.Web.Blazor/Extensions/ServiceCollectionExtensions.cs @@ -4,6 +4,46 @@ namespace DotNetElements.Web.Blazor.Extensions; public static class ServiceCollectionExtensions { + public static IServiceCollection AddCrudService(this IServiceCollection services, CrudOptions options) + where TKey : notnull, IEquatable + where TModel : IModel + { + // todo consider using the options pattern + // Action> configureOptions as parameter + // Call services.Configure(configureOptions); + // In CrudService inject a IOptions> + + services.AddScoped>(provider => new CrudServiceBase( + provider.GetRequiredService(), + provider.GetRequiredService(), + options)); + + return services; + } + + public static IServiceCollection AddCrudService(this IServiceCollection services, CrudOptions options) + where TKey : notnull, IEquatable + where TModel : IModel + where TDetails : ModelDetails + { + // todo consider using the options pattern + // Action> configureOptions as parameter + // Call services.Configure(configureOptions); + // In CrudService inject a IOptions> + + services.AddScoped>(provider => new CrudServiceBase( + provider.GetRequiredService(), + provider.GetRequiredService(), + options)); + + services.AddScoped>(provider => new ReadOnlyCrudService( + provider.GetRequiredService(), + provider.GetRequiredService(), + options)); + + return services; + } + public static IServiceCollection AddCrudService(this IServiceCollection services, CrudOptions options) where TKey : notnull, IEquatable where TModel : IModel @@ -15,6 +55,16 @@ public static IServiceCollection AddCrudService> + services.AddScoped>(provider => new CrudServiceBase( + provider.GetRequiredService(), + provider.GetRequiredService(), + options)); + + services.AddScoped>(provider => new ReadOnlyCrudService( + provider.GetRequiredService(), + provider.GetRequiredService(), + options)); + services.AddScoped>(provider => new CrudService( provider.GetRequiredService(), provider.GetRequiredService(), diff --git a/src/DotNetElements.Web.Blazor/InfoDialog.razor b/src/DotNetElements.Web.Blazor/InfoDialog.razor new file mode 100644 index 0000000..e94291a --- /dev/null +++ b/src/DotNetElements.Web.Blazor/InfoDialog.razor @@ -0,0 +1,13 @@ + + + + @DialogInstance.Title + + + + @DialogContent + + + Ok + + diff --git a/src/DotNetElements.Web.Blazor/InfoDialog.razor.cs b/src/DotNetElements.Web.Blazor/InfoDialog.razor.cs new file mode 100644 index 0000000..721baf4 --- /dev/null +++ b/src/DotNetElements.Web.Blazor/InfoDialog.razor.cs @@ -0,0 +1,15 @@ +namespace DotNetElements.Web.Blazor; + +public partial class InfoDialog : ComponentBase +{ + [CascadingParameter] + private MudDialogInstance DialogInstance { get; set; } = default!; + + [Parameter, EditorRequired] + public RenderFragment DialogContent { get; set; } = default!; + + private void OnClose() + { + DialogInstance.Close(); + } +} diff --git a/src/DotNetElements.Web.Blazor/ReadOnlyCrudService.cs b/src/DotNetElements.Web.Blazor/ReadOnlyCrudService.cs new file mode 100644 index 0000000..0f15050 --- /dev/null +++ b/src/DotNetElements.Web.Blazor/ReadOnlyCrudService.cs @@ -0,0 +1,69 @@ +namespace DotNetElements.Web.Blazor; + +public interface IReadOnlyCrudService : ICrudServiceBase + where TKey : notnull, IEquatable + where TModel : IModel + where TDetails : ModelDetails +{ + Task>>> GetAllEntriesWithDetailsReadOnlyAsync(); + Task>>> GetAllEntriesWithDetailsAsync(); + Task GetEntryDetailsAsync(ModelWithDetails modelWithDetails); +} + +public class ReadOnlyCrudService : CrudServiceBase, IReadOnlyCrudService + where TKey : notnull, IEquatable + where TModel : IModel + where TDetails : ModelDetails +{ + public ReadOnlyCrudService(ISnackbar snackbar, HttpClient httpClient, CrudOptions options) : base(snackbar, httpClient, options) + { + } + + public virtual async Task GetEntryDetailsAsync(ModelWithDetails modelWithDetails) + { + Result detailsResult = await HttpClient.GetFromJsonWithResultAsync(Options.GetDetailsEndpoint(modelWithDetails.Value.Id.ToString())); + + // todo add logging + // todo wrap Snackbar call in bool option NotifyUser + // todo add function OnDeleteSuccess + if (detailsResult.IsFail) + { + Snackbar.Add($"Failed to fetch details.\n{detailsResult.ErrorMessage}", Severity.Error); + return detailsResult; + } + + modelWithDetails.Details = detailsResult.Value; + + return detailsResult; + } + + public virtual async Task>>> GetAllEntriesWithDetailsReadOnlyAsync() + { + Result>> result = await HttpClient.GetModelWithDetailsListFromJsonAsync(Options.GetAllEndpoint); + + // todo add logging + // todo wrap Snackbar call in bool option NotifyUser + // todo add function OnDeleteSuccess + if (result.IsFail) + { + Snackbar.Add("Failed to fetch entries from server", Severity.Error); + } + + return Result.Ok(result.Value as IReadOnlyList>); + } + + public virtual async Task>>> GetAllEntriesWithDetailsAsync() + { + Result>> result = await HttpClient.GetModelWithDetailsListFromJsonAsync(Options.GetAllEndpoint); + + // todo add logging + // todo wrap Snackbar call in bool option NotifyUser + // todo add function OnDeleteSuccess + if (result.IsFail) + { + Snackbar.Add("Failed to fetch entries from server", Severity.Error); + } + + return result; + } +}