Files
calendar/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx
T
171827f547 chore: recategorize apps (#9306)
* Remove unused code in InstalledAppsLayout

* Add new app categories "crm", "conferencing" and "messaging"

* Sort getAppCategories entries alphabetically

* Fix 404s on new category pages (and remove hardcoded category lists)

* Fix admin apps list not showing "no available apps" for new categories

* Recategorise apps

* Sync seed-app-store categories with config files

* Replace unnecessary seed-app-store.config.json with appStoreMetadata

* Copy video.svg to conferencing.svg

* Add messaging.svg

* Remove web3 from getAppCategories (used by installed apps, admin apps)

* Fix app-store-cli categories

- Add conferencing
- Add CRM
- Remove video
- Remove web3

* Remove outdated web3 comment in seed-app-store

* Update apps/web/public/static/locales/en/common.json

* Add cron script to keep db apps in sync with app metadata

* Add redirect for app category "video" to "conferencing"

* Fix up "video" category overrides to apply to conferencing

* Fix conferencing apps not showing as a location for non-team users

* Restore "installed_app" string for conferencing apps

* Make linter happier

* Remove my "installed_app_conferencing_description" as this was fixed upstream

* Quick tidy up

* Add dry-run to syncAppMeta via  CRON_ENABLE_APP_SYNC env

* Replace console.log with logger in syncAppMeta

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-06-28 18:22:51 +02:00

359 lines
11 KiB
TypeScript

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<AppCategories,> 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<typeof Message>[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 <Text>--slug is required</Text>;
}
if (!app) {
return (
<Message
message={{
text: `App with slug ${givenSlug} not found`,
type: "error",
}}
/>
);
}
}
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 (
<Box flexDirection="column">
{status !== "done" && (
<Message
key="progressHeading"
message={{
text: isEditAction
? `Editing app with slug ${slug}`
: `Creating ${
action === "create-template" ? "template" : "app"
} with name '${name}' categorized in '${category}' using template '${template}'`,
type: "info",
showInProgressIndicator: true,
}}
/>
)}
{status === "done" && (
<Box flexDirection="column" paddingTop={2} paddingBottom={2}>
<Text bold>
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)}
</Text>
<Text>
Tip : Go and change the logo of your {isTemplate ? "template" : "app"} by replacing{" "}
{getAppDirPath(slug, isTemplate) + "/static/icon.svg"}
</Text>
<Newline />
<Text bold underline color="blue">
App Summary:
</Text>
<Box flexDirection="column">
<Box flexDirection="row">
<Text color="green">Slug: </Text>
<Text>{slug}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">{isTemplate ? "Template" : "App"} URL: </Text>
<Text>{`http://localhost:3000/apps/${slug}`}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Name: </Text>
<Text>{name}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Description: </Text>
<Text>{description}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Category: </Text>
<Text>{category}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Publisher Name: </Text>
<Text>{publisher}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Publisher Email: </Text>
<Text>{email}</Text>
</Box>
<Text bold>
Next Step: Enable the app from http://localhost:3000/settings/admin/apps as admin user (Email:
admin@example.com, Pass: ADMINadmin2022!)
</Text>
</Box>
</Box>
)}
<Text italic color="gray">
Note: You should not rename app directory manually. Use cli only to do that as it needs to be
updated in DB as well
</Text>
</Box>
);
}
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 <Message message={validationResult} />;
}
}
const selectedOptionIndex =
field?.type === "select" ? field?.options?.findIndex((o) => o.value === fieldValue) : 0;
return (
<Box flexDirection="column">
<Box flexDirection="column">
{isEditAction ? (
<Message
message={{
text: `\nLet's edit your ${isTemplate ? "Template" : "App"}! We have prefilled the details.\n`,
}}
/>
) : (
<Message
message={{
text: `\nLet's create your ${
isTemplate ? "Template" : "App"
}! Start by providing the information that's asked\n`,
}}
/>
)}
<Box>
<Label>{`${fieldLabel}`}</Label>
{field?.type == "text" ? (
<TextInput
value={fieldValue}
placeholder={field?.defaultValue}
onSubmit={(value) => {
if (!value && !field.optional) {
return;
}
setSlugFinalized(true);
setInputIndex((index) => {
return index + 1;
});
}}
onChange={(value) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: value,
};
});
}}
/>
) : (
<SelectInput<string>
items={field?.options}
itemComponent={(item) => {
const myItem = item as { value: string; label: string };
return (
<Box justifyContent="space-between">
<Box flexShrink={0} flexGrow={1}>
<Text color="blue">{myItem.value}: </Text>
</Box>
<Text>{item.label}</Text>
</Box>
);
}}
key={fieldName}
initialIndex={selectedOptionIndex === -1 ? 0 : selectedOptionIndex}
onSelect={(item) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: item.value,
};
});
setInputIndex((index) => {
return index + 1;
});
}}
/>
)}
</Box>
<Box>
{validationResult ? (
<Message message={validationResult} />
) : (
<Text color="gray" italic>
{field?.explainer}
</Text>
)}
</Box>
</Box>
</Box>
);
};