Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 🎸 Custom UI Boiler Plate Code #121

Draft
wants to merge 29 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1126a89
feat: 🎸 Prompt Choices for UI Build Type
ceroy-ak Oct 7, 2024
ba57497
docs: ✏️ Added docs
ceroy-ak Oct 7, 2024
27c494e
fix: 🐛 argument name and invalid argument bug fix
ceroy-ak Oct 7, 2024
3452efe
fix: 🐛 removed unnecessary promises and added initialanswer
ceroy-ak Oct 7, 2024
a3aefdc
feat: 🎸 React Custom UI
ceroy-ak Oct 8, 2024
7b960b2
fix: 🐛 removed forked github repo link
ceroy-ak Oct 8, 2024
96cd85e
introduction of script for additional runtime changes when needed for…
ceroy-ak Oct 8, 2024
8abf037
changes
ceroy-ak Oct 9, 2024
5e61425
feat: 🎸 Custom React UI Components
ceroy-ak Oct 9, 2024
267a54d
feat: 🎸 Custom UI design for EmailPassword and PasswordLess
ceroy-ak Oct 10, 2024
222891d
feat: 🎸 Custom UI for Thirdparty
ceroy-ak Oct 10, 2024
473df43
fixes
ceroy-ak Oct 10, 2024
26da43d
thirdpartypasswordless UI
ceroy-ak Oct 10, 2024
4810897
Thirdparty Passwordless defunct code removed
ceroy-ak Oct 11, 2024
cc31372
thirdpartypasswordless UI
ceroy-ak Oct 11, 2024
19c7fd6
thirdpartyemailpassword UI
ceroy-ak Oct 11, 2024
e008b0e
fix: 🐛 Moved Scripts to scriptUtils
ceroy-ak Oct 11, 2024
16599c0
thirdpartyemailpassword config
ceroy-ak Oct 11, 2024
7c269e9
thirdpartyemailpassword UI
ceroy-ak Oct 11, 2024
a8a0315
fixes
ceroy-ak Oct 11, 2024
911f2fa
reset package.json file
ceroy-ak Oct 11, 2024
4368abc
removed the setup-scripts folder from js build folder
ceroy-ak Oct 11, 2024
b0c1e97
added ui flag check for frontend prompt when condition
ceroy-ak Oct 11, 2024
36dc67a
fixes
ceroy-ak Oct 11, 2024
16b865b
removed import from build type and session initial state is undefined
ceroy-ak Oct 11, 2024
125aafa
moved the business logic from hooks to utils file
ceroy-ak Oct 11, 2024
40d03a4
moved all utils to components itself to reduce end user cognition
ceroy-ak Oct 12, 2024
d7728da
added loading state for thirdparty
ceroy-ak Oct 12, 2024
6a0f191
fixes
ceroy-ak Oct 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions lib/ts/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Answers, QuestionOption, RecipeQuestionOption, UserFlags } from "./types.js";
import { Answers, QuestionOption, RecipeQuestionOption, UIBuildType, UIBuildTypeOption, UserFlags } from "./types.js";
import { FILTER_CHOICES_STRATEGY, filterChoices } from "./filterChoicesUtils.js";
import { validateFolderName } from "./userArgumentUtils.js";
import {
getDjangoPythonRunScripts,
Expand Down Expand Up @@ -467,6 +468,17 @@ export const recipeOptions: RecipeQuestionOption[] = [
},
];

export const uiBuildOptions: UIBuildTypeOption[] = [
{
value: UIBuildType.PRE_BUILT,
displayName: "Pre-built UI (Recommended)",
},
{
value: UIBuildType.CUSTOM,
displayName: "Custom UI",
},
];

