Skip to content

Commit af069ed

Browse files
ducnv-1960claude
andcommitted
feat(upload): add --screen-id flag for uploading by screen ID
Support uploading testcases and specs using screen_id as an alternative to frame-id/file-key. This adds GraphQL queries to resolve frames by screen_id and validates that --screen-id is mutually exclusive with --frame-id and --file-key. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 33deb1b commit af069ed

4 files changed

Lines changed: 223 additions & 18 deletions

File tree

cmd/upload_specs.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var (
2626
specFrameID string
2727
specFileKey string
2828
specFrameName string
29+
specScreenID string
2930
)
3031

3132
// CSV columns are mapped to spec fields:
@@ -51,6 +52,8 @@ Two frame ID formats are supported:
5152
5253
When using a Figma frame ID, the Figma file key is inferred from the path
5354
convention .momorph/specs/{file_key}/... or supplied via --file-key.
55+
56+
Alternatively, use --screen-id to upload by screen ID instead of frame ID.
5457
`,
5558
Example: ` # Upload using MoMorph integer frame ID in filename
5659
momorph upload specs 7323-iOS-Home.csv
@@ -64,6 +67,9 @@ convention .momorph/specs/{file_key}/... or supplied via --file-key.
6467
# Upload with explicit Figma frame ID and file key
6568
momorph upload specs ~/data/my-specs.csv --frame-id=70:1214 --file-key=Dhz3zTL0vjaOTDGUIHugQe
6669
70+
# Upload using screen ID
71+
momorph upload specs ~/data/my-specs.csv --screen-id=42
72+
6773
# Upload all CSV files in a directory recursively
6874
momorph upload specs --dir ./specs/ -r
6975
@@ -80,6 +86,7 @@ func init() {
8086
uploadSpecsCmd.Flags().StringVar(&specFrameID, "frame-id", "", "Frame ID: MoMorph integer (e.g. 7323) or Figma frame ID (e.g. 70:1214). Required when not encoded in the filename.")
8187
uploadSpecsCmd.Flags().StringVar(&specFileKey, "file-key", "", "Figma file key (required with --frame-id when using a Figma frame ID outside .momorph/ path)")
8288
uploadSpecsCmd.Flags().StringVar(&specFrameName, "frame-name", "", "Frame name for display (optional, used with --frame-id)")
89+
uploadSpecsCmd.Flags().StringVar(&specScreenID, "screen-id", "", "Screen ID (MoMorph integer, alternative to --frame-id)")
8390
uploadCmd.AddCommand(uploadSpecsCmd)
8491
}
8592

@@ -112,9 +119,20 @@ func runUploadSpecs(cmd *cobra.Command, args []string) error {
112119
fmt.Println("⚠ Could not get user email for revision tracking")
113120
}
114121

