mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
* MUL-8120 feat(workflows): record workflow owner and let owners edit their workflows Any workspace member may now create a workflow and becomes its owner. Editing, deleting and changing handoffs is open to the owner and to workspace owners/admins. Default has no owner and stays admin-only. Copying a workflow for one project now also requires managing that project. Migration 585 adds issue_workflow.owner_id and backfills existing named workflows with the workspace's earliest owner. Co-authored-by: multica-agent <[email protected]> * MUL-8120 feat(workflows): mirror owner-based workflow permissions in the UI Members can create and duplicate workflows; the editor, column handoff and flow panel are editable by the workflow's owner and admins. The save-for-this-project option only shows to admins and the project lead. Co-authored-by: multica-agent <[email protected]> * MUL-8120 fix(workflows): only admins add statuses through workflow writes Workflow create/update could add statuses to the shared workspace catalog without a role check. Non-admins now get 403 status_create_forbidden when a step names a status that does not exist; reusing a status by key or by name stays open. Co-authored-by: multica-agent <[email protected]> * MUL-8120 fix(workflows): hide new-status for members in the workflow editor Members pick from existing statuses only. A template that needs statuses the workspace lacks shows which ones and keeps Save disabled until they are removed or an admin adds them. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]>
157 lines
5.7 KiB
SQL
157 lines
5.7 KiB
SQL
-- Project workflows (MUL-7420). A workflow selects and orders statuses from
|
|
-- the workspace's issue_status catalog. Projects opt in through
|
|
-- project.workflow_id; a project without one uses the workspace's Default
|
|
-- workflow row. Each step also stores who an issue entering it is handed to.
|
|
|
|
-- name: ListIssueWorkflows :many
|
|
SELECT * FROM issue_workflow
|
|
WHERE workspace_id = sqlc.arg('workspace_id')::uuid
|
|
ORDER BY is_default DESC, lower(name), created_at, id;
|
|
|
|
-- name: GetDefaultIssueWorkflow :one
|
|
SELECT * FROM issue_workflow
|
|
WHERE workspace_id = sqlc.arg('workspace_id')::uuid
|
|
AND is_default;
|
|
|
|
-- name: CreateDefaultIssueWorkflow :many
|
|
-- Returns no row when another request created the Default first.
|
|
INSERT INTO issue_workflow (workspace_id, name, description, initial_status_key, steps, is_default)
|
|
VALUES (
|
|
sqlc.arg('workspace_id')::uuid,
|
|
'Default',
|
|
'',
|
|
'todo',
|
|
sqlc.arg('steps')::jsonb,
|
|
true
|
|
)
|
|
ON CONFLICT (workspace_id) WHERE is_default DO NOTHING
|
|
RETURNING *;
|
|
|
|
-- name: ListNamedIssueWorkflowSteps :many
|
|
-- The steps of every workflow except Default, for the Default snapshot.
|
|
SELECT steps FROM issue_workflow
|
|
WHERE workspace_id = sqlc.arg('workspace_id')::uuid
|
|
AND NOT is_default;
|
|
|
|
-- name: RemoveStatusFromDefaultIssueWorkflow :exec
|
|
-- An archived status leaves Default with it; Default never blocks an archive.
|
|
UPDATE issue_workflow SET
|
|
steps = COALESCE((
|
|
SELECT jsonb_agg(step ORDER BY ord)
|
|
FROM jsonb_array_elements(steps) WITH ORDINALITY AS s(step, ord)
|
|
WHERE step->>'status_key' <> sqlc.arg('status_key')::text
|
|
), '[]'::jsonb),
|
|
updated_at = now()
|
|
WHERE workspace_id = sqlc.arg('workspace_id')::uuid
|
|
AND is_default
|
|
AND steps @> jsonb_build_array(jsonb_build_object('status_key', sqlc.arg('status_key')::text));
|
|
|
|
-- name: ListProjectsUsingDefaultIssueWorkflow :many
|
|
SELECT * FROM project
|
|
WHERE workspace_id = sqlc.arg('workspace_id')::uuid
|
|
AND workflow_id IS NULL
|
|
ORDER BY created_at, id;
|
|
|
|
-- name: GetIssueWorkflow :one
|
|
SELECT * FROM issue_workflow
|
|
WHERE id = sqlc.arg('id')::uuid
|
|
AND workspace_id = sqlc.arg('workspace_id')::uuid;
|
|
|
|
-- name: CreateIssueWorkflow :one
|
|
INSERT INTO issue_workflow (workspace_id, name, description, initial_status_key, steps, owner_id)
|
|
VALUES (
|
|
sqlc.arg('workspace_id')::uuid,
|
|
sqlc.arg('name')::text,
|
|
sqlc.arg('description')::text,
|
|
sqlc.arg('initial_status_key')::text,
|
|
sqlc.arg('steps')::jsonb,
|
|
sqlc.arg('owner_id')::uuid
|
|
)
|
|
RETURNING *;
|
|
|
|
-- name: UpdateIssueWorkflow :one
|
|
UPDATE issue_workflow SET
|
|
name = sqlc.arg('name')::text,
|
|
description = sqlc.arg('description')::text,
|
|
initial_status_key = sqlc.arg('initial_status_key')::text,
|
|
steps = sqlc.arg('steps')::jsonb,
|
|
updated_at = now()
|
|
WHERE id = sqlc.arg('id')::uuid
|
|
AND workspace_id = sqlc.arg('workspace_id')::uuid
|
|
RETURNING *;
|
|
|
|
-- name: DeleteIssueWorkflow :execrows
|
|
DELETE FROM issue_workflow
|
|
WHERE id = sqlc.arg('id')::uuid
|
|
AND workspace_id = sqlc.arg('workspace_id')::uuid
|
|
AND NOT is_default;
|
|
|
|
-- name: DeleteIssueWorkflowsForWorkspace :exec
|
|
-- No foreign keys by project rule, so workspace teardown cleans up here.
|
|
DELETE FROM issue_workflow WHERE workspace_id = sqlc.arg('workspace_id')::uuid;
|
|
|
|
-- name: ListIssueWorkflowNamesUsingStatusKey :many
|
|
-- Archive guard for the status catalog: a status a workflow still lists cannot
|
|
-- be retired, or that workflow's projects would lose a column.
|
|
SELECT name FROM issue_workflow
|
|
WHERE workspace_id = sqlc.arg('workspace_id')::uuid
|
|
AND NOT is_default
|
|
AND steps @> jsonb_build_array(jsonb_build_object('status_key', sqlc.arg('status_key')::text))
|
|
ORDER BY lower(name);
|
|
|
|
-- name: ListProjectsUsingIssueWorkflow :many
|
|
SELECT * FROM project
|
|
WHERE workspace_id = sqlc.arg('workspace_id')::uuid
|
|
AND workflow_id = sqlc.arg('workflow_id')::uuid
|
|
ORDER BY created_at, id;
|
|
|
|
-- name: CountProjectIssuesByStatus :many
|
|
-- Status census for a workflow switch or step removal, including terminal
|
|
-- issues: every issue of the project must end on a status its workflow lists.
|
|
SELECT status, COUNT(*)::bigint AS issue_count
|
|
FROM issue
|
|
WHERE workspace_id = sqlc.arg('workspace_id')::uuid
|
|
AND project_id = ANY(sqlc.arg('project_ids')::uuid[])
|
|
GROUP BY status
|
|
ORDER BY status;
|
|
|
|
-- name: RemapProjectIssueStatus :many
|
|
-- Moves every issue of the given projects from one status to another as part
|
|
-- of an administrative workflow change. Mirrors UpdateIssueStatus: the issue
|
|
-- is re-ranked to the top of its new column, the revision advances, and a
|
|
-- duplicate mark only survives cancelled -> cancelled (never the case here,
|
|
-- because from and to differ). It deliberately touches nothing else, so it
|
|
-- never hands an issue off or starts a run.
|
|
UPDATE issue AS i SET
|
|
status = sqlc.arg('to_status')::text,
|
|
duplicate_of_issue_id = NULL,
|
|
position = (
|
|
SELECT COALESCE(MIN(target.position), 0) - 1
|
|
FROM issue AS target
|
|
WHERE target.workspace_id = i.workspace_id
|
|
AND target.status = sqlc.arg('to_status')::text
|
|
),
|
|
revision = i.revision + 1,
|
|
last_activity_at = GREATEST(COALESCE(i.last_activity_at, i.updated_at), now()),
|
|
updated_at = now()
|
|
WHERE i.workspace_id = sqlc.arg('workspace_id')::uuid
|
|
AND i.project_id = ANY(sqlc.arg('project_ids')::uuid[])
|
|
AND i.status = sqlc.arg('from_status')::text
|
|
AND i.status <> sqlc.arg('to_status')::text
|
|
RETURNING i.*;
|
|
|
|
-- name: LockProjectForWorkflowChange :one
|
|
-- Serializes concurrent workflow switches of one project.
|
|
SELECT * FROM project
|
|
WHERE id = sqlc.arg('id')::uuid
|
|
AND workspace_id = sqlc.arg('workspace_id')::uuid
|
|
FOR UPDATE;
|
|
|
|
-- name: SetProjectWorkflow :one
|
|
UPDATE project SET
|
|
workflow_id = sqlc.narg('workflow_id')::uuid,
|
|
updated_at = now()
|
|
WHERE id = sqlc.arg('id')::uuid
|
|
AND workspace_id = sqlc.arg('workspace_id')::uuid
|
|
RETURNING *;
|