feat: add PATCH /tags/{id}/rules/{then_id} to activate/deactivate rules
- openapi.yaml: new PATCH endpoint with is_active body, returns TagRule
- backend/service: SetRuleActive calls repo.SetActive then returns updated rule
- backend/handler: PatchRule validates body and delegates to service
- backend/router: register PATCH /:tag_id/rules/:then_tag_id
- frontend: TagRuleEditor uses PATCH instead of delete+recreate
- mock: handle PATCH /tags/{id}/rules/{then_id}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
871250345a
commit
21f3acadf0
@ -93,6 +93,7 @@ func NewRouter(
|
|||||||
|
|
||||||
tags.GET("/:tag_id/rules", tagHandler.ListRules)
|
tags.GET("/:tag_id/rules", tagHandler.ListRules)
|
||||||
tags.POST("/:tag_id/rules", tagHandler.CreateRule)
|
tags.POST("/:tag_id/rules", tagHandler.CreateRule)
|
||||||
|
tags.PATCH("/:tag_id/rules/:then_tag_id", tagHandler.PatchRule)
|
||||||
tags.DELETE("/:tag_id/rules/:then_tag_id", tagHandler.DeleteRule)
|
tags.DELETE("/:tag_id/rules/:then_tag_id", tagHandler.DeleteRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -354,6 +354,39 @@ func (h *TagHandler) CreateRule(c *gin.Context) {
|
|||||||
respondJSON(c, http.StatusCreated, toTagRuleJSON(*rule))
|
respondJSON(c, http.StatusCreated, toTagRuleJSON(*rule))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PATCH /tags/:tag_id/rules/:then_tag_id
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (h *TagHandler) PatchRule(c *gin.Context) {
|
||||||
|
whenTagID, ok := parseTagID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
thenTagID, err := uuid.Parse(c.Param("then_tag_id"))
|
||||||
|
if err != nil {
|
||||||
|
respondError(c, domain.ErrValidation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
IsActive *bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil || body.IsActive == nil {
|
||||||
|
respondError(c, domain.ErrValidation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rule, err := h.tagSvc.SetRuleActive(c.Request.Context(), whenTagID, thenTagID, *body.IsActive)
|
||||||
|
if err != nil {
|
||||||
|
respondError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(c, http.StatusOK, toTagRuleJSON(*rule))
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// DELETE /tags/:tag_id/rules/:then_tag_id
|
// DELETE /tags/:tag_id/rules/:then_tag_id
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -191,6 +191,23 @@ func (s *TagService) CreateRule(ctx context.Context, whenTagID, thenTagID uuid.U
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRuleActive toggles a rule's is_active flag and returns the updated rule.
|
||||||
|
func (s *TagService) SetRuleActive(ctx context.Context, whenTagID, thenTagID uuid.UUID, active bool) (*domain.TagRule, error) {
|
||||||
|
if err := s.rules.SetActive(ctx, whenTagID, thenTagID, active); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rules, err := s.rules.ListByTag(ctx, whenTagID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, r := range rules {
|
||||||
|
if r.ThenTagID == thenTagID {
|
||||||
|
return &r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteRule removes a tag rule.
|
// DeleteRule removes a tag rule.
|
||||||
func (s *TagService) DeleteRule(ctx context.Context, whenTagID, thenTagID uuid.UUID) error {
|
func (s *TagService) DeleteRule(ctx context.Context, whenTagID, thenTagID uuid.UUID) error {
|
||||||
return s.rules.Delete(ctx, whenTagID, thenTagID)
|
return s.rules.Delete(ctx, whenTagID, thenTagID)
|
||||||
|
|||||||
@ -62,13 +62,9 @@
|
|||||||
busy = true;
|
busy = true;
|
||||||
error = '';
|
error = '';
|
||||||
const thenTagId = rule.then_tag_id!;
|
const thenTagId = rule.then_tag_id!;
|
||||||
const newActive = !rule.is_active;
|
|
||||||
try {
|
try {
|
||||||
await api.delete(`/tags/${tagId}/rules/${thenTagId}`);
|
const updated = await api.patch<TagRule>(`/tags/${tagId}/rules/${thenTagId}`, {
|
||||||
const updated = await api.post<TagRule>(`/tags/${tagId}/rules`, {
|
is_active: !rule.is_active,
|
||||||
then_tag_id: thenTagId,
|
|
||||||
is_active: newActive,
|
|
||||||
apply_to_existing: false,
|
|
||||||
});
|
});
|
||||||
onRulesChange(rules.map((r) => r.then_tag_id === thenTagId ? updated : r));
|
onRulesChange(rules.map((r) => r.then_tag_id === thenTagId ? updated : r));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -402,6 +402,19 @@ export function mockApiPlugin(): Plugin {
|
|||||||
return json(res, 201, { tag_id: tid, then_tag_id: thenId, then_tag_name: t?.name ?? null, is_active: isActive });
|
return json(res, 201, { tag_id: tid, then_tag_id: thenId, then_tag_name: t?.name ?? null, is_active: isActive });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PATCH /tags/{id}/rules/{then_id} — activate / deactivate
|
||||||
|
const tagRulesPatchMatch = path.match(/^\/tags\/([^/]+)\/rules\/([^/]+)$/);
|
||||||
|
if (method === 'PATCH' && tagRulesPatchMatch) {
|
||||||
|
const [, tid, thenId] = tagRulesPatchMatch;
|
||||||
|
const body = (await readBody(req)) as Record<string, unknown>;
|
||||||
|
const isActive = body.is_active as boolean;
|
||||||
|
const ruleMap = tagRules.get(tid);
|
||||||
|
if (!ruleMap?.has(thenId)) return json(res, 404, { code: 'not_found', message: 'Rule not found' });
|
||||||
|
ruleMap.set(thenId, isActive);
|
||||||
|
const t = MOCK_TAGS.find((x) => x.id === thenId);
|
||||||
|
return json(res, 200, { tag_id: tid, then_tag_id: thenId, then_tag_name: t?.name ?? null, is_active: isActive });
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE /tags/{id}/rules/{then_id}
|
// DELETE /tags/{id}/rules/{then_id}
|
||||||
const tagRulesDelMatch = path.match(/^\/tags\/([^/]+)\/rules\/([^/]+)$/);
|
const tagRulesDelMatch = path.match(/^\/tags\/([^/]+)\/rules\/([^/]+)$/);
|
||||||
if (method === 'DELETE' && tagRulesDelMatch) {
|
if (method === 'DELETE' && tagRulesDelMatch) {
|
||||||
|
|||||||
31
openapi.yaml
31
openapi.yaml
@ -825,6 +825,37 @@ paths:
|
|||||||
$ref: '#/components/schemas/TagRule'
|
$ref: '#/components/schemas/TagRule'
|
||||||
|
|
||||||
/tags/{tag_id}/rules/{then_tag_id}:
|
/tags/{tag_id}/rules/{then_tag_id}:
|
||||||
|
patch:
|
||||||
|
tags: [Tags]
|
||||||
|
summary: Update a tag rule (activate / deactivate)
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/tag_id'
|
||||||
|
- name: then_tag_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [is_active]
|
||||||
|
properties:
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Rule updated
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TagRule'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags: [Tags]
|
tags: [Tags]
|
||||||
summary: Remove a tag rule
|
summary: Remove a tag rule
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user