Skip to content
Snippets Groups Projects
Commit 3b90c06e authored by Danis Kirasirov's avatar Danis Kirasirov :8ball: Committed by Pavel Antonov
Browse files

feat(core): Реализована установка файловой системы для загрузки assets

parent 78caa414
Branches
Tags
No related merge requests found
...@@ -6,47 +6,42 @@ import ( ...@@ -6,47 +6,42 @@ import (
"path/filepath" "path/filepath"
"git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/yaml"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"gopkg.in/yaml.v3"
) )
// Assets предоставляет методы для загрузки данных из файловой системы // Assets предоставляет методы для загрузки данных из файловой системы
type Assets[T any] struct { type Assets[T any] struct {
FS fs.FS
Constructor func() T Constructor func() T
} }
// NewAssets возвращает новый экземпляр загрузчика // NewAssets возвращает новый экземпляр загрузчика
func NewAssets[T any]() *Assets[T] { func NewAssets[T any](fsys fs.FS) *Assets[T] {
return &Assets[T]{ return &Assets[T]{
Constructor: func() (t T) { return t }, FS: fsys,
Constructor: func() (t T) { return t }, // По умолчанию zero-value
} }
} }
type FromFSFunc[T any] func(fsys fs.FS) ([]T, error)
type FromFileFunc[T any] func(file fs.File) ([]T, error)
func (a *Assets[T]) Funcs() (FromFSFunc[T], FromFileFunc[T]) {
return a.FromFS, a.FromFile
}
// WithConstructor устанавливает конструктор для создания новых экземпляров // WithConstructor устанавливает конструктор для создания новых экземпляров
func (a *Assets[T]) WithConstructor(t func() T) *Assets[T] { func (a *Assets[T]) WithConstructor(t func() T) *Assets[T] {
a.Constructor = t a.Constructor = t
return a return a
} }
// MustFrom возвращает все записи в переданной файловой системе // MustFrom возвращает все записи из переданного файла или директории
func (a *Assets[T]) MustFrom(fsys fs.FS, path string) []T { func (a *Assets[T]) MustFrom(path string) []T {
res, err := a.From(fsys, path) res, err := a.From(path)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return res return res
} }
// MustOneFrom возвращает одну запись из переданного файла // MustOneFrom возвращает одну запись из переданного файла или директории
func (a *Assets[T]) MustOneFrom(fsys fs.FS, path string) T { func (a *Assets[T]) MustOneFrom(path string) T {
res, err := a.From(fsys, path) res, err := a.From(path)
if err != nil { if err != nil {
panic(err) panic(err)
} }
...@@ -59,9 +54,9 @@ func (a *Assets[T]) MustOneFrom(fsys fs.FS, path string) T { ...@@ -59,9 +54,9 @@ func (a *Assets[T]) MustOneFrom(fsys fs.FS, path string) T {
return res[0] return res[0]
} }
// From возвращает записи из переданного файла // From возвращает записи из переданного файла или директории
func (a *Assets[T]) From(fsys fs.FS, path string) ([]T, error) { func (a *Assets[T]) From(path string) ([]T, error) {
f, err := fsys.Open(path) f, err := a.FS.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -74,13 +69,8 @@ func (a *Assets[T]) From(fsys fs.FS, path string) ([]T, error) { ...@@ -74,13 +69,8 @@ func (a *Assets[T]) From(fsys fs.FS, path string) ([]T, error) {
} }
if stat.IsDir() { if stat.IsDir() {
sub, err := fs.Sub(fsys, path) return a.FromDir(path)
if err != nil {
return nil, err
}
return a.FromFS(sub)
} }
return a.FromFile(f) return a.FromFile(f)
} }
...@@ -107,10 +97,10 @@ func (a *Assets[T]) FromFile(file fs.File) ([]T, error) { ...@@ -107,10 +97,10 @@ func (a *Assets[T]) FromFile(file fs.File) ([]T, error) {
return nil, errors.Errorf("file '%s' must be in JSON or YAML format", stat.Name()) return nil, errors.Errorf("file '%s' must be in JSON or YAML format", stat.Name())
} }
// FromFS возвращает все записи в переданной файловой системе // FromDir возвращает все записи из переданной папки рекурсивно
func (a *Assets[T]) FromFS(fsys fs.FS) (result []T, err error) { func (a *Assets[T]) FromDir(dir string) (result []T, err error) {
if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, _ error) error { if err := fs.WalkDir(a.FS, dir, func(path string, d fs.DirEntry, _ error) error {
file, err := fsys.Open(path) file, err := a.FS.Open(path)
if err != nil { if err != nil {
return err return err
} }
...@@ -128,8 +118,8 @@ func (a *Assets[T]) FromFS(fsys fs.FS) (result []T, err error) { ...@@ -128,8 +118,8 @@ func (a *Assets[T]) FromFS(fsys fs.FS) (result []T, err error) {
} }
// FromJSON возвращает запись из JSON // FromJSON возвращает запись из JSON
func (c *Assets[T]) FromJSON(r io.Reader) (T, error) { func (a *Assets[T]) FromJSON(r io.Reader) (T, error) {
entry := c.Constructor() entry := a.Constructor()
data, err := io.ReadAll(r) data, err := io.ReadAll(r)
if err != nil { if err != nil {
return entry, err return entry, err
...@@ -140,11 +130,11 @@ func (c *Assets[T]) FromJSON(r io.Reader) (T, error) { ...@@ -140,11 +130,11 @@ func (c *Assets[T]) FromJSON(r io.Reader) (T, error) {
} }
// FromYAML возвращает записи из YAML // FromYAML возвращает записи из YAML
func (c *Assets[T]) FromYAML(r io.Reader) (result []T, err error) { func (a *Assets[T]) FromYAML(r io.Reader) (result []T, err error) {
decoder := yaml.NewDecoder(r) decoder := yaml.NewDecoder(r)
for { for {
var data interface{} var data interface{}
err = decoder.Decode(&data) err = decoder.Decode(yaml.WithTagProcessor(a.FS)(&data))
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
break break
} }
...@@ -157,7 +147,7 @@ func (c *Assets[T]) FromYAML(r io.Reader) (result []T, err error) { ...@@ -157,7 +147,7 @@ func (c *Assets[T]) FromYAML(r io.Reader) (result []T, err error) {
return nil, err return nil, err
} }
entry := c.Constructor() entry := a.Constructor()
if err = jsoniter.Unmarshal(json, &entry); err != nil { if err = jsoniter.Unmarshal(json, &entry); err != nil {
return nil, err return nil, err
} }
......
...@@ -26,7 +26,7 @@ type nested struct { ...@@ -26,7 +26,7 @@ type nested struct {
Option *bool Option *bool
} }
func TestFromFS(t *testing.T) { func TestFromDir(t *testing.T) {
tr := true tr := true
i1 := &testEntry{ i1 := &testEntry{
ID: "item1", ID: "item1",
...@@ -46,8 +46,8 @@ func TestFromFS(t *testing.T) { ...@@ -46,8 +46,8 @@ func TestFromFS(t *testing.T) {
i3 := *i1 i3 := *i1
i3.ID = "item3" i3.ID = "item3"
assets := NewAssets[*testEntry]() assets := NewAssets[*testEntry](os.DirFS("."))
r, err := assets.FromFS(os.DirFS("assets/tests/assets")) r, err := assets.FromDir("assets/tests/assets")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, r, 3) require.Len(t, r, 3)
assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r) assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r)
...@@ -73,13 +73,14 @@ func TestFrom(t *testing.T) { ...@@ -73,13 +73,14 @@ func TestFrom(t *testing.T) {
i3 := *i1 i3 := *i1
i3.ID = "item3" i3.ID = "item3"
assets := NewAssets[*testEntry]() assets := NewAssets[*testEntry](os.DirFS("."))
r, err := assets.From(os.DirFS("assets"), "tests/assets") r, err := assets.From("assets/tests/assets")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, r, 3) require.Len(t, r, 3)
assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r) assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r)
r, err = assets.From(os.DirFS("assets"), "tests/assets/items.yaml") assets = NewAssets[*testEntry](os.DirFS("assets/tests"))
r, err = assets.From("assets/items.yaml")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, r, 2) require.Len(t, r, 2)
assert.Equal(t, []*testEntry{i1, &i2}, r) assert.Equal(t, []*testEntry{i1, &i2}, r)
......
...@@ -2,6 +2,7 @@ package extension ...@@ -2,6 +2,7 @@ package extension
import ( import (
"context" "context"
"io/fs"
"go.uber.org/zap" "go.uber.org/zap"
...@@ -40,9 +41,6 @@ var ( ...@@ -40,9 +41,6 @@ var (
ErrUninstall = errors.New("uninstall failed") ErrUninstall = errors.New("uninstall failed")
ErrNotInstalled = errors.New("not installed") ErrNotInstalled = errors.New("not installed")
ErrUnknownExtension = errors.New("unknown extension") ErrUnknownExtension = errors.New("unknown extension")
ManifestAssets = perxis.NewAssets[*ExtensionDescriptor]()
ManifestFromFile = ManifestAssets.MustOneFrom
) )
// Runnable описывает интерфейс сервиса с запуском и остановкой. Вызывается сервером расширений // Runnable описывает интерфейс сервиса с запуском и остановкой. Вызывается сервером расширений
...@@ -101,3 +99,8 @@ func UpdateExtensionCollections() setup.CollectionOption { ...@@ -101,3 +99,8 @@ func UpdateExtensionCollections() setup.CollectionOption {
} }
} }
} }
// LoadManifest загружает манифест из файла manifest.yaml
func LoadManifest(fsys fs.FS) *ExtensionDescriptor {
return perxis.NewAssets[*ExtensionDescriptor](fsys).MustOneFrom("manifest.yml")
}
...@@ -4,7 +4,6 @@ import ( ...@@ -4,7 +4,6 @@ import (
"context" "context"
"reflect" "reflect"
"git.perx.ru/perxis/perxis-go"
"git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/expr" "git.perx.ru/perxis/perxis-go/pkg/expr"
"git.perx.ru/perxis/perxis-go/pkg/schema/field" "git.perx.ru/perxis/perxis-go/pkg/schema/field"
...@@ -34,10 +33,6 @@ var ( ...@@ -34,10 +33,6 @@ var (
Modify = modify.Modify Modify = modify.Modify
Validate = validate.Validate Validate = validate.Validate
Evaluate = field.Evaluate Evaluate = field.Evaluate
Assets = perxis.NewAssets[*Schema]().WithConstructor(func() *Schema { return New() })
FromFS = Assets.FromFS
FromFile = Assets.FromFile
) )
func (s *Schema) Clone(reset bool) *Schema { func (s *Schema) Clone(reset bool) *Schema {
......
{
"ui": {
"widget": "Tabs",
"options": {
"description": "path",
"collection_icon": "ApartmentOutlined/FileTextOutlined",
"fields": [
"content",
"seo",
"settings",
"advanced",
"design",
"variables"
],
"title": "name",
"key": "path"
},
"list_view": {
"options": {
"sort": [
"path"
],
"page_size": 50,
"fields": [
"path",
"name",
"updated_at",
"updated_by",
"state"
]
}
}
},
"includes": [
{
"ref": "hoop_item_options",
"optional": true
},
{
"ref": "ext_web_pages_*",
"optional": true
}
],
"type": "object",
"params": {
"inline": false,
"fields": {
"design": {
"title": "Дизайн",
"includes": [
{
"ref": "web_design"
}
],
"type": "object",
"params": {
"inline": false,
"fields": {}
}
},
"variables": {
"title": "Переменные",
"ui": {
"options": {
"fields": [
"variables"
]
}
},
"type": "object",
"params": {
"inline": true,
"fields": {
"variables": {
"type": "array",
"params": {
"item": {
"ui": {
"options": {
"collection_icon": "SettingOutlined/FileExcelOutlined",
"fields": [
"id",
"name",
"value"
]
},
"list_view": {
"options": {
"fields": [
"id",
"name",
"updated_at",
"updated_by",
"state"
],
"page_size": 50
}
}
},
"type": "object",
"params": {
"inline": false,
"fields": {
"id": {
"title": "Идентификатор переменной",
"text_search": true,
"options": {
"required": true
},
"type": "string",
"params": {}
},
"name": {
"title": "Название переменной",
"text_search": true,
"type": "string",
"params": {}
},
"value": {
"title": "Значение переменной",
"type": "string",
"params": {}
}
}
}
}
}
}
}
}
},
"content": {
"title": "Содержимое страницы",
"ui": {
"options": {
"fields": [
"blocks"
]
}
},
"type": "object",
"params": {
"inline": true,
"fields": {
"blocks": {
"title": "Блоки",
"ui": {
"widget": "BlockList",
"options": {
"create": {
"classes": [
"class_web_blocks"
]
}
}
},
"type": "array",
"params": {
"item": {
"title": "Блок",
"type": "reference",
"params": {
"allowedCollections": [
"web_block_*"
]
}
}
}
}
}
}
},
"seo": {
"title": "SEO",
"includes": [
{
"ref": "web_seo"
}
],
"type": "object",
"params": {
"inline": false,
"fields": {}
}
},
"settings": {
"title": "Настройки",
"ui": {
"options": {
"fields": [
"name",
"parent",
"path",
"slug",
"section",
"datasource",
"redirect",
"redirect_path",
"redirect_url",
"outputs",
"navTitle",
"navHide",
"weight"
]
}
},
"type": "object",
"params": {
"inline": true,
"fields": {
"path": {
"title": "Путь",
"unique": true,
"text_search": true,
"options": {
"value": "$perxis.Item.Template == true ? _value : parent == nil ? '/' : slug == nil ? make_path(parent, slugify(replace_markers(name))) : make_path(parent, slugify(replace_markers(slug)))"
},
"type": "string",
"params": {}
},
"redirect_url": {
"title": "URL для перехода",
"condition": "redirect == true",
"type": "string",
"params": {}
},
"outputs": {
"title": "Форматы страницы для вывода",
"type": "array",
"params": {
"item": {
"ui": {
"widget": "Lookup",
"options": {
"allowedCollections": [
{
"collection": "web_outputs"
}
]
}
},
"type": "string",
"params": {}
}
}
},
"weight": {
"title": "Порядок следования",
"ui": {
"widget": "NumberInput"
},
"type": "number",
"params": {
"format": "int"
}
},
"navHide": {
"title": "Скрыть страницу из навигации",
"ui": {
"widget": "Checkbox"
},
"type": "bool",
"params": {}
},
"section": {
"title": "Раздел",
"ui": {
"widget": "Checkbox"
},
"type": "bool",
"params": {}
},
"parent": {
"title": "Родительский раздел",
"description": "Раздел сайта, где расположена страница",
"ui": {
"widget": "Lookup",
"options": {
"allowedCollections": [
{
"collection": "web_pages"
}
]
}
},
"type": "string",
"params": {}
},
"name": {
"title": "Название",
"indexed": true,
"text_search": true,
"options": {
"required": true,
"value": "$perxis.Item.Template == true ? _value : replace_markers(_value)"
},
"type": "string",
"params": {}
},
"navTitle": {
"title": "Название для навигации",
"type": "string",
"params": {}
},
"redirect_path": {
"title": "Страница для перехода",
"ui": {
"widget": "Lookup",
"options": {
"allowedCollections": [
{
"collection": "web_pages"
}
]
}
},
"condition": "redirect == true",
"type": "string",
"params": {}
},
"datasource": {
"title": "Источник данных",
"description": "Источник данных из которого будут формироваться подстраницы раздела",
"ui": {
"widget": "Lookup",
"options": {
"allowedCollections": [
{
"collection": "web_datasources"
}
]
}
},
"condition": "section == true",
"type": "string",
"params": {}
},
"redirect": {
"title": "Перенаправление",
"description": "Страница не имеет содержимого и перенаправляет посетителей на другой адрес",
"ui": {
"widget": "Checkbox"
},
"type": "bool",
"params": {}
},
"slug": {
"title": "Slug",
"description": "Идентификатор страницы в адресе URL",
"text_search": true,
"options": {
"value": "$perxis.Item.Template == true ? _value : parent == nil ? nil : _value == nil ? slugify(replace_markers(name)) : slugify(replace_markers(_value))"
},
"type": "string",
"params": {}
}
}
}
},
"advanced": {
"title": "Расширенные настройки",
"ui": {
"options": {
"fields": [
"scripts"
]
}
},
"type": "object",
"params": {
"inline": false,
"fields": {
"scripts": {
"title": "Дополнительные скрипты",
"type": "array",
"params": {
"item": {
"ui": {
"options": {
"fields": [
"src",
"type",
"content"
]
}
},
"type": "object",
"params": {
"inline": false,
"fields": {
"type": {
"title": "Media Type скрипта",
"type": "string",
"params": {}
},
"content": {
"title": "Содержимое скрипта",
"type": "string",
"params": {}
},
"src": {
"title": "URL для загрузки скрипта",
"type": "string",
"params": {}
}
}
}
}
}
}
}
}
}
}
},
"loaded": false,
"metadata": {
"extension": "perxisweb"
}
}
\ No newline at end of file
---
ui:
widget: Tabs
options:
description: path
collection_icon: ApartmentOutlined/FileTextOutlined
fields:
- content
- seo
- settings
- advanced
- design
- variables
title: name
key: path
list_view:
options:
sort:
- path
page_size: 50
fields:
- path
- name
- updated_at
- updated_by
- state
includes:
- ref: hoop_item_options
optional: true
- ref: ext_web_pages_*
optional: true
type: object
params:
inline: false
fields:
design:
title: Дизайн
includes:
- ref: web_design
type: object
params:
inline: false
fields: {}
variables:
title: Переменные
ui:
options:
fields:
- variables
type: object
params:
inline: true
fields:
variables:
type: array
params:
item:
ui:
options:
collection_icon: SettingOutlined/FileExcelOutlined
fields:
- id
- name
- value
list_view:
options:
fields:
- id
- name
- updated_at
- updated_by
- state
page_size: 50
type: object
params:
inline: false
fields:
id:
title: Идентификатор переменной
text_search: true
options:
required: true
type: string
params: {}
name:
title: Название переменной
text_search: true
type: string
params: {}
value:
title: Значение переменной
type: string
params: {}
content:
title: Содержимое страницы
ui:
options:
fields:
- blocks
type: object
params:
inline: true
fields:
blocks:
title: Блоки
ui:
widget: BlockList
options:
create:
classes:
- class_web_blocks
type: array
params:
item:
title: Блок
type: reference
params:
allowedCollections:
- web_block_*
seo:
title: SEO
includes:
- ref: web_seo
type: object
params:
inline: false
fields: {}
settings:
title: Настройки
ui:
options:
fields:
- name
- parent
- path
- slug
- section
- datasource
- redirect
- redirect_path
- redirect_url
- outputs
- navTitle
- navHide
- weight
type: object
params:
inline: true
fields:
path:
title: Путь
unique: true
text_search: true
options:
value: "$perxis.Item.Template == true ? _value : parent == nil ? '/'
: slug == nil ? make_path(parent, slugify(replace_markers(name)))
: make_path(parent, slugify(replace_markers(slug)))"
type: string
params: {}
redirect_url:
title: URL для перехода
condition: redirect == true
type: string
params: {}
outputs:
title: Форматы страницы для вывода
type: array
params:
item:
ui:
widget: Lookup
options:
allowedCollections:
- collection: web_outputs
type: string
params: {}
weight:
title: Порядок следования
ui:
widget: NumberInput
type: number
params:
format: int
navHide:
title: Скрыть страницу из навигации
ui:
widget: Checkbox
type: bool
params: {}
section:
title: Раздел
ui:
widget: Checkbox
type: bool
params: {}
parent:
title: Родительский раздел
description: Раздел сайта, где расположена страница
ui:
widget: Lookup
options:
allowedCollections:
- collection: web_pages
type: string
params: {}
name:
title: Название
indexed: true
text_search: true
options:
required: true
value: "$perxis.Item.Template == true ? _value : replace_markers(_value)"
type: string
params: {}
navTitle:
title: Название для навигации
type: string
params: {}
redirect_path:
title: Страница для перехода
ui:
widget: Lookup
options:
allowedCollections:
- collection: web_pages
condition: redirect == true
type: string
params: {}
datasource:
title: Источник данных
description: Источник данных из которого будут формироваться подстраницы
раздела
ui:
widget: Lookup
options:
allowedCollections:
- collection: web_datasources
condition: section == true
type: string
params: {}
redirect:
title: Перенаправление
description: Страница не имеет содержимого и перенаправляет посетителей
на другой адрес
ui:
widget: Checkbox
type: bool
params: {}
slug:
title: Slug
description: Идентификатор страницы в адресе URL
text_search: true
options:
value: "$perxis.Item.Template == true ? _value : parent == nil ? nil
: _value == nil ? slugify(replace_markers(name)) : slugify(replace_markers(_value))"
type: string
params: {}
advanced:
title: Расширенные настройки
ui:
options:
fields:
- scripts
type: object
params:
inline: false
fields:
scripts:
title: Дополнительные скрипты
type: array
params:
item:
ui:
options:
fields:
- src
- type
- content
type: object
params:
inline: false
fields:
type:
title: Media Type скрипта
type: string
params: {}
content:
title: Содержимое скрипта
type: string
params: {}
src:
title: URL для загрузки скрипта
type: string
params: {}
loaded: false
metadata:
extension: perxisweb
package test
import (
"os"
"testing"
"git.perx.ru/perxis/perxis-go/pkg/extension"
"git.perx.ru/perxis/perxis-go/pkg/references"
"git.perx.ru/perxis/perxis-go/pkg/schema"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
"git.perx.ru/perxis/perxis-go/pkg/schema/modify"
"git.perx.ru/perxis/perxis-go/pkg/schema/validate"
"github.com/stretchr/testify/require"
)
func TestFromFiles(t *testing.T) {
t.Run("Non-existen path", func(t *testing.T) {
schemas, err := schema.FromFS(os.DirFS("non-existen"))
require.Error(t, err)
require.ErrorContains(t, err, "no such file or directory")
require.Nil(t, schemas)
})
t.Run("Success", func(t *testing.T) {
schemas, err := schema.FromFS(os.DirFS("assets"))
for _, s := range schemas {
s.ClearState()
}
require.NoError(t, err)
require.Len(t, schemas, 2, "В директории хранятся две корректные схемы")
require.ElementsMatch(t, []*schema.Schema{getPagesSchema(), getPagesSchema()}, schemas, "Cхемы должны соответствовать объекту из функции")
})
}
// Оригинальное объявление схемы Web/Страницы
// Значение констант подставлено вручную
func getPagesSchema() *schema.Schema {
content := field.Object(
true, "blocks", field.Array(
references.Field([]string{"web_block_*"}).SetTitle("Блок"),
).
SetTitle("Блоки").
WithUI(&field.UI{
Widget: "BlockList",
Options: map[string]interface{}{
"create": map[string]interface{}{
"classes": []string{"class_web_blocks"},
},
},
}),
).SetTitle("Содержимое страницы")
// SEO
seo := field.Object().WithIncludes("web_seo").SetTitle("SEO")
//Settings
settings := field.Object(true,
"name", field.String(
validate.Required(),
modify.Value("$perxis.Item.Template == true ? _value : replace_markers(_value)"),
).SetTitle("Название").SetTextSearch(true).SetIndexed(true),
"parent", field.String().SetTitle("Родительский раздел").SetDescription("Раздел сайта, где расположена страница").WithUI(&field.UI{
Widget: "Lookup",
Options: map[string]interface{}{
"allowedCollections": []interface{}{
map[string]interface{}{"collection": "web_pages"},
},
},
}), // TODO lookup "section == true"
"path", field.String(
modify.Value("$perxis.Item.Template == true ? _value : parent == nil ? '/' : slug == nil ? make_path(parent, slugify(replace_markers(name))) : make_path(parent, slugify(replace_markers(slug)))"),
).SetTitle("Путь").SetUnique(true).SetTextSearch(true), // TODO readonly
"slug", field.String(modify.Value("$perxis.Item.Template == true ? _value : parent == nil ? nil : _value == nil ? slugify(replace_markers(name)) : slugify(replace_markers(_value))")).SetTitle("Slug").SetDescription("Идентификатор страницы в адресе URL").SetTextSearch(true),
"section", field.Bool().SetTitle("Раздел").WithUI(&field.UI{Widget: "Checkbox"}),
"datasource", field.String().WithUI(&field.UI{
Widget: "Lookup",
Options: map[string]interface{}{
"allowedCollections": []interface{}{
map[string]interface{}{"collection": "web_datasources"},
},
},
}).SetTitle("Источник данных").SetDescription("Источник данных из которого будут формироваться подстраницы раздела").SetCondition("section == true"),
"redirect", field.Bool().SetTitle("Перенаправление").SetDescription("Страница не имеет содержимого и перенаправляет посетителей на другой адрес").WithUI(&field.UI{Widget: "Checkbox"}),
"redirect_path", field.String().SetTitle("Страница для перехода").WithUI(&field.UI{
Widget: "Lookup",
Options: map[string]interface{}{
"allowedCollections": []interface{}{
map[string]interface{}{"collection": "web_pages"},
},
},
}).SetCondition("redirect == true"),
"redirect_url", field.String().SetTitle("URL для перехода").SetCondition("redirect == true"),
"outputs", field.Array(field.String().WithUI(&field.UI{
Widget: "Lookup",
Options: map[string]interface{}{
"allowedCollections": []interface{}{
map[string]interface{}{"collection": "web_outputs"},
},
},
})).SetTitle("Форматы страницы для вывода"),
"navTitle", field.String().SetTitle("Название для навигации"),
"navHide", field.Bool().SetTitle("Скрыть страницу из навигации").WithUI(&field.UI{Widget: "Checkbox"}),
"weight", field.Number(field.NumberFormatInt).SetTitle("Порядок следования").WithUI(&field.UI{Widget: "NumberInput"}),
).SetTitle("Настройки")
// Advanced
advanced := field.Object(
"scripts", field.Array(
field.Object(
"src", field.String().SetTitle("URL для загрузки скрипта"),
"type", field.String().SetTitle("Media Type скрипта"),
"content", field.String().SetTitle("Содержимое скрипта"),
),
).SetTitle("Дополнительные скрипты"),
).SetTitle("Расширенные настройки")
// Design
design := field.Object().WithIncludes("web_design").SetTitle("Дизайн")
//Variables
variables := field.Object(true, "variables", field.Array(getVarsField())).SetTitle("Переменные")
// Page
page := schema.New(
"content", content,
"seo", seo,
"settings", settings,
"advanced", advanced,
"design", design,
"variables", variables,
).WithMetadata(extension.MetadataKey, "perxisweb")
// Includes
page.SetIncludes(
field.Include{Ref: "hoop_item_options", Optional: true},
field.Include{Ref: "ext_web_pages_*", Optional: true},
)
//UI
page.Field.UI.ListView = &field.View{Options: map[string]interface{}{
"fields": []string{"path", "name", "updated_at", "updated_by", "state"},
"sort": []string{"path"},
"page_size": 50,
}}
page.Field.UI.Options["title"] = "name"
page.Field.UI.Options["key"] = "path"
page.Field.UI.Options["description"] = "path"
page.Field.UI.Widget = "Tabs"
page.Field.UI.Options["collection_icon"] = "ApartmentOutlined/FileTextOutlined"
_ = page.ConvertTypes()
page.ClearState()
return page
}
func getVarsSchema() (sch *schema.Schema) {
sch = schema.New(
"id", field.String(validate.Required()).SetTextSearch(true).SetTitle("Идентификатор переменной"),
"name", field.String().SetTextSearch(true).SetTitle("Название переменной"),
"value", field.String().SetTitle("Значение переменной"),
).WithMetadata(extension.MetadataKey, "perxisweb")
//UI
sch.Field.UI.ListView = &field.View{Options: map[string]interface{}{
"fields": []string{"id", "name", "updated_at", "updated_by", "state"},
"page_size": 50,
}}
sch.Field.UI.Options["collection_icon"] = "SettingOutlined/FileExcelOutlined"
return sch
}
func getVarsField() *field.Field {
return &getVarsSchema().Field
}
...@@ -38,29 +38,21 @@ func (cfg *Config) Load(fsys fs.FS) (*Config, error) { ...@@ -38,29 +38,21 @@ func (cfg *Config) Load(fsys fs.FS) (*Config, error) {
return nil, errors.Wrapf(err, "Can't load config. (fs=%v)", fsys) return nil, errors.Wrapf(err, "Can't load config. (fs=%v)", fsys)
} }
if subFS, err := fs.Sub(fsys, "collections"); err == nil { if err := cfg.Collections.Load(fsys, "collections"); err != nil && !errors.Is(err, fs.ErrNotExist) {
if err = cfg.Collections.Load(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
}
if subFS, err := fs.Sub(fsys, "items"); err == nil { if err := cfg.Items.Load(fsys, "items", DecodeItem()); err != nil && !errors.Is(err, fs.ErrNotExist) {
if err = cfg.Items.Load(subFS, DecodeItem()); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
}
if subFS, err := fs.Sub(fsys, "roles"); err == nil { if err := cfg.Roles.Load(fsys, "roles"); err != nil && !errors.Is(err, fs.ErrNotExist) {
if err = cfg.Roles.Load(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
}
if subFS, err := fs.Sub(fsys, "clients"); err == nil { if err := cfg.Clients.Load(fsys, "clients"); err != nil && !errors.Is(err, fs.ErrNotExist) {
if err = cfg.Clients.Load(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
}
return cfg, nil return cfg, nil
} }
......
...@@ -120,9 +120,9 @@ func (l *EntityList[C, T]) GetIDs() []string { ...@@ -120,9 +120,9 @@ func (l *EntityList[C, T]) GetIDs() []string {
} }
// Load загружает сущности в список EntityList из указанной файловой системы // Load загружает сущности в список EntityList из указанной файловой системы
func (l *EntityList[C, T]) Load(fsys fs.FS, opt ...EntityOption[C, T]) error { func (l *EntityList[C, T]) Load(fsys fs.FS, dir string, opt ...EntityOption[C, T]) error {
assets := perxis.NewAssets[T]() assets := perxis.NewAssets[T](fsys)
items, err := assets.FromFS(fsys) items, err := assets.FromDir(dir)
if err != nil { if err != nil {
return err return err
} }
...@@ -133,8 +133,8 @@ func (l *EntityList[C, T]) Load(fsys fs.FS, opt ...EntityOption[C, T]) error { ...@@ -133,8 +133,8 @@ func (l *EntityList[C, T]) Load(fsys fs.FS, opt ...EntityOption[C, T]) error {
} }
// MustLoad загружает сущности в список EntityList из указанной файловой системы // MustLoad загружает сущности в список EntityList из указанной файловой системы
func (l *EntityList[C, T]) MustLoad(fsys fs.FS, opt ...EntityOption[C, T]) { func (l *EntityList[C, T]) MustLoad(fsys fs.FS, dir string, opt ...EntityOption[C, T]) {
if err := l.Load(fsys, opt...); err != nil { if err := l.Load(fsys, dir, opt...); err != nil {
panic(err) panic(err)
} }
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment