Skip to content

Commit edda61e

Browse files
Feature/editor 2 (#28)
* feat: Add editor option to annotations and update editable annotations logic * feat: Implement inline editing feature for YAML documents and add file writer functionality * feat: Remove inline edit flag from YAML output and form partials
1 parent db2220d commit edda61e

File tree

14 files changed

+410
-39
lines changed

14 files changed

+410
-39
lines changed

lib/archsight/annotations/annotation.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# Annotation represents a single annotation definition with its schema and behavior
66
class Archsight::Annotations::Annotation
7-
attr_reader :key, :description, :filter, :format, :enum, :sidebar, :type, :list
7+
attr_reader :key, :description, :filter, :format, :enum, :sidebar, :type, :list, :editor
88

99
def initialize(key, options = {})
1010
@key = key
@@ -14,6 +14,7 @@ def initialize(key, options = {})
1414
@enum = options[:enum]
1515
@sidebar = options.fetch(:sidebar, true)
1616
@list = options.fetch(:list, false)
17+
@editor = options.fetch(:editor, true)
1718
@type = options[:type]
1819

1920
# Auto-add filter if enum present

lib/archsight/annotations/generated_annotations.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ def self.included(base)
77
annotation "generated/script",
88
description: "Name of the script that generated this resource",
99
title: "Generated By",
10-
sidebar: false
10+
sidebar: false,
11+
editor: false
1112
annotation "generated/at",
1213
description: "Timestamp when this resource was generated (ISO8601)",
1314
title: "Generated At",
14-
sidebar: false
15+
sidebar: false,
16+
editor: false
1517
annotation "generated/configHash",
1618
description: "Hash of configuration used to generate this resource (for change detection)",
1719
title: "Config Hash",
18-
sidebar: false
20+
sidebar: false,
21+
editor: false
1922
end
2023
end
2124
end

lib/archsight/annotations/git_annotations.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ def self.included(base)
66
base.class_eval do
77
annotation "git/updatedAt",
88
description: "Date when the resource was last updated",
9-
title: "Updated At"
9+
title: "Updated At",
10+
editor: false
1011
annotation "git/updatedBy",
1112
description: "Email of person who last updated the resource",
12-
title: "Updated By"
13+
title: "Updated By",
14+
editor: false
1315
annotation "git/reviewedAt",
1416
description: "Date when the resource was last reviewed",
15-
title: "Reviewed At"
17+
title: "Reviewed At",
18+
editor: false
1619
annotation "git/reviewedBy",
1720
description: "Email of person who last reviewed the resource",
18-
title: "Reviewed By"
21+
title: "Reviewed By",
22+
editor: false
1923
end
2024
end
2125
end

lib/archsight/cli.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ def self.exit_on_failure?
1919
option :production, type: :boolean, default: false, desc: "Run in production mode"
2020
option :disable_reload, type: :boolean, default: false, desc: "Disable the reload button in the UI"
2121
option :enable_logging, type: :boolean, default: nil, desc: "Enable request logging (default: false in dev, true in prod)"
22+
option :inline_edit, type: :boolean, default: false, desc: "Enable inline editing (save directly to source files)"
2223
def web
2324
configure_resources
2425
require "archsight/web/application"
2526

2627
env = options[:production] ? :production : :development
2728
Archsight::Web::Application.configure_environment!(env, logging: options[:enable_logging])
2829
Archsight::Web::Application.set :reload_enabled, !options[:disable_reload]
30+
Archsight::Web::Application.set :inline_edit_enabled, options[:inline_edit]
2931
Archsight::Web::Application.setup_mcp!
3032
Archsight::Web::Application.run!(port: options[:port], bind: options[:host])
3133
rescue Archsight::ResourceError => e

lib/archsight/editor.rb

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require "yaml"
44
require_relative "resources"
5+
require_relative "editor/file_writer"
56

67
module Archsight
78
# Editor handles building and validating resources for the web editor
@@ -92,19 +93,8 @@ def self.apply_block_scalar_style(node)
9293
end
9394
end
9495

95-
# Annotation keys that should not be editable (system-managed fields)
96-
EXCLUDED_ANNOTATION_PREFIXES = %w[
97-
git/updatedAt
98-
git/updatedBy
99-
git/reviewedAt
100-
git/reviewedBy
101-
generated/script
102-
generated/at
103-
generated/configHash
104-
].freeze
105-
10696
# Get editable annotations for a resource kind
107-
# Excludes pattern annotations, computed annotations, and system-managed fields
97+
# Excludes pattern annotations, computed annotations, and annotations with editor: false
10898
# @param kind [String] Resource kind
10999
# @return [Array<Archsight::Annotations::Annotation>]
110100
def self.editable_annotations(kind)
@@ -118,8 +108,8 @@ def self.editable_annotations(kind)
118108
computed_keys = klass.computed_annotations.map(&:key)
119109
annotations = annotations.reject { |a| computed_keys.include?(a.key) }
120110

121-
# Filter out system-managed annotations
122-
annotations.reject { |a| EXCLUDED_ANNOTATION_PREFIXES.include?(a.key) }
111+
# Filter out non-editable annotations
112+
annotations.reject { |a| a.editor == false }
123113
end
124114

125115
# Get available relations for a resource kind
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
3+
module Archsight
4+
class Editor
5+
# FileWriter handles writing YAML documents back to multi-document files
6+
module FileWriter
7+
class WriteError < StandardError; end
8+
9+
# Replace a YAML document in a file starting at a given line
10+
# @param path [String] File path
11+
# @param start_line [Integer] Line number where document starts (1-indexed)
12+
# @param new_yaml [String] New YAML content (without leading ---)
13+
# @raise [WriteError] if file cannot be written or document not found at expected line
14+
def self.replace_document(path:, start_line:, new_yaml:)
15+
raise WriteError, "File not found: #{path}" unless File.exist?(path)
16+
raise WriteError, "File not writable: #{path}" unless File.writable?(path)
17+
18+
lines = File.readlines(path)
19+
start_idx = start_line - 1 # Convert to 0-indexed
20+
21+
raise WriteError, "Line #{start_line} is beyond end of file" if start_idx >= lines.length
22+
23+
# Find the end of this document (next --- or EOF)
24+
end_idx = find_document_end(lines, start_idx)
25+
26+
# Build the new content
27+
# Ensure new_yaml ends with a newline
28+
new_yaml = "#{new_yaml}\n" unless new_yaml.end_with?("\n")
29+
30+
# Replace the document
31+
new_lines = lines[0...start_idx] + [new_yaml] + lines[end_idx..]
32+
33+
# Write atomically by writing to temp file then renaming
34+
File.write(path, new_lines.join)
35+
end
36+
37+
# Find the end index of a document (the line index of the next --- or EOF)
38+
# @param lines [Array<String>] File lines
39+
# @param start_idx [Integer] Starting line index (0-indexed)
40+
# @return [Integer] End index (exclusive - the line after the document ends)
41+
def self.find_document_end(lines, start_idx)
42+
# Start searching from the line after start_idx
43+
idx = start_idx + 1
44+
45+
while idx < lines.length
46+
# Check if this line is a document separator
47+
return idx if lines[idx].strip == "---"
48+
49+
idx += 1
50+
end
51+
52+
# No separator found, document goes to EOF
53+
lines.length
54+
end
55+
end
56+
end
57+
end

lib/archsight/resources/base.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ def self.relations
2525

2626
# Define an annotation using the Annotation class
2727
def self.annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil, sidebar: true,
28-
type: nil, list: false)
28+
type: nil, list: false, editor: true)
2929
@annotations ||= []
3030
options = { description: description, filter: filter, title: title, format: format, enum: enum,
31-
sidebar: sidebar, type: type, list: list }
31+
sidebar: sidebar, type: type, list: list, editor: editor }
3232
@annotations << Archsight::Annotations::Annotation.new(key, options)
3333
end
3434

