[ACA-1743] extension settings (#1399)

* upgrade tslib

* initial settings skeleton

* migrate language picker setting

* support string parameters

* remove process extensions workaround

* update extensions schema

* update docs

* unit tests

* fix unit test
This commit is contained in:
Denys Vuika 2020-04-06 18:58:41 +01:00 committed by GitHub
parent 2b910d5a15
commit 5a88c8c852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 425 additions and 127 deletions

View File

@ -16,6 +16,7 @@ Learn how to extend the features of the Alfresco Content Application.
- [Actions](/extending/actions)
- [Application actions](/extending/application-actions)
- [Rules](/extending/rules)
- [Settings](/extending/settings)
- [Application features](/extending/application-features)
- [Custom icons](/extending/icons)
- [Registration](/extending/registration)

View File

@ -0,0 +1,45 @@
---
Title: Settings
---
# Settings
The application settings can be accessed via the `/settings` route.
You can project custom configuration groups via the `settings` section:
```json
{
"settings": [
{
"id": "extensions.ps.settings",
"name": "Extensions: Process Services",
"parameters": [
{
"name": "Enable Process Services Extensions",
"key": "processServices",
"type": "boolean",
"value": false
}
]
}
]
}
```
At runtime, you are going to get an extra group called "Extensions: Process Services"
with a custom boolean setting "Enable Process Services Extensions".
![Custom settings group](../images/aca-settings-custom-group.png)
## Parameters
Each setting parameter object supports the following properties:
| Property | Description |
| -------- | ----------------------------------------------- |
| id | (optional) Unique identifier |
| name | Public name, can be translation key |
| key | The key to use when saving to the storage |
| type | The type of the value (boolean / string) |
| value | (optional) Default value to use for the setting |

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -605,6 +605,52 @@
"type": "boolean"
}
}
},
"settingsGroupRef": {
"type": "object",
"properties": {
"id": {
"description": "Unique identifier",
"type": "string"
},
"name": {
"description": "Category name, can be translation key",
"type": "string"
},
"parameters": {
"description": "Settings group parameters",
"type": "array",
"items": { "$ref": "#/definitions/settingsGroupParameterRef" },
"minItems": 1
}
},
"required": ["id", "name", "parameters"]
},
"settingsGroupParameterRef": {
"type": "object",
"properties": {
"id": {
"description": "Unique identifier",
"type": "string"
},
"name": {
"description": "Public name, can be a translation key",
"type": "string"
},
"key": {
"description": "The key to use when saving to the storage",
"type": "string"
},
"type": {
"description": "The type of the value",
"type": "string"
},
"value": {
"description": "Default value to use for the setting",
"type": ["boolean", "string", "number"]
}
},
"required": ["name", "key", "type"]
}
},
@ -666,6 +712,12 @@
"items": { "$ref": "#/definitions/actionRef" },
"minItems": 1
},
"settings": {
"description": "List of application-specific setting groups",
"type": "array",
"items": { "$ref": "#/definitions/settingsGroupRef" },
"minItems": 1
},
"features": {
"description": "Application-specific features and extensions",
"type": "object",

6
package-lock.json generated
View File

@ -13380,9 +13380,9 @@
}
},
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
},
"tslint": {
"version": "5.20.1",

View File

@ -91,8 +91,8 @@
"codelyzer": "^5.2.2",
"commander": "^4.0.1",
"cpr": "^3.0.1",
"dotenv": "6.2.0",
"cspell": "^4.0.55",
"dotenv": "6.2.0",
"husky": "^2.4.0",
"jasmine-core": "~2.8.0",
"jasmine-reporters": "^2.2.1",
@ -116,7 +116,7 @@
"selenium-webdriver": "4.0.0-alpha.1",
"ts-node": "^8.0.3",
"tsickle": "0.34.0",
"tslib": "^1.9.0",
"tslib": "^1.11.1",
"tslint": "^5.20.1",
"typescript": "3.2.4",
"wait-on": "^3.0.1",

View File

@ -30,7 +30,6 @@ import * as repository from './repository.rules';
export interface AcaRuleContext extends RuleContext {
languagePicker: boolean;
withCredentials: boolean;
processServices: boolean;
}
/**
@ -554,5 +553,5 @@ export function canShowLogout(context: AcaRuleContext): boolean {
* @param context Rule execution context
*/
export function canShowProcessServices(context: AcaRuleContext): boolean {
return context.processServices;
return localStorage && localStorage.getItem('processServices') === 'true';
}

View File

@ -28,8 +28,8 @@ import { Node, Person, Group, RepositoryInfo } from '@alfresco/js-api';
import { AppState } from '../states/app.state';
export enum AppActionTypes {
SetSettingsParameter = 'SET_SETTINGS_PARAMETER',
SetInitialState = 'SET_INITIAL_STATE',
SetLanguagePicker = 'SET_LANGUAGE_PICKER',
SetCurrentFolder = 'SET_CURRENT_FOLDER',
SetCurrentUrl = 'SET_CURRENT_URL',
SetUserProfile = 'SET_USER_PROFILE',
@ -41,8 +41,13 @@ export enum AppActionTypes {
ResetSelection = 'RESET_SELECTION',
SetInfoDrawerState = 'SET_INFO_DRAWER_STATE',
SetInfoDrawerMetadataAspect = 'SET_INFO_DRAWER_METADATA_ASPECT',
CloseModalDialogs = 'CLOSE_MODAL_DIALOGS',
ToggleProcessServices = 'TOGGLE_PROCESS_SERVICES'
CloseModalDialogs = 'CLOSE_MODAL_DIALOGS'
}
export class SetSettingsParameterAction implements Action {
readonly type = AppActionTypes.SetSettingsParameter;
constructor(public payload: { name: string; value: any }) {}
}
export class SetInitialStateAction implements Action {
@ -51,12 +56,6 @@ export class SetInitialStateAction implements Action {
constructor(public payload: AppState) {}
}
export class SetLanguagePickerAction implements Action {
readonly type = AppActionTypes.SetLanguagePicker;
constructor(public payload: boolean) {}
}
export class SetCurrentFolderAction implements Action {
readonly type = AppActionTypes.SetCurrentFolder;
@ -114,9 +113,3 @@ export class SetRepositoryInfoAction implements Action {
constructor(public payload: RepositoryInfo) {}
}
export class ToggleProcessServicesAction implements Action {
readonly type = AppActionTypes.ToggleProcessServices;
constructor(public payload: boolean) {}
}

View File

@ -133,8 +133,3 @@ export const infoDrawerMetadataAspect = createSelector(
selectApp,
state => state.infoDrawerMetadataAspect
);
export const getProcessServicesState = createSelector(
selectApp,
state => state.processServices
);

View File

@ -44,7 +44,6 @@ export interface AppState {
showFacetFilter: boolean;
documentDisplayMode: string;
repository: RepositoryInfo;
processServices: boolean;
}
export interface AppStore {

View File

@ -26,8 +26,6 @@
"viewer.maxRetries": 1,
"sharedLinkDateTimePickerType": "date",
"headerColor": "#ffffff",
"languagePicker": true,
"processServices": true,
"pagination": {
"size": 25,
"supportedPageSizes": [25, 50, 100]

View File

@ -42,6 +42,12 @@ describe('AppComponent', () => {
}
};
const storageMock: any = {
getItem(): string {
return '';
}
};
beforeAll(() => {
component = new AppComponent(
null,
@ -55,7 +61,8 @@ describe('AppComponent', () => {
null,
null,
null,
null
null,
storageMock
);
});

View File

@ -30,7 +30,8 @@ import {
FileUploadErrorEvent,
PageTitleService,
UploadService,
SharedLinksApiService
SharedLinksApiService,
StorageService
} from '@alfresco/adf-core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router, ActivationEnd } from '@angular/router';
@ -73,7 +74,8 @@ export class AppComponent implements OnInit, OnDestroy {
private extensions: AppExtensionService,
private contentApi: ContentApiService,
private appService: AppService,
private sharedLinksApiService: SharedLinksApiService
private sharedLinksApiService: SharedLinksApiService,
private storage: StorageService
) {}
ngOnInit() {
@ -178,8 +180,7 @@ export class AppComponent implements OnInit, OnDestroy {
const state: AppState = {
...INITIAL_APP_STATE,
languagePicker: this.config.get<boolean>('languagePicker'),
processServices: this.config.get<boolean>('processServices'),
languagePicker: this.storage.getItem('languagePicker') === 'true',
appName: this.config.get<string>('application.name'),
headerColor: this.config.get<string>('headerColor'),
logoPath: this.config.get<string>('application.logo'),

View File

@ -32,7 +32,7 @@ import { Store } from '@ngrx/store';
import {
AppState,
SetUserProfileAction,
SetLanguagePickerAction
SetSettingsParameterAction
} from '@alfresco/aca-shared/store';
describe('CurrentUserComponent', () => {
@ -91,7 +91,9 @@ describe('CurrentUserComponent', () => {
it('should set language picker state', done => {
fixture.detectChanges();
store.dispatch(new SetLanguagePickerAction(true));
store.dispatch(
new SetSettingsParameterAction({ name: 'languagePicker', value: true })
);
component.languagePicker$.subscribe((languagePicker: boolean) => {
expect(languagePicker).toBe(true);

View File

@ -68,39 +68,40 @@
</form>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel *ngFor="let group of settingGroups">
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'APP.SETTINGS.APPLICATION-SETTINGS' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<mat-checkbox
[ngModel]="languagePicker$ | async"
(change)="onLanguagePickerValueChanged($event)"
>
Language Picker
</mat-checkbox>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Extensions</mat-panel-title>
<mat-panel-title>{{ group.name | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<div class="aca-settings-extensions-list">
<mat-checkbox
[ngModel]="aiExtensions$ | async"
(change)="onToggleAiExtensions($event)"
>
Enable AI Extensions
</mat-checkbox>
<div class="aca-settings-parameter-list">
<ng-container *ngFor="let param of group.parameters">
<ng-container [ngSwitch]="param.type">
<ng-container *ngSwitchCase="'boolean'">
<mat-checkbox
[checked]="getBooleanParamValue(param)"
(change)="setParamValue(param, $event.checked)"
>{{ param.name | translate }}</mat-checkbox
>
</ng-container>
<mat-checkbox
[ngModel]="psExtensions$ | async"
(change)="onTogglePsExtensions($event)"
>
Enable Process Services Extensions
</mat-checkbox>
<ng-container *ngSwitchCase="'string'">
<mat-form-field class="settings-input" appearance="outline">
<mat-label>{{ param.name | translate }}</mat-label>
<input
matInput
type="text"
[value]="getStringParamValue(param)"
(blur)="setParamValue(param, $event.target.value)"
(keyup.enter)="setParamValue(param, $event.target.value)"
/>
</mat-form-field>
</ng-container>
<ng-container *ngSwitchDefault>
<span>Unknown parameter type: {{ param.name | translate }}</span>
</ng-container>
</ng-container>
</ng-container>
</div>
</mat-expansion-panel>
</mat-accordion>

View File

@ -24,9 +24,131 @@
*/
import { SettingsComponent } from './settings.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { setupTestBed, StorageService } from '@alfresco/adf-core';
import { AppSettingsModule } from './settings.module';
import { AppTestingModule } from '../../testing/app-testing.module';
import { SettingsParameterRef } from '../../types';
import { AppExtensionService } from '../../extensions/extension.service';
import { By } from '@angular/platform-browser';
import {
TranslateModule,
TranslateLoader,
TranslateFakeLoader
} from '@ngx-translate/core';
describe('SettingsComponent', () => {
it('should be defined', () => {
expect(SettingsComponent).toBeDefined();
let fixture: ComponentFixture<SettingsComponent>;
let component: SettingsComponent;
let storage: StorageService;
let appExtensions: AppExtensionService;
let stringParam: SettingsParameterRef;
let boolParam: SettingsParameterRef;
setupTestBed({
imports: [
AppSettingsModule,
AppTestingModule,
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useClass: TranslateFakeLoader }
})
]
});
beforeEach(() => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
storage = TestBed.get(StorageService);
appExtensions = TestBed.get(AppExtensionService);
stringParam = {
key: 'key',
name: 'param1',
type: 'string',
value: 'paramValue'
};
boolParam = {
key: 'key',
name: 'param2',
type: 'boolean',
value: true
};
});
it('should retrieve string param value from storage', () => {
spyOn(storage, 'getItem').and.returnValue('storageValue');
const value = component.getStringParamValue(stringParam);
expect(value).toBe('storageValue');
});
it('should use param value as fallback when storage is empty', () => {
spyOn(storage, 'getItem').and.returnValue(null);
const value = component.getStringParamValue(stringParam);
expect(value).toBe('paramValue');
});
it('should save param value', () => {
spyOn(storage, 'setItem').and.stub();
component.setParamValue(stringParam, 'test');
expect(stringParam.value).toBe('test');
expect(storage.setItem).toHaveBeenCalledWith(
stringParam.key,
stringParam.value
);
});
it('should save param value only if changed', () => {
spyOn(storage, 'setItem').and.stub();
component.setParamValue(stringParam, 'test');
component.setParamValue(stringParam, 'test');
component.setParamValue(stringParam, 'test');
expect(storage.setItem).toHaveBeenCalledTimes(1);
});
it('should retrieve boolean param value', () => {
const getItemSpy = spyOn(storage, 'getItem').and.returnValue('true');
expect(component.getBooleanParamValue(boolParam)).toBe(true);
getItemSpy.and.returnValue('false');
expect(component.getBooleanParamValue(boolParam)).toBe(false);
});
it('should fallback to boolean param value when storage is empty', () => {
spyOn(storage, 'getItem').and.returnValue(null);
expect(component.getBooleanParamValue(boolParam)).toBe(true);
});
it('should render categories as expansion panels', async () => {
spyOn(component, 'reset').and.stub();
appExtensions.settingGroups = [
{
id: 'group1',
name: 'Group 1',
parameters: []
},
{
id: 'group2',
name: 'Group 2',
parameters: []
}
];
fixture.detectChanges();
await fixture.whenStable();
const panels = fixture.debugElement.queryAll(
By.css('.mat-expansion-panel')
);
expect(panels.length).toBe(3);
});
});

View File

@ -2,6 +2,11 @@
$background: map-get($theme, background);
$app-menu-height: 64px;
.aca-settings-parameter-list {
display: flex;
flex-direction: column;
}
.aca-settings {
&-extensions-list {
display: flex;

View File

@ -30,19 +30,18 @@ import {
OauthConfigModel
} from '@alfresco/adf-core';
import { Validators, FormGroup, FormBuilder } from '@angular/forms';
import { Observable, BehaviorSubject } from 'rxjs';
import { Observable } from 'rxjs';
import { Store } from '@ngrx/store';
import { MatCheckboxChange } from '@angular/material/checkbox';
import {
AppStore,
SetLanguagePickerAction,
getHeaderColor,
getAppName,
getUserProfile,
getLanguagePickerState,
ToggleProcessServicesAction
SetSettingsParameterAction
} from '@alfresco/aca-shared/store';
import { ProfileState } from '@alfresco/adf-extensions';
import { AppExtensionService } from '../../extensions/extension.service';
import { SettingsGroupRef, SettingsParameterRef } from '../../types';
interface RepositoryConfig {
ecmHost: string;
@ -64,11 +63,13 @@ export class SettingsComponent implements OnInit {
profile$: Observable<ProfileState>;
appName$: Observable<string>;
headerColor$: Observable<string>;
languagePicker$: Observable<boolean>;
aiExtensions$: Observable<boolean>;
psExtensions$: Observable<boolean>;
get settingGroups(): SettingsGroupRef[] {
return this.appExtensions.settingGroups;
}
constructor(
private appExtensions: AppExtensionService,
private store: Store<AppStore>,
private appConfig: AppConfigService,
private storage: StorageService,
@ -76,23 +77,14 @@ export class SettingsComponent implements OnInit {
) {
this.profile$ = store.select(getUserProfile);
this.appName$ = store.select(getAppName);
this.languagePicker$ = store.select(getLanguagePickerState);
this.headerColor$ = store.select(getHeaderColor);
}
get logo() {
get logo(): string {
return this.appConfig.get('application.logo', this.defaultPath);
}
ngOnInit() {
this.aiExtensions$ = new BehaviorSubject(
this.storage.getItem('ai') === 'true'
);
this.psExtensions$ = new BehaviorSubject(
this.storage.getItem('processServices') === 'true'
);
this.form = this.fb.group({
ecmHost: [
'',
@ -139,17 +131,33 @@ export class SettingsComponent implements OnInit {
});
}
onLanguagePickerValueChanged(event: MatCheckboxChange) {
this.storage.setItem('languagePicker', event.checked.toString());
this.store.dispatch(new SetLanguagePickerAction(event.checked));
getStringParamValue(param: SettingsParameterRef): string {
return this.storage.getItem(param.key) || param.value;
}
onToggleAiExtensions(event: MatCheckboxChange) {
this.storage.setItem('ai', event.checked.toString());
setParamValue(param: SettingsParameterRef, value: any) {
if (param.value !== value) {
param.value = value;
this.saveToStorage(param);
}
}
onTogglePsExtensions(event: MatCheckboxChange) {
this.storage.setItem('processServices', event.checked.toString());
this.store.dispatch(new ToggleProcessServicesAction(event.checked));
getBooleanParamValue(param: SettingsParameterRef): boolean {
const result = this.storage.getItem(param.key);
if (result) {
return result === 'true';
} else {
return param.value ? true : false;
}
}
private saveToStorage(param: SettingsParameterRef) {
this.storage.setItem(
param.key,
param.value ? param.value.toString() : param.value
);
this.store.dispatch(
new SetSettingsParameterAction({ name: param.key, value: param.value })
);
}
}

View File

@ -31,8 +31,7 @@ import { DomSanitizer } from '@angular/platform-browser';
import {
AppStore,
getRuleContext,
getLanguagePickerState,
getProcessServicesState
getLanguagePickerState
} from '@alfresco/aca-shared/store';
import { NodePermissionService } from '@alfresco/aca-shared';
import {
@ -60,6 +59,7 @@ import { AppConfigService, AuthenticationService } from '@alfresco/adf-core';
import { BehaviorSubject, Observable } from 'rxjs';
import { RepositoryInfo, NodeEntry } from '@alfresco/js-api';
import { ViewerRules } from './viewer.rules';
import { SettingsGroupRef } from '../types';
@Injectable({
providedIn: 'root'
@ -84,6 +84,7 @@ export class AppExtensionService implements RuleContext {
contentMetadata: any;
viewerRules: ViewerRules = {};
userActions: Array<ContentActionRef> = [];
settingGroups: Array<SettingsGroupRef> = [];
documentListPresets: {
files: Array<DocumentListPresetRef>;
@ -111,7 +112,6 @@ export class AppExtensionService implements RuleContext {
repository: RepositoryInfo;
withCredentials: boolean;
languagePicker: boolean;
processServices: boolean;
references$: Observable<ExtensionRef[]>;
@ -137,10 +137,6 @@ export class AppExtensionService implements RuleContext {
this.store.select(getLanguagePickerState).subscribe(result => {
this.languagePicker = result;
});
this.store.select(getProcessServicesState).subscribe(result => {
this.processServices = result;
});
}
async load() {
@ -153,6 +149,12 @@ export class AppExtensionService implements RuleContext {
console.error('Extension configuration not found');
return;
}
this.settingGroups = this.loader.getElements<SettingsGroupRef>(
config,
'settings'
);
this.headerActions = this.loader.getContentActions(
config,
'features.header'

View File

@ -54,8 +54,7 @@ export const INITIAL_APP_STATE: AppState = {
status: <any>{
isQuickShareEnabled: true
}
},
processServices: false
}
};
export const INITIAL_STATE: AppStore = {

View File

@ -30,7 +30,6 @@ import {
NodeActionTypes,
SearchActionTypes,
SetUserProfileAction,
SetLanguagePickerAction,
SetCurrentFolderAction,
SetCurrentUrlAction,
SetInitialStateAction,
@ -38,7 +37,7 @@ import {
SetRepositoryInfoAction,
SetInfoDrawerStateAction,
SetInfoDrawerMetadataAspectAction,
ToggleProcessServicesAction
SetSettingsParameterAction
} from '@alfresco/aca-shared/store';
import { INITIAL_APP_STATE } from '../initial-state';
@ -52,15 +51,18 @@ export function appReducer(
case AppActionTypes.SetInitialState:
newState = Object.assign({}, (<SetInitialStateAction>action).payload);
break;
case AppActionTypes.SetSettingsParameter:
newState = handleSettingsUpdate(
state,
action as SetSettingsParameterAction
);
break;
case NodeActionTypes.SetSelection:
newState = updateSelectedNodes(state, <SetSelectedNodesAction>action);
break;
case AppActionTypes.SetUserProfile:
newState = updateUser(state, <SetUserProfileAction>action);
break;
case AppActionTypes.SetLanguagePicker:
newState = updateLanguagePicker(state, <SetLanguagePickerAction>action);
break;
case AppActionTypes.SetCurrentFolder:
newState = updateCurrentFolder(state, <SetCurrentFolderAction>action);
break;
@ -93,11 +95,6 @@ export function appReducer(
case SearchActionTypes.HideFilter:
newState = hideSearchFilter(state);
break;
case AppActionTypes.ToggleProcessServices:
newState = updateProcessServices(state, <ToggleProcessServicesAction>(
action
));
break;
default:
newState = Object.assign({}, state);
}
@ -123,15 +120,6 @@ function showSearchFilter(state: AppState): AppState {
return newState;
}
function updateLanguagePicker(
state: AppState,
action: SetLanguagePickerAction
): AppState {
const newState = Object.assign({}, state);
newState.languagePicker = action.payload;
return newState;
}
function updateUser(state: AppState, action: SetUserProfileAction): AppState {
const newState = Object.assign({}, state);
const user = action.payload.person;
@ -275,11 +263,15 @@ function updateRepositoryStatus(
return newState;
}
function updateProcessServices(
function handleSettingsUpdate(
state: AppState,
action: ToggleProcessServicesAction
) {
const newState = Object.assign({}, state);
newState.processServices = action.payload;
action: SetSettingsParameterAction
): AppState {
const newState = { ...state };
const { payload } = action;
if (payload.name === 'languagePicker') {
newState.languagePicker = payload.value ? true : false;
}
return newState;
}

38
src/app/types.ts Normal file
View File

@ -0,0 +1,38 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2020 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
export interface SettingsGroupRef {
id: string;
name: string;
parameters: Array<SettingsParameterRef>;
}
export interface SettingsParameterRef {
id?: string;
name: string;
key: string;
type: 'string' | 'boolean';
value?: any;
}

View File

@ -9,6 +9,45 @@
"$description": "Core application extensions and features",
"$references": ["aos.plugin.json", "app.header.json"],
"settings": [
{
"id": "app.settings",
"name": "APP.SETTINGS.APPLICATION-SETTINGS",
"parameters": [
{
"name": "Language Picker",
"key": "languagePicker",
"type": "boolean",
"value": true
}
]
},
{
"id": "extensions.ai.settings",
"name": "Extensions: AI",
"parameters": [
{
"name": "Enable AI Extensions",
"key": "ai",
"type": "boolean",
"value": false
}
]
},
{
"id": "extensions.ps.settings",
"name": "Extensions: Process Services",
"parameters": [
{
"name": "Enable Process Services Extensions",
"key": "processServices",
"type": "boolean",
"value": false
}
]
}
],
"rules": [
{
"id": "app.toolbar.favorite.canAdd",