122+
// Validate conflicting flags
123+
if specScreenID != "" && (specFrameID != "" || specFileKey != "") {
124+
return fmt.Errorf("--screen-id cannot be used together with --frame-id or --file-key")
125+
}
126+
115127
// Parse --frame-id flag when provided
116128
var flagsMeta *upload.MoMorphFrameMeta
117-
if specFrameID != "" {
129+
if specScreenID != "" {
130+
// Screen ID mode takes precedence
131+
flagsMeta = &upload.MoMorphFrameMeta{
132+
ScreenID: specScreenID,
133+
FrameName: specFrameName,
134+
}
135+
} else if specFrameID != "" {
118136
parsedID, err := strconv.Atoi(specFrameID)
119137
if err == nil && parsedID > 0 {
120138
// MoMorph integer frame ID
@@ -143,7 +161,7 @@ func runUploadSpecs(cmd *cobra.Command, args []string) error {
143161
if len(files) == 0 {
144162
fmt.Println("No CSV files found to upload")
145163
fmt.Println("\nProvide CSV file paths directly or use --dir to scan a directory.")
146-
fmt.Println("The frame ID must be in the filename (e.g. 42-MyScreen.csv) or supplied via --frame-id.")
164+
fmt.Println("The frame ID must be in the filename (e.g. 42-MyScreen.csv) or supplied via --frame-id or --screen-id.")
147165
return nil
148166
}
149167

@@ -181,7 +199,9 @@ func runUploadSpecs(cmd *cobra.Command, args []string) error {
181199
}
182200
specs, _ := upload.ParseSpecsCSV(f)
183201
fmt.Printf(" - %s\n", filepath.Base(f))
184-
if meta.FigmaFrameID != "" {
202+
if meta.ScreenID != "" {
203+
fmt.Printf(" Screen ID: %s\n", meta.ScreenID)
204+
} else if meta.FigmaFrameID != "" {
185205
fmt.Printf(" Figma Frame ID: %s\n", meta.FigmaFrameID)
186206
fmt.Printf(" File Key: %s\n", meta.FileKey)
187207
} else {
@@ -306,9 +326,20 @@ func uploadSingleSpecFile(ctx context.Context, client *graphql.Client, filePath
306326

307327
logger.Debug("Parsed %d specs from %s", len(specs), fileName)
308328

309-
// Fetch frame — by MoMorph integer ID or by Figma frame ID
329+
// Fetch frame — by screen ID, MoMorph integer ID, or Figma frame ID
310330
var frame *graphql.Frame
311-
if meta.FigmaFrameID != "" {
331+
if meta.ScreenID != "" {
332+
frame, err = client.GetFrameByScreenID(ctx, meta.ScreenID)
333+
if err != nil {
334+
return upload.UploadResult{
335+
FilePath: filePath,
336+
FileName: fileName,
337+
Status: upload.StatusFailed,
338+
Error: err,
339+
Message: fmt.Sprintf("Frame not found (screen_id=%s): %v", meta.ScreenID, err),
340+
}
341+
}
342+
} else if meta.FigmaFrameID != "" {
312343
if meta.FileKey == "" {
313344
return upload.UploadResult{
314345
FilePath: filePath,

cmd/upload_testcases.go

Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var (
2323
tcFileKey string
2424
tcFrameID string
2525
tcFrameName string
26+
tcScreenID string
2627
)
2728

2829
// CSV columns are mapped to test case fields:
@@ -43,13 +44,18 @@ By default, files must follow the path pattern:
4344
4445
Alternatively, use --file-key (and optionally --frame-id, --frame-name) to
4546
upload CSV files from any location without following the path convention.
47+
48+
You can also use --screen-id to upload by screen ID instead of frame ID.
4649
`,
4750
Example: ` # Upload using path convention
4851
momorph upload testcases .momorph/testcases/xxx/9276:19907-TOP_Channel.csv
4952
5053
# Upload from any location with explicit metadata
5154
momorph upload testcases ~/data/tc.csv --file-key=xxx --frame-id=9276:19907
5255
56+
# Upload using screen ID
57+
momorph upload testcases ~/data/tc.csv --screen-id=42
58+
5359
# Upload all testcases in a directory recursively
5460
momorph upload testcases --dir .momorph/testcases/ -r
5561
@@ -66,6 +72,7 @@ func init() {
6672
uploadTestcasesCmd.Flags().StringVar(&tcFileKey, "file-key", "", "Figma file key (required when CSV is not in .momorph/ path)")
6773
uploadTestcasesCmd.Flags().StringVar(&tcFrameID, "frame-id", "", "Figma frame ID (optional, used with --file-key)")
6874
uploadTestcasesCmd.Flags().StringVar(&tcFrameName, "frame-name", "", "Frame name (optional, used with --file-key)")
75+
uploadTestcasesCmd.Flags().StringVar(&tcScreenID, "screen-id", "", "Screen ID (MoMorph integer, alternative to --frame-id)")
6976
uploadCmd.AddCommand(uploadTestcasesCmd)
7077
}
7178

@@ -91,8 +98,13 @@ func runUploadTestcases(cmd *cobra.Command, args []string) error {
9198
return nil
9299
}
93100

94-
// Determine if using flags mode (--file-key provided)
95-
useFlags := tcFileKey != ""
101+
// Validate conflicting flags
102+
if tcScreenID != "" && (tcFileKey != "" || tcFrameID != "") {
103+
return fmt.Errorf("--screen-id cannot be used together with --file-key or --frame-id")
104+
}
105+
106+
// Determine if using flags mode (--file-key or --screen-id provided)
107+
useFlags := tcFileKey != "" || tcScreenID != ""
96108

97109
// Build parsed metadata from flags when in flags mode
98110
var flagsParsed *upload.ParsedFilePath
@@ -116,8 +128,9 @@ func runUploadTestcases(cmd *cobra.Command, args []string) error {
116128
if !useFlags {
117129
fmt.Println("\nMake sure files are in the correct path format:")
118130
fmt.Println(" .momorph/testcases/{file_key}/{frame_id}-{frame_name}.csv")
119-
fmt.Println("\nOr use --file-key to upload from any location:")
131+
fmt.Println("\nOr use --file-key or --screen-id to upload from any location:")
120132
fmt.Println(" momorph upload testcases myfile.csv --file-key=<figma_file_key>")
133+
fmt.Println(" momorph upload testcases myfile.csv --screen-id=<screen_id>")
121134
}
122135
return nil
123136
}
@@ -147,8 +160,12 @@ func runUploadTestcases(cmd *cobra.Command, args []string) error {
147160
parsed, _ = upload.ParseFilePath(f)
148161
}
149162
fmt.Printf(" - %s\n", filepath.Base(f))
150-
fmt.Printf(" File Key: %s\n", parsed.FileKey)
151-
fmt.Printf(" Frame ID: %s\n", parsed.FrameID)
163+
if tcScreenID != "" {
164+
fmt.Printf(" Screen ID: %s\n", tcScreenID)
165+
} else {
166+
fmt.Printf(" File Key: %s\n", parsed.FileKey)
167+
fmt.Printf(" Frame ID: %s\n", parsed.FrameID)
168+
}
152169
fmt.Printf(" Frame Name: %s\n", parsed.FrameName)
153170
}
154171
return nil
@@ -163,7 +180,7 @@ func runUploadTestcases(cmd *cobra.Command, args []string) error {
163180

164181
// Upload files
165182
fmt.Printf("\nUploading %d test case file(s)...\n", len(validFiles))
166-
results := uploadTestcaseFiles(ctx, client, validFiles, flagsParsed, tcUploadContinue)
183+
results := uploadTestcaseFiles(ctx, client, validFiles, flagsParsed, tcScreenID, tcUploadContinue)
167184

168185
// Combine with skipped files
169186
allResults := append(skipped, results...)
@@ -174,7 +191,7 @@ func runUploadTestcases(cmd *cobra.Command, args []string) error {
174191
return nil
175192
}
176193

177-
func uploadTestcaseFiles(ctx context.Context, client *graphql.Client, files []string, flagsParsed *upload.ParsedFilePath, continueOnError bool) []upload.UploadResult {
194+
func uploadTestcaseFiles(ctx context.Context, client *graphql.Client, files []string, flagsParsed *upload.ParsedFilePath, screenID string, continueOnError bool) []upload.UploadResult {
178195
var results []upload.UploadResult
179196

180197
for i, file := range files {
@@ -188,7 +205,7 @@ func uploadTestcaseFiles(ctx context.Context, client *graphql.Client, files []st
188205
fileName := filepath.Base(file)
189206
fmt.Printf(" [%d/%d] %s ", i+1, len(files), fileName)
190207

191-
result := uploadSingleTestcaseFile(ctx, client, file, flagsParsed)
208+
result := uploadSingleTestcaseFile(ctx, client, file, flagsParsed, screenID)
192209
results = append(results, result)
193210

194211
switch result.Status {
@@ -211,9 +228,92 @@ func uploadTestcaseFiles(ctx context.Context, client *graphql.Client, files []st
211228

212229
// uploadSingleTestcaseFile uploads a single testcase CSV file. When flagsParsed
213230
// is non-nil it is used as metadata instead of parsing from the file path.
214-
func uploadSingleTestcaseFile(ctx context.Context, client *graphql.Client, filePath string, flagsParsed *upload.ParsedFilePath) upload.UploadResult {
231+
// When screenID != "", the frame is resolved by screen_id instead of file-key + frame-id.
232+
func uploadSingleTestcaseFile(ctx context.Context, client *graphql.Client, filePath string, flagsParsed *upload.ParsedFilePath, screenID string) upload.UploadResult {
215233
fileName := filepath.Base(filePath)
216234

235+
// Screen ID mode: resolve frame by screen_id directly
236+
if screenID != "" {
237+
// Parse CSV file
238+
frameName := ""
239+
if flagsParsed != nil {
240+
frameName = flagsParsed.FrameName
241+
}
242+
content, err := upload.ParseTestcasesCSV(filePath, frameName)
243+
if err != nil {
244+
return upload.UploadResult{
245+
FilePath: filePath,
246+
FileName: fileName,
247+
Status: upload.StatusFailed,
248+
Error: err,
249+
Message: fmt.Sprintf("Failed to parse CSV: %v", err),
250+
}
251+
}
252+
253+
if len(content.TestCases) == 0 {
254+
return upload.UploadResult{
255+
FilePath: filePath,
256+
FileName: fileName,
257+
Status: upload.StatusSkipped,
258+
Message: "CSV file contains no test cases",
259+
}
260+
}
261+
262+
logger.Debug("Parsed %d test cases from %s", len(content.TestCases), fileName)
263+
264+
// Check if test cases already exist for this screen
265+
existingTestCases, err := client.GetFrameTestCasesByScreenID(ctx, screenID)
266+
if err != nil {
267+
logger.Debug("No existing test cases found: %v", err)
268+
}
269+
270+
if len(existingTestCases) > 0 {
271+
logger.Debug("Updating existing test case ID: %d", existingTestCases[0].ID)
272+
_, err = client.UpdateFrameTestcase(ctx, existingTestCases[0].ID, content)
273+
if err != nil {
274+
return upload.UploadResult{
275+
FilePath: filePath,
276+
FileName: fileName,
277+
Status: upload.StatusFailed,
278+
Error: err,
279+
Message: fmt.Sprintf("Failed to update test case: %v", err),
280+
}
281+
}
282+
} else {
283+
// Get frame by screen_id to get internal ID
284+
frame, err := client.GetFrameByScreenID(ctx, screenID)
285+
if err != nil {
286+
return upload.UploadResult{
287+
FilePath: filePath,
288+
FileName: fileName,
289+
Status: upload.StatusFailed,
290+
Error: err,
291+
Message: fmt.Sprintf("Frame not found for screen_id=%s: %v", screenID, err),
292+
}
293+
}
294+
295+
logger.Debug("Creating new test case for frame ID: %d (screen_id=%s)", frame.ID, screenID)
296+
297+
_, err = client.InsertFrameTestcase(ctx, frame.ID, content)
298+
if err != nil {
299+
return upload.UploadResult{
300+
FilePath: filePath,
301+
FileName: fileName,
302+
Status: upload.StatusFailed,
303+
Error: err,
304+
Message: fmt.Sprintf("Failed to insert test case: %v", err),
305+
}
306+
}
307+
}
308+
309+
return upload.UploadResult{
310+
FilePath: filePath,
311+
FileName: fileName,
312+
Status: upload.StatusSuccess,
313+
Message: fmt.Sprintf("Uploaded %d test cases", len(content.TestCases)),
314+
}
315+
}
316+
217317
// Resolve metadata: use flags or parse from path
218318
var parsed *upload.ParsedFilePath
219319
if flagsParsed != nil {
@@ -261,7 +361,7 @@ func uploadSingleTestcaseFile(ctx context.Context, client *graphql.Client, fileP
261361
FilePath: filePath,
262362
FileName: fileName,
263363
Status: upload.StatusFailed,
264-
Message: "frame-id is required for uploading test cases (use --frame-id flag)",
364+
Message: "frame-id is required for uploading test cases (use --frame-id or --screen-id flag)",
265365
}
266366
}
267367

internal/graphql/operations.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Frame struct {
1313
FileID int `json:"file_id"`
1414
Name string `json:"name"`
1515
Status string `json:"status"`
16+
ScreenID string `json:"screen_id"`
1617
}
1718

1819
// FrameTestCase represents a test case for a frame
@@ -167,6 +168,39 @@ query GetFrameByID($id: bigint!) {
167168
status
168169
}
169170
}
171+
`
172+
173+
// GetFrameByScreenID query - fetch a MoMorph frame by its screen_id
174+
queryGetFrameByScreenID = `
175+
query GetFrameByScreenID($screenId: String!) {
176+
frames(where: {screen_id: {_eq: $screenId}}, limit: 1) {
177+
id
178+
frame_link_id
179+
file_id
180+
name
181+
status
182+
screen_id
183+
}
184+
}
185+
`
186+
187+
// GetFrameTestCasesByScreenID query - fetch test cases by screen_id
188+
queryGetFrameTestCasesByScreenID = `
189+
query GetFrameTestCasesByScreenID($screenId: String!) {
190+
frame_testcases(
191+
where: {
192+
frame: {screen_id: {_eq: $screenId}}
193+
}
194+
) {
195+
id
196+
testcasable_id
197+
content
198+
base_structure
199+
status
200+
created_at
201+
updated_at
202+
}
203+
}
170204
`
171205

172206
// ListDesignItemsByFrameIDAndNodeLinkIds query - fetch design items by frame ID and node link IDs
@@ -500,6 +534,44 @@ func (c *Client) GetFrameByID(ctx context.Context, id int) (*Frame, error) {
500534
return &result.Frames[0], nil
501535
}
502536

537+
// GetFrameByScreenID fetches a MoMorph frame by its screen_id
538+
func (c *Client) GetFrameByScreenID(ctx context.Context, screenID string) (*Frame, error) {
539+
variables := map[string]interface{}{
540+
"screenId": screenID,
541+
}
542+
543+
var result struct {
544+
Frames []Frame `json:"frames"`
545+
}
546+
547+
if err := c.ExecuteWithResult(ctx, queryGetFrameByScreenID, variables, &result); err != nil {
548+
return nil, err
549+
}
550+
551+
if len(result.Frames) == 0 {
552+
return nil, fmt.Errorf("frame not found: screen_id=%s", screenID)
553+
}
554+
555+
return &result.Frames[0], nil
556+
}
557+
558+
// GetFrameTestCasesByScreenID fetches test cases for a frame by screen_id
559+
func (c *Client) GetFrameTestCasesByScreenID(ctx context.Context, screenID string) ([]FrameTestCase, error) {
560+
variables := map[string]interface{}{
561+
"screenId": screenID,
562+
}
563+
564+
var result struct {
565+
FrameTestcases []FrameTestCase `json:"frame_testcases"`
566+
}
567+
568+
if err := c.ExecuteWithResult(ctx, queryGetFrameTestCasesByScreenID, variables, &result); err != nil {
569+
return nil, err
570+
}
571+
572+
return result.FrameTestcases, nil
573+
}
574+
503575
// ListDesignItemsByFrameID fetches design items by MoMorph frame ID.
504576
// When nodeLinkIds is non-empty only items matching those IDs are returned;
505577
// otherwise all design items for the frame are returned.

0 commit comments

Comments
 (0)