/**
* Export for all the questions to ask the user, should follow the exact format mentioned here https://github.com/SBoudrias/Inquirer.js#objects because this config is passed to inquirer. The order of questions depends on the position of the object in the array
*/
Expand Down Expand Up @@ -496,11 +508,29 @@ export async function getQuestions(flags: UserFlags) {
return "Invalid project name: " + validations.problems![0];
},
},
{
name: "ui",
type: "list",
message: "Choose the ui built type for your frontend:",
choices: mapOptionsToChoices(uiBuildOptions),
when: (answers: Answers) => {
if (flags.ui) {
answers.ui = flags.ui;
ceroy-ak marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
return true;
},
},
{
name: "frontend",
type: "list",
message: "Choose a frontend framework (Visit our documentation for integration with other frameworks):",
choices: mapOptionsToChoices(await getFrontendOptions(flags)),
choices: async (answers: Answers) =>
filterChoices(
mapOptionsToChoices(await getFrontendOptions(flags)),
answers,
FILTER_CHOICES_STRATEGY.filterFrontendByUiType
),
when: flags.frontend === undefined,
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
},
{
Expand Down Expand Up @@ -555,7 +585,12 @@ export async function getQuestions(flags: UserFlags) {
name: "recipe",
type: "list",
message: "What type of authentication do you want to use?",
choices: mapOptionsToChoices(recipeOptions),
choices: async (answers: Answers) =>
filterChoices(
mapOptionsToChoices(recipeOptions),
answers,
FILTER_CHOICES_STRATEGY.filterRecipeByUiType
),
when: (answers: Answers) => {
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
// For capacitor we don't ask this question because it has its own way of swapping between recipes
if (answers.frontend === "capacitor") {
Expand Down
111 changes: 111 additions & 0 deletions lib/ts/filterChoicesUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Answers, IPromptFilterStrategy, PromptListChoice, UIBuildType, UserFlagsRaw } from "./types.js";

const getIsValueSupported = (value: string, supportedValues: string[]) => {
return supportedValues.includes(value);
};

const filterFrontendByUiType: IPromptFilterStrategy = {
validValues: ["react"],
ceroy-ak marked this conversation as resolved.
Show resolved Hide resolved
filterChoices(choices, answers) {
if (answers.ui === UIBuildType.PRE_BUILT) {
return choices;
}
return choices.filter((choice) => getIsValueSupported(choice.value, this.validValues));
},
validateUserArguments(userArguments) {
if (!userArguments.frontend) return true;
return (
userArguments.ui === UIBuildType.PRE_BUILT || getIsValueSupported(userArguments.frontend, this.validValues)
);
},
};

const filterRecipeByUiType: IPromptFilterStrategy = {
validValues: ["emailpassword", "thirdpartyemailpassword"],
ceroy-ak marked this conversation as resolved.
Show resolved Hide resolved
filterChoices(choices, answers) {
if (answers.ui === UIBuildType.PRE_BUILT) {
return choices;
}
return choices.filter((choice) => getIsValueSupported(choice.value, this.validValues));
},
validateUserArguments(userArguments) {
if (!userArguments.recipe) return true;
return (
userArguments.ui === UIBuildType.PRE_BUILT || getIsValueSupported(userArguments.recipe, this.validValues)
);
},
};

export const FILTER_CHOICES_STRATEGY = {
filterFrontendByUiType,
filterRecipeByUiType,
};

/**
* Filters a list of choices based on the provided strategy or strategies.
*
* @param choices - An array of `PromptListChoice` objects or a promise that resolves to such an array.
* @param answers - An object containing the answers to previous prompts.
* @param strategy - An optional filtering strategy or an array of strategies. This can be one of the predefined
* `FILTER_CHOICES_STRATEGY` values, an array of such values, or a custom function that takes an
* array of choices and returns a promise that resolves to a filtered array of choices.
* @returns A promise that resolves to the filtered array of `PromptListChoice` objects.
*/
export const filterChoices = async (
choices: PromptListChoice[] | Promise<PromptListChoice[]>,
ceroy-ak marked this conversation as resolved.
Show resolved Hide resolved
answers: Answers,
strategy?:
| IPromptFilterStrategy
| IPromptFilterStrategy[]
ceroy-ak marked this conversation as resolved.
Show resolved Hide resolved
| ((choices: PromptListChoice[]) => Promise<PromptListChoice[]>)
ceroy-ak marked this conversation as resolved.
Show resolved Hide resolved
): Promise<PromptListChoice[]> => {
if (choices instanceof Promise) {
choices = await choices;
}
if (!strategy || (Array.isArray(strategy) && strategy.length === 0)) {
return choices;
}
if (typeof strategy === "function") {
return strategy(choices);
}
if (!Array.isArray(strategy)) {
strategy = [strategy];
}
for (const filter of strategy) {
choices = filter.filterChoices(choices, answers);
}
return choices;
};

export const validateUserArgumentsByFilterStrategy = (
userArguments: UserFlagsRaw,
argumentToValidateKey: keyof UserFlagsRaw,
strategy?: IPromptFilterStrategy | IPromptFilterStrategy[]
): boolean => {
if (!strategy) {
return true;
}
if (!Array.isArray(strategy)) {
strategy = [strategy];
}
if (!userArguments?.[argumentToValidateKey]) {
throw new Error(`Invalid ${argumentToValidateKey} provided`);
}

for (const filter of strategy) {
if (!filter.validateUserArguments(userArguments)) {
return false;
}
}
return true;
};

export const isValidUiBasedOnFilters = (userArguments: UserFlagsRaw): boolean => {
if (!userArguments.ui) return true;
if (!Object.values(UIBuildType).includes(userArguments.ui)) return false;
if (userArguments.ui === UIBuildType.PRE_BUILT) return true;
return validateUserArgumentsByFilterStrategy(userArguments, "ui", [
FILTER_CHOICES_STRATEGY.filterFrontendByUiType,
FILTER_CHOICES_STRATEGY.filterRecipeByUiType,
]);
};
25 changes: 25 additions & 0 deletions lib/ts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,10 @@ export type RecipeQuestionOption = {
shouldDisplay?: boolean;
};

export type UIBuildTypeOption = RecipeQuestionOption;

export type Answers = {
ui: UIBuildType;
frontend?: SupportedFrontends;
backend?: SupportedBackends;
recipe: string;
Expand All @@ -216,6 +219,11 @@ export type SupportedPackageManagers = "npm" | "yarn" | "pnpm" | "bun";

export const allPackageManagers: SupportedPackageManagers[] = ["npm", "yarn", "pnpm", "bun"];

export enum UIBuildType {
CUSTOM = "custom",
PRE_BUILT = "pre-built",
}

export function isValidPackageManager(manager: string): manager is SupportedPackageManagers {
if (allPackageManagers.includes(manager as SupportedPackageManagers)) {
return true;
Expand All @@ -232,6 +240,7 @@ export type UserFlagsRaw = {
frontend?: SupportedFrontends;
backend?: SupportedBackends;
manager?: SupportedPackageManagers;
ui?: UIBuildType;
autostart?: string | boolean;
};

Expand Down Expand Up @@ -268,3 +277,19 @@ export type AnalyticsEventWithCommonProperties = AnalyticsEvent & {
os: string;
cliversion: string;
};

export type PromptListChoice = {
name: string;
value: string;
};

export const isValidUIBuildType = (userArguments: UserFlagsRaw): boolean => {
if (!userArguments.ui) return true;
return Object.values(UIBuildType).includes(userArguments.ui);
};

export interface IPromptFilterStrategy {
validValues: string[];
filterChoices: (choices: PromptListChoice[], answers: Answers) => PromptListChoice[];
validateUserArguments: (userArguments: UserFlagsRaw) => boolean;
}
19 changes: 19 additions & 0 deletions lib/ts/userArgumentUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import {
isValidFrontend,
isValidPackageManager,
isValidRecipeName,
isValidUIBuildType,
UIBuildType,
UserFlags,
UserFlagsRaw,
} from "./types.js";
import validateProjectName from "validate-npm-package-name";
import path from "path";
import { isValidUiBasedOnFilters } from "./filterChoicesUtils.js";

export function validateNpmName(name: string): {
valid: boolean;
Expand Down Expand Up @@ -78,6 +81,18 @@ export function validateUserArguments(userArguments: UserFlagsRaw) {
throw new Error("Invalid package manager provided, valid values:\n" + availableManagers);
}
}

if (userArguments.ui !== undefined) {
if (!isValidUIBuildType(userArguments)) {
throw new Error(
`Invalid argument for ui provided. Supported arguments are ${Object.values(UIBuildType).join(", ")}`
);
}

if (!isValidUiBasedOnFilters(userArguments)) {
throw new Error(`UI type ${userArguments.ui} is not supported for the selected frontend or recipe.`);
}
ceroy-ak marked this conversation as resolved.
Show resolved Hide resolved
}
}

export function modifyAnswersBasedOnSelection(answers: Answers): Answers {
Expand Down Expand Up @@ -113,6 +128,10 @@ export function modifyAnswersBasedOnFlags(answers: Answers, userArguments: UserF
_answers.recipe = userArguments.recipe;
}

if (userArguments.ui !== undefined) {
_answers.ui = userArguments.ui;
}

if (userArguments.frontend !== undefined) {
const selectedFrontend = allFrontends.filter((i) => userArguments.frontend === i.id);

Expand Down