Class: PlanMyStuff::BaseProjectItem

Inherits:
ApplicationRecord show all
Defined in:
lib/plan_my_stuff/base_project_item.rb

Overview

Shared base for GitHub Projects V2 item wrappers. Holds attribute definitions, generic build/create/move/update/delete/assign machinery, and core instance helpers.

Not meant to be used directly. Concrete subclasses (ProjectItem, TestingProjectItem) add their own domain-specific methods.

Class methods:

BaseProjectItem.create!(issue)                             -- add existing issue
BaseProjectItem.create!(title, draft: true, body: "...")  -- create draft item
BaseProjectItem.move_item(item_id:, status:)
BaseProjectItem.assign(item_id:, assignee:)

Instance methods:

item.move_to!("Done")
item.assign!("octocat")

Direct Known Subclasses

ProjectItem, TestingProjectItem

Instance Attribute Summary

Attributes inherited from ApplicationRecord

#github_response

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

#destroyed?, #initialize, #new_record?, #persisted?, read_field

Constructor Details

This class inherits a constructor from PlanMyStuff::ApplicationRecord

Class Method Details

.assign(number:, content_node_id:, assignees:, draft: false, repo: nil) ⇒ void

This method returns an undefined value.

Assigns users to a project item. Issues/PRs use REST via Issue.update!, drafts use GraphQL.

Parameters:

  • number (Integer, nil)

    issue number (nil for drafts)

  • content_node_id (String)

    node ID of the underlying content

  • assignees (Array<String>)

    GitHub usernames

  • draft (Boolean) (defaults to: false)

    whether the item is a draft issue

  • repo (Symbol, String, nil) (defaults to: nil)

    repo key (for issues)



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/plan_my_stuff/base_project_item.rb', line 253

def assign(number:, content_node_id:, assignees:, draft: false, repo: nil)
  if draft
    client = PlanMyStuff.client
    user_ids = assignees.map do |assignee|
      user_data = client.graphql(
        PlanMyStuff::GraphQL::Queries::USER_NODE_ID,
        variables: { login: assignee },
      )
      user_id = user_data.dig(:user, :id)

      raise(APIError, "GitHub user not found: #{assignee}") if user_id.nil?

      user_id
    end

    client.graphql(
      PlanMyStuff::GraphQL::Queries::ASSIGN_DRAFT,
      variables: { draftIssueId: content_node_id, assigneeIds: user_ids },
    )
  else
    Issue.update!(number: number, repo: repo, assignees: assignees)
  end
end

.build(item_hash, project:) ⇒ PlanMyStuff::BaseProjectItem

Builds a persisted item from parsed item data.

Parameters:

  • item_hash (Hash)

    parsed item data (from BaseProject.parse_project_item)

  • project (PlanMyStuff::BaseProject)

    parent project

Returns:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/plan_my_stuff/base_project_item.rb', line 61

def build(item_hash, project:)
  raw_body_val = item_hash[:body]

  item = new(
    id: item_hash[:id],
    type: item_hash[:type],
    content_node_id: item_hash[:content_node_id],
    title: item_hash[:title],
    raw_body: raw_body_val,
    number: item_hash[:number],
    url: item_hash[:url],
    repo: item_hash[:repo],
    state: item_hash[:state],
    status: item_hash[:status],
    field_values: item_hash[:field_values] || {},
    project: project,
  )

  if raw_body_val
    parsed = PlanMyStuff::MetadataParser.parse(raw_body_val)
    item. = PlanMyStuff::ProjectItemMetadata.from_hash(parsed[:metadata] || {})
    item.body = parsed[:body]
  end

  item.instance_variable_set(:@github_response, item_hash[:github_response])
  item.__send__(:persisted!)
  item
end

.create!(issue_or_title, draft: false, body: nil, project_number: nil, user: nil) ⇒ PlanMyStuff::BaseProjectItem

Creates a project item by adding an existing issue or creating a draft.

Parameters:

  • issue_or_title (PlanMyStuff::Issue, String)

    Issue instance (non-draft) or title string (draft)

  • draft (Boolean) (defaults to: false)

    when true, creates a draft item from a title string

  • body (String, nil) (defaults to: nil)

    body for draft items (ignored for non-draft)

  • project_number (Integer, nil) (defaults to: nil)

    defaults to config.default_project_number