@@ -51,15 +51,15 @@ def self.annotations
5151
# @param list [Boolean] Whether values are lists (default false)
5252
# @yield Block that computes the annotation value, evaluated in Evaluator context
5353
def self.computed_annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil,
54-
sidebar: false, type: nil, list: false, &)
54+
sidebar: false, type: nil, list: false, editor: true, &)
5555
require_relative "../annotations/computed"
5656
@computed_annotations ||= []
5757
@computed_annotations << Archsight::Annotations::Computed.new(key, description: description, type: type, &)
5858

5959
# Also register as a regular annotation so it passes validation and is recognized
6060
@annotations ||= []
6161
options = { description: description, filter: filter, title: title, format: format, enum: enum,
62-
sidebar: sidebar, type: type, list: list }
62+
sidebar: sidebar, type: type, list: list, editor: editor }
6363
@annotations << Archsight::Annotations::Annotation.new(key, options)
6464
end
6565

lib/archsight/web/application.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def configure_environment!(env, logging: nil)
6060
set :haml, format: :html5
6161
set :server, :puma
6262
set :reload_enabled, true
63+
set :inline_edit_enabled, false
6364
end
6465

6566
# MCP Server setup
@@ -97,6 +98,10 @@ def reload_enabled?
9798
settings.reload_enabled
9899
end
99100

