Improve deployment process and add login redirect logic
Deployment improvements: - Add template env files (.envs.example/) for documentation - Create init-production.sh for one-time server setup - Create build-activepieces.sh for building/deploying AP image - Update deploy.sh with --deploy-ap flag - Make custom-pieces-metadata.sql idempotent - Update DEPLOYMENT.md with comprehensive instructions Frontend: - Redirect logged-in business owners from root domain to tenant dashboard - Redirect logged-in users from /login to /dashboard on their tenant - Log out customers on wrong subdomain instead of redirecting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@activepieces/piece-interfaces",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "pieces-interfaces",
|
||||
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/pieces/community/interfaces/src",
|
||||
"projectType": "library",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/pieces/community/interfaces",
|
||||
"tsConfig": "packages/pieces/community/interfaces/tsconfig.lib.json",
|
||||
"packageJson": "packages/pieces/community/interfaces/package.json",
|
||||
"main": "packages/pieces/community/interfaces/src/index.ts",
|
||||
"assets": [],
|
||||
"buildableProjectDepsInPackageJsonType": "dependencies",
|
||||
"updateBuildableProjectDepsInPackageJson": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"^build",
|
||||
"prebuild"
|
||||
]
|
||||
},
|
||||
"publish": {
|
||||
"command": "node tools/scripts/publish.mjs pieces-interfaces {args.ver} {args.tag}",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
]
|
||||
},
|
||||
"prebuild": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "packages/pieces/community/interfaces",
|
||||
"command": "bun install --no-save --silent"
|
||||
},
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createPiece, PieceAuth } from '@activepieces/pieces-framework';
|
||||
import { PieceCategory } from '@activepieces/shared';
|
||||
|
||||
export const interfaces = createPiece({
|
||||
displayName: 'Interfaces',
|
||||
description: 'Create custom forms and interfaces for your workflows.',
|
||||
auth: PieceAuth.None(),
|
||||
categories: [PieceCategory.CORE],
|
||||
minimumSupportedRelease: '0.52.0',
|
||||
logoUrl: 'https://cdn.activepieces.com/pieces/interfaces.svg',
|
||||
authors: ['activepieces'],
|
||||
actions: [],
|
||||
triggers: [],
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"outDir": "../../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { createEventAction, findEventsAction, updateEventAction, cancelEventActi
|
||||
import { listResourcesAction } from './lib/actions/list-resources';
|
||||
import { listServicesAction } from './lib/actions/list-services';
|
||||
import { listInactiveCustomersAction } from './lib/actions/list-inactive-customers';
|
||||
import { sendEmailAction } from './lib/actions/send-email';
|
||||
import { listEmailTemplatesAction } from './lib/actions/list-email-templates';
|
||||
import { eventCreatedTrigger, eventUpdatedTrigger, eventCancelledTrigger, eventStatusChangedTrigger } from './lib/triggers';
|
||||
import { API_URL } from './lib/common';
|
||||
|
||||
@@ -75,6 +77,8 @@ export const smoothSchedule = createPiece({
|
||||
listResourcesAction,
|
||||
listServicesAction,
|
||||
listInactiveCustomersAction,
|
||||
sendEmailAction,
|
||||
listEmailTemplatesAction,
|
||||
createCustomApiCallAction({
|
||||
auth: smoothScheduleAuth,
|
||||
baseUrl: (auth) => (auth as SmoothScheduleAuth)?.props?.baseUrl ?? '',
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
|
||||
import { makeRequest } from '../common';
|
||||
|
||||
export const listEmailTemplatesAction = createAction({
|
||||
auth: smoothScheduleAuth,
|
||||
name: 'list_email_templates',
|
||||
displayName: 'List Email Templates',
|
||||
description: 'Get all available email templates (system and custom)',
|
||||
props: {},
|
||||
async run(context) {
|
||||
const auth = context.auth as SmoothScheduleAuth;
|
||||
|
||||
const response = await makeRequest(
|
||||
auth,
|
||||
HttpMethod.GET,
|
||||
'/emails/templates/'
|
||||
);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
|
||||
import { makeRequest } from '../common';
|
||||
|
||||
export const sendEmailAction = createAction({
|
||||
auth: smoothScheduleAuth,
|
||||
name: 'send_email',
|
||||
displayName: 'Send Email',
|
||||
description: 'Send an email using a SmoothSchedule email template',
|
||||
props: {
|
||||
template_type: Property.StaticDropdown({
|
||||
displayName: 'Template Type',
|
||||
description: 'Choose whether to use a system template or a custom template',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'System Template', value: 'system' },
|
||||
{ label: 'Custom Template', value: 'custom' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
email_type: Property.StaticDropdown({
|
||||
displayName: 'System Email Type',
|
||||
description: 'Select a system email template',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Appointment Confirmation', value: 'appointment_confirmation' },
|
||||
{ label: 'Appointment Reminder', value: 'appointment_reminder' },
|
||||
{ label: 'Appointment Rescheduled', value: 'appointment_rescheduled' },
|
||||
{ label: 'Appointment Cancelled', value: 'appointment_cancelled' },
|
||||
{ label: 'Welcome Email', value: 'welcome_email' },
|
||||
{ label: 'Password Reset', value: 'password_reset' },
|
||||
{ label: 'Invoice', value: 'invoice' },
|
||||
{ label: 'Payment Receipt', value: 'payment_receipt' },
|
||||
{ label: 'Staff Invitation', value: 'staff_invitation' },
|
||||
{ label: 'Customer Winback', value: 'customer_winback' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
template_slug: Property.ShortText({
|
||||
displayName: 'Custom Template Slug',
|
||||
description: 'The slug/identifier of your custom email template',
|
||||
required: false,
|
||||
}),
|
||||
to_email: Property.ShortText({
|
||||
displayName: 'Recipient Email',
|
||||
description: 'The email address to send to',
|
||||
required: true,
|
||||
}),
|
||||
subject_override: Property.ShortText({
|
||||
displayName: 'Subject Override',
|
||||
description: 'Override the template subject (optional)',
|
||||
required: false,
|
||||
}),
|
||||
reply_to: Property.ShortText({
|
||||
displayName: 'Reply-To Email',
|
||||
description: 'Reply-to email address (optional)',
|
||||
required: false,
|
||||
}),
|
||||
context: Property.Object({
|
||||
displayName: 'Template Variables',
|
||||
description: 'Variables to replace in the template (e.g., customer_name, appointment_date)',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { template_type, email_type, template_slug, to_email, subject_override, reply_to, context: templateContext } = context.propsValue;
|
||||
const auth = context.auth as SmoothScheduleAuth;
|
||||
|
||||
// Validate that the right template identifier is provided based on type
|
||||
if (template_type === 'system' && !email_type) {
|
||||
throw new Error('System Email Type is required when using System Template');
|
||||
}
|
||||
if (template_type === 'custom' && !template_slug) {
|
||||
throw new Error('Custom Template Slug is required when using Custom Template');
|
||||
}
|
||||
|
||||
// Build the request body
|
||||
const requestBody: Record<string, unknown> = {
|
||||
to_email,
|
||||
};
|
||||
|
||||
if (template_type === 'system') {
|
||||
requestBody['email_type'] = email_type;
|
||||
} else {
|
||||
requestBody['template_slug'] = template_slug;
|
||||
}
|
||||
|
||||
if (subject_override) {
|
||||
requestBody['subject_override'] = subject_override;
|
||||
}
|
||||
|
||||
if (reply_to) {
|
||||
requestBody['reply_to'] = reply_to;
|
||||
}
|
||||
|
||||
if (templateContext && Object.keys(templateContext).length > 0) {
|
||||
requestBody['context'] = templateContext;
|
||||
}
|
||||
|
||||
const response = await makeRequest(
|
||||
auth,
|
||||
HttpMethod.POST,
|
||||
'/emails/send/',
|
||||
requestBody
|
||||
);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from 'i18next';
|
||||
import { Plus, Globe } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { AutoFormFieldWrapper } from '@/app/builder/piece-properties/auto-form-field-wrapper';
|
||||
@@ -80,6 +80,27 @@ function ConnectionSelect(params: ConnectionSelectProps) {
|
||||
PropertyExecutionType.DYNAMIC;
|
||||
const isPLatformAdmin = useIsPlatformAdmin();
|
||||
|
||||
// Auto-select connection with autoSelect metadata if no connection is selected
|
||||
useEffect(() => {
|
||||
if (isLoadingConnections || !connections?.data) return;
|
||||
|
||||
const currentAuth = form.getValues().settings.input.auth;
|
||||
// Only auto-select if no connection is currently selected
|
||||
if (currentAuth && removeBrackets(currentAuth)) return;
|
||||
|
||||
// Find a connection with autoSelect metadata
|
||||
const autoSelectConnection = connections.data.find(
|
||||
(connection) => (connection as any).metadata?.autoSelect === true
|
||||
);
|
||||
|
||||
if (autoSelectConnection) {
|
||||
form.setValue('settings.input.auth', addBrackets(autoSelectConnection.externalId), {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
}, [connections?.data, isLoadingConnections, form]);
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -22,8 +22,8 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { LoadingSpinner } from '@/components/ui/spinner';
|
||||
import { TemplateCard } from '@/features/templates/components/template-card';
|
||||
import { TemplateDetailsView } from '@/features/templates/components/template-details-view';
|
||||
import { useTemplates } from '@/features/templates/hooks/templates-hook';
|
||||
import { Template, TemplateType } from '@activepieces/shared';
|
||||
import { useAllTemplates } from '@/features/templates/hooks/templates-hook';
|
||||
import { Template } from '@activepieces/shared';
|
||||
|
||||
const SelectFlowTemplateDialog = ({
|
||||
children,
|
||||
@@ -32,9 +32,7 @@ const SelectFlowTemplateDialog = ({
|
||||
children: React.ReactNode;
|
||||
folderId: string;
|
||||
}) => {
|
||||
const { filteredTemplates, isLoading, search, setSearch } = useTemplates({
|
||||
type: TemplateType.CUSTOM,
|
||||
});
|
||||
const { filteredTemplates, isLoading, search, setSearch } = useAllTemplates();
|
||||
const carousel = useRef<CarouselApi>();
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||
null,
|
||||
|
||||
@@ -247,6 +247,23 @@ export const appConnectionService = (log: FastifyBaseLogger) => ({
|
||||
},
|
||||
|
||||
async delete(params: DeleteParams): Promise<void> {
|
||||
// Check if connection is protected before deleting
|
||||
const connection = await appConnectionsRepo().findOneBy({
|
||||
id: params.id,
|
||||
platformId: params.platformId,
|
||||
scope: params.scope,
|
||||
...(params.projectId ? { projectIds: ArrayContains([params.projectId]) } : {}),
|
||||
})
|
||||
|
||||
if (connection?.metadata?.protected) {
|
||||
throw new ActivepiecesError({
|
||||
code: ErrorCode.VALIDATION,
|
||||
params: {
|
||||
message: 'This connection is protected and cannot be deleted. It is required for SmoothSchedule integration.',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await appConnectionsRepo().delete({
|
||||
id: params.id,
|
||||
platformId: params.platformId,
|
||||
|
||||
Reference in New Issue
Block a user