@cat-factory/app 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Service, WorkspaceMount } from '~/types/domain'
|
|
2
|
+
import type {
|
|
3
|
+
CreateScheduleInput,
|
|
4
|
+
PipelineSchedule,
|
|
5
|
+
ScheduleRun,
|
|
6
|
+
UpdateScheduleInput,
|
|
7
|
+
} from '~/types/recurring'
|
|
8
|
+
import type { ApiContext, Position } from './context'
|
|
9
|
+
|
|
10
|
+
/** Recurring (scheduled) pipelines + the in-org shared-service mount catalog. */
|
|
11
|
+
export function recurringApi({ http, ws }: ApiContext) {
|
|
12
|
+
return {
|
|
13
|
+
// ---- recurring pipelines (scheduled runs against a service) -----------
|
|
14
|
+
listRecurringPipelines: (workspaceId: string) =>
|
|
15
|
+
http<PipelineSchedule[]>(`${ws(workspaceId)}/recurring-pipelines`),
|
|
16
|
+
|
|
17
|
+
createRecurringPipeline: (workspaceId: string, body: CreateScheduleInput) =>
|
|
18
|
+
http<PipelineSchedule>(`${ws(workspaceId)}/recurring-pipelines`, { method: 'POST', body }),
|
|
19
|
+
|
|
20
|
+
updateRecurringPipeline: (workspaceId: string, id: string, body: UpdateScheduleInput) =>
|
|
21
|
+
http<PipelineSchedule>(`${ws(workspaceId)}/recurring-pipelines/${encodeURIComponent(id)}`, {
|
|
22
|
+
method: 'PATCH',
|
|
23
|
+
body,
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
deleteRecurringPipeline: (workspaceId: string, id: string) =>
|
|
27
|
+
http(`${ws(workspaceId)}/recurring-pipelines/${encodeURIComponent(id)}`, {
|
|
28
|
+
method: 'DELETE',
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
listScheduleRuns: (workspaceId: string, id: string) =>
|
|
32
|
+
http<ScheduleRun[]>(`${ws(workspaceId)}/recurring-pipelines/${encodeURIComponent(id)}/runs`),
|
|
33
|
+
|
|
34
|
+
runScheduleNow: (workspaceId: string, id: string) =>
|
|
35
|
+
http<PipelineSchedule>(
|
|
36
|
+
`${ws(workspaceId)}/recurring-pipelines/${encodeURIComponent(id)}/run-now`,
|
|
37
|
+
{ method: 'POST' },
|
|
38
|
+
),
|
|
39
|
+
|
|
40
|
+
// ---- in-org shared services (mount/unmount + org catalog) -------------
|
|
41
|
+
// The services this workspace mounts, and the org catalog it can mount from. A 503
|
|
42
|
+
// means the feature isn't wired (the store hides its UI on any error here).
|
|
43
|
+
listServiceMounts: (workspaceId: string) =>
|
|
44
|
+
http<WorkspaceMount[]>(`${ws(workspaceId)}/services`),
|
|
45
|
+
|
|
46
|
+
listServiceCatalog: (workspaceId: string) =>
|
|
47
|
+
http<Service[]>(`${ws(workspaceId)}/services/catalog`),
|
|
48
|
+
|
|
49
|
+
mountService: (workspaceId: string, serviceId: string, body: { position?: Position } = {}) =>
|
|
50
|
+
http<WorkspaceMount>(`${ws(workspaceId)}/services/${encodeURIComponent(serviceId)}`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body,
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
unmountService: (workspaceId: string, serviceId: string) =>
|
|
56
|
+
http(`${ws(workspaceId)}/services/${encodeURIComponent(serviceId)}`, { method: 'DELETE' }),
|
|
57
|
+
|
|
58
|
+
updateMountLayout: (
|
|
59
|
+
workspaceId: string,
|
|
60
|
+
serviceId: string,
|
|
61
|
+
body: { position?: Position; size?: { w: number; h: number } | null },
|
|
62
|
+
) =>
|
|
63
|
+
http<WorkspaceMount>(`${ws(workspaceId)}/services/${encodeURIComponent(serviceId)}/layout`, {
|
|
64
|
+
method: 'PATCH',
|
|
65
|
+
body,
|
|
66
|
+
}),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatadogConnectionView,
|
|
3
|
+
ReleaseHealthConfig,
|
|
4
|
+
UpsertDatadogConnectionInput,
|
|
5
|
+
UpsertReleaseHealthConfigInput,
|
|
6
|
+
} from '~/types/releaseHealth'
|
|
7
|
+
import type { ApiContext } from './context'
|
|
8
|
+
|
|
9
|
+
/** Datadog post-release-health: the connection + per-block monitor/SLO mapping. */
|
|
10
|
+
export function releaseHealthApi({ http, ws }: ApiContext) {
|
|
11
|
+
return {
|
|
12
|
+
// ---- Datadog post-release-health settings -----------------------------
|
|
13
|
+
getDatadogConnection: (workspaceId: string) =>
|
|
14
|
+
http<DatadogConnectionView>(`${ws(workspaceId)}/datadog/connection`),
|
|
15
|
+
|
|
16
|
+
setDatadogConnection: (workspaceId: string, body: UpsertDatadogConnectionInput) =>
|
|
17
|
+
http<DatadogConnectionView>(`${ws(workspaceId)}/datadog/connection`, {
|
|
18
|
+
method: 'PUT',
|
|
19
|
+
body,
|
|
20
|
+
}),
|
|
21
|
+
|
|
22
|
+
deleteDatadogConnection: (workspaceId: string) =>
|
|
23
|
+
http(`${ws(workspaceId)}/datadog/connection`, { method: 'DELETE' }),
|
|
24
|
+
|
|
25
|
+
listReleaseHealthConfigs: (workspaceId: string) =>
|
|
26
|
+
http<ReleaseHealthConfig[]>(`${ws(workspaceId)}/release-health-configs`),
|
|
27
|
+
|
|
28
|
+
upsertReleaseHealthConfig: (
|
|
29
|
+
workspaceId: string,
|
|
30
|
+
blockId: string,
|
|
31
|
+
body: UpsertReleaseHealthConfigInput,
|
|
32
|
+
) =>
|
|
33
|
+
http<ReleaseHealthConfig>(
|
|
34
|
+
`${ws(workspaceId)}/release-health-configs/${encodeURIComponent(blockId)}`,
|
|
35
|
+
{ method: 'PUT', body },
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
deleteReleaseHealthConfig: (workspaceId: string, blockId: string) =>
|
|
39
|
+
http(`${ws(workspaceId)}/release-health-configs/${encodeURIComponent(blockId)}`, {
|
|
40
|
+
method: 'DELETE',
|
|
41
|
+
}),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { ClarityReview, ResolveClarityExceededChoice } from '~/types/clarity'
|
|
2
|
+
import type { ConsensusSession } from '~/types/consensus'
|
|
3
|
+
import type {
|
|
4
|
+
RequirementReview,
|
|
5
|
+
ResolveRequirementsExceededChoice,
|
|
6
|
+
ReviewItemStatus,
|
|
7
|
+
} from '~/types/requirements'
|
|
8
|
+
import type { ApiContext } from './context'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The two iterative gate reviewers (requirements + clarity) and the consensus
|
|
12
|
+
* session read. Each reviewer follows the same answer → incorporate → re-review
|
|
13
|
+
* → proceed/resolve-exceeded loop.
|
|
14
|
+
*/
|
|
15
|
+
export function reviewsApi({ http, ws }: ApiContext) {
|
|
16
|
+
return {
|
|
17
|
+
// ---- requirements review (stateless reviewer agent) ------------------
|
|
18
|
+
// The current review for a block (null when none has been run). A 503 means
|
|
19
|
+
// the feature is unconfigured (the panel hides on any error here).
|
|
20
|
+
getRequirementReview: (workspaceId: string, blockId: string) =>
|
|
21
|
+
http<RequirementReview | null>(
|
|
22
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review`,
|
|
23
|
+
),
|
|
24
|
+
|
|
25
|
+
// The latest consensus session for a block (`{ session: null }` when none / consensus
|
|
26
|
+
// off). The live transcript also arrives via the `consensus` stream event.
|
|
27
|
+
getConsensusSession: (workspaceId: string, blockId: string) =>
|
|
28
|
+
http<{ session: ConsensusSession | null }>(
|
|
29
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/consensus-session`,
|
|
30
|
+
),
|
|
31
|
+
|
|
32
|
+
replyRequirementItem: (workspaceId: string, reviewId: string, itemId: string, reply: string) =>
|
|
33
|
+
http<RequirementReview>(
|
|
34
|
+
`${ws(workspaceId)}/requirement-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}/reply`,
|
|
35
|
+
{ method: 'POST', body: { reply } },
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
setRequirementItemStatus: (
|
|
39
|
+
workspaceId: string,
|
|
40
|
+
reviewId: string,
|
|
41
|
+
itemId: string,
|
|
42
|
+
status: ReviewItemStatus,
|
|
43
|
+
) =>
|
|
44
|
+
http<RequirementReview>(
|
|
45
|
+
`${ws(workspaceId)}/requirement-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}`,
|
|
46
|
+
{ method: 'PATCH', body: { status } },
|
|
47
|
+
),
|
|
48
|
+
|
|
49
|
+
// Incorporate the answers ASYNCHRONOUSLY (every finding must be answered or dismissed).
|
|
50
|
+
// The durable driver folds them and re-reviews in the background. Optional `feedback` is
|
|
51
|
+
// the "do it differently" lever when redoing a merge. Returns the `incorporating` review
|
|
52
|
+
// at once; a notification calls the user back only if the re-review needs input.
|
|
53
|
+
incorporateRequirements: (workspaceId: string, blockId: string, feedback?: string) =>
|
|
54
|
+
http<RequirementReview>(
|
|
55
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review/incorporate`,
|
|
56
|
+
{ method: 'POST', body: feedback ? { feedback } : {} },
|
|
57
|
+
),
|
|
58
|
+
|
|
59
|
+
// Re-review the incorporated document (one more reviewer pass). On convergence the
|
|
60
|
+
// parked run advances; otherwise the response carries the next cycle / cap state.
|
|
61
|
+
reReviewRequirements: (workspaceId: string, blockId: string) =>
|
|
62
|
+
http<RequirementReview>(
|
|
63
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review/re-review`,
|
|
64
|
+
{ method: 'POST' },
|
|
65
|
+
),
|
|
66
|
+
|
|
67
|
+
// Proceed: settle the requirements and advance the parked run (all findings dismissed).
|
|
68
|
+
proceedRequirements: (workspaceId: string, blockId: string) =>
|
|
69
|
+
http<RequirementReview>(
|
|
70
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review/proceed`,
|
|
71
|
+
{ method: 'POST' },
|
|
72
|
+
),
|
|
73
|
+
|
|
74
|
+
// Resolve a review that hit its iteration cap: extra-round / proceed / stop-reset.
|
|
75
|
+
resolveRequirementsExceeded: (
|
|
76
|
+
workspaceId: string,
|
|
77
|
+
blockId: string,
|
|
78
|
+
choice: ResolveRequirementsExceededChoice,
|
|
79
|
+
) =>
|
|
80
|
+
http<RequirementReview>(
|
|
81
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/requirement-review/resolve-exceeded`,
|
|
82
|
+
{ method: 'POST', body: { choice } },
|
|
83
|
+
),
|
|
84
|
+
|
|
85
|
+
// ---- clarity review (bug-report triage reviewer agent) ---------------
|
|
86
|
+
// The current review for a block (null when none has been run). A 503 means
|
|
87
|
+
// the feature is unconfigured (the panel hides on any error here).
|
|
88
|
+
getClarityReview: (workspaceId: string, blockId: string) =>
|
|
89
|
+
http<ClarityReview | null>(
|
|
90
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review`,
|
|
91
|
+
),
|
|
92
|
+
|
|
93
|
+
replyClarityItem: (workspaceId: string, reviewId: string, itemId: string, reply: string) =>
|
|
94
|
+
http<ClarityReview>(
|
|
95
|
+
`${ws(workspaceId)}/clarity-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}/reply`,
|
|
96
|
+
{ method: 'POST', body: { reply } },
|
|
97
|
+
),
|
|
98
|
+
|
|
99
|
+
setClarityItemStatus: (
|
|
100
|
+
workspaceId: string,
|
|
101
|
+
reviewId: string,
|
|
102
|
+
itemId: string,
|
|
103
|
+
status: ReviewItemStatus,
|
|
104
|
+
) =>
|
|
105
|
+
http<ClarityReview>(
|
|
106
|
+
`${ws(workspaceId)}/clarity-reviews/${encodeURIComponent(reviewId)}/items/${encodeURIComponent(itemId)}`,
|
|
107
|
+
{ method: 'PATCH', body: { status } },
|
|
108
|
+
),
|
|
109
|
+
|
|
110
|
+
// Incorporate the answers ASYNCHRONOUSLY (every finding must be answered or dismissed).
|
|
111
|
+
// The durable driver folds them and re-reviews in the background. Optional `feedback` is
|
|
112
|
+
// the "do it differently" lever when redoing a merge. Returns the `incorporating` review
|
|
113
|
+
// at once; a notification calls the user back only if the re-review needs input.
|
|
114
|
+
incorporateClarity: (workspaceId: string, blockId: string, feedback?: string) =>
|
|
115
|
+
http<ClarityReview>(
|
|
116
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review/incorporate`,
|
|
117
|
+
{ method: 'POST', body: feedback ? { feedback } : {} },
|
|
118
|
+
),
|
|
119
|
+
|
|
120
|
+
// Re-review the clarified report (one more reviewer pass). On convergence the parked run
|
|
121
|
+
// advances; otherwise the response carries the next cycle / cap state.
|
|
122
|
+
reReviewClarity: (workspaceId: string, blockId: string) =>
|
|
123
|
+
http<ClarityReview>(
|
|
124
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review/re-review`,
|
|
125
|
+
{ method: 'POST' },
|
|
126
|
+
),
|
|
127
|
+
|
|
128
|
+
// Proceed: settle the clarity review and advance the parked run (all findings dismissed).
|
|
129
|
+
proceedClarity: (workspaceId: string, blockId: string) =>
|
|
130
|
+
http<ClarityReview>(
|
|
131
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review/proceed`,
|
|
132
|
+
{ method: 'POST' },
|
|
133
|
+
),
|
|
134
|
+
|
|
135
|
+
// Resolve a review that hit its iteration cap: extra-round / proceed / stop-reset.
|
|
136
|
+
resolveClarityExceeded: (
|
|
137
|
+
workspaceId: string,
|
|
138
|
+
blockId: string,
|
|
139
|
+
choice: ResolveClarityExceededChoice,
|
|
140
|
+
) =>
|
|
141
|
+
http<ClarityReview>(
|
|
142
|
+
`${ws(workspaceId)}/blocks/${encodeURIComponent(blockId)}/clarity-review/resolve-exceeded`,
|
|
143
|
+
{ method: 'POST', body: { choice } },
|
|
144
|
+
),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SlackChannel,
|
|
3
|
+
SlackConnection,
|
|
4
|
+
SlackMemberMappingEntry,
|
|
5
|
+
SlackNotificationSettings,
|
|
6
|
+
} from '~/types/slack'
|
|
7
|
+
import type { ApiContext } from './context'
|
|
8
|
+
|
|
9
|
+
/** Slack integration: per-account connection, per-workspace routing + member map. */
|
|
10
|
+
export function slackApi({ http, ws }: ApiContext) {
|
|
11
|
+
return {
|
|
12
|
+
// ---- slack integration (extra notification transport) -----------------
|
|
13
|
+
// Per-account connection (manual bot-token paste + the OAuth "Add to Slack"
|
|
14
|
+
// URL), per-workspace routing, and the per-account member map. A 503 from
|
|
15
|
+
// `getSlackConnection` means the integration is off (the store hides its UI).
|
|
16
|
+
getSlackConnection: (workspaceId: string) =>
|
|
17
|
+
http<{ connection: SlackConnection | null; oauthEnabled: boolean }>(
|
|
18
|
+
`${ws(workspaceId)}/slack/connection`,
|
|
19
|
+
),
|
|
20
|
+
|
|
21
|
+
getSlackInstallUrl: (workspaceId: string) =>
|
|
22
|
+
http<{ url: string }>(`${ws(workspaceId)}/slack/install-url`),
|
|
23
|
+
|
|
24
|
+
connectSlack: (workspaceId: string, token: string) =>
|
|
25
|
+
http<SlackConnection>(`${ws(workspaceId)}/slack/connect`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
body: { token },
|
|
28
|
+
}),
|
|
29
|
+
|
|
30
|
+
disconnectSlack: (workspaceId: string) =>
|
|
31
|
+
http(`${ws(workspaceId)}/slack/connection`, { method: 'DELETE' }),
|
|
32
|
+
|
|
33
|
+
listSlackChannels: (workspaceId: string) =>
|
|
34
|
+
http<{ channels: SlackChannel[] }>(`${ws(workspaceId)}/slack/channels`),
|
|
35
|
+
|
|
36
|
+
getSlackSettings: (workspaceId: string) =>
|
|
37
|
+
http<SlackNotificationSettings>(`${ws(workspaceId)}/slack/settings`),
|
|
38
|
+
|
|
39
|
+
updateSlackSettings: (
|
|
40
|
+
workspaceId: string,
|
|
41
|
+
body: { routes: SlackNotificationSettings['routes']; mentionsEnabled: boolean },
|
|
42
|
+
) =>
|
|
43
|
+
http<SlackNotificationSettings>(`${ws(workspaceId)}/slack/settings`, { method: 'PUT', body }),
|
|
44
|
+
|
|
45
|
+
getSlackMemberMapping: (workspaceId: string) =>
|
|
46
|
+
http<{ entries: SlackMemberMappingEntry[] }>(`${ws(workspaceId)}/slack/member-mapping`),
|
|
47
|
+
|
|
48
|
+
updateSlackMemberMapping: (workspaceId: string, entries: SlackMemberMappingEntry[]) =>
|
|
49
|
+
http<{ entries: SlackMemberMappingEntry[] }>(`${ws(workspaceId)}/slack/member-mapping`, {
|
|
50
|
+
method: 'PUT',
|
|
51
|
+
body: { entries },
|
|
52
|
+
}),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Block,
|
|
3
|
+
SourceTask,
|
|
4
|
+
TaskConnection,
|
|
5
|
+
TaskSearchResult,
|
|
6
|
+
TaskSourceDescriptor,
|
|
7
|
+
TaskSourceKind,
|
|
8
|
+
} from '~/types/domain'
|
|
9
|
+
import type { PutTrackerSettingsInput, TrackerSettings } from '~/types/tracker'
|
|
10
|
+
import type { ApiContext } from './context'
|
|
11
|
+
|
|
12
|
+
/** Task sources (Jira, …): connect/import/search/link + the workspace tracker selection. */
|
|
13
|
+
export function tasksApi({ http, ws }: ApiContext) {
|
|
14
|
+
return {
|
|
15
|
+
// ---- task sources (Jira, …) ------------------------------------------
|
|
16
|
+
// The configured trackers + their connect/import metadata. A 503 means the
|
|
17
|
+
// integration is off (the store hides its UI on any error here).
|
|
18
|
+
listTaskSources: (workspaceId: string) =>
|
|
19
|
+
http<{ sources: TaskSourceDescriptor[] }>(`${ws(workspaceId)}/task-sources`),
|
|
20
|
+
|
|
21
|
+
listTaskConnections: (workspaceId: string) =>
|
|
22
|
+
http<{ connections: TaskConnection[] }>(`${ws(workspaceId)}/task-sources/connections`),
|
|
23
|
+
|
|
24
|
+
connectTaskSource: (
|
|
25
|
+
workspaceId: string,
|
|
26
|
+
source: TaskSourceKind,
|
|
27
|
+
credentials: Record<string, string>,
|
|
28
|
+
) =>
|
|
29
|
+
http<TaskConnection>(`${ws(workspaceId)}/task-sources/${source}/connect`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
body: { credentials },
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
disconnectTaskSource: (workspaceId: string, source: TaskSourceKind) =>
|
|
35
|
+
http(`${ws(workspaceId)}/task-sources/${source}/connection`, { method: 'DELETE' }),
|
|
36
|
+
|
|
37
|
+
listTasks: (workspaceId: string) => http<SourceTask[]>(`${ws(workspaceId)}/tasks`),
|
|
38
|
+
|
|
39
|
+
importTask: (workspaceId: string, source: TaskSourceKind, body: { ref: string }) =>
|
|
40
|
+
http<SourceTask>(`${ws(workspaceId)}/task-sources/${source}/import`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
body,
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
searchTaskSource: (workspaceId: string, source: TaskSourceKind, query: string) =>
|
|
46
|
+
http<{ results: TaskSearchResult[] }>(`${ws(workspaceId)}/task-sources/${source}/search`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: { query },
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
linkTask: (
|
|
52
|
+
workspaceId: string,
|
|
53
|
+
body: { source: TaskSourceKind; externalId: string; blockId: string },
|
|
54
|
+
) => http<SourceTask>(`${ws(workspaceId)}/tasks/link`, { method: 'POST', body }),
|
|
55
|
+
|
|
56
|
+
createTaskFromIssue: (
|
|
57
|
+
workspaceId: string,
|
|
58
|
+
body: { source: TaskSourceKind; externalId: string; containerId: string },
|
|
59
|
+
) =>
|
|
60
|
+
http<{ block: Block; task: SourceTask }>(`${ws(workspaceId)}/tasks/create-block`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body,
|
|
63
|
+
}),
|
|
64
|
+
|
|
65
|
+
// ---- issue-tracker selection (workspace-level) ------------------------
|
|
66
|
+
getTrackerSettings: (workspaceId: string) =>
|
|
67
|
+
http<TrackerSettings>(`${ws(workspaceId)}/tracker-settings`),
|
|
68
|
+
|
|
69
|
+
putTrackerSettings: (workspaceId: string, body: PutTrackerSettingsInput) =>
|
|
70
|
+
http<TrackerSettings>(`${ws(workspaceId)}/tracker-settings`, { method: 'PUT', body }),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UpdateWorkspaceSettingsInput,
|
|
3
|
+
Workspace,
|
|
4
|
+
WorkspaceSettings,
|
|
5
|
+
WorkspaceSnapshot,
|
|
6
|
+
} from '~/types/domain'
|
|
7
|
+
import type { ApiContext } from './context'
|
|
8
|
+
|
|
9
|
+
/** Workspace CRUD + the full snapshot read. */
|
|
10
|
+
export function workspacesApi({ http, ws }: ApiContext) {
|
|
11
|
+
return {
|
|
12
|
+
// ---- workspaces -------------------------------------------------------
|
|
13
|
+
listWorkspaces: () => http<Workspace[]>('/workspaces'),
|
|
14
|
+
|
|
15
|
+
createWorkspace: (
|
|
16
|
+
body: { name?: string; description?: string; seed?: boolean; accountId?: string } = {},
|
|
17
|
+
) => http<WorkspaceSnapshot>('/workspaces', { method: 'POST', body }),
|
|
18
|
+
|
|
19
|
+
getWorkspace: (workspaceId: string) => http<WorkspaceSnapshot>(ws(workspaceId)),
|
|
20
|
+
|
|
21
|
+
updateWorkspace: (workspaceId: string, body: { name?: string; description?: string | null }) =>
|
|
22
|
+
http<Workspace>(ws(workspaceId), { method: 'PATCH', body }),
|
|
23
|
+
|
|
24
|
+
renameWorkspace: (workspaceId: string, name: string) =>
|
|
25
|
+
http<Workspace>(ws(workspaceId), { method: 'PATCH', body: { name } }),
|
|
26
|
+
|
|
27
|
+
deleteWorkspace: (workspaceId: string) => http(ws(workspaceId), { method: 'DELETE' }),
|
|
28
|
+
|
|
29
|
+
// ---- workspace runtime settings (human-wait escalation + per-service task limit) --
|
|
30
|
+
getWorkspaceSettings: (workspaceId: string) =>
|
|
31
|
+
http<WorkspaceSettings>(`${ws(workspaceId)}/settings`),
|
|
32
|
+
|
|
33
|
+
updateWorkspaceSettings: (workspaceId: string, body: UpdateWorkspaceSettingsInput) =>
|
|
34
|
+
http<WorkspaceSettings>(`${ws(workspaceId)}/settings`, { method: 'PUT', body }),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { FragmentOwnerKind } from '~/types/domain'
|
|
2
|
+
import type { ApiContext } from './api/context'
|
|
3
|
+
import { accountsApi } from './api/accounts'
|
|
4
|
+
import { authApi } from './api/auth'
|
|
5
|
+
import { bootstrapApi } from './api/bootstrap'
|
|
6
|
+
import { boardApi } from './api/board'
|
|
7
|
+
import { documentsApi } from './api/documents'
|
|
8
|
+
import { executionApi } from './api/execution'
|
|
9
|
+
import { fragmentsApi } from './api/fragments'
|
|
10
|
+
import { githubApi } from './api/github'
|
|
11
|
+
import { modelsApi } from './api/models'
|
|
12
|
+
import { notificationsApi } from './api/notifications'
|
|
13
|
+
import { presetsApi } from './api/presets'
|
|
14
|
+
import { recurringApi } from './api/recurring'
|
|
15
|
+
import { releaseHealthApi } from './api/releaseHealth'
|
|
16
|
+
import { reviewsApi } from './api/reviews'
|
|
17
|
+
import { slackApi } from './api/slack'
|
|
18
|
+
import { tasksApi } from './api/tasks'
|
|
19
|
+
import { workspacesApi } from './api/workspaces'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Thin typed client over the cat-factory backend (a Hono worker). Every method
|
|
23
|
+
* maps to one REST endpoint; the request/response shapes mirror
|
|
24
|
+
* `@cat-factory/contracts`, so responses drop straight into the Pinia stores.
|
|
25
|
+
*
|
|
26
|
+
* The endpoints are grouped by domain into the `./api/*` factory modules; this
|
|
27
|
+
* function builds the shared {@link ApiContext} (the authed `$fetch` client +
|
|
28
|
+
* path/header helpers) and spreads every group into a single flat client, so
|
|
29
|
+
* call sites stay `useApi().someMethod(...)`.
|
|
30
|
+
*
|
|
31
|
+
* The base URL comes from runtime config (`NUXT_PUBLIC_API_BASE`), defaulting to
|
|
32
|
+
* the local wrangler dev server — see `nuxt.config.ts`.
|
|
33
|
+
*/
|
|
34
|
+
export function useApi() {
|
|
35
|
+
const apiBase = useRuntimeConfig().public.apiBase
|
|
36
|
+
const http = $fetch.create({
|
|
37
|
+
baseURL: apiBase,
|
|
38
|
+
// Attach the session token (when signed in) so the backend's auth gate lets
|
|
39
|
+
// the request through. Read lazily from the store so a fresh token applies
|
|
40
|
+
// without rebuilding the client.
|
|
41
|
+
onRequest({ options }) {
|
|
42
|
+
const token = useAuthStore().token
|
|
43
|
+
if (!token) return
|
|
44
|
+
const headers = new Headers(options.headers)
|
|
45
|
+
headers.set('Authorization', `Bearer ${token}`)
|
|
46
|
+
options.headers = headers
|
|
47
|
+
},
|
|
48
|
+
// A 401 means our token lapsed or was revoked — drop it so the UI re-gates.
|
|
49
|
+
onResponseError({ response }) {
|
|
50
|
+
if (response?.status === 401) useAuthStore().handleUnauthorized()
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// The personal-subscription unlock password (individual-usage vendors) rides as an
|
|
55
|
+
// ambient request header — like the bearer token — so it never lands in a request
|
|
56
|
+
// body/wire-contract payload. Mirrors PERSONAL_PASSWORD_HEADER in @cat-factory/contracts.
|
|
57
|
+
const pwHeaders = (password?: string): Record<string, string> | undefined =>
|
|
58
|
+
password ? { 'X-Personal-Password': password } : undefined
|
|
59
|
+
|
|
60
|
+
const ws = (workspaceId: string) => `/workspaces/${encodeURIComponent(workspaceId)}`
|
|
61
|
+
// Prompt-fragment library routes exist at both tiers; resolve the prefix from
|
|
62
|
+
// the owner scope (ADR 0006 §8).
|
|
63
|
+
const scope = (kind: FragmentOwnerKind, id: string) =>
|
|
64
|
+
kind === 'account'
|
|
65
|
+
? `/accounts/${encodeURIComponent(id)}`
|
|
66
|
+
: `/workspaces/${encodeURIComponent(id)}`
|
|
67
|
+
|
|
68
|
+
const ctx: ApiContext = { http, ws, scope, pwHeaders }
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...authApi(ctx),
|
|
72
|
+
...fragmentsApi(ctx),
|
|
73
|
+
...modelsApi(ctx),
|
|
74
|
+
...accountsApi(ctx),
|
|
75
|
+
...workspacesApi(ctx),
|
|
76
|
+
...boardApi(ctx),
|
|
77
|
+
...executionApi(ctx),
|
|
78
|
+
...documentsApi(ctx),
|
|
79
|
+
...tasksApi(ctx),
|
|
80
|
+
...reviewsApi(ctx),
|
|
81
|
+
...notificationsApi(ctx),
|
|
82
|
+
...presetsApi(ctx),
|
|
83
|
+
...releaseHealthApi(ctx),
|
|
84
|
+
...recurringApi(ctx),
|
|
85
|
+
...githubApi(ctx),
|
|
86
|
+
...slackApi(ctx),
|
|
87
|
+
...bootstrapApi(ctx),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pointer-driven dragging for blocks positioned inside a container's 2D canvas
|
|
6
|
+
* (tasks inside services/modules, modules inside services). Movement is divided
|
|
7
|
+
* by the board zoom so the block tracks the cursor. When `reparent` is set, the
|
|
8
|
+
* drop point is hit-tested against `[data-drop-zone]` ancestors so a task can be
|
|
9
|
+
* dragged from a service into a module (or back out).
|
|
10
|
+
*/
|
|
11
|
+
export function useBlockDrag() {
|
|
12
|
+
const board = useBoardStore()
|
|
13
|
+
const ui = useUiStore()
|
|
14
|
+
const draggingId = ref<string | null>(null)
|
|
15
|
+
|
|
16
|
+
function startDrag(
|
|
17
|
+
block: Block,
|
|
18
|
+
e: PointerEvent,
|
|
19
|
+
opts: { reparent?: boolean; clamp?: boolean } = {},
|
|
20
|
+
) {
|
|
21
|
+
if (e.button !== 0) return
|
|
22
|
+
e.preventDefault()
|
|
23
|
+
e.stopPropagation()
|
|
24
|
+
const startX = e.clientX
|
|
25
|
+
const startY = e.clientY
|
|
26
|
+
const orig = { ...block.position }
|
|
27
|
+
// Container-local blocks (tasks/modules) are clamped to their parent's origin;
|
|
28
|
+
// frames live in free-floating flow space, so they opt out via `clamp: false`.
|
|
29
|
+
const clamp = opts.clamp ?? true
|
|
30
|
+
draggingId.value = block.id
|
|
31
|
+
// Position is only previewed locally while dragging and persisted once on
|
|
32
|
+
// release. Writing every move raced — a late, out-of-order response could land
|
|
33
|
+
// a stale position last and make the block jump after the user let go.
|
|
34
|
+
let moved = false
|
|
35
|
+
let last = orig
|
|
36
|
+
|
|
37
|
+
const onMove = (ev: PointerEvent) => {
|
|
38
|
+
const z = ui.zoom || 1
|
|
39
|
+
const nx = orig.x + (ev.clientX - startX) / z
|
|
40
|
+
const ny = orig.y + (ev.clientY - startY) / z
|
|
41
|
+
moved = true
|
|
42
|
+
last = { x: clamp ? Math.max(0, nx) : nx, y: clamp ? Math.max(0, ny) : ny }
|
|
43
|
+
board.previewMove(block.id, last)
|
|
44
|
+
}
|
|
45
|
+
const onUp = (ev: PointerEvent) => {
|
|
46
|
+
window.removeEventListener('pointermove', onMove)
|
|
47
|
+
window.removeEventListener('pointerup', onUp)
|
|
48
|
+
if (moved) {
|
|
49
|
+
// A successful reparent persists the move itself; otherwise commit the final
|
|
50
|
+
// position in place. Either way it's a single write, not one per frame. Run
|
|
51
|
+
// the hit-test BEFORE clearing draggingId so the dragged element is still
|
|
52
|
+
// marked non-interactive (see DraggableTask) and the zone beneath resolves.
|
|
53
|
+
const reparented = opts.reparent && reparentAt(block, ev.clientX, ev.clientY)
|
|
54
|
+
if (!reparented) void board.moveBlock(block.id, last)
|
|
55
|
+
}
|
|
56
|
+
draggingId.value = null
|
|
57
|
+
}
|
|
58
|
+
window.addEventListener('pointermove', onMove)
|
|
59
|
+
window.addEventListener('pointerup', onUp)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Returns true when the block was dropped into a *different* container. */
|
|
63
|
+
function reparentAt(block: Block, clientX: number, clientY: number): boolean {
|
|
64
|
+
const el = document.querySelector(`[data-block-id="${block.id}"]`) as HTMLElement | null
|
|
65
|
+
if (!el) return false
|
|
66
|
+
// The dragged block is already non-interactive while dragging (DraggableTask
|
|
67
|
+
// drops pointer-events on the whole wrapper, handle included); belt-and-braces,
|
|
68
|
+
// also neutralise this node so elementFromPoint resolves the zone beneath it.
|
|
69
|
+
const prev = el.style.pointerEvents
|
|
70
|
+
el.style.pointerEvents = 'none'
|
|
71
|
+
const under = document.elementFromPoint(clientX, clientY) as HTMLElement | null
|
|
72
|
+
const zoneEl = under?.closest('[data-drop-zone]') as HTMLElement | null
|
|
73
|
+
el.style.pointerEvents = prev
|
|
74
|
+
if (!zoneEl) return false
|
|
75
|
+
|
|
76
|
+
const newParent = zoneEl.getAttribute('data-drop-zone')!
|
|
77
|
+
if (newParent === block.parentId) return false // same container — caller commits position
|
|
78
|
+
|
|
79
|
+
const z = ui.zoom || 1
|
|
80
|
+
const zr = zoneEl.getBoundingClientRect()
|
|
81
|
+
const er = el.getBoundingClientRect()
|
|
82
|
+
void board.reparentBlock(block.id, newParent, {
|
|
83
|
+
x: Math.max(0, (er.left - zr.left) / z),
|
|
84
|
+
y: Math.max(0, (er.top - zr.top) / z),
|
|
85
|
+
})
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { draggingId, startDrag }
|
|
90
|
+
}
|