From c4dc7f008e4f16ee7c2a6db77aad24acc10ff8a9 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 15 Feb 2026 16:26:47 -0500 Subject: [PATCH 1/4] fix: Add notebook forking logic and improve notebook item UI Backend - Fixed project forking to properly clone notebooks and versions Frontend - Refactored notebook and targeted project UI to use global design tokens - Improved button hierarchy, hover states, and visual consistency --- .../project-content.component.html | 20 +- .../project-content.component.scss | 66 ++++- .../project-notebook-item.component.html | 260 ++++++++++++++---- .../project-notebook-item.component.scss | 98 ++++++- .../projects/project/project.component.html | 46 ++-- .../projects/project/project.component.scss | 256 +++++++++-------- .../styles/base/_bootstrap-overrides.scss | 33 +++ .../Controllers/ProjectController.cs | 133 +++++++-- 8 files changed, 683 insertions(+), 229 deletions(-) diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-content/project-content.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-content/project-content.component.html index 3ba6aee..6ad2088 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-content/project-content.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-content/project-content.component.html @@ -1,22 +1,22 @@ -
-
+
+
Notebooks
-
- -
-
    -
  • +
  • . . .
  • -
  • No Notebooks Found
  • +
  • No Notebooks Found
  • -
    - - {{notebook.name+notebook.extension}} - -
    - - - - - -
    -
    - - {{notebook.name}} -
    -
    @@ -45,20 +137,38 @@ -
@@ -104,8 +104,10 @@

-
{{isFull ? 'Read Less..' : 'Read More..'}}
-
Run Notebook
+ +
@@ -141,20 +143,28 @@

