github-issue-tower-defence-management 1.142.0 → 1.142.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +1 -0
- package/CHANGELOG.md +14 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-2c5E3tdu.js +81 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BaGbcbYI.css +2 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/repositories/GoogleSpreadsheetRepository.js +10 -2
- package/bin/adapter/repositories/GoogleSpreadsheetRepository.js.map +1 -1
- package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js +18 -14
- package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js.map +1 -1
- package/package.json +40 -40
- package/src/adapter/entry-points/console/ui/vite.config.ts +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-2c5E3tdu.js +81 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-BaGbcbYI.css +2 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/repositories/GoogleSpreadsheetRepository.integration.test.ts +189 -55
- package/src/adapter/repositories/GoogleSpreadsheetRepository.test.ts +63 -5
- package/src/adapter/repositories/GoogleSpreadsheetRepository.ts +17 -2
- package/src/adapter/repositories/LocalStorageRepository.integration.test.ts +1 -1
- package/src/domain/usecases/dashboard/ComposeDashboardUseCase.test.ts +47 -35
- package/src/domain/usecases/dashboard/ComposeDashboardUseCase.ts +19 -16
- package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts +1 -0
- package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts.map +1 -1
- package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts +3 -0
- package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BTtIpfv_.css +0 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CMK3yyfo.js +0 -103
- package/src/adapter/entry-points/console/ui-dist/assets/index-BTtIpfv_.css +0 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-CMK3yyfo.js +0 -103
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-outline-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-input:#e5e5e5;--color-ring:#0a0a0a;--color-background:#fff;--color-foreground:#0a0a0a;--color-primary:#171717;--color-primary-foreground:#fafafa;--color-secondary:#f5f5f5;--color-secondary-foreground:#171717;--color-accent:#f5f5f5;--color-accent-foreground:#171717}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.relative{position:relative}.static{position:static}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.block{display:block}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-input{border-color:var(--color-input)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background)}.bg-primary{background-color:var(--color-primary)}.bg-secondary{background-color:var(--color-secondary)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-foreground{color:var(--color-foreground)}.text-primary-foreground{color:var(--color-primary-foreground)}.text-secondary-foreground{color:var(--color-secondary-foreground)}.lowercase{text-transform:lowercase}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-accent:hover{background-color:var(--color-accent)}.hover\:bg-primary\/90:hover{background-color:#171717e6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary) 90%, transparent)}}.hover\:bg-secondary\/80:hover{background-color:#f5f5f5cc}@supports (color:color-mix(in lab, red, red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab, var(--color-secondary) 80%, transparent)}}.hover\:text-accent-foreground:hover{color:var(--color-accent-foreground)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}}html{font-size:clamp(15px,2.1vw,26px)}body{color:#e6edf3;background-color:#0d1117;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.console-app{flex-direction:column;width:100%;min-height:100dvh;display:flex}.console-tabbar{-webkit-overflow-scrolling:touch;background:#161b22;border-bottom:2px solid #30363d;flex-wrap:nowrap;align-items:stretch;gap:0;min-height:42px;padding:0 8px;display:flex;overflow-x:auto}.console-tab{color:#8b949e;white-space:nowrap;cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;align-items:center;gap:6px;margin-bottom:-2px;padding:10px 14px;font-size:.8125rem;font-weight:500;line-height:1.2;text-decoration:none;display:inline-flex}.console-tab:hover{color:#e6edf3}.console-tab[data-active=true]{color:#e6edf3;border-bottom-color:#2f81f7;font-weight:700}.console-tab-badge{text-align:center;color:#e6edf3;background:#484f58;border-radius:20px;min-width:20px;padding:1px 7px;font-size:.6875rem;font-weight:700;line-height:1.5}.console-tab-badge[data-zero=true]{color:#8b949e;background:#30363d}.console-tab-pjname{color:#8b949e;align-self:center;margin-left:auto;padding:0 8px;font-size:.7188rem}.console-tab-geninfo{color:#8b949e;align-self:center;padding:0 4px;font-size:.6875rem}.console-list{margin:0;padding:12px 18px 18px;list-style:none}.console-list-group{list-style:none}.console-item-row .console-item-icon{flex:none;margin-top:3px}.console-group-header{background:#0b0f14;border-bottom:1px solid #21262d;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-storytag{align-items:center;gap:8px;font-size:.8125rem;font-weight:700;display:inline-flex}.console-story-dot{border-radius:999px;width:10px;height:10px;display:inline-block}.console-group-count{color:#8b949e;font-size:.75rem}.console-item-row{color:#e6edf3;text-align:left;cursor:pointer;background:#161b22;border:1px solid #30363d;border-radius:8px;align-items:flex-start;gap:14px;width:100%;margin-bottom:10px;padding:12px 16px;display:flex}.console-item-row:hover{background:#1a2029;border-color:#484f58}.console-item-row[data-active=true]{background:#1a2029;border-color:#4493f8}.console-item-meta{flex:1;min-width:0}.console-item-title{font-size:.9063rem;font-weight:600;display:block}.console-item-sub{color:#8b949e;margin-top:3px;font-size:.7813rem;display:block}.console-item-pill{color:#8b949e;border:1px solid #30363d;border-radius:20px;margin-right:6px;padding:1px 8px;font-size:.6875rem;display:inline-block}.console-item-createdat{color:#8b949e;cursor:help}.console-item-fields{color:#adbac7;flex-wrap:wrap;gap:4px 10px;margin-top:4px;font-size:.7188rem;display:flex}.console-item-field{word-break:break-all;align-items:baseline;gap:4px;min-width:0;display:inline-flex}.console-item-field-label{color:#6e7681;text-transform:uppercase;letter-spacing:.03em;font-size:.625rem}.console-list-message{color:#8b949e;padding:16px;font-size:.875rem}.console-list-empty{text-align:center;color:#8b949e;padding:40px;font-size:.875rem}.console-list-error,.console-comment-error,.console-files-error,.console-commits-error,.console-detail-body-error{color:#f85149}.console-detail{flex-direction:column;flex:1 0 auto;gap:12px;padding:16px 16px 0;display:flex}.console-detail-title{align-items:center;gap:8px;margin:0;font-size:1.25rem;display:flex}.console-detail-title-text{flex:1}.console-detail-number{color:#8b949e;font-weight:400}.console-detail-closed-label{color:#a371f7;font-size:.8125rem}.console-detail-subbar{align-items:center;gap:12px;font-size:.8125rem;display:flex}.console-detail-link{color:#4493f8}.console-detail-repo{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-copy-url-button{color:#c9d1d9;border:1px solid #30363d;border-radius:6px;height:24px;padding:0 8px;font-size:.75rem}.console-copy-url-button:hover{color:#e6edf3;background-color:#21262d}.console-detail-pill,.console-label-chip,.console-detail-status-chip{border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:.75rem;display:inline-block}.console-detail-pr-status-row{flex-wrap:wrap;align-items:center;gap:6px;margin-top:-4px;display:flex}.console-detail-pr-status{flex-wrap:wrap;align-items:center;gap:6px;display:inline-flex}.console-detail-ci-missing{opacity:.85;font-weight:400}.console-detail-mergeable-chip{font-weight:600}.console-detail-mergeable-chip-unknown{opacity:.7;font-weight:400}.console-detail-labels{flex-wrap:wrap;gap:6px;display:flex}.console-detail-createdat{color:#8b949e;font-size:.75rem}.console-panel{border:1px solid #30363d;border-radius:8px;overflow:hidden}.console-panel-header{background:#161b22;justify-content:space-between;align-items:center;display:flex}.console-panel-toggle{color:#e6edf3;text-align:left;cursor:pointer;background:0 0;border:none;flex:1;align-items:center;gap:8px;padding:6px 12px;font-size:.875rem;font-weight:600;display:flex}.console-panel-toggle:hover{background:#1a2029}.console-panel-action{padding:6px 12px}.console-panel-body{padding:12px}.console-markdown{word-break:break-word;font-size:.875rem;line-height:1.5}.console-markdown ul{margin:.5em 0;padding-left:1.5em;list-style:outside}.console-markdown ol{margin:.5em 0;padding-left:1.5em;list-style:decimal}.console-markdown li{margin:.25em 0}.console-markdown table{border-collapse:collapse;margin:.5em 0}.console-markdown th,.console-markdown td{border:1px solid #30363d;padding:4px 8px}.console-markdown th{background:#161b22}.console-markdown h1,.console-markdown h2,.console-markdown h3,.console-markdown h4,.console-markdown h5,.console-markdown h6{margin:1em 0 .5em;font-weight:700;line-height:1.25}.console-markdown h1{font-size:1.6em}.console-markdown h2{font-size:1.4em}.console-markdown h3{font-size:1.2em}.console-markdown h4{font-size:1.05em}.console-markdown h5{font-size:.95em}.console-markdown h6{color:#8b949e;font-size:.9em}.console-markdown pre{background:#161b22;border:1px solid #30363d;border-radius:6px;margin:.5em 0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8125rem;line-height:1.45;overflow-x:auto}.console-markdown code{background:#6e768166;border-radius:4px;padding:.15em .4em;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}.console-markdown pre code{font-size:inherit;background:0 0;border-radius:0;padding:0}.console-markdown blockquote{color:#8b949e;border-left:3px solid #30363d;margin:.5em 0;padding:0 1em}.console-markdown a{color:#4493f8;text-decoration:none}.console-markdown a:hover{text-decoration:underline}.console-markdown-reference-host{display:inline}.console-markdown-reference{vertical-align:baseline;align-items:center;gap:.25em;display:inline-flex}.console-markdown-reference .console-item-icon{flex-shrink:0}.console-markdown-reference-title{text-decoration:none}.console-markdown-reference:hover .console-markdown-reference-title{text-decoration:underline}.console-markdown hr{border:none;border-top:1px solid #30363d;margin:1em 0}.console-mermaid-error{color:#f85149;font-size:.8125rem}.console-comment{background:#161b22;border:1px solid #30363d;border-radius:8px;margin-bottom:10px;padding:12px 16px}.console-comment-header{color:#8b949e;gap:8px;font-size:.75rem;display:flex}.console-comment-show-all{color:#e6edf3;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;margin-bottom:10px;padding:6px 12px;font-size:.75rem;font-weight:600}.console-comment-show-all:hover{background-color:#262c34;border-color:#484f58}.console-comment-author{color:#e6edf3;font-weight:600}.console-files,.console-commits{margin:0;padding:0;list-style:none}.console-commit{align-items:center;gap:8px;padding:4px 0;font-size:.8125rem;display:flex}.console-file{flex-direction:column;font-size:.8125rem;display:flex}.console-file-tree,.console-file-tree-children{margin:0;padding:0;list-style:none}.console-file-tree-dir{flex-direction:column;display:flex}.console-file-tree-dir-name{color:#8b949e;align-items:center;gap:6px;padding:4px 0;font-weight:600;display:flex}.console-file-tree-dir-icon{flex:none}.console-file-badge{text-align:center;border:1px solid;border-radius:4px;width:18px;font-size:.6875rem}.console-file-path,.console-commit-message{flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-add,.console-pr-add{color:#3fb950}.console-file-del,.console-pr-del{color:#f85149}.console-commit-sha{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-actionbar{z-index:100;padding:10px 16px;padding-bottom:calc(10px + env(safe-area-inset-bottom));background:#161b22;border-top:2px solid #30363d;margin-top:auto;position:sticky;bottom:0}.console-operation-bar{flex-direction:column;gap:8px;width:100%;display:flex}.console-op-group{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.console-op-group-review{gap:10px}.console-op-group-stories{max-height:33vh;overflow-y:auto}.console-op-button{color:#e6edf3;white-space:nowrap;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:9px 16px;font-size:.8125rem;font-weight:600}.console-op-button:hover{border-color:#484f58}.console-op-button-approve{color:#fff;background:#238636;border-color:#2ea043}.console-op-button-reject{color:#ffd166;background:#7d5000;border-color:#a06800}.console-op-button-wrong{color:#f85149;background:#3a1518;border-color:#f85149}.console-op-button-unneeded{color:#aab0b8;background:#2a2d31;border-color:#6e7681}.console-op-button-snooze{color:#79c0ff;background:#1c2b4a;border-color:#4493f8}.console-pr-section{border:1px solid #30363d;border-radius:8px;flex-direction:column;gap:10px;padding:12px;display:flex}.console-pr-statbar{color:#8b949e;gap:12px;font-size:.75rem;display:flex}.console-detail-screen{flex-direction:column;flex:1 0 auto;display:flex}.console-panel-open-link{color:#4493f8;font-size:.8125rem;font-weight:400}.console-composer{margin-top:4px}.console-composer-toggle{color:#8b949e;cursor:pointer;background:0 0;border:none;padding:2px 0;font-size:.7813rem}.console-composer-toggle:hover{color:#e6edf3}.console-composer-posted{flex-direction:column;gap:8px;margin-top:8px;display:flex}.console-composer-form{margin-top:8px}.console-composer-input{box-sizing:border-box;color:#e6edf3;width:100%;font:inherit;resize:vertical;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:8px;font-size:.875rem}.console-composer-row{justify-content:flex-end;align-items:center;gap:8px;margin-top:6px;display:flex}.console-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #2ea043;border-radius:6px;padding:7px 16px;font-size:.8125rem;font-weight:600}.console-composer-submit:disabled{opacity:.6;cursor:default}.console-composer-status{color:#8b949e;font-size:.75rem}.console-composer-error{color:#f85149}.console-undo-toast{z-index:9998;white-space:nowrap;border-radius:10px;align-items:center;gap:12px;max-width:92vw;padding:12px 18px;font-size:.8438rem;display:flex;position:fixed;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 20px #0009}.console-undo-toast-green{color:#3fb950;background:#0d3320;border:1px solid #2ea043}.console-undo-toast-amber{color:#ffd166;background:#3a2800;border:1px solid #a06800}.console-undo-toast-red{color:#f85149;background:#3a1518;border:1px solid #da3633}.console-undo-toast-gray{color:#aab0b8;background:#2a2d31;border:1px solid #6e7681}.console-undo-toast-blue{color:#79c0ff;background:#0d1f40;border:1px solid #4493f8}.console-undo-toast-error{color:#f85149;background:#3a1518;border:1px solid #f85149}.console-undo-toast-message{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.console-undo-toast-undo{cursor:pointer;color:inherit;background:#ffffff1f;border:none;border-radius:6px;flex:none;padding:4px 12px;font-size:.8125rem;font-weight:700}.console-undo-toast-undo:hover{background:#ffffff38}.console-undo-toast-countdown{opacity:.7;flex:none;font-size:.6875rem}.console-undo-toast-bar{opacity:.5;background:currentColor;border-radius:0 0 10px 10px;height:3px;transition:width .1s linear;position:absolute;bottom:0;left:0}.console-file-row{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:4px 0;font-size:.8125rem;display:flex}.console-file-row:hover{background:#1a2029}.console-file-caret{color:#8b949e;flex:none;width:1em}.console-file-diff{border-collapse:collapse;width:100%;margin:4px 0 8px;font:.8125rem/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-diff td{white-space:pre-wrap;word-break:break-word;vertical-align:top;padding:0 10px}.console-diff-ln{text-align:right;color:#6e7681;-webkit-user-select:none;user-select:none;white-space:nowrap;border-right:1px solid #30363d;width:1%;padding:0 8px}.console-file-diff td.console-diff-ln{white-space:nowrap;word-break:normal;overflow-wrap:normal}.console-diff-add td{background:#2ea04326}.console-diff-add .console-diff-code{color:#aff5b4}.console-diff-del td{background:#f8514926}.console-diff-del .console-diff-code{color:#ffdcd7}.console-diff-hunk td{color:#79c0ff;background:#161b22}.console-diff-ctx .console-diff-code{color:#c9d1d9}.console-file-diff-empty{color:#8b949e;padding:8px 14px;font-size:.75rem}.console-diff-comment-cell{text-align:center;-webkit-user-select:none;user-select:none;width:1%;padding:0 2px}.console-diff-comment-button{color:#6e7681;cursor:pointer;opacity:.6;background:0 0;border:1px solid #30363d;border-radius:4px;width:18px;height:18px;padding:0;font-size:.875rem;line-height:16px}.console-diff-row:hover .console-diff-comment-button,.console-diff-comment-button:focus-visible,.console-diff-comment-button[aria-expanded=true]{opacity:1;color:#fff;background:#1f6feb;border-color:#2f81f7}.console-diff-composer-row td{background:#0d1117;padding:6px 10px}.console-diff-composer{background:#161b22;border:1px solid #30363d;border-radius:6px;flex-direction:column;gap:6px;padding:8px;display:flex}.console-diff-composer-anchor{color:#8b949e;font-size:.6875rem}.console-diff-composer-input{resize:vertical;color:#c9d1d9;width:100%;font:inherit;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:6px 8px}.console-diff-composer-controls{align-items:center;gap:8px;display:flex}.console-diff-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #238636;border-radius:6px;padding:4px 12px}.console-diff-composer-submit:disabled{opacity:.6;cursor:default}.console-diff-composer-cancel{color:#c9d1d9;cursor:pointer;background:0 0;border:1px solid #30363d;border-radius:6px;padding:4px 12px}.console-diff-composer-status{color:#8b949e;font-size:.75rem}.console-diff-composer-error{color:#ff7b72}.console-diff-composer-posted{color:#3fb950;margin:0;font-size:.75rem}.console-pr-header{flex-wrap:wrap;align-items:center;gap:10px;padding-top:4px;display:flex}.console-pr-section-title{color:#e6edf3;font-size:.9375rem;font-weight:600}.console-pr-section-state{color:#8b949e;border:1px solid #6e7681;border-radius:999px;padding:1px 8px;font-size:.6875rem}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-2c5E3tdu.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BaGbcbYI.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
|
+
import { google } from 'googleapis';
|
|
2
3
|
import { GoogleSpreadsheetRepository } from './GoogleSpreadsheetRepository';
|
|
3
4
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
4
5
|
|
|
@@ -6,6 +7,24 @@ dotenv.config();
|
|
|
6
7
|
|
|
7
8
|
const GOOGLE_SERVICE_ACCOUNT_KEY = process.env.GOOGLE_SERVICE_ACCOUNT_KEY;
|
|
8
9
|
|
|
10
|
+
const integrationTestIsRequired =
|
|
11
|
+
process.env.GOOGLE_SHEETS_INTEGRATION_REQUIRED === 'true';
|
|
12
|
+
|
|
13
|
+
const credentialIsMissingWhileRequired =
|
|
14
|
+
integrationTestIsRequired && !GOOGLE_SERVICE_ACCOUNT_KEY;
|
|
15
|
+
|
|
16
|
+
describe('GoogleSpreadsheetRepository integration test credential', () => {
|
|
17
|
+
test('is available whenever these integration tests are required to run', () => {
|
|
18
|
+
expect(
|
|
19
|
+
credentialIsMissingWhileRequired
|
|
20
|
+
? 'GOOGLE_SERVICE_ACCOUNT_KEY is absent while GOOGLE_SHEETS_INTEGRATION_REQUIRED is true, so the Google Sheets integration tests would silently skip and prove nothing'
|
|
21
|
+
: 'the Google Sheets integration tests either hold their credential or are not required in this context',
|
|
22
|
+
).toBe(
|
|
23
|
+
'the Google Sheets integration tests either hold their credential or are not required in this context',
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
9
28
|
const describeWhenCredentials = GOOGLE_SERVICE_ACCOUNT_KEY
|
|
10
29
|
? describe
|
|
11
30
|
: describe.skip;
|
|
@@ -15,17 +34,101 @@ describeWhenCredentials('GoogleSpreadsheetRepository integration tests', () => {
|
|
|
15
34
|
jest.retryTimes(3, { logErrorsBeforeRetry: true });
|
|
16
35
|
|
|
17
36
|
const localStorageRepository = new LocalStorageRepository();
|
|
18
|
-
const
|
|
19
|
-
|
|
37
|
+
const spreadsheetId = '1N_3y0y46v5tHbra5YSm6PldflcsF1bkfeWDdQ3MRuXM';
|
|
38
|
+
const spreadsheetUrl = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit?gid=0#gid=0`;
|
|
20
39
|
const repository = new GoogleSpreadsheetRepository(
|
|
21
40
|
localStorageRepository,
|
|
22
41
|
GOOGLE_SERVICE_ACCOUNT_KEY,
|
|
23
42
|
);
|
|
24
43
|
|
|
44
|
+
const scratchSheetNamePrefix = 'IntegrationScratch_';
|
|
45
|
+
const scratchSheetName = `${scratchSheetNamePrefix}${Date.now()}_${process.pid}`;
|
|
46
|
+
const abandonedScratchSheetAgeMilliseconds = 2 * 60 * 60 * 1000;
|
|
47
|
+
const rawSheetsClientKeyFile =
|
|
48
|
+
'./tmp/integration-test-service-account-key.json';
|
|
49
|
+
|
|
50
|
+
const createRawSheetsClient = () => {
|
|
51
|
+
if (!GOOGLE_SERVICE_ACCOUNT_KEY) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'GOOGLE_SERVICE_ACCOUNT_KEY is required to run the Google Sheets integration tests',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
localStorageRepository.write(
|
|
57
|
+
rawSheetsClientKeyFile,
|
|
58
|
+
GOOGLE_SERVICE_ACCOUNT_KEY,
|
|
59
|
+
);
|
|
60
|
+
const auth = new google.auth.GoogleAuth({
|
|
61
|
+
keyFile: rawSheetsClientKeyFile,
|
|
62
|
+
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
63
|
+
});
|
|
64
|
+
return google.sheets({ version: 'v4', auth });
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const listSheetTitles = async (): Promise<string[]> => {
|
|
68
|
+
const response = await createRawSheetsClient().spreadsheets.get({
|
|
69
|
+
spreadsheetId,
|
|
70
|
+
});
|
|
71
|
+
return (response.data.sheets ?? [])
|
|
72
|
+
.map((sheet) => sheet.properties?.title)
|
|
73
|
+
.filter((title): title is string => typeof title === 'string');
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const deleteSheetsByTitle = async (titles: string[]): Promise<void> => {
|
|
77
|
+
if (titles.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const sheetsClient = createRawSheetsClient();
|
|
81
|
+
const response = await sheetsClient.spreadsheets.get({ spreadsheetId });
|
|
82
|
+
const sheetIds = (response.data.sheets ?? [])
|
|
83
|
+
.filter((sheet) => {
|
|
84
|
+
const title = sheet.properties?.title;
|
|
85
|
+
return typeof title === 'string' && titles.includes(title);
|
|
86
|
+
})
|
|
87
|
+
.map((sheet) => sheet.properties?.sheetId)
|
|
88
|
+
.filter((sheetId): sheetId is number => typeof sheetId === 'number');
|
|
89
|
+
if (sheetIds.length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await sheetsClient.spreadsheets.batchUpdate({
|
|
93
|
+
spreadsheetId,
|
|
94
|
+
requestBody: {
|
|
95
|
+
requests: sheetIds.map((sheetId) => ({ deleteSheet: { sheetId } })),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const findAbandonedScratchSheetTitles = async (): Promise<string[]> => {
|
|
101
|
+
const titles = await listSheetTitles();
|
|
102
|
+
return titles
|
|
103
|
+
.filter((title) => title.startsWith(scratchSheetNamePrefix))
|
|
104
|
+
.filter((title) => title !== scratchSheetName)
|
|
105
|
+
.filter((title) => {
|
|
106
|
+
const createdAtMilliseconds = Number(
|
|
107
|
+
title.slice(scratchSheetNamePrefix.length).split('_')[0],
|
|
108
|
+
);
|
|
109
|
+
return (
|
|
110
|
+
Number.isFinite(createdAtMilliseconds) &&
|
|
111
|
+
Date.now() - createdAtMilliseconds >
|
|
112
|
+
abandonedScratchSheetAgeMilliseconds
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
25
117
|
beforeEach(async () => {
|
|
26
118
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
27
119
|
});
|
|
28
120
|
|
|
121
|
+
afterAll(async () => {
|
|
122
|
+
const abandonedTitles = await findAbandonedScratchSheetTitles();
|
|
123
|
+
await deleteSheetsByTitle([scratchSheetName, ...abandonedTitles]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('getSpreadsheetId', () => {
|
|
127
|
+
test('extracts the spreadsheet id from the spreadsheet url', () => {
|
|
128
|
+
expect(repository.getSpreadsheetId(spreadsheetUrl)).toBe(spreadsheetId);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
29
132
|
describe('getSheet', () => {
|
|
30
133
|
const testCases: [string, string[][] | null][] = [
|
|
31
134
|
['SheetUndefined', null],
|
|
@@ -56,65 +159,96 @@ describeWhenCredentials('GoogleSpreadsheetRepository integration tests', () => {
|
|
|
56
159
|
});
|
|
57
160
|
});
|
|
58
161
|
|
|
59
|
-
describe('
|
|
60
|
-
|
|
61
|
-
[
|
|
62
|
-
|
|
63
|
-
['Sheet1', 1, 1, '123'],
|
|
64
|
-
['Sheet1', 2, 2, 'Test'],
|
|
65
|
-
];
|
|
162
|
+
describe('createNewSheetIfNotExists', () => {
|
|
163
|
+
test('creates the sheet and stays idempotent on a second call', async () => {
|
|
164
|
+
await deleteSheetsByTitle([scratchSheetName]);
|
|
165
|
+
expect(await listSheetTitles()).not.toContain(scratchSheetName);
|
|
66
166
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
167
|
+
await repository.createNewSheetIfNotExists(
|
|
168
|
+
spreadsheetUrl,
|
|
169
|
+
scratchSheetName,
|
|
170
|
+
);
|
|
171
|
+
expect(await listSheetTitles()).toContain(scratchSheetName);
|
|
172
|
+
expect(
|
|
173
|
+
await repository.getSheet(spreadsheetUrl, scratchSheetName),
|
|
174
|
+
).toBeNull();
|
|
175
|
+
|
|
176
|
+
await repository.createNewSheetIfNotExists(
|
|
177
|
+
spreadsheetUrl,
|
|
178
|
+
scratchSheetName,
|
|
179
|
+
);
|
|
180
|
+
const titlesAfterSecondCall = await listSheetTitles();
|
|
181
|
+
expect(
|
|
182
|
+
titlesAfterSecondCall.filter((title) => title === scratchSheetName)
|
|
183
|
+
.length,
|
|
184
|
+
).toBe(1);
|
|
185
|
+
});
|
|
78
186
|
});
|
|
79
187
|
|
|
80
188
|
describe('appendSheetValues', () => {
|
|
81
|
-
|
|
82
|
-
[
|
|
83
|
-
|
|
84
|
-
[
|
|
85
|
-
[
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
]
|
|
90
|
-
|
|
189
|
+
test('appends rows to a sheet it creates and keeps the earlier rows', async () => {
|
|
190
|
+
await deleteSheetsByTitle([scratchSheetName]);
|
|
191
|
+
|
|
192
|
+
await repository.appendSheetValues(spreadsheetUrl, scratchSheetName, [
|
|
193
|
+
['firstRowFirstColumn', 'firstRowSecondColumn'],
|
|
194
|
+
]);
|
|
195
|
+
expect(
|
|
196
|
+
await repository.getSheet(spreadsheetUrl, scratchSheetName),
|
|
197
|
+
).toEqual([['firstRowFirstColumn', 'firstRowSecondColumn']]);
|
|
198
|
+
|
|
199
|
+
await repository.appendSheetValues(spreadsheetUrl, scratchSheetName, [
|
|
200
|
+
['secondRowFirstColumn', 'secondRowSecondColumn'],
|
|
201
|
+
['thirdRowFirstColumn', 'thirdRowSecondColumn'],
|
|
202
|
+
]);
|
|
203
|
+
expect(
|
|
204
|
+
await repository.getSheet(spreadsheetUrl, scratchSheetName),
|
|
205
|
+
).toEqual([
|
|
206
|
+
['firstRowFirstColumn', 'firstRowSecondColumn'],
|
|
207
|
+
['secondRowFirstColumn', 'secondRowSecondColumn'],
|
|
208
|
+
['thirdRowFirstColumn', 'thirdRowSecondColumn'],
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
91
212
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const sheetName = 'AppendTest';
|
|
96
|
-
const initialSheet = await repository.getSheet(
|
|
97
|
-
spreadsheetUrl,
|
|
98
|
-
sheetName,
|
|
99
|
-
);
|
|
100
|
-
const initialLength = initialSheet ? initialSheet.length : 0;
|
|
213
|
+
describe('updateCell', () => {
|
|
214
|
+
test('creates the sheet when missing, writes a cell, and overwrites it', async () => {
|
|
215
|
+
await deleteSheetsByTitle([scratchSheetName]);
|
|
101
216
|
|
|
102
|
-
|
|
217
|
+
await repository.updateCell(
|
|
218
|
+
spreadsheetUrl,
|
|
219
|
+
scratchSheetName,
|
|
220
|
+
0,
|
|
221
|
+
0,
|
|
222
|
+
'firstValue',
|
|
223
|
+
);
|
|
224
|
+
expect(
|
|
225
|
+
await repository.getSheet(spreadsheetUrl, scratchSheetName),
|
|
226
|
+
).toEqual([['firstValue']]);
|
|
103
227
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
228
|
+
await repository.updateCell(
|
|
229
|
+
spreadsheetUrl,
|
|
230
|
+
scratchSheetName,
|
|
231
|
+
0,
|
|
232
|
+
0,
|
|
233
|
+
'updatedValue',
|
|
234
|
+
);
|
|
235
|
+
expect(
|
|
236
|
+
await repository.getSheet(spreadsheetUrl, scratchSheetName),
|
|
237
|
+
).toEqual([['updatedValue']]);
|
|
238
|
+
|
|
239
|
+
await repository.updateCell(
|
|
240
|
+
spreadsheetUrl,
|
|
241
|
+
scratchSheetName,
|
|
242
|
+
2,
|
|
243
|
+
2,
|
|
244
|
+
'thirdRowThirdColumn',
|
|
245
|
+
);
|
|
246
|
+
const sheet = await repository.getSheet(spreadsheetUrl, scratchSheetName);
|
|
247
|
+
if (!sheet) {
|
|
248
|
+
throw new Error('Sheet not found');
|
|
249
|
+
}
|
|
250
|
+
expect(sheet[0][0]).toBe('updatedValue');
|
|
251
|
+
expect(sheet[2][2]).toBe('thirdRowThirdColumn');
|
|
252
|
+
});
|
|
119
253
|
});
|
|
120
254
|
});
|
|
@@ -23,11 +23,43 @@ describe('GoogleSpreadsheetRepository', () => {
|
|
|
23
23
|
const spreadsheetUrl =
|
|
24
24
|
'https://docs.google.com/spreadsheets/d/1N_3y0y46v5tHbra5YSm6PldflcsF1bkfeWDdQ3MRuXM/edit?gid=0#gid=0';
|
|
25
25
|
|
|
26
|
-
const mockSpreadsheetsGet =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
26
|
+
const mockSpreadsheetsGet =
|
|
27
|
+
jest.fn<
|
|
28
|
+
(params: { spreadsheetId: string }) => Promise<SpreadsheetGetResponse>
|
|
29
|
+
>();
|
|
30
|
+
const mockSpreadsheetsValuesGet =
|
|
31
|
+
jest.fn<
|
|
32
|
+
(params: {
|
|
33
|
+
spreadsheetId: string;
|
|
34
|
+
range: string;
|
|
35
|
+
}) => Promise<ValuesGetResponse>
|
|
36
|
+
>();
|
|
37
|
+
const mockSpreadsheetsValuesUpdate =
|
|
38
|
+
jest.fn<
|
|
39
|
+
(params: {
|
|
40
|
+
spreadsheetId: string;
|
|
41
|
+
range: string;
|
|
42
|
+
valueInputOption: string;
|
|
43
|
+
requestBody: { values: string[][] };
|
|
44
|
+
}) => Promise<SimpleResponse>
|
|
45
|
+
>();
|
|
46
|
+
const mockSpreadsheetsValuesAppend =
|
|
47
|
+
jest.fn<
|
|
48
|
+
(params: {
|
|
49
|
+
spreadsheetId: string;
|
|
50
|
+
range: string;
|
|
51
|
+
valueInputOption: string;
|
|
52
|
+
requestBody: { values: string[][] };
|
|
53
|
+
}) => Promise<SimpleResponse>
|
|
54
|
+
>();
|
|
55
|
+
const mockSpreadsheetsBatchUpdate = jest.fn<
|
|
56
|
+
(params: {
|
|
57
|
+
spreadsheetId: string;
|
|
58
|
+
requestBody: {
|
|
59
|
+
requests: Array<{ addSheet?: { properties?: { title?: string } } }>;
|
|
60
|
+
};
|
|
61
|
+
}) => Promise<SimpleResponse>
|
|
62
|
+
>();
|
|
31
63
|
|
|
32
64
|
const mockSheetsClient = {
|
|
33
65
|
spreadsheets: {
|
|
@@ -289,6 +321,32 @@ describe('GoogleSpreadsheetRepository', () => {
|
|
|
289
321
|
);
|
|
290
322
|
});
|
|
291
323
|
|
|
324
|
+
test('does not recreate an existing sheet that holds no values', async () => {
|
|
325
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
326
|
+
status: 200,
|
|
327
|
+
data: { sheets: [{ properties: { title: 'EmptyExistingSheet' } }] },
|
|
328
|
+
});
|
|
329
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
330
|
+
status: 200,
|
|
331
|
+
data: {},
|
|
332
|
+
});
|
|
333
|
+
mockSpreadsheetsValuesAppend.mockResolvedValue({
|
|
334
|
+
status: 200,
|
|
335
|
+
data: {},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await repository.appendSheetValues(spreadsheetUrl, 'EmptyExistingSheet', [
|
|
339
|
+
['Row'],
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
expect(mockSpreadsheetsBatchUpdate).not.toHaveBeenCalled();
|
|
343
|
+
expect(mockSpreadsheetsValuesAppend).toHaveBeenCalledWith(
|
|
344
|
+
expect.objectContaining({
|
|
345
|
+
range: 'EmptyExistingSheet!A1:A',
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
292
350
|
test('creates new sheet via batchUpdate when sheet does not exist', async () => {
|
|
293
351
|
mockSpreadsheetsGet.mockResolvedValue({
|
|
294
352
|
status: 200,
|
|
@@ -155,14 +155,29 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
155
155
|
);
|
|
156
156
|
}
|
|
157
157
|
};
|
|
158
|
+
private sheetExists = async (
|
|
159
|
+
spreadsheetUrl: string,
|
|
160
|
+
sheetName: string,
|
|
161
|
+
): Promise<boolean> => {
|
|
162
|
+
const sheets = this.sheetsClient;
|
|
163
|
+
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
164
|
+
const response = await sheets.spreadsheets.get({ spreadsheetId });
|
|
165
|
+
if (response.status !== 200) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Failed to get sheet: ${response.status}. ${JSON.stringify(response.data)}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return (response.data.sheets ?? []).some(
|
|
171
|
+
(sheet) => sheet.properties?.title === sheetName,
|
|
172
|
+
);
|
|
173
|
+
};
|
|
158
174
|
createNewSheetIfNotExists = async (
|
|
159
175
|
spreadsheetUrl: string,
|
|
160
176
|
sheetName: string,
|
|
161
177
|
): Promise<void> => {
|
|
162
178
|
const sheets = this.sheetsClient;
|
|
163
179
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
164
|
-
|
|
165
|
-
if (sheet !== null) {
|
|
180
|
+
if (await this.sheetExists(spreadsheetUrl, sheetName)) {
|
|
166
181
|
return;
|
|
167
182
|
}
|
|
168
183
|
const response = await sheets.spreadsheets.batchUpdate({
|
|
@@ -132,7 +132,7 @@ describe('LocalStorageRepository', () => {
|
|
|
132
132
|
testCases.forEach(({ name, params }) => {
|
|
133
133
|
test(name, () => {
|
|
134
134
|
if (fs.existsSync(params[0])) {
|
|
135
|
-
fs.
|
|
135
|
+
fs.rmSync(params[0], { recursive: true });
|
|
136
136
|
}
|
|
137
137
|
repository.mkdir(...params);
|
|
138
138
|
expect(fs.existsSync(params[0])).toBe(true);
|
|
@@ -2,6 +2,9 @@ import {
|
|
|
2
2
|
ComposeDashboardInput,
|
|
3
3
|
ComposeDashboardUseCase,
|
|
4
4
|
PROJECT_ROW_WIDTH_BUDGET,
|
|
5
|
+
SEVEN_DAY_UTILIZATION_COLUMN_START,
|
|
6
|
+
STATUS_DOT_DISPLAY_WIDTH,
|
|
7
|
+
TOKEN_UTILIZATION_WIDTH,
|
|
5
8
|
formatMachineStatusLines,
|
|
6
9
|
formatProjectHeaderLine,
|
|
7
10
|
formatProjectRowLine,
|
|
@@ -11,7 +14,7 @@ import {
|
|
|
11
14
|
roundHalfToEven,
|
|
12
15
|
} from './ComposeDashboardUseCase';
|
|
13
16
|
import { DashboardRow } from './GenerateDashboardRowUseCase';
|
|
14
|
-
import { TokenStatus } from './GenerateTokenStatusUseCase';
|
|
17
|
+
import { TokenStatus, TokenStatusColor } from './GenerateTokenStatusUseCase';
|
|
15
18
|
|
|
16
19
|
const codePointLength = (value: string): number => [...value].length;
|
|
17
20
|
|
|
@@ -487,56 +490,63 @@ describe('ComposeDashboardUseCase', () => {
|
|
|
487
490
|
});
|
|
488
491
|
});
|
|
489
492
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
+
const inDisplayColumns = (tokenRowLine: string): string =>
|
|
494
|
+
' '.repeat(STATUS_DOT_DISPLAY_WIDTH) + [...tokenRowLine].slice(1).join('');
|
|
495
|
+
|
|
496
|
+
const sevenDayUtilizationColumn = (line: string): number =>
|
|
497
|
+
[...line].lastIndexOf('%') + 1 - TOKEN_UTILIZATION_WIDTH;
|
|
493
498
|
|
|
494
|
-
|
|
499
|
+
describe('formatSevenDayWindowAggregateLine', () => {
|
|
500
|
+
it('renders only the used percentage, with no label and no count when every token is included', () => {
|
|
495
501
|
expect(
|
|
496
502
|
formatSevenDayWindowAggregateLine({
|
|
497
503
|
usedPercent: 63,
|
|
498
504
|
includedTokenCount: 10,
|
|
499
505
|
totalTokenCount: 10,
|
|
500
506
|
}),
|
|
501
|
-
).toBe('
|
|
507
|
+
).toBe(' '.repeat(21) + '63%');
|
|
502
508
|
});
|
|
503
509
|
|
|
504
|
-
it('
|
|
505
|
-
const tokenRow = formatTokenRowLine(
|
|
506
|
-
tokenStatus({
|
|
507
|
-
name: 'dev4',
|
|
508
|
-
color: 'K',
|
|
509
|
-
fiveHourUtilizationPercent: 0,
|
|
510
|
-
fiveHourResetSeconds: 60,
|
|
511
|
-
sevenDayUtilizationPercent: 100,
|
|
512
|
-
sevenDayResetSeconds: 96060,
|
|
513
|
-
}),
|
|
514
|
-
);
|
|
510
|
+
it('places the used percentage in the same display column as the seven day utilization of every token row', () => {
|
|
515
511
|
const aggregateLine = formatSevenDayWindowAggregateLine({
|
|
516
512
|
usedPercent: 63.36,
|
|
517
513
|
includedTokenCount: 11,
|
|
518
514
|
totalTokenCount: 11,
|
|
519
515
|
});
|
|
520
|
-
expect(
|
|
521
|
-
|
|
522
|
-
expect(columnEnd(aggregateLine ?? '', '37%')).toBe(
|
|
523
|
-
columnEnd(tokenRow, '100%'),
|
|
516
|
+
expect(sevenDayUtilizationColumn(aggregateLine ?? '')).toBe(
|
|
517
|
+
SEVEN_DAY_UTILIZATION_COLUMN_START,
|
|
524
518
|
);
|
|
519
|
+
const colors: TokenStatusColor[] = ['G', 'Y', 'K'];
|
|
520
|
+
for (const color of colors) {
|
|
521
|
+
const tokenRow = formatTokenRowLine(
|
|
522
|
+
tokenStatus({
|
|
523
|
+
name: 'dev4',
|
|
524
|
+
color,
|
|
525
|
+
fiveHourUtilizationPercent: 0,
|
|
526
|
+
fiveHourResetSeconds: 60,
|
|
527
|
+
sevenDayUtilizationPercent: 100,
|
|
528
|
+
sevenDayResetSeconds: 96060,
|
|
529
|
+
}),
|
|
530
|
+
);
|
|
531
|
+
expect(sevenDayUtilizationColumn(inDisplayColumns(tokenRow))).toBe(
|
|
532
|
+
SEVEN_DAY_UTILIZATION_COLUMN_START,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
525
535
|
});
|
|
526
536
|
|
|
527
|
-
it('keeps the alignment when the
|
|
537
|
+
it('keeps the alignment when the used percentage needs fewer digits', () => {
|
|
528
538
|
const wide = formatSevenDayWindowAggregateLine({
|
|
529
|
-
usedPercent:
|
|
539
|
+
usedPercent: 100,
|
|
530
540
|
includedTokenCount: 1,
|
|
531
541
|
totalTokenCount: 1,
|
|
532
542
|
});
|
|
533
543
|
const narrow = formatSevenDayWindowAggregateLine({
|
|
534
|
-
usedPercent:
|
|
544
|
+
usedPercent: 5,
|
|
535
545
|
includedTokenCount: 1,
|
|
536
546
|
totalTokenCount: 1,
|
|
537
547
|
});
|
|
538
|
-
expect(wide).toBe('
|
|
539
|
-
expect(narrow).toBe('
|
|
548
|
+
expect(wide).toBe(' '.repeat(20) + '100%');
|
|
549
|
+
expect(narrow).toBe(' '.repeat(22) + '5%');
|
|
540
550
|
expect(codePointLength(wide ?? '')).toBe(codePointLength(narrow ?? ''));
|
|
541
551
|
});
|
|
542
552
|
|
|
@@ -547,24 +557,24 @@ describe('formatSevenDayWindowAggregateLine', () => {
|
|
|
547
557
|
includedTokenCount: 9,
|
|
548
558
|
totalTokenCount: 10,
|
|
549
559
|
}),
|
|
550
|
-
).toBe('
|
|
560
|
+
).toBe(' '.repeat(21) + '63% (9)');
|
|
551
561
|
});
|
|
552
562
|
|
|
553
|
-
it('rounds the
|
|
563
|
+
it('rounds the used percentage with half-to-even before rendering', () => {
|
|
554
564
|
expect(
|
|
555
565
|
formatSevenDayWindowAggregateLine({
|
|
556
566
|
usedPercent: 62.5,
|
|
557
567
|
includedTokenCount: 2,
|
|
558
568
|
totalTokenCount: 2,
|
|
559
569
|
}),
|
|
560
|
-
).toBe('
|
|
570
|
+
).toBe(' '.repeat(21) + '62%');
|
|
561
571
|
expect(
|
|
562
572
|
formatSevenDayWindowAggregateLine({
|
|
563
573
|
usedPercent: 63.5,
|
|
564
574
|
includedTokenCount: 2,
|
|
565
575
|
totalTokenCount: 2,
|
|
566
576
|
}),
|
|
567
|
-
).toBe('
|
|
577
|
+
).toBe(' '.repeat(21) + '64%');
|
|
568
578
|
});
|
|
569
579
|
|
|
570
580
|
it('renders nothing when the aggregate is absent', () => {
|
|
@@ -573,11 +583,11 @@ describe('formatSevenDayWindowAggregateLine', () => {
|
|
|
573
583
|
|
|
574
584
|
it('fits the width budget at the widest rendering', () => {
|
|
575
585
|
const line = formatSevenDayWindowAggregateLine({
|
|
576
|
-
usedPercent:
|
|
586
|
+
usedPercent: 100,
|
|
577
587
|
includedTokenCount: 999,
|
|
578
588
|
totalTokenCount: 1000,
|
|
579
589
|
});
|
|
580
|
-
expect(line).toBe('
|
|
590
|
+
expect(line).toBe(' '.repeat(20) + '100% (999)');
|
|
581
591
|
expect(codePointLength(line ?? '')).toBeLessThanOrEqual(
|
|
582
592
|
PROJECT_ROW_WIDTH_BUDGET,
|
|
583
593
|
);
|
|
@@ -618,7 +628,7 @@ describe('ComposeDashboardUseCase seven day window aggregate line', () => {
|
|
|
618
628
|
}),
|
|
619
629
|
),
|
|
620
630
|
);
|
|
621
|
-
const aggregateIndex = lines.indexOf('
|
|
631
|
+
const aggregateIndex = lines.indexOf(' '.repeat(21) + '63%');
|
|
622
632
|
expect(aggregateIndex).toBeGreaterThan(-1);
|
|
623
633
|
expect(lines[aggregateIndex - 1]).toBe(
|
|
624
634
|
formatProjectRowLine({
|
|
@@ -629,11 +639,13 @@ describe('ComposeDashboardUseCase seven day window aggregate line', () => {
|
|
|
629
639
|
expect(lines[aggregateIndex + 1]).toContain('alice');
|
|
630
640
|
});
|
|
631
641
|
|
|
642
|
+
const AGGREGATE_LINE_SHAPE = /^ +\d+%( \(\d+\))?$/;
|
|
643
|
+
|
|
632
644
|
it('keeps the blank separator line when the aggregate is absent', () => {
|
|
633
645
|
const lines = unwrappedLines(
|
|
634
646
|
new ComposeDashboardUseCase().run(aggregateInput(null)),
|
|
635
647
|
);
|
|
636
|
-
expect(lines.some((line) =>
|
|
648
|
+
expect(lines.some((line) => AGGREGATE_LINE_SHAPE.test(line))).toBe(false);
|
|
637
649
|
expect(lines).toContain('');
|
|
638
650
|
});
|
|
639
651
|
|
|
@@ -645,7 +657,7 @@ describe('ComposeDashboardUseCase seven day window aggregate line', () => {
|
|
|
645
657
|
tokens: [tokenStatus({ name: 'alice' })],
|
|
646
658
|
}),
|
|
647
659
|
);
|
|
648
|
-
expect(lines.some((line) =>
|
|
660
|
+
expect(lines.some((line) => AGGREGATE_LINE_SHAPE.test(line))).toBe(false);
|
|
649
661
|
expect(lines).toContain('');
|
|
650
662
|
});
|
|
651
663
|
});
|