diff --git a/client/playbook.go b/client/playbook.go index 4dd3cee31b..dbbb531abd 100644 --- a/client/playbook.go +++ b/client/playbook.go @@ -82,12 +82,20 @@ type ChecklistItem struct { LastSkipped int64 `json:"delete_at"` DueDate int64 `json:"due_date"` TaskActions []TaskAction `json:"task_actions"` + Requirements []TaskRequirement `json:"requirements"` ConditionID string `json:"condition_id"` ConditionAction string `json:"condition_action"` ConditionReason string `json:"condition_reason"` UpdateAt int64 `json:"update_at"` } +// TaskRequirement is a labeled field that must be completed when checking off a task. +type TaskRequirement struct { + ID string `json:"id"` + Label string `json:"label"` + Value string `json:"value"` +} + // TaskAction represents a task action in an item type TaskAction struct { Trigger TriggerAction `json:"trigger"` diff --git a/client/settings.go b/client/settings.go index 10de967c5d..c42faf6983 100644 --- a/client/settings.go +++ b/client/settings.go @@ -10,6 +10,7 @@ import ( type GlobalSettings struct { EnableExperimentalFeatures bool `json:"enable_experimental_features"` + EnableTaskRequirements bool `json:"enable_task_requirements"` } // SettingsService handles communication with the settings related methods. diff --git a/plugin.json b/plugin.json index c5d51c698c..246ce9e2a0 100644 --- a/plugin.json +++ b/plugin.json @@ -55,6 +55,15 @@ "display_name": "Expose Playbooks MCP tools externally (experimental)", "help_text": "Requires Enable Experimental Features to be enabled. When enabled, allows the Agents plugin to expose Playbooks MCP tools through its external MCP endpoint. The Agents plugin admin settings must also allow and enable the server/tools.", "default": false + }, + { + "key": "BetaFeatures", + "type": "custom", + "display_name": "Beta Features", + "help_text": "Opt-in beta features. Expand the section to enable individual features.", + "default": { + "task_requirements": false + } } ] } diff --git a/server/api/graphql_playbook.go b/server/api/graphql_playbook.go index b42dc39614..52581f2368 100644 --- a/server/api/graphql_playbook.go +++ b/server/api/graphql_playbook.go @@ -240,8 +240,9 @@ type UpdateChecklistItem struct { Description string `json:"description"` LastSkipped float64 `json:"delete_at"` DueDate float64 `json:"due_date"` - TaskActions *[]app.TaskAction `json:"task_actions"` - ConditionID string `json:"condition_id"` + TaskActions *[]app.TaskAction `json:"task_actions"` + ConditionID string `json:"condition_id"` + Requirements *[]app.TaskRequirement `json:"requirements"` } func (ci *UpdateChecklistItem) GetAssigneeID() string { diff --git a/server/api/playbook_runs.go b/server/api/playbook_runs.go index bef201bafe..baf031cfbe 100644 --- a/server/api/playbook_runs.go +++ b/server/api/playbook_runs.go @@ -1464,7 +1464,8 @@ func (h *PlaybookRunHandler) itemSetState(c *Context, w http.ResponseWriter, r * userID := r.Header.Get("Mattermost-User-ID") var params struct { - NewState string `json:"new_state"` + NewState string `json:"new_state"` + RequirementValues map[string]string `json:"requirement_values"` } if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal", err) @@ -1476,7 +1477,12 @@ func (h *PlaybookRunHandler) itemSetState(c *Context, w http.ResponseWriter, r * return } - if err := h.playbookRunService.ModifyCheckedState(id, userID, params.NewState, checklistNum, itemNum); err != nil { + var opts []app.ModifyCheckedStateOptions + if params.RequirementValues != nil { + opts = append(opts, app.ModifyCheckedStateOptions{RequirementValues: params.RequirementValues}) + } + + if err := h.playbookRunService.ModifyCheckedState(id, userID, params.NewState, checklistNum, itemNum, opts...); err != nil { h.HandleError(w, c.logger, err) return } diff --git a/server/api/schema.graphqls b/server/api/schema.graphqls index bd3137f872..2f62ed8a18 100644 --- a/server/api/schema.graphqls +++ b/server/api/schema.graphqls @@ -117,6 +117,13 @@ input ChecklistItemUpdates { dueDate: Float! taskActions: [TaskActionUpdates!] conditionID: String! + requirements: [TaskRequirementUpdates!] +} + +input TaskRequirementUpdates { + id: String! + label: String! + value: String! } input TaskActionUpdates { @@ -214,6 +221,13 @@ type ChecklistItem { conditionID: String! conditionAction: String! conditionReason: String! + requirements: [TaskRequirement!]! +} + +type TaskRequirement { + id: String! + label: String! + value: String! } type TaskAction { diff --git a/server/api/settings.go b/server/api/settings.go index 535067a11c..f123f77d7d 100644 --- a/server/api/settings.go +++ b/server/api/settings.go @@ -37,6 +37,7 @@ func NewSettingsHandler(router *mux.Router, api *pluginapi.Client, configService func (h *SettingsHandler) getSettings(w http.ResponseWriter, r *http.Request) { settings := client.GlobalSettings{ EnableExperimentalFeatures: h.config.IsExperimentalFeaturesEnabled(), + EnableTaskRequirements: h.config.IsTaskRequirementsEnabled(), } ReturnJSON(w, &settings, http.StatusOK) diff --git a/server/app/permissions_service_test.go b/server/app/permissions_service_test.go index fe29737c86..5c8cef036e 100644 --- a/server/app/permissions_service_test.go +++ b/server/app/permissions_service_test.go @@ -457,7 +457,7 @@ func (s *stubRunService) IsOwner(string, string) bool { func (s *stubRunService) ChangeOwner(string, string, string) error { panic("stubRunService: ChangeOwner not implemented") } -func (s *stubRunService) ModifyCheckedState(string, string, string, int, int) error { +func (s *stubRunService) ModifyCheckedState(string, string, string, int, int, ...ModifyCheckedStateOptions) error { panic("stubRunService: ModifyCheckedState not implemented") } func (s *stubRunService) ToggleCheckedState(string, string, int, int) error { diff --git a/server/app/playbook.go b/server/app/playbook.go index 975fe56505..6bea905a10 100644 --- a/server/app/playbook.go +++ b/server/app/playbook.go @@ -342,6 +342,9 @@ type ChecklistItem struct { // TaskActions is an array of all the task actions associated with this task. TaskActions []TaskAction `json:"task_actions" export:"-"` + // Requirements are fields that must be filled when checking off this task in a run. + Requirements []TaskRequirement `json:"requirements" export:"requirements"` + // UpdateAt is when this checklist item was last modified UpdateAt int64 `json:"update_at" export:"-"` @@ -355,6 +358,13 @@ type ChecklistItem struct { ConditionReason string `json:"condition_reason" export:"-"` } +// TaskRequirement is a labeled field that must be completed when checking off a task. +type TaskRequirement struct { + ID string `json:"id" export:"id"` + Label string `json:"label" export:"label"` + Value string `json:"value" export:"-"` +} + func (ci *ChecklistItem) GetAssigneeID() string { return ci.AssigneeID } diff --git a/server/app/playbook_run.go b/server/app/playbook_run.go index 31de77f547..40f74c87f2 100644 --- a/server/app/playbook_run.go +++ b/server/app/playbook_run.go @@ -795,6 +795,9 @@ func GetChecklistItemUpdates(previous, current []ChecklistItem) ItemChanges { if !reflect.DeepEqual(prev.TaskActions, item.TaskActions) { fields["task_actions"] = item.TaskActions } + if !reflect.DeepEqual(prev.Requirements, item.Requirements) { + fields["requirements"] = item.Requirements + } if prev.UpdateAt != item.UpdateAt { fields["update_at"] = item.UpdateAt } @@ -1254,6 +1257,12 @@ const ( TriggerTypeStatusUpdatePosted = "status_update_posted" ) +// ModifyCheckedStateOptions holds optional parameters for ModifyCheckedState. +type ModifyCheckedStateOptions struct { + // RequirementValues maps requirement IDs to the values filled in when checking off a task. + RequirementValues map[string]string +} + // PlaybookRunService is the playbook run service interface. type PlaybookRunService interface { // GetPlaybookRuns returns filtered playbook runs and the total count before paging. @@ -1321,8 +1330,9 @@ type PlaybookRunService interface { ChangeOwner(playbookRunID string, userID string, ownerID string) error // ModifyCheckedState modifies the state of the specified checklist item - // Idempotent, will not perform any actions if the checklist item is already in the specified state - ModifyCheckedState(playbookRunID, userID, newState string, checklistNumber int, itemNumber int) error + // Idempotent, will not perform any actions if the checklist item is already in the specified state. + // Optional opts may include requirement values to apply when checking off a task. + ModifyCheckedState(playbookRunID, userID, newState string, checklistNumber int, itemNumber int, opts ...ModifyCheckedStateOptions) error // ToggleCheckedState checks or unchecks the specified checklist item ToggleCheckedState(playbookRunID, userID string, checklistNumber, itemNumber int) error diff --git a/server/app/playbook_run_service.go b/server/app/playbook_run_service.go index da19649d26..ec1d31e15c 100644 --- a/server/app/playbook_run_service.go +++ b/server/app/playbook_run_service.go @@ -2265,8 +2265,10 @@ func (s *PlaybookRunServiceImpl) ChangeOwner(playbookRunID, userID, ownerID stri } // ModifyCheckedState checks or unchecks the specified checklist item. Idempotent, will not perform -// any action if the checklist item is already in the given checked state -func (s *PlaybookRunServiceImpl) ModifyCheckedState(playbookRunID, userID, newState string, checklistNumber, itemNumber int) error { +// any action if the checklist item is already in the given checked state. +// When checking off a task that has requirements, opts may supply RequirementValues; all +// requirements must have non-empty values or the call fails. +func (s *PlaybookRunServiceImpl) ModifyCheckedState(playbookRunID, userID, newState string, checklistNumber, itemNumber int, opts ...ModifyCheckedStateOptions) error { auditRec := plugin.MakeAuditRecord("modifyChecklistItemState", model.AuditStatusFail) defer s.api.LogAuditRec(auditRec) @@ -2302,11 +2304,46 @@ func (s *PlaybookRunServiceImpl) ModifyCheckedState(playbookRunID, userID, newSt originalRun = playbookRunToModify.Clone() } - if newState == itemToCheck.State { + var requirementValues map[string]string + if len(opts) > 0 { + requirementValues = opts[0].RequirementValues + } + + wasClosed := itemToCheck.State == ChecklistItemStateClosed + + requirementsChanged := false + if len(itemToCheck.Requirements) > 0 && requirementValues != nil { + reqs := make([]TaskRequirement, len(itemToCheck.Requirements)) + copy(reqs, itemToCheck.Requirements) + itemToCheck.Requirements = reqs + for i := range itemToCheck.Requirements { + if val, ok := requirementValues[itemToCheck.Requirements[i].ID]; ok { + if itemToCheck.Requirements[i].Value != val { + itemToCheck.Requirements[i].Value = val + requirementsChanged = true + } + } + } + } + + // Only require all fields when checking off while beta features are enabled. + // Saving values alone may leave some fields empty. + if newState == ChecklistItemStateClosed && !wasClosed && len(itemToCheck.Requirements) > 0 && s.configService.IsTaskRequirementsEnabled() { + for _, req := range itemToCheck.Requirements { + if strings.TrimSpace(req.Value) == "" { + return errors.Wrap(ErrMalformedPlaybookRun, "all task requirements must be filled before checking off") + } + } + } + + if newState == itemToCheck.State && !requirementsChanged { auditRec.Success() return nil } + stateChanged := newState != itemToCheck.State + timestamp := model.GetMillis() + details := Details{ Action: "check", Task: stripmd.Strip(itemToCheck.Title), @@ -2326,9 +2363,10 @@ func (s *PlaybookRunServiceImpl) ModifyCheckedState(playbookRunID, userID, newSt modifyMessage = fmt.Sprintf("restored checklist item **%v**", stripmd.Strip(itemToCheck.Title)) } - itemToCheck.State = newState - timestamp := model.GetMillis() - itemToCheck.StateModified = timestamp + if stateChanged { + itemToCheck.State = newState + itemToCheck.StateModified = timestamp + } updateChecklistAndItemTimestamp(&playbookRunToModify.Checklists[checklistNumber], &itemToCheck, timestamp) playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] = itemToCheck @@ -2337,23 +2375,25 @@ func (s *PlaybookRunServiceImpl) ModifyCheckedState(playbookRunID, userID, newSt return errors.Wrapf(err, "failed to update playbook run, is now in inconsistent state") } - detailsJSON, err := json.Marshal(details) - if err != nil { - return errors.Wrap(err, "failed to encode timeline event details") - } + if stateChanged { + detailsJSON, err := json.Marshal(details) + if err != nil { + return errors.Wrap(err, "failed to encode timeline event details") + } - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: itemToCheck.StateModified, - EventAt: itemToCheck.StateModified, - EventType: TaskStateModified, - Summary: modifyMessage, - SubjectUserID: userID, - Details: string(detailsJSON), - } + event := &TimelineEvent{ + PlaybookRunID: playbookRunID, + CreateAt: itemToCheck.StateModified, + EventAt: itemToCheck.StateModified, + EventType: TaskStateModified, + Summary: modifyMessage, + SubjectUserID: userID, + Details: string(detailsJSON), + } - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") + if _, err = s.store.CreateTimelineEvent(event); err != nil { + return errors.Wrap(err, "failed to create timeline event") + } } s.sendPlaybookRunObjectUpdatedWS(playbookRunID, originalRun, nil) diff --git a/server/app/task_requirements_test.go b/server/app/task_requirements_test.go new file mode 100644 index 0000000000..d36bae8ff9 --- /dev/null +++ b/server/app/task_requirements_test.go @@ -0,0 +1,53 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetChecklistItemUpdates_Requirements(t *testing.T) { + previous := []ChecklistItem{ + { + ID: "item-1", + Title: "Task", + Requirements: []TaskRequirement{ + {ID: "req-1", Label: "Ticket URL", Value: ""}, + }, + }, + } + current := []ChecklistItem{ + { + ID: "item-1", + Title: "Task", + Requirements: []TaskRequirement{ + {ID: "req-1", Label: "Ticket URL", Value: "https://example.com"}, + }, + }, + } + + updates := GetChecklistItemUpdates(previous, current) + require.Len(t, updates.Updates, 1) + assert.Equal(t, "item-1", updates.Updates[0].ID) + + reqs, ok := updates.Updates[0].Fields["requirements"] + require.True(t, ok, "requirements field should be included when values change") + assert.Equal(t, current[0].Requirements, reqs) +} + +func TestGetChecklistItemUpdates_RequirementsUnchanged(t *testing.T) { + item := ChecklistItem{ + ID: "item-1", + Title: "Task", + Requirements: []TaskRequirement{ + {ID: "req-1", Label: "Ticket URL", Value: "abc"}, + }, + } + + updates := GetChecklistItemUpdates([]ChecklistItem{item}, []ChecklistItem{item}) + assert.Empty(t, updates.Updates) +} diff --git a/server/config/beta_features_test.go b/server/config/beta_features_test.go new file mode 100644 index 0000000000..79ed357566 --- /dev/null +++ b/server/config/beta_features_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsTaskRequirementsEnabled(t *testing.T) { + t.Run("disabled by default", func(t *testing.T) { + svc := &ServiceImpl{configuration: &Configuration{}} + assert.False(t, svc.IsTaskRequirementsEnabled()) + }) + + t.Run("enabled when task_requirements is true", func(t *testing.T) { + svc := &ServiceImpl{ + configuration: &Configuration{ + BetaFeatures: BetaFeaturesConfig{TaskRequirements: true}, + }, + } + assert.True(t, svc.IsTaskRequirementsEnabled()) + }) +} + +func TestSerializeIncludesBetaFeatures(t *testing.T) { + cfg := &Configuration{ + BetaFeatures: BetaFeaturesConfig{TaskRequirements: true}, + } + serialized := cfg.serialize() + + raw, ok := serialized["BetaFeatures"] + require.True(t, ok, "serialize() must include BetaFeatures key matching plugin.json") + + beta, ok := raw.(BetaFeaturesConfig) + require.True(t, ok) + assert.True(t, beta.TaskRequirements) +} diff --git a/server/config/config.go b/server/config/config.go index 97141e32d9..43b3922ff8 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -46,4 +46,7 @@ type Service interface { // IsExperimentalFeaturesEnabled returns true when experimental features are enabled. IsExperimentalFeaturesEnabled() bool + + // IsTaskRequirementsEnabled returns true when the task requirements beta feature is enabled. + IsTaskRequirementsEnabled() bool } diff --git a/server/config/configuration.go b/server/config/configuration.go index 470190cade..1d87d6ff86 100644 --- a/server/config/configuration.go +++ b/server/config/configuration.go @@ -32,11 +32,20 @@ type Configuration struct { // These features may have in-progress UI, bugs, and other issues. EnableExperimentalFeatures bool `json:"enableexperimentalfeatures"` + // BetaFeatures holds individual beta feature toggles (task requirements, etc.). + // Stored as a JSON object in plugin settings; disabled by default. + BetaFeatures BetaFeaturesConfig `json:"betafeatures"` + // ExposeMCPExternal controls whether the Playbooks MCP tools may be exposed // through the Agents plugin's external MCP endpoint. ExposeMCPExternal bool `json:"exposemcpexternal"` } +// BetaFeaturesConfig stores per-feature beta toggles from the System Console accordion. +type BetaFeaturesConfig struct { + TaskRequirements bool `json:"task_requirements"` +} + // Clone shallow copies the configuration. Your implementation may require a deep copy if // your configuration has reference types. func (c *Configuration) Clone() *Configuration { @@ -55,6 +64,7 @@ func (c *Configuration) serialize() map[string]any { ret["TeamsTabAppBotUserID"] = c.TeamsTabAppBotUserID ret["enableincrementalupdates"] = c.EnableIncrementalUpdates ret["EnableExperimentalFeatures"] = c.EnableExperimentalFeatures + ret["BetaFeatures"] = c.BetaFeatures ret["ExposeMCPExternal"] = c.ExposeMCPExternal return ret } diff --git a/server/config/service.go b/server/config/service.go index fd0cad9897..5584244c2d 100644 --- a/server/config/service.go +++ b/server/config/service.go @@ -199,6 +199,9 @@ func (c *ServiceImpl) OnConfigurationChange() error { if oldConfig.EnableExperimentalFeatures != configuration.EnableExperimentalFeatures { settingsPayload["enable_experimental_features"] = configuration.EnableExperimentalFeatures } + if oldConfig.BetaFeatures.TaskRequirements != configuration.BetaFeatures.TaskRequirements { + settingsPayload["enable_task_requirements"] = configuration.BetaFeatures.TaskRequirements + } } if c.websocketPublisher != nil && len(settingsPayload) > 0 { @@ -297,3 +300,8 @@ func (c *ServiceImpl) IsIncrementalUpdatesEnabled() bool { func (c *ServiceImpl) IsExperimentalFeaturesEnabled() bool { return c.GetConfiguration().EnableExperimentalFeatures } + +// IsTaskRequirementsEnabled returns true when the task requirements beta feature is enabled. +func (c *ServiceImpl) IsTaskRequirementsEnabled() bool { + return c.GetConfiguration().BetaFeatures.TaskRequirements +} diff --git a/webapp/src/client.ts b/webapp/src/client.ts index 25f18fe069..bce2331e55 100644 --- a/webapp/src/client.ts +++ b/webapp/src/client.ts @@ -375,11 +375,19 @@ export async function setDueDate(playbookRunId: string, checklistNum: number, it } } -export async function setChecklistItemState(playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState, itemID?: string) { +export async function setChecklistItemState( + playbookRunID: string, + checklistNum: number, + itemNum: number, + newState: ChecklistItemState, + itemID?: string, + requirementValues?: Record, +) { // Include item ID in request body when available (for incremental updates) const body = JSON.stringify({ new_state: newState, ...(itemID && {item_id: itemID}), + ...(requirementValues && {requirement_values: requirementValues}), }); try { return await doPut(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/state`, body); diff --git a/webapp/src/components/backstage/beta_features_setting.test.tsx b/webapp/src/components/backstage/beta_features_setting.test.tsx new file mode 100644 index 0000000000..ed5b6e8b50 --- /dev/null +++ b/webapp/src/components/backstage/beta_features_setting.test.tsx @@ -0,0 +1,67 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import renderer, {act, ReactTestRendererJSON} from 'react-test-renderer'; + +import BetaFeaturesSetting from './beta_features_setting'; + +jest.mock('react-intl', () => ({ + useIntl: () => ({ + formatMessage: () => 'translated', + }), +})); + +describe('BetaFeaturesSetting', () => { + const baseProps = { + id: 'BetaFeatures', + disabled: false, + onChange: jest.fn(), + setSaveNeeded: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders collapsed accordion by default', () => { + const component = renderer.create( + , + ); + const tree = component.toJSON() as ReactTestRendererJSON; + expect(tree.props['data-testid']).toBe('beta-features-accordion'); + expect(JSON.stringify(tree)).toContain('translated'); + expect(JSON.stringify(tree)).not.toContain('beta-feature-task-requirements'); + }); + + it('expands and toggles task requirements', () => { + const onChange = jest.fn(); + const setSaveNeeded = jest.fn(); + const component = renderer.create( + , + ); + + const header = component.root.findByProps({'aria-expanded': false}); + act(() => { + header.props.onClick(); + }); + + const checkbox = component.root.findByProps({'data-testid': 'beta-feature-task-requirements'}); + expect(checkbox.props.checked).toBe(false); + + act(() => { + checkbox.props.onChange(); + }); + + expect(onChange).toHaveBeenCalledWith('BetaFeatures', {task_requirements: true}); + expect(setSaveNeeded).toHaveBeenCalled(); + }); +}); diff --git a/webapp/src/components/backstage/beta_features_setting.tsx b/webapp/src/components/backstage/beta_features_setting.tsx new file mode 100644 index 0000000000..583208e633 --- /dev/null +++ b/webapp/src/components/backstage/beta_features_setting.tsx @@ -0,0 +1,227 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useMemo, useState} from 'react'; +import {useIntl} from 'react-intl'; +import styled from 'styled-components'; + +export type BetaFeaturesValue = { + task_requirements?: boolean; +}; + +type Props = { + id: string; + value?: BetaFeaturesValue | null; + disabled?: boolean; + onChange: (id: string, value: BetaFeaturesValue) => void; + setSaveNeeded: () => void; +}; + +const defaultValue: BetaFeaturesValue = { + task_requirements: false, +}; + +const BetaFeaturesSetting = ({id, value, disabled, onChange, setSaveNeeded}: Props) => { + const {formatMessage} = useIntl(); + const [expanded, setExpanded] = useState(false); + + const features = useMemo(() => ({ + ...defaultValue, + ...(value || {}), + }), [value]); + + const enabledCount = Object.values(features).filter(Boolean).length; + + const toggleFeature = (key: keyof BetaFeaturesValue) => { + if (disabled) { + return; + } + const next = { + ...features, + [key]: !features[key], + }; + onChange(id, next); + setSaveNeeded(); + }; + + return ( + + setExpanded((v) => !v)} + aria-expanded={expanded} + disabled={disabled} + > + + + + + {formatMessage({defaultMessage: 'Beta Features'})} + + + {formatMessage({defaultMessage: 'Opt-in features that are still being refined and may change.'})} + + + + + {enabledCount === 0 ? formatMessage({defaultMessage: 'None enabled'}) : formatMessage( + {defaultMessage: '{count} enabled'}, + {count: enabledCount}, + )} + + + {expanded && ( + + + + toggleFeature('task_requirements')} + data-testid='beta-feature-task-requirements' + /> + + + {formatMessage({defaultMessage: 'Task requirements'})} + + + {formatMessage({defaultMessage: 'Require users to fill in labeled fields when checking off a task.'})} + + + + + + )} + + ); +}; + +const Accordion = styled.div` + width: 100%; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.12); + border-radius: 4px; + background: var(--center-channel-bg); + overflow: hidden; +`; + +const AccordionHeader = styled.button` + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 16px 20px; + border: none; + background: transparent; + color: var(--center-channel-color); + cursor: pointer; + text-align: left; + + &:disabled { + cursor: default; + opacity: 0.64; + } + + &:not(:disabled):hover { + background: rgba(var(--center-channel-color-rgb), 0.04); + } +`; + +const HeaderLeft = styled.div` + display: flex; + min-width: 0; + align-items: flex-start; + gap: 8px; +`; + +const Chevron = styled.i` + margin-top: 2px; + color: rgba(var(--center-channel-color-rgb), 0.56); + font-size: 16px; +`; + +const HeaderText = styled.div` + display: flex; + min-width: 0; + flex-direction: column; + gap: 4px; +`; + +const Title = styled.div` + font-size: 14px; + font-weight: 600; + line-height: 20px; +`; + +const Subtitle = styled.div` + color: rgba(var(--center-channel-color-rgb), 0.72); + font-size: 12px; + font-weight: 400; + line-height: 16px; +`; + +const Badge = styled.div` + flex-shrink: 0; + color: rgba(var(--center-channel-color-rgb), 0.64); + font-size: 12px; + font-weight: 600; + line-height: 16px; + white-space: nowrap; +`; + +const AccordionBody = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + padding: 0 20px 16px 44px; + border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08); +`; + +const FeatureRow = styled.div` + padding-top: 12px; +`; + +const CheckboxLabel = styled.label` + display: flex; + align-items: flex-start; + gap: 10px; + margin: 0; + cursor: pointer; + font-weight: normal; +`; + +const Checkbox = styled.input` + width: 16px; + height: 16px; + flex-shrink: 0; + margin-top: 2px; + accent-color: var(--button-bg); + cursor: pointer; + + &:disabled { + cursor: default; + } +`; + +const FeatureCopy = styled.div` + display: flex; + min-width: 0; + flex-direction: column; + gap: 2px; +`; + +const FeatureName = styled.div` + color: var(--center-channel-color); + font-size: 14px; + font-weight: 600; + line-height: 20px; +`; + +const FeatureHelp = styled.div` + color: rgba(var(--center-channel-color-rgb), 0.72); + font-size: 12px; + line-height: 16px; +`; + +export default BetaFeaturesSetting; diff --git a/webapp/src/components/checklist/checklist_list.tsx b/webapp/src/components/checklist/checklist_list.tsx index ba77ca6371..492c7c1715 100644 --- a/webapp/src/components/checklist/checklist_list.tsx +++ b/webapp/src/components/checklist/checklist_list.tsx @@ -98,6 +98,11 @@ export const mapChecklistItemToInput = (ci: ChecklistItem) => ({ dueDate: ci.due_date, taskActions: ci.task_actions, conditionID: ci.condition_id, + requirements: (ci.requirements || []).map((r) => ({ + id: r.id, + label: r.label, + value: r.value || '', + })), }); const ChecklistList = ({ @@ -175,6 +180,7 @@ const ChecklistList = ({ assignee_id: ci.assignee_id || '', assignee_modified: ci.assignee_modified || 0, assignee_property_field_id: ci.assignee_property_field_id || '', + requirements: ci.requirements || [], }; }), }; diff --git a/webapp/src/components/checklist_item/checklist_item.test.tsx b/webapp/src/components/checklist_item/checklist_item.test.tsx index 6111cb1a95..4d701d1459 100644 --- a/webapp/src/components/checklist_item/checklist_item.test.tsx +++ b/webapp/src/components/checklist_item/checklist_item.test.tsx @@ -32,6 +32,10 @@ jest.mock('src/graphql/hooks', () => ({ useUpdateRunItemTaskActions: jest.fn(() => ({updateRunTaskActions: jest.fn()})), })); +jest.mock('src/hooks/redux', () => ({ + useAppSelector: jest.fn(() => false), +})); + jest.mock('src/client', () => ({ setAssignee: jest.fn(async () => ({})), setRoleAssignee: jest.fn(async () => ({})), diff --git a/webapp/src/components/checklist_item/checklist_item.tsx b/webapp/src/components/checklist_item/checklist_item.tsx index bf7c6acabc..8fb0b104fa 100644 --- a/webapp/src/components/checklist_item/checklist_item.tsx +++ b/webapp/src/components/checklist_item/checklist_item.tsx @@ -33,6 +33,7 @@ import { ChecklistItemState, ChecklistItem as ChecklistItemType, TaskAction as TaskActionType, + TaskRequirement, isRoleBasedAssigneeType, } from 'src/types/playbook'; import {useUpdateRunItemTaskActions} from 'src/graphql/hooks'; @@ -47,6 +48,8 @@ import {DateTimeOption} from 'src/components/datetime_selector'; import {Mode} from 'src/components/datetime_input'; import AssigneeDropdown from 'src/components/checklists/assignee_dropdown'; +import {useAppSelector} from 'src/hooks/redux'; +import {selectTaskRequirementsEnabled} from 'src/selectors'; import ChecklistItemHoverMenu, {HoverMenu} from './hover_menu'; import ChecklistItemDescription from './description'; @@ -59,6 +62,9 @@ import ConditionIndicator from './condition_indicator'; import TaskActions from './task_actions'; import {haveAtleastOneEnabledAction} from './task_actions_modal'; +import EditRequirementsModal from './edit_requirements_modal'; +import FillRequirementsModal from './fill_requirements_modal'; +import RequirementsAccordion from './requirements_accordion'; export enum ButtonsFormat { @@ -83,7 +89,7 @@ interface ChecklistItemProps { playbookId?: string; teamId?: string; channelId?: string; - onChange?: (item: ChecklistItemState) => ReturnType | undefined; + onChange?: (item: ChecklistItemState, requirementValues?: Record) => ReturnType | undefined; draggableProvided?: DraggableProvided; dragging: boolean; readOnly: boolean; @@ -122,6 +128,7 @@ export const ChecklistItem = (props: ChecklistItemProps): React.ReactElement => const {formatMessage} = useIntl(); const toaster = useToaster(); const isPlaybookEditor = !props.playbookRunId; + const betaFeaturesEnabled = useAppSelector(selectTaskRequirementsEnabled); const isMounted = useRef(true); useEffect(() => { return () => { @@ -169,8 +176,13 @@ export const ChecklistItem = (props: ChecklistItemProps): React.ReactElement => const [assigneeType, setAssigneeType] = useState(props.checklistItem.assignee_type || ''); const [assigneePropertyFieldID, setAssigneePropertyFieldID] = useState(props.checklistItem.assignee_property_field_id || ''); const [dueDate, setDueDate] = useState(props.checklistItem.due_date); + const [showAddRequirementModal, setShowAddRequirementModal] = useState(false); + const [showFillRequirementsModal, setShowFillRequirementsModal] = useState(false); + const [fillRequirementsEditMode, setFillRequirementsEditMode] = useState(false); const {updateRunTaskActions} = useUpdateRunItemTaskActions(props.playbookRunId); + const requirements = props.checklistItem.requirements || []; + const userPropertyFields = useMemo( () => props.propertyFields?.filter((f) => f.type === PropertyFieldType.User) ?? [], [props.propertyFields], @@ -500,6 +512,7 @@ export const ChecklistItem = (props: ChecklistItemProps): React.ReactElement => assignee_id: assigneeID, assignee_type: assigneeType, task_actions: taskActions, + requirements: props.checklistItem.requirements || [], state_modified: 0, assignee_modified: 0, condition_id: '', @@ -609,6 +622,8 @@ export const ChecklistItem = (props: ChecklistItemProps): React.ReactElement => availableConditions={props.availableConditions} propertyFields={props.propertyFields} isChannelChecklist={props.isChannelChecklist} + onAddRequirement={isPlaybookEditor && betaFeaturesEnabled ? () => setShowAddRequirementModal(true) : undefined} + hasRequirements={requirements.length > 0} /> } readOnly={props.readOnly} disabled={isSkipped() || props.playbookRunId === undefined || props.newItem} item={props.checklistItem} - onChange={(item: ChecklistItemState) => props.onChange?.(item)} + onChange={(item: ChecklistItemState) => { + if ( + betaFeaturesEnabled && + item === ChecklistItemState.Closed && + requirements.length > 0 && + props.playbookRunId + ) { + setFillRequirementsEditMode(false); + setShowFillRequirementsModal(true); + return Promise.resolve({cancelled: true}); + } + return props.onChange?.(item); + }} onReadOnlyInteract={props.onReadOnlyInteract} /> title={titleValue} /> } + {betaFeaturesEnabled && requirements.length > 0 && ( + { + setFillRequirementsEditMode(false); + setShowFillRequirementsModal(true); + } : undefined} + onEditValues={!isPlaybookEditor && props.playbookRunId ? () => { + setFillRequirementsEditMode(true); + setShowFillRequirementsModal(true); + } : undefined} + /> + )} {isEditing && renderAssigneeEditor()} {renderRow()} {isEditing && @@ -668,6 +711,42 @@ export const ChecklistItem = (props: ChecklistItemProps): React.ReactElement => /> } + {showAddRequirementModal && betaFeaturesEnabled && ( + { + props.onUpdateChecklistItem?.({ + ...props.checklistItem, + requirements: nextRequirements, + }); + setShowAddRequirementModal(false); + }} + onCancel={() => setShowAddRequirementModal(false)} + /> + )} + {showFillRequirementsModal && betaFeaturesEnabled && ( + ) => { + setShowFillRequirementsModal(false); + setFillRequirementsEditMode(false); + const currentState = (props.checklistItem.state || ChecklistItemState.Open) as ChecklistItemState; + props.onChange?.(currentState, values); + }} + onSaveAndComplete={(values: Record) => { + setShowFillRequirementsModal(false); + setFillRequirementsEditMode(false); + props.onChange?.(ChecklistItemState.Closed, values); + }} + onCancel={() => { + setShowFillRequirementsModal(false); + setFillRequirementsEditMode(false); + }} + /> + )} ); diff --git a/webapp/src/components/checklist_item/checklist_item_draggable.tsx b/webapp/src/components/checklist_item/checklist_item_draggable.tsx index 818653972f..32d4f65f65 100644 --- a/webapp/src/components/checklist_item/checklist_item_draggable.tsx +++ b/webapp/src/components/checklist_item/checklist_item_draggable.tsx @@ -64,8 +64,15 @@ const DraggableChecklistItem = (props: Props) => { teamId={props.playbookRun?.team_id} channelId={props.playbookRun?.channel_id} participantUserIds={props.playbookRun?.participant_ids ?? []} - onChange={(newState: ChecklistItemState) => { - return props.playbookRun && setChecklistItemState(props.playbookRun.id, props.checklistIndex, props.itemIndex, newState, props.item.id); + onChange={(newState: ChecklistItemState, requirementValues?: Record) => { + return props.playbookRun && setChecklistItemState( + props.playbookRun.id, + props.checklistIndex, + props.itemIndex, + newState, + props.item.id, + requirementValues, + ); }} draggableProvided={draggableProvided} dragging={snapshot.isDragging || snapshot.combineWith != null} diff --git a/webapp/src/components/checklist_item/edit_requirements_modal.tsx b/webapp/src/components/checklist_item/edit_requirements_modal.tsx new file mode 100644 index 0000000000..9eadf647f8 --- /dev/null +++ b/webapp/src/components/checklist_item/edit_requirements_modal.tsx @@ -0,0 +1,222 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState} from 'react'; +import {useIntl} from 'react-intl'; +import styled from 'styled-components'; + +import {generateId} from 'mattermost-redux/utils/helpers'; + +import {TaskRequirement} from 'src/types/playbook'; +import GenericModal from 'src/components/widgets/generic_modal'; + +type DraftRequirement = { + id: string; + label: string; + isNew?: boolean; +}; + +type Props = { + initialRequirements?: TaskRequirement[]; + onConfirm: (requirements: TaskRequirement[]) => void; + onCancel: () => void; +}; + +const EditRequirementsModal = ({initialRequirements = [], onConfirm, onCancel}: Props) => { + const {formatMessage} = useIntl(); + const isEditing = initialRequirements.length > 0; + + const [drafts, setDrafts] = useState(() => { + if (initialRequirements.length === 0) { + return [{id: generateId(), label: '', isNew: true}]; + } + return initialRequirements.map((r) => ({id: r.id, label: r.label})); + }); + + const trimmed = drafts + .map((d) => ({...d, label: d.label.trim()})) + .filter((d) => d.label !== ''); + + // When adding for the first time, require at least one. When editing, allow clearing all. + const canSave = isEditing || trimmed.length > 0; + + const updateLabel = (id: string, value: string) => { + setDrafts((prev) => prev.map((d) => (d.id === id ? {...d, label: value} : d))); + }; + + const addField = () => { + setDrafts((prev) => [...prev, {id: generateId(), label: '', isNew: true}]); + }; + + const removeField = (id: string) => { + setDrafts((prev) => { + const next = prev.filter((d) => d.id !== id); + return next.length === 0 ? [{id: generateId(), label: '', isNew: true}] : next; + }); + }; + + const handleSave = () => { + const valueById = new Map(initialRequirements.map((r) => [r.id, r.value || ''])); + onConfirm(trimmed.map((d) => ({ + id: d.id, + label: d.label, + value: valueById.get(d.id) || '', + }))); + }; + + return ( + + + {formatMessage({defaultMessage: 'Anyone checking off this task will be asked to fill in these fields. Add, edit, or remove text inputs.'})} + + + {drafts.map((draft, index) => ( + + + + updateLabel(draft.id, e.target.value)} + /> + {(drafts.length > 1 || (isEditing && draft.label.trim() !== '')) && ( + removeField(draft.id)} + data-testid={`remove-requirement-draft-${draft.id}`} + > + + + )} + + + ))} + + + + {formatMessage({defaultMessage: 'Add another requirement'})} + + + ); +}; + +const Description = styled.p` + margin: 0 0 16px; + color: rgba(var(--center-channel-color-rgb), 0.72); + font-size: 14px; + line-height: 20px; +`; + +const Fields = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const Field = styled.div` + display: flex; + flex-direction: column; +`; + +const Label = styled.label` + display: block; + margin-bottom: 8px; + color: var(--center-channel-color); + font-size: 14px; + font-weight: 600; + line-height: 20px; +`; + +const FieldRow = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const Input = styled.input` + width: 100%; + height: 40px; + box-sizing: border-box; + padding: 0 16px; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); + border-radius: 4px; + background: var(--center-channel-bg); + color: var(--center-channel-color); + font-size: 14px; + + &:focus { + border-color: var(--button-bg); + outline: none; + box-shadow: 0 0 0 1px var(--button-bg); + } +`; + +const RemoveButton = styled.button` + display: flex; + width: 32px; + height: 32px; + flex-shrink: 0; + align-items: center; + justify-content: center; + padding: 0; + border: none; + border-radius: 4px; + background: transparent; + color: rgba(var(--center-channel-color-rgb), 0.56); + cursor: pointer; + + &:hover { + background: rgba(var(--center-channel-color-rgb), 0.08); + color: var(--error-text); + } +`; + +const AddAnother = styled.button` + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 16px; + padding: 0; + border: none; + background: transparent; + color: var(--button-bg); + cursor: pointer; + font-size: 14px; + font-weight: 600; + line-height: 20px; + + &:hover { + text-decoration: underline; + } + + i { + font-size: 16px; + } +`; + +export default EditRequirementsModal; diff --git a/webapp/src/components/checklist_item/fill_requirements_modal.test.tsx b/webapp/src/components/checklist_item/fill_requirements_modal.test.tsx new file mode 100644 index 0000000000..ce4220350f --- /dev/null +++ b/webapp/src/components/checklist_item/fill_requirements_modal.test.tsx @@ -0,0 +1,129 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import renderer, {act} from 'react-test-renderer'; + +import FillRequirementsModal from './fill_requirements_modal'; + +jest.mock('react-intl', () => { + const reactIntl = jest.requireActual('react-intl'); + const intl = reactIntl.createIntl({locale: 'en'}); + return { + ...reactIntl, + useIntl: () => intl, + }; +}); + +jest.mock('src/components/widgets/generic_modal', () => { + return function MockGenericModal({children, footer}: {children: React.ReactNode; footer?: React.ReactNode}) { + return ( +
+ {children} + {footer} +
+ ); + }; +}); + +jest.mock('src/components/assets/buttons', () => ({ + PrimaryButton: ({children, onClick, ...rest}: any) => ( + + ), + TertiaryButton: ({children, onClick, ...rest}: any) => ( + + ), +})); + +const requirements = [ + {id: 'r1', label: 'Ticket URL', value: ''}, + {id: 'r2', label: 'Root cause', value: ''}, +]; + +describe('FillRequirementsModal', () => { + it('Save allows partial values', () => { + const onSave = jest.fn(); + const onSaveAndComplete = jest.fn(); + const component = renderer.create( + , + ); + + const input = component.root.findByProps({'data-testid': 'requirement-value-r1'}); + act(() => { + input.props.onChange({target: {value: 'https://example.com'}}); + }); + + const save = component.root.findByProps({'data-testid': 'modal-save-requirements'}); + act(() => { + save.props.onClick(); + }); + + expect(onSave).toHaveBeenCalledWith({r1: 'https://example.com', r2: ''}); + expect(onSaveAndComplete).not.toHaveBeenCalled(); + }); + + it('Save and mark complete shows errors when fields are empty', () => { + const onSave = jest.fn(); + const onSaveAndComplete = jest.fn(); + const component = renderer.create( + , + ); + + const complete = component.root.findByProps({'data-testid': 'modal-save-and-complete'}); + act(() => { + complete.props.onClick(); + }); + + expect(onSaveAndComplete).not.toHaveBeenCalled(); + expect(component.root.findByProps({'data-testid': 'requirement-error-r1'})).toBeTruthy(); + expect(component.root.findByProps({'data-testid': 'requirement-error-r2'})).toBeTruthy(); + }); + + it('Save and mark complete succeeds when all fields are filled', () => { + const onSaveAndComplete = jest.fn(); + const component = renderer.create( + , + ); + + act(() => { + component.root.findByProps({'data-testid': 'requirement-value-r1'}).props.onChange({target: {value: 'url'}}); + component.root.findByProps({'data-testid': 'requirement-value-r2'}).props.onChange({target: {value: 'cause'}}); + }); + + act(() => { + component.root.findByProps({'data-testid': 'modal-save-and-complete'}).props.onClick(); + }); + + expect(onSaveAndComplete).toHaveBeenCalledWith({r1: 'url', r2: 'cause'}); + }); +}); diff --git a/webapp/src/components/checklist_item/fill_requirements_modal.tsx b/webapp/src/components/checklist_item/fill_requirements_modal.tsx new file mode 100644 index 0000000000..3781e53543 --- /dev/null +++ b/webapp/src/components/checklist_item/fill_requirements_modal.tsx @@ -0,0 +1,217 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState} from 'react'; +import {useIntl} from 'react-intl'; +import styled from 'styled-components'; + +import {TaskRequirement} from 'src/types/playbook'; +import {PrimaryButton, TertiaryButton} from 'src/components/assets/buttons'; +import GenericModal from 'src/components/widgets/generic_modal'; + +type Props = { + taskTitle: string; + requirements: TaskRequirement[]; + /** When true, user opened via Edit; when false, via Complete / checkbox. */ + editMode?: boolean; + /** When true, task is already checked off — hide "mark complete". */ + isTaskComplete?: boolean; + onSave: (values: Record) => void; + onSaveAndComplete: (values: Record) => void; + onCancel: () => void; +}; + +const FillRequirementsModal = ({ + taskTitle, + requirements, + editMode, + isTaskComplete, + onSave, + onSaveAndComplete, + onCancel, +}: Props) => { + const {formatMessage} = useIntl(); + const [values, setValues] = useState>(() => { + const initial: Record = {}; + for (const req of requirements) { + initial[req.id] = req.value || ''; + } + return initial; + }); + const [errors, setErrors] = useState>({}); + + const getTrimmedValues = () => { + const trimmed: Record = {}; + for (const req of requirements) { + trimmed[req.id] = (values[req.id] || '').trim(); + } + return trimmed; + }; + + const validateAllFilled = (trimmed: Record) => { + const nextErrors: Record = {}; + for (const req of requirements) { + if (!trimmed[req.id]) { + nextErrors[req.id] = formatMessage({defaultMessage: 'This field is required to mark the task complete'}); + } + } + setErrors(nextErrors); + return Object.keys(nextErrors).length === 0; + }; + + const handleSave = () => { + setErrors({}); + onSave(getTrimmedValues()); + }; + + const handleSaveAndComplete = () => { + const trimmed = getTrimmedValues(); + if (!validateAllFilled(trimmed)) { + return; + } + onSaveAndComplete(trimmed); + }; + + const showMarkComplete = !isTaskComplete; + + return ( + + + {formatMessage({defaultMessage: 'Cancel'})} + + + {formatMessage({defaultMessage: 'Save'})} + + {showMarkComplete && ( + + {formatMessage({defaultMessage: 'Save and mark complete'})} + + )} + + )} + > + + {editMode || isTaskComplete ? formatMessage( + {defaultMessage: 'Update the required fields for “{taskTitle}”. Save anytime, or fill every field to mark the task complete.'}, + {taskTitle}, + ) : formatMessage( + {defaultMessage: 'Fill in the required fields for “{taskTitle}”. You can save a draft, or mark the task complete when all fields are filled.'}, + {taskTitle}, + )} + + + {requirements.map((req) => { + const hasError = Boolean(errors[req.id]); + return ( + + + { + const next = e.target.value; + setValues((prev) => ({...prev, [req.id]: next})); + if (errors[req.id] && next.trim()) { + setErrors((prev) => { + const {[req.id]: _, ...rest} = prev; + return rest; + }); + } + }} + /> + {hasError && ( + + {errors[req.id]} + + )} + + ); + })} + + + ); +}; + +const Description = styled.p` + margin: 0 0 16px; + color: rgba(var(--center-channel-color-rgb), 0.72); + font-size: 14px; + line-height: 20px; +`; + +const Fields = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const Field = styled.div` + display: flex; + flex-direction: column; +`; + +const Label = styled.label` + margin-bottom: 8px; + color: var(--center-channel-color); + font-size: 14px; + font-weight: 600; + line-height: 20px; +`; + +const Input = styled.input<{$hasError?: boolean}>` + width: 100%; + height: 40px; + box-sizing: border-box; + padding: 0 16px; + border: 1px solid ${({$hasError}) => ($hasError ? 'var(--error-text)' : 'rgba(var(--center-channel-color-rgb), 0.16)')}; + border-radius: 4px; + background: var(--center-channel-bg); + color: var(--center-channel-color); + font-size: 14px; + + &:focus { + border-color: ${({$hasError}) => ($hasError ? 'var(--error-text)' : 'var(--button-bg)')}; + outline: none; + box-shadow: 0 0 0 1px ${({$hasError}) => ($hasError ? 'var(--error-text)' : 'var(--button-bg)')}; + } +`; + +const ErrorText = styled.div` + margin-top: 4px; + color: var(--error-text); + font-size: 12px; + line-height: 16px; +`; + +const FooterButtons = styled.div` + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-end; +`; + +export default FillRequirementsModal; diff --git a/webapp/src/components/checklist_item/hover_menu.tsx b/webapp/src/components/checklist_item/hover_menu.tsx index 2f1302df8e..7e69e09ad9 100644 --- a/webapp/src/components/checklist_item/hover_menu.tsx +++ b/webapp/src/components/checklist_item/hover_menu.tsx @@ -60,6 +60,8 @@ export interface Props { availableConditions?: Condition[]; propertyFields?: PropertyField[]; isChannelChecklist?: boolean; + onAddRequirement?: () => void; + hasRequirements?: boolean; } const ChecklistItemHoverMenu = (props: Props) => { @@ -142,6 +144,15 @@ const ChecklistItemHoverMenu = (props: Props) => { {formatMessage({defaultMessage: 'Add condition'})} } + {props.playbookRunId === undefined && props.onAddRequirement && + props.onAddRequirement?.()} + > + + {props.hasRequirements ? formatMessage({defaultMessage: 'Edit requirements'}) : formatMessage({defaultMessage: 'Add a requirement'})} + + } {props.playbookRunId === undefined && props.hasCondition && props.onRemoveFromCondition && undefined | Promise; + onChange: (item: ChecklistItemState) => undefined | Promise; item: ChecklistItem; readOnly: boolean;//when true, component can receive events, but can't be modified. disabled?: boolean; @@ -41,7 +41,7 @@ export const CheckBoxButton = (props: CheckBoxButtonProps) => { const newValue = isChecked ? ChecklistItemState.Open : ChecklistItemState.Closed; setIsChecked(!isChecked); const res = await props.onChange(newValue); - if (res?.error) { + if (res?.error || res?.cancelled) { setIsChecked(isChecked); } }; diff --git a/webapp/src/components/checklist_item/requirements_accordion.test.tsx b/webapp/src/components/checklist_item/requirements_accordion.test.tsx new file mode 100644 index 0000000000..2106801673 --- /dev/null +++ b/webapp/src/components/checklist_item/requirements_accordion.test.tsx @@ -0,0 +1,65 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import renderer, {act} from 'react-test-renderer'; + +import RequirementsAccordion from './requirements_accordion'; + +jest.mock('react-intl', () => { + const reactIntl = jest.requireActual('react-intl'); + const intl = reactIntl.createIntl({locale: 'en'}); + return { + ...reactIntl, + useIntl: () => intl, + }; +}); + +const requirements = [ + {id: 'r1', label: 'Ticket URL', value: ''}, + {id: 'r2', label: 'Root cause', value: 'filled'}, +]; + +describe('RequirementsAccordion', () => { + it('returns null when there are no requirements', () => { + const component = renderer.create( + , + ); + expect(component.toJSON()).toBeNull(); + }); + + it('shows Complete when incomplete and no values filled', () => { + const onComplete = jest.fn(); + const component = renderer.create( + , + ); + + const complete = component.root.findByProps({'data-testid': 'complete-requirement-values'}); + expect(complete).toBeTruthy(); + expect(() => component.root.findByProps({'data-testid': 'edit-requirement-values'})).toThrow(); + + act(() => { + complete.props.onClick(); + }); + expect(onComplete).toHaveBeenCalled(); + }); + + it('hides Complete and shows Edit when any field is filled', () => { + const component = renderer.create( + , + ); + + expect(() => component.root.findByProps({'data-testid': 'complete-requirement-values'})).toThrow(); + expect(component.root.findByProps({'data-testid': 'edit-requirement-values'})).toBeTruthy(); + }); +}); diff --git a/webapp/src/components/checklist_item/requirements_accordion.tsx b/webapp/src/components/checklist_item/requirements_accordion.tsx new file mode 100644 index 0000000000..d1814e493e --- /dev/null +++ b/webapp/src/components/checklist_item/requirements_accordion.tsx @@ -0,0 +1,226 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState} from 'react'; +import {useIntl} from 'react-intl'; +import styled from 'styled-components'; + +import {TaskRequirement} from 'src/types/playbook'; + +type Props = { + requirements: TaskRequirement[]; + /** When true, show labels only (playbook editor). When false, show filled values (run). */ + editMode?: boolean; + isTaskComplete?: boolean; + /** Shown on runs when values can be edited. */ + onEditValues?: () => void; + /** Shown on runs when the task is not yet complete. */ + onComplete?: () => void; + readOnly?: boolean; +}; + +const RequirementsAccordion = ({ + requirements, + editMode, + isTaskComplete, + onEditValues, + onComplete, + readOnly, +}: Props) => { + const {formatMessage} = useIntl(); + const [expanded, setExpanded] = useState(false); + + if (!requirements?.length) { + return null; + } + + const filledCount = requirements.filter((r) => (r.value || '').trim() !== '').length; + const showValues = !editMode && filledCount > 0; + + const headerLabel = editMode ? formatMessage( + {defaultMessage: '{count, plural, one {# requirement} other {# requirements}}'}, + {count: requirements.length}, + ) : showValues ? formatMessage( + {defaultMessage: '{count, plural, one {# required field} other {# required fields}}'}, + {count: requirements.length}, + ) : formatMessage( + {defaultMessage: '{count, plural, one {# requirement} other {# requirements}}'}, + {count: requirements.length}, + ); + + return ( + + +
setExpanded((v) => !v)} + aria-expanded={expanded} + > + + {headerLabel} +
+ {!editMode && !readOnly && ( + + {!isTaskComplete && !showValues && onComplete && ( + + {formatMessage({defaultMessage: 'Complete'})} + + )} + {onEditValues && (showValues || isTaskComplete) && ( + + {formatMessage({defaultMessage: 'Edit'})} + + )} + + )} +
+ {expanded && ( + + {requirements.map((req) => ( + + {req.label} + {showValues || (!editMode && (req.value || '').trim() !== '') ? ( + {req.value || '—'} + ) : editMode ? ( + + {formatMessage({defaultMessage: 'Text input — filled when the task is checked off'})} + + ) : ( + + {formatMessage({defaultMessage: 'Not filled yet'})} + + )} + + ))} + + )} +
+ ); +}; + +const Container = styled.div` + margin: 12px 0 4px 36px; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.08); + border-radius: 4px; + background: rgba(var(--center-channel-color-rgb), 0.04); +`; + +const HeaderRow = styled.div` + display: flex; + align-items: center; + gap: 4px; + padding-right: 4px; +`; + +const Header = styled.button` + display: flex; + flex: 1; + min-width: 0; + align-items: center; + padding: 6px 8px; + border: none; + background: transparent; + color: rgba(var(--center-channel-color-rgb), 0.72); + cursor: pointer; + font-size: 12px; + font-weight: 600; + line-height: 16px; + text-align: left; + + &:hover { + color: var(--center-channel-color); + } +`; + +const Chevron = styled.i` + margin-right: 4px; + font-size: 12px; +`; + +const HeaderText = styled.span` + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const Actions = styled.div` + display: flex; + flex-shrink: 0; + align-items: center; + gap: 2px; +`; + +const ActionButton = styled.button` + flex-shrink: 0; + padding: 4px 8px; + border: none; + border-radius: 4px; + background: transparent; + color: var(--button-bg); + cursor: pointer; + font-size: 12px; + font-weight: 600; + line-height: 16px; + + &:hover { + background: rgba(var(--button-bg-rgb), 0.08); + } +`; + +const Body = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + padding: 4px 12px 12px 28px; +`; + +const Field = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +`; + +const ReqLabel = styled.div` + color: rgba(var(--center-channel-color-rgb), 0.72); + font-size: 12px; + font-weight: 600; + line-height: 16px; +`; + +const ReqValue = styled.div` + min-height: 32px; + box-sizing: border-box; + padding: 6px 10px; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.12); + border-radius: 4px; + background: var(--center-channel-bg); + color: var(--center-channel-color); + font-size: 13px; + font-weight: 400; + line-height: 18px; + overflow-wrap: anywhere; + white-space: pre-wrap; +`; + +const Placeholder = styled.div` + min-height: 32px; + box-sizing: border-box; + padding: 6px 10px; + border: 1px dashed rgba(var(--center-channel-color-rgb), 0.16); + border-radius: 4px; + color: rgba(var(--center-channel-color-rgb), 0.48); + font-size: 12px; + font-style: italic; + line-height: 18px; +`; + +export default RequirementsAccordion; diff --git a/webapp/src/graphql/generated/gql.ts b/webapp/src/graphql/generated/gql.ts index 80b2cf975a..32275d05b8 100644 --- a/webapp/src/graphql/generated/gql.ts +++ b/webapp/src/graphql/generated/gql.ts @@ -22,7 +22,7 @@ const documents = { "\n query PlaybookLHS($userID: String!, $teamID: String!, $types: [PlaybookRunType!]) {\n runs (participantOrFollowerID: $userID, teamID: $teamID, sort: \"name\", statuses: [\"InProgress\"], types: $types){\n edges {\n node {\n id\n name\n isFavorite\n playbookID\n ownerUserID\n participantIDs\n followers\n type\n }\n }\n }\n playbooks (teamID: $teamID, withMembershipOnly: true) {\n id\n title\n isFavorite\n public\n }\n }\n": types.PlaybookLhsDocument, "\n query PlaybookRunReminder($runID: String!) {\n run (id: $runID){\n id\n name\n previousReminder\n reminderTimerDefaultSeconds\n }\n }\n": types.PlaybookRunReminderDocument, "\n query FirstActiveRunInChannel($channelID: String!) {\n runs(\n channelID: $channelID,\n statuses: [\"InProgress\"],\n first: 1,\n ) {\n edges {\n node {\n id\n name\n previousReminder\n reminderTimerDefaultSeconds\n }\n }\n }\n }\n": types.FirstActiveRunInChannelDocument, - "query Playbook($id: String!) {\n playbook(id: $id) {\n id\n title\n description\n team_id: teamID\n public\n delete_at: deleteAt\n default_playbook_member_role: defaultPlaybookMemberRole\n invited_user_ids: invitedUserIDs\n invited_group_ids: invitedGroupIDs\n broadcast_channel_ids: broadcastChannelIDs\n webhook_on_creation_urls: webhookOnCreationURLs\n reminder_timer_default_seconds: reminderTimerDefaultSeconds\n reminder_message_template: reminderMessageTemplate\n broadcast_enabled: broadcastEnabled\n webhook_on_status_update_enabled: webhookOnStatusUpdateEnabled\n webhook_on_status_update_urls: webhookOnStatusUpdateURLs\n status_update_enabled: statusUpdateEnabled\n retrospective_enabled: retrospectiveEnabled\n retrospective_reminder_interval_seconds: retrospectiveReminderIntervalSeconds\n retrospective_template: retrospectiveTemplate\n default_owner_id: defaultOwnerID\n run_summary_template: runSummaryTemplate\n run_summary_template_enabled: runSummaryTemplateEnabled\n message_on_join: messageOnJoin\n category_name: categoryName\n invite_users_enabled: inviteUsersEnabled\n default_owner_enabled: defaultOwnerEnabled\n webhook_on_creation_enabled: webhookOnCreationEnabled\n message_on_join_enabled: messageOnJoinEnabled\n categorize_channel_enabled: categorizeChannelEnabled\n signal_any_keywords_enabled: signalAnyKeywordsEnabled\n signal_any_keywords: signalAnyKeywords\n create_public_playbook_run: createPublicPlaybookRun\n channel_name_template: channelNameTemplate\n create_channel_member_on_new_participant: createChannelMemberOnNewParticipant\n remove_channel_member_on_removed_participant: removeChannelMemberOnRemovedParticipant\n channel_id: channelID\n channel_mode: channelMode\n is_favorite: isFavorite\n checklists {\n title\n items {\n title\n description\n state\n state_modified: stateModified\n assignee_id: assigneeID\n assignee_type: assigneeType\n assignee_modified: assigneeModified\n command\n command_last_run: commandLastRun\n due_date: dueDate\n condition_id: conditionID\n condition_action: conditionAction\n condition_reason: conditionReason\n assignee_property_field_id: assigneePropertyFieldID\n task_actions: taskActions {\n trigger: trigger {\n type\n payload\n }\n actions: actions {\n type\n payload\n }\n }\n }\n }\n members {\n user_id: userID\n roles\n scheme_roles: schemeRoles\n }\n metrics {\n id\n title\n description\n type\n target\n }\n }\n}\n\nmutation UpdatePlaybookFavorite($id: String!, $favorite: Boolean!) {\n updatePlaybookFavorite(id: $id, favorite: $favorite)\n}\n\nmutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) {\n updatePlaybook(id: $id, updates: $updates)\n}\n\nmutation AddPlaybookMember($playbookID: String!, $userID: String!) {\n addPlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nmutation RemovePlaybookMember($playbookID: String!, $userID: String!) {\n removePlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nquery PlaybookProperty($playbookID: String!, $propertyID: String!) {\n playbookProperty(playbookID: $playbookID, propertyID: $propertyID) {\n id\n name\n type\n group_id: groupID\n attrs {\n visibility\n sort_order: sortOrder\n options {\n id\n name\n color\n }\n parent_id: parentID\n value_type: valueType\n }\n create_at: createAt\n update_at: updateAt\n delete_at: deleteAt\n }\n}\n\nmutation AddPlaybookPropertyField($playbookID: String!, $propertyField: PropertyFieldInput!) {\n addPlaybookPropertyField(playbookID: $playbookID, propertyField: $propertyField)\n}\n\nmutation UpdatePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!, $propertyField: PropertyFieldInput!) {\n updatePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n propertyField: $propertyField\n )\n}\n\nmutation DeletePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!) {\n deletePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n )\n}": types.PlaybookDocument, + "query Playbook($id: String!) {\n playbook(id: $id) {\n id\n title\n description\n team_id: teamID\n public\n delete_at: deleteAt\n default_playbook_member_role: defaultPlaybookMemberRole\n invited_user_ids: invitedUserIDs\n invited_group_ids: invitedGroupIDs\n broadcast_channel_ids: broadcastChannelIDs\n webhook_on_creation_urls: webhookOnCreationURLs\n reminder_timer_default_seconds: reminderTimerDefaultSeconds\n reminder_message_template: reminderMessageTemplate\n broadcast_enabled: broadcastEnabled\n webhook_on_status_update_enabled: webhookOnStatusUpdateEnabled\n webhook_on_status_update_urls: webhookOnStatusUpdateURLs\n status_update_enabled: statusUpdateEnabled\n retrospective_enabled: retrospectiveEnabled\n retrospective_reminder_interval_seconds: retrospectiveReminderIntervalSeconds\n retrospective_template: retrospectiveTemplate\n default_owner_id: defaultOwnerID\n run_summary_template: runSummaryTemplate\n run_summary_template_enabled: runSummaryTemplateEnabled\n message_on_join: messageOnJoin\n category_name: categoryName\n invite_users_enabled: inviteUsersEnabled\n default_owner_enabled: defaultOwnerEnabled\n webhook_on_creation_enabled: webhookOnCreationEnabled\n message_on_join_enabled: messageOnJoinEnabled\n categorize_channel_enabled: categorizeChannelEnabled\n signal_any_keywords_enabled: signalAnyKeywordsEnabled\n signal_any_keywords: signalAnyKeywords\n create_public_playbook_run: createPublicPlaybookRun\n channel_name_template: channelNameTemplate\n create_channel_member_on_new_participant: createChannelMemberOnNewParticipant\n remove_channel_member_on_removed_participant: removeChannelMemberOnRemovedParticipant\n channel_id: channelID\n channel_mode: channelMode\n is_favorite: isFavorite\n checklists {\n title\n items {\n title\n description\n state\n state_modified: stateModified\n assignee_id: assigneeID\n assignee_type: assigneeType\n assignee_modified: assigneeModified\n command\n command_last_run: commandLastRun\n due_date: dueDate\n condition_id: conditionID\n condition_action: conditionAction\n condition_reason: conditionReason\n assignee_property_field_id: assigneePropertyFieldID\n task_actions: taskActions {\n trigger: trigger {\n type\n payload\n }\n actions: actions {\n type\n payload\n }\n }\n requirements {\n id\n label\n value\n }\n }\n }\n members {\n user_id: userID\n roles\n scheme_roles: schemeRoles\n }\n metrics {\n id\n title\n description\n type\n target\n }\n }\n}\n\nmutation UpdatePlaybookFavorite($id: String!, $favorite: Boolean!) {\n updatePlaybookFavorite(id: $id, favorite: $favorite)\n}\n\nmutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) {\n updatePlaybook(id: $id, updates: $updates)\n}\n\nmutation AddPlaybookMember($playbookID: String!, $userID: String!) {\n addPlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nmutation RemovePlaybookMember($playbookID: String!, $userID: String!) {\n removePlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nquery PlaybookProperty($playbookID: String!, $propertyID: String!) {\n playbookProperty(playbookID: $playbookID, propertyID: $propertyID) {\n id\n name\n type\n group_id: groupID\n attrs {\n visibility\n sort_order: sortOrder\n options {\n id\n name\n color\n }\n parent_id: parentID\n value_type: valueType\n }\n create_at: createAt\n update_at: updateAt\n delete_at: deleteAt\n }\n}\n\nmutation AddPlaybookPropertyField($playbookID: String!, $propertyField: PropertyFieldInput!) {\n addPlaybookPropertyField(playbookID: $playbookID, propertyField: $propertyField)\n}\n\nmutation UpdatePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!, $propertyField: PropertyFieldInput!) {\n updatePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n propertyField: $propertyField\n )\n}\n\nmutation DeletePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!) {\n deletePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n )\n}": types.PlaybookDocument, "mutation SetRunFavorite($id: String!, $fav: Boolean!) {\n setRunFavorite(id: $id, fav: $fav)\n}\n\nmutation UpdateRun($id: String!, $updates: RunUpdates!) {\n updateRun(id: $id, updates: $updates)\n}\n\nmutation AddRunParticipants($runID: String!, $userIDs: [String!]!, $forceAddToChannel: Boolean = false) {\n addRunParticipants(\n runID: $runID\n userIDs: $userIDs\n forceAddToChannel: $forceAddToChannel\n )\n}\n\nmutation RemoveRunParticipants($runID: String!, $userIDs: [String!]!) {\n removeRunParticipants(runID: $runID, userIDs: $userIDs)\n}\n\nmutation ChangeRunOwner($runID: String!, $ownerID: String!) {\n changeRunOwner(runID: $runID, ownerID: $ownerID)\n}\n\nmutation UpdateRunTaskActions($runID: String!, $checklistNum: Float!, $itemNum: Float!, $taskActions: [TaskActionUpdates!]!) {\n updateRunTaskActions(\n runID: $runID\n checklistNum: $checklistNum\n itemNum: $itemNum\n taskActions: $taskActions\n )\n}\n\nmutation SetRunPropertyValue($runID: String!, $propertyFieldID: String!, $value: JSON) {\n setRunPropertyValue(\n runID: $runID\n propertyFieldID: $propertyFieldID\n value: $value\n )\n}": types.SetRunFavoriteDocument, }; @@ -79,7 +79,7 @@ export function graphql(source: "\n query FirstActiveRunInChannel($channelID: /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query Playbook($id: String!) {\n playbook(id: $id) {\n id\n title\n description\n team_id: teamID\n public\n delete_at: deleteAt\n default_playbook_member_role: defaultPlaybookMemberRole\n invited_user_ids: invitedUserIDs\n invited_group_ids: invitedGroupIDs\n broadcast_channel_ids: broadcastChannelIDs\n webhook_on_creation_urls: webhookOnCreationURLs\n reminder_timer_default_seconds: reminderTimerDefaultSeconds\n reminder_message_template: reminderMessageTemplate\n broadcast_enabled: broadcastEnabled\n webhook_on_status_update_enabled: webhookOnStatusUpdateEnabled\n webhook_on_status_update_urls: webhookOnStatusUpdateURLs\n status_update_enabled: statusUpdateEnabled\n retrospective_enabled: retrospectiveEnabled\n retrospective_reminder_interval_seconds: retrospectiveReminderIntervalSeconds\n retrospective_template: retrospectiveTemplate\n default_owner_id: defaultOwnerID\n run_summary_template: runSummaryTemplate\n run_summary_template_enabled: runSummaryTemplateEnabled\n message_on_join: messageOnJoin\n category_name: categoryName\n invite_users_enabled: inviteUsersEnabled\n default_owner_enabled: defaultOwnerEnabled\n webhook_on_creation_enabled: webhookOnCreationEnabled\n message_on_join_enabled: messageOnJoinEnabled\n categorize_channel_enabled: categorizeChannelEnabled\n signal_any_keywords_enabled: signalAnyKeywordsEnabled\n signal_any_keywords: signalAnyKeywords\n create_public_playbook_run: createPublicPlaybookRun\n channel_name_template: channelNameTemplate\n create_channel_member_on_new_participant: createChannelMemberOnNewParticipant\n remove_channel_member_on_removed_participant: removeChannelMemberOnRemovedParticipant\n channel_id: channelID\n channel_mode: channelMode\n is_favorite: isFavorite\n checklists {\n title\n items {\n title\n description\n state\n state_modified: stateModified\n assignee_id: assigneeID\n assignee_type: assigneeType\n assignee_modified: assigneeModified\n command\n command_last_run: commandLastRun\n due_date: dueDate\n condition_id: conditionID\n condition_action: conditionAction\n condition_reason: conditionReason\n assignee_property_field_id: assigneePropertyFieldID\n task_actions: taskActions {\n trigger: trigger {\n type\n payload\n }\n actions: actions {\n type\n payload\n }\n }\n }\n }\n members {\n user_id: userID\n roles\n scheme_roles: schemeRoles\n }\n metrics {\n id\n title\n description\n type\n target\n }\n }\n}\n\nmutation UpdatePlaybookFavorite($id: String!, $favorite: Boolean!) {\n updatePlaybookFavorite(id: $id, favorite: $favorite)\n}\n\nmutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) {\n updatePlaybook(id: $id, updates: $updates)\n}\n\nmutation AddPlaybookMember($playbookID: String!, $userID: String!) {\n addPlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nmutation RemovePlaybookMember($playbookID: String!, $userID: String!) {\n removePlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nquery PlaybookProperty($playbookID: String!, $propertyID: String!) {\n playbookProperty(playbookID: $playbookID, propertyID: $propertyID) {\n id\n name\n type\n group_id: groupID\n attrs {\n visibility\n sort_order: sortOrder\n options {\n id\n name\n color\n }\n parent_id: parentID\n value_type: valueType\n }\n create_at: createAt\n update_at: updateAt\n delete_at: deleteAt\n }\n}\n\nmutation AddPlaybookPropertyField($playbookID: String!, $propertyField: PropertyFieldInput!) {\n addPlaybookPropertyField(playbookID: $playbookID, propertyField: $propertyField)\n}\n\nmutation UpdatePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!, $propertyField: PropertyFieldInput!) {\n updatePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n propertyField: $propertyField\n )\n}\n\nmutation DeletePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!) {\n deletePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n )\n}"): (typeof documents)["query Playbook($id: String!) {\n playbook(id: $id) {\n id\n title\n description\n team_id: teamID\n public\n delete_at: deleteAt\n default_playbook_member_role: defaultPlaybookMemberRole\n invited_user_ids: invitedUserIDs\n invited_group_ids: invitedGroupIDs\n broadcast_channel_ids: broadcastChannelIDs\n webhook_on_creation_urls: webhookOnCreationURLs\n reminder_timer_default_seconds: reminderTimerDefaultSeconds\n reminder_message_template: reminderMessageTemplate\n broadcast_enabled: broadcastEnabled\n webhook_on_status_update_enabled: webhookOnStatusUpdateEnabled\n webhook_on_status_update_urls: webhookOnStatusUpdateURLs\n status_update_enabled: statusUpdateEnabled\n retrospective_enabled: retrospectiveEnabled\n retrospective_reminder_interval_seconds: retrospectiveReminderIntervalSeconds\n retrospective_template: retrospectiveTemplate\n default_owner_id: defaultOwnerID\n run_summary_template: runSummaryTemplate\n run_summary_template_enabled: runSummaryTemplateEnabled\n message_on_join: messageOnJoin\n category_name: categoryName\n invite_users_enabled: inviteUsersEnabled\n default_owner_enabled: defaultOwnerEnabled\n webhook_on_creation_enabled: webhookOnCreationEnabled\n message_on_join_enabled: messageOnJoinEnabled\n categorize_channel_enabled: categorizeChannelEnabled\n signal_any_keywords_enabled: signalAnyKeywordsEnabled\n signal_any_keywords: signalAnyKeywords\n create_public_playbook_run: createPublicPlaybookRun\n channel_name_template: channelNameTemplate\n create_channel_member_on_new_participant: createChannelMemberOnNewParticipant\n remove_channel_member_on_removed_participant: removeChannelMemberOnRemovedParticipant\n channel_id: channelID\n channel_mode: channelMode\n is_favorite: isFavorite\n checklists {\n title\n items {\n title\n description\n state\n state_modified: stateModified\n assignee_id: assigneeID\n assignee_type: assigneeType\n assignee_modified: assigneeModified\n command\n command_last_run: commandLastRun\n due_date: dueDate\n condition_id: conditionID\n condition_action: conditionAction\n condition_reason: conditionReason\n assignee_property_field_id: assigneePropertyFieldID\n task_actions: taskActions {\n trigger: trigger {\n type\n payload\n }\n actions: actions {\n type\n payload\n }\n }\n }\n }\n members {\n user_id: userID\n roles\n scheme_roles: schemeRoles\n }\n metrics {\n id\n title\n description\n type\n target\n }\n }\n}\n\nmutation UpdatePlaybookFavorite($id: String!, $favorite: Boolean!) {\n updatePlaybookFavorite(id: $id, favorite: $favorite)\n}\n\nmutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) {\n updatePlaybook(id: $id, updates: $updates)\n}\n\nmutation AddPlaybookMember($playbookID: String!, $userID: String!) {\n addPlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nmutation RemovePlaybookMember($playbookID: String!, $userID: String!) {\n removePlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nquery PlaybookProperty($playbookID: String!, $propertyID: String!) {\n playbookProperty(playbookID: $playbookID, propertyID: $propertyID) {\n id\n name\n type\n group_id: groupID\n attrs {\n visibility\n sort_order: sortOrder\n options {\n id\n name\n color\n }\n parent_id: parentID\n value_type: valueType\n }\n create_at: createAt\n update_at: updateAt\n delete_at: deleteAt\n }\n}\n\nmutation AddPlaybookPropertyField($playbookID: String!, $propertyField: PropertyFieldInput!) {\n addPlaybookPropertyField(playbookID: $playbookID, propertyField: $propertyField)\n}\n\nmutation UpdatePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!, $propertyField: PropertyFieldInput!) {\n updatePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n propertyField: $propertyField\n )\n}\n\nmutation DeletePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!) {\n deletePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n )\n}"]; +export function graphql(source: "query Playbook($id: String!) {\n playbook(id: $id) {\n id\n title\n description\n team_id: teamID\n public\n delete_at: deleteAt\n default_playbook_member_role: defaultPlaybookMemberRole\n invited_user_ids: invitedUserIDs\n invited_group_ids: invitedGroupIDs\n broadcast_channel_ids: broadcastChannelIDs\n webhook_on_creation_urls: webhookOnCreationURLs\n reminder_timer_default_seconds: reminderTimerDefaultSeconds\n reminder_message_template: reminderMessageTemplate\n broadcast_enabled: broadcastEnabled\n webhook_on_status_update_enabled: webhookOnStatusUpdateEnabled\n webhook_on_status_update_urls: webhookOnStatusUpdateURLs\n status_update_enabled: statusUpdateEnabled\n retrospective_enabled: retrospectiveEnabled\n retrospective_reminder_interval_seconds: retrospectiveReminderIntervalSeconds\n retrospective_template: retrospectiveTemplate\n default_owner_id: defaultOwnerID\n run_summary_template: runSummaryTemplate\n run_summary_template_enabled: runSummaryTemplateEnabled\n message_on_join: messageOnJoin\n category_name: categoryName\n invite_users_enabled: inviteUsersEnabled\n default_owner_enabled: defaultOwnerEnabled\n webhook_on_creation_enabled: webhookOnCreationEnabled\n message_on_join_enabled: messageOnJoinEnabled\n categorize_channel_enabled: categorizeChannelEnabled\n signal_any_keywords_enabled: signalAnyKeywordsEnabled\n signal_any_keywords: signalAnyKeywords\n create_public_playbook_run: createPublicPlaybookRun\n channel_name_template: channelNameTemplate\n create_channel_member_on_new_participant: createChannelMemberOnNewParticipant\n remove_channel_member_on_removed_participant: removeChannelMemberOnRemovedParticipant\n channel_id: channelID\n channel_mode: channelMode\n is_favorite: isFavorite\n checklists {\n title\n items {\n title\n description\n state\n state_modified: stateModified\n assignee_id: assigneeID\n assignee_type: assigneeType\n assignee_modified: assigneeModified\n command\n command_last_run: commandLastRun\n due_date: dueDate\n condition_id: conditionID\n condition_action: conditionAction\n condition_reason: conditionReason\n assignee_property_field_id: assigneePropertyFieldID\n task_actions: taskActions {\n trigger: trigger {\n type\n payload\n }\n actions: actions {\n type\n payload\n }\n }\n requirements {\n id\n label\n value\n }\n }\n }\n members {\n user_id: userID\n roles\n scheme_roles: schemeRoles\n }\n metrics {\n id\n title\n description\n type\n target\n }\n }\n}\n\nmutation UpdatePlaybookFavorite($id: String!, $favorite: Boolean!) {\n updatePlaybookFavorite(id: $id, favorite: $favorite)\n}\n\nmutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) {\n updatePlaybook(id: $id, updates: $updates)\n}\n\nmutation AddPlaybookMember($playbookID: String!, $userID: String!) {\n addPlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nmutation RemovePlaybookMember($playbookID: String!, $userID: String!) {\n removePlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nquery PlaybookProperty($playbookID: String!, $propertyID: String!) {\n playbookProperty(playbookID: $playbookID, propertyID: $propertyID) {\n id\n name\n type\n group_id: groupID\n attrs {\n visibility\n sort_order: sortOrder\n options {\n id\n name\n color\n }\n parent_id: parentID\n value_type: valueType\n }\n create_at: createAt\n update_at: updateAt\n delete_at: deleteAt\n }\n}\n\nmutation AddPlaybookPropertyField($playbookID: String!, $propertyField: PropertyFieldInput!) {\n addPlaybookPropertyField(playbookID: $playbookID, propertyField: $propertyField)\n}\n\nmutation UpdatePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!, $propertyField: PropertyFieldInput!) {\n updatePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n propertyField: $propertyField\n )\n}\n\nmutation DeletePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!) {\n deletePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n )\n}"): (typeof documents)["query Playbook($id: String!) {\n playbook(id: $id) {\n id\n title\n description\n team_id: teamID\n public\n delete_at: deleteAt\n default_playbook_member_role: defaultPlaybookMemberRole\n invited_user_ids: invitedUserIDs\n invited_group_ids: invitedGroupIDs\n broadcast_channel_ids: broadcastChannelIDs\n webhook_on_creation_urls: webhookOnCreationURLs\n reminder_timer_default_seconds: reminderTimerDefaultSeconds\n reminder_message_template: reminderMessageTemplate\n broadcast_enabled: broadcastEnabled\n webhook_on_status_update_enabled: webhookOnStatusUpdateEnabled\n webhook_on_status_update_urls: webhookOnStatusUpdateURLs\n status_update_enabled: statusUpdateEnabled\n retrospective_enabled: retrospectiveEnabled\n retrospective_reminder_interval_seconds: retrospectiveReminderIntervalSeconds\n retrospective_template: retrospectiveTemplate\n default_owner_id: defaultOwnerID\n run_summary_template: runSummaryTemplate\n run_summary_template_enabled: runSummaryTemplateEnabled\n message_on_join: messageOnJoin\n category_name: categoryName\n invite_users_enabled: inviteUsersEnabled\n default_owner_enabled: defaultOwnerEnabled\n webhook_on_creation_enabled: webhookOnCreationEnabled\n message_on_join_enabled: messageOnJoinEnabled\n categorize_channel_enabled: categorizeChannelEnabled\n signal_any_keywords_enabled: signalAnyKeywordsEnabled\n signal_any_keywords: signalAnyKeywords\n create_public_playbook_run: createPublicPlaybookRun\n channel_name_template: channelNameTemplate\n create_channel_member_on_new_participant: createChannelMemberOnNewParticipant\n remove_channel_member_on_removed_participant: removeChannelMemberOnRemovedParticipant\n channel_id: channelID\n channel_mode: channelMode\n is_favorite: isFavorite\n checklists {\n title\n items {\n title\n description\n state\n state_modified: stateModified\n assignee_id: assigneeID\n assignee_type: assigneeType\n assignee_modified: assigneeModified\n command\n command_last_run: commandLastRun\n due_date: dueDate\n condition_id: conditionID\n condition_action: conditionAction\n condition_reason: conditionReason\n assignee_property_field_id: assigneePropertyFieldID\n task_actions: taskActions {\n trigger: trigger {\n type\n payload\n }\n actions: actions {\n type\n payload\n }\n }\n requirements {\n id\n label\n value\n }\n }\n }\n members {\n user_id: userID\n roles\n scheme_roles: schemeRoles\n }\n metrics {\n id\n title\n description\n type\n target\n }\n }\n}\n\nmutation UpdatePlaybookFavorite($id: String!, $favorite: Boolean!) {\n updatePlaybookFavorite(id: $id, favorite: $favorite)\n}\n\nmutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) {\n updatePlaybook(id: $id, updates: $updates)\n}\n\nmutation AddPlaybookMember($playbookID: String!, $userID: String!) {\n addPlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nmutation RemovePlaybookMember($playbookID: String!, $userID: String!) {\n removePlaybookMember(playbookID: $playbookID, userID: $userID)\n}\n\nquery PlaybookProperty($playbookID: String!, $propertyID: String!) {\n playbookProperty(playbookID: $playbookID, propertyID: $propertyID) {\n id\n name\n type\n group_id: groupID\n attrs {\n visibility\n sort_order: sortOrder\n options {\n id\n name\n color\n }\n parent_id: parentID\n value_type: valueType\n }\n create_at: createAt\n update_at: updateAt\n delete_at: deleteAt\n }\n}\n\nmutation AddPlaybookPropertyField($playbookID: String!, $propertyField: PropertyFieldInput!) {\n addPlaybookPropertyField(playbookID: $playbookID, propertyField: $propertyField)\n}\n\nmutation UpdatePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!, $propertyField: PropertyFieldInput!) {\n updatePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n propertyField: $propertyField\n )\n}\n\nmutation DeletePlaybookPropertyField($playbookID: String!, $propertyFieldID: String!) {\n deletePlaybookPropertyField(\n playbookID: $playbookID\n propertyFieldID: $propertyFieldID\n )\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/webapp/src/graphql/generated/graphql.ts b/webapp/src/graphql/generated/graphql.ts index e25a9b099d..98e43ced3a 100644 --- a/webapp/src/graphql/generated/graphql.ts +++ b/webapp/src/graphql/generated/graphql.ts @@ -45,6 +45,7 @@ export type ChecklistItem = { conditionReason: Scalars['String']; description: Scalars['String']; dueDate: Scalars['Float']; + requirements: Array; state: Scalars['String']; stateModified: Scalars['Float']; taskActions: Array; @@ -61,6 +62,7 @@ export type ChecklistItemUpdates = { conditionID: Scalars['String']; description: Scalars['String']; dueDate: Scalars['Float']; + requirements?: InputMaybe>; state: Scalars['String']; stateModified: Scalars['Float']; taskActions?: InputMaybe>; @@ -562,6 +564,19 @@ export type TaskActionUpdates = { trigger: TriggerUpdates; }; +export type TaskRequirement = { + __typename?: 'TaskRequirement'; + id: Scalars['String']; + label: Scalars['String']; + value: Scalars['String']; +}; + +export type TaskRequirementUpdates = { + id: Scalars['String']; + label: Scalars['String']; + value: Scalars['String']; +}; + export type TimelineEvent = { __typename?: 'TimelineEvent'; createAt: Scalars['Float']; @@ -657,7 +672,7 @@ export type PlaybookQueryVariables = Exact<{ }>; -export type PlaybookQuery = { __typename?: 'Query', playbook?: { __typename?: 'Playbook', id: string, title: string, description: string, public: boolean, team_id: string, delete_at: number, default_playbook_member_role: string, invited_user_ids: Array, invited_group_ids: Array, broadcast_channel_ids: Array, webhook_on_creation_urls: Array, reminder_timer_default_seconds: number, reminder_message_template: string, broadcast_enabled: boolean, webhook_on_status_update_enabled: boolean, webhook_on_status_update_urls: Array, status_update_enabled: boolean, retrospective_enabled: boolean, retrospective_reminder_interval_seconds: number, retrospective_template: string, default_owner_id: string, run_summary_template: string, run_summary_template_enabled: boolean, message_on_join: string, category_name: string, invite_users_enabled: boolean, default_owner_enabled: boolean, webhook_on_creation_enabled: boolean, message_on_join_enabled: boolean, categorize_channel_enabled: boolean, signal_any_keywords_enabled: boolean, signal_any_keywords: Array, create_public_playbook_run: boolean, channel_name_template: string, create_channel_member_on_new_participant: boolean, remove_channel_member_on_removed_participant: boolean, channel_id: string, channel_mode: string, is_favorite: boolean, checklists: Array<{ __typename?: 'Checklist', title: string, items: Array<{ __typename?: 'ChecklistItem', title: string, description: string, state: string, command: string, state_modified: number, assignee_id: string, assignee_type: string, assignee_modified: number, command_last_run: number, due_date: number, condition_id: string, condition_action: string, condition_reason: string, assignee_property_field_id: string, task_actions: Array<{ __typename?: 'TaskAction', trigger: { __typename?: 'Trigger', type: string, payload: string }, actions: Array<{ __typename?: 'Action', type: string, payload: string }> }> }> }>, members: Array<{ __typename?: 'Member', roles: Array, user_id: string, scheme_roles: Array }>, metrics: Array<{ __typename?: 'PlaybookMetricConfig', id: string, title: string, description: string, type: MetricType, target?: number | null }> } | null }; +export type PlaybookQuery = { __typename?: 'Query', playbook?: { __typename?: 'Playbook', id: string, title: string, description: string, public: boolean, team_id: string, delete_at: number, default_playbook_member_role: string, invited_user_ids: Array, invited_group_ids: Array, broadcast_channel_ids: Array, webhook_on_creation_urls: Array, reminder_timer_default_seconds: number, reminder_message_template: string, broadcast_enabled: boolean, webhook_on_status_update_enabled: boolean, webhook_on_status_update_urls: Array, status_update_enabled: boolean, retrospective_enabled: boolean, retrospective_reminder_interval_seconds: number, retrospective_template: string, default_owner_id: string, run_summary_template: string, run_summary_template_enabled: boolean, message_on_join: string, category_name: string, invite_users_enabled: boolean, default_owner_enabled: boolean, webhook_on_creation_enabled: boolean, message_on_join_enabled: boolean, categorize_channel_enabled: boolean, signal_any_keywords_enabled: boolean, signal_any_keywords: Array, create_public_playbook_run: boolean, channel_name_template: string, create_channel_member_on_new_participant: boolean, remove_channel_member_on_removed_participant: boolean, channel_id: string, channel_mode: string, is_favorite: boolean, checklists: Array<{ __typename?: 'Checklist', title: string, items: Array<{ __typename?: 'ChecklistItem', title: string, description: string, state: string, command: string, state_modified: number, assignee_id: string, assignee_type: string, assignee_modified: number, command_last_run: number, due_date: number, condition_id: string, condition_action: string, condition_reason: string, assignee_property_field_id: string, task_actions: Array<{ __typename?: 'TaskAction', trigger: { __typename?: 'Trigger', type: string, payload: string }, actions: Array<{ __typename?: 'Action', type: string, payload: string }> }>, requirements: Array<{ __typename?: 'TaskRequirement', id: string, label: string, value: string }> }> }>, members: Array<{ __typename?: 'Member', roles: Array, user_id: string, scheme_roles: Array }>, metrics: Array<{ __typename?: 'PlaybookMetricConfig', id: string, title: string, description: string, type: MetricType, target?: number | null }> } | null }; export type UpdatePlaybookFavoriteMutationVariables = Exact<{ id: Scalars['String']; @@ -793,7 +808,7 @@ export const RhsRunsDocument = {"kind":"Document","definitions":[{"kind":"Operat export const PlaybookLhsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PlaybookLHS"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userID"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"teamID"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"types"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PlaybookRunType"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"participantOrFollowerID"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userID"}}},{"kind":"Argument","name":{"kind":"Name","value":"teamID"},"value":{"kind":"Variable","name":{"kind":"Name","value":"teamID"}}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"StringValue","value":"name","block":false}},{"kind":"Argument","name":{"kind":"Name","value":"statuses"},"value":{"kind":"ListValue","values":[{"kind":"StringValue","value":"InProgress","block":false}]}},{"kind":"Argument","name":{"kind":"Name","value":"types"},"value":{"kind":"Variable","name":{"kind":"Name","value":"types"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isFavorite"}},{"kind":"Field","name":{"kind":"Name","value":"playbookID"}},{"kind":"Field","name":{"kind":"Name","value":"ownerUserID"}},{"kind":"Field","name":{"kind":"Name","value":"participantIDs"}},{"kind":"Field","name":{"kind":"Name","value":"followers"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"playbooks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"teamID"},"value":{"kind":"Variable","name":{"kind":"Name","value":"teamID"}}},{"kind":"Argument","name":{"kind":"Name","value":"withMembershipOnly"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isFavorite"}},{"kind":"Field","name":{"kind":"Name","value":"public"}}]}}]}}]} as unknown as DocumentNode; export const PlaybookRunReminderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PlaybookRunReminder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"runID"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"run"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"runID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"previousReminder"}},{"kind":"Field","name":{"kind":"Name","value":"reminderTimerDefaultSeconds"}}]}}]}}]} as unknown as DocumentNode; export const FirstActiveRunInChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FirstActiveRunInChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"channelID"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"channelID"},"value":{"kind":"Variable","name":{"kind":"Name","value":"channelID"}}},{"kind":"Argument","name":{"kind":"Name","value":"statuses"},"value":{"kind":"ListValue","values":[{"kind":"StringValue","value":"InProgress","block":false}]}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"previousReminder"}},{"kind":"Field","name":{"kind":"Name","value":"reminderTimerDefaultSeconds"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const PlaybookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Playbook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"playbook"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","alias":{"kind":"Name","value":"team_id"},"name":{"kind":"Name","value":"teamID"}},{"kind":"Field","name":{"kind":"Name","value":"public"}},{"kind":"Field","alias":{"kind":"Name","value":"delete_at"},"name":{"kind":"Name","value":"deleteAt"}},{"kind":"Field","alias":{"kind":"Name","value":"default_playbook_member_role"},"name":{"kind":"Name","value":"defaultPlaybookMemberRole"}},{"kind":"Field","alias":{"kind":"Name","value":"invited_user_ids"},"name":{"kind":"Name","value":"invitedUserIDs"}},{"kind":"Field","alias":{"kind":"Name","value":"invited_group_ids"},"name":{"kind":"Name","value":"invitedGroupIDs"}},{"kind":"Field","alias":{"kind":"Name","value":"broadcast_channel_ids"},"name":{"kind":"Name","value":"broadcastChannelIDs"}},{"kind":"Field","alias":{"kind":"Name","value":"webhook_on_creation_urls"},"name":{"kind":"Name","value":"webhookOnCreationURLs"}},{"kind":"Field","alias":{"kind":"Name","value":"reminder_timer_default_seconds"},"name":{"kind":"Name","value":"reminderTimerDefaultSeconds"}},{"kind":"Field","alias":{"kind":"Name","value":"reminder_message_template"},"name":{"kind":"Name","value":"reminderMessageTemplate"}},{"kind":"Field","alias":{"kind":"Name","value":"broadcast_enabled"},"name":{"kind":"Name","value":"broadcastEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"webhook_on_status_update_enabled"},"name":{"kind":"Name","value":"webhookOnStatusUpdateEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"webhook_on_status_update_urls"},"name":{"kind":"Name","value":"webhookOnStatusUpdateURLs"}},{"kind":"Field","alias":{"kind":"Name","value":"status_update_enabled"},"name":{"kind":"Name","value":"statusUpdateEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"retrospective_enabled"},"name":{"kind":"Name","value":"retrospectiveEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"retrospective_reminder_interval_seconds"},"name":{"kind":"Name","value":"retrospectiveReminderIntervalSeconds"}},{"kind":"Field","alias":{"kind":"Name","value":"retrospective_template"},"name":{"kind":"Name","value":"retrospectiveTemplate"}},{"kind":"Field","alias":{"kind":"Name","value":"default_owner_id"},"name":{"kind":"Name","value":"defaultOwnerID"}},{"kind":"Field","alias":{"kind":"Name","value":"run_summary_template"},"name":{"kind":"Name","value":"runSummaryTemplate"}},{"kind":"Field","alias":{"kind":"Name","value":"run_summary_template_enabled"},"name":{"kind":"Name","value":"runSummaryTemplateEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"message_on_join"},"name":{"kind":"Name","value":"messageOnJoin"}},{"kind":"Field","alias":{"kind":"Name","value":"category_name"},"name":{"kind":"Name","value":"categoryName"}},{"kind":"Field","alias":{"kind":"Name","value":"invite_users_enabled"},"name":{"kind":"Name","value":"inviteUsersEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"default_owner_enabled"},"name":{"kind":"Name","value":"defaultOwnerEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"webhook_on_creation_enabled"},"name":{"kind":"Name","value":"webhookOnCreationEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"message_on_join_enabled"},"name":{"kind":"Name","value":"messageOnJoinEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"categorize_channel_enabled"},"name":{"kind":"Name","value":"categorizeChannelEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"signal_any_keywords_enabled"},"name":{"kind":"Name","value":"signalAnyKeywordsEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"signal_any_keywords"},"name":{"kind":"Name","value":"signalAnyKeywords"}},{"kind":"Field","alias":{"kind":"Name","value":"create_public_playbook_run"},"name":{"kind":"Name","value":"createPublicPlaybookRun"}},{"kind":"Field","alias":{"kind":"Name","value":"channel_name_template"},"name":{"kind":"Name","value":"channelNameTemplate"}},{"kind":"Field","alias":{"kind":"Name","value":"create_channel_member_on_new_participant"},"name":{"kind":"Name","value":"createChannelMemberOnNewParticipant"}},{"kind":"Field","alias":{"kind":"Name","value":"remove_channel_member_on_removed_participant"},"name":{"kind":"Name","value":"removeChannelMemberOnRemovedParticipant"}},{"kind":"Field","alias":{"kind":"Name","value":"channel_id"},"name":{"kind":"Name","value":"channelID"}},{"kind":"Field","alias":{"kind":"Name","value":"channel_mode"},"name":{"kind":"Name","value":"channelMode"}},{"kind":"Field","alias":{"kind":"Name","value":"is_favorite"},"name":{"kind":"Name","value":"isFavorite"}},{"kind":"Field","name":{"kind":"Name","value":"checklists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","alias":{"kind":"Name","value":"state_modified"},"name":{"kind":"Name","value":"stateModified"}},{"kind":"Field","alias":{"kind":"Name","value":"assignee_id"},"name":{"kind":"Name","value":"assigneeID"}},{"kind":"Field","alias":{"kind":"Name","value":"assignee_type"},"name":{"kind":"Name","value":"assigneeType"}},{"kind":"Field","alias":{"kind":"Name","value":"assignee_modified"},"name":{"kind":"Name","value":"assigneeModified"}},{"kind":"Field","name":{"kind":"Name","value":"command"}},{"kind":"Field","alias":{"kind":"Name","value":"command_last_run"},"name":{"kind":"Name","value":"commandLastRun"}},{"kind":"Field","alias":{"kind":"Name","value":"due_date"},"name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","alias":{"kind":"Name","value":"condition_id"},"name":{"kind":"Name","value":"conditionID"}},{"kind":"Field","alias":{"kind":"Name","value":"condition_action"},"name":{"kind":"Name","value":"conditionAction"}},{"kind":"Field","alias":{"kind":"Name","value":"condition_reason"},"name":{"kind":"Name","value":"conditionReason"}},{"kind":"Field","alias":{"kind":"Name","value":"assignee_property_field_id"},"name":{"kind":"Name","value":"assigneePropertyFieldID"}},{"kind":"Field","alias":{"kind":"Name","value":"task_actions"},"name":{"kind":"Name","value":"taskActions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"trigger"},"name":{"kind":"Name","value":"trigger"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"actions"},"name":{"kind":"Name","value":"actions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"members"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"user_id"},"name":{"kind":"Name","value":"userID"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}},{"kind":"Field","alias":{"kind":"Name","value":"scheme_roles"},"name":{"kind":"Name","value":"schemeRoles"}}]}},{"kind":"Field","name":{"kind":"Name","value":"metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"target"}}]}}]}}]}}]} as unknown as DocumentNode; +export const PlaybookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Playbook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"playbook"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","alias":{"kind":"Name","value":"team_id"},"name":{"kind":"Name","value":"teamID"}},{"kind":"Field","name":{"kind":"Name","value":"public"}},{"kind":"Field","alias":{"kind":"Name","value":"delete_at"},"name":{"kind":"Name","value":"deleteAt"}},{"kind":"Field","alias":{"kind":"Name","value":"default_playbook_member_role"},"name":{"kind":"Name","value":"defaultPlaybookMemberRole"}},{"kind":"Field","alias":{"kind":"Name","value":"invited_user_ids"},"name":{"kind":"Name","value":"invitedUserIDs"}},{"kind":"Field","alias":{"kind":"Name","value":"invited_group_ids"},"name":{"kind":"Name","value":"invitedGroupIDs"}},{"kind":"Field","alias":{"kind":"Name","value":"broadcast_channel_ids"},"name":{"kind":"Name","value":"broadcastChannelIDs"}},{"kind":"Field","alias":{"kind":"Name","value":"webhook_on_creation_urls"},"name":{"kind":"Name","value":"webhookOnCreationURLs"}},{"kind":"Field","alias":{"kind":"Name","value":"reminder_timer_default_seconds"},"name":{"kind":"Name","value":"reminderTimerDefaultSeconds"}},{"kind":"Field","alias":{"kind":"Name","value":"reminder_message_template"},"name":{"kind":"Name","value":"reminderMessageTemplate"}},{"kind":"Field","alias":{"kind":"Name","value":"broadcast_enabled"},"name":{"kind":"Name","value":"broadcastEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"webhook_on_status_update_enabled"},"name":{"kind":"Name","value":"webhookOnStatusUpdateEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"webhook_on_status_update_urls"},"name":{"kind":"Name","value":"webhookOnStatusUpdateURLs"}},{"kind":"Field","alias":{"kind":"Name","value":"status_update_enabled"},"name":{"kind":"Name","value":"statusUpdateEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"retrospective_enabled"},"name":{"kind":"Name","value":"retrospectiveEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"retrospective_reminder_interval_seconds"},"name":{"kind":"Name","value":"retrospectiveReminderIntervalSeconds"}},{"kind":"Field","alias":{"kind":"Name","value":"retrospective_template"},"name":{"kind":"Name","value":"retrospectiveTemplate"}},{"kind":"Field","alias":{"kind":"Name","value":"default_owner_id"},"name":{"kind":"Name","value":"defaultOwnerID"}},{"kind":"Field","alias":{"kind":"Name","value":"run_summary_template"},"name":{"kind":"Name","value":"runSummaryTemplate"}},{"kind":"Field","alias":{"kind":"Name","value":"run_summary_template_enabled"},"name":{"kind":"Name","value":"runSummaryTemplateEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"message_on_join"},"name":{"kind":"Name","value":"messageOnJoin"}},{"kind":"Field","alias":{"kind":"Name","value":"category_name"},"name":{"kind":"Name","value":"categoryName"}},{"kind":"Field","alias":{"kind":"Name","value":"invite_users_enabled"},"name":{"kind":"Name","value":"inviteUsersEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"default_owner_enabled"},"name":{"kind":"Name","value":"defaultOwnerEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"webhook_on_creation_enabled"},"name":{"kind":"Name","value":"webhookOnCreationEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"message_on_join_enabled"},"name":{"kind":"Name","value":"messageOnJoinEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"categorize_channel_enabled"},"name":{"kind":"Name","value":"categorizeChannelEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"signal_any_keywords_enabled"},"name":{"kind":"Name","value":"signalAnyKeywordsEnabled"}},{"kind":"Field","alias":{"kind":"Name","value":"signal_any_keywords"},"name":{"kind":"Name","value":"signalAnyKeywords"}},{"kind":"Field","alias":{"kind":"Name","value":"create_public_playbook_run"},"name":{"kind":"Name","value":"createPublicPlaybookRun"}},{"kind":"Field","alias":{"kind":"Name","value":"channel_name_template"},"name":{"kind":"Name","value":"channelNameTemplate"}},{"kind":"Field","alias":{"kind":"Name","value":"create_channel_member_on_new_participant"},"name":{"kind":"Name","value":"createChannelMemberOnNewParticipant"}},{"kind":"Field","alias":{"kind":"Name","value":"remove_channel_member_on_removed_participant"},"name":{"kind":"Name","value":"removeChannelMemberOnRemovedParticipant"}},{"kind":"Field","alias":{"kind":"Name","value":"channel_id"},"name":{"kind":"Name","value":"channelID"}},{"kind":"Field","alias":{"kind":"Name","value":"channel_mode"},"name":{"kind":"Name","value":"channelMode"}},{"kind":"Field","alias":{"kind":"Name","value":"is_favorite"},"name":{"kind":"Name","value":"isFavorite"}},{"kind":"Field","name":{"kind":"Name","value":"checklists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","alias":{"kind":"Name","value":"state_modified"},"name":{"kind":"Name","value":"stateModified"}},{"kind":"Field","alias":{"kind":"Name","value":"assignee_id"},"name":{"kind":"Name","value":"assigneeID"}},{"kind":"Field","alias":{"kind":"Name","value":"assignee_type"},"name":{"kind":"Name","value":"assigneeType"}},{"kind":"Field","alias":{"kind":"Name","value":"assignee_modified"},"name":{"kind":"Name","value":"assigneeModified"}},{"kind":"Field","name":{"kind":"Name","value":"command"}},{"kind":"Field","alias":{"kind":"Name","value":"command_last_run"},"name":{"kind":"Name","value":"commandLastRun"}},{"kind":"Field","alias":{"kind":"Name","value":"due_date"},"name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","alias":{"kind":"Name","value":"condition_id"},"name":{"kind":"Name","value":"conditionID"}},{"kind":"Field","alias":{"kind":"Name","value":"condition_action"},"name":{"kind":"Name","value":"conditionAction"}},{"kind":"Field","alias":{"kind":"Name","value":"condition_reason"},"name":{"kind":"Name","value":"conditionReason"}},{"kind":"Field","alias":{"kind":"Name","value":"assignee_property_field_id"},"name":{"kind":"Name","value":"assigneePropertyFieldID"}},{"kind":"Field","alias":{"kind":"Name","value":"task_actions"},"name":{"kind":"Name","value":"taskActions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"trigger"},"name":{"kind":"Name","value":"trigger"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"actions"},"name":{"kind":"Name","value":"actions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"requirements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"members"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"user_id"},"name":{"kind":"Name","value":"userID"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}},{"kind":"Field","alias":{"kind":"Name","value":"scheme_roles"},"name":{"kind":"Name","value":"schemeRoles"}}]}},{"kind":"Field","name":{"kind":"Name","value":"metrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"target"}}]}}]}}]}}]} as unknown as DocumentNode; export const UpdatePlaybookFavoriteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdatePlaybookFavorite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"favorite"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updatePlaybookFavorite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"favorite"},"value":{"kind":"Variable","name":{"kind":"Name","value":"favorite"}}}]}]}}]} as unknown as DocumentNode; export const UpdatePlaybookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdatePlaybook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"updates"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PlaybookUpdates"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updatePlaybook"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"updates"},"value":{"kind":"Variable","name":{"kind":"Name","value":"updates"}}}]}]}}]} as unknown as DocumentNode; export const AddPlaybookMemberDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddPlaybookMember"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"playbookID"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userID"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addPlaybookMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"playbookID"},"value":{"kind":"Variable","name":{"kind":"Name","value":"playbookID"}}},{"kind":"Argument","name":{"kind":"Name","value":"userID"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userID"}}}]}]}}]} as unknown as DocumentNode; diff --git a/webapp/src/graphql/playbook.graphql b/webapp/src/graphql/playbook.graphql index 74fe58ac0f..2d3eae90b7 100644 --- a/webapp/src/graphql/playbook.graphql +++ b/webapp/src/graphql/playbook.graphql @@ -66,6 +66,11 @@ query Playbook($id: String!) { payload } } + requirements { + id + label + value + } } } members { diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index f24f47457b..e1150fb730 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -21,6 +21,7 @@ import {isConfiguredForDevelopment} from 'src/license'; import {GlobalSelectStyle} from 'src/components/backstage/styles'; import GlobalHeaderRight from 'src/components/global_header_right'; import LoginHook from 'src/components/login_hook'; +import BetaFeaturesSetting from 'src/components/backstage/beta_features_setting'; import {makeRHSOpener} from 'src/rhs_opener'; import {makeWelcomeMessagePoster} from 'src/welcome_message_poster'; import {makeSlashCommandHook} from 'src/slash_command'; @@ -218,6 +219,11 @@ export default class Plugin { registry.registerRootComponent(ChannelActionsModal); registry.registerRootComponent(LoginHook); + // System Console: Beta Features accordion (must be last among plugin settings) + if (registry.registerAdminConsoleCustomSetting) { + registry.registerAdminConsoleCustomSetting('BetaFeatures', BetaFeaturesSetting, {showTitle: false}); + } + // App Bar icon if (registry.registerAppBarComponent) { registry.registerAppBarComponent(appIcon, boundToggleRHSAction, ChannelHeaderTooltip); diff --git a/webapp/src/selectors.ts b/webapp/src/selectors.ts index 7d3efd7311..93b262ed9a 100644 --- a/webapp/src/selectors.ts +++ b/webapp/src/selectors.ts @@ -57,6 +57,9 @@ export const clientId = (state: GlobalState): string => pluginState(state).clien export const globalSettings = (state: GlobalState): GlobalSettings | null => pluginState(state).globalSettings; +export const selectTaskRequirementsEnabled = (state: GlobalState): boolean => + Boolean(globalSettings(state)?.enable_task_requirements); + /** * @returns runs indexed by playbookRunId->playbookRun */ diff --git a/webapp/src/types/playbook.ts b/webapp/src/types/playbook.ts index 3a70da8a48..f2b09b4197 100644 --- a/webapp/src/types/playbook.ts +++ b/webapp/src/types/playbook.ts @@ -145,6 +145,7 @@ export interface ChecklistItem { command_last_run: number; due_date: number; task_actions: TaskAction[]; + requirements?: TaskRequirement[]; update_at?: number; // Timestamp for idempotency checks condition_id: string; condition_action: string; @@ -152,6 +153,12 @@ export interface ChecklistItem { assignee_property_field_id?: string; } +export interface TaskRequirement { + id: string; + label: string; + value: string; +} + export interface TaskAction { trigger: Trigger; actions: Action[]; @@ -262,6 +269,7 @@ export function emptyChecklistItem(): ChecklistItem { command_last_run: 0, due_date: 0, task_actions: [] as TaskAction[], + requirements: [] as TaskRequirement[], state_modified: 0, assignee_modified: 0, assignee_id: '', @@ -281,6 +289,7 @@ export const newChecklistItem = (title = '', description = '', command = '', sta state, due_date: 0, task_actions: [] as TaskAction[], + requirements: [] as TaskRequirement[], state_modified: 0, assignee_modified: 0, assignee_id: '', diff --git a/webapp/src/types/settings.ts b/webapp/src/types/settings.ts index 4ca1380757..1c3485f6e4 100644 --- a/webapp/src/types/settings.ts +++ b/webapp/src/types/settings.ts @@ -4,11 +4,13 @@ export interface GlobalSettings { link_run_to_existing_channel_enabled: boolean; enable_experimental_features: boolean; + enable_task_requirements: boolean; } const defaults: GlobalSettings = { link_run_to_existing_channel_enabled: false, enable_experimental_features: false, + enable_task_requirements: false, }; export function globalSettingsSetDefaults(