Skip to content

Instantly share code, notes, and snippets.

@bilalbhojani24
Last active February 25, 2026 01:46
Show Gist options
  • Select an option

  • Save bilalbhojani24/a36a04381d0d75446ab060f46c92c6ce to your computer and use it in GitHub Desktop.

Select an option

Save bilalbhojani24/a36a04381d0d75446ab060f46c92c6ce to your computer and use it in GitHub Desktop.
RFC: Decouple Product Imports from Core — Select Components Migration
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: Arial, sans-serif;
font-size: 11pt;
line-height: 1.5;
max-width: 900px;
margin: 40px auto;
padding: 0 20px;
}
h1 {
font-size: 20pt;
border-bottom: 2px solid #333;
padding-bottom: 8px;
}
h2 {
font-size: 16pt;
border-bottom: 1px solid #ccc;
padding-bottom: 4px;
margin-top: 32px;
}
h3 {
font-size: 13pt;
margin-top: 24px;
}
h4 {
font-size: 11pt;
margin-top: 20px;
}
table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
th,
td {
border: 1px solid #ccc;
padding: 6px 10px;
text-align: left;
font-size: 10pt;
}
th {
background: #f0f0f0;
font-weight: bold;
}
code {
background: #f4f4f4;
padding: 1px 4px;
font-family: 'Courier New', monospace;
font-size: 10pt;
}
pre {
background: #f4f4f4;
padding: 12px;
overflow-x: auto;
font-size: 9pt;
font-family: 'Courier New', monospace;
}
strong {
font-weight: bold;
}
ul,
ol {
margin: 8px 0;
}
li {
margin: 4px 0;
}
.risk-low {
color: #2e7d32;
font-weight: bold;
}
.risk-medium {
color: #ef6c00;
font-weight: bold;
}
.risk-high {
color: #c62828;
font-weight: bold;
}
</style>
</head>
<body>
<h1>RFC: Decouple Product Imports from Core — Select Components Migration</h1>
<h2>Problem</h2>
<p><code>app/core/</code> has product imports (<code>app/products/</code>) in multiple places, violating the module
boundary rule. This RFC focuses on 5 select cell types in Grid plus related imports in <code>GeneratedForm</code>,
<code>dataImport</code>, and <code>inputRenderer</code>.</p>
<h3>Where product imports exist in core today</h3>
<table>
<tr>
<th>Core File</th>
<th>Product Imports</th>
</tr>
<tr>
<td><code>Grid/cellTypes/teamsSelect/TeamsSelectEditor.tsx</code></td>
<td><code>TeamsSelect</code></td>
</tr>
<tr>
<td><code>Grid/cellTypes/departmentSelect/DepartmentSelectEditor.tsx</code></td>
<td><code>DepartmentSelect</code></td>
</tr>
<tr>
<td><code>Grid/cellTypes/levelSelect/LevelSelectEditor.tsx</code></td>
<td><code>LevelSelect</code></td>
</tr>
<tr>
<td><code>Grid/cellTypes/workLocationSelect/WorkLocationSelectEditor.tsx</code></td>
<td><code>LocationSelect</code></td>
</tr>
<tr>
<td><code>Grid/cellTypes/jobFunctionSelect/JobFunctionSelectEditor.tsx</code></td>
<td><code>JobFunctionSelect</code></td>
</tr>
<tr>
<td><code>Grid/cellTypes/jobFunctionSelect/JobFunctionSelect.helpers.ts</code></td>
<td><code>convertJobFunctionsToSelectOptions</code> (already moved to core in prior PR)</td>
</tr>
<tr>
<td><code>generatedForm/GeneratedForm.tsx</code></td>
<td><code>Address</code>, <code>DepartmentSelect</code>, <code>LevelSelect</code>, <code>TeamsSelect</code>,
<code>PositionInput</code>, <code>LocationSelect</code>, <code>ManagerSelect</code>,
<code>LookupFieldSelectInput</code></td>
</tr>
<tr>
<td><code>dataImport/.../columnValuesMapSelect.tsx</code></td>
<td><code>JobFunctionSelect</code>, <code>DepartmentSelect</code>, <code>LevelSelect</code>,
<code>TeamsSelect</code>, <code>WorkLocationSelect</code></td>
</tr>
<tr>
<td><code>dataImport/.../uploadCsv.tsx</code></td>
<td><code>uploadToS3</code></td>
</tr>
<tr>
<td><code>inputRenderer/InputRenderer.constants.ts</code></td>
<td><code>LevelSelectRenderer</code></td>
</tr>
</table>
<p><strong>Goal:</strong> Zero product imports in <code>app/core/</code>. If something is used by more than one
product, it should be maintained centrally in core.</p>
<h2>Cross-Product Usage (why central ownership is correct)</h2>
<table>
<tr>
<th>Cell Type</th>
<th># Products Using</th>
<th>Key Products</th>
</tr>
<tr>
<td><code>DEPARTMENT_SELECT</code></td>
<td><strong>10</strong></td>
<td>Hris/Org, Hris/Emp, HeadcountPlanning, Compensation, Payroll, AccountingIntegrations, Ats, TimeTracking,
Hris/Documents, ReviewCycles</td>
</tr>
<tr>
<td><code>WORKLOCATION_SELECT</code></td>
<td><strong>8</strong></td>
<td>Compensation, Hris/Org, HeadcountPlanning, TimeTracking, EEOCReporting, Payroll, AccountingIntegrations,
Hris/Documents</td>
</tr>
<tr>
<td><code>LEVEL_SELECT</code></td>
<td><strong>7</strong></td>
<td>Compensation (9 files), Hris/Org (5), HeadcountPlanning (3), ReviewCycles, Hris/Emp, Hris/Documents, Payroll
</td>
</tr>
<tr>
<td><code>JOB_FUNCTION_SELECT</code></td>
<td><strong>4</strong></td>
<td>Compensation (12 files), HeadcountPlanning (2), ReviewCycles, Hris/Org</td>
</tr>
<tr>
<td><code>TEAMS_SELECT</code></td>
<td><strong>3</strong></td>
<td>HeadcountPlanning, Payroll, Hris/Org</td>
</tr>
</table>
<h2>Approach: Move Everything to Core</h2>
<p>Move select components AND all their product-specific dependencies to <code>app/core/</code>. Original product
files become deprecated re-exports for backward compatibility.</p>
<h2>Dependency Chain Analysis</h2>
<h3>Layer 1: Shared Select Infrastructure (already partially done)</h3>
<p>These utilities are shared across ALL 5 select components:</p>
<table>
<tr>
<th>Item</th>
<th>Current Location</th>
<th>Depends On</th>
</tr>
<tr>
<td><code>withCompositeInput</code> HOC</td>
<td><code>app/products/hr/Hub/inputs/withCompositeInput</code></td>
<td><code>withSpinner</code> (core), Pebble</td>
</tr>
<tr>
<td><code>withSelectVariations</code> HOC</td>
<td><code>app/products/hr/Hub/inputs/withSelectVariations</code></td>
<td>Pebble</td>
</tr>
<tr>
<td><code>PositionInput</code> component</td>
<td><code>app/products/hr/Hub/components/positionInput/</code></td>
<td>PositionContext, PositionInput.actions, PositionInput.types</td>
</tr>
<tr>
<td><code>FLEXIBILITY_TYPES</code> constant</td>
<td><code>app/products/hr/Hub/modules/Positions/actions/common</code></td>
<td>None</td>
</tr>
<tr>
<td><code>isEqualArrayWith</code> util</td>
<td><code>app/products/hr/Hub/utils/common</code></td>
<td>None</td>
</tr>
<tr>
<td>Conversion helpers</td>
<td><code>app/products/hr/*/helpers</code></td>
<td>Already in <code>app/core/utils/selectHelpers/</code> from prior PR</td>
</tr>
</table>
<p><strong>Complexity: <span class="risk-low">LOW</span></strong> — These have no deep product dependencies.</p>
<h3>Layer 2: Select Components Themselves</h3>
<table>
<tr>
<th>Component</th>
<th>Current Location</th>
<th>Product Imports (after Layer 1 is in core)</th>
</tr>
<tr>
<td><strong>TeamsSelect</strong></td>
<td><code>hr/Hris/Org/inputs/teamsSelect/</code></td>
<td><code>Teams</code> component</td>
</tr>
<tr>
<td><strong>DepartmentSelect</strong></td>
<td><code>hr/Hris/Org/inputs/departmentSelect/</code></td>
<td><code>Departments</code> component (dead code), <code>ORG_DATA_DEPARTMENTS_PATH</code> (inline-able string)
</td>
</tr>
<tr>
<td><strong>LevelSelect</strong></td>
<td><code>hr/Hris/Org/inputs/trackLevelSelect/</code></td>
<td><code>Tracks</code> endpoint, <code>routeUtils</code> (inline-able string)</td>
</tr>
<tr>
<td><strong>LocationSelect</strong></td>
<td><code>hr/Hub/inputs/locationSelect/</code></td>
<td><code>Address</code> component, <code>workLocationActions</code>, <code>addPossibleValueToPosition</code></td>
</tr>
<tr>
<td><strong>JobFunctionSelect</strong></td>
<td><code>hr/Compensation/inputs/jobFunctionSelect/</code></td>
<td><code>AddJobFunction</code>, <code>CompensationManagementConfigAPI</code>, <code>useJobFunctionAccess</code>,
<code>COMPENSATION_SETTINGS_DATA_KEYS</code></td>
</tr>
</table>
<h3>Layer 3: Product-Specific Dependencies (the hard part)</h3>
<h4>3A. Teams + Departments (Hierarchy ecosystem)</h4>
<p>Both <code>Teams</code> and <code>Departments</code> share the same <code>Hierarchy</code> component infrastructure
from <code>hr/Hub</code>.</p>
<pre>
Teams.tsx / Departments.tsx
├── Hierarchy (Hub component)
│ ├── HierarchyView — no product deps
│ ├── HierarchyEdit — no product deps
│ ├── HierarchyNodeRenderer
│ │ └── OrganizationDataDeleteModal (Hris/Org)
│ │ → FlowIds (Hris/Emp), eorRouteUtils (Hub)
│ ├── HierarchyDeleteModal — no product deps
│ └── HierarchyEditModal — no product deps
├── hierarchyActions (Hub)
│ ├── hierarchy.helpers (Hub)
│ ├── Department endpoint → re-export from app/api/ ✅
│ ├── Team endpoint → re-export from app/api/ ✅
│ ├── Level endpoint → re-export from app/api/ ✅
│ └── JobFunction endpoint (Compensation) → app/api/ + types
└── withSpinner, withRouterLatest → already in core/routes ✅
</pre>
<p><strong>Total unique product files: ~20</strong><br>
<strong>Products crossed:</strong> Hub, Hris/Org, Compensation (endpoints + types), Hris/Emp (1 constant)<br>
<strong>Complexity: <span class="risk-medium">HIGH</span></strong> — Substantial but self-contained. Teams and
Departments share it, so moving it once covers both.
</p>
<h4>3B. LevelSelect Dependencies</h4>
<pre>
Tracks endpoint → only depends on app/api/baseModel ✅
routeUtils → pure string function, zero deps ✅
</pre>
<p><strong>Total product files: 2</strong><br>
<strong>Complexity: <span class="risk-low">SIMPLE</span></strong>
</p>
<h4>3C. LocationSelect Dependencies</h4>
<pre>
Address component (25 product files!)
├── EmploymentAuthorization endpoints + utils
│ ├── flowInfraRoutesUtils → EmploymentAuth constants, GlobalContractors constants
│ │ └── bulkCensusInfraCommon.service → Documents utils, EorAPIConstants
│ └── Hub/containers/employmentAuthorization utils
├── Hub/actions/address → app/api only ✅
└── Hub/modules/Employee/components/setup/setupRegex → app/lib only ✅
workLocationActions (19 product files!)
└── CrossSell/Triggers/UserEvents/workLocationCrossSell
└── CrossSell/constants → SpendManagement/constants
→ Payroll/constants, BillPay types...
addPossibleValueToPosition (2 product files)
└── Hub/modules/Positions/actions/common → no product deps ✅
</pre>
<p><strong>Total unique product files: ~42</strong><br>
<strong>Products crossed: 7+</strong> (Hris/Emp, EmploymentAuthorization, Hub, Peo, GlobalContractors,
Hris/Documents, growth/CrossSell, finance/SpendManagement, finance/Payroll)<br>
<strong>Complexity: <span class="risk-high">VERY HIGH</span></strong> — Address and workLocationActions have the
deepest, widest dependency chains.
</p>
<h4>3D. JobFunctionSelect Dependencies</h4>
<pre>
AddJobFunction (30+ product files)
├── JobFunctionAPI → JobFunctionEndpoints, CompanyDetails.actions
│ └── CompanyDetailsPermission.types → platform/HubPlatform/Permissions
├── useJobFunctionMutation → CrossSell/jobFunctionCrossSell
├── compensationQueryKeys → Benchmarking types, EquityGrant constants
└── Compensation/types → platform/HubPlatform/superGroup (50+ files)
CompensationManagementConfigAPI → app/api/baseModel only ✅ (1 file)
useJobFunctionAccess → reuses same chains as AddJobFunction (overlapping)
COMPENSATION_SETTINGS_DATA_KEYS → inline-able or 1 file + type chain
</pre>
<p><strong>Total unique product files: ~30+</strong> (heavy overlap between AddJobFunction and
useJobFunctionAccess)<br>
<strong>Products crossed:</strong> Compensation, Hris/Org, growth/CrossSell, platform/HubPlatform<br>
<strong>Complexity: <span class="risk-high">VERY HIGH</span></strong> — Most depth is TYPE-only and can be addressed
via <code>@rippling/types</code>.
</p>
<h2>Migration Plan — PR Breakdown</h2>
<h3>PR 1: Shared Select Infrastructure → Core</h3>
<p><span class="risk-low">LOW RISK</span> — Already partially done for TeamsSelect. Complete for all selects.</p>
<p>Move to <code>app/core/components/selectUtils/</code>:</p>
<ul>
<li><code>withCompositeInput</code> HOC</li>
<li><code>withSelectVariations</code> HOC + <code>useSelectState</code> hook</li>
<li><code>PositionInput</code> component + context + actions + types</li>
<li><code>FLEXIBILITY_TYPES</code>, <code>DATA_KEY</code>, <code>CUSTOM_DATA_KEY_NAMES</code> constants</li>
<li><code>isEqualArrayWith</code> utility</li>
</ul>
<p>Move types to <code>@rippling/types</code>: <code>CompositeInputPropsType</code>,
<code>WithSelectVariationsPropsType</code>, <code>PositionDataType</code></p>
<p>Re-export from original product locations (deprecated).</p>
<p><strong>Files changed:</strong> ~15 new core files, ~8 product re-exports</p>
<h3>PR 2: DepartmentSelect + LevelSelect → Core</h3>
<p><span class="risk-low">LOW RISK</span></p>
<p><strong>DepartmentSelect:</strong></p>
<ul>
<li>Move component to <code>app/core/components/departmentSelect/</code></li>
<li>Remove dead <code>Departments</code> import (<code>showAddModal</code> is never set to <code>true</code>)</li>
<li>Inline <code>ORG_DATA_DEPARTMENTS_PATH</code> string constant</li>
<li>Update Grid editor import, GeneratedForm, dataImport</li>
</ul>
<p><strong>LevelSelect:</strong></p>
<ul>
<li>Move component to <code>app/core/components/levelSelect/</code></li>
<li>Move <code>Tracks</code> endpoint to <code>app/api/endpoints/</code> (only depends on BaseModel)</li>
<li>Inline <code>routeUtils.getTrackListRoute()</code> string</li>
<li>Move <code>LevelSelectRenderer</code></li>
<li>Update Grid editor, GeneratedForm, dataImport, inputRenderer</li>
</ul>
<p><strong>Files changed:</strong> ~10 new core files, ~5 product re-exports</p>
<h3>PR 3: TeamsSelect + Hierarchy Ecosystem → Core</h3>
<p><span class="risk-medium">MEDIUM RISK</span></p>
<p><strong>Key insight: Teams and Departments share the Hierarchy infrastructure. Moving it once unlocks
both.</strong></p>
<p>Move to <code>app/core/components/hierarchy/</code>:</p>
<ul>
<li><code>Hierarchy</code> component (HierarchyView, HierarchyEdit, HierarchyNodeRenderer, etc.)</li>
<li><code>hierarchyActions</code> (Hub)</li>
<li><code>hierarchy.helpers</code> (Hub)</li>
<li>HierarchyDeleteModal, HierarchyEditModal</li>
<li>AssignHierarchyModal + assignDepartment component</li>
</ul>
<p>Move to <code>app/core/components/teamsSelect/</code>:</p>
<ul>
<li><code>TeamsSelect</code> component</li>
</ul>
<p>Handle transitive dependencies:</p>
<ul>
<li>Endpoint re-exports (Department, Team, Level, JobFunction) → already in <code>app/api/</code>, change imports
</li>
<li><code>OrganizationDataDeleteModal</code> → move to core or lazy-load</li>
<li><code>FlowIds</code> constant (Hris/Emp) → inline or move to core constants</li>
<li><code>eorRouteUtils</code> (Hub) → inline route string</li>
<li><code>DarkLaunchConfig</code> (Hub) → evaluate if needed or can be removed from hierarchy code path</li>
</ul>
<p><strong>Files changed:</strong> ~25 new core files, ~10 product re-exports</p>
<h3>PR 4: LocationSelect + Address + workLocationActions → Core</h3>
<p><span class="risk-high">HIGH RISK</span> — Deepest dependency chain. Consider splitting further.</p>
<p><strong>Phase 4A: workLocationActions + addPossibleValueToPosition</strong></p>
<ul>
<li><code>addPossibleValueToPosition</code> → already inlined in prior work</li>
<li><code>workLocationActions</code> → move to <code>app/core/</code></li>
<li>Handle CrossSell dependency: move CrossSell trigger to core or make injectable/optional</li>
</ul>
<p><strong>Phase 4B: Address component</strong></p>
<ul>
<li>Move to <code>app/core/components/address/</code></li>
<li>Handle transitive deps: EmploymentAuthorization, flowInfraRoutesUtils, bulkCensusInfraCommon</li>
<li>Audit which dependencies are actually used at runtime vs. brought in by barrel exports</li>
</ul>
<p><strong>Phase 4C: LocationSelect</strong></p>
<ul>
<li>Move component to <code>app/core/components/locationSelect/</code></li>
<li>All dependencies now in core (from 4A + 4B)</li>
<li>Update Grid editor, GeneratedForm, dataImport</li>
</ul>
<p><strong>Files changed:</strong> ~30+ new core files</p>
<h3>PR 5: JobFunctionSelect + Compensation Dependencies → Core</h3>
<p><span class="risk-high">HIGH RISK</span></p>
<p><strong>Phase 5A: Simple Compensation dependencies</strong></p>
<ul>
<li><code>CompensationManagementConfigAPI</code> → move to <code>app/api/endpoints/</code> (only depends on
BaseModel)</li>
<li><code>COMPENSATION_SETTINGS_DATA_KEYS</code> → inline the specific key or move enum to core constants</li>
<li><code>JobFunctionGroup</code> type → move to <code>@rippling/types</code></li>
</ul>
<p><strong>Phase 5B: AddJobFunction component</strong></p>
<ul>
<li>Move to <code>app/core/components/addJobFunction/</code></li>
<li>Handle transitive deps: JobFunctionAPI, compensationQueryKeys, CrossSell trigger</li>
<li>Compensation types chain → move needed types to <code>@rippling/types</code>; avoid pulling in
<code>platform/HubPlatform/superGroup</code></li>
</ul>
<p><strong>Phase 5C: useJobFunctionAccess hook</strong></p>
<ul>
<li>Move to <code>app/core/hooks/</code> (depends on JobFunctionAPI + query keys from 5B)</li>
</ul>
<p><strong>Phase 5D: JobFunctionSelect</strong></p>
<ul>
<li>Move component to <code>app/core/components/jobFunctionSelect/</code></li>
<li>Update Grid editor, dataImport</li>
</ul>
<p><strong>Files changed:</strong> ~25+ new core files</p>
<h3>PR 6: GeneratedForm + dataImport + inputRenderer Cleanup</h3>
<p><span class="risk-low">LOW RISK</span></p>
<p>After PRs 1-5, all select components are in core. Update remaining core consumers:</p>
<ul>
<li><code>GeneratedForm.tsx</code> → update 8 imports to core paths</li>
<li><code>columnValuesMapSelect.tsx</code> → update 5 imports to core paths</li>
<li><code>uploadCsv.tsx</code> → move <code>uploadToS3</code> to core or inline</li>
<li><code>InputRenderer.constants.ts</code> → update <code>LevelSelectRenderer</code> import</li>
<li><code>ManagerSelect</code> and <code>LookupFieldSelectInput</code> → evaluate separately</li>
</ul>
<p><strong>Files changed:</strong> ~6 files updated</p>
<h3>PR 7: Cleanup + module.config.json</h3>
<p><span class="risk-low">LOW RISK</span></p>
<ul>
<li>Remove all product paths from <code>app/core/components/Grid/module.config.json</code></li>
<li>Update <code>app/core/module.config.json</code> — remove <code>allowedImports</code> for product selects</li>
<li>Update <code>service_catalog.yml</code> — assign ownership for new core files</li>
<li>Run <code>npm run module-boundary:check</code> to verify zero violations</li>
</ul>
<h2>PR Summary</h2>
<table>
<tr>
<th>PR</th>
<th>Scope</th>
<th>Risk</th>
<th>Est. Files</th>
<th>Can Parallelize?</th>
</tr>
<tr>
<td><strong>PR 1</strong></td>
<td>Shared select utils → core</td>
<td><span class="risk-low">Low</span></td>
<td>~23</td>
<td>—</td>
</tr>
<tr>
<td><strong>PR 2</strong></td>
<td>DepartmentSelect + LevelSelect → core</td>
<td><span class="risk-low">Low</span></td>
<td>~15</td>
<td>After PR 1</td>
</tr>
<tr>
<td><strong>PR 3</strong></td>
<td>TeamsSelect + Hierarchy → core</td>
<td><span class="risk-medium">Medium</span></td>
<td>~35</td>
<td>After PR 1</td>
</tr>
<tr>
<td><strong>PR 4</strong></td>
<td>LocationSelect + Address → core</td>
<td><span class="risk-high">High</span></td>
<td>~30+</td>
<td>After PR 1</td>
</tr>
<tr>
<td><strong>PR 5</strong></td>
<td>JobFunctionSelect + Compensation deps → core</td>
<td><span class="risk-high">High</span></td>
<td>~25+</td>
<td>After PR 1</td>
</tr>
<tr>
<td><strong>PR 6</strong></td>
<td>GeneratedForm + dataImport cleanup</td>
<td><span class="risk-low">Low</span></td>
<td>~6</td>
<td>After PR 2-5</td>
</tr>
<tr>
<td><strong>PR 7</strong></td>
<td>module.config + service_catalog cleanup</td>
<td><span class="risk-low">Low</span></td>
<td>~3</td>
<td>After PR 6</td>
</tr>
</table>
<p><strong>Total estimated files:</strong> ~140+<br>
<strong>Recommended order:</strong> PR1 → PR2 → PR3 → PR4/PR5 (parallel) → PR6 → PR7
</p>
<h2>Risk Mitigation</h2>
<ol>
<li><strong>Testing:</strong> Each PR must include running existing unit tests for the moved components. The Grid
cell type tests (<code>__tests__/cellTypes/*.test.tsx</code>) validate renderer/formatter/paste behavior.</li>
<li><strong>Barrel export analysis:</strong> Before moving Address and AddJobFunction, audit which transitive
dependencies are actually used at runtime vs. pulled in by barrel <code>index.ts</code> files. This can
significantly reduce the dependency chain.</li>
<li><strong>Type-only imports:</strong> Many deep dependencies (especially
<code>platform/HubPlatform/superGroup</code>, <code>finance/BillPay</code>) are reached through
<code>import type</code> chains. These can be cut by moving types to <code>@rippling/types</code> without moving
runtime code.</li>
<li><strong>CrossSell dependencies:</strong> Both <code>workLocationActions</code> and <code>AddJobFunction</code>
trigger CrossSell events. These can be made injectable (callback prop) or moved to core as a generic eventing
utility.</li>
<li><strong>Re-exports:</strong> Every moved file gets a deprecated re-export at the original location to avoid
breaking existing consumers. These can be cleaned up later by each product team.</li>
<li><strong>Incremental verification:</strong> After each PR, run <code>npm run module-boundary:check</code>,
<code>npm run ts:check</code>, and <code>npm run lint</code> to catch issues early.</li>
</ol>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment