feat: editor write path

This commit is contained in:
Florian Sylvain committed 2026-11-02 03:24:30 +01:00
1 parent d9158025d4
commit a0b3c58c52
88 files changed
+5738 -263

No files matched your search

+3
View File
@@ -137,6 +137,9 @@ type Article struct {
Lang string `json:"lang"`
StoryID uint32 `json:"story_id"`
ContentRevision uint32 `json:"-"`
EditRevision uint32 `json:"-"`
SourceArticleID *uint32 `json:"-"`
SourceRevision uint32 `json:"-"`
}
func (a Article) UsesImage(imageID uint32) bool {
@@ -0,0 +1,161 @@
package article
import (
"RenewCMS/internal/domain/author"
"errors"
"slices"
)
var (
ErrRevisionConflict = errors.New("the article changed since it was opened")
ErrLanguageTaken = errors.New("the story already holds a version in that language")
ErrLanguageUnavailable = errors.New("the language cannot be used for this story")
ErrLanguageExists = errors.New("a readable version in that language already exists")
ErrLanguageNotEnabled = errors.New("the language is not enabled on the site")
ErrLanguageDisabled = errors.New("the language is disabled on the site")
ErrSourceUnavailable = errors.New("the source version cannot be used")
ErrSourceOtherStory = errors.New("the source version belongs to another story")
ErrInvalidTranslation = errors.New("the translation request is not valid")
ErrStoryMoveRequired = errors.New("the story must be moved as a whole")
ErrSlugConflict = errors.New("an address already exists on the destination site")
)
const (
CapabilityRead = "read"
CapabilityEdit = "edit"
CapabilityPublish = "publish"
CapabilityDelete = "delete"
CapabilityCreateTranslation = "create_translation"
)
type RevisionConflictError struct {
Current uint32
}
func (e *RevisionConflictError) Error() string {
return ErrRevisionConflict.Error()
}
func (e *RevisionConflictError) Is(target error) bool {
return target == ErrRevisionConflict
}
type LanguageExistsError struct {
ArticleID uint32
}
func (e *LanguageExistsError) Error() string {
return ErrLanguageExists.Error()
}
func (e *LanguageExistsError) Is(target error) bool {
return target == ErrLanguageExists
}
type LanguageNotEnabledError struct {
Missing []string
}
func (e *LanguageNotEnabledError) Error() string {
return ErrLanguageNotEnabled.Error()
}
func (e *LanguageNotEnabledError) Is(target error) bool {
return target == ErrLanguageNotEnabled
}
type SlugConflictError struct {
ArticleIDs []uint32
}
func (e *SlugConflictError) Error() string {
return ErrSlugConflict.Error()
}
func (e *SlugConflictError) Is(target error) bool {
return target == ErrSlugConflict
}
type Translation struct {
ID uint32 `json:"id"`
Lang string `json:"lang"`
Title string `json:"title"`
IsOnline bool `json:"is_online"`
LanguageEnabled bool `json:"language_enabled"`
Capabilities []string `json:"capabilities"`
}
type Source struct {
ArticleID uint32 `json:"article_id"`
Revision uint32 `json:"revision"`
}
type Impact struct {
Edited bool
Content bool
}
func ImpactOf(before Article, after Article, authorsChanged bool) Impact {
content := before.Title != after.Title ||
before.Body != after.Body ||
before.Excerpt != after.Excerpt ||
before.Slug != after.Slug ||
before.CoverImageAlt != after.CoverImageAlt ||
before.SEOTitle != after.SEOTitle ||
before.SEODescription != after.SEODescription ||
before.SocialTitle != after.SocialTitle ||
before.SocialDescription != after.SocialDescription
edited := content ||
authorsChanged ||
before.SiteID != after.SiteID ||
before.CanonicalURL != after.CanonicalURL ||
before.Noindex != after.Noindex ||
!sameImageID(before.CoverImageID, after.CoverImageID) ||
!sameImageID(before.SocialImageID, after.SocialImageID) ||
!slices.Equal(before.SEODismissed, after.SEODismissed)
return Impact{Edited: edited, Content: content}
}
const InitialRevision uint32 = 1
func (a Article) Opened() Article {
a.EditRevision = InitialRevision
a.ContentRevision = InitialRevision
return a
}
func (a Article) Revised(impact Impact) Article {
if impact.Edited {
a.EditRevision++
}
if impact.Content {
a.ContentRevision++
}
return a
}
func sameImageID(left *uint32, right *uint32) bool {
if left == nil || right == nil {
return left == right
}
return *left == *right
}
func (a Article) CopiedInto(draft Article) Article {
draft.Body = a.Body
draft.Excerpt = a.Excerpt
draft.SEOTitle = a.SEOTitle
draft.SEODescription = a.SEODescription
draft.SocialTitle = a.SocialTitle
draft.SocialDescription = a.SocialDescription
draft.CoverImageID = a.CoverImageID
draft.CoverImageAlt = a.CoverImageAlt
draft.SocialImageID = a.SocialImageID
draft.Authors = slices.Clone(a.Authors)
if draft.Authors == nil {
draft.Authors = []author.Byline{}
}
draft.SourceArticleID = &a.ID
draft.SourceRevision = a.ContentRevision
return draft
}
@@ -47,6 +47,8 @@ type Nullable[T any] struct {
}
type Update struct {
Title *string
IfRevision *uint32
Body *string
SiteID *uint32
AuthorIDs *[]uint32
@@ -107,4 +109,6 @@ type Repository interface {
RestoreImage(references ImageReferences, imageID uint32) error
RepointImage(fromImageIDs []uint32, toImageID uint32) ([]uint32, error)
AssignUnassignedLanguage(siteID uint32, lang string) (int, error)
ListByStory(storyID uint32) ([]Article, error)
MoveToSite(ids []uint32, siteID uint32) error
}
+3
View File
@@ -214,6 +214,9 @@ const (
FieldLanguages = "languages"
FieldDefaultLanguage = "default_language"
ParamAssigned = "assigned"
ParamLang = "lang"
ParamStoryID = "story_id"
ParamSourceArticleID = "source_article_id"
maxNameLength = 255
maxActionLength = 64
maxFailureReasonSize = 255
@@ -93,3 +93,44 @@ func Plan(current []Row, counts map[string]VersionCount, change Change) ([]Row,
}
return next, nil
}
func EnabledTag(rows []Row, tag string) bool {
return slices.ContainsFunc(rows, func(row Row) bool { return row.Tag == tag && row.Enabled })
}
func HasRow(rows []Row, tag string) bool {
return slices.ContainsFunc(rows, func(row Row) bool { return row.Tag == tag })
}
func MissingTags(rows []Row, tags []string) []string {
missing := []string{}
for _, tag := range tags {
if tag != language.Unassigned && !EnabledTag(rows, tag) && !slices.Contains(missing, tag) {
missing = append(missing, tag)
}
}
slices.Sort(missing)
return missing
}
func DefaultTag(rows []Row) string {
for _, row := range rows {
if row.IsDefault {
return row.Tag
}
}
return language.Unassigned
}
func CompareTags(defaultTag string, left string, right string) int {
if left == right {
return 0
}
if left == defaultTag {
return -1
}
if right == defaultTag {
return 1
}
return strings.Compare(left, right)
}