Merge pull request #253 from bknd-io/feat/ui-readonly-and-fixes

ui: reflect readonly mode by hiding controls + various small styling fixes
This commit is contained in:
dswbx
2025-09-05 17:10:55 +02:00
committed by GitHub
18 changed files with 153 additions and 74 deletions

View File

@@ -88,7 +88,7 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
{...props} {...props}
ref={ref} ref={ref}
className={twMerge( className={twMerge(
"bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full", "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full disabled:cursor-not-allowed",
disabledOrReadonly && "bg-muted/50 text-primary/50", disabledOrReadonly && "bg-muted/50 text-primary/50",
!disabledOrReadonly && !disabledOrReadonly &&
"focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all", "focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all",

View File

@@ -88,6 +88,7 @@ const FieldImpl = ({
}, [inputProps?.defaultValue]); }, [inputProps?.defaultValue]);
const disabled = firstDefined( const disabled = firstDefined(
ctx.readOnly,
inputProps?.disabled, inputProps?.disabled,
props.disabled, props.disabled,
schema.readOnly, schema.readOnly,

View File

@@ -61,6 +61,7 @@ export type FormContext<Data> = {
options: FormOptions; options: FormOptions;
root: string; root: string;
_formStateAtom: PrimitiveAtom<FormState<Data>>; _formStateAtom: PrimitiveAtom<FormState<Data>>;
readOnly: boolean;
}; };
const FormContext = createContext<FormContext<any>>(undefined!); const FormContext = createContext<FormContext<any>>(undefined!);
@@ -81,6 +82,7 @@ export function Form<
hiddenSubmit = true, hiddenSubmit = true,
ignoreKeys = [], ignoreKeys = [],
options = {}, options = {},
readOnly = false,
...props ...props
}: Omit<ComponentPropsWithoutRef<"form">, "onChange" | "onSubmit"> & { }: Omit<ComponentPropsWithoutRef<"form">, "onChange" | "onSubmit"> & {
schema: Schema; schema: Schema;
@@ -93,6 +95,7 @@ export function Form<
hiddenSubmit?: boolean; hiddenSubmit?: boolean;
options?: FormOptions; options?: FormOptions;
initialValues?: Schema extends JSONSchema ? FromSchema<Schema> : never; initialValues?: Schema extends JSONSchema ? FromSchema<Schema> : never;
readOnly?: boolean;
}) { }) {
const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues); const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues);
const lib = useMemo(() => new Draft2019(schema), [JSON.stringify(schema)]); const lib = useMemo(() => new Draft2019(schema), [JSON.stringify(schema)]);
@@ -190,8 +193,9 @@ export function Form<
options, options,
root: "", root: "",
path: "", path: "",
readOnly,
}), }),
[schema, initialValues, options], [schema, initialValues, options, readOnly],
) as any; ) as any;
return ( return (

View File

@@ -339,15 +339,15 @@ export const SectionHeaderLink = <E extends React.ElementType = "a">({
<Tag <Tag
{...props} {...props}
className={twMerge( className={twMerge(
"hover:bg-primary/5 flex flex-row items-center justify-center gap-2.5 px-5 h-12 leading-none font-medium text-primary/80 rounded-tr-lg rounded-tl-lg", "flex flex-row items-center justify-center gap-2.5 px-5 h-12 leading-none font-medium text-primary/80 rounded-tr-lg rounded-tl-lg cursor-pointer z-2",
active active
? "bg-background hover:bg-background text-primary border border-muted border-b-0" ? "bg-primary/3 text-primary border border-muted border-b-0"
: "link", : "link hover:bg-primary/2",
badge && "pr-4", badge && "pr-4",
className, className,
)} )}
> >
{children} <span className="truncate">{children}</span>
{badge ? ( {badge ? (
<span className="px-3 py-1 rounded-full font-mono bg-primary/5 text-sm leading-none"> <span className="px-3 py-1 rounded-full font-mono bg-primary/5 text-sm leading-none">
{badge} {badge}
@@ -365,8 +365,8 @@ export type SectionHeaderTabsProps = {
}; };
export const SectionHeaderTabs = ({ title, items }: SectionHeaderTabsProps) => { export const SectionHeaderTabs = ({ title, items }: SectionHeaderTabsProps) => {
return ( return (
<SectionHeader className="mt-10 border-t pl-3 pb-0 items-end"> <SectionHeader className="mt-10 border-t border-t-muted pl-3 pb-0 items-end overflow-x-scroll app-scrollbar relative after:inset-0 after:z-1">
<div className="flex flex-row items-center gap-6 -mb-px"> <div className="flex flex-row items-center gap-6 relative">
{title && ( {title && (
<SectionHeaderTitle className="pl-2 hidden md:block">{title}</SectionHeaderTitle> <SectionHeaderTitle className="pl-2 hidden md:block">{title}</SectionHeaderTitle>
)} )}

View File

@@ -232,7 +232,7 @@ const PopoverTable = ({
data={container ?? []} data={container ?? []}
entity={entity} entity={entity}
select={query.select} select={query.select}
total={container.meta?.count} total={container.body.meta?.count}
page={query.page} page={query.page}
onClickRow={onClickRow} onClickRow={onClickRow}
onClickPage={onClickPage} onClickPage={onClickPage}

View File

@@ -8,7 +8,9 @@ import { s } from "bknd/utils";
import { cloneSchema } from "core/utils/schema"; import { cloneSchema } from "core/utils/schema";
const schema = s.object({ const schema = s.object({
name: entitySchema.properties.name, name: s.string({
pattern: /^[a-z][a-zA-Z_]+$/,
}),
config: entitySchema.properties.config.partial().optional(), config: entitySchema.properties.config.partial().optional(),
}); });
type Schema = s.Static<typeof schema>; type Schema = s.Static<typeof schema>;

View File

@@ -26,6 +26,7 @@ function AuthRolesEditInternal({ params }) {
const roleName = params.role; const roleName = params.role;
const role = config.roles?.[roleName]; const role = config.roles?.[roleName];
const formRef = useRef<AuthRoleFormRef>(null); const formRef = useRef<AuthRoleFormRef>(null);
const { readonly } = useBknd();
async function handleUpdate() { async function handleUpdate() {
console.log("data", formRef.current?.isValid()); console.log("data", formRef.current?.isValid());
@@ -57,7 +58,7 @@ function AuthRolesEditInternal({ params }) {
absolute: true, absolute: true,
}), }),
}, },
{ !readonly && {
label: "Delete", label: "Delete",
onClick: handleDelete, onClick: handleDelete,
destructive: true, destructive: true,
@@ -67,9 +68,11 @@ function AuthRolesEditInternal({ params }) {
> >
<IconButton Icon={TbDots} /> <IconButton Icon={TbDots} />
</Dropdown> </Dropdown>
!readonly && (
<Button variant="primary" onClick={handleUpdate}> <Button variant="primary" onClick={handleUpdate}>
Update Update
</Button> </Button>
)
</> </>
} }
className="pl-3" className="pl-3"

View File

@@ -11,10 +11,12 @@ import { Button } from "../../components/buttons/Button";
import { CellValue, DataTable } from "../../components/table/DataTable"; import { CellValue, DataTable } from "../../components/table/DataTable";
import * as AppShell from "../../layouts/AppShell/AppShell"; import * as AppShell from "../../layouts/AppShell/AppShell";
import { routes, useNavigate } from "../../lib/routes"; import { routes, useNavigate } from "../../lib/routes";
import { useBknd } from "ui/client/bknd";
export function AuthRolesList() { export function AuthRolesList() {
const [navigate] = useNavigate(); const [navigate] = useNavigate();
const { config, actions } = useBkndAuth(); const { config, actions } = useBkndAuth();
const { readonly } = useBknd();
const data = Object.values( const data = Object.values(
transformObject(config.roles ?? {}, (role, name) => ({ transformObject(config.roles ?? {}, (role, name) => ({
@@ -30,6 +32,7 @@ export function AuthRolesList() {
} }
function openCreateModal() { function openCreateModal() {
if (readonly) return;
bkndModals.open( bkndModals.open(
"form", "form",
{ {
@@ -59,9 +62,11 @@ export function AuthRolesList() {
<> <>
<AppShell.SectionHeader <AppShell.SectionHeader
right={ right={
!readonly && (
<Button variant="primary" onClick={openCreateModal}> <Button variant="primary" onClick={openCreateModal}>
Create new Create new
</Button> </Button>
)
} }
> >
Roles & Permissions Roles & Permissions

View File

@@ -52,6 +52,7 @@ const formConfig = {
function AuthSettingsInternal() { function AuthSettingsInternal() {
const { config, schema: _schema, actions, $auth } = useBkndAuth(); const { config, schema: _schema, actions, $auth } = useBkndAuth();
const { readonly } = useBknd();
const schema = JSON.parse(JSON.stringify(_schema)); const schema = JSON.parse(JSON.stringify(_schema));
schema.properties.jwt.required = ["alg"]; schema.properties.jwt.required = ["alg"];
@@ -61,7 +62,13 @@ function AuthSettingsInternal() {
} }
return ( return (
<Form schema={schema} initialValues={config as any} onSubmit={onSubmit} {...formConfig}> <Form
schema={schema}
initialValues={config as any}
onSubmit={onSubmit}
{...formConfig}
readOnly={readonly}
>
<Subscribe <Subscribe
selector={(state) => ({ selector={(state) => ({
dirty: state.dirty, dirty: state.dirty,
@@ -73,6 +80,7 @@ function AuthSettingsInternal() {
<AppShell.SectionHeader <AppShell.SectionHeader
className="pl-4" className="pl-4"
right={ right={
!readonly && (
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"
@@ -80,6 +88,7 @@ function AuthSettingsInternal() {
> >
Update Update
</Button> </Button>
)
} }
> >
Settings Settings

View File

@@ -60,6 +60,7 @@ const formOptions = {
}; };
function AuthStrategiesListInternal() { function AuthStrategiesListInternal() {
const { readonly } = useBknd();
const $auth = useBkndAuth(); const $auth = useBkndAuth();
const config = $auth.config.strategies; const config = $auth.config.strategies;
const schema = $auth.schema.properties.strategies; const schema = $auth.schema.properties.strategies;
@@ -80,6 +81,7 @@ function AuthStrategiesListInternal() {
initialValues={config} initialValues={config}
onSubmit={handleSubmit} onSubmit={handleSubmit}
options={formOptions} options={formOptions}
readOnly={readonly}
> >
<Subscribe <Subscribe
selector={(state) => ({ selector={(state) => ({
@@ -92,6 +94,7 @@ function AuthStrategiesListInternal() {
<AppShell.SectionHeader <AppShell.SectionHeader
className="pl-4" className="pl-4"
right={ right={
!readonly && (
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"
@@ -99,6 +102,7 @@ function AuthStrategiesListInternal() {
> >
Update Update
</Button> </Button>
)
} }
> >
Strategies Strategies

View File

@@ -112,9 +112,15 @@ const EntityLinkList = ({
suggestCreate = false, suggestCreate = false,
}: { entities: Entity[]; title?: string; context: "data" | "schema"; suggestCreate?: boolean }) => { }: { entities: Entity[]; title?: string; context: "data" | "schema"; suggestCreate?: boolean }) => {
const { $data } = useBkndData(); const { $data } = useBkndData();
const { readonly } = useBknd();
const navigate = useRouteNavigate(); const navigate = useRouteNavigate();
if (entities.length === 0) { if (entities.length === 0) {
return suggestCreate ? ( if (suggestCreate) {
if (readonly) {
return <Empty className="py-10" description="No entities created." />;
}
return (
<Empty <Empty
className="py-10" className="py-10"
description="Create your first entity to get started." description="Create your first entity to get started."
@@ -123,7 +129,10 @@ const EntityLinkList = ({
onClick: () => $data.modals.createEntity(), onClick: () => $data.modals.createEntity(),
}} }}
/> />
) : null; );
}
return null;
} }
function handleClick(entity: Entity) { function handleClick(entity: Entity) {
@@ -163,7 +172,7 @@ const EntityLinkList = ({
href={href} href={href}
className="justify-between items-center" className="justify-between items-center"
> >
{entity.label} <span className="truncate">{entity.label}</span>
{isLinkActive(href, 1) && ( {isLinkActive(href, 1) && (
<Button <Button

View File

@@ -17,6 +17,7 @@ import { bkndModals } from "ui/modals";
import { EntityForm } from "ui/modules/data/components/EntityForm"; import { EntityForm } from "ui/modules/data/components/EntityForm";
import { EntityTable2 } from "ui/modules/data/components/EntityTable2"; import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm"; import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
import { notifications } from "@mantine/notifications";
export function DataEntityUpdate({ params }) { export function DataEntityUpdate({ params }) {
return <DataEntityUpdateImpl params={params} key={params.entity} />; return <DataEntityUpdateImpl params={params} key={params.entity} />;
@@ -58,14 +59,22 @@ function DataEntityUpdateImpl({ params }) {
async function onSubmitted(changeSet?: EntityData) { async function onSubmitted(changeSet?: EntityData) {
//return; //return;
if (!changeSet) { if (!changeSet) {
goBack(); notifications.show({
title: `Updating ${entity?.label}`,
message: "No changes to update",
color: "yellow",
});
return; return;
} }
try { try {
await $q.update(changeSet); await $q.update(changeSet);
if (error) setError(null); if (error) setError(null);
goBack(); notifications.show({
title: `Updating ${entity?.label}`,
message: `Successfully updated ID ${entityId}`,
color: "green",
});
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : "Failed to update"); setError(e instanceof Error ? e.message : "Failed to update");
} }
@@ -76,6 +85,11 @@ function DataEntityUpdateImpl({ params }) {
try { try {
await $q._delete(); await $q._delete();
if (error) setError(null); if (error) setError(null);
notifications.show({
title: `Deleting ${entity?.label}`,
message: `Successfully deleted ID ${entityId}`,
color: "green",
});
goBack(); goBack();
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : "Failed to delete"); setError(e instanceof Error ? e.message : "Failed to delete");
@@ -233,7 +247,7 @@ function EntityDetailRelations({
return { return {
as: "button", as: "button",
type: "button", type: "button",
label: ucFirst(other.reference), label: ucFirst(other.entity.label),
onClick: () => handleClick(relation), onClick: () => handleClick(relation),
active: selected?.other(entity).reference === other.reference, active: selected?.other(entity).reference === other.reference,
badge: relation.type(), badge: relation.type(),

View File

@@ -12,6 +12,7 @@ import { routes, useNavigate } from "ui/lib/routes";
import { EntityForm } from "ui/modules/data/components/EntityForm"; import { EntityForm } from "ui/modules/data/components/EntityForm";
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm"; import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
import { s } from "bknd/utils"; import { s } from "bknd/utils";
import { notifications } from "@mantine/notifications";
export function DataEntityCreate({ params }) { export function DataEntityCreate({ params }) {
const { $data } = useBkndData(); const { $data } = useBkndData();
@@ -39,10 +40,18 @@ export function DataEntityCreate({ params }) {
if (!changeSet) return; if (!changeSet) return;
try { try {
await $q.create(changeSet); const result = await $q.create(changeSet);
if (error) setError(null); if (error) setError(null);
// @todo: navigate to created? if (result.id) {
notifications.show({
title: `Creating ${entity?.label}`,
message: `Successfully created with ID ${result.id}`,
color: "green",
});
navigate(routes.data.entity.edit(params.entity, result.id));
} else {
goBack(); goBack();
}
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : "Failed to create"); setError(e instanceof Error ? e.message : "Failed to create");
} }
@@ -79,8 +88,12 @@ export function DataEntityCreate({ params }) {
/> />
</> </>
} }
className="pl-3"
> >
<Breadcrumbs2 backTo={backHref} path={[{ label: entity.label }, { label: "Create" }]} /> <Breadcrumbs2
backTo={backHref}
path={[{ label: entity.label, href: backHref }, { label: "Create" }]}
/>
</AppShell.SectionHeader> </AppShell.SectionHeader>
<AppShell.Scrollable key={entity.name}> <AppShell.Scrollable key={entity.name}>
{error && ( {error && (

View File

@@ -169,6 +169,8 @@ function EntityCreateButton({ entity }: { entity: Entity }) {
media: b.app.config.media.entity_name, media: b.app.config.media.entity_name,
}; };
if (system.users === entity.name) { if (system.users === entity.name) {
if (b.readonly) return null;
return ( return (
<Button onClick={createUserModal.open} variant="primary"> <Button onClick={createUserModal.open} variant="primary">
New User New User

View File

@@ -40,6 +40,7 @@ const formConfig = {
function MediaSettingsInternal() { function MediaSettingsInternal() {
const { config, schema: _schema, actions } = useBkndMedia(); const { config, schema: _schema, actions } = useBkndMedia();
const { readonly } = useBknd();
const schema = JSON.parse(JSON.stringify(_schema)); const schema = JSON.parse(JSON.stringify(_schema));
schema.if = { properties: { enabled: { const: true } } }; schema.if = { properties: { enabled: { const: true } } };
@@ -53,7 +54,13 @@ function MediaSettingsInternal() {
return ( return (
<> <>
<Form schema={schema} initialValues={config as any} onSubmit={onSubmit} {...formConfig}> <Form
schema={schema}
initialValues={config as any}
onSubmit={onSubmit}
{...formConfig}
readOnly={readonly}
>
<Subscribe <Subscribe
selector={(state) => ({ selector={(state) => ({
dirty: state.dirty, dirty: state.dirty,
@@ -64,6 +71,7 @@ function MediaSettingsInternal() {
{({ dirty, errors, submitting }) => ( {({ dirty, errors, submitting }) => (
<AppShell.SectionHeader <AppShell.SectionHeader
right={ right={
!readonly && (
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"
@@ -71,6 +79,7 @@ function MediaSettingsInternal() {
> >
Update Update
</Button> </Button>
)
} }
> >
Settings Settings
@@ -132,6 +141,7 @@ const AdapterIcon = ({ type }: { type: string }) => {
function Adapters() { function Adapters() {
const ctx = AnyOf.useContext(); const ctx = AnyOf.useContext();
const { readonly } = useBknd();
return ( return (
<Formy.Group> <Formy.Group>
@@ -150,6 +160,7 @@ function Adapters() {
"flex flex-row items-center justify-center gap-3 border", "flex flex-row items-center justify-center gap-3 border",
ctx.selected === i && "border-primary", ctx.selected === i && "border-primary",
)} )}
disabled={readonly}
> >
<div> <div>
<AdapterIcon type={schema.properties.type.const} /> <AdapterIcon type={schema.properties.type.const} />

View File

@@ -107,8 +107,8 @@ export function Setting<Schema extends s.ObjectSchema = s.ObjectSchema>({
return; return;
}); });
const deleteAllowed = options?.allowDelete?.(config) ?? true; const deleteAllowed = (options?.allowDelete?.(config) ?? true) && !readonly;
const editAllowed = options?.allowEdit?.(config) ?? true; const editAllowed = (options?.allowEdit?.(config) ?? true) && !readonly;
const showAlert = options?.showAlert?.(config) ?? undefined; const showAlert = options?.showAlert?.(config) ?? undefined;
console.log("--setting", { schema, config, prefix, path, exclude }); console.log("--setting", { schema, config, prefix, path, exclude });
@@ -120,14 +120,14 @@ export function Setting<Schema extends s.ObjectSchema = s.ObjectSchema>({
extractedKeys.find((key) => window.location.pathname.endsWith(key)) ?? extractedKeys[0]; extractedKeys.find((key) => window.location.pathname.endsWith(key)) ?? extractedKeys[0];
const onToggleEdit = useEvent(() => { const onToggleEdit = useEvent(() => {
if (!editAllowed || readonly) return; if (!editAllowed) return;
setEditing((prev) => !prev); setEditing((prev) => !prev);
//formRef.current?.cancel(); //formRef.current?.cancel();
}); });
const onSave = useEvent(async () => { const onSave = useEvent(async () => {
if (!editAllowed || !editing || readonly) return; if (!editAllowed || !editing) return;
if (formRef.current?.validateForm()) { if (formRef.current?.validateForm()) {
setSubmitting(true); setSubmitting(true);
@@ -215,14 +215,14 @@ export function Setting<Schema extends s.ObjectSchema = s.ObjectSchema>({
> >
<IconButton Icon={TbSettings} /> <IconButton Icon={TbSettings} />
</Dropdown> </Dropdown>
<Button onClick={onToggleEdit} disabled={!editAllowed || readonly}> <Button onClick={onToggleEdit} disabled={!editAllowed}>
{editing ? "Cancel" : "Edit"} {editing ? "Cancel" : "Edit"}
</Button> </Button>
{editing && ( {editing && (
<Button <Button
variant="primary" variant="primary"
onClick={onSave} onClick={onSave}
disabled={submitting || !editAllowed || readonly} disabled={submitting || !editAllowed}
> >
{submitting ? "Save..." : "Save"} {submitting ? "Save..." : "Save"}
</Button> </Button>

View File

@@ -30,7 +30,7 @@ export const SettingNewModal = ({
const [location, navigate] = useLocation(); const [location, navigate] = useLocation();
const [formSchema, setFormSchema] = useState(schema); const [formSchema, setFormSchema] = useState(schema);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const { actions } = useBknd(); const { actions, readonly } = useBknd();
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const isGeneratedKey = generateKey !== undefined; const isGeneratedKey = generateKey !== undefined;
const isStaticGeneratedKey = typeof generateKey === "string"; const isStaticGeneratedKey = typeof generateKey === "string";
@@ -98,6 +98,7 @@ export const SettingNewModal = ({
return ( return (
<> <>
{!readonly && (
<div className="flex flex-row"> <div className="flex flex-row">
{isAnyOf ? ( {isAnyOf ? (
<Dropdown position="top-start" items={anyOfItems} itemsClassName="gap-3"> <Dropdown position="top-start" items={anyOfItems} itemsClassName="gap-3">
@@ -107,6 +108,7 @@ export const SettingNewModal = ({
<Button onClick={open}>Add new</Button> <Button onClick={open}>Add new</Button>
)} )}
</div> </div>
)}
<Modal <Modal
open={opened} open={opened}

View File

@@ -67,7 +67,7 @@ export const DataSettings = ({
schema, schema,
config, config,
}: { schema: ModuleSchemas["data"]; config: ModuleConfigs["data"] }) => { }: { schema: ModuleSchemas["data"]; config: ModuleConfigs["data"] }) => {
const { app } = useBknd(); const { app, readonly } = useBknd();
const prefix = app.getAbsolutePath("settings"); const prefix = app.getAbsolutePath("settings");
const entities = Object.keys(config.entities ?? {}); const entities = Object.keys(config.entities ?? {});
@@ -105,7 +105,7 @@ export const DataSettings = ({
options={{ options={{
showAlert: (config: any) => { showAlert: (config: any) => {
// it's weird, but after creation, the config is not set (?) // it's weird, but after creation, the config is not set (?)
if (config?.type === "primary") { if (config?.type === "primary" && !readonly) {
return "Modifying the primary field may result in strange behaviors."; return "Modifying the primary field may result in strange behaviors.";
} }
return; return;
@@ -137,7 +137,7 @@ export const DataSettings = ({
config={config.entities?.[entity] as any} config={config.entities?.[entity] as any}
options={{ options={{
showAlert: (config: any) => { showAlert: (config: any) => {
if (config.type === "system") { if (config.type === "system" && !readonly) {
return "Modifying the system entities may result in strange behaviors."; return "Modifying the system entities may result in strange behaviors.";
} }
return; return;