Returns:



99
100
101
102
103
104
105
106
107
108
109
# File 'lib/plan_my_stuff/base_project_item.rb', line 99

def create!(issue_or_title, draft: false, body: nil, project_number: nil, user: nil)
  item =
    if draft
      add_draft_item(title: issue_or_title, body: body, project_number: project_number)
    else
      add_item(issue: issue_or_title, project_number: project_number)
    end

  PlanMyStuff::Notifications.instrument('project_item.added', item, user: user)
  item
end

.delete_item(item_id:, project_number: nil) ⇒ String

Deletes a project item from its parent project. Returns the deletedItemId from the GraphQL response on success.

Parameters:

  • item_id (String)

    project item ID (e.g. “PVTI_…”)

  • project_number (Integer, nil) (defaults to: nil)

    defaults to config.default_project_number

Returns:

  • (String)

    the deleted item ID echoed back by GitHub

Raises:



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/plan_my_stuff/base_project_item.rb', line 226

def delete_item(item_id:, project_number: nil)
  project_number = resolve_default_project_number(project_number)
  org = PlanMyStuff.configuration.organization
  project_id = resolve_project_id(org, project_number)

  data = PlanMyStuff.client.graphql(
    PlanMyStuff::GraphQL::Queries::DELETE_PROJECT_ITEM,
    variables: { projectId: project_id, itemId: item_id },
  )

  deleted_id = data.dig(:deleteProjectV2Item, :deletedItemId)
  raise(PlanMyStuff::APIError, "Failed to delete project item #{item_id}") if deleted_id.nil?

  deleted_id
end

.move_item(item_id:, status:, project_number: nil) ⇒ Hash

Moves a project item to a new status column.

Parameters:

  • item_id (String)

    project item ID (e.g. “PVTI_…”)

  • status (String)

    status name, resolved to option ID internally

  • project_number (Integer, nil) (defaults to: nil)

    defaults to config.default_project_number

Returns:

  • (Hash)

    the updated item



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/plan_my_stuff/base_project_item.rb', line 119

def move_item(item_id:, status:, project_number: nil)
  project_number = resolve_default_project_number(project_number)
  project = BaseProject.find(project_number)

  status_field = project.status_field
  option_id = resolve_status_option_id(status_field, status)

  PlanMyStuff.client.graphql(
    PlanMyStuff::GraphQL::Queries::UPDATE_SINGLE_SELECT_FIELD,
    variables: {
      projectId: project.id,
      itemId: item_id,
      fieldId: status_field[:id],
      optionId: option_id,
    },
  )
end

.update_date_field!(item_id:, field_name:, date:, project_number: nil) ⇒ Hash

Updates a date custom field on a project item.

Parameters:

  • item_id (String)

    project item ID (e.g. “PVTI_…”)

  • field_name (String)

    date field name (e.g. “Due Date”)

  • date (Date, String)

    date value to set

  • project_number (Integer, nil) (defaults to: nil)

    defaults to config.default_project_number

Returns:

  • (Hash)

    mutation result



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/plan_my_stuff/base_project_item.rb', line 199

def update_date_field!(item_id:, field_name:, date:, project_number: nil)
  project_number = resolve_default_project_number(project_number)
  project = BaseProject.find(project_number)

  field = resolve_text_field(project, field_name)

  PlanMyStuff.client.graphql(
    PlanMyStuff::GraphQL::Queries::UPDATE_DATE_FIELD,
    variables: {
      projectId: project.id,
      itemId: item_id,
      fieldId: field[:id],
      date: date.to_s,
    },
  )
end

.update_field!(item_id:, field_name:, value:, project_number: nil) ⇒ Hash

Updates a text custom field on a project item.

Parameters:

  • item_id (String)

    project item ID (e.g. “PVTI_…”)

  • field_name (String)

    text field name (e.g. “Deployment ID”)

  • value (String)

    text value to set

  • project_number (Integer, nil) (defaults to: nil)

    defaults to config.default_project_number

