10 Commits

Author SHA1 Message Date
Eric Tuvesson
41711c3934 Merge branch 'main' into feature/store-cloudservices-in-project-folder 2024-06-17 10:35:36 +02:00
Eric Tuvesson
0ee55c26eb feat(editor): Search panel show "(Cloud Function)" if result is in Cloud Functions sheet (#40) 2024-06-17 10:35:02 +02:00
Eric Tuvesson
3a31b86d48 chore(runtime): Clean up "Push Component To Stack" (#30) 2024-06-13 22:25:16 +02:00
Eric Tuvesson
2f06952e4a fix(runtime): JavaScript Records API error handling (#32) 2024-06-13 22:24:34 +02:00
Eric Tuvesson
44a40aef96 fix: Add more error handling to JavaScript Records API (#33) 2024-06-13 22:24:05 +02:00
Eric Tuvesson
0a69765460 chore: Update template provider name (#38) 2024-06-13 22:23:30 +02:00
Eric Tuvesson
5d8b2d5bba Merge branch 'main' into feature/store-cloudservices-in-project-folder 2024-06-03 10:29:29 +02:00
alan-x-n
2ebd57b29a chore: Update README.md 2024-06-02 15:29:25 -07:00
alan-x-n
5225d26870 chore: Updated README.md 2024-06-02 15:27:35 -07:00
Eric Tuvesson
851dbc98d3 feat: Save Cloud Service in the project folder instead 2024-05-22 10:01:46 +02:00
18 changed files with 307 additions and 131 deletions

View File

@@ -1,21 +1,21 @@
# Noodl
# Fluxscape
[Noodl](https://noodl.net) is a low-code platform where designers and developers build custom applications and experiences. Designed as a visual programming environment, it aims to expedite your development process. It promotes the swift and efficient creation of applications, requiring minimal coding knowledge.
Fluxscape is a low-code platform where designers and developers build custom applications and experiences. Designed as a visual programming environment, it aims to expedite your development process. It promotes the swift and efficient creation of applications, requiring minimal coding knowledge.
## Documentation
Documentation for how to use Noodl can be found here:
[https://noodlapp.github.io/noodl-docs/](https://noodlapp.github.io/noodl-docs/)
Documentation for how to use Fluxscape can be found here:
[Fluxscape Documentation](https://docs.fluxscape.io)
## Community
Main support channel is Discord: [https://www.noodl.net/community](https://www.noodl.net/community)
Main support channel is Discord: [Fluxscape Discord](https://discord.gg/fXNW9EXa6A)
## Download releases
Pre-built binaries can be [downloaded from Github](https://github.com/noodlapp/noodl/releases)
Pre-built binaries can be [downloaded from Github](https://github.com/fluxscape/fluxscape/releases)
## Note for users who are migrating from the deprecated closed source version
- [Migrating the project files and workspaces to a Git provider](https://noodlapp.github.io/noodl-docs/docs/guides/collaboration/migrating-from-noodl-hosted-git)
- [Migrate backend and database](https://noodlapp.github.io/noodl-docs/docs/guides/deploy/using-an-external-backend#migrating-from-a-noodl-cloud-service)
- [Self-host frontend](https://noodlapp.github.io/noodl-docs/docs/guides/deploy/hosting-frontend)
- [Migrating the project files and workspaces to a Git provider](https://docs.fluxscape.io/docs/guides/collaboration/migrating-from-noodl-hosted-git/)
- [Migrate backend and database](https://docs.fluxscape.io/docs/guides/deploy/using-an-external-backend/#migrating-from-a-noodl-cloud-service)
- [Self-host frontend](https://docs.fluxscape.io/docs/guides/deploy/hosting-frontend/)
## Building from source
@@ -23,24 +23,24 @@ Pre-built binaries can be [downloaded from Github](https://github.com/noodlapp/n
# Install all dependencies
$ npm install
# Start the Noodl Editor and build a production version of the cloud and react runtime (useful when running Noodl from source but want to deploy to production)
# Start the Fluxscape Editor and build a production version of the cloud and react runtime (useful when running Fluxscape from source but want to deploy to production)
$ npm start
# Start the Noodl Editor and watch the filesystem for changes to the runtimes. Development versions of the runtimes, not meant for production (mostly due to source maps and file size)
# Start the Fluxscape Editor and watch the filesystem for changes to the runtimes. Development versions of the runtimes, not meant for production (mostly due to source maps and file size)
# This is ideal for a quick workflow when doing changes on the runtimes.
$ npm run dev
# Start Noodl Editor test runner
# Start Fluxscape Editor test runner
$ npm run test:editor
```
## Licenses
This repository contains two different licenses for different parts of the Noodl platform.
This repository contains two different licenses for different parts of the Fluxscape platform.
- Components related to the editor, used to edit Noodl projects, are under GPLv3
- Components related to the end applications, used by the applications Noodl deploys, are under MIT
- Components related to the editor, used to edit Fluxscape projects, are under GPLv3
- Components related to the end applications, used by the applications Fluxscape deploys, are under MIT
All of the source code of applications created with Noodl are under MIT. This means you can do project specific changes to the runtime without having to redistribute your changes.
All of the source code of applications created with Fluxscape are under MIT. This means you can do project specific changes to the runtime without having to redistribute your changes.
Packaged licensed under MIT:
- `noodl-runtime`

View File

@@ -48,7 +48,7 @@ export async function execute(command: Command, event: MessageEvent) {
// Deploy to temp folder
await compilation.deployToFolder(tempDir, {
environment: command.cloudService ? new Environment(command.cloudService) : undefined
environment: command.cloudService ? new Environment("", command.cloudService) : undefined
});
// Upload to S3

View File

@@ -1,9 +1,9 @@
import { Environment, ExternalCloudService } from '@noodl-models/CloudServices/ExternalCloudService';
import {
CloudServiceEvent,
CloudServiceEvents,
CreateEnvironment,
CreateEnvironmentRequest,
Environment,
ICloudBackendService,
ICloudService,
UpdateEnvironmentRequest
@@ -12,6 +12,9 @@ import { ProjectModel } from '@noodl-models/projectmodel';
import { getCloudServices } from '@noodl-models/projectmodel.editor';
import { Model } from '@noodl-utils/model';
import { GlobalCloudService } from './providers/GlobalCloudService';
import { ProjectCloudService } from './providers/ProjectCloudService';
export type CloudQueueItem = {
frontendId: string;
environmentId: string;
@@ -20,7 +23,9 @@ export type CloudQueueItem = {
class CloudBackendService implements ICloudBackendService {
private _isLoading = false;
private _collection?: Environment[];
private _localExternal = new ExternalCloudService();
private _globalProvider = new GlobalCloudService();
private _projectProvider = new ProjectCloudService();
get isLoading(): boolean {
return this._isLoading;
@@ -32,12 +37,14 @@ class CloudBackendService implements ICloudBackendService {
constructor(private readonly service: CloudService) {}
async fetch(): Promise<Environment[]> {
async fetch(project: ProjectModel): Promise<Environment[]> {
this._isLoading = true;
try {
// Fetch environments from local machine
const localEnvironments = await this._localExternal.list();
this._collection = localEnvironments.map((x) => new Environment(x));
const projectResults = await this._projectProvider.list(project);
this._collection = projectResults.map((x) => new Environment("project", x));
const globalResults = await this._globalProvider.list(project);
this._collection = this._collection.concat(globalResults.map((x) => new Environment("global", x)));
} finally {
this._isLoading = false;
this.service.notifyListeners(CloudServiceEvent.BackendUpdated);
@@ -48,7 +55,7 @@ class CloudBackendService implements ICloudBackendService {
async fromProject(project: ProjectModel): Promise<Environment> {
const activeCloudServices = getCloudServices(project);
if (!this._collection) {
await this.fetch();
await this.fetch(project);
}
return this.items.find((b) => {
@@ -56,16 +63,16 @@ class CloudBackendService implements ICloudBackendService {
});
}
async create(options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
return await this._localExternal.create(options);
async create(project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
return await this._projectProvider.create(project, options);
}
async update(options: UpdateEnvironmentRequest): Promise<boolean> {
return await this._localExternal.update(options);
async update(project: ProjectModel, options: UpdateEnvironmentRequest): Promise<boolean> {
return await this._projectProvider.update(project, options);
}
async delete(id: string): Promise<boolean> {
return await this._localExternal.delete(id);
async delete(project: ProjectModel, id: string): Promise<boolean> {
return await this._projectProvider.delete(project, id);
}
}
@@ -95,15 +102,11 @@ export class CloudService extends Model<CloudServiceEvent, CloudServiceEvents> i
*/
public async prefetch() {
this.reset();
await this.fetch();
}
public async fetch() {
await this.backend.fetch();
await this.backend.fetch(ProjectModel.instance);
}
public async getActiveEnvironment(project: ProjectModel): Promise<Environment> {
await this.backend.fetch();
await this.backend.fetch(project);
return this.backend.fromProject(project);
}
}

View File

@@ -1,3 +1,2 @@
export * from './CloudService';
export * from './type';
export * from './ExternalCloudService';

View File

@@ -1,25 +1,21 @@
import { JSONStorage } from '@noodl/platform';
import { CreateEnvironment, CreateEnvironmentRequest, UpdateEnvironmentRequest } from '@noodl-models/CloudServices';
import {
CreateEnvironment,
CreateEnvironmentRequest,
EnvironmentDataFormat,
ICloudServiceProvider,
UpdateEnvironmentRequest
} from '@noodl-models/CloudServices';
import { ProjectModel } from '@noodl-models/projectmodel';
/** The data format is separated from our internal model. */
export type EnvironmentDataFormat = {
enabled: boolean;
id: string;
name: string;
description: string;
masterKey: string;
appId: string;
endpoint: string;
};
export class ExternalCloudService {
async list(): Promise<EnvironmentDataFormat[]> {
export class GlobalCloudService implements ICloudServiceProvider {
async list(_project: ProjectModel): Promise<EnvironmentDataFormat[]> {
const local = await JSONStorage.get('externalBrokers');
return local.brokers || [];
}
async create(options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
async create(_project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
const id = `${options.url}-${options.appId}`;
const newBroker: EnvironmentDataFormat = {
@@ -44,7 +40,7 @@ export class ExternalCloudService {
};
}
async update(options: UpdateEnvironmentRequest): Promise<boolean> {
async update(_project: ProjectModel, options: UpdateEnvironmentRequest): Promise<boolean> {
const local = await JSONStorage.get('externalBrokers');
const brokers: EnvironmentDataFormat[] = local.brokers || [];
@@ -63,7 +59,7 @@ export class ExternalCloudService {
return true;
}
async delete(id: string): Promise<boolean> {
async delete(_project: ProjectModel, id: string): Promise<boolean> {
const local = await JSONStorage.get('externalBrokers');
const brokers: EnvironmentDataFormat[] = local.brokers || [];
@@ -79,25 +75,3 @@ export class ExternalCloudService {
return true;
}
}
export class Environment {
id: string;
name: string;
description: string;
createdAt: string;
masterKeyUpdatedAt: string;
masterKey: string;
appId: string;
url: string;
constructor(item: EnvironmentDataFormat) {
this.id = item.id;
this.name = item.name;
this.description = item.description;
this.createdAt = '';
this.masterKeyUpdatedAt = '';
this.masterKey = item.masterKey;
this.appId = item.appId;
this.url = item.endpoint;
}
}

View File

@@ -0,0 +1,101 @@
import { filesystem } from '@noodl/platform';
import {
CreateEnvironment,
CreateEnvironmentRequest,
EnvironmentDataFormat,
ICloudServiceProvider,
UpdateEnvironmentRequest
} from '@noodl-models/CloudServices';
import { ProjectModel } from '@noodl-models/projectmodel';
/**
* Store the Cloud Service relative to the project folder.
*/
export class ProjectCloudService implements ICloudServiceProvider {
private async load(project: ProjectModel) {
const dirpath = filesystem.resolve(project._retainedProjectDirectory, ".noodl");
const filepath = filesystem.resolve(dirpath, "cloudservices.json");
if (!filesystem.exists(filepath)) {
return []
}
// TODO: Validate file content
return filesystem.readJson(filepath);
}
private async save(project: ProjectModel, items: EnvironmentDataFormat[]) {
const dirpath = filesystem.resolve(project._retainedProjectDirectory, ".noodl");
const filepath = filesystem.resolve(dirpath, "cloudservices.json");
if (!filesystem.exists(filepath)) {
await filesystem.makeDirectory(dirpath);
}
await filesystem.writeJson(filepath, items);
}
async list(project: ProjectModel): Promise<EnvironmentDataFormat[]> {
if (!project || !project._retainedProjectDirectory) {
return []
}
return await this.load(project);
}
async create(project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
const id = `${options.url}-${options.appId}`;
const newBroker: EnvironmentDataFormat = {
enabled: true,
id,
name: options.name,
description: options.description,
masterKey: options.masterKey,
appId: options.appId,
endpoint: options.url
};
const current = await this.load(project);
await this.save(project, [...current, newBroker]);
return {
id: newBroker.id,
appId: newBroker.appId,
url: newBroker.endpoint,
masterKey: newBroker.masterKey
};
}
async update(project: ProjectModel, options: UpdateEnvironmentRequest): Promise<boolean> {
const current = await this.load(project);
// Find and update
const broker = current.find((x) => x.id === options.id);
if (!broker) return false;
if (typeof options.name !== undefined) broker.name = options.name;
if (typeof options.description !== undefined) broker.description = options.description;
if (typeof options.appId !== undefined) broker.appId = options.appId;
if (typeof options.masterKey !== undefined) broker.masterKey = options.masterKey;
if (typeof options.url !== undefined) broker.endpoint = options.url;
await this.save(project, current);
return true;
}
async delete(project: ProjectModel, id: string): Promise<boolean> {
const current = await this.load(project);
// Find the environment
const found = current.find((b) => b.id === id);
if (found) {
// Delete the environment
current.splice(current.indexOf(found), 1);
}
// Save the list
await this.save(project, current);
return true;
}
}

View File

@@ -1,4 +1,3 @@
import { Environment } from '@noodl-models/CloudServices';
import { ProjectModel } from '@noodl-models/projectmodel';
import { IModel } from '@noodl-utils/model';
@@ -30,11 +29,11 @@ export interface ICloudBackendService {
get isLoading(): boolean;
get items(): Environment[];
fetch(): Promise<Environment[]>;
fetch(project: ProjectModel): Promise<Environment[]>;
fromProject(project: ProjectModel): Promise<Environment> | undefined;
create(options: CreateEnvironmentRequest): Promise<CreateEnvironment>;
update(options: UpdateEnvironmentRequest): Promise<boolean>;
delete(id: string): Promise<boolean>;
create(project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment>;
update(project: ProjectModel, options: UpdateEnvironmentRequest): Promise<boolean>;
delete(project: ProjectModel, id: string): Promise<boolean>;
}
export enum CloudServiceEvent {
@@ -55,3 +54,54 @@ export interface ICloudService extends IModel<CloudServiceEvent, CloudServiceEve
get backend(): ICloudBackendService;
}
/** The data format is separated from our internal model. */
export type EnvironmentDataFormat = {
enabled: boolean;
id: string;
name: string;
description: string;
masterKey: string;
appId: string;
endpoint: string;
};
export class Environment {
id: string;
name: string;
description: string;
createdAt: string;
masterKeyUpdatedAt: string;
masterKey: string;
appId: string;
url: string;
provider: string;
get typeDisplayName() {
switch (this.provider) {
case "project": return "Self hosted";
default: return "Global Self hosted"
}
}
constructor(provider: string, item: EnvironmentDataFormat) {
this.provider = provider;
this.id = item.id;
this.name = item.name;
this.description = item.description;
this.createdAt = '';
this.masterKeyUpdatedAt = '';
this.masterKey = item.masterKey;
this.appId = item.appId;
this.url = item.endpoint;
}
}
export interface ICloudServiceProvider {
list(project: ProjectModel | undefined): Promise<EnvironmentDataFormat[]>;
create(project: ProjectModel | undefined, options: CreateEnvironmentRequest): Promise<CreateEnvironment>;
update(project: ProjectModel | undefined, options: UpdateEnvironmentRequest): Promise<boolean>;
delete(project: ProjectModel | undefined, id: string): Promise<boolean>;
}

View File

@@ -1,4 +1,5 @@
import { CloudService } from '@noodl-models/CloudServices';
import { ProjectModel } from '@noodl-models/projectmodel';
import SchemaModel from '@noodl-models/schemamodel';
class FormCollection {
@@ -193,7 +194,7 @@ export default class CloudFormation {
}) {
// Create new cloud services if needed
if (options.cloudServices.id === undefined) {
CloudService.instance.backend.fetch().then((collection) => {
CloudService.instance.backend.fetch(ProjectModel.instance).then((collection) => {
// TODO(OS): Cloud formation Cloud Service
// // Make sure we have a unique name for the cloud services
// const orgName = options.cloudServices.name;

View File

@@ -6,7 +6,7 @@ import { ITemplateProvider, ProgressCallback, TemplateItem, TemplateListFilter }
*/
export class NoodlDocsTemplateProvider implements ITemplateProvider {
get name(): string {
return 'https://docs.noodl.net';
return this.getDocsEndpoint() || 'https://docs.fluxscape.io';
}
constructor(private readonly getDocsEndpoint: () => string) {}

View File

@@ -41,7 +41,7 @@ export default class SchemaHandler {
return; // No project broker
}
CloudService.instance.backend.fetch().then((collection) => {
CloudService.instance.backend.fetch(ProjectModel.instance).then((collection) => {
// Find by the Url / Endpoint and app id
let environment = collection.find((b) => {
return b.url === activeBroker.endpoint && b.appId === activeBroker.appId;

View File

@@ -87,7 +87,7 @@ export function CloudServiceCard({
<div className={css['MetaBar']}>
<div className={classNames([css['TypeDisplay'], isEditorEnvironment && css['is-editor-environment']])}>
<Icon icon={IconName.CloudCheck} size={IconSize.Small} UNSAFE_style={{ marginRight: 4 }} />
{'Self hosted '}
{environment.typeDisplayName + ' '}
{errorMessage && <span className={css['ArchivedDisplay']}>({errorMessage})</span>}
{isEditorEnvironment && <span className={css['UsedInEditorDisplay']}>(Used in editor)</span>}
</div>

View File

@@ -5,6 +5,7 @@ import { CloudService, Environment } from '@noodl-models/CloudServices';
import { ToastType } from '../../../ToastLayer/components/ToastCard';
import { CloudServiceCard } from '../CloudServiceCard';
import { useCloudServiceContext } from '../CloudServicePanel.context';
import { ProjectModel } from '@noodl-models/projectmodel';
export interface CloudServiceCardItemProps {
environment: Environment;
@@ -20,7 +21,7 @@ export function CloudServiceCardItem({ environment, deleteEnvironment }: CloudSe
async function onDelete() {
await deleteEnvironment();
await runActivity('Deleting cloud service...', async () => {
const response: boolean = await CloudService.instance.backend.delete(environment.id);
const response: boolean = await CloudService.instance.backend.delete(ProjectModel.instance, environment.id);
return {
type: ToastType.Success,
message: 'Cloud service deleted'

View File

@@ -12,6 +12,7 @@ import { Text, TextType } from '@noodl-core-ui/components/typography/Text';
import { ToastType } from '../../../ToastLayer/components/ToastCard';
import { useCloudServiceContext } from '../CloudServicePanel.context';
import { ProjectModel } from '@noodl-models/projectmodel';
function isValidParseUrl(url: string) {
if (!url) return false;
@@ -63,7 +64,7 @@ export function CloudServiceCreateModal({ isVisible, onClose }: CloudServiceCrea
async function onCreate() {
await runActivity('Creating Cloud Service...', async () => {
await cloudService.backend.create({
await cloudService.backend.create(ProjectModel.instance, {
name,
description,
masterKey: masterKey ? masterKey : undefined,

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { FeedbackType } from '@noodl-constants/FeedbackType';
import { CloudService, Environment } from '@noodl-models/CloudServices';
@@ -12,6 +12,7 @@ import { HStack, VStack } from '@noodl-core-ui/components/layout/Stack';
import { Text } from '@noodl-core-ui/components/typography/Text';
import { ToastLayer } from '../../../ToastLayer';
import { ProjectModel } from '@noodl-models/projectmodel';
export interface CloudServiceModalProps {
isVisible: boolean;
@@ -60,7 +61,7 @@ function AsSelfHosted({
}
CloudService.instance.backend
.update({
.update(ProjectModel.instance, {
id: environment.id,
name,
description,
@@ -70,7 +71,7 @@ function AsSelfHosted({
})
.then(() => {
ToastLayer.showSuccess(`Updated Cloud Service`);
CloudService.instance.backend.fetch();
CloudService.instance.backend.fetch(ProjectModel.instance);
})
.catch(() => {
ToastLayer.showError(`Failed to update Cloud Service`);

View File

@@ -35,7 +35,7 @@ export function CloudServiceContextProvider({ children }) {
const { hasActivity, runActivity } = useActivityQueue({
onSuccess: async () => {
// Always fetch all the backends after something have changed
await cloudService.backend.fetch();
await cloudService.backend.fetch(ProjectModel.instance);
}
});

View File

@@ -5,6 +5,8 @@ import { useSidePanelKeyboardCommands } from '@noodl-hooks/useKeyboardCommands';
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import { ComponentModel } from '@noodl-models/componentmodel';
import { NodeGraphNode } from '@noodl-models/nodegraphmodel';
import { KeyCode, KeyMod } from '@noodl-utils/keyboard/KeyCode';
import { performSearch } from '@noodl-utils/universal-search';
@@ -54,7 +56,7 @@ export function SearchPanel() {
}
}, [debouncedSearchTerm]);
function onSearchItemClicked(searchResult) {
function onSearchItemClicked(searchResult: SearchResultItem) {
if (searchResult.type === 'Component') {
NodeGraphContextTmp.switchToComponent(searchResult.componentTarget, {
breadcrumbs: false,
@@ -85,29 +87,7 @@ export function SearchPanel() {
<div className={css.SearchResults}>
{searchResults.map((component) => (
<Section
title={`${component.componentName} (${component.results.length} result${
component.results.length > 1 ? 's' : ''
})`}
key={component.componentId}
variant={SectionVariant.Panel}
>
{component.results.map((result, index) => (
<div
className={classNames(
css.SearchResultItem
// lastActiveComponentId === result.componentTarget.id && css['is-active']
)}
key={index}
onClick={() => onSearchItemClicked(result)}
>
<Label variant={TextType.Proud}>
{result.userLabel ? result.type + ' - ' + result.userLabel : result.type}
</Label>
<Text>{result.label}</Text>
</div>
))}
</Section>
<SearchItem key={component.componentId} component={component} onSearchItemClicked={onSearchItemClicked} />
))}
{searchResults.length === 0 && debouncedSearchTerm.length > 0 && (
<Container hasXSpacing hasYSpacing>
@@ -118,3 +98,51 @@ export function SearchPanel() {
</BasePanel>
);
}
type SearchResultItem = {
componentTarget: ComponentModel;
label: string;
nodeTarget: NodeGraphNode;
type: string;
userLabel: string;
};
type SearchItemProps = {
component: {
componentName: string;
componentId: string;
results: SearchResultItem[];
};
onSearchItemClicked: (item: SearchResultItem) => void;
};
function SearchItem({ component, onSearchItemClicked }: SearchItemProps) {
const resultCountText = `${component.results.length} result${component.results.length > 1 ? 's' : ''}`;
let titleText = `${component.componentName} (${resultCountText})`;
// We expect there to always be at least one result, to get the full component name.
const isInCloudFunctions = component.results[0].componentTarget.name.startsWith('/#__cloud__/');
if (isInCloudFunctions) {
titleText += ' (Cloud Function)';
}
return (
<Section title={titleText} variant={SectionVariant.Panel}>
{component.results.map((result, index) => (
<div
className={classNames(
css.SearchResultItem
// lastActiveComponentId === result.componentTarget.id && css['is-active']
)}
key={index}
onClick={() => onSearchItemClicked(result)}
>
<Label variant={TextType.Proud}>
{result.userLabel ? result.type + ' - ' + result.userLabel : result.type}
</Label>
<Text>{result.label}</Text>
</div>
))}
</Section>
);
}

View File

@@ -12,6 +12,7 @@ function createRecordsAPI(modelScope) {
return {
async query(className, query, options) {
if (typeof className === "undefined") throw new Error("'className' is undefined");
return new Promise((resolve, reject) => {
cloudstore().query({
collection: className,
@@ -39,6 +40,7 @@ function createRecordsAPI(modelScope) {
},
async count(className, query) {
if (typeof className === "undefined") throw new Error("'className' is undefined");
return new Promise((resolve, reject) => {
cloudstore().count({
collection: className,
@@ -60,6 +62,7 @@ function createRecordsAPI(modelScope) {
},
async distinct(className, property, query) {
if (typeof className === "undefined") throw new Error("'className' is undefined");
return new Promise((resolve, reject) => {
cloudstore().distinct({
collection: className,
@@ -82,6 +85,7 @@ function createRecordsAPI(modelScope) {
},
async aggregate(className, group, query) {
if (typeof className === "undefined") throw new Error("'className' is undefined");
return new Promise((resolve, reject) => {
cloudstore().aggregate({
collection: className,
@@ -104,6 +108,7 @@ function createRecordsAPI(modelScope) {
},
async fetch(objectOrId, options) {
if (typeof objectOrId === 'undefined') return Promise.reject(new Error("'objectOrId' is undefined."));
if (typeof objectOrId !== 'string') objectOrId = objectOrId.getId();
const className = (options ? options.className : undefined) || (modelScope || Model).get(objectOrId)._class;
@@ -126,6 +131,7 @@ function createRecordsAPI(modelScope) {
},
async increment(objectOrId, properties, options) {
if (typeof objectOrId === 'undefined') return Promise.reject(new Error("'objectOrId' is undefined."));
if (typeof objectOrId !== 'string') objectOrId = objectOrId.getId();
const className = (options ? options.className : undefined) || (modelScope || Model).get(objectOrId)._class;
@@ -149,6 +155,7 @@ function createRecordsAPI(modelScope) {
},
async save(objectOrId, properties, options) {
if (typeof objectOrId === 'undefined') return Promise.reject(new Error("'objectOrId' is undefined."));
if (typeof objectOrId !== 'string') objectOrId = objectOrId.getId();
const className = (options ? options.className : undefined) || (modelScope || Model).get(objectOrId)._class;
@@ -179,6 +186,7 @@ function createRecordsAPI(modelScope) {
},
async create(className, properties, options) {
if (typeof className === "undefined") throw new Error("'className' is undefined");
return new Promise((resolve, reject) => {
cloudstore().create({
collection: className,
@@ -197,6 +205,7 @@ function createRecordsAPI(modelScope) {
},
async delete(objectOrId, options) {
if (typeof objectOrId === 'undefined') return Promise.reject(new Error("'objectOrId' is undefined."));
if (typeof objectOrId !== 'string') objectOrId = objectOrId.getId();
const className = (options ? options.className : undefined) || (modelScope || Model).get(objectOrId)._class;
@@ -265,7 +274,7 @@ function createRecordsAPI(modelScope) {
resolve();
},
error: (err) => {
reject(Error(rr || 'Failed to add relation.'));
reject(Error(err || 'Failed to add relation.'));
}
});
});

View File

@@ -72,8 +72,10 @@ const Navigate = {
backCallback: (action, results) => {
this._internal.backResults = results;
for (var key in results) {
if (this.hasOutput('backResult-' + key)) this.flagOutputDirty('backResult-' + key);
for (const key in results) {
if (this.hasOutput('backResult-' + key)) {
this.flagOutputDirty('backResult-' + key);
}
}
if (action !== undefined) this.sendSignalOnOutput(action);
@@ -114,22 +116,23 @@ const Navigate = {
return;
}
if (name === 'target')
if (name === 'target') {
return this.registerInput(name, {
set: this.setTargetPageId.bind(this)
});
else if (name === 'transition')
} else if (name === 'transition') {
return this.registerInput(name, {
set: this.setTransition.bind(this)
});
else if (name.startsWith('tr-'))
} else if (name.startsWith('tr-')) {
return this.registerInput(name, {
set: this.setTransitionParam.bind(this, name.substring('tr-'.length))
});
else if (name.startsWith('pm-'))
} else if (name.startsWith('pm-')) {
return this.registerInput(name, {
set: this.setPageParam.bind(this, name.substring('pm-'.length))
});
}
},
registerOutputIfNeeded: function (name) {
if (this.hasOutput(name)) {
@@ -174,18 +177,24 @@ function setup(context, graphModel) {
if (Transitions[transition]) ports = ports.concat(Transitions[transition].ports(node.parameters));
}
// if(node.parameters['stack'] !== undefined) {
var pageStacks = graphModel.getNodesWithType('Page Stack');
var pageStack = pageStacks.find(
const pageStacks = graphModel.getNodesWithType('Page Stack');
const pageStack = pageStacks.find(
(ps) => (ps.parameters['name'] || 'Main') === (node.parameters['stack'] || 'Main')
);
if (pageStack !== undefined) {
var pages = pageStack.parameters['pages'];
const pages = pageStack.parameters['pages'];
if (pages !== undefined && pages.length > 0) {
ports.push({
plug: 'input',
type: { name: 'enum', enums: pages.map((p) => ({ label: p.label, value: p.id })), allowEditOnly: true },
type: {
name: 'enum',
enums: pages.map((p) => ({
label: p.label,
value: p.id
})),
allowEditOnly: true
},
group: 'General',
displayName: 'Target Page',
name: 'target',
@@ -193,14 +202,14 @@ function setup(context, graphModel) {
});
// See if there is a target page with component
var targetPageId = node.parameters['target'] || pages[0].id;
var targetComponentName = pageStack.parameters['pageComp-' + targetPageId];
const targetPageId = node.parameters['target'] || pages[0].id;
const targetComponentName = pageStack.parameters['pageComp-' + targetPageId];
if (targetComponentName !== undefined) {
const component = graphModel.components[targetComponentName];
if (component !== undefined) {
// Make all inputs of the component to inputs of this navigation node
for (var inputName in component.inputPorts) {
for (const inputName in component.inputPorts) {
ports.push({
name: 'pm-' + inputName,
displayName: inputName,
@@ -245,7 +254,6 @@ function setup(context, graphModel) {
}
}
}
// }
context.editorConnection.sendDynamicPorts(node.id, ports);
}