101+
def inline_edit_enabled?
102+
settings.inline_edit_enabled
103+
end
104+
100105
def production?
101106
settings.environment == :production
102107
end

lib/archsight/web/editor/routes.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ def parse_form_relations(params)
144144
@klass = Archsight::Resources[@kind]
145145
halt 404, "Kind not found" unless @klass
146146

147+
# Get original instance for path_ref (for inline save)
148+
original_instance = db.instance_by_kind(@kind, @instance_name)
149+
@path_ref = original_instance&.path_ref
150+
147151
@editor_mode = true
148152
@mode = :edit
149153
@name = (params["name_field"] || @instance_name).strip
@@ -173,6 +177,39 @@ def parse_form_relations(params)
173177
haml :index
174178
end
175179

180+
# Save YAML to source file (inline edit)
181+
post "/api/v1/editor/kinds/:kind/instances/:name/save" do
182+
content_type :json
183+
184+
# Check if inline edit is enabled
185+
halt 403, JSON.generate({ success: false, error: "Inline edit is disabled. Start server with --inline-edit flag." }) unless settings.inline_edit_enabled
186+
187+
kind = params["kind"]
188+
name = params["name"]
189+
190+
begin
191+
yaml_content = JSON.parse(request.body.read)["yaml"]
192+
rescue JSON::ParserError
193+
halt 400, JSON.generate({ success: false, error: "Invalid JSON" })
194+
end
195+
196+
instance = db.instance_by_kind(kind, name)
197+
halt 404, JSON.generate({ success: false, error: "Instance not found" }) unless instance
198+
199+
begin
200+
Archsight::Editor::FileWriter.replace_document(
201+
path: instance.path_ref.path,
202+
start_line: instance.path_ref.line_no,
203+
new_yaml: yaml_content
204+
)
205+
db.reload!
206+
JSON.generate({ success: true, message: "Saved to #{instance.path_ref}" })
207+
rescue Archsight::Editor::FileWriter::WriteError => e
208+
status 400
209+
JSON.generate({ success: false, error: e.message })
210+
end
211+
end
212+
176213
# HTMX API - Get instance names for a kind (for relation dropdown)
177214
get "/api/v1/editor/kinds/:kind/instances" do
178215
kind = params["kind"]

lib/archsight/web/public/css/editor.css

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,30 +299,45 @@
299299
display: flex;
300300
justify-content: space-between;
301301
align-items: center;
302-
margin: 0 calc(var(--block-spacing-horizontal) * -1);
303-
padding: 1rem var(--block-spacing-horizontal);
304302
gap: 1rem;
305303
}
306304

305+
.yaml-file-info,
307306
.yaml-instructions {
308307
display: flex;
309-
align-items: flex-start;
310308
gap: 0.5rem;
311309
margin: 0;
312310
font-size: 0.9em;
313311
color: var(--muted-color);
314312
flex: 1;
313+
min-width: 0;
315314
line-height: 1.5;
316315
}
317316

317+
.yaml-file-info {
318+
align-items: center;
319+
}
320+
321+
.yaml-instructions {
322+
align-items: flex-start;
323+
}
324+
325+
.yaml-file-info > span,
326+
.yaml-instructions > span {
327+
overflow: hidden;
328+
text-overflow: ellipsis;
329+
white-space: nowrap;
330+
}
331+
332+
.yaml-file-info > i,
318333
.yaml-instructions > i {
319-
line-height: 1.5;
320334
flex-shrink: 0;
321335
}
322336

323337
.yaml-actions {
324338
display: flex;
325339
gap: 0.5rem;
340+
flex-shrink: 0;
326341
}
327342

328343
.yaml-actions button,

0 commit comments

Comments
 (0)