import fs from "fs"; import { Box, Newline, Text, useApp } from "ink"; import SelectInput from "ink-select-input"; import TextInput from "ink-text-input"; import React, { useEffect, useState } from "react"; import type { AppMeta } from "@calcom/types/App"; import { getSlugFromAppName, BaseAppFork, generateAppFiles, getAppDirPath } from "../core"; import { getApp } from "../utils/getApp"; import Templates from "../utils/templates"; import Label from "./Label"; import { Message } from "./Message"; export const AppForm = ({ template: cliTemplate = "", slug: givenSlug = "", action, }: { template?: string; slug?: string; action: "create" | "edit" | "create-template" | "edit-template"; }) => { cliTemplate = Templates.find((t) => t.value === cliTemplate)?.value || ""; const { exit } = useApp(); const isTemplate = action === "create-template" || action === "edit-template"; const isEditAction = action === "edit" || action === "edit-template"; let initialConfig = { template: cliTemplate, name: "", description: "", category: "", publisher: "", email: "", }; const [app] = useState(() => getApp(givenSlug, isTemplate)); if ((givenSlug && action === "edit-template") || action === "edit") try { const config = JSON.parse( fs.readFileSync(`${getAppDirPath(givenSlug, isTemplate)}/config.json`).toString() ) as AppMeta; initialConfig = { ...config, category: config.categories[0], template: config.__template, }; } catch (e) {} const fields = [ { label: "App Title", name: "name", type: "text", explainer: "Keep it short and sweet like 'Google Meet'", optional: false, defaultValue: "", }, { label: "App Description", name: "description", type: "text", explainer: "A detailed description of your app. You can later modify DESCRIPTION.mdx to add markdown as well", optional: false, defaultValue: "", }, // You can't edit the base template of an App or Template - You need to start fresh for that. cliTemplate || isEditAction ? null : { label: "Choose a base Template", name: "template", type: "select", options: Templates, optional: false, defaultValue: "", }, { optional: false, label: "Category of App", name: "category", type: "select", // TODO: Refactor and reuse getAppCategories or type as Record to enforce consistency options: [ // Manually sorted alphabetically { label: "Analytics", value: "analytics" }, { label: "Automation", value: "automation" }, { label: "Calendar", value: "calendar" }, { label: "Conferencing", value: "conferencing" }, { label: "CRM", value: "crm" }, { label: "Messaging", value: "messaging" }, { label: "Payment", value: "payment" }, { label: "Other", value: "other" }, ], defaultValue: "", explainer: "This is how apps are categorized in App Store.", }, { optional: true, label: "Publisher Name", name: "publisher", type: "text", explainer: "Let users know who you are", defaultValue: "Your Name", }, { optional: true, label: "Publisher Email", name: "email", type: "text", explainer: "Let users know how they can contact you.", defaultValue: "email@example.com", }, ].filter((f) => f); const [appInputData, setAppInputData] = useState(initialConfig); const [inputIndex, setInputIndex] = useState(0); const [slugFinalized, setSlugFinalized] = useState(false); const field = fields[inputIndex]; const fieldLabel = field?.label || ""; const fieldName = field?.name || ""; let fieldValue = appInputData[fieldName as keyof typeof appInputData] || ""; let validationResult: Parameters[0]["message"] | null = null; const { name, category, description, publisher, email, template } = appInputData; const [status, setStatus] = useState<"inProgress" | "done">("inProgress"); const formCompleted = inputIndex === fields.length; if (field?.name === "appCategory") { // Use template category as the default category fieldValue = Templates.find((t) => t.value === appInputData["template"])?.category || ""; } const slug = getSlugFromAppName(name) || givenSlug; useEffect(() => { // When all fields have been filled (async () => { if (formCompleted) { await BaseAppFork.create({ category, description, name, slug, publisher, email, template, editMode: isEditAction, isTemplate, oldSlug: givenSlug, }); await generateAppFiles(); // FIXME: Even after CLI showing this message, it is stuck doing work before exiting // So we ask the user to wait for some time setStatus("done"); } })(); }, [formCompleted]); if (action === "edit" || action === "edit-template") { if (!slug) { return --slug is required; } if (!app) { return ( ); } } if (status === "done") { // HACK: This is a hack to exit the process manually because due to some reason cli isn't automatically exiting setTimeout(() => { exit(); }, 500); } if (formCompleted) { return ( {status !== "done" && ( )} {status === "done" && ( Just wait for a few seconds for process to exit and then you are good to go. Your{" "} {isTemplate ? "Template" : "App"} code exists at {getAppDirPath(slug, isTemplate)} Tip : Go and change the logo of your {isTemplate ? "template" : "app"} by replacing{" "} {getAppDirPath(slug, isTemplate) + "/static/icon.svg"} App Summary: Slug: {slug} {isTemplate ? "Template" : "App"} URL: {`http://localhost:3000/apps/${slug}`} Name: {name} Description: {description} Category: {category} Publisher Name: {publisher} Publisher Email: {email} Next Step: Enable the app from http://localhost:3000/settings/admin/apps as admin user (Email: admin@example.com, Pass: ADMINadmin2022!) )} Note: You should not rename app directory manually. Use cli only to do that as it needs to be updated in DB as well ); } if (slug && slug !== givenSlug && fs.existsSync(getAppDirPath(slug, isTemplate))) { validationResult = { text: `${ action === "create" ? "App" : "Template" } with slug ${slug} already exists. If you want to edit it, use edit command`, type: "error", }; if (slugFinalized) { return ; } } const selectedOptionIndex = field?.type === "select" ? field?.options?.findIndex((o) => o.value === fieldValue) : 0; return ( {isEditAction ? ( ) : ( )} {field?.type == "text" ? ( { if (!value && !field.optional) { return; } setSlugFinalized(true); setInputIndex((index) => { return index + 1; }); }} onChange={(value) => { setAppInputData((appInputData) => { return { ...appInputData, [fieldName]: value, }; }); }} /> ) : ( items={field?.options} itemComponent={(item) => { const myItem = item as { value: string; label: string }; return ( {myItem.value}: {item.label} ); }} key={fieldName} initialIndex={selectedOptionIndex === -1 ? 0 : selectedOptionIndex} onSelect={(item) => { setAppInputData((appInputData) => { return { ...appInputData, [fieldName]: item.value, }; }); setInputIndex((index) => { return index + 1; }); }} /> )} {validationResult ? ( ) : ( {field?.explainer} )} ); };