- - - -
- - - - - - + +
+ + +
diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.scss b/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.scss index 03ee2de..7839799 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.scss +++ b/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.scss @@ -1,39 +1,3 @@ -// :host { -// height : 100% -// } - -// .test { -// background-color: aqua; -// } - -// .test2 { -// background-color: rebeccapurple; -// } - -// .test3 { -// background-color:black; -// } - -// .list-group-item{ -// color: rgba(255,255,255,.5); -// background-color: white !important; -// } - -// .membersList{ -// padding-top: 120px; -// float: left; -// height: 500px; -// width: fit-content; -// padding-right: 50px; -// } - -// .btn-primary{ -// color: rgb(112, 41, 41); -// background-color: #0069d9; -// border-color: #0062cc; -// } - - :host ::ng-deep .project-overview-container { border: 1px solid rgba(0,0,0,.125); padding-top: 20px; @@ -42,6 +6,11 @@ padding: 0px 20px; } + .project-view-option-row { + padding-left: 0 !important; + padding-right: 0 !important; + } + .project-notebook-row{ /* Jupyter Notebook-like styling */ .notebook-readme{ @@ -55,44 +24,34 @@ margin-bottom: 4px; overflow-x: scroll; } - -.notebook-cell pre code div { - background: #cfc8c8; - padding: 10px; -} -.notebook-cell pre{ - padding: 10px; -} -.notebook-cell img { - max-width: 100%; - height: auto; -} -pre code.hljs{display:block;overflow-x:auto;padding:1em} -code.hljs{padding:3px 5px} -.hljs{background:#f3f3f3;color:#444} -.hljs-comment{color:#697070} -.hljs-punctuation,.hljs-tag{color:#444a} -.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444} -.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} + .notebook-cell pre code div { + background: #cfc8c8; + padding: 10px; + } + .notebook-cell pre{ + padding: 10px; + } + .notebook-cell img { + max-width: 100%; + height: auto; + } + + pre code.hljs{display:block;overflow-x:auto;padding:1em} + code.hljs{padding:3px 5px} + .hljs{background:#f3f3f3;color:#444} + .hljs-comment{color:#697070} + .hljs-punctuation,.hljs-tag{color:#444a} + .hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444} + .hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} } - + .buttons{ - display: flex; - gap: 20px; - width: max-content; - margin: auto; - } - .readme-button{ - margin: auto; - width: max-content; - padding: 8px; - color: white; - background-color: var(--button-background-color) !important; - border-radius: 5px; - // margin-top:-20px; - margin-bottom: 30px; - cursor: pointer; + display: flex; + gap: var(--space-3); + justify-content: center; + margin: var(--space-4) 0 var(--space-5); + flex-wrap: wrap; } .project-detail-row { @@ -112,7 +71,7 @@ code.hljs{padding:3px 5px} &:hover { .project-author-name { - color: #0056b3; + color: var(--link-hover-color); text-decoration: underline; } } @@ -146,9 +105,8 @@ code.hljs{padding:3px 5px} align-items: center; margin-bottom: 5px; - .project-statistics-header { - color: #007bff; + color: var(--link-on-light); cursor: pointer; } @@ -162,15 +120,15 @@ code.hljs{padding:3px 5px} .project-statistics-pill { padding: 1px 5px; - border: 1px solid gray; + border: 1px solid var(--border-color); border-radius: 20px; margin-right: 10px; font-size: 12px; - color: #007bff; + color: var(--link-on-light); } .project-statistics-current { - color: green; + color: var(--color-success); } } } @@ -197,10 +155,10 @@ code.hljs{padding:3px 5px} li { margin-left: 10px; - color: #007bff; + color: var(--link-on-light); &:hover { - color: #0056b3; + color: var(--link-hover-color); text-decoration: underline; } } @@ -215,13 +173,13 @@ code.hljs{padding:3px 5px} input, button { border: none; background-color: unset; - color: #007bff; + color: var(--link-on-light); display: flex; margin-left: auto; margin-right: auto; &:hover { - color: #0056b3; + color: var(--link-hover-color); text-decoration: underline; } } @@ -235,69 +193,146 @@ code.hljs{padding:3px 5px} input, button { border: none; background-color: unset; - color: #007bff; + color: var(--link-on-light); &:hover { - color: #0056b3; + color: var(--link-hover-color); text-decoration: underline; } } } .project-view-option-row { - padding: 0px; border-top: 1px solid rgba(0,0,0,.125); align-items: center; .project-view-toggle { - & > input { padding: 10px 20px; background: transparent; border: none; - //border-right: 1px solid rgba(0,0,0,.125); &.active { - background-color: #edfffb; + background-color: var(--background-color-tertiary); } } } .project-view-interaction { margin-left: auto; - margin-right: 10px; + margin-right: 0; display: flex; + align-items: center; + gap: var(--space-2); + margin-top: 2px; + margin-bottom: 2px; - input, button { - padding: 0px 10px; - background-color: unset; - border: none; - color: #007bff; + .project-view-extra-interaction { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; + } + + /* === More / Less pill === */ + .more-block { + margin-right: 20px; + display: inline-flex; + align-items: center; + justify-content: flex-end; + + height: 38px; + box-sizing: border-box; + + border: var(--border-w-1) var(--border-style) var(--border-color); + border-radius: var(--nav-link-radius); + + padding-left: var(--space-2); + padding-right: var(--space-2); + + white-space: nowrap; + } + + /* The actions expand LEFT from the right edge */ + .more-actions { + display: inline-flex; + align-items: center; + gap: var(--space-2); + + max-width: 0; + opacity: 0; + transform: translateX(8px); + overflow: hidden; + pointer-events: none; + + transition: + max-width 240ms ease, + opacity 180ms ease, + transform 240ms ease; + } + + .more-actions.is-open { + max-width: 520px; + opacity: 1; + transform: translateX(0); + pointer-events: auto; + } + + /* Links inside “More” */ + .more-link { + display: inline-flex; + align-items: center; + height: 100%; + + padding: 0 var(--space-1); + border-radius: var(--nav-link-radius); + + color: var(--link-on-light); + cursor: pointer; + text-decoration: none; + + transition: var(--motion-nav); &:hover { - color: #0056b3; + color: var(--link-hover-color); text-decoration: underline; } } - .fork { - margin-right: 10px; - padding: 5px 10px; - min-width: 90px; - color: var(--button-font-color); - background-color: var(--button-background-color); + .more-toggle { + display: inline-flex; + align-items: center; + justify-content: flex-end; + + height: 100%; + padding: 0; + border: none; + background: transparent; + + color: var(--link-on-light); + cursor: pointer; + + min-width: 74px; + text-align: right; + + transition: color var(--motion-fast); &:hover { - color: var(--button-font-color-secondary-hov); - background-color: var(--button-background-color-secondary-hov); - border: var(--button-border-width) var(--button-border-style) var(--button-border-color-secondary-hov); - text-decoration: none; + color: var(--link-hover-color); + text-decoration: underline; } } - .project-view-extra-interaction { - display: flex; + .more-toggle__label { + display: inline-flex; + align-items: center; + gap: var(--space-1); + line-height: 1; + padding-right: var(--space-2); + } + + .is-hidden { + display: none; } } } @@ -308,13 +343,12 @@ code.hljs{padding:3px 5px} .project-view-container { width: 100%; padding: 20px; - border-top: 1px solid #edfffb; - background-color: #edfffb; + border-top: 1px solid var(--background-color-tertiary); + background-color: var(--background-color-tertiary); } } } - .badge { color: #212529; background-color: #f8f9fa; @@ -345,7 +379,6 @@ code.hljs{padding:3px 5px} } .project-comment-container { - .project-comment-header { display: flex; justify-content: center; @@ -377,5 +410,4 @@ code.hljs{padding:3px 5px} } } } -} - +} \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/assets/styles/base/_bootstrap-overrides.scss b/src/Analysim.Web/ClientApp/src/assets/styles/base/_bootstrap-overrides.scss index 4df6a7e..9763e8f 100644 --- a/src/Analysim.Web/ClientApp/src/assets/styles/base/_bootstrap-overrides.scss +++ b/src/Analysim.Web/ClientApp/src/assets/styles/base/_bootstrap-overrides.scss @@ -49,3 +49,36 @@ var(--button-border-color-secondary-hov) !important; color: var(--button-font-color-secondary-hov) !important; } + +/* Outline primary button (secondary actions) */ +.btn-outline-primary { + background: transparent !important; + border: var(--button-border-width) var(--button-border-style) + var(--button-border-color) !important; + color: var(--button-border-color) !important; +} + +.btn-outline-primary:hover:not(:disabled):not(.disabled), +.btn-outline-primary:focus:not(:disabled):not(.disabled) { + background: var(--button-background-color) !important; + border: var(--button-border-width) var(--button-border-style) + var(--button-border-color) !important; + color: var(--button-font-color) !important; +} + +/* Quiet danger button */ +.btn-danger { + background: transparent !important; + border: var(--button-border-width) var(--button-border-style) + var(--color-danger) !important; + color: var(--color-danger) !important; +} + +/* Hover = full danger */ +.btn-danger:hover:not(:disabled):not(.disabled), +.btn-danger:focus:not(:disabled):not(.disabled) { + background: var(--color-danger) !important; + border: var(--button-border-width) var(--button-border-style) + var(--color-danger) !important; + color: var(--c-text-inverse) !important; +} diff --git a/src/Analysim.Web/Controllers/ProjectController.cs b/src/Analysim.Web/Controllers/ProjectController.cs index fe15958..e1063b0 100644 --- a/src/Analysim.Web/Controllers/ProjectController.cs +++ b/src/Analysim.Web/Controllers/ProjectController.cs @@ -464,43 +464,124 @@ public async Task ForkProjectWithoutBlob([FromForm] ProjectForkVM aup.UserRole == "owner")); // If the project exists, return a conflict response - if (projectExists) return Conflict(new { message = "Project Already Exists" }); - - // Create Project - var newProject = new Project + if (projectExists) return Conflict(new { message = "Project Already Exists" }); + + await using var tx = await _dbContext.Database.BeginTransactionAsync(); + + try { - Name = project.Name, - Visibility = project.Visibility, - Description = project.Description, - DateCreated = DateTimeOffset.UtcNow, - LastUpdated = DateTimeOffset.UtcNow, - Route = user.UserName + "/" + project.Name, - ForkedFromProjectID = project.ProjectID, - }; + // 1) Create forked Project + var newProject = new Project + { + Name = project.Name, + Visibility = project.Visibility, + Description = project.Description, + DateCreated = DateTimeOffset.UtcNow, + LastUpdated = DateTimeOffset.UtcNow, + Route = user.UserName + "/" + project.Name, + ForkedFromProjectID = project.ProjectID, + }; - // Add Project And Save Change - await _dbContext.Projects.AddAsync(newProject); - await _dbContext.SaveChangesAsync(); + // Add Project And Save Change + await _dbContext.Projects.AddAsync(newProject); + await _dbContext.SaveChangesAsync(); - // Add ProjectUser And Save Change - await _dbContext.AddAsync( - new ProjectUser + // 2) Add owner + await _dbContext.AddAsync(new ProjectUser { UserID = user.Id, ProjectID = newProject.ProjectID, UserRole = "owner", IsFollowing = true + }); + await _dbContext.SaveChangesAsync(); + + // 3) Clone Notebooks + Contents + ObservableNotebookDataset + var sourceNotebooks = await _dbContext.Notebook + .Where(n => n.ProjectID == project.ProjectID) + .Include(n => n.NotebookContents) + .Include(n => n.observableNotebookDatasets) + .ToListAsync(); + + foreach (var oldNotebook in sourceNotebooks) + { + var newNotebook = new Notebook + { + ProjectID = newProject.ProjectID, + Project = newProject, + + Name = oldNotebook.Name, + Directory = oldNotebook.Directory, + Extension = oldNotebook.Extension, + + Container = "notebook-" + newProject.Name.ToLower(), + Route = user.UserName + "/" + newProject.Name, + + Uri = oldNotebook.Uri, + Size = oldNotebook.Size, + + DateCreated = DateTimeOffset.UtcNow, + LastModified = DateTimeOffset.UtcNow, + + type = oldNotebook.type + }; + + await _dbContext.Notebook.AddAsync(newNotebook); + await _dbContext.SaveChangesAsync(); + + // Copy versions + if (oldNotebook.NotebookContents != null && oldNotebook.NotebookContents.Count > 0) + { + foreach (var oldContent in oldNotebook.NotebookContents) + { + await _dbContext.NotebookContent.AddAsync(new NotebookContent + { + NotebookID = newNotebook.NotebookID, + Version = oldContent.Version, + Content = oldContent.Content, + Author = oldContent.Author, + Size = oldContent.Size, + DateCreated = oldContent.DateCreated + }); + } + } + + // Copy observable dataset links + if (oldNotebook.observableNotebookDatasets != null && oldNotebook.observableNotebookDatasets.Count > 0) + { + foreach (var oldObs in oldNotebook.observableNotebookDatasets) + { + await _dbContext.ObservableNotebookDataset.AddAsync(new ObservableNotebookDataset + { + NotebookID = newNotebook.NotebookID, + datasetName = oldObs.datasetName, + datasetURL = oldObs.datasetURL, + BlobFileID = oldObs.BlobFileID + }); + } + } + + await _dbContext.SaveChangesAsync(); } - ); - await _dbContext.SaveChangesAsync(); - // Return Ok Request - return Ok(new - { - result = newProject, - message = "Project Successfully Forked" - }); + await tx.CommitAsync(); + return Ok(new + { + result = newProject, + message = "Project Successfully Forked" + }); + } + catch (Exception ex) + { + await tx.RollbackAsync(); + Console.WriteLine(ex); + return BadRequest(new + { + message = "Fork failed", + detail = ex.Message + }); + } } /* From f8d20146cbf4f511cd45378f386b05a2312e4ee3 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 18 Feb 2026 14:35:41 -0500 Subject: [PATCH 2/4] fix: CSV files reliably load in Datasets tab Issue: - currentDirectory was being derived from the route inconsistently Fix: - Removed string slicing that caused inconsistent routing - Added normalized file routing logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI: - Tweaked Datasets tab UI - Improved button hierarchy - Header shows Current folder / path clearly - Clarified navigation by replacing the confusing “…” row with a clear back/close behavior - Added preview context so it is more obvious what file is open --- .../project-file-explorer-item.component.html | 59 ++- .../project-file-explorer-item.component.scss | 98 ++++- .../project-file-explorer.component.html | 382 +++++++++++++----- .../project-file-explorer.component.scss | 261 ++++++++++-- .../project-file-explorer.component.ts | 28 +- .../project-file-preview.component.html | 56 ++- .../project-file-preview.component.scss | 56 ++- 7 files changed, 745 insertions(+), 195 deletions(-) diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer-item/project-file-explorer-item.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer-item/project-file-explorer-item.component.html index 97c0ee7..6922a88 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer-item/project-file-explorer-item.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer-item/project-file-explorer-item.component.html @@ -1,15 +1,50 @@ -
  • -
    - - +
  • +
    + + +
    + {{ timeSince }} +
    -
  • \ No newline at end of file + + diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer-item/project-file-explorer-item.component.scss b/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer-item/project-file-explorer-item.component.scss index 6eb0442..1df8a4f 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer-item/project-file-explorer-item.component.scss +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer-item/project-file-explorer-item.component.scss @@ -1,6 +1,92 @@ -.list-group-item{ - border-top-right-radius: 0 !important; - border-top-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; - border-bottom-left-radius: 0 !important; -} \ No newline at end of file +.file-item { + border: var(--border-w-1) var(--border-style) var(--border-color); + border-radius: var(--radius-1); + padding: var(--space-3) var(--space-4); + background: var(--surface-0); + + transition: background-color var(--motion-fast), border-color var(--motion-fast); +} + +/* Selected state */ +.file-item.is-selected { + background: var(--background-color-tertiary); + border-color: var(--c-accent-600); +} + +.file-row { + width: 100%; + display: flex; + align-items: center; + gap: var(--space-3); +} + +.file-left { + display: flex; + align-items: center; + gap: var(--space-2); + flex: 1 1 auto; + min-width: 0; +} + +.file-icon { + color: var(--c-accent-600); + font-size: var(--fs-16); + flex: 0 0 auto; +} + +.file-link { + cursor: pointer; + font-size: var(--fs-16); + font-weight: 500; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + color: var(--link-on-light); + text-decoration: none; +} + +.file-link:hover { + color: var(--link-hover-color); + text-decoration: underline; +} + +.file-right { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: var(--space-2); + flex: 0 0 auto; +} + +.file-meta { + font-size: var(--fs-12); + opacity: 0.75; + color: var(--text-on-light); + white-space: nowrap; +} + +.file-item:hover { + border-color: var(--c-accent-600); +} + +.file-icon.fa-level-up-alt { + color: var(--c-accent-600); +} + +.file-link { + cursor: pointer; + font-size: var(--fs-16); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--link-on-light); + text-decoration: none; +} + +.file-link:hover { + color: var(--link-hover-color); + text-decoration: underline; +} diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer.component.html index ff8e655..37c8da4 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-file-explorer/project-file-explorer.component.html @@ -1,116 +1,282 @@ -
    - - -
    - - - - {{folders}} - - -
    - -
    - - - - - - -
    - -
    - - -
    -
    -
    - -
      -
    • No File Found
    • - -
    -