Returns:

  • (Hash)

    mutation result



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/plan_my_stuff/base_project_item.rb', line 173

def update_field!(item_id:, field_name:, value:, project_number: nil)
  project_number = resolve_default_project_number(project_number)
  project = BaseProject.find(project_number)

  field = resolve_text_field(project, field_name)

  PlanMyStuff.client.graphql(
    PlanMyStuff::GraphQL::Queries::UPDATE_TEXT_FIELD,
    variables: {
      projectId: project.id,
      itemId: item_id,
      fieldId: field[:id],
      value: value,
    },
  )
end

.update_single_select_field!(item_id:, field_name:, value:, project_number: nil) ⇒ Hash

Updates a single-select custom field on a project item.

Parameters:

  • item_id (String)

    project item ID (e.g. “PVTI_…”)

  • field_name (String)

    single-select field name (e.g. “Pass Mode”)

  • value (String)

    option name to select

  • project_number (Integer, nil) (defaults to: nil)

    defaults to config.default_project_number

Returns:

  • (Hash)

    mutation result



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/plan_my_stuff/base_project_item.rb', line 146

def update_single_select_field!(item_id:, field_name:, value:, project_number: nil)
  project_number = resolve_default_project_number(project_number)
  project = BaseProject.find(project_number)

  field = resolve_single_select_field(project, field_name)
  option_id = resolve_status_option_id(field, value)

  PlanMyStuff.client.graphql(
    PlanMyStuff::GraphQL::Queries::UPDATE_SINGLE_SELECT_FIELD,
    variables: {
      projectId: project.id,
      itemId: item_id,
      fieldId: field[:id],
      optionId: option_id,
    },
  )
end

Instance Method Details

#as_json(options = {}) ⇒ Hash

Serializes the project item to a JSON-safe hash, excluding the back-reference to the parent project to prevent recursive serialization cycles.

Returns:

  • (Hash)


533
534
535
536
537
538
539
540
# File 'lib/plan_my_stuff/base_project_item.rb', line 533

def as_json(options = {})
  merged_except = Array.wrap(options[:except]) + ['project']
  merged_methods = Array.wrap(options[:methods]) + [:draft?]
  super(options.merge(except: merged_except, methods: merged_methods)).merge(
    'project_id' => project&.id,
    'project_number' => project&.number,
  )
end

#assign!(assignees, user: nil) ⇒ void

This method returns an undefined value.

Assigns users to this item on its parent project.

Parameters:

  • assignees (String, Array<String>)

    GitHub username(s)



509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/plan_my_stuff/base_project_item.rb', line 509

def assign!(assignees, user: nil)
  assignee_list = Array.wrap(assignees)

  self.class.assign(
    number: number,
    content_node_id: content_node_id,
    assignees: assignee_list,
    draft: draft?,
    repo: repo,
  )

  PlanMyStuff::Notifications.instrument(
    'project_item.assigned',
    self,
    user: user,
    assignees: assignee_list,
  )
end

#bodyString?

Returns user-visible body (metadata comment stripped).

Returns:

  • (String, nil)

    user-visible body (metadata comment stripped)



33
# File 'lib/plan_my_stuff/base_project_item.rb', line 33

attribute :body, :string

#content_node_idString?

Returns node ID of the underlying content (Issue, PR, or DraftIssue).

Returns:

  • (String, nil)

    node ID of the underlying content (Issue, PR, or DraftIssue)



25
# File 'lib/plan_my_stuff/base_project_item.rb', line 25

attribute :content_node_id, :string

#destroy!(user: nil) ⇒ String?

Deletes this item from its parent project. Marks the in-memory instance as destroyed so destroyed? returns true and persisted? returns false.

No-op if the instance is already destroyed.

Returns:

  • (String, nil)

    the deleted item ID, or nil if already destroyed

Raises:



490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/plan_my_stuff/base_project_item.rb', line 490

def destroy!(user: nil)
  return if destroyed?

  deleted_id = self.class.delete_item(
    project_number: project.number,
    item_id: id,
  )

  PlanMyStuff::Notifications.instrument('project_item.removed', self, user: user)
  destroyed!
  deleted_id
end

#draft?Boolean

