diff --git a/front-end/pom.xml b/front-end/pom.xml
index 1a067cd27..b3234b717 100644
--- a/front-end/pom.xml
+++ b/front-end/pom.xml
@@ -11,7 +11,7 @@
pomapicurio-studio-fe
- app
+ studioservlet
\ No newline at end of file
diff --git a/front-end/servlet/pom.xml b/front-end/servlet/pom.xml
index 0f08ae611..3661e113e 100644
--- a/front-end/servlet/pom.xml
+++ b/front-end/servlet/pom.xml
@@ -59,7 +59,7 @@
${project.groupId}
- apicurio-studio-fe-app
+ apicurio-studio-fe-studio${project.version}provided
diff --git a/front-end/studio/.angular-cli.json b/front-end/studio/.angular-cli.json
new file mode 100644
index 000000000..deb06a774
--- /dev/null
+++ b/front-end/studio/.angular-cli.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "project": {
+ "name": "studio"
+ },
+ "apps": [
+ {
+ "root": "src",
+ "outDir": "dist",
+ "assets": [
+ "assets",
+ "keycloak.json",
+ "keycloak.js",
+ "version.js",
+ "config.js"
+ ],
+ "index": "index.html",
+ "main": "main.ts",
+ "polyfills": "polyfills.ts",
+ "tsconfig": "tsconfig.app.json",
+ "prefix": "app",
+ "styles": [
+ "styles.css"
+ ],
+ "scripts": [],
+ "environmentSource": "environments/environment.ts",
+ "environments": {
+ "dev": "environments/environment.ts",
+ "prod": "environments/environment.prod.ts"
+ }
+ }
+ ],
+ "defaults": {
+ "styleExt": "css",
+ "component": {}
+ }
+}
diff --git a/front-end/studio/.gitignore b/front-end/studio/.gitignore
new file mode 100644
index 000000000..46223375c
--- /dev/null
+++ b/front-end/studio/.gitignore
@@ -0,0 +1,47 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+/deploy-prod.sh
+/serve.sh
+/node
+
+# compiled output
+/dist
+/dist-server
+/tmp
+/out-tsc
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+testem.log
+/typings
+
+# e2e
+/e2e/*.js
+/e2e/*.map
+
+# System Files
+.DS_Store
+Thumbs.db
diff --git a/front-end/studio/README.md b/front-end/studio/README.md
new file mode 100644
index 000000000..541caecd3
--- /dev/null
+++ b/front-end/studio/README.md
@@ -0,0 +1,16 @@
+# Apicurio Studio
+
+This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.6.8.
+
+## Development server
+
+Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically
+reload if you change any of the source files.
+
+To run on a different port, try `ng serve --port 8888` for example. And if you want AOT enabled,
+you can run `ng serve --aot --port 8888`.
+
+## Build
+
+Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
+Use the `-prod` flag for a production build.
diff --git a/front-end/studio/package.json b/front-end/studio/package.json
new file mode 100644
index 000000000..f11619198
--- /dev/null
+++ b/front-end/studio/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "apicurio-design-studio",
+ "version": "1.0.0",
+ "description": "A web application to help design restful APIs.",
+ "author": "Eric Wittmann",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/apicurio/apicurio-studio"
+ },
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve --port 8888 --aot",
+ "build": "ng build -prod"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "^5.2.0",
+ "@angular/common": "^5.2.0",
+ "@angular/compiler": "^5.2.0",
+ "@angular/core": "^5.2.0",
+ "@angular/forms": "^5.2.0",
+ "@angular/platform-browser": "^5.2.0",
+ "@angular/platform-browser-dynamic": "^5.2.0",
+ "@angular/router": "^5.2.0",
+ "oai-ts-core": "0.2.9",
+ "oai-ts-commands": "0.2.18",
+ "core-js": "^2.4.1",
+ "ngx-bootstrap": "2.0.2",
+ "ng2-ace-editor": "0.3.4",
+ "yamljs": "0.3.0",
+ "rxjs": "^5.5.6",
+ "zone.js": "^0.8.19"
+ },
+ "devDependencies": {
+ "@angular/cli": "1.6.8",
+ "@angular/compiler-cli": "^5.2.0",
+ "@angular/language-service": "^5.2.0",
+ "@types/node": "~6.0.60",
+ "ts-node": "~4.1.0",
+ "typescript": "~2.5.3"
+ }
+}
diff --git a/front-end/studio/pom.xml b/front-end/studio/pom.xml
new file mode 100644
index 000000000..f67a93dfe
--- /dev/null
+++ b/front-end/studio/pom.xml
@@ -0,0 +1,84 @@
+
+ 4.0.0
+
+ io.apicurio
+ apicurio-studio-fe
+ 0.2.3-SNAPSHOT
+ ../pom.xml
+
+ apicurio-studio-fe-studio
+ jar
+ apicurio-studio-fe-studio
+
+
+
+
+ com.github.eirslett
+ frontend-maven-plugin
+
+
+
+ install node and yarn
+
+ install-node-and-yarn
+
+
+ v6.11.2
+ v1.3.2
+
+
+
+
+ yarn install
+
+ yarn
+
+
+ install
+
+
+
+
+ ng build
+
+ yarn
+
+ generate-resources
+
+ build
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+
+
+ copy-dist
+ process-resources
+
+ copy-resources
+
+
+ ${project.build.outputDirectory}
+
+
+ ${basedir}/dist
+ false
+
+ config.js
+ keycloak.js
+ keycloak.json
+ version.js
+
+
+
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/.gitignore b/front-end/studio/src/.gitignore
new file mode 100644
index 000000000..1a5bd6424
--- /dev/null
+++ b/front-end/studio/src/.gitignore
@@ -0,0 +1,3 @@
+keycloak.*
+config.js
+
diff --git a/front-end/studio/src/app/app-routing.module.ts b/front-end/studio/src/app/app-routing.module.ts
new file mode 100644
index 000000000..ddee3aec7
--- /dev/null
+++ b/front-end/studio/src/app/app-routing.module.ts
@@ -0,0 +1,108 @@
+/**
+ * @license
+ * Copyright 2018 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {DashboardPageComponent} from './pages/dashboard/dashboard.page';
+import {AuthenticationCanActivateGuard} from './guards/auth.guard';
+import {SettingsPageComponent} from './pages/settings/settings';
+import {ProfilePageComponent} from './pages/settings/profile/profile.page';
+import {LinkedAccountsPageComponent} from './pages/settings/accounts/accounts.page';
+import {CreatedLinkedAccountPageComponent} from './pages/settings/accounts/{accountType}/created/created.page';
+import {ApisPageComponent} from './pages/apis/apis.page';
+import {CreateApiPageComponent} from './pages/apis/create/create.page';
+import {ImportApiPageComponent} from './pages/apis/import/import.page';
+import {ApiDetailPageComponent} from './pages/apis/{apiId}/api-detail.page';
+import {ApiCollaborationPageComponent} from './pages/apis/{apiId}/collaboration/api-collaboration.page';
+import {ApiAcceptPageComponent} from './pages/apis/{apiId}/collaboration/accept/api-accept.page';
+import {ApiEditorPageComponent, ApiEditorPageGuard} from './pages/apis/{apiId}/editor/api-editor.page';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: DashboardPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "settings",
+ component: SettingsPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "settings/profile",
+ component: ProfilePageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "settings/accounts",
+ component: LinkedAccountsPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "settings/accounts/:accountType/created",
+ component: CreatedLinkedAccountPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "apis",
+ component: ApisPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "apis/create",
+ component: CreateApiPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "apis/import",
+ component: ImportApiPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "apis/:apiId",
+ component: ApiDetailPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "apis/:apiId/collaboration",
+ component: ApiCollaborationPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "apis/:apiId/collaboration/accept/:inviteId",
+ component: ApiAcceptPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+ {
+ path: "apis/:apiId/editor",
+ component: ApiEditorPageComponent,
+ canActivate: [ AuthenticationCanActivateGuard ]
+ },
+];
+
+
+@NgModule({
+ imports: [
+ RouterModule.forRoot(routes)
+ ],
+ exports: [
+ RouterModule
+ ],
+ declarations: []
+})
+export class AppRoutingModule {
+}
diff --git a/front-end/studio/src/app/app.component.css b/front-end/studio/src/app/app.component.css
new file mode 100644
index 000000000..c7b9e62aa
--- /dev/null
+++ b/front-end/studio/src/app/app.component.css
@@ -0,0 +1,3 @@
+.studio-component {
+ color: inherit;
+}
diff --git a/front-end/studio/src/app/app.component.html b/front-end/studio/src/app/app.component.html
new file mode 100644
index 000000000..0a4190d27
--- /dev/null
+++ b/front-end/studio/src/app/app.component.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/app.component.ts b/front-end/studio/src/app/app.component.ts
new file mode 100644
index 000000000..b2fb47f80
--- /dev/null
+++ b/front-end/studio/src/app/app.component.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component } from '@angular/core';
+import {IAuthenticationService} from './services/auth.service';
+
+@Component({
+ moduleId: module.id,
+ selector: "apicurio-studio",
+ templateUrl: "app.component.html",
+ styleUrls: [ "app.component.css" ]
+})
+export class AppComponent {
+
+ public routerOutletWrapperId: string;
+ public routerOutletWrapperClass: string;
+
+ constructor(public authService: IAuthenticationService) {
+ this.routerOutletWrapperId = "api-page-body";
+ this.routerOutletWrapperClass = "";
+
+ authService.isAuthenticated().subscribe(authed => {
+ if (authed) {
+ this.routerOutletWrapperId = "api-page-body";
+ this.routerOutletWrapperClass = "";
+ } else {
+ this.routerOutletWrapperId = "login-form";
+ this.routerOutletWrapperClass = "login-pf";
+ }
+ });
+ }
+
+}
diff --git a/front-end/studio/src/app/app.module.ts b/front-end/studio/src/app/app.module.ts
new file mode 100644
index 000000000..11c12d5ff
--- /dev/null
+++ b/front-end/studio/src/app/app.module.ts
@@ -0,0 +1,131 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {BrowserModule} from '@angular/platform-browser';
+import {NgModule} from '@angular/core';
+import {AppComponent} from './app.component';
+import {FormsModule} from '@angular/forms';
+import {HttpClientModule} from '@angular/common/http';
+import {ApisServiceProvider} from './services/apis.service.provider';
+import {LinkedAccountsServiceProvider} from './services/accounts.service.provider';
+import {AuthenticationServiceProvider} from './services/auth.service.provider';
+import {ConfigService} from './services/config.service';
+import {AuthenticationCanActivateGuard} from './guards/auth.guard';
+import {DashboardPageComponent} from './pages/dashboard/dashboard.page';
+import {BreadcrumbsComponent} from './components/breadcrumbs/breadcrumbs.component';
+import {BreadcrumbComponent} from './components/breadcrumbs/breadcrumb.component';
+import {PageErrorComponent} from './components/page-error.component';
+import {VerticalNavComponent} from './components/vertical-nav.component';
+import {NavHeaderComponent} from './components/nav-header.component';
+import {AppRoutingModule} from './app-routing.module';
+import {ConfirmDeleteDialogComponent} from './components/dialogs/confirm-delete.component';
+import {CopyUrlDialogComponent} from './components/dialogs/copy-url.component';
+import {BsDropdownModule, ModalModule} from 'ngx-bootstrap';
+import {DivAutoHeight, TextAreaAutosize, TextBoxAutosize} from './directives/autosize.directive';
+import {NotFoundPageComponent} from './pages/404.page';
+import {SettingsNavComponent} from './pages/settings/_components/settings-nav.component';
+import {CreatedLinkedAccountPageComponent} from './pages/settings/accounts/{accountType}/created/created.page';
+import {LinkedAccountsPageComponent} from './pages/settings/accounts/accounts.page';
+import {ProfilePageComponent} from './pages/settings/profile/profile.page';
+import {SettingsPageComponent} from './pages/settings/settings';
+import {ApisPageComponent} from './pages/apis/apis.page';
+import {CreateApiPageComponent} from './pages/apis/create/create.page';
+import {ImportApiPageComponent} from './pages/apis/import/import.page';
+import {ImportApiFormComponent} from './pages/apis/import/_components/import-form.component';
+import {CreateApiFormComponent} from './pages/apis/create/_components/create-form.component';
+import {ApisListComponent} from './pages/apis/_components/apis-list.component';
+import {ApisCardsComponent} from './pages/apis/_components/apis-cards.component';
+import {DropDownComponent} from './components/common/drop-down.component';
+import {AceEditorModule} from 'ng2-ace-editor';
+import {ActivityItemComponent} from './pages/apis/{apiId}/_components/activity-item.component';
+import {ApiCollaborationPageComponent} from './pages/apis/{apiId}/collaboration/api-collaboration.page';
+import {ApiAcceptPageComponent} from './pages/apis/{apiId}/collaboration/accept/api-accept.page';
+import {ApiDetailPageComponent} from './pages/apis/{apiId}/api-detail.page';
+import {ValidationIconComponent} from './pages/apis/{apiId}/editor/_components/common/validation-icon.component';
+import {ServerUrlComponent} from './pages/apis/{apiId}/editor/_components/common/server-url.component';
+import {SearchComponent} from './pages/apis/{apiId}/editor/_components/common/search.component';
+import {InlineTextEditorComponent} from './pages/apis/{apiId}/editor/_components/common/inline-text-editor.component';
+import {SchemaTypeComponent} from './pages/apis/{apiId}/editor/_components/common/schema-type.component';
+import {ResponseItemComponent} from './pages/apis/{apiId}/editor/_components/common/response-item.component';
+import {PathItemComponent} from './pages/apis/{apiId}/editor/_components/common/path-item.component';
+import {InlineTextAreaComponent} from './pages/apis/{apiId}/editor/_components/common/inline-textarea-editor.component';
+import {ContextHelpComponent} from './pages/apis/{apiId}/editor/_components/common/context-help.component';
+import {SecurityScheme30DialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/security-scheme-30.component';
+import {SetLicenseDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/set-license.component';
+import {SetContactDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/set-contact.component';
+import {SecurityScheme20DialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/security-scheme-20.component';
+import {AddTagDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/add-tag.component';
+import {AddSchemaPropertyDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/add-schema-property.component';
+import {EditorDisconnectedDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/editor-disconnected.component';
+import {AddServerDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/add-server.component';
+import {CloneDefinitionDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/clone-definition.component';
+import {ClonePathDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/clone-path.component';
+import {ResponseRow30Component} from './pages/apis/{apiId}/editor/_components/forms/operation/response-row-30.component';
+import {ResponseRowComponent} from './pages/apis/{apiId}/editor/_components/forms/operation/response-row.component';
+import {ParamRowComponent} from './pages/apis/{apiId}/editor/_components/forms/operation/param-row.component';
+import {OperationFormComponent} from './pages/apis/{apiId}/editor/_components/forms/operation-form.component';
+import {ContentComponent} from './pages/apis/{apiId}/editor/_components/forms/operation/content.component';
+import {Operation30FormComponent} from './pages/apis/{apiId}/editor/_components/forms/operation-30-form.component';
+import {PropertyRowComponent} from './pages/apis/{apiId}/editor/_components/forms/definition/property-row.component';
+import {ServersSectionComponent} from './pages/apis/{apiId}/editor/_components/forms/shared/servers.component';
+import {DefinitionItemComponent} from './pages/apis/{apiId}/editor/_components/forms/definition-item.component';
+import {DefinitionFormComponent} from './pages/apis/{apiId}/editor/_components/forms/definition-form.component';
+import {ProblemFormComponent} from './pages/apis/{apiId}/editor/_components/forms/problem-form.component';
+import {PathFormComponent} from './pages/apis/{apiId}/editor/_components/forms/path-form.component';
+import {EditorMasterComponent} from './pages/apis/{apiId}/editor/_components/master.component';
+import {ApiEditorPageComponent} from './pages/apis/{apiId}/editor/api-editor.page';
+import {ApiEditorComponent} from './pages/apis/{apiId}/editor/editor.component';
+import {AddQueryParamDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/add-query-param.component';
+import {AddPathDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/add-path.component';
+import {AddResponseDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/add-response.component';
+import {AddFormDataParamDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/add-formData-param.component';
+import {AddDefinitionDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/add-definition.component';
+import {Main20FormComponent, Main30FormComponent} from './pages/apis/{apiId}/editor/_components/forms/main-form.component';
+import {AddMediaTypeDialogComponent} from './pages/apis/{apiId}/editor/_components/dialogs/add-media-type.component';
+
+
+@NgModule({
+ imports: [
+ BrowserModule, FormsModule, HttpClientModule, AppRoutingModule, ModalModule.forRoot(), BsDropdownModule.forRoot(),
+ AceEditorModule
+ ],
+ declarations: [
+ AppComponent, DashboardPageComponent, BreadcrumbsComponent, BreadcrumbComponent, PageErrorComponent,
+ VerticalNavComponent, NavHeaderComponent, ConfirmDeleteDialogComponent, CopyUrlDialogComponent,
+ TextAreaAutosize, DivAutoHeight, TextBoxAutosize, NotFoundPageComponent, SettingsNavComponent,
+ CreatedLinkedAccountPageComponent, LinkedAccountsPageComponent, ProfilePageComponent, SettingsPageComponent,
+ ApisPageComponent, CreateApiPageComponent, ImportApiPageComponent, ImportApiFormComponent, CreateApiFormComponent,
+ ApisListComponent, ApisCardsComponent, DropDownComponent, ActivityItemComponent, ApiCollaborationPageComponent,
+ ApiAcceptPageComponent, ApiDetailPageComponent, ValidationIconComponent, ServerUrlComponent, SearchComponent,
+ SchemaTypeComponent, ResponseItemComponent, PathItemComponent, InlineTextAreaComponent, InlineTextEditorComponent,
+ ContextHelpComponent, SetLicenseDialogComponent, SetContactDialogComponent, SecurityScheme30DialogComponent,
+ SecurityScheme20DialogComponent, EditorDisconnectedDialogComponent, ClonePathDialogComponent, CloneDefinitionDialogComponent,
+ AddTagDialogComponent, AddServerDialogComponent, AddSchemaPropertyDialogComponent, ResponseRow30Component, ResponseRowComponent,
+ ParamRowComponent, ContentComponent, PropertyRowComponent, ServersSectionComponent, ProblemFormComponent, PathFormComponent,
+ OperationFormComponent, Operation30FormComponent, DefinitionItemComponent, DefinitionFormComponent, EditorMasterComponent,
+ ApiEditorPageComponent, ApiEditorComponent, AddQueryParamDialogComponent, AddPathDialogComponent, AddResponseDialogComponent,
+ AddFormDataParamDialogComponent, AddDefinitionDialogComponent, AddMediaTypeDialogComponent, Main20FormComponent,
+ Main30FormComponent
+ ],
+ providers: [
+ ApisServiceProvider, LinkedAccountsServiceProvider, AuthenticationServiceProvider, ConfigService,
+ AuthenticationCanActivateGuard
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule {
+}
diff --git a/front-end/studio/src/app/components/breadcrumbs/breadcrumb.component.css b/front-end/studio/src/app/components/breadcrumbs/breadcrumb.component.css
new file mode 100644
index 000000000..11cd498be
--- /dev/null
+++ b/front-end/studio/src/app/components/breadcrumbs/breadcrumb.component.css
@@ -0,0 +1,3 @@
+.api-breadcrumb {
+ color: inherit;
+}
diff --git a/front-end/studio/src/app/components/breadcrumbs/breadcrumb.component.html b/front-end/studio/src/app/components/breadcrumbs/breadcrumb.component.html
new file mode 100644
index 000000000..708dbf0fa
--- /dev/null
+++ b/front-end/studio/src/app/components/breadcrumbs/breadcrumb.component.html
@@ -0,0 +1,2 @@
+{{ label }}
+{{ label }}
diff --git a/front-end/studio/src/app/components/breadcrumbs/breadcrumb.component.ts b/front-end/studio/src/app/components/breadcrumbs/breadcrumb.component.ts
new file mode 100644
index 000000000..69c82b2df
--- /dev/null
+++ b/front-end/studio/src/app/components/breadcrumbs/breadcrumb.component.ts
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Input} from "@angular/core";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "[breadcrumb]",
+ templateUrl: "breadcrumb.component.html",
+ styleUrls: ["breadcrumb.component.css"]
+})
+export class BreadcrumbComponent {
+
+ @Input() label: string;
+ @Input() icon: string;
+ @Input() route: string[];
+
+}
diff --git a/front-end/studio/src/app/components/breadcrumbs/breadcrumbs.component.css b/front-end/studio/src/app/components/breadcrumbs/breadcrumbs.component.css
new file mode 100644
index 000000000..132e58f1f
--- /dev/null
+++ b/front-end/studio/src/app/components/breadcrumbs/breadcrumbs.component.css
@@ -0,0 +1,8 @@
+
+.api-breadcrumbs {
+ float: left;
+}
+.api-breadcrumbs .breadcrumb {
+ margin-bottom: 0;
+ float: left;
+}
diff --git a/front-end/studio/src/app/components/breadcrumbs/breadcrumbs.component.html b/front-end/studio/src/app/components/breadcrumbs/breadcrumbs.component.html
new file mode 100644
index 000000000..56bc66223
--- /dev/null
+++ b/front-end/studio/src/app/components/breadcrumbs/breadcrumbs.component.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/components/breadcrumbs/breadcrumbs.component.ts b/front-end/studio/src/app/components/breadcrumbs/breadcrumbs.component.ts
new file mode 100644
index 000000000..68c27a8c8
--- /dev/null
+++ b/front-end/studio/src/app/components/breadcrumbs/breadcrumbs.component.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component} from "@angular/core";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "breadcrumbs",
+ templateUrl: "breadcrumbs.component.html",
+ styleUrls: ["breadcrumbs.component.css"]
+})
+export class BreadcrumbsComponent {
+
+}
diff --git a/front-end/studio/src/app/components/common/drop-down.component.html b/front-end/studio/src/app/components/common/drop-down.component.html
new file mode 100644
index 000000000..51c588e95
--- /dev/null
+++ b/front-end/studio/src/app/components/common/drop-down.component.html
@@ -0,0 +1,10 @@
+
diff --git a/front-end/studio/src/app/components/common/drop-down.component.ts b/front-end/studio/src/app/components/common/drop-down.component.ts
new file mode 100644
index 000000000..385a25bfa
--- /dev/null
+++ b/front-end/studio/src/app/components/common/drop-down.component.ts
@@ -0,0 +1,84 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Input, Output} from "@angular/core";
+
+
+export class DropDownOption {
+ name?: string;
+ value?: string;
+ divider?: boolean;
+}
+
+
+@Component({
+ moduleId: module.id,
+ selector: "drop-down",
+ templateUrl: "drop-down.component.html"
+})
+export class DropDownComponent {
+
+ public _open: boolean = false;
+
+ @Input() id: string;
+ @Input() value: string;
+ @Input() options: DropDownOption[];
+ @Input() noSelectionLabel: string = "No Selection";
+
+ @Output() onValueChange: EventEmitter = new EventEmitter();
+
+ public toggle(): void {
+ this._open = !this._open;
+ }
+
+ public open(): void {
+ this._open = true;
+ }
+
+ public close(): void {
+ this._open = false;
+ }
+
+ public isOpen(): boolean {
+ return this._open;
+ }
+
+ public hasValue(): boolean {
+ for (let option of this.options) {
+ if (!option.divider && option.value === this.value) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public displayValue(): string {
+ for (let option of this.options) {
+ if (!option.divider && option.value === this.value) {
+ return option.name;
+ }
+ }
+ return null;
+ }
+
+ public setValue(value: string): void {
+ this.value = value;
+ //this.close();
+ this.onValueChange.emit(this.value);
+ }
+
+}
diff --git a/front-end/studio/src/app/components/dialogs/confirm-delete.component.html b/front-end/studio/src/app/components/dialogs/confirm-delete.component.html
new file mode 100644
index 000000000..c82a3d3d4
--- /dev/null
+++ b/front-end/studio/src/app/components/dialogs/confirm-delete.component.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
Confirm Delete
+
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/components/dialogs/confirm-delete.component.ts b/front-end/studio/src/app/components/dialogs/confirm-delete.component.ts
new file mode 100644
index 000000000..5f256bf54
--- /dev/null
+++ b/front-end/studio/src/app/components/dialogs/confirm-delete.component.ts
@@ -0,0 +1,77 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Output, EventEmitter, ViewChildren, QueryList} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "confirm-delete-dialog",
+ templateUrl: "confirm-delete.component.html"
+})
+export class ConfirmDeleteDialogComponent {
+
+ @Output() onDelete: EventEmitter = new EventEmitter();
+
+ @ViewChildren("confirmDeleteModal") confirmDeleteModal: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(): void {
+ this._isOpen = true;
+ this.confirmDeleteModal.changes.subscribe( thing => {
+ if (this.confirmDeleteModal.first) {
+ this.confirmDeleteModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ }
+
+ /**
+ * Called when the user clicks "Yes".
+ */
+ protected delete(): void {
+ this.onDelete.emit(true);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.confirmDeleteModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+}
diff --git a/front-end/studio/src/app/components/dialogs/copy-url.component.html b/front-end/studio/src/app/components/dialogs/copy-url.component.html
new file mode 100644
index 000000000..2230a1895
--- /dev/null
+++ b/front-end/studio/src/app/components/dialogs/copy-url.component.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
Copy URL
+
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/components/dialogs/copy-url.component.ts b/front-end/studio/src/app/components/dialogs/copy-url.component.ts
new file mode 100644
index 000000000..b5aee0b49
--- /dev/null
+++ b/front-end/studio/src/app/components/dialogs/copy-url.component.ts
@@ -0,0 +1,67 @@
+/**
+ * @license
+ * Copyright 2018 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "copy-url-dialog",
+ templateUrl: "copy-url.component.html"
+})
+export class CopyUrlDialogComponent {
+
+ @ViewChildren("copyUrlModal") copyUrlModal: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(): void {
+ this._isOpen = true;
+ this.copyUrlModal.changes.subscribe( thing => {
+ if (this.copyUrlModal.first) {
+ this.copyUrlModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.copyUrlModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+}
diff --git a/front-end/studio/src/app/components/dimensions.less b/front-end/studio/src/app/components/dimensions.less
new file mode 100644
index 000000000..2eab7c0d9
--- /dev/null
+++ b/front-end/studio/src/app/components/dimensions.less
@@ -0,0 +1,2 @@
+@navbar-height: 48px;
+@navbar-logo-size: 30px;
diff --git a/front-end/studio/src/app/components/nav-header.component.css b/front-end/studio/src/app/components/nav-header.component.css
new file mode 100644
index 000000000..90c6b590f
--- /dev/null
+++ b/front-end/studio/src/app/components/nav-header.component.css
@@ -0,0 +1,54 @@
+
+.navbar {
+ height: 48px;
+}
+.navbar .navbar-header {
+ height: 45px;
+}
+
+.navbar .navbar-brand {
+ padding: 0;
+}
+.navbar .navbar-brand .navbar-brand-logo {
+ line-height: 45px;
+ font-family: 'Lato', sans-serif;
+ font-size: 30px;
+ font-weight: 300;
+ color: #DCD7D7;
+ padding-left: 40px;
+ background-image: url('../../assets/apicurio_icon_darkbkg_32px.png');
+ background-repeat: no-repeat;
+ background-position-y: 6px;
+}
+.navbar .navbar-brand .navbar-brand-logo:hover {
+ color: rgb(203, 37, 38);
+}
+
+.navbar a.dropdown-toggle {
+ height: 45px;
+ line-height: 34px;
+ font-size: 13px;
+ vertical-align: middle;
+}
+.navbar a.dropdown-toggle .pficon {
+ position: initial;
+}
+
+.navbar .dropdown-menu a {
+ cursor: pointer;
+}
+.navbar .dropdown-menu a.disabled {
+ cursor: not-allowed;
+ color: #999;
+}
+
+.product-versions-pf table tr td {
+ color: white;
+ font-size: 13px;
+ padding-right: 10px;
+}
+
+.about-title {
+ font-size: 32px;
+ font-weight: 600;
+}
diff --git a/front-end/studio/src/app/components/nav-header.component.html b/front-end/studio/src/app/components/nav-header.component.html
new file mode 100644
index 000000000..9d45e9b27
--- /dev/null
+++ b/front-end/studio/src/app/components/nav-header.component.html
@@ -0,0 +1,61 @@
+
+
diff --git a/front-end/studio/src/app/components/nav-header.component.ts b/front-end/studio/src/app/components/nav-header.component.ts
new file mode 100644
index 000000000..1369c83db
--- /dev/null
+++ b/front-end/studio/src/app/components/nav-header.component.ts
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, OnInit} from "@angular/core";
+import {User} from "../models/user.model";
+import {IAuthenticationService} from "../services/auth.service";
+import {Observable} from "rxjs";
+
+@Component({
+ moduleId: module.id,
+ selector: "nav-header",
+ templateUrl: "nav-header.component.html",
+ styleUrls: [ "nav-header.component.css" ]
+})
+export class NavHeaderComponent implements OnInit {
+
+ version: string = "N/A";
+ builtOn: Date = new Date();
+ projectUrl: string = "http://www.apicur.io/";
+
+ constructor(protected authService: IAuthenticationService) {
+ let w: any = window;
+ if (w["ApicurioStudioInfo"]) {
+ console.info("[NavHeaderComponent] Found app info: %o", w["ApicurioStudioInfo"]);
+ this.version = w["ApicurioStudioInfo"].version;
+ this.builtOn = new Date(w["ApicurioStudioInfo"].builtOn);
+ this.projectUrl = w["ApicurioStudioInfo"].url;
+ } else {
+ console.info("[NavHeaderComponent] App info not found.");
+ }
+ }
+
+ ngOnInit(): void {
+ }
+
+ public user(): Observable {
+ return this.authService.getAuthenticatedUser();
+ }
+
+ public logout(): void {
+ this.authService.logout();
+ }
+
+}
diff --git a/front-end/studio/src/app/components/page-base.component.ts b/front-end/studio/src/app/components/page-base.component.ts
new file mode 100644
index 000000000..bbd894be3
--- /dev/null
+++ b/front-end/studio/src/app/components/page-base.component.ts
@@ -0,0 +1,95 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {OnDestroy, OnInit} from "@angular/core";
+import {ActivatedRoute} from "@angular/router";
+import {Observable} from "rxjs/Observable";
+
+export class DataMap {
+ [key: string]: boolean;
+}
+
+export abstract class AbstractPageComponent implements OnInit, OnDestroy {
+
+ public dataLoaded: DataMap = new DataMap();
+ public pageError: any;
+
+ /**
+ * C'tor.
+ * @param {ActivatedRoute} route
+ */
+ constructor(protected route: ActivatedRoute) {
+ }
+
+ /**
+ * Called when the page is initialized. Triggers the loading of asynchronous
+ * page data.
+ */
+ public ngOnInit(): void {
+ // Extract route params and query params and pass them to "loadAsyncPageData"
+ let combined = Observable.combineLatest(this.route.params, this.route.queryParams, (params, qparams) => ({params, qparams}));
+ combined.subscribe( ap => {
+ this.loadAsyncPageData(ap.params, ap.qparams);
+ });
+ }
+
+ /**
+ * Called when the page is destroyed.
+ */
+ public ngOnDestroy(): void {
+ }
+
+ /**
+ * Called to kick off loading the page's async data. Subclasses should
+ * override to provide page-specific data loading.
+ * @param pathParams
+ * @param queryParams
+ */
+ public loadAsyncPageData(pathParams: any, queryParams: any): void {
+ }
+
+ /**
+ * Called by a subclass (page) to report an error during loading of data.
+ * @param error
+ */
+ public error(error: any): void {
+ console.error(" Error: %o", error);
+ this.pageError = error;
+ }
+
+ /**
+ * Called when page data has been loaded.
+ * @param key
+ */
+ public loaded(key: string): void {
+ this.dataLoaded[key] = true;
+ }
+
+ /**
+ * Called to determine whether some page data has been loaded yet.
+ * @param key
+ * @return {boolean}
+ */
+ public isLoaded(key: string): boolean {
+ if (this.dataLoaded[key]) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/front-end/studio/src/app/components/page-error.component.css b/front-end/studio/src/app/components/page-error.component.css
new file mode 100644
index 000000000..4b01f5de1
--- /dev/null
+++ b/front-end/studio/src/app/components/page-error.component.css
@@ -0,0 +1,14 @@
+.card-pf-error {
+ border-top-color: red;
+}
+
+.card-pf-error .card-pf-title .fa {
+ color: red;
+}
+
+.card-pf-error .card-pf-body .details {
+}
+.card-pf-error .card-pf-body .details pre {
+ max-height: 350px;
+ overflow-y: auto;
+}
diff --git a/front-end/studio/src/app/components/page-error.component.html b/front-end/studio/src/app/components/page-error.component.html
new file mode 100644
index 000000000..cd340601d
--- /dev/null
+++ b/front-end/studio/src/app/components/page-error.component.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+ Server Error Encountered
+
+
+
+
+ Uh oh! It looks like there was an error either receiving data from the server or
+ sending data to it. You have a couple options at this point. You can reload the
+ page or we can shoot you back to the Dashboard. Your choice!
+
+ You attempted to navigate to a page that does not exist! I guess you should either
+ hit the back button or go back to the Dashboard (button below).
+
+ It appears you attempted to add or create something that already exists
+ in Apicurio Studio. Instead of adding it again, try finding the resource
+ that is already there and editing it!
+
+ Something unexpected happened and we aren't sure how to deal with it.
+ You could try reloading the page, or you could just go back to the
+ Dashboard. See the buttons below...
+
diff --git a/front-end/studio/src/app/components/vertical-nav.component.ts b/front-end/studio/src/app/components/vertical-nav.component.ts
new file mode 100644
index 000000000..c5b963b95
--- /dev/null
+++ b/front-end/studio/src/app/components/vertical-nav.component.ts
@@ -0,0 +1,103 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, OnInit, Inject} from "@angular/core";
+import {Router, NavigationStart} from "@angular/router";
+import {IApisService} from "../services/apis.service";
+
+/**
+ * Models the sub-menus off the main left-hand vertical nav.
+ */
+export enum VerticalNavSubMenuType {
+ None, Dashboard, APIs, Settings
+}
+
+
+@Component({
+ moduleId: module.id,
+ selector: "vertical-nav",
+ templateUrl: "vertical-nav.component.html",
+ styleUrls: ["vertical-nav.component.css"]
+})
+export class VerticalNavComponent implements OnInit {
+
+ public subMenuTypes: any = VerticalNavSubMenuType;
+ public currentSubMenu: VerticalNavSubMenuType = VerticalNavSubMenuType.None;
+ public subMenuOut: boolean = false;
+
+ constructor(private router: Router, @Inject(IApisService) private apis: IApisService) {
+ }
+
+ ngOnInit(): void {
+ console.log("Subscribing to router events.");
+ this.router.events.subscribe(event => {
+ if (event instanceof NavigationStart) {
+ this.onShadeClick();
+ }
+ });
+ }
+
+ /**
+ * Returns true if the currently active route is the dashboard.
+ * @returns {boolean}
+ */
+ isDashboardRoute(): boolean {
+ return this.router.isActive("/", true);
+ }
+
+ /**
+ * Returns true if the currently active route is /apis/*
+ * @returns {boolean}
+ */
+ isAPIsRoute(): boolean {
+ return this.router.isActive("/apis", false);
+ }
+
+ /**
+ * Returns true if the currently active route is /settings/*
+ * @returns {boolean}
+ */
+ isSettingsRoute(): boolean {
+ return this.router.isActive("/settings", false);
+ }
+
+ /**
+ * Called when the user clicks the vertical menu shade (the grey shaded area behind the submenu div that
+ * is displayed when a sub-menu is selected). Clicking the shade makes the sub-menu div go away.
+ */
+ onShadeClick(): void {
+ this.subMenuOut = false;
+ setTimeout(() => {
+ this.currentSubMenu = VerticalNavSubMenuType.None;
+ }, 180);
+ }
+
+ /**
+ * Toggles a sub-menu off the main vertical left-hand menu bar. If the sub-menu is
+ * already selected, it de-selects it.
+ * @param subMenu the sub-menu to toggle
+ */
+ toggleSubMenu(subMenu: VerticalNavSubMenuType): void {
+ if (this.subMenuOut && this.currentSubMenu === subMenu) {
+ this.onShadeClick();
+ } else {
+ this.currentSubMenu = subMenu;
+ this.subMenuOut = true;
+ }
+ }
+
+}
diff --git a/front-end/studio/src/app/directives/autosize.directive.ts b/front-end/studio/src/app/directives/autosize.directive.ts
new file mode 100644
index 000000000..ce8b60220
--- /dev/null
+++ b/front-end/studio/src/app/directives/autosize.directive.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {AfterContentChecked, AfterViewChecked, Directive, ElementRef, HostListener, Input} from "@angular/core";
+
+@Directive({
+ selector: "textarea[autosize]"
+})
+export class TextAreaAutosize implements AfterContentChecked {
+
+ @HostListener("input", ["$event.target"])
+ onInput(textArea: HTMLTextAreaElement): void {
+ this.adjust();
+ }
+
+ constructor(public element: ElementRef) {}
+
+ ngAfterContentChecked(): void {
+ this.adjust();
+ }
+
+ adjust(): void {
+ this.element.nativeElement.style.overflow = "hidden";
+ this.element.nativeElement.style.height = "auto";
+ this.element.nativeElement.style.height = this.element.nativeElement.scrollHeight + "px";
+ }
+}
+
+
+@Directive({
+ selector: "div[autoheight]"
+})
+export class DivAutoHeight implements AfterViewChecked {
+
+ lastHeight: number = -1;
+
+ @Input() maxHeight: number;
+
+ constructor(public element: ElementRef) {}
+
+ ngAfterViewChecked(): void {
+ this.adjust();
+ }
+
+ adjust(): void {
+ let height: number = this.element.nativeElement.scrollHeight;
+ if (this.maxHeight && height > this.maxHeight) {
+ height = this.maxHeight;
+ }
+ if (Math.abs(height - this.lastHeight) > 5) {
+ this.element.nativeElement.style.height = height + "px";
+ this.lastHeight = height;
+ if (height == this.maxHeight) {
+ this.element.nativeElement.style.overflowY = "auto";
+ } else {
+ this.element.nativeElement.style.overflowY = "visible";
+ }
+ }
+ }
+}
+
+
+@Directive({
+ selector: "input[autosize]"
+})
+export class TextBoxAutosize implements AfterContentChecked {
+
+ @HostListener("input", ["$event.target"])
+ onInput(textInput: HTMLInputElement): void {
+ this.adjust();
+ }
+
+ constructor(public element: ElementRef) {}
+
+ ngAfterContentChecked(): void {
+ this.adjust();
+ }
+
+ adjust(): void {
+ this.element.nativeElement.style.width = "auto";
+ this.element.nativeElement.style.width = this.element.nativeElement.scrollWidth + "px";
+ }
+}
diff --git a/front-end/studio/src/app/guards/auth.guard.ts b/front-end/studio/src/app/guards/auth.guard.ts
new file mode 100644
index 000000000..223516f3f
--- /dev/null
+++ b/front-end/studio/src/app/guards/auth.guard.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Injectable} from "@angular/core";
+import {CanActivate, Router} from "@angular/router";
+import {IAuthenticationService} from "../services/auth.service";
+import {Subscription} from "rxjs";
+
+@Injectable()
+export class AuthenticationCanActivateGuard implements CanActivate {
+
+ private isAuthenticated: boolean;
+ private sub: Subscription;
+
+ constructor(protected authService: IAuthenticationService, private router: Router) {
+ this.sub = authService.isAuthenticated().subscribe(value => {
+ this.isAuthenticated = value;
+ });
+ }
+
+ canActivate() {
+ if (!this.isAuthenticated) {
+ let path: string = location.pathname;
+ let query: string = location.search.substring(1);
+
+ sessionStorage.setItem("apicurio.studio.pages.login.redirect-to.path", path);
+ sessionStorage.setItem("apicurio.studio.pages.login.redirect-to.query", query);
+
+ this.router.navigate(["/login"]);
+ }
+ return this.isAuthenticated;
+ }
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/ack.model.ts b/front-end/studio/src/app/models/ack.model.ts
new file mode 100644
index 000000000..3e4188751
--- /dev/null
+++ b/front-end/studio/src/app/models/ack.model.ts
@@ -0,0 +1,23 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class ApiDesignCommandAck {
+
+ commandId: number;
+ contentVersion: number;
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/api-collaborator.ts b/front-end/studio/src/app/models/api-collaborator.ts
new file mode 100644
index 000000000..a0e830adb
--- /dev/null
+++ b/front-end/studio/src/app/models/api-collaborator.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2018 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+export class ApiCollaborator {
+
+ userId: string;
+ userName: string;
+ role: string;
+
+ constructor() {
+ this.userId = "";
+ this.userName = "";
+ this.role = "";
+ }
+}
+
diff --git a/front-end/studio/src/app/models/api-contributors.ts b/front-end/studio/src/app/models/api-contributors.ts
new file mode 100644
index 000000000..e5fb7a1d8
--- /dev/null
+++ b/front-end/studio/src/app/models/api-contributors.ts
@@ -0,0 +1,37 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+export class ApiContributor {
+ name: string;
+ edits: number;
+
+ constructor() {
+ this.name = "";
+ this.edits = 0;
+ }
+}
+
+export class ApiContributors {
+ public totalEdits: number;
+ public contributors: ApiContributor[];
+
+ constructor() {
+ this.totalEdits = 0;
+ this.contributors = [];
+ }
+}
diff --git a/front-end/studio/src/app/models/api-design-change.ts b/front-end/studio/src/app/models/api-design-change.ts
new file mode 100644
index 000000000..9e41752b2
--- /dev/null
+++ b/front-end/studio/src/app/models/api-design-change.ts
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+export class ApiDesignChange {
+
+ version: number;
+ type: string;
+ data: string;
+ by: string;
+ on: Date;
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/api.model.ts b/front-end/studio/src/app/models/api.model.ts
new file mode 100644
index 000000000..1d4542ff0
--- /dev/null
+++ b/front-end/studio/src/app/models/api.model.ts
@@ -0,0 +1,85 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+export class Api {
+
+ id: string;
+ name: string;
+ description: string;
+ createdOn: Date;
+ createdBy: string;
+ tags: string[];
+
+ constructor() {
+ this.id = "";
+ this.name = "";
+ this.description = "";
+ this.createdOn = new Date();
+ this.createdBy = "";
+ this.tags = null;
+ }
+
+}
+
+
+export class ApiDefinition extends Api {
+
+ spec: any;
+
+ constructor() {
+ super();
+ this.spec = {};
+ }
+
+ public static fromApi(api: Api): ApiDefinition {
+ let apiDef: ApiDefinition = new ApiDefinition();
+ apiDef.id = api.id;
+ apiDef.name = api.name;
+ apiDef.description = api.description;
+ apiDef.createdOn = api.createdOn;
+ apiDef.createdBy = api.createdBy;
+
+ return apiDef;
+ }
+
+}
+
+
+export class EditableApiDefinition extends ApiDefinition {
+
+ editingSessionUuid: string;
+ contentVersion: number;
+
+ constructor() {
+ super();
+ this.editingSessionUuid = null;
+ this.contentVersion = 0;
+ }
+
+ public static fromApi(api: Api): EditableApiDefinition {
+ let apiDef: EditableApiDefinition = new EditableApiDefinition();
+ apiDef.id = api.id;
+ apiDef.name = api.name;
+ apiDef.description = api.description;
+ apiDef.createdOn = api.createdOn;
+ apiDef.createdBy = api.createdBy;
+
+ return apiDef;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/bitbucket-repository.ts b/front-end/studio/src/app/models/bitbucket-repository.ts
new file mode 100644
index 000000000..f51bac976
--- /dev/null
+++ b/front-end/studio/src/app/models/bitbucket-repository.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class BitbucketRepository {
+
+ name: string;
+ slug: string;
+ uuid: boolean;
+
+ constructor() {
+ this.name = null;
+ this.slug = null;
+ this.uuid = false;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/bitbucket-team.ts b/front-end/studio/src/app/models/bitbucket-team.ts
new file mode 100644
index 000000000..4fc6f66bc
--- /dev/null
+++ b/front-end/studio/src/app/models/bitbucket-team.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class BitbucketTeam {
+
+ displayName: string;
+ username: string;
+ uuid: boolean;
+
+ constructor() {
+ this.displayName = null;
+ this.username = null;
+ this.uuid = false;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/complete-linked-account.ts b/front-end/studio/src/app/models/complete-linked-account.ts
new file mode 100644
index 000000000..7b4081b32
--- /dev/null
+++ b/front-end/studio/src/app/models/complete-linked-account.ts
@@ -0,0 +1,26 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class CompleteLinkedAccount {
+
+ nonce: string;
+
+ constructor() {
+ this.nonce = null;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/create-linked-account.ts b/front-end/studio/src/app/models/create-linked-account.ts
new file mode 100644
index 000000000..b947e386c
--- /dev/null
+++ b/front-end/studio/src/app/models/create-linked-account.ts
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class CreateLinkedAccount {
+
+ type: string;
+ redirectUrl: string;
+
+ constructor() {
+ this.type = null;
+ this.redirectUrl = null;
+ }
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/editor-user.model.ts b/front-end/studio/src/app/models/editor-user.model.ts
new file mode 100644
index 000000000..6309dbd70
--- /dev/null
+++ b/front-end/studio/src/app/models/editor-user.model.ts
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Represents a single user currently connected to the editor. The local user
+ * is not represented by one of these - only remote collaborators.
+ */
+export class ApiEditorUser {
+ public userId: string;
+ public userName: string;
+ public attributes: any = {};
+}
+
diff --git a/front-end/studio/src/app/models/github-organization.ts b/front-end/studio/src/app/models/github-organization.ts
new file mode 100644
index 000000000..2b103f429
--- /dev/null
+++ b/front-end/studio/src/app/models/github-organization.ts
@@ -0,0 +1,28 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class GitHubOrganization {
+
+ id: string;
+ userOrg: boolean;
+
+ constructor() {
+ this.id = null;
+ this.userOrg = false;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/github-repository.ts b/front-end/studio/src/app/models/github-repository.ts
new file mode 100644
index 000000000..87aa7a88b
--- /dev/null
+++ b/front-end/studio/src/app/models/github-repository.ts
@@ -0,0 +1,28 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class GitHubRepository {
+
+ name: string;
+ priv: boolean;
+
+ constructor() {
+ this.name = null;
+ this.priv = false;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/gitlab-group.ts b/front-end/studio/src/app/models/gitlab-group.ts
new file mode 100644
index 000000000..bd521959f
--- /dev/null
+++ b/front-end/studio/src/app/models/gitlab-group.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class GitLabGroup {
+
+ id: number;
+ name: string;
+ path: string;
+
+ constructor() {
+ this.id = null;
+ this.name = null;
+ this.path = null;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/gitlab-project.ts b/front-end/studio/src/app/models/gitlab-project.ts
new file mode 100644
index 000000000..087fd7fa5
--- /dev/null
+++ b/front-end/studio/src/app/models/gitlab-project.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class GitLabProject {
+
+ id: number;
+ name: string;
+ path: string;
+
+ constructor() {
+ this.id = null;
+ this.name = null;
+ this.path = null;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/import-api.model.ts b/front-end/studio/src/app/models/import-api.model.ts
new file mode 100644
index 000000000..64f41ca1d
--- /dev/null
+++ b/front-end/studio/src/app/models/import-api.model.ts
@@ -0,0 +1,28 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class ImportApi {
+
+ url: string;
+ data: string;
+
+ constructor() {
+ this.url = null;
+ this.data = null;
+ }
+
+}
diff --git a/front-end/studio/src/app/models/initiated-linked-account.ts b/front-end/studio/src/app/models/initiated-linked-account.ts
new file mode 100644
index 000000000..642cc0298
--- /dev/null
+++ b/front-end/studio/src/app/models/initiated-linked-account.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+export class InitiatedLinkedAccount {
+
+ authUrl: string;
+ nonce: string;
+
+ constructor() {
+ this.authUrl = null;
+ this.nonce = null;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/invitation.ts b/front-end/studio/src/app/models/invitation.ts
new file mode 100644
index 000000000..f6cf3dbb3
--- /dev/null
+++ b/front-end/studio/src/app/models/invitation.ts
@@ -0,0 +1,41 @@
+/**
+ * @license
+ * Copyright 2018 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+export class Invitation {
+
+ inviteId: string;
+ designId: string;
+ createdBy: string;
+ createdOn: Date;
+ status: string;
+ modifiedBy: string;
+ modifiedOn: Date;
+ role: string;
+ subject: string;
+
+ constructor() {
+ this.inviteId = "";
+ this.designId = "";
+ this.createdBy = "";
+ this.createdOn = new Date();
+ this.status = "";
+ this.role = "";
+ this.subject = "";
+ }
+}
+
diff --git a/front-end/studio/src/app/models/linked-account.ts b/front-end/studio/src/app/models/linked-account.ts
new file mode 100644
index 000000000..046f8222c
--- /dev/null
+++ b/front-end/studio/src/app/models/linked-account.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class LinkedAccount {
+
+ type: string;
+ linkedOn: Date;
+ usedOn: Date;
+
+ constructor() {
+ this.type = null;
+ this.linkedOn = null;
+ this.usedOn = null;
+ }
+
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/models/new-api.model.ts b/front-end/studio/src/app/models/new-api.model.ts
new file mode 100644
index 000000000..b2358868c
--- /dev/null
+++ b/front-end/studio/src/app/models/new-api.model.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class NewApi {
+
+ specVersion: string;
+ name: string;
+ description: string;
+
+ constructor() {
+ this.specVersion = null;
+ this.name = "";
+ this.description = "";
+ }
+
+}
diff --git a/front-end/studio/src/app/models/user.model.ts b/front-end/studio/src/app/models/user.model.ts
new file mode 100644
index 000000000..cc6671acc
--- /dev/null
+++ b/front-end/studio/src/app/models/user.model.ts
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+export class User {
+ login: string;
+ id: number;
+ name: string;
+ email: string;
+ avatar: string;
+
+ constructor() {
+ this.login = "";
+ this.name = "";
+ this.email = "";
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/404.page.html b/front-end/studio/src/app/pages/404.page.html
new file mode 100644
index 000000000..42405df94
--- /dev/null
+++ b/front-end/studio/src/app/pages/404.page.html
@@ -0,0 +1,5 @@
+
+
+
Page Not Found
+
+
\ No newline at end of file
diff --git a/front-end/studio/src/app/pages/404.page.ts b/front-end/studio/src/app/pages/404.page.ts
new file mode 100644
index 000000000..5d433a5c3
--- /dev/null
+++ b/front-end/studio/src/app/pages/404.page.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component} from "@angular/core";
+import {AbstractPageComponent} from "../components/page-base.component";
+import {ActivatedRoute} from "@angular/router";
+
+/**
+ * The Not Found (404) Page component - shown when navigating to a route that does not exist.
+ */
+@Component({
+ moduleId: module.id,
+ selector: "not-found-page",
+ templateUrl: "404.page.html"
+})
+export class NotFoundPageComponent extends AbstractPageComponent {
+
+ /**
+ * C'tor.
+ * @param {ActivatedRoute} route
+ */
+ constructor(route: ActivatedRoute) {
+ super(route);
+ }
+
+ /**
+ * @see AbstractPageComponent.loadAsyncPageData
+ */
+ public loadAsyncPageData(): void {
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/_components/apis-cards.component.css b/front-end/studio/src/app/pages/apis/_components/apis-cards.component.css
new file mode 100644
index 000000000..262f1d612
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/_components/apis-cards.component.css
@@ -0,0 +1,65 @@
+.container-cards-pf {
+ padding: 0;
+ margin-top: 0;
+}
+
+.row-cards-pf {
+ padding: 0;
+}
+
+
+.api-card-icon {
+ border: 2px solid #39a5dc;
+ border-radius: 50%;
+ padding: 5px;
+ margin-right: 10px;
+ width: 32px;
+}
+
+.api-card .api-card-title {
+ font-size: 16px;
+ font-weight: bold;
+}
+
+.api-card {
+ -webkit-transition: background-color 300ms;
+ -moz-transition: background-color 300ms;
+ -ms-transition: background-color 300ms;
+ -o-transition: background-color 300ms;
+ transition: background-color 300ms;
+ height: 220px;
+}
+.api-card:hover {
+ background-color: rgb(237, 237, 237);
+}
+
+.api-card.active {
+ background-color: rgb(221, 234, 255);
+}
+
+.api-description {
+ font-size: 13px;
+ overflow-y: auto;
+}
+
+.api-card .api-tags {
+ margin-bottom: 8px;
+}
+.api-card .api-tags .api-tags-label {
+ font-weight: bold;
+ margin-right: 5px;
+}
+.api-card .api-tags .api-tag {
+ margin-right: 5px;
+ border: 1px solid #ccc;
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+ padding: 2px 4px;
+}
+.api-card .api-tags .api-tag:hover {
+ cursor: pointer;
+ background-color: #0088ce;
+ border-color: #00659c;
+ color: white;
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/pages/apis/_components/apis-cards.component.html b/front-end/studio/src/app/pages/apis/_components/apis-cards.component.html
new file mode 100644
index 000000000..1675e4649
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/_components/apis-cards.component.html
@@ -0,0 +1,22 @@
+
+
+ No APIs matched the filter! Try changing your filter criteria - there is definitely at least one API you can work on...
+
+
+
+
+
+
+
+
+
+
+
Loading API Designs...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Do you really want to delete the selected API?
+
1">Do you really want to delete the {{ selectedApis.length }} selected APIs?
+
+
+
+ NOTE:
+ This will permanently delete the API definition from Apicurio Studio. This operation cannot be undone.
+
+
diff --git a/front-end/studio/src/app/pages/apis/apis.page.ts b/front-end/studio/src/app/pages/apis/apis.page.ts
new file mode 100644
index 000000000..c1fbdc8c3
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/apis.page.ts
@@ -0,0 +1,215 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, OnInit, Inject, OnDestroy} from "@angular/core";
+import {ActivatedRoute, Router} from "@angular/router";
+
+import {IApisService} from "../../services/apis.service";
+import {Api} from "../../models/api.model";
+import {ArrayUtils} from "../../util/common";
+import {AbstractPageComponent} from "../../components/page-base.component";
+
+
+const API_FILTERS_KEY = "apicurio.studio.pages.apis.filters";
+
+
+class Filters {
+ nameFilter: string;
+ sortDirection: string;
+ layout: string;
+
+ constructor(params?: any) {
+ this.reset();
+ if (params) {
+ for (let key in params) {
+ let value: string = params[key];
+ this[key] = value;
+ }
+ }
+ }
+
+ public accepts(api: Api): boolean {
+ let name: string = api.name.toLocaleLowerCase();
+ let namef: string = this.nameFilter.toLocaleLowerCase();
+ if (name.indexOf(namef) >= 0) {
+ return true;
+ }
+ if (api.tags && api.tags.length > 0) {
+ return api.tags.map(tag => {
+ return tag.toLocaleLowerCase() == namef;
+ }).reduce( (v1, v2) => {
+ return v1 || v2;
+ });
+ }
+ return false;
+ }
+
+ public reset(): void {
+ this.nameFilter = "";
+ this.sortDirection = "ASC";
+ this.layout = "card";
+ }
+}
+
+
+@Component({
+ moduleId: module.id,
+ selector: "apis-page",
+ templateUrl: "apis.page.html",
+ styleUrls: ["apis.page.css"]
+})
+export class ApisPageComponent extends AbstractPageComponent implements OnDestroy {
+
+ public allApis: Api[] = [];
+ public filteredApis: Api[];
+ public selectedApis: Api[];
+ public filters: Filters = new Filters();
+
+ public numApisToDelete: number = 0;
+
+ /**
+ * C'tor.
+ * @param {IApisService} apis
+ * @param {ActivatedRoute} route
+ */
+ constructor(@Inject(IApisService) private apis: IApisService, route: ActivatedRoute) {
+ super(route);
+ this.filteredApis = [];
+ this.selectedApis = [];
+
+ let fsaved: string = sessionStorage.getItem(API_FILTERS_KEY);
+ if (fsaved) {
+ this.filters = new Filters(JSON.parse(fsaved));
+ }
+ }
+
+ /**
+ * Called to asynchronously load data needed by the page.
+ */
+ public loadAsyncPageData(): void {
+ console.log("[ApisPageComponent] loadAsyncPageData")
+ this.apis.getApis().then( apis => {
+ console.info("APIS: %O", apis);
+ this.allApis = apis;
+ this.filterApis();
+ this.loaded("apis");
+ }).catch( error => {
+ console.error("[ApisPageComponent] Error fetching API list.");
+ this.error(error);
+ });
+ }
+
+ ngOnDestroy(): void {
+ sessionStorage.setItem(API_FILTERS_KEY, JSON.stringify(this.filters));
+ }
+
+ /**
+ * Filters and sorts the list of apis based on the user's
+ */
+ private filterApis(): Api[] {
+ // Clear the array first.
+ this.filteredApis.splice(0, this.filteredApis.length);
+ for (let api of this.allApis) {
+ if (this.filters.accepts(api)) {
+ this.filteredApis.push(api);
+ }
+ }
+ this.filteredApis.sort( (a1:Api, a2:Api) => {
+ let rval: number = a1.name.localeCompare(a2.name);
+ if (this.filters.sortDirection === "DESC") {
+ rval *= -1;
+ }
+ return rval;
+ });
+
+ this.selectedApis = ArrayUtils.intersect(this.selectedApis, this.filteredApis);
+
+ return this.filteredApis;
+ }
+
+ public isFiltered(): boolean {
+ return this.allApis.length !== this.filteredApis.length;
+ }
+
+ public toggleSortDirection(): void {
+ if (this.filters.sortDirection === "ASC") {
+ this.filters.sortDirection = "DESC";
+ } else {
+ this.filters.sortDirection = "ASC";
+ }
+ this.filterApis();
+ }
+
+ public clearFilters(): void {
+ this.filters.nameFilter = "";
+ this.filterApis();
+ }
+
+ public onSelected(api: Api): void {
+ console.info("[ApisPageComponent] Caught the onApiSelected event! Data: %o", api);
+ this.selectedApis.push(api);
+ }
+
+ public onDeselected(api: Api): void {
+ console.info("[ApisPageComponent] Caught the onApiDeselected event! Data: %o", api);
+ this.selectedApis.splice(this.selectedApis.indexOf(api), 1);
+ }
+
+ public onTagSelected(tag: string): void {
+ this.filters.nameFilter = tag;
+ this.filterApis();
+ }
+
+ /**
+ * Called to delete all selected APIs.
+ */
+ public deleteApis(): void {
+ // TODO deleting the APIs is done asynchronously - we need some sort of visual status (spinner) to watch progress, and then close the dialog when it's done
+
+ // Note: we can only delete selected items that we can see in the UI.
+ let itemsToDelete: Api[] = ArrayUtils.intersect(this.selectedApis, this.filteredApis);
+ console.log("[ApisPageComponent] Deleting %s selected APIs.", itemsToDelete.length);
+ this.numApisToDelete = itemsToDelete.length;
+ for (let api of itemsToDelete) {
+ this.apis.deleteApi(api).then( () => {
+ this.removeApiFromList(api);
+ this.filterApis();
+ this.numApisToDelete--;
+ }).catch( error => {
+ console.error("[ApisPageComponent] Error deleting an API with ID %s", api.id);
+ this.error(error);
+ });
+ }
+ this.selectedApis = [];
+ }
+
+ public onListLayout(): void {
+ this.filters.layout = "list";
+ }
+
+ public onCardLayout(): void {
+ this.filters.layout = "card";
+ }
+
+ public onReset(): void {
+ this.filters.reset();
+ }
+
+ private removeApiFromList(api: Api) {
+ this.allApis.splice(this.allApis.indexOf(api), 1);
+ }
+}
diff --git a/front-end/studio/src/app/pages/apis/create/_components/create-form.component.css b/front-end/studio/src/app/pages/apis/create/_components/create-form.component.css
new file mode 100644
index 000000000..331bd6fc4
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/create/_components/create-form.component.css
@@ -0,0 +1,60 @@
+select {
+ width: auto;
+}
+
+.createapi-form-panel.dragging {
+ border: 1px dashed #39a5dc;
+ background-color: #eef;
+}
+
+.createapi-form-panel {
+ padding-top: 25px;
+}
+
+span.disabled {
+ color: #999;
+}
+
+.dropdown ul {
+ max-height: 250px;
+ overflow: auto;
+}
+
+div.spinner {
+ display: inline-block;
+ margin-right: 5px;
+}
+
+.platform-toggle {
+ display: inline-block;
+ text-align: center;
+ cursor: pointer;
+ border: 1px solid transparent;
+ padding: 5px;
+ margin-right: 8px;
+ border-radius: 3px;
+}
+.platform-toggle:hover {
+ border: 1px solid #bee1f4;
+ background-color: #def3ff;
+}
+.platform-toggle.selected {
+ background-color: #0088ce;
+ color: white;
+ cursor: default;
+ border: 1px solid transparent;
+}
+.platform-toggle.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+.platform-toggle span.fa {
+ font-size: 32px;
+}
+.platform-toggle span.lbl {
+ font-size: 14px;
+}
+
+.account-link-warning {
+ margin-top: 10px;
+}
\ No newline at end of file
diff --git a/front-end/studio/src/app/pages/apis/create/_components/create-form.component.html b/front-end/studio/src/app/pages/apis/create/_components/create-form.component.html
new file mode 100644
index 000000000..4c8e74d0d
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/create/_components/create-form.component.html
@@ -0,0 +1,40 @@
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/create/_components/create-form.component.ts b/front-end/studio/src/app/pages/apis/create/_components/create-form.component.ts
new file mode 100644
index 000000000..7d13719d5
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/create/_components/create-form.component.ts
@@ -0,0 +1,93 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Inject, Output} from "@angular/core";
+import {IApisService} from "../../../../services/apis.service";
+import {User} from "../../../../models/user.model";
+import {IAuthenticationService} from "../../../../services/auth.service";
+import {NewApi} from "../../../../models/new-api.model";
+import {ILinkedAccountsService} from "../../../../services/accounts.service";
+import {DropDownOption} from '../../../../components/common/drop-down.component';
+
+
+@Component({
+ moduleId: module.id,
+ selector: "createapi-form",
+ templateUrl: "create-form.component.html",
+ styleUrls: ["create-form.component.css"]
+})
+export class CreateApiFormComponent {
+
+ @Output() onCreateApi = new EventEmitter();
+
+ model: any = {
+ type: "3.0.1",
+ name: null,
+ description: null
+ };
+ creatingApi: boolean = false;
+ error: string;
+
+ private _user: User;
+
+ public ngOnInit(): void {
+ }
+
+ /**
+ * Constructor.
+ * @param {IApisService} apisService
+ * @param {IAuthenticationService} authService
+ * @param {ILinkedAccountsService} accountsService
+ */
+ constructor(@Inject(IApisService) private apisService: IApisService,
+ @Inject(IAuthenticationService) private authService: IAuthenticationService,
+ @Inject(ILinkedAccountsService) private accountsService: ILinkedAccountsService)
+ {
+ this.creatingApi = false;
+
+ authService.getAuthenticatedUser().subscribe( user => {
+ this._user = user;
+ });
+ }
+
+ public typeOptions(): DropDownOption[] {
+ return [
+ { name: "Open API 2.0 (Swagger)", value: "2.0"},
+ { name: "Open API 3.0.1", value: "3.0.1"}
+ ];
+ }
+
+ public changeType(value: string): void {
+ this.model.type = value;
+ }
+
+
+ /**
+ * Called when the user clicks the "Create API" submit button on the form.
+ */
+ public createApi(): void {
+ let api: NewApi = new NewApi();
+ api.specVersion = this.model.type;
+ api.name = this.model.name;
+ api.description = this.model.description;
+
+ console.info("[CreateApiFormComponent] Firing 'create-api' event: %o", api);
+
+ this.creatingApi = true;
+ this.onCreateApi.emit(api);
+ }
+}
diff --git a/front-end/studio/src/app/pages/apis/create/create.page.css b/front-end/studio/src/app/pages/apis/create/create.page.css
new file mode 100644
index 000000000..a72f9c8e2
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/create/create.page.css
@@ -0,0 +1,12 @@
+.createapi-form {
+ padding: 15px
+}
+
+.createapi-form .form-instructions {
+ font-size: 15px;
+ padding-top: 10px
+}
+
+.createapi-form .form-instructions ol {
+ padding-left: 20px;
+}
diff --git a/front-end/studio/src/app/pages/apis/create/create.page.html b/front-end/studio/src/app/pages/apis/create/create.page.html
new file mode 100644
index 000000000..312b2af1f
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/create/create.page.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ What does this page do?
+
+ Here you can create a brand new API by telling us the name (and description) of the new API.
+
+
+
+
Choose a name (Required)
+
Set the description
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/create/create.page.ts b/front-end/studio/src/app/pages/apis/create/create.page.ts
new file mode 100644
index 000000000..0f35fc662
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/create/create.page.ts
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, OnInit, Inject, ViewChild} from "@angular/core";
+import {ActivatedRoute, Router} from "@angular/router";
+
+import {IApisService} from "../../../services/apis.service";
+import {Api} from "../../../models/api.model";
+import {CreateApiFormComponent} from "./_components/create-form.component";
+import {NewApi} from "../../../models/new-api.model";
+import {AbstractPageComponent} from "../../../components/page-base.component";
+
+@Component({
+ moduleId: module.id,
+ selector: "createapi-page",
+ templateUrl: "create.page.html",
+ styleUrls: ["create.page.css"]
+})
+export class CreateApiPageComponent extends AbstractPageComponent {
+
+ @ViewChild("createApiForm") form: CreateApiFormComponent;
+
+ /**
+ * Constructor.
+ * @param {Router} router
+ * @param {ActivatedRoute} route
+ * @param {IApisService} apis
+ */
+ constructor(private router: Router, route: ActivatedRoute, @Inject(IApisService) private apis: IApisService) {
+ super(route);
+ }
+
+ /**
+ * Called when the Create API form (component) emits a "create-api" event. This is bound to
+ * from the createapi.page.html template.
+ * @param {NewApi} newApi
+ */
+ public onCreateApi(newApi: NewApi) {
+ console.log("[CreateApiPageComponent] onCreateApi(): " + JSON.stringify(newApi))
+ this.apis.createApi(newApi).then(api => {
+ let link: string[] = [ "/apis", api.id ];
+ console.info("[CreateApiPageComponent] Navigating to: %o", link);
+ this.router.navigate(link);
+ }).catch( error => {
+ console.error("[CreateApiPageComponent] Error creating an API");
+ this.error(error);
+ })
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/import/_components/import-form.component.css b/front-end/studio/src/app/pages/apis/import/_components/import-form.component.css
new file mode 100644
index 000000000..055def26b
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/import/_components/import-form.component.css
@@ -0,0 +1,12 @@
+select {
+ width: auto;
+}
+
+.importapi-form-panel.dragging {
+ border: 1px dashed #39a5dc;
+ background-color: #eef;
+}
+
+.importapi-form-panel {
+ padding-top: 25px;
+}
diff --git a/front-end/studio/src/app/pages/apis/import/_components/import-form.component.html b/front-end/studio/src/app/pages/apis/import/_components/import-form.component.html
new file mode 100644
index 000000000..c754868e1
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/import/_components/import-form.component.html
@@ -0,0 +1,45 @@
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/import/_components/import-form.component.ts b/front-end/studio/src/app/pages/apis/import/_components/import-form.component.ts
new file mode 100644
index 000000000..62f1f41cd
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/import/_components/import-form.component.ts
@@ -0,0 +1,139 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Inject, Output} from "@angular/core";
+import {IApisService} from "../../../../services/apis.service";
+import {ImportApi} from "../../../../models/import-api.model";
+import {DropDownOption} from '../../../../components/common/drop-down.component';
+
+
+@Component({
+ moduleId: module.id,
+ selector: "importapi-form",
+ templateUrl: "import-form.component.html",
+ styleUrls: ["import-form.component.css"]
+})
+export class ImportApiFormComponent {
+
+ @Output() onImportApi = new EventEmitter();
+
+ importType: string;
+ textMode: string = "yaml";
+ model: any;
+ importingApi: boolean;
+ dragging: boolean;
+ error: string;
+
+ /**
+ * Constructor.
+ * @param apis
+ */
+ constructor(@Inject(IApisService) private apis: IApisService) {
+ this.importType = "from-url";
+ this.model = {
+ url: "",
+ data: ""
+ };
+ this.importingApi = false;
+ this.dragging = false;
+ this.error = null;
+ }
+
+ /**
+ * Called when the user clicks the "Import API" submit button on the form.
+ */
+ public importApi(): void {
+ this.error = null;
+
+ let importApi: ImportApi = new ImportApi();
+ if (this.model.url) {
+ importApi.url = this.model.url;
+ } else if (this.model.data) {
+ importApi.data = btoa(this.model.data);
+ }
+
+ this.importingApi = true;
+ this.onImportApi.emit(importApi);
+ }
+
+ public onDragOver(event: DragEvent): void {
+ if (!this.dragging) {
+ this.dragging = true;
+ }
+ event.preventDefault();
+ }
+
+ public onDrop(event: DragEvent): void {
+ console.info("DROP DATA: %o", event.dataTransfer);
+ this.dragging = false;
+ event.preventDefault();
+
+ let files: FileList = event.dataTransfer.files;
+ let dropData: string = event.dataTransfer.getData("text");
+ if (files && files.length == 1) {
+ let isJson: boolean = files[0].type === "application/json";
+ let isYaml: boolean = files[0].name.endsWith(".yaml") || files[0].name.endsWith(".yml");
+ if (isJson || isYaml) {
+ let reader: FileReader = new FileReader();
+ reader.onload = (fileLoadedEvent) => {
+ let content: string = fileLoadedEvent.target["result"];
+ this.model.data = content;
+ this.model.url = null;
+ this.importType = "from-text";
+ this.textMode = "json";
+ if (isYaml) {
+ this.textMode = "yaml";
+ }
+ };
+ reader.readAsText(files[0]);
+ }
+ } else if (dropData && dropData.startsWith("http")) {
+ this.model.url = dropData;
+ this.model.data = null;
+ this.importType = "from-url";
+ if (this.model.url.indexOf("github.com") > 0 || this.model.url.indexOf("gitlab.com") || this.model.url.indexOf("bitbucket.org") > 0) {
+ this.importType = "from-scs";
+ }
+ }
+ }
+
+ public onDragEnd(event: DragEvent): void {
+ this.dragging = false;
+ }
+
+ public importTypeOptions(): DropDownOption[] {
+ return [
+ { name: "Import From URL", value: "from-url"},
+ { name: "Import From Source Control", value: "from-scs"},
+ { name: "Import From File/Clipboard", value: "from-text"}
+ ];
+ }
+
+ public changeImportType(value: string): void {
+ this.importType = value;
+ this.model.url = null;
+ this.model.data = null;
+ }
+
+ public urlPlaceholder(): string {
+ if (this.importType === "from-url") {
+ return "https://gist.githubusercontent.com/Tim/94445d/raw/5dba00/oai-import.json";
+ } else {
+ return "https://github.com/ORG/REPO/blob/master/path/to/open-api-doc.json";
+ }
+ }
+}
diff --git a/front-end/studio/src/app/pages/apis/import/import.page.css b/front-end/studio/src/app/pages/apis/import/import.page.css
new file mode 100644
index 000000000..f29c2618f
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/import/import.page.css
@@ -0,0 +1,12 @@
+.importapi-form {
+ padding: 15px
+}
+
+.importapi-form .form-instructions {
+ font-size: 15px;
+ padding-top: 10px
+}
+
+.importapi-form .form-instructions ol {
+ padding-left: 20px;
+}
diff --git a/front-end/studio/src/app/pages/apis/import/import.page.html b/front-end/studio/src/app/pages/apis/import/import.page.html
new file mode 100644
index 000000000..abc49b78c
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/import/import.page.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ What is this page?
+
+ Hey there! Here you can import an existing API to the Studio by either giving us the OpenAPI document or
+ telling us where we can get it.
+
+
+ Choose from the following options:
+
+
+
Import from Source Control URL
+
Import raw content from URL
+
Copy/Paste raw content directly
+
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/import/import.page.ts b/front-end/studio/src/app/pages/apis/import/import.page.ts
new file mode 100644
index 000000000..3a5678a42
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/import/import.page.ts
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Inject} from "@angular/core";
+import {ActivatedRoute, Router} from "@angular/router";
+
+import {IApisService} from "../../../services/apis.service";
+import {AbstractPageComponent} from "../../../components/page-base.component";
+import {ImportApi} from "../../../models/import-api.model";
+
+@Component({
+ moduleId: module.id,
+ selector: "importapi-page",
+ templateUrl: "import.page.html",
+ styleUrls: ["import.page.css"]
+})
+export class ImportApiPageComponent extends AbstractPageComponent {
+
+ /**
+ * Constructor.
+ * @param {Router} router
+ * @param {ActivatedRoute} route
+ * @param {IApisService} apis
+ */
+ constructor(private router: Router, route: ActivatedRoute, @Inject(IApisService) private apis: IApisService) {
+ super(route);
+ }
+
+ /**
+ * Called when the Import API form (component) emits a "import-api" event. This is bound to
+ * from the importapi.page.html template.
+ * @param {ImportApi} api
+ */
+ public onImportApi(api: ImportApi) {
+ console.log("[ImportApiPageComponent] onImportApi(): " + JSON.stringify(api))
+ this.apis.importApi(api).then(updatedApi => {
+ let link: string[] = [ "/apis", updatedApi.id ];
+ console.info("[ImportApiPageComponent] Navigating to: %o", link);
+ this.router.navigate(link);
+ }).catch( error => {
+ console.error("[ImportApiPageComponent] Error importing API: %o", error);
+ this.error(error);
+ })
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/_components/activity-item.component.css b/front-end/studio/src/app/pages/apis/{apiId}/_components/activity-item.component.css
new file mode 100644
index 000000000..177d85928
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/_components/activity-item.component.css
@@ -0,0 +1,26 @@
+.activity-item {
+ background: transparent;
+ border-bottom: 1px dashed #dde;
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+.activity-item:hover {
+ background-color: #ffe065;
+ background-image: linear-gradient(to bottom, #ffe065 0, #ffe580 100%);
+}
+
+.activity-item .activity-item-icon {
+ font-size: 24px;
+}
+.activity-item .activity-item-description {
+ padding-top: 5px;
+ line-height: 14px;
+}
+.activity-item .activity-item-description .by {
+ color: #3D4255;
+}
+.activity-item .activity-item-date {
+ padding-top: 5px;
+ color: #646484;
+ font-size: 11px;
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/_components/activity-item.component.html b/front-end/studio/src/app/pages/apis/{apiId}/_components/activity-item.component.html
new file mode 100644
index 000000000..c35a215f6
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/_components/activity-item.component.html
@@ -0,0 +1,5 @@
+
+
+
{{ item.by }}{{ description() }}
+
{{ item.on | date }}
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/_components/activity-item.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/_components/activity-item.component.ts
new file mode 100644
index 000000000..805d95a96
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/_components/activity-item.component.ts
@@ -0,0 +1,353 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Input} from "@angular/core";
+import {ApiDesignChange} from "../../../../models/api-design-change";
+import {ICommand, MarshallUtils} from "oai-ts-commands";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "activity-item",
+ templateUrl: "activity-item.component.html",
+ styleUrls: ["activity-item.component.css"]
+})
+export class ActivityItemComponent {
+
+ @Input() item: ApiDesignChange;
+ _command: ICommand = null;
+
+ /**
+ * Constructor.
+ */
+ constructor() {}
+
+ /**
+ * Get the command for this change.
+ * @return {any}
+ */
+ protected command(): ICommand {
+ if (this._command == null) {
+ this._command = MarshallUtils.unmarshallCommand(JSON.parse(this.item.data));
+ }
+ return this._command;
+ }
+
+ /**
+ * Returns an appropriate icon for the activity item, based on its type.
+ * @return {string}
+ */
+ public icon(): string {
+ let rval: string = "user";
+ switch (this.command()["type"]()) {
+ case "AddPathItemCommand_20":
+ case "AddPathItemCommand_30":
+ case "AddSchemaDefinitionCommand_20":
+ case "AddSchemaDefinitionCommand_30":
+ rval = "plus";
+ break;
+ case "ChangeContactCommand_20":
+ case "ChangeContactCommand_30":
+ rval = "id-card-o";
+ break;
+ case "ChangeDescriptionCommand_20":
+ case "ChangeDescriptionCommand_30":
+ rval = "pencil-square-o";
+ break;
+ case "ChangeLicenseCommand_20":
+ case "ChangeLicenseCommand_30":
+ rval = "copyright";
+ break;
+ case "ChangeMediaTypeTypeCommand":
+ case "ChangeParameterDefinitionTypeCommand_20":
+ case "ChangeParameterDefinitionTypeCommand_30":
+ case "ChangeParameterTypeCommand_20":
+ case "ChangeParameterTypeCommand_30":
+ case "ChangePropertyTypeCommand_20":
+ case "ChangePropertyTypeCommand_30":
+ case "ChangeResponseTypeCommand_20":
+ case "ChangeResponseDefinitionTypeCommand_20":
+ rval = "info";
+ break;
+ case "ChangePropertyCommand_20":
+ case "ChangePropertyCommand_30":
+ case "ChangeSecuritySchemeCommand_20":
+ case "ChangeSecuritySchemeCommand_30":
+ case "ChangeServerCommand":
+ case "ChangeTitleCommand_20":
+ case "ChangeTitleCommand_30":
+ case "ChangeVersionCommand_20":
+ case "ChangeVersionCommand_30":
+ rval = "pencil";
+ break;
+ case "DeleteAllParametersCommand_20":
+ case "DeleteAllParametersCommand_30":
+ case "DeleteAllPropertiesCommand_20":
+ case "DeleteAllPropertiesCommand_30":
+ case "DeleteMediaTypeCommand":
+ case "DeleteOperationCommand_20":
+ case "DeleteOperationCommand_30":
+ case "DeleteParameterCommand_20":
+ case "DeleteParameterCommand_30":
+ case "DeletePathCommand_20":
+ case "DeletePathCommand_30":
+ case "DeletePropertyCommand_20":
+ case "DeletePropertyCommand_30":
+ case "DeleteResponseCommand_20":
+ case "DeleteResponseCommand_30":
+ case "DeleteSchemaDefinitionCommand_20":
+ case "DeleteSchemaDefinitionCommand_30":
+ case "DeleteSecuritySchemeCommand_20":
+ case "DeleteSecuritySchemeCommand_30":
+ case "DeleteServerCommand":
+ case "DeleteTagCommand_20":
+ case "DeleteTagCommand_30":
+ case "DeleteRequestBodyCommand_30":
+ case "DeleteAllResponsesCommand_20":
+ case "DeleteAllResponsesCommand_30":
+ case "DeleteContactCommand_20":
+ case "DeleteContactCommand_30":
+ case "DeleteLicenseCommand_20":
+ case "DeleteLicenseCommand_30":
+ rval = "trash-o";
+ break;
+ case "NewMediaTypeCommand":
+ case "NewOperationCommand_20":
+ case "NewOperationCommand_30":
+ case "NewParamCommand_20":
+ case "NewParamCommand_30":
+ case "NewPathCommand_20":
+ case "NewPathCommand_30":
+ case "NewRequestBodyCommand_20":
+ case "NewRequestBodyCommand_30":
+ case "NewResponseCommand_20":
+ case "NewResponseCommand_30":
+ case "NewSchemaDefinitionCommand_20":
+ case "NewSchemaDefinitionCommand_30":
+ case "NewSchemaPropertyCommand_20":
+ case "NewSchemaPropertyCommand_30":
+ case "NewSecuritySchemeCommand_20":
+ case "NewSecuritySchemeCommand_30":
+ rval = "plus";
+ break;
+ case "NewServerCommand":
+ rval = "server";
+ break;
+ case "NewTagCommand_20":
+ case "NewTagCommand_30":
+ rval = "tag";
+ break;
+ case "ReplaceOperationCommand_20":
+ case "ReplaceOperationCommand_30":
+ case "ReplacePathItemCommand_20":
+ case "ReplacePathItemCommand_30":
+ case "ReplaceSchemaDefinitionCommand_20":
+ case "ReplaceSchemaDefinitionCommand_30":
+ rval = "code";
+ break;
+ default:
+ rval = "question";
+ }
+ return rval;
+ }
+
+ /**
+ * Returns an appropriate description for the activity item, based on its type.
+ * @return {string}
+ */
+ public description(): string {
+ let rval: string = "user";
+ switch (this.command()["type"]()) {
+ case "AddPathItemCommand_20":
+ case "AddPathItemCommand_30":
+ rval = "added a Path Item named " + this.command()["_newPathItemName"] + ".";
+ break;
+ case "AddSchemaDefinitionCommand_20":
+ case "AddSchemaDefinitionCommand_30":
+ rval = "added a Schema Definition named " + this.command()["_newDefinitionName"] + ".";
+ break;
+ case "ChangeContactCommand_20":
+ case "ChangeContactCommand_30":
+ rval = "altered the API's Contact information.";
+ break;
+ case "ChangeDescriptionCommand_20":
+ case "ChangeDescriptionCommand_30":
+ rval = "altered the API's description.";
+ break;
+ case "ChangeLicenseCommand_20":
+ case "ChangeLicenseCommand_30":
+ rval = "changed the API's license to " + this.command()["_newLicenseUrl"] + ".";
+ break;
+ case "ChangeMediaTypeTypeCommand":
+ rval = "modified a Media Type (for node " + this.command()["_mediaTypePath"] + ").";
+ break;
+ case "ChangeParameterDefinitionTypeCommand_20":
+ case "ChangeParameterDefinitionTypeCommand_30":
+ case "ChangeParameterTypeCommand_20":
+ case "ChangeParameterTypeCommand_30":
+ rval = "changed the type of a Parameter at location " + this.command()["_paramPath"] + ".";
+ break;
+ case "ChangePropertyTypeCommand_20":
+ case "ChangePropertyTypeCommand_30":
+ rval = "changed the type of the Schema Property named '" + this.command()["_propName"] + "' at location " + this.command()["_propPath"] + ".";
+ break;
+ case "ChangeResponseTypeCommand_20":
+ case "ChangeResponseDefinitionTypeCommand_20":
+ rval = "changed the type of an operation Response at location " + this.command()["_responsePath"] + ".";
+ break;
+ case "ChangePropertyCommand_20":
+ case "ChangePropertyCommand_30":
+ rval = "changed the value of property '" + this.command()["_property"] + "' at location " + this.command()["_nodePath"] + ".";
+ break;
+ case "ChangeSecuritySchemeCommand_20":
+ case "ChangeSecuritySchemeCommand_30":
+ rval = "modified the details of Security Scheme named '" + this.command()["_schemeName"] + "'.";
+ break;
+ case "ChangeServerCommand":
+ rval = "modified the details of Server '" + this.command()["_serverUrl"] + "' at location " + this.command()["_parentPath"] + ".";
+ break;
+ case "ChangeTitleCommand_20":
+ case "ChangeTitleCommand_30":
+ rval = "altered the API's title to '" + this.command()["_newTitle"] + "'";
+ break;
+ case "ChangeVersionCommand_20":
+ case "ChangeVersionCommand_30":
+ rval = "altered the API's version to '" + this.command()["_newVersion"] + "'";
+ break;
+ case "DeleteAllParametersCommand_20":
+ case "DeleteAllParametersCommand_30":
+ rval = "deleted all of the " + this.command()["_paramType"] + " style parameters at location " + this.command()["_parentPath"] + ".";
+ break;
+ case "DeleteAllPropertiesCommand_20":
+ case "DeleteAllPropertiesCommand_30":
+ rval = "deleted all of the Schema properties at location " + this.command()["_schemaPath"] + ".";
+ break;
+ case "DeleteMediaTypeCommand":
+ rval = "deleted Media Type '" + this.command()["_mediaTypeName"] + "' at location " + this.command()["_mediaTypePath"] + ".";
+ break;
+ case "DeleteOperationCommand_20":
+ case "DeleteOperationCommand_30":
+ rval = "deleted the '" + this.command()["_property"] + "' Operation at location " + this.command()["_parentPath"] + ".";
+ break;
+ case "DeleteParameterCommand_20":
+ case "DeleteParameterCommand_30":
+ rval = "deleted a parameter at location " + this.command()["_parameterPath"] + ".";
+ break;
+ case "DeletePathCommand_20":
+ case "DeletePathCommand_30":
+ rval = "deleted a Path Item named '" + this.command()["_path"] + "'.";
+ break;
+ case "DeletePropertyCommand_20":
+ case "DeletePropertyCommand_30":
+ rval = "deleted a Property named '" + this.command()["_propertyName"] + "' at location " + this.command()["_propertyPath"] + ".";
+ break;
+ case "DeleteResponseCommand_20":
+ case "DeleteResponseCommand_30":
+ rval = "deleted a Response with code '" + this.command()["_responseCode"] + "' at location " + this.command()["_responsePath"] + ".";
+ break;
+ case "DeleteSchemaDefinitionCommand_20":
+ case "DeleteSchemaDefinitionCommand_30":
+ rval = "deleted the Schema Definition named '" + this.command()["_definitionName"] + "'.";
+ break;
+ case "DeleteSecuritySchemeCommand_20":
+ case "DeleteSecuritySchemeCommand_30":
+ rval = "deleted the Security Scheme named '" + this.command()["_schemeName"] + "'.";
+ break;
+ case "DeleteServerCommand":
+ rval = "deleted a Server with url '" + this.command()["_serverUrl"] + "' at location " + this.command()["_parentPath"] + ".";
+ break;
+ case "DeleteTagCommand_20":
+ case "DeleteTagCommand_30":
+ rval = "deleted the global Tag definition with name '" + this.command()["_tagName"] + "'.";
+ break;
+ case "DeleteRequestBodyCommand_30":
+ rval = "deleted the global Tag definition with name '" + this.command()["_tagName"] + "'.";
+ break;
+ case "DeleteAllResponsesCommand_20":
+ case "DeleteAllResponsesCommand_30":
+ rval = "deleted all of the Responses at location " + this.command()["_parentPath"] + ".";
+ break;
+ case "DeleteContactCommand_20":
+ case "DeleteContactCommand_30":
+ rval = "deleted the API's Contact information.";
+ break;
+ case "DeleteLicenseCommand_20":
+ case "DeleteLicenseCommand_30":
+ rval = "deleted the API's License information.";
+ break;
+ case "NewMediaTypeCommand":
+ rval = "added a new Media Type named '" + this.command()["_newMediaType"] + "' at location " + this.command()["_nodePath"] + ".";
+ break;
+ case "NewOperationCommand_20":
+ case "NewOperationCommand_30":
+ rval = "added a new Operation named '" + this.command()["_method"] + "' at location " + this.command()["_path"] + ".";
+ break;
+ case "NewParamCommand_20":
+ case "NewParamCommand_30":
+ rval = "added a new Parameter named '" + this.command()["_paramName"] + "' at location " + this.command()["_parentPath"] + ".";
+ break;
+ case "NewPathCommand_20":
+ case "NewPathCommand_30":
+ rval = "added a new Path named '" + this.command()["_newPath"] + "'.";
+ break;
+ case "NewRequestBodyCommand_20":
+ case "NewRequestBodyCommand_30":
+ rval = "added a Request Body for Operation at location " + this.command()["_operationPath"] + ".";
+ break;
+ case "NewResponseCommand_20":
+ case "NewResponseCommand_30":
+ rval = "added a new Response for response code '" + this.command()["_statusCode"] + "' for Operation at location " + this.command()["_operationPath"] + ".";
+ break;
+ case "NewSchemaDefinitionCommand_20":
+ case "NewSchemaDefinitionCommand_30":
+ rval = "added a new Schema Definition for response code '" + this.command()["_statusCode"] + "' for Operation at location " + this.command()["_operationPath"] + ".";
+ break;
+ case "NewSchemaPropertyCommand_20":
+ case "NewSchemaPropertyCommand_30":
+ rval = "added a new Schema Property named '" + this.command()["_propertyName"] + "' at location " + this.command()["_schemaPath"] + ".";
+ break;
+ case "NewSecuritySchemeCommand_20":
+ case "NewSecuritySchemeCommand_30":
+ rval = "added a new Security Scheme named '" + this.command()["_schemeName"] + "'.";
+ break;
+ case "NewServerCommand":
+ rval = "added a new Server with url '" + this.command()["_server"].url + "' at location " + this.command()["_parentPath"] + ".";
+ break;
+ case "NewTagCommand_20":
+ case "NewTagCommand_30":
+ rval = "added a new Security Scheme named '" + this.command()["_tagName"] + "'.";
+ break;
+ case "ReplaceOperationCommand_20":
+ case "ReplaceOperationCommand_30":
+ rval = "fully replaced the source for Operation '" + this.command()["_method"] + "' at location " + this.command()["_path"] + ".";
+ break;
+ case "ReplacePathItemCommand_20":
+ case "ReplacePathItemCommand_30":
+ rval = "fully replaced the source for Path '" + this.command()["_pathName"] + "'.";
+ break;
+ case "ReplaceSchemaDefinitionCommand_20":
+ case "ReplaceSchemaDefinitionCommand_30":
+ rval = "fully replaced the source for Schema Definition '" + this.command()["_defName"] + "'.";
+ break;
+ default:
+ console.info("[ActivityItemComponent] WARNING - unhandled change item type: %s", this.command()["type"]());
+ rval = "performed some unknown action...";
+ }
+ return rval;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/api-detail.page.css b/front-end/studio/src/app/pages/apis/{apiId}/api-detail.page.css
new file mode 100644
index 000000000..5d9af96bc
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/api-detail.page.css
@@ -0,0 +1,71 @@
+
+.api-tags {
+ margin-top: 15px;
+}
+.api-tags .api-tags-label {
+ font-weight: bold;
+ margin-right: 5px;
+}
+.api-tags .api-tag {
+ margin-right: 5px;
+ border: 1px solid #ccc;
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+ padding: 2px 4px;
+}
+.api-tags .api-tag:hover {
+ background-color: #0088ce;
+ border-color: #00659c;
+ color: white;
+}
+
+.api-description {
+ margin-top: 30px;
+ margin-bottom: 25px;
+}
+
+.api-created-info {
+ margin-left: 15px
+}
+.api-created-info .info-left {
+ display: inline-block;
+ width: 40%;
+}
+.api-created-info .info-right {
+ display: inline;
+ width: 50%;
+}
+
+.api-meta-data {
+ margin-top: 15px
+}
+
+.api-meta-data label {
+ text-decoration: underline;
+}
+
+.api-resource-url-value {
+}
+
+.api-action-buttons {
+ margin-top: 20px
+}
+
+.api-action-buttons a.pull-right {
+ margin-left: 10px;
+}
+
+.dropdown-menu a.disabled {
+ cursor: not-allowed;
+ color: #999;
+}
+
+.card-activity {
+}
+.card-activity .actions {
+ padding-top: 10px;
+}
+.card-activity .actions button {
+ font-size: 11px;
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/api-detail.page.html b/front-end/studio/src/app/pages/apis/{apiId}/api-detail.page.html
new file mode 100644
index 000000000..a047f73f9
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/api-detail.page.html
@@ -0,0 +1,172 @@
+
+ Created on {{ invitation.createdOn | date }} by {{ invitation.createdBy }}
+ Creating, please wait...
+ Deleting invitation, please wait...
+ Pending (copy the link and send it to someone!)
+ Accepted by {{ invitation.modifiedBy }} on {{ invitation.modifiedOn | date }}
+ Rejected by {{ invitation.modifiedBy }} on {{ invitation.modifiedOn | date }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Do you really want to remove this collaborator?
+
+
+ NOTE:
+
+ This will make it so the user can no longer access this API design. If you wish to collaborate with the user again
+ in the future you will need to re-invite them.
+
+
+
+
+
Copy the following link to your clipboard and then send it to someone you want to collaborate with!
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/collaboration/api-collaboration.page.ts b/front-end/studio/src/app/pages/apis/{apiId}/collaboration/api-collaboration.page.ts
new file mode 100644
index 000000000..9793ac148
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/collaboration/api-collaboration.page.ts
@@ -0,0 +1,187 @@
+/**
+ * @license
+ * Copyright 2018 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Inject} from "@angular/core";
+import {ActivatedRoute, Router} from "@angular/router";
+
+import {IApisService} from "../../../../services/apis.service";
+import {Api} from "../../../../models/api.model";
+import {AbstractPageComponent} from "../../../../components/page-base.component";
+import {ApiCollaborator} from "../../../../models/api-collaborator";
+import {Invitation} from "../../../../models/invitation";
+import {IAuthenticationService} from "../../../../services/auth.service";
+import {User} from "../../../../models/user.model";
+
+@Component({
+ moduleId: module.id,
+ selector: "api-collaboration-page",
+ templateUrl: "api-collaboration.page.html",
+ styleUrls: ["api-collaboration.page.css"]
+})
+export class ApiCollaborationPageComponent extends AbstractPageComponent {
+
+ public api: Api;
+ public collaborators: ApiCollaborator[];
+ public invitations: Invitation[];
+
+ private _isOwner: boolean = false;
+ public _copyLink: string = "";
+
+
+ /**
+ * Constructor.
+ * @param {Router} router
+ * @param {ActivatedRoute} route
+ * @param {IApisService} apis
+ * @param {IAuthenticationService} authService
+ */
+ constructor(private router: Router, route: ActivatedRoute,
+ @Inject(IApisService) private apis: IApisService,
+ @Inject(IAuthenticationService) private authService: IAuthenticationService) {
+ super(route);
+ this.api = new Api();
+ }
+
+ /**
+ * Called to kick off loading the page's async data.
+ * @param params
+ */
+ public loadAsyncPageData(params: any): void {
+ console.info("[ApiCollaborationPageComponent] Loading async page data");
+ let apiId: string = params["apiId"];
+ this.apis.getApi(apiId).then(api => {
+ this.api = api;
+ this.dataLoaded["api"] = true;
+ }).catch(error => {
+ console.error("[ApiCollaborationPageComponent] Error getting API");
+ this.error(error);
+ });
+ this.apis.getCollaborators(apiId).then(collaborators => {
+ console.info("[ApiCollaborationPageComponent] Collaborators data loaded: %o", collaborators);
+ this.collaborators = collaborators;
+ this.collaborators.sort( (c1, c2) => {
+ if (c1.role === "owner" && c2.role != "owner") {
+ return -1;
+ }
+ if (c1.role != "owner" && c2.role === "owner") {
+ return 1;
+ }
+ return c1.userName.toLowerCase().localeCompare(c1.userName.toLowerCase());
+ });
+ this.setIsOwner();
+ this.dataLoaded["collaborators"] = true;
+ }).catch(error => {
+ console.error("[ApiCollaborationPageComponent] Error getting API collaborators");
+ this.error(error);
+ });
+ this.apis.getInvitations(apiId).then(invitations => {
+ console.info("[ApiCollaborationPageComponent] Invitations data loaded: %o", invitations);
+ this.invitations = invitations;
+ this.invitations.sort( (i1, i2) => {
+ if (i1.createdOn > i2.createdOn) {
+ return -1;
+ }
+ if (i1.createdOn < i2.createdOn) {
+ return 1;
+ }
+ return i1.inviteId.localeCompare(i2.inviteId);
+ });
+ this.dataLoaded["invitations"] = true;
+ }).catch(error => {
+ console.error("[ApiCollaborationPageComponent] Error getting API invitations");
+ this.error(error);
+ });
+ }
+
+ /**
+ * Called when the user clicks the "Create Invitation" button.
+ */
+ public createInvitation(): void {
+ let newInvitation: Invitation = new Invitation();
+ newInvitation.createdBy = this.authService.getAuthenticatedUserNow().login;
+ newInvitation.createdOn = new Date();
+ newInvitation.designId = this.api.id;
+ newInvitation.status = "creating";
+ newInvitation.inviteId = "12345";
+ this.invitations.unshift(newInvitation);
+ this.apis.createInvitation(this.api.id).then( (invitation) => {
+ newInvitation.createdBy = invitation.createdBy;
+ newInvitation.createdOn = invitation.createdOn;
+ newInvitation.status = invitation.status;
+ newInvitation.inviteId = invitation.inviteId;
+ newInvitation.role = invitation.role;
+ }).catch( error => {
+ console.error("[ApiCollaborationPageComponent] Error creating invitation");
+ this.error(error);
+ });
+ }
+
+ /**
+ * Called to remove a collaborator. The user will no longer have access to the API.
+ * @param {ApiCollaborator} collaborator
+ */
+ public removeCollaborator(collaborator: ApiCollaborator): void {
+ }
+
+ /**
+ * Called to cancel a pending invitation.
+ */
+ public cancelInvitation(invite: Invitation): void {
+ invite.status = "rejecting";
+ this.apis.rejectInvitation(this.api.id, invite.inviteId).then( () => {
+ invite.status = "rejected";
+ invite.modifiedOn = new Date();
+ invite.modifiedBy = this.authService.getAuthenticatedUserNow().login;
+ }).catch( error => {
+ this.error(error);
+ });
+ }
+
+ /**
+ * Called to copy the invitation link to the clipboard.
+ * @param {Invitation} invite
+ */
+ public copyInvitationLink(invite: Invitation): void {
+ this._copyLink = window.location.toString() + "/accept/" + invite.inviteId;
+ }
+
+ /**
+ * Returns true if the currently logged-in user is an Owner of the API.
+ * @return {boolean}
+ */
+ public isOwner(): boolean {
+ return this._isOwner;
+ }
+
+ /**
+ * Set the isOwner property.
+ */
+ private setIsOwner(): void {
+ this._isOwner = false;
+
+ let user: User = this.authService.getAuthenticatedUserNow();
+ if (user != null) {
+ for (let collaborator of this.collaborators) {
+ if (collaborator.userId == user.login) {
+ this._isOwner = collaborator.role === "owner";
+ return;
+ }
+ }
+ }
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/context-help.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/context-help.component.html
new file mode 100644
index 000000000..1a27d0ec9
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/context-help.component.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/context-help.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/context-help.component.ts
new file mode 100644
index 000000000..13acba2af
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/context-help.component.ts
@@ -0,0 +1,67 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, ViewChildren, QueryList, ElementRef, HostListener} from "@angular/core";
+
+@Component({
+ moduleId: module.id,
+ selector: "context-help",
+ templateUrl: "context-help.component.html"
+})
+export class ContextHelpComponent {
+
+ private _open: boolean = false;
+
+ private left: string;
+ private top: string;
+
+ @ViewChildren("helppanel") helppanel: QueryList;
+
+ @HostListener("document:click", ["$event"])
+ public onDocumentClick(event: MouseEvent): void {
+ if (this._open) {
+ this.close();
+ }
+ }
+
+ public open(event: MouseEvent): void {
+ this.left = event.clientX + "px";
+ this.top = event.clientY + "px";
+ event.preventDefault();
+ event.stopPropagation();
+ this._open = true;
+ }
+
+ public close(): void {
+ this._open = false;
+ }
+
+ public isOpen(): boolean {
+ return this._open;
+ }
+
+ /**
+ * Called whenever the user presses a key.
+ * @param event
+ */
+ public onGlobalKeyDown(event: KeyboardEvent): void {
+ if (event.key === "Escape" && !event.metaKey && !event.altKey && !event.ctrlKey) {
+ this.close();
+ }
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-editor.base.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-editor.base.ts
new file mode 100644
index 000000000..3d18ab8d3
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-editor.base.ts
@@ -0,0 +1,277 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ EventEmitter, Output, Input, AfterViewInit, ViewChildren, QueryList, ElementRef,
+ ViewChild, HostListener
+} from "@angular/core";
+import {TimerObservable} from "rxjs/observable/TimerObservable";
+import {Subscription} from "rxjs";
+
+
+/**
+ * Base class for all inline editors.
+ */
+export abstract class AbstractInlineEditor {
+
+ static s_activeEditor: any = null;
+
+ @Input() enabled = true;
+ @Input() labelClass = "";
+ @Input() inputClass = "";
+ @Input() formClass = "";
+ @Input() topIncrement: number = 0;
+ @Input() leftIncrement: number = 0;
+ @Input() rightIncrement: number = 0;
+ @Input() bottomIncrement: number = 0;
+
+ @Output() onChange: EventEmitter = new EventEmitter();
+
+ @ViewChildren("clickcontainer") clickcontainer: QueryList;
+
+ private _mousein: boolean = false;
+ private _hoverSub: Subscription;
+ private _hoverElem: any;
+
+ public hovering: boolean = false;
+ public editing: boolean = false;
+ public canClose: boolean = false;
+ public hoverDims: any = {
+ left: 0,
+ top: 0,
+ width: 0,
+ height: 0
+ };
+
+ public inputHover: boolean = false;
+ public inputFocus: boolean = false;
+
+ public evalue: T;
+
+ @HostListener("document:click", ["$event"])
+ public onDocumentClick(event: MouseEvent): void {
+ if (this.clickcontainer && this.clickcontainer.first && this.clickcontainer.first.nativeElement) {
+ if (!this.clickcontainer.first.nativeElement.contains(event.target) && this.canClose) {
+ this.onCancel();
+ } else {
+ }
+ }
+ }
+
+ public onMouseIn(event: MouseEvent): void {
+ if (this.editing || !this.enabled) {
+ return;
+ }
+ this._mousein = true;
+ this._hoverElem = event.currentTarget;
+ this._hoverSub = TimerObservable.create(100).subscribe(() => {
+ if (this._mousein) {
+ let target: any = this._hoverElem;
+ let targetRect: any = target.getBoundingClientRect();
+ this.hoverDims = this.calcHoverDimensions(targetRect);
+ this.hovering = true;
+ }
+ });
+ }
+
+ public hoverTop(): string {
+ return this.hoverDims.top + "px";
+ }
+
+ public hoverLeft(): string {
+ return this.hoverDims.left + "px";
+ }
+
+ public hoverWidth(): string {
+ return this.hoverDims.width + "px";
+ }
+
+ public hoverHeight(): string {
+ return this.hoverDims.height + "px";
+ }
+
+ public onMouseOut(): void {
+ if (this.editing) {
+ return;
+ }
+ if (this._mousein && !this.hovering) {
+ this.hovering = false;
+ }
+ if (this._hoverSub) {
+ this._hoverSub.unsubscribe();
+ this._hoverSub = null;
+ }
+ this._mousein = false;
+ }
+
+ public onOverlayOut(): void {
+ if (this.hovering) {
+ this.hovering = false;
+ this._mousein = false;
+ }
+ }
+
+ public onStartEditing(): void {
+ this.canClose = false;
+ this.evalue = this.initialValueForEditing();
+ this.hovering = false;
+ this._mousein = false;
+ this.inputFocus = true;
+ this.inputHover = true;
+ this.editing = true;
+
+ if (AbstractInlineEditor.s_activeEditor != null && AbstractInlineEditor.s_activeEditor !== this) {
+ AbstractInlineEditor.s_activeEditor.onCancel();
+ }
+ AbstractInlineEditor.s_activeEditor = this;
+
+ // TODO watch for changes to children rather than simply wait 250ms??
+ TimerObservable.create(250).subscribe(() => {
+ this.canClose = true;
+ });
+ }
+
+ protected abstract initialValueForEditing(): T;
+
+ public onSave(): void {
+ this.onChange.emit(this.evalue);
+ this.editing = false;
+ AbstractInlineEditor.s_activeEditor = this;
+ }
+
+ public onCancel(): void {
+ this.editing = false;
+ AbstractInlineEditor.s_activeEditor = this;
+ this.evalue = this.initialValueForEditing();
+ }
+
+ public onInputKeypress(event: KeyboardEvent): void {
+ if (event.key === "Escape") {
+ this.onCancel();
+ }
+ }
+
+ public onInputFocus(isFocus: boolean): void {
+ if (this.inputFocus !== isFocus) {
+ this.inputFocus = isFocus;
+ }
+ }
+
+ public onInputIn(isIn: boolean): void {
+ if (this.inputHover !== isIn) {
+ this.inputHover = isIn;
+ }
+ }
+
+ protected calcHoverDimensions(targetRect: any): any {
+ return {
+ left: targetRect.left - 5 - this.leftIncrement,
+ top: targetRect.top - this.topIncrement,
+ width: targetRect.right - targetRect.left + 10 + 20 + this.leftIncrement + this.rightIncrement,
+ height: targetRect.bottom - targetRect.top + 3 + this.topIncrement + this.bottomIncrement
+ }
+ }
+
+}
+
+
+
+/**
+ * Base class for any inline editor that edits a single value of an arbitrary type.
+ */
+export abstract class AbstractInlineValueEditor extends AbstractInlineEditor {
+
+ @Input() value: T;
+ @Input() noValueMessage: string;
+
+ protected displayValue(): string {
+ if (this.isEmpty()) {
+ return this.noValueMessage;
+ }
+ return this.formatValue(this.value);
+ }
+
+ protected abstract formatValue(value: T): string;
+
+ protected isEmpty(): boolean {
+ return this.value === undefined || this.value === null;
+ }
+
+ protected initialValueForEditing(): T {
+ return this.value;
+ }
+
+}
+
+/**
+ * Base class for any inline editor that is built on a single text input element. The template
+ * must include an 'input' element named #newvalue.
+ */
+export abstract class TextInputEditorComponent extends AbstractInlineValueEditor implements AfterViewInit {
+
+ @ViewChildren("newvalue") input: QueryList;
+
+ ngAfterViewInit(): void {
+ this.input.changes.subscribe(changes => {
+ if (changes.last) {
+ changes.last.nativeElement.focus();
+ changes.last.nativeElement.select();
+ }
+ });
+ }
+
+ protected isEmpty(): boolean {
+ return super.isEmpty() || this.value.length === 0;
+ }
+
+ protected formatValue(value: string): string {
+ return value;
+ }
+
+}
+
+
+/**
+ * Base class for any inline editor that is built on a single textarea element. The template
+ * must include a 'textarea' element named #newvalue.
+ */
+export abstract class TextAreaEditorComponent extends TextInputEditorComponent {
+
+ public onInputKeypress(event: KeyboardEvent): void {
+ super.onInputKeypress(event);
+
+ if (event.ctrlKey && event.key === "Enter" && this.isValid()) {
+ this.onSave();
+ }
+ }
+
+ /**
+ * Subclasses can override this to provide validation status of the current value.
+ * @return {boolean}
+ */
+ protected isValid(): boolean {
+ return true;
+ }
+
+ protected calcHoverDimensions(targetRect: any): any {
+ let dims: any = super.calcHoverDimensions(targetRect);
+ dims.top = dims.top - 2;
+ dims.height += 2;
+ return dims;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-text-editor.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-text-editor.component.html
new file mode 100644
index 000000000..74f19dedc
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-text-editor.component.html
@@ -0,0 +1,27 @@
+{{ displayValue() }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-text-editor.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-text-editor.component.ts
new file mode 100644
index 000000000..99bb0a913
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-text-editor.component.ts
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Component, ViewEncapsulation
+} from "@angular/core";
+import {TextInputEditorComponent} from "./inline-editor.base";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "inline-text-editor",
+ templateUrl: "inline-text-editor.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class InlineTextEditorComponent extends TextInputEditorComponent {
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-textarea-editor.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-textarea-editor.component.html
new file mode 100644
index 000000000..02a330b94
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-textarea-editor.component.html
@@ -0,0 +1,28 @@
+
{{ displayValue() }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-textarea-editor.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-textarea-editor.component.ts
new file mode 100644
index 000000000..1445f7e4c
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/inline-textarea-editor.component.ts
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Component, ViewEncapsulation
+} from "@angular/core";
+import {TextAreaEditorComponent} from "./inline-editor.base";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "inline-textarea-editor",
+ templateUrl: "inline-textarea-editor.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class InlineTextAreaComponent extends TextAreaEditorComponent {
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/path-item.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/path-item.component.html
new file mode 100644
index 000000000..2a50138be
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/path-item.component.html
@@ -0,0 +1 @@
+{{ pathSegment }}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/path-item.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/path-item.component.ts
new file mode 100644
index 000000000..5894c0901
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/path-item.component.ts
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Input, ViewEncapsulation} from "@angular/core";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "[path-item]",
+ templateUrl: "path-item.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class PathItemComponent {
+
+ @Input() path: string;
+
+ public pathSegments(): string[] {
+ let rval: string[] = this.path.split("/");
+ if (rval[0] === "") {
+ rval = rval.splice(1);
+ }
+ return rval;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/response-item.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/response-item.component.html
new file mode 100644
index 000000000..ddc8e5aa0
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/response-item.component.html
@@ -0,0 +1,2 @@
+
+{{ name }}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/response-item.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/response-item.component.ts
new file mode 100644
index 000000000..49b198b9e
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/response-item.component.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Input, ViewEncapsulation} from "@angular/core";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "[response-item]",
+ templateUrl: "response-item.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class ResponseItemComponent {
+
+ @Input() name: string;
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/schema-type.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/schema-type.component.html
new file mode 100644
index 000000000..850fee908
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/schema-type.component.html
@@ -0,0 +1 @@
+{{ displayType() }}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/schema-type.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/schema-type.component.ts
new file mode 100644
index 000000000..79058306d
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/schema-type.component.ts
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Input} from "@angular/core";
+import {ObjectUtils} from "../../_util/object.util";
+import {SimplifiedType} from "oai-ts-commands";
+
+@Component({
+ moduleId: module.id,
+ selector: "schema-type",
+ templateUrl: "schema-type.component.html"
+})
+export class SchemaTypeComponent {
+
+ @Input() type: SimplifiedType;
+
+ public displayType(): string {
+ if (ObjectUtils.isNullOrUndefined(this.type) || ObjectUtils.isNullOrUndefined(this.type.type)) {
+ return "No Type";
+ }
+ if (this.type.isArray()) {
+ if (this.type.of && this.type.of.as) {
+ return "Array of: " + this.type.of.type + " as " + this.type.of.as;
+ }
+ if (this.type.of && this.type.of.isSimpleType()) {
+ return "Array of: " + this.type.of.type;
+ }
+ if (this.type.of && this.type.of.isRef()) {
+ return "Array of: " + this.type.of.type.substr(this.type.of.type.lastIndexOf('/') + 1);
+ }
+ }
+ if (this.type.isSimpleType()) {
+ if (this.type.as) {
+ return this.type.type + " as " + this.type.as;
+ } else {
+ return this.type.type;
+ }
+ }
+ if (this.type.isRef()) {
+ return this.type.type.substr(this.type.type.lastIndexOf('/') + 1);
+ }
+ return "Unknown Type";
+ }
+
+ public hasType(): boolean {
+ return !ObjectUtils.isNullOrUndefined(this.type.type);
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/search.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/search.component.html
new file mode 100644
index 000000000..d5f127906
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/search.component.html
@@ -0,0 +1,13 @@
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/search.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/search.component.ts
new file mode 100644
index 000000000..527108806
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/search.component.ts
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Input, Output, EventEmitter} from "@angular/core";
+import {BehaviorSubject} from "rxjs/BehaviorSubject";
+
+@Component({
+ moduleId: module.id,
+ selector: "search",
+ templateUrl: "search.component.html"
+})
+export class SearchComponent {
+
+ @Input() initialValue: string;
+ @Input() placeholder: string;
+ @Input() searchId: string;
+
+ @Output() onSearch: EventEmitter = new EventEmitter();
+
+ value: string;
+ private _valueObs: BehaviorSubject = new BehaviorSubject(null);
+
+ constructor() {
+ this._valueObs.debounceTime(200).subscribe( value => {
+ this.onSearch.emit(value);
+ });
+ }
+
+ public changeValue(newValue: string): void {
+ this.value = newValue;
+ this._valueObs.next(newValue);
+ }
+
+ public clear(): void {
+ this.value = null;
+ this.onSearch.emit(null);
+ }
+
+ public hasValue(): boolean {
+ return this.value && this.value.length > 0;
+ }
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/server-url.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/server-url.component.html
new file mode 100644
index 000000000..9a401461f
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/server-url.component.html
@@ -0,0 +1 @@
+{{ segment }}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/server-url.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/server-url.component.ts
new file mode 100644
index 000000000..efd61a41d
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/server-url.component.ts
@@ -0,0 +1,55 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Input, ViewEncapsulation} from "@angular/core";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "[server-url]",
+ templateUrl: "server-url.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class ServerUrlComponent {
+
+ @Input() url: string;
+
+ public segments(): string[] {
+ let segments: string[] = [];
+ let i: number = 0;
+ let sep: string = '{';
+ while (i < this.url.length) {
+ let from: number = i;
+ let to: number = this.url.indexOf(sep, from + 1);
+ if (to == -1) {
+ to = this.url.length;
+ } else {
+ if (sep === '{') {
+ sep = '}';
+ } else {
+ to++;
+ sep = '{';
+ }
+ }
+ let segment: string = this.url.substring(from, to);
+ segments.push(segment);
+ i = to;
+ }
+ return segments;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/validation-icon.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/validation-icon.component.html
new file mode 100644
index 000000000..36ccdeeac
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/validation-icon.component.html
@@ -0,0 +1,3 @@
+ ({{ numErrors() }})
\ No newline at end of file
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/validation-icon.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/validation-icon.component.ts
new file mode 100644
index 000000000..fa5feba93
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/common/validation-icon.component.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Input, Output} from "@angular/core";
+import {OasValidationError} from "oai-ts-core";
+
+@Component({
+ moduleId: module.id,
+ selector: "validation-icon",
+ templateUrl: "validation-icon.component.html"
+})
+export class ValidationIconComponent {
+
+ @Input() validationErrors: OasValidationError[] = [];
+ @Output() onClick: EventEmitter = new EventEmitter();
+
+ public message(): string {
+ if (this.hasErrors()) {
+ return "Found " + this.numErrors() + " validation problems."
+ } else {
+ return "No validation errors/warnings found.";
+ }
+ }
+
+ public hasErrors(): boolean {
+ return this.validationErrors.length > 0;
+ }
+
+ public numErrors(): number {
+ return this.validationErrors.length;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-definition.component.css b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-definition.component.css
new file mode 100644
index 000000000..35f2cb2fe
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-definition.component.css
@@ -0,0 +1,3 @@
+.example-section .example button {
+ margin-top: 5px;
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-definition.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-definition.component.html
new file mode 100644
index 000000000..ca86f1dac
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-definition.component.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
Add Definition
+
+
+
Enter a new definition name below and then click Add.
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-definition.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-definition.component.ts
new file mode 100644
index 000000000..c0f130945
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-definition.component.ts
@@ -0,0 +1,171 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+import {AceEditorDirective} from "ng2-ace-editor";
+import {Subject} from "rxjs/Subject";
+import {Oas20SchemaDefinition, Oas30SchemaDefinition, OasDocument, OasLibraryUtils, OasVisitorUtil} from "oai-ts-core";
+import {FindSchemaDefinitionsVisitor} from "../../_visitors/schema-definitions.visitor";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "add-definition-dialog",
+ templateUrl: "add-definition.component.html",
+ styleUrls: ["add-definition.component.css"]
+})
+export class AddDefinitionDialogComponent {
+
+ @Output() onAdd: EventEmitter = new EventEmitter();
+
+ @ViewChildren("addDefinitionModal") addDefinitionModal: QueryList;
+ @ViewChildren("exampleEditor") exampleEditor: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected name: string = "";
+
+ protected example: string = "";
+ protected exampleValid: boolean = true;
+ protected exampleFormattable: boolean = false;
+
+ protected defChanged: Subject = new Subject();
+ protected defs: string[] = [];
+ protected defExists: boolean = false;
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(document: OasDocument): void {
+ this._isOpen = true;
+ this.name = "";
+ this.example = "";
+ this.exampleValid = true;
+ this.exampleFormattable = false;
+
+ this.addDefinitionModal.changes.subscribe( () => {
+ if (this.addDefinitionModal.first) {
+ this.addDefinitionModal.first.show();
+ }
+ });
+
+ this.defs = [];
+ this.defExists = false;
+ let definitions: (Oas20SchemaDefinition | Oas30SchemaDefinition)[] = this.getDefinitions(document);
+ definitions.forEach( definition => {
+ this.defs.push(FindSchemaDefinitionsVisitor.definitionName(definition));
+ });
+ this.defChanged
+ .debounceTime(300)
+ .distinctUntilChanged()
+ .subscribe( def => {
+ this.defExists = this.defs.indexOf(def) != -1;
+ });
+ }
+
+ private getDefinitions(document: OasDocument): (Oas20SchemaDefinition | Oas30SchemaDefinition)[] {
+ let vizzy: FindSchemaDefinitionsVisitor = new FindSchemaDefinitionsVisitor(null);
+ OasVisitorUtil.visitTree(document, vizzy);
+ return vizzy.getSortedSchemaDefinitions()
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ }
+
+ /**
+ * Called when the user clicks "add".
+ */
+ protected add(): void {
+ let data: any = {
+ name: this.name,
+ example: this.example
+ };
+ this.onAdd.emit(data);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.addDefinitionModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+ /**
+ * Called when the user changes the example definition in the Add Definition modal dialog.
+ * @param definition
+ */
+ public setExampleDefinition(definition: string): void {
+ this.example = definition;
+ if (this.example === "") {
+ this.exampleValid = true;
+ this.exampleFormattable = false;
+ } else {
+ try {
+ JSON.parse(this.example);
+ this.exampleValid = true;
+ this.exampleFormattable = true;
+ } catch (e) {
+ this.exampleValid = false;
+ this.exampleFormattable = false;
+ }
+ }
+ }
+
+ /**
+ * Returns true if it's possible to format the example definition (it must be non-null and
+ * syntactically valid).
+ * @return {boolean}
+ */
+ public isExampleDefinitionFormattable(): boolean {
+ return this.exampleFormattable;
+ }
+
+ /**
+ * Returns true if the example definition added by the user in the Add Definition modal
+ * dialog is valid (syntactically).
+ * @return {boolean}
+ */
+ public isExampleDefinitionValid(): boolean {
+ return this.exampleValid;
+ }
+
+ /**
+ * Called to format the example definition.
+ */
+ public formatExampleDefinition(): void {
+ if (this.exampleEditor.first) {
+ let jsObj: any = JSON.parse(this.example);
+ let nsrcStr: string = JSON.stringify(jsObj, null, 4);
+ this.exampleEditor.first.setText(nsrcStr);
+ }
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-formData-param.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-formData-param.component.html
new file mode 100644
index 000000000..f44eb77e6
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-formData-param.component.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
Add Form Data Parameter
+
+
+
Enter information about the new form data parameter below and then click Add.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-formData-param.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-formData-param.component.ts
new file mode 100644
index 000000000..8c8ae0b8b
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-formData-param.component.ts
@@ -0,0 +1,84 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "add-formData-param-dialog",
+ templateUrl: "add-formData-param.component.html"
+})
+export class AddFormDataParamDialogComponent {
+
+ @Output() onAdd: EventEmitter = new EventEmitter();
+
+ @ViewChildren("addFormDataParamModal") addFormDataParamModal: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected name: string = "";
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(name?: string): void {
+ this.name = name;
+ if (!name) {
+ this.name = "";
+ }
+ this._isOpen = true;
+ this.addFormDataParamModal.changes.subscribe( thing => {
+ if (this.addFormDataParamModal.first) {
+ this.addFormDataParamModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ this.name = "";
+ }
+
+ /**
+ * Called when the user clicks "add".
+ */
+ protected add(): void {
+ this.onAdd.emit(this.name);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.addFormDataParamModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-media-type.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-media-type.component.html
new file mode 100644
index 000000000..04df9e0d3
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-media-type.component.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
Add Media Type
+
+
+
Enter a new resource mediaType below and then click Add.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-media-type.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-media-type.component.ts
new file mode 100644
index 000000000..944ec8588
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-media-type.component.ts
@@ -0,0 +1,93 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, ElementRef, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "add-media-type-dialog",
+ templateUrl: "add-media-type.component.html"
+})
+export class AddMediaTypeDialogComponent {
+
+ @Output() onAdd: EventEmitter = new EventEmitter();
+
+ @ViewChildren("addMediaTypeModal") addMediaTypeModal: QueryList;
+ @ViewChildren("addMediaTypeInput") addMediaTypeInput: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected mediaType: string = "";
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(mediaType?: string): void {
+ this.mediaType = mediaType;
+ if (!mediaType) {
+ this.mediaType = "application/json";
+ }
+ this._isOpen = true;
+ this.addMediaTypeModal.changes.subscribe( thing => {
+ if (this.addMediaTypeModal.first) {
+ this.addMediaTypeModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ this.mediaType = "";
+ }
+
+ /**
+ * Called when the user clicks "add".
+ */
+ protected add(): void {
+ this.onAdd.emit(this.mediaType);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.addMediaTypeModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+ /**
+ * Called to initialize the selection/focus to the addMediaTypeInput field.
+ */
+ public doSelect(): void {
+ this.addMediaTypeInput.first.nativeElement.focus();
+ this.addMediaTypeInput.first.nativeElement.selectionStart = this.addMediaTypeInput.first.nativeElement.selectionEnd = this.mediaType.length
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-path.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-path.component.html
new file mode 100644
index 000000000..c17792f0b
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-path.component.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
Add Path
+
+
+
Enter a new resource path below and then click Add.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-path.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-path.component.ts
new file mode 100644
index 000000000..73d7dc8ef
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-path.component.ts
@@ -0,0 +1,119 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, ElementRef, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+import {OasDocument, OasLibraryUtils} from "oai-ts-core";
+import {FormControl} from "@angular/forms";
+import {Subject} from "rxjs/Subject";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "add-path-dialog",
+ templateUrl: "add-path.component.html"
+})
+export class AddPathDialogComponent {
+
+ @Output() onAdd: EventEmitter = new EventEmitter();
+
+ @ViewChildren("addPathModal") addPathModal: QueryList;
+ @ViewChildren("addPathInput") addPathInput: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected path: string = "";
+
+ protected pathChanged: Subject = new Subject();
+ protected paths: string[] = [];
+ protected pathExists: boolean = false;
+
+ /**
+ * Called to open the dialog.
+ * @param {OasDocument} document
+ * @param {string} path
+ */
+ public open(document: OasDocument, path?: string): void {
+ this.path = path;
+ if (!path) {
+ this.path = "";
+ }
+ if (!this.path.endsWith("/")) {
+ this.path = this.path + "/";
+ }
+ this._isOpen = true;
+ this.addPathModal.changes.subscribe( thing => {
+ if (this.addPathModal.first) {
+ this.addPathModal.first.show();
+ }
+ });
+
+ this.paths = [];
+ this.pathExists = false;
+ if (document.paths) {
+ document.paths.pathItems().forEach( pathItem => {
+ this.paths.push(pathItem.path());
+ });
+ this.pathChanged
+ .debounceTime(300)
+ .distinctUntilChanged()
+ .subscribe( path => {
+ this.pathExists = this.paths.indexOf(path) != -1;
+ });
+ }
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ this.path = "";
+ }
+
+ /**
+ * Called when the user clicks "add".
+ */
+ protected add(): void {
+ this.onAdd.emit(this.path);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.addPathModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+ /**
+ * Called to initialize the selection/focus to the addPathInput field.
+ */
+ public doSelect(): void {
+ this.addPathInput.first.nativeElement.focus();
+ this.addPathInput.first.nativeElement.selectionStart = this.addPathInput.first.nativeElement.selectionEnd = this.path.length
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-query-param.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-query-param.component.html
new file mode 100644
index 000000000..f0bc6acc1
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-query-param.component.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
Add Query Parameter
+
+
+
Enter information about the new query parameter below and then click Add.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-query-param.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-query-param.component.ts
new file mode 100644
index 000000000..70c9332f7
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-query-param.component.ts
@@ -0,0 +1,84 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "add-query-param-dialog",
+ templateUrl: "add-query-param.component.html"
+})
+export class AddQueryParamDialogComponent {
+
+ @Output() onAdd: EventEmitter = new EventEmitter();
+
+ @ViewChildren("addQueryParamModal") addQueryParamModal: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected name: string = "";
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(name?: string): void {
+ this.name = name;
+ if (!name) {
+ this.name = "";
+ }
+ this._isOpen = true;
+ this.addQueryParamModal.changes.subscribe( thing => {
+ if (this.addQueryParamModal.first) {
+ this.addQueryParamModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ this.name = "";
+ }
+
+ /**
+ * Called when the user clicks "add".
+ */
+ protected add(): void {
+ this.onAdd.emit(this.name);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.addQueryParamModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-response.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-response.component.html
new file mode 100644
index 000000000..4d560dd1d
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-response.component.html
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
Add Response
+
+
+
Enter information about the new response below and then click Add.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-response.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-response.component.ts
new file mode 100644
index 000000000..c0d86edb3
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-response.component.ts
@@ -0,0 +1,93 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "add-response-dialog",
+ templateUrl: "add-response.component.html"
+})
+export class AddResponseDialogComponent {
+
+ @Output() onAdd: EventEmitter = new EventEmitter();
+
+ @ViewChildren("addResponseModal") addResponseModal: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected statusCode: string = "";
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(statusCode?: string): void {
+ this.statusCode = statusCode;
+ if (!statusCode) {
+ this.statusCode = "";
+ }
+ this._isOpen = true;
+ this.addResponseModal.changes.subscribe( thing => {
+ if (this.addResponseModal.first) {
+ this.addResponseModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ this.statusCode = "";
+ }
+
+ /**
+ * Called when the user clicks "add".
+ */
+ protected add(): void {
+ this.onAdd.emit(this.statusCode);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.addResponseModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+ /**
+ * Returns true if today is the first of April. (teapot related)
+ * @return {boolean}
+ */
+ public isAprilFirst(): boolean {
+ let d: Date = new Date();
+ return d.getMonth() === 3 && d.getDate() === 1;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-schema-property.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-schema-property.component.html
new file mode 100644
index 000000000..a6d605d06
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-schema-property.component.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
Add Schema Property
+
+
+
Enter information about the new schema property below and then click Add.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-schema-property.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-schema-property.component.ts
new file mode 100644
index 000000000..57abf8194
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-schema-property.component.ts
@@ -0,0 +1,84 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "add-schema-property-dialog",
+ templateUrl: "add-schema-property.component.html"
+})
+export class AddSchemaPropertyDialogComponent {
+
+ @Output() onAdd: EventEmitter = new EventEmitter();
+
+ @ViewChildren("addSchemaPropertyModal") addSchemaPropertyModal: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected name: string = "";
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(name?: string): void {
+ this.name = name;
+ if (!name) {
+ this.name = "";
+ }
+ this._isOpen = true;
+ this.addSchemaPropertyModal.changes.subscribe( thing => {
+ if (this.addSchemaPropertyModal.first) {
+ this.addSchemaPropertyModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ this.name = "";
+ }
+
+ /**
+ * Called when the user clicks "add".
+ */
+ protected add(): void {
+ this.onAdd.emit(this.name);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.addSchemaPropertyModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-server.component.css b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-server.component.css
new file mode 100644
index 000000000..8795f7a13
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-server.component.css
@@ -0,0 +1,3 @@
+#addServerModal .modal-dialog .modal-body form textarea {
+ min-height: 60px;
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-server.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-server.component.html
new file mode 100644
index 000000000..0a8784bac
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-server.component.html
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+ Add Server
+ Edit Server
+
+
+
+
Enter information about the new server below and then click OK.
+
Make changes to the server details below and click OK.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-server.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-server.component.ts
new file mode 100644
index 000000000..b21ba6ac4
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-server.component.ts
@@ -0,0 +1,199 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+import {Oas30Server} from "oai-ts-core";
+
+
+export interface ServerVariableData {
+ default: string;
+ description: string;
+ enum: string[];
+}
+
+export interface ServerEventData {
+ url: string;
+ description: string;
+ variables: any; // map of string to ServerVariableData
+}
+
+
+@Component({
+ moduleId: module.id,
+ selector: "add-server-dialog",
+ templateUrl: "add-server.component.html",
+ styleUrls: ["add-server.component.css"]
+})
+export class AddServerDialogComponent {
+
+ @Output() onAdd: EventEmitter = new EventEmitter();
+ @Output() onChange: EventEmitter = new EventEmitter();
+
+ @ViewChildren("addServerModal") addServerModal: QueryList;
+
+ protected mode: string;
+
+ protected _isOpen: boolean = false;
+
+ protected model: ServerEventData = {
+ url: "",
+ description: "",
+ variables: {}
+ };
+
+ protected _varSelected: string = null;
+
+ /**
+ * Called to open the dialog.
+ * @param {Oas30Server} server
+ */
+ public open(server?: Oas30Server): void {
+ if (server) {
+ this.mode = "edit";
+ this.model.url = server.url;
+ this.model.description = server.description;
+ this.model.variables = {};
+ server.getServerVariables().forEach( variable => {
+ this.model.variables[variable.name()] = {
+ "default": variable.default,
+ "description": variable.description,
+ "enum": variable.enum
+ };
+ });
+ this.updateVariables();
+ } else {
+ this.mode = "create";
+ this.model = {
+ url: "",
+ description: "",
+ variables: {}
+ };
+ }
+ this._isOpen = true;
+ this.addServerModal.changes.subscribe( () => {
+ if (this.addServerModal.first) {
+ this.addServerModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called when the URL changes. The URL will be parsed to find any variable substitutions
+ * present in the URL spec. The map of variables is then updated to reflect whatever is found.
+ */
+ public updateVariables(): void {
+ console.info("[AddServerDialogComponent] Updating variables for URL: %s", this.model.url);
+
+ let url = this.model.url;
+ if (!url) {
+ this.model.variables = {};
+ }
+
+ console.info(" Executing regex against: %s", url);
+ let regex:RegExp = /{([^}]+)}/gi;
+ let result: RegExpExecArray;
+ let varNames: string[] = [];
+ while ( (result = regex.exec(url)) ) {
+ varNames.push(result[1]);
+ }
+ console.info(" Variable names found: %o", varNames);
+
+ let newVariables: any = {};
+
+ varNames.forEach( varName => {
+ if (this.model.variables[varName]) {
+ newVariables[varName] = this.model.variables[varName];
+ } else {
+ newVariables[varName] = {};
+ }
+ });
+
+ this.model.variables = newVariables;
+
+ this._varSelected = null;
+ if (varNames.length > 0) {
+ this._varSelected = varNames[0];
+ }
+ }
+
+ /**
+ * Returns true if there are any variables to be configured.
+ * @return {boolean}
+ */
+ public hasVariables(): boolean {
+ let rval: boolean = false;
+ if (this.model.variables) {
+ for (let v in this.model.variables) {
+ rval = true;
+ }
+ }
+ return rval;
+ }
+
+ /**
+ * Returns the names of the variables.
+ * @return {string[]}
+ */
+ public variableNames(): string[] {
+ let rval: string[] = [];
+ if (this.model.variables) {
+ for (let v in this.model.variables) {
+ rval.push(v);
+ }
+ }
+ return rval;
+
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ this.model.url = "";
+ this.model.description = "";
+ }
+
+ /**
+ * Called when the user clicks "ok".
+ */
+ protected ok(): void {
+ if (this.mode === "create") {
+ this.onAdd.emit(this.model);
+ } else {
+ this.onChange.emit(this.model);
+ }
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.addServerModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-tag.component.css b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-tag.component.css
new file mode 100644
index 000000000..90fdf0d08
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-tag.component.css
@@ -0,0 +1,3 @@
+#addTagModal .modal-dialog .modal-body form textarea {
+ min-height: 60px;
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-tag.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-tag.component.html
new file mode 100644
index 000000000..c886149ce
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-tag.component.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
Add Tag
+
+
+
Enter information about the new tag below and then click Add.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-tag.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-tag.component.ts
new file mode 100644
index 000000000..b762fc9b8
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/add-tag.component.ts
@@ -0,0 +1,99 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, ElementRef, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "add-tag-dialog",
+ templateUrl: "add-tag.component.html",
+ styleUrls: ["add-tag.component.css"]
+})
+export class AddTagDialogComponent {
+
+ @Output() onAdd: EventEmitter = new EventEmitter();
+
+ @ViewChildren("addTagModal") addTagModal: QueryList;
+ @ViewChildren("addTagInput") addTagInput: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected tag: string = "";
+ protected description: string = "";
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(tag?: string): void {
+ this.tag = tag;
+ if (!tag) {
+ this.tag = "";
+ }
+ this._isOpen = true;
+ this.addTagModal.changes.subscribe( thing => {
+ if (this.addTagModal.first) {
+ this.addTagModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ this.tag = "";
+ this.description = "";
+ }
+
+ /**
+ * Called when the user clicks "add".
+ */
+ protected add(): void {
+ let tagInfo: any = {
+ name: this.tag,
+ description: this.description
+ };
+ this.onAdd.emit(tagInfo);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.addTagModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+ /**
+ * Called to initialize the selection/focus to the addTagInput field.
+ */
+ public doSelect(): void {
+ this.addTagInput.first.nativeElement.focus();
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-definition.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-definition.component.html
new file mode 100644
index 000000000..6e1064f6a
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-definition.component.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
Clone Definition
+
+
+
Enter a new definition name below and then click Clone.
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-definition.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-definition.component.ts
new file mode 100644
index 000000000..39d2b61cb
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-definition.component.ts
@@ -0,0 +1,119 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+import {Oas20SchemaDefinition, Oas30SchemaDefinition, OasDocument, OasVisitorUtil} from "oai-ts-core";
+import {Subject} from "rxjs/Subject";
+import {FindSchemaDefinitionsVisitor} from "../../_visitors/schema-definitions.visitor";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "clone-definition-dialog",
+ templateUrl: "clone-definition.component.html"
+})
+export class CloneDefinitionDialogComponent {
+
+ @Output() onClone: EventEmitter = new EventEmitter();
+
+ @ViewChildren("cloneDefinitionModal") cloneDefinitionModal: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected name: string = "";
+ protected definition: Oas20SchemaDefinition | Oas30SchemaDefinition;
+
+ protected defChanged: Subject = new Subject();
+ protected defs: string[] = [];
+ protected defExists: boolean = false;
+
+ /**
+ * Called to open the dialog.
+ * @param {Oas20SchemaDefinition | Oas30SchemaDefinition} definition
+ */
+ public open(document: OasDocument, definition: Oas20SchemaDefinition | Oas30SchemaDefinition): void {
+ this._isOpen = true;
+ this.definition = definition;
+ this.name = "CloneOf";
+ if (this.definition.ownerDocument().getSpecVersion() === "2.0") {
+ this.name += (definition as Oas20SchemaDefinition).definitionName();
+ } else {
+ this.name += (definition as Oas30SchemaDefinition).name();
+ }
+
+ this.cloneDefinitionModal.changes.subscribe( () => {
+ if (this.cloneDefinitionModal.first) {
+ this.cloneDefinitionModal.first.show();
+ }
+ });
+
+ this.defs = [];
+ this.defExists = false;
+ let definitions: (Oas20SchemaDefinition | Oas30SchemaDefinition)[] = this.getDefinitions(document);
+ definitions.forEach( definition => {
+ this.defs.push(FindSchemaDefinitionsVisitor.definitionName(definition));
+ });
+ this.defChanged
+ .debounceTime(300)
+ .distinctUntilChanged()
+ .subscribe( def => {
+ this.defExists = this.defs.indexOf(def) != -1;
+ });
+ }
+
+ private getDefinitions(document: OasDocument): (Oas20SchemaDefinition | Oas30SchemaDefinition)[] {
+ let vizzy: FindSchemaDefinitionsVisitor = new FindSchemaDefinitionsVisitor(null);
+ OasVisitorUtil.visitTree(document, vizzy);
+ return vizzy.getSortedSchemaDefinitions()
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ }
+
+ /**
+ * Called when the user clicks "clone".
+ */
+ protected clone(): void {
+ let data: any = {
+ name: this.name,
+ definition: this.definition
+ };
+ this.onClone.emit(data);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.cloneDefinitionModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-path.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-path.component.html
new file mode 100644
index 000000000..db083b8ca
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-path.component.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
Clone Path
+
+
+
Enter a new resource path below and then click Clone.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-path.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-path.component.ts
new file mode 100644
index 000000000..95efaaee4
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/clone-path.component.ts
@@ -0,0 +1,121 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, ElementRef, EventEmitter, Output, QueryList, ViewChildren} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+import {OasDocument, OasPathItem} from "oai-ts-core";
+import {Subject} from "rxjs/Subject";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "clone-path-dialog",
+ templateUrl: "clone-path.component.html"
+})
+export class ClonePathDialogComponent {
+
+ @Output() onClone: EventEmitter = new EventEmitter();
+
+ @ViewChildren("clonePathModal") clonePathModal: QueryList;
+ @ViewChildren("clonePathInput") clonePathInput: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ protected path: string = "";
+ protected object: OasPathItem;
+
+ protected pathChanged: Subject = new Subject();
+ protected paths: string[] = [];
+ protected pathExists: boolean = false;
+
+ /**
+ * Called to open the dialog.
+ * @param {OasDocument} document
+ * @param {OasPathItem} path
+ */
+ public open(document: OasDocument, path: OasPathItem): void {
+ this.object = path;
+ this.path = path.path();
+ if (!this.path.endsWith("/")) {
+ this.path = this.path + "/";
+ }
+ this._isOpen = true;
+ this.clonePathModal.changes.subscribe( () => {
+ if (this.clonePathModal.first) {
+ this.clonePathModal.first.show();
+ }
+ });
+
+ this.paths = [];
+ this.pathExists = false;
+ if (document.paths) {
+ document.paths.pathItems().forEach( pathItem => {
+ this.paths.push(pathItem.path());
+ });
+ this.pathChanged
+ .debounceTime(300)
+ .distinctUntilChanged()
+ .subscribe( path => {
+ this.pathExists = this.paths.indexOf(path) != -1;
+ });
+ }
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ this.path = "";
+ }
+
+ /**
+ * Called when the user clicks "clone".
+ */
+ protected clone(): void {
+ let modalData: any = {
+ path: this.path,
+ object: this.object
+ };
+ this.onClone.emit(modalData);
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.clonePathModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+ /**
+ * Called to initialize the selection/focus to the clonePathInput field.
+ */
+ public doSelect(): void {
+ this.clonePathInput.first.nativeElement.focus();
+ this.clonePathInput.first.nativeElement.selectionStart = this.clonePathInput.first.nativeElement.selectionEnd = this.path.length
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/editor-disconnected.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/editor-disconnected.component.html
new file mode 100644
index 000000000..5f48fae49
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/editor-disconnected.component.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
Editor Disconnected
+
+
+
+
+ Editor Disconnected! The socket connecting you to the server has been dropped (unexpectedly).
+ It is likely that none of your changes have been lost, but you must reload this page to reconnect.
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/editor-disconnected.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/editor-disconnected.component.ts
new file mode 100644
index 000000000..f8d0deded
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/editor-disconnected.component.ts
@@ -0,0 +1,67 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, ViewChildren, QueryList} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "editor-disconnected-dialog",
+ templateUrl: "editor-disconnected.component.html"
+})
+export class EditorDisconnectedDialogComponent {
+
+ @ViewChildren("editorDisconnectedModal") editorDisconnectedModal: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(): void {
+ this._isOpen = true;
+ this.editorDisconnectedModal.changes.subscribe( thing => {
+ if (this.editorDisconnectedModal.first) {
+ this.editorDisconnectedModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ }
+
+ /**
+ * Called when the user clicks "Reload Page".
+ */
+ protected reload(): void {
+ window.location.reload();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/security-scheme-20.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/security-scheme-20.component.html
new file mode 100644
index 000000000..008fb565d
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/security-scheme-20.component.html
@@ -0,0 +1,165 @@
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/set-license.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/set-license.component.ts
new file mode 100644
index 000000000..596b5a4f5
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/dialogs/set-license.component.ts
@@ -0,0 +1,106 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, Output, EventEmitter, ViewChildren, QueryList} from "@angular/core";
+import {ModalDirective} from "ngx-bootstrap";
+import {LicenseService, ILicense} from "../../_services/license.service";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "set-license-dialog",
+ templateUrl: "set-license.component.html"
+})
+export class SetLicenseDialogComponent {
+
+ private static licenseService: LicenseService = new LicenseService();
+
+ @Output() onLicenseChosen: EventEmitter = new EventEmitter();
+
+ @ViewChildren("setLicenseModal") setLicenseModal: QueryList;
+
+ protected _isOpen: boolean = false;
+
+ /**
+ * Called to open the dialog.
+ */
+ public open(): void {
+ this._isOpen = true;
+ this.setLicenseModal.changes.subscribe( thing => {
+ if (this.setLicenseModal.first) {
+ this.setLicenseModal.first.show();
+ }
+ });
+ }
+
+ /**
+ * Called to close the dialog.
+ */
+ public close(): void {
+ this._isOpen = false;
+ }
+
+ /**
+ * Called when the user clicks "add".
+ */
+ protected setLicense(): void {
+ this.onLicenseChosen.emit({});
+ this.cancel();
+ }
+
+ /**
+ * Called when the user clicks "cancel".
+ */
+ protected cancel(): void {
+ this.setLicenseModal.first.hide();
+ }
+
+ /**
+ * Returns true if the dialog is open.
+ * @return {boolean}
+ */
+ public isOpen(): boolean {
+ return this._isOpen;
+ }
+
+ /**
+ * Returns a list of possible licenses.
+ */
+ public licenses(): ILicense[] {
+ return SetLicenseDialogComponent.licenseService.getLicenses();
+ }
+
+ /**
+ * Returns the license service.
+ */
+ public licenseService(): LicenseService {
+ return SetLicenseDialogComponent.licenseService;
+ }
+
+ /**
+ * Called when the user picks a license.
+ * @param license
+ */
+ public chooseLicense(license: ILicense): void {
+ this.onLicenseChosen.emit({
+ name: license.name,
+ url: license.url
+ });
+ this.cancel();
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/definition-form.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/definition-form.component.html
new file mode 100644
index 000000000..9f80d0405
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/definition-form.component.html
@@ -0,0 +1,104 @@
+
+ Is your API licensed?
+ Configure that here! We'll help by showing you the most common licenses
+ you can choose from (and even describe each one a little bit!).
+
+
+
+
+
+
+
+
+ None Found
+ No license has been configured for this API.
+
+
You have configured a license that we are not familiar with. For information about the license, click the link above! Or else click the button below to choose a different license...
+ Use this section to configure your API's security options. There are three types of
+ security you may choose from (each with its own specific configuration details):
+
+ This section is visible because the path/endpoint this operation belongs to has dynamic parameters.
+ Configure the parameters' descriptions and types below.
+
+ Use this section to configure the operation's Request Body. Typically a
+ request body is used for PUT and POST only. Other operation
+ types usually do not include a body.
+
+
+
+
+
+
+
+
+ None Found
+ No request body has been declared.
+
+
+
+
+
+
+ Not Recommended
+ It is unusual and not recommended to define a request body for a GET operation.
+
+
+
+ It is
+
+ that clients include the request body.
+
+ An operation may, optionally, allow additional options to be sent via URL query
+ parameters. This section allows you to document what query parameters are
+ accepted/expected by this operation.
+
+
+
+
+
+
+
+
+
+ None Found
+ No query parameters have been declared.
+
+
+ Every operation must include at least one Response. Multiple responses (with
+ different HTTP response codes) are possible for each operation - so add as many as you like.
+ Some common choices for HTTP responses are:
+
+
+
+
200 OK - successful GET (retrieve data) operations
+
201 Created - successful POST (create) operations
+
204 No Content - successful PUT (update) operations
+
400 Bad Request - failed to PUT/POST something due to incorrect input
+
404 Not Found - failed to GET something
+
+
+
+
+
+
+
+
+
+ None Found
+ No responses have been declared.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation-30-form.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation-30-form.component.ts
new file mode 100644
index 000000000..12f7543f5
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation-30-form.component.ts
@@ -0,0 +1,387 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from "@angular/core";
+import {Oas30MediaType, Oas30Operation, Oas30Parameter, Oas30PathItem, Oas30Response, OasPathItem} from "oai-ts-core";
+import {
+ createChangeMediaTypeTypeCommand,
+ createChangeParameterTypeCommand,
+ createChangePropertyCommand,
+ createDeleteAllParametersCommand,
+ createDeleteMediaTypeCommand,
+ createDeleteParameterCommand,
+ createDeleteResponseCommand,
+ createNewMediaTypeCommand,
+ createNewParamCommand,
+ createNewRequestBodyCommand,
+ createNewResponseCommand,
+ createReplaceOperationCommand,
+ createDeleteOperationCommand,
+ createDeleteAllResponsesCommand,
+ createDeleteRequestBodyCommand,
+ SimplifiedParameterType, ICommand
+} from "oai-ts-commands";
+import {AddQueryParamDialogComponent} from "../dialogs/add-query-param.component";
+import {AddResponseDialogComponent} from "../dialogs/add-response.component";
+import {SourceFormComponent} from "./source-form.base";
+import {ModelUtils} from "../../_util/model.util";
+import {ObjectUtils} from "../../_util/object.util";
+import {MediaTypeChangeEvent} from "./operation/content.component";
+import {DropDownOption} from '../../../../../../components/common/drop-down.component';
+
+
+@Component({
+ moduleId: module.id,
+ selector: "operation-30-form",
+ templateUrl: "operation-30-form.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class Operation30FormComponent extends SourceFormComponent {
+
+ protected _operation: Oas30Operation;
+ @Input()
+ set operation(operation: Oas30Operation) {
+ this._operation = operation;
+ this.sourceNode = operation;
+ }
+ get operation(): Oas30Operation {
+ return this._operation;
+ }
+
+ @Output() onDeselect: EventEmitter = new EventEmitter();
+
+ @ViewChild("addQueryParamDialog") public addQueryParamDialog: AddQueryParamDialogComponent;
+ @ViewChild("addResponseDialog") public addResponseDialog: AddResponseDialogComponent;
+
+ protected createEmptyNodeForSource(): Oas30Operation {
+ return (this.operation.parent()).createOperation(this.operation.method());
+ }
+
+ protected createReplaceNodeCommand(node: Oas30Operation): ICommand {
+ return createReplaceOperationCommand(this.operation.ownerDocument(), this.operation, node);
+ }
+
+ public summary(): string {
+ if (this.operation.summary) {
+ return this.operation.summary;
+ } else {
+ return null;
+ }
+ }
+
+ public hasSummary(): boolean {
+ if (this.operation.summary) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public description(): string {
+ if (this.operation.description) {
+ return this.operation.description;
+ } else {
+ return null;
+ }
+ }
+
+ public hasDescription(): boolean {
+ if (this.operation.description) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public bodyDescription(): string {
+ if (this.operation.requestBody) {
+ return this.operation.requestBody.description;
+ }
+ return null;
+ }
+
+ public parameters(paramType: string): Oas30Parameter[] {
+ let params: Oas30Parameter[] = this.operation.getParameters(paramType) as Oas30Parameter[];
+ return params.sort((param1, param2) => {
+ return param1.name.localeCompare(param2.name);
+ });
+ }
+
+ public pathParam(paramName: string): Oas30Parameter {
+ let param: Oas30Parameter = this.operation.parameter("path", paramName) as Oas30Parameter;
+
+ if (param === null) {
+ param = this.operation.createParameter();
+ param.in = "path";
+ param.name = paramName;
+ param.required = true;
+ param.n_attribute("missing", true);
+ }
+
+ return param;
+ }
+
+ public pathParameters(): Oas30Parameter[] {
+ let pathParamNames: string[] = ModelUtils.detectPathParamNames((this.operation.parent()).path());
+ return pathParamNames.map( pname => {
+ return this.pathParam(pname);
+ });
+ }
+
+ public queryParameters(): Oas30Parameter[] {
+ let opParams: Oas30Parameter[] = this.parameters("query");
+ let piParams: Oas30Parameter[] = (this.operation.parent()).getParameters("query") as Oas30Parameter[];
+ let hasOpParam = function(param: Oas30Parameter): boolean {
+ var found: boolean = false;
+ opParams.forEach( opParam => {
+ if (opParam.name === param.name) {
+ found = true;
+ }
+ });
+ return found;
+ };
+ piParams.forEach( param => {
+ if (!hasOpParam(param)) {
+ let missingParam: Oas30Parameter = this.operation.createParameter();
+ missingParam.in = "query";
+ missingParam.name = param.name;
+ missingParam.required = true;
+ missingParam.n_attribute("missing", true);
+ opParams.push(missingParam);
+ }
+ });
+ return opParams.sort((param1, param2) => {
+ return param1.name.localeCompare(param2.name);
+ });
+ }
+
+ public headerParameters(): Oas30Parameter[] {
+ return this.parameters("header");
+ }
+
+ public formDataParameters(): Oas30Parameter[] {
+ return this.parameters("formData");
+ }
+
+ public hasFormDataParams(): boolean {
+ return this.hasParameters("formData");
+ }
+
+ public hasQueryParameters(): boolean {
+ return this.operation.getParameters("query").length > 0 ||
+ (this.operation.parent()).getParameters("query").length > 0;
+ }
+
+ public canHavePathParams(): boolean {
+ return (this.operation.parent()).path().indexOf("{") !== -1;
+ }
+
+ public hasParameters(type: string): boolean {
+ if (!this.operation.parameters) {
+ return false;
+ }
+ return this.operation.parameters.filter((value) => {
+ return value.in === type;
+ }).length > 0;
+ }
+
+ public responses(): Oas30Response[] {
+ if (!this.operation.responses) {
+ return [];
+ }
+ let rval: Oas30Response[] = [];
+ for (let scode of this.operation.responses.responseStatusCodes()) {
+ let response: Oas30Response = this.operation.responses.response(scode) as Oas30Response;
+ rval.push(response);
+ }
+ return rval.sort((a, b) => {
+ return a.statusCode().localeCompare(b.statusCode());
+ });
+ }
+
+ public hasRequestBody(): boolean {
+ return !ObjectUtils.isNullOrUndefined(this.operation.requestBody);
+ }
+
+ public hasResponses(): boolean {
+ if (!this.operation.responses) {
+ return false;
+ }
+ if (this.operation.responses.responseStatusCodes().length === 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public changeSummary(newSummary: string): void {
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), this.operation,"summary", newSummary);
+ this.onCommand.emit(command);
+ }
+
+ public changeDescription(newDescription: string): void {
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), this.operation, "description", newDescription);
+ this.onCommand.emit(command);
+ }
+
+ public changeBodyDescription(newBodyDescription: string): void {
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), this.operation.requestBody,"description", newBodyDescription);
+ this.onCommand.emit(command);
+ }
+
+ public changeParamDescription(param: Oas30Parameter, newParamDescription: string): void {
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), param, "description", newParamDescription);
+ this.onCommand.emit(command);
+ }
+
+ public changeParamType(param: Oas30Parameter, newType: SimplifiedParameterType): void {
+ let command: ICommand = createChangeParameterTypeCommand(this.operation.ownerDocument(), param, newType);
+ this.onCommand.emit(command);
+ }
+
+ public changeResponseDescription(response: Oas30Response, newDescription: string): void {
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), response, "description", newDescription);
+ this.onCommand.emit(command);
+ }
+
+ public delete(): void {
+ let command: ICommand = createDeleteOperationCommand(this.operation.ownerDocument(), this.operation.method(),
+ this.operation.parent() as OasPathItem);
+ this.onCommand.emit(command);
+ this.onDeselect.emit(true);
+ }
+
+ public deleteAllQueryParams(): void {
+ let command: ICommand = createDeleteAllParametersCommand(this.operation.ownerDocument(), this.operation, "query");
+ this.onCommand.emit(command);
+ }
+
+ public deleteAllResponses(): void {
+ let command: ICommand = createDeleteAllResponsesCommand(this.operation.ownerDocument(), this.operation);
+ this.onCommand.emit(command);
+ }
+
+ public deleteParam(parameter: Oas30Parameter): void {
+ let command: ICommand = createDeleteParameterCommand(this.operation.ownerDocument(), parameter);
+ this.onCommand.emit(command);
+ }
+
+ public deleteResponse(response: Oas30Response): void {
+ let command: ICommand = createDeleteResponseCommand(this.operation.ownerDocument(), response);
+ this.onCommand.emit(command);
+ }
+
+ public openAddQueryParamModal(): void {
+ this.addQueryParamDialog.open();
+ }
+
+ public addQueryParam(name: string): void {
+ let command: ICommand = createNewParamCommand(this.operation.ownerDocument(), this.operation, name, "query");
+ this.onCommand.emit(command);
+ }
+
+ public addRequestBody(): void {
+ let command: ICommand = createNewRequestBodyCommand(this.operation.ownerDocument(), this.operation);
+ this.onCommand.emit(command);
+ }
+
+ public deleteRequestBody(): void {
+ let command: ICommand = createDeleteRequestBodyCommand(this.operation.ownerDocument(), this.operation);
+ this.onCommand.emit(command);
+ }
+
+ public createRequestBodyMediaType(mediaType: string): void {
+ console.info("[Operation30FormComponent] Creating request body media type: " + mediaType);
+ let command: ICommand = createNewMediaTypeCommand(this.operation.ownerDocument(), this.operation.requestBody, mediaType);
+ this.onCommand.emit(command);
+ }
+
+ public deleteRequestBodyMediaType(mediaType: string): void {
+ console.info("[Operation30FormComponent] Deleting request body media type: " + mediaType);
+ let mt: Oas30MediaType = this.operation.requestBody.getMediaType(mediaType);
+ let command: ICommand = createDeleteMediaTypeCommand(this.operation.ownerDocument(), mt);
+ this.onCommand.emit(command);
+ }
+
+ public changeRequestBodyMediaType(event: MediaTypeChangeEvent): void {
+ console.info("[Operation30FormComponent] Changing request body media type: " + event.name);
+ let mt: Oas30MediaType = this.operation.requestBody.getMediaType(event.name);
+ let command: ICommand = createChangeMediaTypeTypeCommand(this.operation.ownerDocument(), mt, event.type);
+ this.onCommand.emit(command);
+ }
+
+ public createResponseMediaType(response: Oas30Response, mediaType: string): void {
+ console.info("[Operation30FormComponent] Creating response media type: " + mediaType);
+ let command: ICommand = createNewMediaTypeCommand(this.operation.ownerDocument(), response, mediaType);
+ this.onCommand.emit(command);
+ }
+
+ public deleteResponseMediaType(response: Oas30Response, mediaType: string): void {
+ console.info("[Operation30FormComponent] Deleting response media type: " + mediaType);
+ let mt: Oas30MediaType = response.getMediaType(mediaType);
+ let command: ICommand = createDeleteMediaTypeCommand(this.operation.ownerDocument(), mt);
+ this.onCommand.emit(command);
+ }
+
+ public changeResponseMediaType(response: Oas30Response, event: MediaTypeChangeEvent): void {
+ console.info("[Operation30FormComponent] Changing response media type: " + event.name);
+ let mt: Oas30MediaType = response.getMediaType(event.name);
+ let command: ICommand = createChangeMediaTypeTypeCommand(this.operation.ownerDocument(), mt, event.type);
+ this.onCommand.emit(command);
+ }
+
+ public openAddResponseModal(): void {
+ this.addResponseDialog.open("200");
+ }
+
+ public addResponse(statusCode: string): void {
+ let command: ICommand = createNewResponseCommand(this.operation.ownerDocument(), this.operation, statusCode);
+ this.onCommand.emit(command);
+ }
+
+ public createPathParam(paramName: string): void {
+ let command: ICommand = createNewParamCommand(this.operation.ownerDocument(), this.operation, paramName, "path");
+ this.onCommand.emit(command);
+ }
+
+ public formType(): string {
+ return "operation";
+ }
+
+ public enableSourceMode(): void {
+ this.sourceNode = this.operation;
+ super.enableSourceMode();
+ }
+
+ public requestBodyRequiredOptions(): DropDownOption[] {
+ return [
+ { name: "Required", value: "required" },
+ { name: "Not Required", value: "not-required" }
+ ];
+ }
+
+ public changeRequestBodyRequired(value: string): void {
+ let isRequired: boolean = value === "required";
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), this.operation.requestBody, "required", isRequired);
+ this.onCommand.emit(command);
+ }
+
+ public parentPath() {
+ return (this.operation.parent() as OasPathItem).path()
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation-form.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation-form.component.html
new file mode 100644
index 000000000..2ef599a4d
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation-form.component.html
@@ -0,0 +1,295 @@
+
+ This section is visible because the path/endpoint this operation belongs to has dynamic parameters.
+ Configure the parameters' descriptions and types below.
+
+ Use this section to configure the operation's Request Body. Typically a
+ request body is used for PUT and POST only. Other operation
+ types usually do not include a body.
+
+
+
+
+
+
+
+
+
+ None Found
+ No request body has been declared.
+
+
+
+
+
+
+
+
+ Not Recommended
+ It is unusual and not recommended to define a request body for a GET operation.
+
+
+
+ Request Body Type:
+
+ of
+
+ as
+
+
+
+
+
+
+
+
+ Form Data Parameters Needed
+ You have indicated a desire to pass data via Form Data parameters but none have been defined.
+
+
+ An operation may, optionally, allow additional options to be sent via URL query
+ parameters. This section allows you to document what query parameters are
+ accepted/expected by this operation.
+
+
+
+
+
+
+
+
+
+ None Found
+ No query parameters have been declared.
+
+
+ Every operation must include at least one Response. Multiple responses (with
+ different HTTP response codes) are possible for each operation - so add as many as you like.
+ Some common choices for HTTP responses are:
+
+
+
+
200 OK - successful GET (retrieve data) operations
+
201 Created - successful POST (create) operations
+
204 No Content - successful PUT (update) operations
+
400 Bad Request - failed to PUT/POST something due to incorrect input
+
404 Not Found - failed to GET something
+
+
+
+
+
+
+
+
+
+ None Found
+ No responses have been declared.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation-form.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation-form.component.ts
new file mode 100644
index 000000000..3b35b1d15
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation-form.component.ts
@@ -0,0 +1,567 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from "@angular/core";
+import {Oas20Document, Oas20Operation, Oas20Parameter, Oas20PathItem, Oas20Response, OasPathItem} from "oai-ts-core";
+import {
+ createChangeParameterTypeCommand,
+ createChangePropertyCommand,
+ createChangeResponseTypeCommand,
+ createDeleteAllParametersCommand,
+ createDeleteParameterCommand,
+ createDeleteResponseCommand,
+ createNewParamCommand,
+ createNewRequestBodyCommand,
+ createNewResponseCommand,
+ createReplaceOperationCommand,
+ createDeleteOperationCommand,
+ createDeleteAllResponsesCommand,
+ SimplifiedParameterType, ICommand
+} from "oai-ts-commands";
+import {AddQueryParamDialogComponent} from "../dialogs/add-query-param.component";
+import {AddResponseDialogComponent} from "../dialogs/add-response.component";
+import {SourceFormComponent} from "./source-form.base";
+import {ObjectUtils} from "../../_util/object.util";
+import {SimplifiedType} from "oai-ts-commands";
+import {AddFormDataParamDialogComponent} from "../dialogs/add-formData-param.component";
+import {ModelUtils} from "../../_util/model.util";
+import {DropDownOption} from '../../../../../../components/common/drop-down.component';
+
+
+@Component({
+ moduleId: module.id,
+ selector: "operation-form",
+ templateUrl: "operation-form.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class OperationFormComponent extends SourceFormComponent {
+
+ protected _operation: Oas20Operation;
+ @Input()
+ set operation(operation: Oas20Operation) {
+ this._operation = operation;
+ this.sourceNode = operation;
+ }
+ get operation(): Oas20Operation {
+ return this._operation;
+ }
+
+ @Output() onDeselect: EventEmitter = new EventEmitter();
+
+ @ViewChild("addFormDataParamDialog") public addFormDataParamDialog: AddFormDataParamDialogComponent;
+ @ViewChild("addQueryParamDialog") public addQueryParamDialog: AddQueryParamDialogComponent;
+ @ViewChild("addResponseDialog") public addResponseDialog: AddResponseDialogComponent;
+
+ protected createEmptyNodeForSource(): Oas20Operation {
+ return (this.operation.parent()).createOperation(this.operation.method());
+ }
+
+ protected createReplaceNodeCommand(node: Oas20Operation): ICommand {
+ return createReplaceOperationCommand(this.operation.ownerDocument(), this.operation, node);
+ }
+
+ public summary(): string {
+ if (this.operation.summary) {
+ return this.operation.summary;
+ } else {
+ return null;
+ }
+ }
+
+ public hasSummary(): boolean {
+ if (this.operation.summary) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public description(): string {
+ if (this.operation.description) {
+ return this.operation.description;
+ } else {
+ return null;
+ }
+ }
+
+ public hasDescription(): boolean {
+ if (this.operation.description) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public bodyParam(): Oas20Parameter {
+ let params: Oas20Parameter[] = this.operation.parameters as Oas20Parameter[];
+ if (params) {
+ for (let param of params) {
+ if (param.in === "body") {
+ return param;
+ }
+ }
+ }
+ return null;
+ }
+
+ public requestBodyType(): string {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ if (bodyParam && bodyParam.schema) {
+ return SimplifiedType.fromSchema(bodyParam.schema).type;
+ }
+ return null;
+ }
+
+ public requestBodyTypeOf(): string {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ if (bodyParam && bodyParam.schema) {
+ let st: SimplifiedType = SimplifiedType.fromSchema(bodyParam.schema);
+ if (st.of) {
+ return st.of.type;
+ }
+ }
+ return null;
+ }
+
+ public requestBodyTypeAs(): string {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ if (bodyParam && bodyParam.schema) {
+ let st: SimplifiedType = SimplifiedType.fromSchema(bodyParam.schema);
+ if (st.isArray() && st.of) {
+ return st.of.as;
+ }
+ if (st.isSimpleType()) {
+ return st.as;
+ }
+ }
+ return null;
+ }
+
+ public hasBodyParam(): boolean {
+ if (this.bodyParam() !== null) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public requestBodyTypeOptions(): DropDownOption[] {
+ let options: DropDownOption[] = [
+ { value: "array", name: "Array" },
+ { divider: true },
+ { value: "string", name: "String" },
+ { value: "integer", name: "Integer" },
+ { value: "boolean", name: "Boolean" },
+ { value: "number", name: "Number" }
+ ];
+
+ let doc: Oas20Document = (this.operation.ownerDocument());
+ if (doc.definitions) {
+ let co: DropDownOption[] = doc.definitions.definitions().sort( (def1, def2) => {
+ return def1.definitionName().toLocaleLowerCase().localeCompare(def2.definitionName().toLocaleLowerCase());
+ }).map( def => {
+ return {
+ value: "#/definitions/" + def.definitionName(),
+ name: def.definitionName()
+ };
+ });
+ if (co && co.length > 0) {
+ options.push({ divider: true });
+ co.forEach( o => options.push(o) );
+ }
+ }
+
+ return options;
+ }
+
+ public requestBodyTypeOfOptions(): DropDownOption[] {
+ let options: DropDownOption[] = [
+ { value: "string", name: "String" },
+ { value: "integer", name: "Integer" },
+ { value: "boolean", name: "Boolean" },
+ { value: "number", name: "Number" }
+ ];
+
+ let doc: Oas20Document = (this.operation.ownerDocument());
+ if (doc.definitions) {
+ let co: DropDownOption[] = doc.definitions.definitions().sort( (def1, def2) => {
+ return def1.definitionName().toLocaleLowerCase().localeCompare(def2.definitionName().toLocaleLowerCase());
+ }).map( def => {
+ return {
+ value: "#/definitions/" + def.definitionName(),
+ name: def.definitionName()
+ };
+ });
+ if (co && co.length > 0) {
+ options.push({ divider: true });
+ co.forEach( o => options.push(o) );
+ }
+ }
+
+ return options;
+ }
+
+ public requestBodyTypeAsOptions(): DropDownOption[] {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ if (ObjectUtils.isNullOrUndefined(bodyParam)) {
+ return [];
+ }
+ if (ObjectUtils.isNullOrUndefined(bodyParam.schema)) {
+ return [];
+ }
+ let options: DropDownOption[] = [];
+ let st: SimplifiedType = SimplifiedType.fromSchema(this.bodyParam().schema);
+ if (st.isArray() && st.of && st.of.isSimpleType()) {
+ st = st.of;
+ }
+ if (st.type === "string") {
+ options = [
+ { value: null, name: "String" },
+ { value: "byte", name: "Byte" },
+ { value: "binary", name: "Binary" },
+ { value: "date", name: "Date" },
+ { value: "date-time", name: "DateTime" },
+ { value: "password", name: "Password" }
+ ];
+ } else if (st.type === "integer") {
+ options = [
+ { value: null, name: "Integer" },
+ { value: "int32", name: "32-Bit Integer" },
+ { value: "int64", name: "64-Bit Integer" }
+ ];
+ } else if (st.type === "number") {
+ options = [
+ { value: null, name: "Number" },
+ { value: "float", name: "Float" },
+ { value: "double", name: "Double" }
+ ];
+ }
+ return options;
+ }
+
+ public shouldShowRequestBodyTypeOf(): boolean {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ if (!bodyParam) {
+ return false;
+ }
+ let nt: SimplifiedType = SimplifiedType.fromSchema(bodyParam.schema);
+ return nt.isArray();
+ }
+
+ public shouldShowRequestBodyTypeAs(): boolean {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ if (!bodyParam) {
+ return false;
+ }
+ let nt: SimplifiedType = SimplifiedType.fromSchema(bodyParam.schema);
+ return (nt.isSimpleType() && nt.type !== "boolean") ||
+ (nt.isArray() && nt.of && nt.of.isSimpleType() && nt.of.type !== "boolean");
+ }
+
+ public changeRequestBodyType(newType: string): void {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ let nt: SimplifiedParameterType = new SimplifiedParameterType();
+ nt.type = newType;
+ let command: ICommand = createChangeParameterTypeCommand(this.operation.ownerDocument(), bodyParam, nt);
+ this.onCommand.emit(command);
+ }
+
+ public changeRequestBodyTypeOf(newOf: string): void {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ let newType: SimplifiedParameterType = SimplifiedParameterType.fromParameter(bodyParam);
+ newType.of = new SimplifiedType();
+ newType.of.type = newOf;
+ newType.as = null;
+ let command: ICommand = createChangeParameterTypeCommand(this.operation.ownerDocument(), bodyParam, newType);
+ this.onCommand.emit(command);
+ }
+
+ public changeRequestBodyTypeAs(newAs: string): void {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ let newType: SimplifiedParameterType = SimplifiedParameterType.fromParameter(bodyParam);
+ if (newType.isSimpleType()) {
+ newType.as = newAs;
+ }
+ if (newType.isArray() && newType.of) {
+ newType.of.as = newAs;
+ }
+ let command: ICommand = createChangeParameterTypeCommand(this.operation.ownerDocument(), bodyParam, newType);
+ this.onCommand.emit(command);
+ }
+
+ public bodyDescription(): string {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ if (bodyParam === null) {
+ return "";
+ }
+ if (bodyParam.description) {
+ return bodyParam.description;
+ } else {
+ return null;
+ }
+ }
+
+ public parameters(paramType: string): Oas20Parameter[] {
+ let params: Oas20Parameter[] = this.operation.getParameters(paramType) as Oas20Parameter[];
+ return params.sort((param1, param2) => {
+ return param1.name.localeCompare(param2.name);
+ });
+ }
+
+ public pathParam(paramName: string): Oas20Parameter {
+ let param: Oas20Parameter = this.operation.parameter("path", paramName) as Oas20Parameter;
+
+ if (param === null) {
+ param = this.operation.createParameter();
+ param.in = "path";
+ param.name = paramName;
+ param.required = true;
+ param.n_attribute("missing", true);
+ }
+
+ return param;
+ }
+
+ public pathParameters(): Oas20Parameter[] {
+ let pathParamNames: string[] = ModelUtils.detectPathParamNames((this.operation.parent()).path());
+ return pathParamNames.map( pname => {
+ return this.pathParam(pname);
+ });
+ }
+
+ public queryParameters(): Oas20Parameter[] {
+ let opParams: Oas20Parameter[] = this.parameters("query");
+ let piParams: Oas20Parameter[] = (this.operation.parent()).getParameters("query") as Oas20Parameter[];
+ let hasOpParam = function(param: Oas20Parameter): boolean {
+ var found: boolean = false;
+ opParams.forEach( opParam => {
+ if (opParam.name === param.name) {
+ found = true;
+ }
+ });
+ return found;
+ };
+ piParams.forEach( param => {
+ if (!hasOpParam(param)) {
+ let missingParam: Oas20Parameter = this.operation.createParameter();
+ missingParam.in = "query";
+ missingParam.name = param.name;
+ missingParam.required = true;
+ missingParam.n_attribute("missing", true);
+ opParams.push(missingParam);
+ }
+ });
+ return opParams.sort((param1, param2) => {
+ return param1.name.localeCompare(param2.name);
+ });
+ }
+
+ public headerParameters(): Oas20Parameter[] {
+ return this.parameters("header");
+ }
+
+ public formDataParameters(): Oas20Parameter[] {
+ return this.parameters("formData");
+ }
+
+ public hasFormDataParams(): boolean {
+ return this.hasParameters("formData");
+ }
+
+ public hasQueryParameters(): boolean {
+ return this.operation.getParameters("query").length > 0 ||
+ (this.operation.parent()).getParameters("query").length > 0;
+ }
+
+ public canHavePathParams(): boolean {
+ return (this.operation.parent()).path().indexOf("{") !== -1;
+ }
+
+ public hasParameters(type: string): boolean {
+ if (!this.operation.parameters) {
+ return false;
+ }
+ return this.operation.parameters.filter((value) => {
+ return value.in === type;
+ }).length > 0;
+ }
+
+ public responses(): Oas20Response[] {
+ if (!this.operation.responses) {
+ return [];
+ }
+ let rval: Oas20Response[] = [];
+ for (let scode of this.operation.responses.responseStatusCodes()) {
+ let response: Oas20Response = this.operation.responses.response(scode) as Oas20Response;
+ rval.push(response);
+ }
+ return rval.sort((a, b) => {
+ return a.statusCode().localeCompare(b.statusCode());
+ });
+ }
+
+ public hasResponses(): boolean {
+ if (!this.operation.responses) {
+ return false;
+ }
+ if (this.operation.responses.responseStatusCodes().length === 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public hasDefinitions(): boolean {
+ if (this.definitionNames()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public definitionNames(): string[] {
+ let doc: Oas20Document = this.operation.ownerDocument();
+ if (ObjectUtils.isNullOrUndefined(doc.definitions)) {
+ return [];
+ } else {
+ return doc.definitions.getItemNames().sort();
+ }
+ }
+
+ public changeSummary(newSummary: string): void {
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), this.operation,"summary", newSummary);
+ this.onCommand.emit(command);
+ }
+
+ public changeDescription(newDescription: string): void {
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), this.operation, "description", newDescription);
+ this.onCommand.emit(command);
+ }
+
+ public changeBodyDescription(newBodyDescription: string): void {
+ let bodyParam: Oas20Parameter = this.bodyParam();
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), bodyParam,"description", newBodyDescription);
+ this.onCommand.emit(command);
+ }
+
+ public changeParamDescription(param: Oas20Parameter, newParamDescription: string): void {
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), param, "description", newParamDescription);
+ this.onCommand.emit(command);
+ }
+
+ public changeParamType(param: Oas20Parameter, newType: SimplifiedParameterType): void {
+ let command: ICommand = createChangeParameterTypeCommand(this.operation.ownerDocument(), param, newType);
+ this.onCommand.emit(command);
+ }
+
+ public changeResponseType(response: Oas20Response, newType: SimplifiedType): void {
+ let command: ICommand = createChangeResponseTypeCommand(this.operation.ownerDocument(), response, newType);
+ this.onCommand.emit(command);
+ }
+
+ public changeResponseDescription(response: Oas20Response, newDescription: string): void {
+ let command: ICommand = createChangePropertyCommand(this.operation.ownerDocument(), response, "description", newDescription);
+ this.onCommand.emit(command);
+ }
+
+ public createRequestBody(): void {
+ let command: ICommand = createNewRequestBodyCommand(this.operation.ownerDocument(), this.operation);
+ this.onCommand.emit(command);
+ }
+
+ public delete(): void {
+ let command: ICommand = createDeleteOperationCommand(this.operation.ownerDocument(), this.operation.method(),
+ this.operation.parent() as OasPathItem);
+ this.onCommand.emit(command);
+ this.onDeselect.emit(true);
+ }
+
+ public deleteRequestBody(): void {
+ if (this.hasBodyParam()) {
+ let command: ICommand = createDeleteAllParametersCommand(this.operation.ownerDocument(), this.operation, "body");
+ this.onCommand.emit(command);
+ } else {
+ let command: ICommand = createDeleteAllParametersCommand(this.operation.ownerDocument(), this.operation, "formData");
+ this.onCommand.emit(command);
+ }
+ }
+
+ public deleteAllQueryParams(): void {
+ let command: ICommand = createDeleteAllParametersCommand(this.operation.ownerDocument(), this.operation, "query");
+ this.onCommand.emit(command);
+ }
+
+ public deleteAllResponses(): void {
+ let command: ICommand = createDeleteAllResponsesCommand(this.operation.ownerDocument(), this.operation);
+ this.onCommand.emit(command);
+ }
+
+ public deleteParam(parameter: Oas20Parameter): void {
+ let command: ICommand = createDeleteParameterCommand(this.operation.ownerDocument(), parameter);
+ this.onCommand.emit(command);
+ }
+
+ public deleteResponse(response: Oas20Response): void {
+ let command: ICommand = createDeleteResponseCommand(this.operation.ownerDocument(), response);
+ this.onCommand.emit(command);
+ }
+
+ public openAddQueryParamModal(): void {
+ this.addQueryParamDialog.open();
+ }
+
+ public openAddFormDataParamModal(): void {
+ this.addFormDataParamDialog.open();
+ }
+
+ public addQueryParam(name: string): void {
+ let command: ICommand = createNewParamCommand(this.operation.ownerDocument(), this.operation, name, "query");
+ this.onCommand.emit(command);
+ }
+
+ public addFormDataParam(name: string): void {
+ let command: ICommand = createNewParamCommand(this.operation.ownerDocument(), this.operation, name, "formData");
+ this.onCommand.emit(command);
+ }
+
+ public openAddResponseModal(): void {
+ this.addResponseDialog.open("200");
+ }
+
+ public addResponse(statusCode: string): void {
+ let command: ICommand = createNewResponseCommand(this.operation.ownerDocument(), this.operation, statusCode);
+ this.onCommand.emit(command);
+ }
+
+ public createPathParam(paramName: string): void {
+ let command: ICommand = createNewParamCommand(this.operation.ownerDocument(), this.operation, paramName, "path");
+ this.onCommand.emit(command);
+ }
+
+ public formType(): string {
+ return "operation";
+ }
+
+ public enableSourceMode(): void {
+ this.sourceNode = this.operation;
+ super.enableSourceMode();
+ }
+
+ public parentPath() {
+ return (this.operation.parent() as OasPathItem).path()
+ }
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation/content.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation/content.component.html
new file mode 100644
index 000000000..7bcf0562c
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/operation/content.component.html
@@ -0,0 +1,55 @@
+
+ This section is visible because this path has at least one dynamic path parameter.
+ Here you can configure information about the path parameter, such as its description
+ and type. This information will be inherited by all operations in the path (but can
+ optionally be overridden by each respective operation).
+
+ Use this section to define HTTP Query Parameters for all of the
+ Operations in this path. These query parameters will apply to all operations and
+ can be overridden (though not removed) at the operation level.
+
+
+
+
+
+
+
+
+
+ None Found
+ No query parameters have been declared.
+
+
+ Below you will find a list of all possible Operations that could be
+ supported by this path. You can quickly indicate that this path supports these operations
+ by clicking the appropriate Create Operation button. You can also quickly
+ edit the Name and Description of each operation directly
+ from this list.
+
+
+
+ Note: to configure a single operation (inputs, outputs, etc) simply
+ click the colored operation name such as GET or PUT.
+
+
+
+
+
+
+
+
GET
+
+
+
+
+
+
+
+
+
+
PUT
+
+
+
+
+
+
+
+
+
+
POST
+
+
+
+
+
+
+
+
+
+
DELETE
+
+
+
+
+
+
+
+
+
+
OPTIONS
+
+
+
+
+
+
+
+
+
+
HEAD
+
+
+
+
+
+
+
+
+
+
PATCH
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/path-form.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/path-form.component.ts
new file mode 100644
index 000000000..38b9047e3
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/path-form.component.ts
@@ -0,0 +1,318 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from "@angular/core";
+import {OasDocument, OasOperation, OasParameterBase, OasPathItem, OasPaths} from "oai-ts-core";
+import {SourceFormComponent} from "./source-form.base";
+import {ModelUtils} from "../../_util/model.util";
+import {
+ createAddPathItemCommand,
+ createChangeParameterTypeCommand,
+ createChangePropertyCommand,
+ createDeleteAllParametersCommand,
+ createDeleteParameterCommand,
+ createDeletePathCommand,
+ createNewOperationCommand,
+ createNewParamCommand,
+ createNewPathCommand,
+ createReplacePathItemCommand, ICommand,
+ SimplifiedParameterType
+} from "oai-ts-commands";
+import {AddQueryParamDialogComponent} from "../dialogs/add-query-param.component";
+import {ClonePathDialogComponent} from "../dialogs/clone-path.component";
+import {AddPathDialogComponent} from "../dialogs/add-path.component";
+import {NodeSelectionEvent} from "../../_events/node-selection.event";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "path-form",
+ templateUrl: "path-form.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class PathFormComponent extends SourceFormComponent {
+
+ protected _path: OasPathItem;
+ @Input()
+ set path(path: OasPathItem) {
+ this._path = path;
+ this.sourceNode = path;
+ }
+ get path(): OasPathItem {
+ return this._path;
+ }
+
+ @Output() onDeselect: EventEmitter = new EventEmitter();
+
+ @ViewChild("addQueryParamDialog") public addQueryParamDialog: AddQueryParamDialogComponent;
+ @ViewChild("clonePathDialog") clonePathDialog: ClonePathDialogComponent;
+ @ViewChild("addPathDialog") addPathDialog: AddPathDialogComponent;
+
+ protected createEmptyNodeForSource(): OasPathItem {
+ return (this.path.parent()).createPathItem(this.path.path());
+ }
+
+ protected createReplaceNodeCommand(node: OasPathItem) {
+ return createReplacePathItemCommand(this.path.ownerDocument(), this.path as any, node as any);
+ }
+
+ public document(): OasDocument {
+ return this.path.ownerDocument();
+ }
+
+ public hasGet(): boolean {
+ return this.path.get !== undefined && this.path.get !== null;
+ }
+ public hasPut(): boolean {
+ return this.path.put !== undefined && this.path.put !== null;
+ }
+ public hasPost(): boolean {
+ return this.path.post !== undefined && this.path.post !== null;
+ }
+ public hasDelete(): boolean {
+ return this.path.delete !== undefined && this.path.delete !== null;
+ }
+ public hasOptions(): boolean {
+ return this.path.options !== undefined && this.path.options !== null;
+ }
+ public hasHead(): boolean {
+ return this.path.head !== undefined && this.path.head !== null;
+ }
+ public hasPatch(): boolean {
+ return this.path.patch !== undefined && this.path.patch !== null;
+ }
+
+ public getSummary(): string {
+ return this.summary(this.path.get);
+ }
+
+ public putSummary(): string {
+ return this.summary(this.path.put);
+ }
+
+ public postSummary(): string {
+ return this.summary(this.path.post);
+ }
+
+ public deleteSummary(): string {
+ return this.summary(this.path.delete);
+ }
+
+ public optionsSummary(): string {
+ return this.summary(this.path.options);
+ }
+
+ public patchSummary(): string {
+ return this.summary(this.path.patch);
+ }
+
+ public headSummary(): string {
+ return this.summary(this.path.head);
+ }
+
+ private summary(operation: OasOperation): string {
+ if (operation === null || operation === undefined) {
+ return "Not Supported";
+ } else {
+ if (operation.summary) {
+ return operation.summary;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public getDescription(): string {
+ return this.description(this.path.get);
+ }
+
+ public putDescription(): string {
+ return this.description(this.path.put);
+ }
+
+ public postDescription(): string {
+ return this.description(this.path.post);
+ }
+
+ public deleteDescription(): string {
+ return this.description(this.path.delete);
+ }
+
+ public optionsDescription(): string {
+ return this.description(this.path.options);
+ }
+
+ public patchDescription(): string {
+ return this.description(this.path.patch);
+ }
+
+ public headDescription(): string {
+ return this.description(this.path.head);
+ }
+
+ private description(operation: OasOperation): string {
+ if (operation === null || operation === undefined) {
+ return "Not Supported";
+ } else {
+ if (operation.description) {
+ return operation.description;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public selectOperation(operation: OasOperation): void {
+ this.onNodeSelected.emit(new NodeSelectionEvent(operation, "operation"));
+ }
+
+ public createOperation(operationType: string): void {
+ let command: ICommand = createNewOperationCommand(this.path.ownerDocument(), this.path.path(), operationType);
+ this.onCommand.emit(command);
+ }
+
+ public changeSummary(newSummary: string, operation: OasOperation): void {
+ let command: ICommand = createChangePropertyCommand(this.path.ownerDocument(), operation, "summary", newSummary);
+ this.onCommand.emit(command);
+ }
+
+ public changeDescription(newDescription: string, operation: OasOperation): void {
+ let command: ICommand = createChangePropertyCommand(this.path.ownerDocument(), operation, "description", newDescription);
+ this.onCommand.emit(command);
+ }
+
+ public delete(): void {
+ let command: ICommand = createDeletePathCommand(this.path.ownerDocument(), this.path.path());
+ this.onCommand.emit(command);
+ this.onDeselect.emit(true);
+ }
+
+ public newPath(): void {
+ this.addPathDialog.open(this.path.ownerDocument(), this.path.path());
+ }
+
+ public addPath(path: string): void {
+ let command: ICommand = createNewPathCommand(this.path.ownerDocument(), path);
+ this.onCommand.emit(command);
+ }
+
+ public clone(modalData?: any): void {
+ if (undefined === modalData || modalData === null) {
+ this.clonePathDialog.open(this.path.ownerDocument(), this.path);
+ } else {
+ let pathItem: OasPathItem = modalData.object;
+ console.info("[PathFormComponent] Clone path item: %s", modalData.path);
+ let cloneSrcObj: any = this.oasLibrary().writeNode(pathItem);
+ let command: ICommand = createAddPathItemCommand(this.path.ownerDocument(), modalData.path, cloneSrcObj);
+ this.onCommand.emit(command);
+ }
+ }
+
+ public canHavePathParams(): boolean {
+ return this.path.path().indexOf('{') != -1;
+ }
+
+ public pathParam(paramName: string): OasParameterBase {
+ let param: OasParameterBase = this.path.parameter("path", paramName) as OasParameterBase;
+
+ if (param === null) {
+ param = this.path.createParameter();
+ param.in = "path";
+ param.name = paramName;
+ param.required = true;
+ param.n_attribute("missing", true);
+ }
+
+ return param;
+ }
+
+ public pathParameters(): OasParameterBase[] {
+ let pathParamNames: string[] = ModelUtils.detectPathParamNames(this.path.path());
+ return pathParamNames.map( pname => {
+ return this.pathParam(pname);
+ });
+ }
+
+ public hasParameters(type: string): boolean {
+ if (!this.path.parameters) {
+ return false;
+ }
+ return this.path.parameters.filter((value) => {
+ return value.in === type;
+ }).length > 0;
+ }
+
+ public parameters(paramType: string): OasParameterBase[] {
+ if (!this.path.parameters) {
+ return [];
+ }
+ let params: OasParameterBase[] = this.path.parameters;
+ return params.filter( value => {
+ return value.in === paramType;
+ }).sort((param1, param2) => {
+ return param1.name.localeCompare(param2.name);
+ });
+ }
+
+ public createPathParam(paramName: string): void {
+ let command: ICommand = createNewParamCommand(this.path.ownerDocument(), this.path as any, paramName, "path");
+ this.onCommand.emit(command);
+ }
+
+ public changeParamDescription(param: OasParameterBase, newParamDescription: string): void {
+ let command: ICommand = createChangePropertyCommand(this.path.ownerDocument(), param, "description", newParamDescription);
+ this.onCommand.emit(command);
+ }
+
+ public changeParamType(param: OasParameterBase, newType: SimplifiedParameterType): void {
+ let command: ICommand = createChangeParameterTypeCommand(this.path.ownerDocument(), param as any, newType);
+ this.onCommand.emit(command);
+ }
+
+ public queryParameters(): OasParameterBase[] {
+ return this.parameters("query");
+ }
+
+ public deleteParam(parameter: OasParameterBase): void {
+ let command: ICommand = createDeleteParameterCommand(this.path.ownerDocument(), parameter as any);
+ this.onCommand.emit(command);
+ }
+
+ public openAddQueryParamModal(): void {
+ this.addQueryParamDialog.open();
+ }
+
+ public addQueryParam(name: string): void {
+ let command: ICommand = createNewParamCommand(this.path.ownerDocument(), this.path as any, name, "query");
+ this.onCommand.emit(command);
+ }
+
+ public deleteAllQueryParams(): void {
+ let command: ICommand = createDeleteAllParametersCommand(this.path.ownerDocument(), this.path as any, "query");
+ this.onCommand.emit(command);
+ }
+
+ public formType(): string {
+ return "path";
+ }
+
+ public enableSourceMode(): void {
+ this.sourceNode = this.path;
+ super.enableSourceMode();
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/problem-form.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/problem-form.component.html
new file mode 100644
index 000000000..1d4f7a772
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/problem-form.component.html
@@ -0,0 +1,49 @@
+
This section provides additional information about the Validation Problem we found.
+
+
+
+
{{ problem.message }}
+
+
{{ problem.nodePath.toString() }}
+
+
+
{{ explanation() }}
+
+
+
+
+
+
+
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/problem-form.component.ts b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/problem-form.component.ts
new file mode 100644
index 000000000..bf4e32bee
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/problem-form.component.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, EventEmitter, Input, Output, ViewEncapsulation} from "@angular/core";
+import {OasValidationError} from "oai-ts-core";
+import {ProblemsService} from "../../_services/problems.service";
+import {ICommand} from "oai-ts-commands";
+
+
+@Component({
+ moduleId: module.id,
+ selector: "problem-form",
+ templateUrl: "problem-form.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class ProblemFormComponent {
+
+ private static problems: ProblemsService = new ProblemsService();
+
+ @Input() problem: OasValidationError;
+ @Output() onCommand: EventEmitter = new EventEmitter();
+ @Output() onGoToProblem: EventEmitter = new EventEmitter();
+
+ explanation(): string {
+ return ProblemFormComponent.problems.explanation(this.problem);
+ }
+
+ goToProblem(): void {
+ this.onGoToProblem.emit(true);
+ }
+
+}
diff --git a/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/shared/servers.component.html b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/shared/servers.component.html
new file mode 100644
index 000000000..50402f75a
--- /dev/null
+++ b/front-end/studio/src/app/pages/apis/{apiId}/editor/_components/forms/shared/servers.component.html
@@ -0,0 +1,46 @@
+