integration stuff
This commit is contained in:
@@ -1,31 +1,172 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Collections;
|
||||
using zero.Mapper;
|
||||
|
||||
namespace zero.Api.Endpoints.Integrations;
|
||||
|
||||
public class IntegrationsController : ZeroApiController
|
||||
{
|
||||
readonly IIntegrationTypeService IntegrationTypes;
|
||||
readonly IIntegrationStore Store;
|
||||
|
||||
public IntegrationsController(IIntegrationTypeService integrationTypes)
|
||||
public IntegrationsController(IIntegrationTypeService integrationTypes, IIntegrationStore store)
|
||||
{
|
||||
IntegrationTypes = integrationTypes;
|
||||
Store = store;
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet("types")]
|
||||
//[ZeroAuthorize(SpacePermissions.Read)]
|
||||
public virtual ActionResult<IEnumerable> GetTypes()
|
||||
public virtual async Task<ActionResult<IEnumerable>> GetTypes()
|
||||
{
|
||||
IEnumerable<IntegrationTypeDisplay> result = Mapper.Map<IntegrationType, IntegrationTypeDisplay>(IntegrationTypes.GetAll());
|
||||
|
||||
foreach (IntegrationTypeDisplay display in result)
|
||||
{
|
||||
Integration model = await Store.Load(display.Alias);
|
||||
|
||||
if (model != null)
|
||||
{
|
||||
display.IsConfigured = true;
|
||||
display.IsActivated = model.IsActive;
|
||||
display.ModelId = model.Id;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("types/{alias}")]
|
||||
//[ZeroAuthorize(SpacePermissions.Read)]
|
||||
public virtual ActionResult<SpaceType> GetTypes(string alias)
|
||||
public virtual async Task<ActionResult<IntegrationTypeDisplay>> GetType(string alias)
|
||||
{
|
||||
IntegrationTypeDisplay result = Mapper.Map<IntegrationType, IntegrationTypeDisplay>(IntegrationTypes.GetByAlias(alias));
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
Integration model = await Store.Load(result.Alias);
|
||||
|
||||
if (model != null)
|
||||
{
|
||||
result.IsConfigured = true;
|
||||
result.IsActivated = model.IsActive;
|
||||
result.ModelId = model.Id;
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("empty/{alias}")]
|
||||
//[ZeroAuthorize(CountryPermissions.Create)]
|
||||
public virtual async Task<ActionResult<Integration>> Empty(string alias)
|
||||
{
|
||||
Integration model = await Store.Empty(alias);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("{alias}")]
|
||||
//[ZeroAuthorize(CountryPermissions.Read)]
|
||||
public virtual async Task<ActionResult<Integration>> Get(string alias)
|
||||
{
|
||||
Integration model = await Store.Load(alias);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
HttpContext.Items[ApiConstants.ChangeToken] = Store.GetChangeToken(model);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("")]
|
||||
//[ZeroAuthorize(CountryPermissions.Create)]
|
||||
public virtual async Task<ActionResult<Result>> Create(Integration saveModel)
|
||||
{
|
||||
Result<Integration> result = await Store.Create(saveModel);
|
||||
|
||||
bool minimalResponse = Hints.ResponsePreference == ApiResponsePreference.Minimal;
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Created("/", minimalResponse ? null : saveModel);
|
||||
}
|
||||
|
||||
return result.WithoutModel();
|
||||
}
|
||||
|
||||
|
||||
[HttpPut("{alias}")]
|
||||
//[ZeroAuthorize(CountryPermissions.Update)]
|
||||
public virtual async Task<ActionResult<Result>> Update(string alias, Integration updateModel, [FromQuery] string changeToken = null)
|
||||
{
|
||||
if (alias != updateModel.TypeAlias)
|
||||
{
|
||||
return BadRequest(Result.Fail(nameof(alias), "@integration.errors.noaliasmatch"));
|
||||
}
|
||||
|
||||
Result<Integration> result = await Store.Update(updateModel);
|
||||
|
||||
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
|
||||
{
|
||||
return result.WithoutModel();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
[HttpPut("{alias}/activate")]
|
||||
//[ZeroAuthorize(CountryPermissions.Update)]
|
||||
public virtual async Task<ActionResult<Result>> Activate(string alias)
|
||||
{
|
||||
Result<Integration> result = await Store.Activate(alias);
|
||||
|
||||
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
|
||||
{
|
||||
return result.WithoutModel();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
[HttpPut("{alias}/deactivate")]
|
||||
//[ZeroAuthorize(CountryPermissions.Update)]
|
||||
public virtual async Task<ActionResult<Result>> Deactivate(string alias)
|
||||
{
|
||||
Result<Integration> result = await Store.Deactivate(alias);
|
||||
|
||||
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
|
||||
{
|
||||
return result.WithoutModel();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
[HttpDelete("{alias}")]
|
||||
//[ZeroAuthorize(CountryPermissions.Delete)]
|
||||
public virtual async Task<ActionResult<Result>> Delete(string alias)
|
||||
{
|
||||
Result<Integration> result = await Store.Delete(alias);
|
||||
return result.WithoutModel();
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//using Microsoft.AspNetCore.Mvc;
|
||||
//using System.Collections.Generic;
|
||||
//using System.Threading.Tasks;
|
||||
//using zero.Core.Collections;
|
||||
//using zero.Core.Entities;
|
||||
//using zero.Core.Integrations;
|
||||
//using zero.Web.Models;
|
||||
|
||||
//namespace zero.Web.Controllers
|
||||
//{
|
||||
// //[ZeroAuthorize(Permissions.Sections.PREFIX + "commerce", PermissionsValue.Read)]
|
||||
// public class IntegrationsController : BackofficeController
|
||||
// {
|
||||
// IIntegrationsCollection Collection;
|
||||
|
||||
// public IntegrationsController(IIntegrationsCollection collection)
|
||||
// {
|
||||
// Collection = collection;
|
||||
// }
|
||||
|
||||
|
||||
// public EditModel<Integration> GetEmpty([FromQuery] string alias) => Edit(Collection.GetEmpty(alias));
|
||||
|
||||
|
||||
// public async Task<EditModel<Integration>> GetByAlias([FromQuery] string alias) => Edit(await Collection.GetByAlias(alias));
|
||||
|
||||
|
||||
// public async Task<Paged<Integration>> Load([FromQuery] ListQuery<Integration> query) => await Collection.Load(query);
|
||||
|
||||
|
||||
// public async Task<IList<IntegrationTypeWithStatus>> GetTypes() => await Collection.GetTypesWithStatus();
|
||||
|
||||
|
||||
// [HttpPost]
|
||||
// public async Task<Result<Integration>> Save([FromBody] Integration model) => await Collection.Save(model);
|
||||
|
||||
// [HttpPost]
|
||||
// public async Task<Result<Integration>> SaveActiveState([FromBody] Integration model) => model.IsActive ? await Collection.Activate(model.Alias) : await Collection.Deactivate(model.Alias);
|
||||
|
||||
// [HttpDelete]
|
||||
// public async Task<Result<Integration>> Delete([FromQuery] string alias) => await Collection.DeleteByAlias(alias);
|
||||
// }
|
||||
//}
|
||||
@@ -28,10 +28,6 @@ import uiDatagrid from './ui-datagrid.vue';
|
||||
import uiProgress from './ui-progress.vue';
|
||||
import uiDate from './ui-date.vue';
|
||||
|
||||
import uiEditor from './editor/ui-editor.vue';
|
||||
import uiEditorInfos from './editor/ui-editor-infos.vue';
|
||||
import uiEditorHeader from './editor/ui-editor-header.vue';
|
||||
|
||||
export {
|
||||
uiIcon,
|
||||
uiLoading,
|
||||
@@ -61,9 +57,5 @@ export {
|
||||
uiPick,
|
||||
uiDatagrid,
|
||||
uiProgress,
|
||||
uiDate,
|
||||
|
||||
uiEditor,
|
||||
uiEditorInfos,
|
||||
uiEditorHeader
|
||||
uiDate
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { createRouter, RouteRecordRaw, RouterOptions } from 'vue-router';
|
||||
import registerDirectives from '../directives/register';
|
||||
import registerComponents from '../components/register';
|
||||
import registerFormComponents from '../forms/register';
|
||||
//import registerEditorComponents from '../editor/register';
|
||||
import registerEditorComponents from '../editor/register';
|
||||
import { getRouterConfig, appendRouterGuards } from './router/routerConfig';
|
||||
import
|
||||
{
|
||||
@@ -73,7 +73,7 @@ export class ZeroRuntime implements Zero
|
||||
registerDirectives(app);
|
||||
registerComponents(app);
|
||||
registerFormComponents(app);
|
||||
//registerEditorComponents(app);
|
||||
registerEditorComponents(app);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
|
||||
|
||||
<script>
|
||||
import * as overlays from '../../../services/overlay';
|
||||
import * as overlays from '../../services/overlay';
|
||||
|
||||
export default {
|
||||
name: 'uiEditorBlueprintProperty',
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
|
||||
|
||||
<script>
|
||||
import { arrayRemove } from '../../../utils/arrays';
|
||||
import { arrayRemove } from '../../utils/arrays';
|
||||
//import Localization from 'zero/helpers/localization.js';
|
||||
//import BlueprintApi from 'zero/api/blueprint.js';
|
||||
|
||||
@@ -126,8 +126,13 @@ export function compileField(zero: Zero, editor: ZeroEditor, field: ZeroEditorFi
|
||||
}
|
||||
|
||||
|
||||
export function compileEditor(zero: Zero, editor: ZeroEditor): ZeroCompiledEditor
|
||||
export function compileEditor(zero: Zero, editor: ZeroEditor): ZeroCompiledEditor | null
|
||||
{
|
||||
if (!editor)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let model = {
|
||||
alias: editor.alias,
|
||||
blueprint: null,
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
|
||||
.ui-icon
|
||||
{
|
||||
color: var(--color-text);
|
||||
color: var(--color-accent);
|
||||
margin-top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import { ZeroPlugin, ZeroPluginOptions } from '../core';
|
||||
import { ZeroEditor } from './editor';
|
||||
import createFields from './fields/createFields';
|
||||
|
||||
const editor = new ZeroEditor('integration.analytics.fathom');
|
||||
editor.field('siteId').text({ maxLength: 30 });
|
||||
editor.field('customDomain', { optional: true }).text({ maxLength: 150 });
|
||||
|
||||
const editor2 = new ZeroEditor('integration.analytics.google');
|
||||
editor2.field('trackingId').text({ maxLength: 30 });
|
||||
editor2.field('siteVerificationId').text({ maxLength: 150 });
|
||||
|
||||
export default {
|
||||
name: "zero.editor",
|
||||
|
||||
install(app: ZeroPluginOptions)
|
||||
{
|
||||
createFields(app);
|
||||
|
||||
app.schema('integration.analytics.fathom', editor);
|
||||
app.schema('integration.analytics.google', editor2);
|
||||
}
|
||||
} as ZeroPlugin;
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
import { App, defineAsyncComponent } from 'vue';
|
||||
|
||||
export default function (app: App)
|
||||
{
|
||||
app.component('ui-editor', defineAsyncComponent(() => import('./ui-editor.vue')));
|
||||
app.component('ui-editor-infos', defineAsyncComponent(() => import('./ui-editor-infos.vue')));
|
||||
app.component('ui-editor-header', defineAsyncComponent(() => import('./ui-editor-header.vue')));
|
||||
};
|
||||
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
|
||||
<script>
|
||||
import { selectorToArray, setObjectValue } from '../../utils';
|
||||
import { localize } from '../../services/localization';
|
||||
import { selectorToArray, setObjectValue } from '../utils';
|
||||
import { localize } from '../services/localization';
|
||||
import { defineComponent } from 'vue';
|
||||
//import Overlay from 'zero/helpers/overlay.js';
|
||||
|
||||
+12
-4
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="editor-outer" v-if="loaded">
|
||||
<div class="editor-outer" v-if="loaded && editorConfig">
|
||||
<header class="editor-above" v-if="aboveDefined">
|
||||
<!--<slot name="above" v-bind:config="editorConfig"></slot>-->
|
||||
</header>
|
||||
@@ -38,6 +38,15 @@
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-outer" v-if="loaded && !editorConfig">
|
||||
<div class="page page-error" style="min-height: 400px;">
|
||||
<ui-icon symbol="fth-cloud-snow" class="page-error-icon" :size="82" />
|
||||
<p class="page-error-text">
|
||||
<strong class="page-error-headline">Could not find editor</strong><br>
|
||||
<span v-if="typeof config === 'string'">Please register an editor schema with the alias:<br />[{{config}}]</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -49,7 +58,7 @@
|
||||
import EditorComponent from './ui-editor-component.vue';
|
||||
import BlueprintProperty from './blueprint/property.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { compileEditor } from '../../editor/compile';
|
||||
import { compileEditor } from './compile';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'uiEditor',
|
||||
@@ -118,8 +127,7 @@
|
||||
{
|
||||
this.system = this.$route.query['zero.scope'] == 'system';
|
||||
const schema = typeof this.config === 'string' ? await this.zero.getSchema(this.config) : this.config;
|
||||
const editor = compileEditor(this.zero, schema);
|
||||
this.editorConfig = editor;
|
||||
this.editorConfig = compileEditor(this.zero, schema);
|
||||
|
||||
this.onConfigure(this);
|
||||
|
||||
@@ -6,11 +6,17 @@ export default {
|
||||
|
||||
getType: (alias: string, config?: ApiRequestConfig) => get('integrations/types/' + alias, { ...config }),
|
||||
|
||||
//getEmpty: async alias => await get(base + 'getEmpty', { params: { alias } }),
|
||||
//getTypes: async () => await get(base + 'getTypes'),
|
||||
//getByAlias: async (alias, config) => await get(base + 'getByAlias', { ...config, params: { alias } }),
|
||||
//getByQuery: async (query, config) => await get(base + 'getByQuery', { ...config, params: { query } }),
|
||||
//save: async (model, config) => await post(base + 'save', model, { ...config }),
|
||||
//saveActiveState: async (model, config) => await post(base + 'saveActiveState', model, { ...config }),
|
||||
//delete: async alias => await del(base + 'delete', { params: { alias } })
|
||||
getEmpty: (alias: string, config?: ApiRequestConfig) => get('integrations/empty/' + alias, { ...config }),
|
||||
|
||||
getByAlias: (alias: string, config?: ApiRequestConfig) => get('integrations/' + alias, { ...config }),
|
||||
|
||||
create: (model: any, config?: ApiRequestConfig) => post('integrations', model, config),
|
||||
|
||||
update: (model: any, config?: ApiRequestConfig) => put('integrations/' + model.typeAlias, model, config),
|
||||
|
||||
activate: (alias: string, config?: ApiRequestConfig) => put('integrations/' + alias + '/activate', null, config),
|
||||
|
||||
deactivate: (alias: string, config?: ApiRequestConfig) => put('integrations/' + alias + '/deactivate', null, config),
|
||||
|
||||
delete: (alias: string, config?: ApiRequestConfig) => del('integrations/' + alias, null, config),
|
||||
};
|
||||
@@ -1,48 +1,112 @@
|
||||
<template>
|
||||
<ui-form ref="form" class="translation" v-slot="form" @submit="onSubmit" @load="onLoad" :route="route">
|
||||
<ui-form-header v-model:value="model" prefix="@translation.list" title="@translation.name" :disabled="disabled" :is-create="!id" :state="form.state" :can-delete="meta.canDelete" @delete="onDelete" />
|
||||
<ui-editor config="translations:edit" v-model="model" :meta="meta" :disabled="disabled" :scope="true">
|
||||
<template v-slot:below>
|
||||
<ui-editor-infos v-model="model" :disabled="disabled" />
|
||||
<ui-form ref="form" v-slot="form" @submit="onSubmit" @load="onLoad">
|
||||
<ui-trinity class="ui-editor-overlay integration">
|
||||
|
||||
<template v-slot:header>
|
||||
<ui-header-bar :title="integration.name" prefix="@integration.list" :back-button="false" :close-button="true" />
|
||||
</template>
|
||||
</ui-editor>
|
||||
|
||||
<template v-slot:footer>
|
||||
<ui-button type="light onbg" label="@ui.close" @click="config.close"></ui-button>
|
||||
<template v-if="integration.isConfigured">
|
||||
<ui-button type="light onbg" label="@ui.remove" @click="onDelete"></ui-button>
|
||||
<ui-button v-if="!disabled" type="primary" :submit="true" label="@ui.save" :state="form.state" :disabled="loading"></ui-button>
|
||||
</template>
|
||||
<template v-if="!integration.isConfigured && !disabled">
|
||||
<ui-button type="light onbg" :submit="true" label="@ui.save" :state="form.state" :disabled="loading"></ui-button>
|
||||
<ui-button type="accent" @click="saveAndActivate" label="Save and activate" :state="form.state" :disabled="loading"></ui-button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<ui-loading v-if="loading" :is-big="true" />
|
||||
|
||||
<div v-if="!loading" class="ui-editor-overlay-editor">
|
||||
<ui-editor :config="editor" v-model="model" :meta="meta" :is-page="false" infos="none" :disabled="disabled" />
|
||||
</div>
|
||||
|
||||
</ui-trinity>
|
||||
</ui-form>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import * as overlays from '../../services/overlay';
|
||||
//import Notification from 'zero/helpers/notification.js';
|
||||
import api from './api';
|
||||
|
||||
export default {
|
||||
props: ['id'],
|
||||
|
||||
props: {
|
||||
config: Object
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
integration: {},
|
||||
disabled: false,
|
||||
loading: true,
|
||||
state: 'default',
|
||||
editor: null,
|
||||
meta: {},
|
||||
model: { },
|
||||
route: 'translations-edit',
|
||||
disabled: false
|
||||
model: {}
|
||||
}),
|
||||
|
||||
created()
|
||||
{
|
||||
this.integration = { ...this.config.model };
|
||||
//this.$el.style.setProperty('--color-primary', this.config.model.color);
|
||||
//this.$el.style.setProperty('--color-button', this.config.model.color);
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async onLoad(form)
|
||||
{
|
||||
const response = await form.load(() => this.id ? api.getById(this.id, undefined, { system: this.$route.query.scope == 'system' }) : api.getEmpty(this.$route.query['zero.flavor']));
|
||||
const alias = this.integration.alias;
|
||||
this.editor = this.integration.editorAlias || 'integration.' + alias;
|
||||
|
||||
const response = await form.load(() => this.integration.isConfigured ? api.getByAlias(alias) : api.getEmpty(alias));
|
||||
this.model = response;
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
|
||||
saveAndActivate(e)
|
||||
{
|
||||
this.model.isActive = true;
|
||||
this.onSubmit(this.$refs.form);
|
||||
},
|
||||
|
||||
|
||||
async onSubmit(form)
|
||||
{
|
||||
const response = this.id ? await api.update(this.model) : await api.create(this.model);
|
||||
const response = this.integration.isConfigured ? await api.update(this.model) : await api.create(this.model);
|
||||
await form.handle(response);
|
||||
|
||||
if (response.success)
|
||||
{
|
||||
this.config.confirm(response.data);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onDelete(item, opts)
|
||||
async onDelete()
|
||||
{
|
||||
opts.hide();
|
||||
this.$refs.form.onDelete(api.delete.bind(this, this.id));
|
||||
const result = await overlays.confirmDelete();
|
||||
|
||||
if (result.eventType === 'confirm')
|
||||
{
|
||||
result.state('loading');
|
||||
|
||||
const response = await api.delete(this.integration.alias);
|
||||
|
||||
if (response.success)
|
||||
{
|
||||
result.state('success');
|
||||
result.close();
|
||||
//Notification.success('@deleteoverlay.success', '@deleteoverlay.success_text');
|
||||
this.config.confirm(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
<span class="integrations-item-icon" :style="{'background-color': hasColor ? model.color : null }">
|
||||
<ui-icon :symbol="model.icon || 'fth-box'" :size="26" />
|
||||
</span>
|
||||
<!--<button type="button" v-if="!model.isConfigured" @click="open" class="ui-button type-primary type-block">
|
||||
<span class="ui-button-text" v-localize="'Setup'" />
|
||||
</button>
|
||||
<!--
|
||||
<button type="button" v-else @click="open" class="ui-button type-primary type-block">
|
||||
<span class="ui-button-text" v-localize="'Edit'" />
|
||||
</button>-->
|
||||
@@ -20,9 +18,10 @@
|
||||
<span v-localize="model.description"></span>
|
||||
</template>
|
||||
</p>
|
||||
<div class="integrations-item-tags">
|
||||
<!--<div class="integrations-item-tags">
|
||||
<span class="ui-tag" v-for="tag in model.tags">{{tag}}</span>
|
||||
</div>
|
||||
</div>-->
|
||||
<ui-button class="integrations-item-button" type="action small" v-if="!model.isConfigured" v-localize="'Setup'" @click="open" />
|
||||
<ui-toggle class="integrations-item-toggle" v-if="model.isConfigured" v-model="model.isActivacted" @input="$emit('onActiveChange', model)" on-content="Active" off-content="Active" />
|
||||
</main>
|
||||
</div>
|
||||
@@ -30,7 +29,7 @@
|
||||
|
||||
|
||||
<script>
|
||||
//import Overlay from 'zero/helpers/overlay.js';
|
||||
import * as overlays from '../../services/overlay';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -52,20 +51,19 @@
|
||||
},
|
||||
|
||||
methods: {
|
||||
open()
|
||||
async open()
|
||||
{
|
||||
// open editing overlay
|
||||
//return Overlay.open({
|
||||
// component: () => import('./integration.vue'),
|
||||
// display: 'editor',
|
||||
// model: this.model.type,
|
||||
// isCreate: !this.model.isConfigured,
|
||||
// alias: this.model.type.alias,
|
||||
// width: 960
|
||||
//}).then(value =>
|
||||
//{
|
||||
// this.$emit('change', value);
|
||||
//});
|
||||
const result = await overlays.open({
|
||||
component: () => import('./integration.vue'),
|
||||
display: 'editor',
|
||||
model: this.model,
|
||||
width: 960
|
||||
});
|
||||
|
||||
if (result.eventType === 'confirm')
|
||||
{
|
||||
this.$emit('change', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +78,11 @@
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--padding);
|
||||
align-items: flex-start;
|
||||
|
||||
> aside
|
||||
{
|
||||
align-self: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.integrations-item .ui-button.type-block
|
||||
@@ -95,9 +98,14 @@
|
||||
padding-top: var(--padding-m);
|
||||
}
|
||||
|
||||
.integrations-item-button
|
||||
{
|
||||
margin-top: var(--padding-s);
|
||||
}
|
||||
|
||||
.integrations-item-tags
|
||||
{
|
||||
margin-top: var(--padding-s);
|
||||
margin-top: var(--padding-s);
|
||||
}
|
||||
|
||||
.integrations-item-icon
|
||||
@@ -106,7 +114,8 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
min-height: 90px;
|
||||
height: 100%;
|
||||
font-size: 22px;
|
||||
text-align: center;
|
||||
background: var(--color-box-nested);
|
||||
@@ -123,7 +132,7 @@
|
||||
{
|
||||
line-height: 1.3;
|
||||
color: var(--color-text-dim);
|
||||
margin: 0.5em 0 0;
|
||||
margin: 0;
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
|
||||
@@ -544,8 +544,10 @@
|
||||
"errors": {
|
||||
"typenotfound": "The integration type does not exist",
|
||||
"multiplenotallowed": "Can not create multiple integrations per type",
|
||||
"alreadycreated": "You have already created an integration for this type",
|
||||
"notfound": "Could not find a matching integration",
|
||||
"couldnotupdatestate": "Not updated"
|
||||
"couldnotupdatestate": "Not updated",
|
||||
"noaliasmatch": "The alias as part of the URL does not match the alias of the model"
|
||||
},
|
||||
"fields": {
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public class ConfigurationModule : ZeroModule
|
||||
|
||||
services.AddTransient<IZeroOptions, ZeroOptions>(factory => factory.GetService<IOptions<ZeroOptions>>().Value);
|
||||
services.AddScoped<IIntegrationTypeService, IntegrationTypeService>();
|
||||
services.AddScoped<IIntegrationStore, IntegrationStore>();
|
||||
|
||||
services.AddOptions<FeatureOptions>().Bind(configuration.GetSection("Zero:Features"));
|
||||
services.AddOptions<IntegrationOptions>();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
namespace zero.Configuration;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace zero.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// An integration is an application part which has a public configuration per app.
|
||||
/// It's up to the user to provide functionality.
|
||||
/// </summary>
|
||||
[RavenCollection("Integrations")]
|
||||
public class IntegrationModel : ZeroEntity
|
||||
public class Integration : ZeroEntity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string TypeAlias { get; set; }
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace zero.Configuration;
|
||||
|
||||
public class IntegrationOptions : List<IntegrationType>
|
||||
{
|
||||
public void Add<T>(string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null) where T : IntegrationModel, new()
|
||||
public void Add<T>(string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null) where T : Integration , new()
|
||||
{
|
||||
Add(new IntegrationType(typeof(T))
|
||||
{
|
||||
@@ -22,6 +22,11 @@ public class IntegrationOptions : List<IntegrationType>
|
||||
|
||||
public void Add(Type type, string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null)
|
||||
{
|
||||
if (!typeof(Integration).IsAssignableFrom(type))
|
||||
{
|
||||
throw new ArgumentException("Type has to inherit the Integration base model", nameof(type));
|
||||
}
|
||||
|
||||
Add(new IntegrationType(type)
|
||||
{
|
||||
Alias = alias,
|
||||
@@ -30,7 +35,7 @@ public class IntegrationOptions : List<IntegrationType>
|
||||
ImagePath = imagePath,
|
||||
Tags = tags,
|
||||
Validator = validator,
|
||||
Construct = cfg => Activator.CreateInstance(type),
|
||||
Construct = cfg => Activator.CreateInstance(type) as Integration,
|
||||
EditorAlias = editorAlias
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//using Microsoft.Extensions.Logging;
|
||||
|
||||
//namespace zero.Configuration;
|
||||
|
||||
//public class IntegrationService : IntegrationsCollection, IIntegrationService
|
||||
//{
|
||||
// public IntegrationService(IStoreContext<Integration> context, ILogger<IntegrationsCollection> logger) : base(context, logger)
|
||||
// {
|
||||
// Options = new(false);
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
//public interface IIntegrationService : IIntegrationsCollection { }
|
||||
@@ -0,0 +1,170 @@
|
||||
using FluentValidation.Results;
|
||||
|
||||
namespace zero.Configuration;
|
||||
|
||||
public class IntegrationStore : IIntegrationStore
|
||||
{
|
||||
protected IStoreOperations Operations { get; private set; }
|
||||
|
||||
protected IIntegrationTypeService IntegrationTypes { get; private set; }
|
||||
|
||||
|
||||
public IntegrationStore(IStoreContext context, IIntegrationTypeService integrationTypes)
|
||||
{
|
||||
IntegrationTypes = integrationTypes;
|
||||
Operations = new StoreOperations(context.Context, context.Interceptors, context.Options, new StoreConfig() { IncludeInactive = true });
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<Integration> Empty(string alias)
|
||||
{
|
||||
IntegrationType type = IntegrationTypes.GetByAlias(alias);
|
||||
Integration integration = type?.Construct(type);
|
||||
|
||||
if (integration != null)
|
||||
{
|
||||
integration.TypeAlias = alias;
|
||||
}
|
||||
|
||||
return Task.FromResult(integration);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Integration> Load(string alias)
|
||||
{
|
||||
Paged<Integration> result = await Operations.Load<Integration>(1, 1, q => q.Where(x => x.TypeAlias == alias));
|
||||
return result.Items.FirstOrDefault();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string GetChangeToken(Integration model) => Operations.GetChangeToken(model);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Result<Integration>> Create(Integration model) => await Save(model, true);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Result<Integration>> Update(Integration model) => await Save(model, false);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Result<Integration>> Activate(string alias)
|
||||
{
|
||||
Integration model = await Load(alias);
|
||||
if (model != null)
|
||||
{
|
||||
model.IsActive = true;
|
||||
}
|
||||
return await Update(model);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Result<Integration>> Deactivate(string alias)
|
||||
{
|
||||
Integration model = await Load(alias);
|
||||
if (model != null)
|
||||
{
|
||||
model.IsActive = false;
|
||||
}
|
||||
return await Update(model);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<Integration>> Delete(string alias)
|
||||
{
|
||||
Integration integration = await Load(alias);
|
||||
return await Operations.Delete(integration);
|
||||
}
|
||||
|
||||
|
||||
// <inheritdoc />
|
||||
protected virtual async Task<ValidationResult> Validate(Integration model)
|
||||
{
|
||||
await Task.Delay(0);
|
||||
return new ValidationResult();
|
||||
//ZeroValidator<T> validator = new();
|
||||
//ValidationRules(validator);
|
||||
//return await validator.ValidateAsync(model);
|
||||
//return base.Validate(model);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected virtual async Task<Result<Integration>> Save(Integration model, bool create = false)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return Result<Integration>.Fail("@integration.errors.notfound");
|
||||
}
|
||||
|
||||
IntegrationType type = IntegrationTypes.GetByAlias(model.TypeAlias);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
return Result<Integration>.Fail("@integration.errors.typenotfound");
|
||||
}
|
||||
|
||||
if (create && await Operations.Any<Integration>(q => q.Where(x => x.TypeAlias == model.TypeAlias)))
|
||||
{
|
||||
return Result<Integration>.Fail("@integration.errors.multiplenotallowed");
|
||||
}
|
||||
|
||||
if (!create && await Operations.Any<Integration>(q => q.Where(x => x.TypeAlias == model.TypeAlias && x.Id != model.Id)))
|
||||
{
|
||||
return Result<Integration>.Fail("@integration.errors.alreadycreated");
|
||||
}
|
||||
|
||||
return await Operations.Create(model, async x => await Validate(x));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IIntegrationStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Get new instance of an integration by integration alias
|
||||
/// </summary>
|
||||
Task<Integration> Empty(string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Get an integration by integration alias
|
||||
/// </summary>
|
||||
Task<Integration> Load(string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Get the change vector for a model (Proxy to IAsyncDocumentSession.GetChangeVectorFor<>)
|
||||
/// </summary>
|
||||
string GetChangeToken(Integration model);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new integration
|
||||
/// </summary>
|
||||
Task<Result<Integration>> Create(Integration model);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an integration
|
||||
/// </summary>
|
||||
Task<Result<Integration>> Update(Integration model);
|
||||
|
||||
/// <summary>
|
||||
/// Activates a configured integration
|
||||
/// </summary>
|
||||
Task<Result<Integration>> Activate(string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Disables a configured integration
|
||||
/// </summary>
|
||||
Task<Result<Integration>> Deactivate(string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes configuration of an integration and disables it
|
||||
/// </summary>
|
||||
Task<Result<Integration>> Delete(string alias);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public class IntegrationType
|
||||
public IValidator Validator { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Func<FlavorConfig, object> Construct { get; set; }
|
||||
public Func<IntegrationType, Integration> Construct { get; set; }
|
||||
|
||||
public IntegrationType(Type type)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ public class IntegrationTypeService : IIntegrationTypeService
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IntegrationType GetByType<T>() where T : IntegrationModel
|
||||
public IntegrationType GetByType<T>() where T : Integration
|
||||
{
|
||||
Type type = typeof(T);
|
||||
return Types.FirstOrDefault(x => x.ModelType == type);
|
||||
@@ -55,5 +55,5 @@ public interface IIntegrationTypeService
|
||||
/// <summary>
|
||||
/// Get a specific integration type by model type.
|
||||
/// </summary>
|
||||
IntegrationType GetByType<T>() where T : IntegrationModel;
|
||||
IntegrationType GetByType<T>() where T : Integration;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,14 @@ public partial class StoreOperations : IStoreOperations
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<bool> Any<T>(Func<IRavenQueryable<T>, IQueryable<T>> querySelector = default) where T : ZeroIdEntity, new()
|
||||
{
|
||||
querySelector ??= x => x;
|
||||
return await querySelector(Session.Query<T>()).AnyAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Paged<T>> Load<T>(int pageNumber, int pageSize, Func<IRavenQueryable<T>, IQueryable<T>> querySelector = default) where T : ZeroIdEntity, new()
|
||||
{
|
||||
|
||||
@@ -141,6 +141,11 @@ public interface IStoreOperations
|
||||
/// </summary>
|
||||
T PrepareForSave<T>(T model) where T : ZeroIdEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Check if any items exist in this collection (with optional query)
|
||||
/// </summary>
|
||||
Task<bool> Any<T>(Func<IRavenQueryable<T>, IQueryable<T>> querySelector = default) where T : ZeroIdEntity, new();
|
||||
|
||||
/// <summary>
|
||||
/// Get an entity by Id
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace zero.Utils;
|
||||
|
||||
public class JsonFlavorVariantConverter : JsonConverter<ISupportsFlavors>
|
||||
{
|
||||
public JsonFlavorVariantConverter() : base()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeof(ISupportsFlavors).IsAssignableFrom(typeToConvert);
|
||||
}
|
||||
|
||||
|
||||
public override ISupportsFlavors Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Utf8JsonReader variantReader = reader;
|
||||
string flavor = null;
|
||||
|
||||
while (variantReader.Read())
|
||||
{
|
||||
if (variantReader.TokenType == JsonTokenType.PropertyName)
|
||||
{
|
||||
string property = variantReader.GetString();
|
||||
|
||||
if (property == "Flavor")
|
||||
{
|
||||
variantReader.Read();
|
||||
flavor = variantReader.GetString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (reader.Read()) { }
|
||||
|
||||
return new ZeroEntity() { Flavor = flavor };
|
||||
}
|
||||
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ISupportsFlavors value, JsonSerializerOptions options)
|
||||
{
|
||||
JsonSerializerOptions newOptions = new(options);
|
||||
JsonConverter toRemove = newOptions.GetConverter(typeof(JsonFlavorVariantConverter));
|
||||
newOptions.Converters.Remove(toRemove);
|
||||
JsonSerializer.Serialize(writer, value, value.GetType(), newOptions);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace zero.Demo
|
||||
{
|
||||
public class FathomAnalyticsIntegration : IntegrationModel
|
||||
public class FathomAnalyticsIntegration : Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the site in Fathom
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace zero.Demo
|
||||
{
|
||||
public class GoogleAnalyticsIntegration : IntegrationModel
|
||||
public class GoogleAnalyticsIntegration : Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// Provided tracking ID from Google
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System.Text.Json;
|
||||
using zero.Configuration;
|
||||
using zero.Context;
|
||||
using zero.Persistence;
|
||||
using zero.Routing;
|
||||
using zero.Utils;
|
||||
|
||||
namespace zero.Demo.Controllers;
|
||||
|
||||
@@ -40,6 +43,19 @@ public class SetupController : Controller
|
||||
//}
|
||||
|
||||
|
||||
[HttpGet("/api/setup/json")]
|
||||
public async Task<IActionResult> TestJson([FromServices] IIntegrationStore store)
|
||||
{
|
||||
Integration integration = await store.Load("analytics.fathom");
|
||||
|
||||
JsonFlavorVariantConverter converter = new();
|
||||
JsonSerializerOptions opts = new();
|
||||
opts.Converters.Add(converter);
|
||||
|
||||
return Content(JsonSerializer.Serialize(integration, opts), "application/json");
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("/api/setup/indexes")]
|
||||
public async Task<IActionResult> Indexes([FromServices] IZeroStore store, [FromServices] IZeroContext context)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user