mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
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:
@@ -88,7 +88,7 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
|
||||
{...props}
|
||||
ref={ref}
|
||||
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 &&
|
||||
"focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all",
|
||||
|
||||
@@ -88,6 +88,7 @@ const FieldImpl = ({
|
||||
}, [inputProps?.defaultValue]);
|
||||
|
||||
const disabled = firstDefined(
|
||||
ctx.readOnly,
|
||||
inputProps?.disabled,
|
||||
props.disabled,
|
||||
schema.readOnly,
|
||||
|
||||
@@ -61,6 +61,7 @@ export type FormContext<Data> = {
|
||||
options: FormOptions;
|
||||
root: string;
|
||||
_formStateAtom: PrimitiveAtom<FormState<Data>>;
|
||||
readOnly: boolean;
|
||||
};
|
||||
|
||||
const FormContext = createContext<FormContext<any>>(undefined!);
|
||||
@@ -81,6 +82,7 @@ export function Form<
|
||||
hiddenSubmit = true,
|
||||
ignoreKeys = [],
|
||||
options = {},
|
||||
readOnly = false,
|
||||
...props
|
||||
}: Omit<ComponentPropsWithoutRef<"form">, "onChange" | "onSubmit"> & {
|
||||
schema: Schema;
|
||||
@@ -93,6 +95,7 @@ export function Form<
|
||||
hiddenSubmit?: boolean;
|
||||
options?: FormOptions;
|
||||
initialValues?: Schema extends JSONSchema ? FromSchema<Schema> : never;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues);
|
||||
const lib = useMemo(() => new Draft2019(schema), [JSON.stringify(schema)]);
|
||||
@@ -190,8 +193,9 @@ export function Form<
|
||||
options,
|
||||
root: "",
|
||||
path: "",
|
||||
readOnly,
|
||||
}),
|
||||
[schema, initialValues, options],
|
||||
[schema, initialValues, options, readOnly],
|
||||
) as any;
|
||||
|
||||
return (
|
||||
|
||||
@@ -339,15 +339,15 @@ export const SectionHeaderLink = <E extends React.ElementType = "a">({
|
||||
<Tag
|
||||
{...props}
|
||||
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
|
||||
? "bg-background hover:bg-background text-primary border border-muted border-b-0"
|
||||
: "link",
|
||||
? "bg-primary/3 text-primary border border-muted border-b-0"
|
||||
: "link hover:bg-primary/2",
|
||||
badge && "pr-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<span className="truncate">{children}</span>
|
||||
{badge ? (
|
||||
<span className="px-3 py-1 rounded-full font-mono bg-primary/5 text-sm leading-none">
|
||||
{badge}
|
||||
@@ -365,8 +365,8 @@ export type SectionHeaderTabsProps = {
|
||||
};
|
||||
export const SectionHeaderTabs = ({ title, items }: SectionHeaderTabsProps) => {
|
||||
return (
|
||||
<SectionHeader className="mt-10 border-t pl-3 pb-0 items-end">
|
||||
<div className="flex flex-row items-center gap-6 -mb-px">
|
||||
<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 relative">
|
||||
{title && (
|
||||
<SectionHeaderTitle className="pl-2 hidden md:block">{title}</SectionHeaderTitle>
|
||||
)}
|
||||
|
||||
@@ -232,7 +232,7 @@ const PopoverTable = ({
|
||||
data={container ?? []}
|
||||
entity={entity}
|
||||
select={query.select}
|
||||
total={container.meta?.count}
|
||||
total={container.body.meta?.count}
|
||||
page={query.page}
|
||||
onClickRow={onClickRow}
|
||||
onClickPage={onClickPage}
|
||||
|
||||
@@ -8,7 +8,9 @@ import { s } from "bknd/utils";
|
||||
import { cloneSchema } from "core/utils/schema";
|
||||
|
||||
const schema = s.object({
|
||||
name: entitySchema.properties.name,
|
||||
name: s.string({
|
||||
pattern: /^[a-z][a-zA-Z_]+$/,
|
||||
}),
|
||||
config: entitySchema.properties.config.partial().optional(),
|
||||
});
|
||||
type Schema = s.Static<typeof schema>;
|
||||
|
||||
@@ -26,6 +26,7 @@ function AuthRolesEditInternal({ params }) {
|
||||
const roleName = params.role;
|
||||
const role = config.roles?.[roleName];
|
||||
const formRef = useRef<AuthRoleFormRef>(null);
|
||||
const { readonly } = useBknd();
|
||||
|
||||
async function handleUpdate() {
|
||||
console.log("data", formRef.current?.isValid());
|
||||
@@ -57,7 +58,7 @@ function AuthRolesEditInternal({ params }) {
|
||||
absolute: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
!readonly && {
|
||||
label: "Delete",
|
||||
onClick: handleDelete,
|
||||
destructive: true,
|
||||
@@ -67,9 +68,11 @@ function AuthRolesEditInternal({ params }) {
|
||||
>
|
||||
<IconButton Icon={TbDots} />
|
||||
</Dropdown>
|
||||
!readonly && (
|
||||
<Button variant="primary" onClick={handleUpdate}>
|
||||
Update
|
||||
</Button>
|
||||
)
|
||||
</>
|
||||
}
|
||||
className="pl-3"
|
||||
|
||||
@@ -11,10 +11,12 @@ import { Button } from "../../components/buttons/Button";
|
||||
import { CellValue, DataTable } from "../../components/table/DataTable";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
import { routes, useNavigate } from "../../lib/routes";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
export function AuthRolesList() {
|
||||
const [navigate] = useNavigate();
|
||||
const { config, actions } = useBkndAuth();
|
||||
const { readonly } = useBknd();
|
||||
|
||||
const data = Object.values(
|
||||
transformObject(config.roles ?? {}, (role, name) => ({
|
||||
@@ -30,6 +32,7 @@ export function AuthRolesList() {
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
if (readonly) return;
|
||||
bkndModals.open(
|
||||
"form",
|
||||
{
|
||||
@@ -59,9 +62,11 @@ export function AuthRolesList() {
|
||||
<>
|
||||
<AppShell.SectionHeader
|
||||
right={
|
||||
<Button variant="primary" onClick={openCreateModal}>
|
||||
Create new
|
||||
</Button>
|
||||
!readonly && (
|
||||
<Button variant="primary" onClick={openCreateModal}>
|
||||
Create new
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
Roles & Permissions
|
||||
|
||||
@@ -52,6 +52,7 @@ const formConfig = {
|
||||
|
||||
function AuthSettingsInternal() {
|
||||
const { config, schema: _schema, actions, $auth } = useBkndAuth();
|
||||
const { readonly } = useBknd();
|
||||
const schema = JSON.parse(JSON.stringify(_schema));
|
||||
|
||||
schema.properties.jwt.required = ["alg"];
|
||||
@@ -61,7 +62,13 @@ function AuthSettingsInternal() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Form schema={schema} initialValues={config as any} onSubmit={onSubmit} {...formConfig}>
|
||||
<Form
|
||||
schema={schema}
|
||||
initialValues={config as any}
|
||||
onSubmit={onSubmit}
|
||||
{...formConfig}
|
||||
readOnly={readonly}
|
||||
>
|
||||
<Subscribe
|
||||
selector={(state) => ({
|
||||
dirty: state.dirty,
|
||||
@@ -73,13 +80,15 @@ function AuthSettingsInternal() {
|
||||
<AppShell.SectionHeader
|
||||
className="pl-4"
|
||||
right={
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={!dirty || errors || submitting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
!readonly && (
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={!dirty || errors || submitting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
Settings
|
||||
|
||||
@@ -60,6 +60,7 @@ const formOptions = {
|
||||
};
|
||||
|
||||
function AuthStrategiesListInternal() {
|
||||
const { readonly } = useBknd();
|
||||
const $auth = useBkndAuth();
|
||||
const config = $auth.config.strategies;
|
||||
const schema = $auth.schema.properties.strategies;
|
||||
@@ -80,6 +81,7 @@ function AuthStrategiesListInternal() {
|
||||
initialValues={config}
|
||||
onSubmit={handleSubmit}
|
||||
options={formOptions}
|
||||
readOnly={readonly}
|
||||
>
|
||||
<Subscribe
|
||||
selector={(state) => ({
|
||||
@@ -92,13 +94,15 @@ function AuthStrategiesListInternal() {
|
||||
<AppShell.SectionHeader
|
||||
className="pl-4"
|
||||
right={
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={!dirty || errors || submitting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
!readonly && (
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={!dirty || errors || submitting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
Strategies
|
||||
|
||||
@@ -112,18 +112,27 @@ const EntityLinkList = ({
|
||||
suggestCreate = false,
|
||||
}: { entities: Entity[]; title?: string; context: "data" | "schema"; suggestCreate?: boolean }) => {
|
||||
const { $data } = useBkndData();
|
||||
const { readonly } = useBknd();
|
||||
const navigate = useRouteNavigate();
|
||||
|
||||
if (entities.length === 0) {
|
||||
return suggestCreate ? (
|
||||
<Empty
|
||||
className="py-10"
|
||||
description="Create your first entity to get started."
|
||||
secondary={{
|
||||
children: "Create entity",
|
||||
onClick: () => $data.modals.createEntity(),
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
if (suggestCreate) {
|
||||
if (readonly) {
|
||||
return <Empty className="py-10" description="No entities created." />;
|
||||
}
|
||||
return (
|
||||
<Empty
|
||||
className="py-10"
|
||||
description="Create your first entity to get started."
|
||||
secondary={{
|
||||
children: "Create entity",
|
||||
onClick: () => $data.modals.createEntity(),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleClick(entity: Entity) {
|
||||
@@ -163,7 +172,7 @@ const EntityLinkList = ({
|
||||
href={href}
|
||||
className="justify-between items-center"
|
||||
>
|
||||
{entity.label}
|
||||
<span className="truncate">{entity.label}</span>
|
||||
|
||||
{isLinkActive(href, 1) && (
|
||||
<Button
|
||||
|
||||
@@ -17,6 +17,7 @@ import { bkndModals } from "ui/modals";
|
||||
import { EntityForm } from "ui/modules/data/components/EntityForm";
|
||||
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
||||
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export function DataEntityUpdate({ params }) {
|
||||
return <DataEntityUpdateImpl params={params} key={params.entity} />;
|
||||
@@ -58,14 +59,22 @@ function DataEntityUpdateImpl({ params }) {
|
||||
async function onSubmitted(changeSet?: EntityData) {
|
||||
//return;
|
||||
if (!changeSet) {
|
||||
goBack();
|
||||
notifications.show({
|
||||
title: `Updating ${entity?.label}`,
|
||||
message: "No changes to update",
|
||||
color: "yellow",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await $q.update(changeSet);
|
||||
if (error) setError(null);
|
||||
goBack();
|
||||
notifications.show({
|
||||
title: `Updating ${entity?.label}`,
|
||||
message: `Successfully updated ID ${entityId}`,
|
||||
color: "green",
|
||||
});
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to update");
|
||||
}
|
||||
@@ -76,6 +85,11 @@ function DataEntityUpdateImpl({ params }) {
|
||||
try {
|
||||
await $q._delete();
|
||||
if (error) setError(null);
|
||||
notifications.show({
|
||||
title: `Deleting ${entity?.label}`,
|
||||
message: `Successfully deleted ID ${entityId}`,
|
||||
color: "green",
|
||||
});
|
||||
goBack();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to delete");
|
||||
@@ -233,7 +247,7 @@ function EntityDetailRelations({
|
||||
return {
|
||||
as: "button",
|
||||
type: "button",
|
||||
label: ucFirst(other.reference),
|
||||
label: ucFirst(other.entity.label),
|
||||
onClick: () => handleClick(relation),
|
||||
active: selected?.other(entity).reference === other.reference,
|
||||
badge: relation.type(),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { routes, useNavigate } from "ui/lib/routes";
|
||||
import { EntityForm } from "ui/modules/data/components/EntityForm";
|
||||
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
||||
import { s } from "bknd/utils";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export function DataEntityCreate({ params }) {
|
||||
const { $data } = useBkndData();
|
||||
@@ -39,10 +40,18 @@ export function DataEntityCreate({ params }) {
|
||||
if (!changeSet) return;
|
||||
|
||||
try {
|
||||
await $q.create(changeSet);
|
||||
const result = await $q.create(changeSet);
|
||||
if (error) setError(null);
|
||||
// @todo: navigate to created?
|
||||
goBack();
|
||||
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();
|
||||
}
|
||||
} catch (e) {
|
||||
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.Scrollable key={entity.name}>
|
||||
{error && (
|
||||
|
||||
@@ -169,6 +169,8 @@ function EntityCreateButton({ entity }: { entity: Entity }) {
|
||||
media: b.app.config.media.entity_name,
|
||||
};
|
||||
if (system.users === entity.name) {
|
||||
if (b.readonly) return null;
|
||||
|
||||
return (
|
||||
<Button onClick={createUserModal.open} variant="primary">
|
||||
New User
|
||||
|
||||
@@ -40,6 +40,7 @@ const formConfig = {
|
||||
|
||||
function MediaSettingsInternal() {
|
||||
const { config, schema: _schema, actions } = useBkndMedia();
|
||||
const { readonly } = useBknd();
|
||||
const schema = JSON.parse(JSON.stringify(_schema));
|
||||
|
||||
schema.if = { properties: { enabled: { const: true } } };
|
||||
@@ -53,7 +54,13 @@ function MediaSettingsInternal() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form schema={schema} initialValues={config as any} onSubmit={onSubmit} {...formConfig}>
|
||||
<Form
|
||||
schema={schema}
|
||||
initialValues={config as any}
|
||||
onSubmit={onSubmit}
|
||||
{...formConfig}
|
||||
readOnly={readonly}
|
||||
>
|
||||
<Subscribe
|
||||
selector={(state) => ({
|
||||
dirty: state.dirty,
|
||||
@@ -64,13 +71,15 @@ function MediaSettingsInternal() {
|
||||
{({ dirty, errors, submitting }) => (
|
||||
<AppShell.SectionHeader
|
||||
right={
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={!dirty || errors || submitting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
!readonly && (
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={!dirty || errors || submitting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
Settings
|
||||
@@ -132,6 +141,7 @@ const AdapterIcon = ({ type }: { type: string }) => {
|
||||
|
||||
function Adapters() {
|
||||
const ctx = AnyOf.useContext();
|
||||
const { readonly } = useBknd();
|
||||
|
||||
return (
|
||||
<Formy.Group>
|
||||
@@ -150,6 +160,7 @@ function Adapters() {
|
||||
"flex flex-row items-center justify-center gap-3 border",
|
||||
ctx.selected === i && "border-primary",
|
||||
)}
|
||||
disabled={readonly}
|
||||
>
|
||||
<div>
|
||||
<AdapterIcon type={schema.properties.type.const} />
|
||||
|
||||
@@ -107,8 +107,8 @@ export function Setting<Schema extends s.ObjectSchema = s.ObjectSchema>({
|
||||
return;
|
||||
});
|
||||
|
||||
const deleteAllowed = options?.allowDelete?.(config) ?? true;
|
||||
const editAllowed = options?.allowEdit?.(config) ?? true;
|
||||
const deleteAllowed = (options?.allowDelete?.(config) ?? true) && !readonly;
|
||||
const editAllowed = (options?.allowEdit?.(config) ?? true) && !readonly;
|
||||
const showAlert = options?.showAlert?.(config) ?? undefined;
|
||||
|
||||
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];
|
||||
|
||||
const onToggleEdit = useEvent(() => {
|
||||
if (!editAllowed || readonly) return;
|
||||
if (!editAllowed) return;
|
||||
|
||||
setEditing((prev) => !prev);
|
||||
//formRef.current?.cancel();
|
||||
});
|
||||
|
||||
const onSave = useEvent(async () => {
|
||||
if (!editAllowed || !editing || readonly) return;
|
||||
if (!editAllowed || !editing) return;
|
||||
|
||||
if (formRef.current?.validateForm()) {
|
||||
setSubmitting(true);
|
||||
@@ -215,14 +215,14 @@ export function Setting<Schema extends s.ObjectSchema = s.ObjectSchema>({
|
||||
>
|
||||
<IconButton Icon={TbSettings} />
|
||||
</Dropdown>
|
||||
<Button onClick={onToggleEdit} disabled={!editAllowed || readonly}>
|
||||
<Button onClick={onToggleEdit} disabled={!editAllowed}>
|
||||
{editing ? "Cancel" : "Edit"}
|
||||
</Button>
|
||||
{editing && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSave}
|
||||
disabled={submitting || !editAllowed || readonly}
|
||||
disabled={submitting || !editAllowed}
|
||||
>
|
||||
{submitting ? "Save..." : "Save"}
|
||||
</Button>
|
||||
|
||||
@@ -30,7 +30,7 @@ export const SettingNewModal = ({
|
||||
const [location, navigate] = useLocation();
|
||||
const [formSchema, setFormSchema] = useState(schema);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { actions } = useBknd();
|
||||
const { actions, readonly } = useBknd();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const isGeneratedKey = generateKey !== undefined;
|
||||
const isStaticGeneratedKey = typeof generateKey === "string";
|
||||
@@ -98,15 +98,17 @@ export const SettingNewModal = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row">
|
||||
{isAnyOf ? (
|
||||
<Dropdown position="top-start" items={anyOfItems} itemsClassName="gap-3">
|
||||
<Button>Add new</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button onClick={open}>Add new</Button>
|
||||
)}
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className="flex flex-row">
|
||||
{isAnyOf ? (
|
||||
<Dropdown position="top-start" items={anyOfItems} itemsClassName="gap-3">
|
||||
<Button>Add new</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button onClick={open}>Add new</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
open={opened}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const DataSettings = ({
|
||||
schema,
|
||||
config,
|
||||
}: { schema: ModuleSchemas["data"]; config: ModuleConfigs["data"] }) => {
|
||||
const { app } = useBknd();
|
||||
const { app, readonly } = useBknd();
|
||||
const prefix = app.getAbsolutePath("settings");
|
||||
const entities = Object.keys(config.entities ?? {});
|
||||
|
||||
@@ -105,7 +105,7 @@ export const DataSettings = ({
|
||||
options={{
|
||||
showAlert: (config: any) => {
|
||||
// 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;
|
||||
@@ -137,7 +137,7 @@ export const DataSettings = ({
|
||||
config={config.entities?.[entity] as any}
|
||||
options={{
|
||||
showAlert: (config: any) => {
|
||||
if (config.type === "system") {
|
||||
if (config.type === "system" && !readonly) {
|
||||
return "Modifying the system entities may result in strange behaviors.";
|
||||
}
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user