Returns:

  • (Boolean)


543
544
545
# File 'lib/plan_my_stuff/base_project_item.rb', line 543

def draft?
  type == 'DRAFT_ISSUE'
end

#field_valuesHash

Returns:

  • (Hash)


47
# File 'lib/plan_my_stuff/base_project_item.rb', line 47

attribute :field_values, default: -> { {} }

#idString?

Returns GitHub node ID (e.g. “PVTI_…”).

Returns:

  • (String, nil)

    GitHub node ID (e.g. “PVTI_…”)



23
# File 'lib/plan_my_stuff/base_project_item.rb', line 23

attribute :id, :string

#issuePlanMyStuff::Issue?

Returns:



51
# File 'lib/plan_my_stuff/base_project_item.rb', line 51

attribute :issue

#issue=(val) ⇒ void

This method returns an undefined value.



548
549
550
551
# File 'lib/plan_my_stuff/base_project_item.rb', line 548

def issue=(val)
  @issue_assigned = true
  super
end

#metadataPlanMyStuff::ProjectItemMetadata

Returns parsed metadata.

Returns:



35
# File 'lib/plan_my_stuff/base_project_item.rb', line 35

attribute :metadata, default: -> { PlanMyStuff::ProjectItemMetadata.new }

#move_to!(status, user: nil) ⇒ Hash

Moves this item to a new status column on its parent project.

Parameters:

  • status (String)

    status name (e.g. “In Progress”, “Done”)

Returns:

  • (Hash)

    mutation result



445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/plan_my_stuff/base_project_item.rb', line 445

def move_to!(status, user: nil)
  previous_status = self.status
  result = self.class.move_item(
    project_number: project.number,
    item_id: id,
    status: status,
  )

  PlanMyStuff::Notifications.instrument(
    'project_item.status_changed',
    self,
    user: user,
    status: status,
    previous_status: previous_status,
  )

  result
end

#numberInteger?

Returns:

  • (Integer, nil)


37
# File 'lib/plan_my_stuff/base_project_item.rb', line 37

attribute :number, :integer

#projectPlanMyStuff::BaseProject?

Returns:



49
# File 'lib/plan_my_stuff/base_project_item.rb', line 49

attribute :project

#raw_bodyString?

Returns full body as stored on GitHub (draft items only).

Returns:

  • (String, nil)

    full body as stored on GitHub (draft items only)



31
# File 'lib/plan_my_stuff/base_project_item.rb', line 31

attribute :raw_body, :string

#repoPlanMyStuff::Repo?

Returns:



41
# File 'lib/plan_my_stuff/base_project_item.rb', line 41

attribute :repo

#stateString?

Returns:

  • (String, nil)


43
# File 'lib/plan_my_stuff/base_project_item.rb', line 43

attribute :state, :string

#statusString?

Returns:

  • (String, nil)


45
# File 'lib/plan_my_stuff/base_project_item.rb', line 45

attribute :status, :string

#titleString?

Returns:

  • (String, nil)


29
# File 'lib/plan_my_stuff/base_project_item.rb', line 29

attribute :title, :string

#typeString?

Returns GitHub item type (e.g. “DRAFT_ISSUE”, “ISSUE”, “PULL_REQUEST”).

Returns:

  • (String, nil)

    GitHub item type (e.g. “DRAFT_ISSUE”, “ISSUE”, “PULL_REQUEST”)



27
# File 'lib/plan_my_stuff/base_project_item.rb', line 27

attribute :type, :string

#update_field!(field_name, value) ⇒ Hash

Updates a text custom field on this item.

Parameters:

  • field_name (String)

    text field name (e.g. “Deployment ID”)

  • value (String)

    text value to set

Returns:

  • (Hash)

    mutation result



471
472
473
474
475
476
477
478
# File 'lib/plan_my_stuff/base_project_item.rb', line 471

def update_field!(field_name, value)
  self.class.update_field!(
    project_number: project.number,
    item_id: id,
    field_name: field_name,
    value: value,
  )
end

#urlString?

Returns:

  • (String, nil)


39
# File 'lib/plan_my_stuff/base_project_item.rb', line 39

attribute :url, :string