mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
Merge branch 'main' into next
This commit is contained in:
commit
c4b6a004e6
@ -32,8 +32,8 @@
|
|||||||
"@nocobase/utils": "1.4.0-alpha",
|
"@nocobase/utils": "1.4.0-alpha",
|
||||||
"ahooks": "^3.7.2",
|
"ahooks": "^3.7.2",
|
||||||
"antd": "5.12.8",
|
"antd": "5.12.8",
|
||||||
"antd-style": "3.4.5",
|
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.7.0",
|
||||||
|
"antd-style": "3.7.1",
|
||||||
"bignumber.js": "^9.1.2",
|
"bignumber.js": "^9.1.2",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"cronstrue": "^2.11.0",
|
"cronstrue": "^2.11.0",
|
||||||
|
@ -7,11 +7,18 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useForm } from '@formily/react';
|
|
||||||
import { useCollectionRecordData } from '../../../../../data-source/collection-record/CollectionRecordProvider';
|
import { useCollectionRecordData } from '../../../../../data-source/collection-record/CollectionRecordProvider';
|
||||||
import { useSatisfiedActionValues } from '../../../../../schema-settings/LinkageRules/useActionValues';
|
import { useSatisfiedActionValues } from '../../../../../schema-settings/LinkageRules/useActionValues';
|
||||||
|
import { useFormBlockContext } from '../../../../../block-provider';
|
||||||
|
import { useSubFormValue } from '../../../../../schema-component/antd/association-field/hooks';
|
||||||
export function useDataFormItemProps() {
|
export function useDataFormItemProps() {
|
||||||
const data = useCollectionRecordData();
|
const record = useCollectionRecordData();
|
||||||
const { valueMap: style } = useSatisfiedActionValues({ category: 'style', formValues: data });
|
const { form } = useFormBlockContext();
|
||||||
|
const subForm = useSubFormValue();
|
||||||
|
const { valueMap: style } = useSatisfiedActionValues({
|
||||||
|
category: 'style',
|
||||||
|
formValues: subForm?.formValue || form?.values || record,
|
||||||
|
form,
|
||||||
|
});
|
||||||
return { wrapperStyle: style };
|
return { wrapperStyle: style };
|
||||||
}
|
}
|
||||||
|
@ -936,7 +936,7 @@ test.describe('actions schema settings', () => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('open mode', async ({ page, mockPage }) => {
|
test.skip('open mode', async ({ page, mockPage }) => {
|
||||||
const nocoPage = await mockPage(testingOfOpenModeForAddChild).waitForInit();
|
const nocoPage = await mockPage(testingOfOpenModeForAddChild).waitForInit();
|
||||||
await nocoPage.goto();
|
await nocoPage.goto();
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ describe('CollectionSelect', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-wwtqkl ant-app"
|
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -54,7 +54,7 @@ describe('CollectionSelect', () => {
|
|||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-wwtqkl"
|
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-formily-item-label"
|
class="ant-formily-item-label"
|
||||||
@ -84,7 +84,7 @@ describe('CollectionSelect', () => {
|
|||||||
class="ant-formily-item-control-content-component"
|
class="ant-formily-item-control-content-component"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-select css-dev-only-do-not-override-wwtqkl ant-select-focused ant-select-single ant-select-show-arrow ant-select-show-search"
|
class="ant-select css-dev-only-do-not-override-11aiz3o ant-select-focused ant-select-single ant-select-show-arrow ant-select-show-search"
|
||||||
data-testid="select-collection"
|
data-testid="select-collection"
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
@ -182,7 +182,7 @@ describe('CollectionSelect', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-wwtqkl ant-app"
|
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -191,7 +191,7 @@ describe('CollectionSelect', () => {
|
|||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-wwtqkl"
|
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-formily-item-label"
|
class="ant-formily-item-label"
|
||||||
@ -222,7 +222,7 @@ describe('CollectionSelect', () => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
class="ant-tag css-dev-only-do-not-override-wwtqkl"
|
class="ant-tag css-dev-only-do-not-override-11aiz3o"
|
||||||
>
|
>
|
||||||
Users
|
Users
|
||||||
</span>
|
</span>
|
||||||
|
@ -26,7 +26,7 @@ describe('ColorPicker', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-wwtqkl ant-app"
|
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -35,7 +35,7 @@ describe('ColorPicker', () => {
|
|||||||
style="display: inline-block;"
|
style="display: inline-block;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-color-picker-trigger css-dev-only-do-not-override-wwtqkl"
|
class="ant-color-picker-trigger css-dev-only-do-not-override-11aiz3o"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-color-picker-color-block"
|
class="ant-color-picker-color-block"
|
||||||
@ -90,7 +90,7 @@ describe('ColorPicker', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-wwtqkl ant-app"
|
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -99,7 +99,7 @@ describe('ColorPicker', () => {
|
|||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-color-picker-trigger ant-color-picker-sm css-dev-only-do-not-override-wwtqkl ant-color-picker-trigger-disabled"
|
class="ant-color-picker-trigger ant-color-picker-sm css-dev-only-do-not-override-11aiz3o ant-color-picker-trigger-disabled"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-color-picker-color-block"
|
class="ant-color-picker-color-block"
|
||||||
|
@ -21,12 +21,12 @@ describe('Pagination', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-wwtqkl ant-app"
|
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<ul
|
<ul
|
||||||
class="ant-pagination css-dev-only-do-not-override-wwtqkl"
|
class="ant-pagination css-dev-only-do-not-override-11aiz3o"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
@ -131,7 +131,7 @@ describe('Pagination', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-wwtqkl ant-app"
|
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,11 +20,11 @@ describe('UnixTimestamp', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-wwtqkl ant-app"
|
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-picker css-dev-only-do-not-override-wwtqkl"
|
class="ant-picker css-dev-only-do-not-override-11aiz3o"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-picker-input"
|
class="ant-picker-input"
|
||||||
@ -77,7 +77,7 @@ describe('UnixTimestamp', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-wwtqkl ant-app"
|
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { Form, onFormValuesChange } from '@formily/core';
|
||||||
import { useVariables, useLocalVariables } from '../../variables';
|
import { useVariables, useLocalVariables } from '../../variables';
|
||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type';
|
import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type';
|
||||||
@ -18,11 +20,13 @@ export function useSatisfiedActionValues({
|
|||||||
category = 'default',
|
category = 'default',
|
||||||
rules,
|
rules,
|
||||||
schema,
|
schema,
|
||||||
|
form,
|
||||||
}: {
|
}: {
|
||||||
category: `${LinkageRuleCategory}`;
|
category: `${LinkageRuleCategory}`;
|
||||||
formValues: Record<string, any>;
|
formValues: Record<string, any>;
|
||||||
rules?: any;
|
rules?: any;
|
||||||
schema?: any;
|
schema?: any;
|
||||||
|
form?: Form;
|
||||||
}) {
|
}) {
|
||||||
const [valueMap, setValueMap] = useState({});
|
const [valueMap, setValueMap] = useState({});
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
@ -30,8 +34,7 @@ export function useSatisfiedActionValues({
|
|||||||
const localVariables = useLocalVariables({ currentForm: { values: formValues } as any });
|
const localVariables = useLocalVariables({ currentForm: { values: formValues } as any });
|
||||||
const localSchema = schema ?? fieldSchema;
|
const localSchema = schema ?? fieldSchema;
|
||||||
const linkageRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]];
|
const linkageRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]];
|
||||||
|
const compute = useCallback(() => {
|
||||||
useEffect(() => {
|
|
||||||
if (linkageRules && formValues) {
|
if (linkageRules && formValues) {
|
||||||
getSatisfiedValueMap({ rules: linkageRules, variables, localVariables })
|
getSatisfiedValueMap({ rules: linkageRules, variables, localVariables })
|
||||||
.then((valueMap) => {
|
.then((valueMap) => {
|
||||||
@ -43,6 +46,22 @@ export function useSatisfiedActionValues({
|
|||||||
throw new Error(err.message);
|
throw new Error(err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [variables, localVariables, formValues, linkageRules]);
|
}, [variables, localVariables, linkageRules, formValues]);
|
||||||
|
useEffect(() => {
|
||||||
|
compute();
|
||||||
|
}, [compute]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (form) {
|
||||||
|
const id = uid();
|
||||||
|
form.addEffects(id, () => {
|
||||||
|
onFormValuesChange(() => {
|
||||||
|
compute();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
form.removeEffects(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [form, compute]);
|
||||||
return { valueMap };
|
return { valueMap };
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"@formily/react": "2.x",
|
"@formily/react": "2.x",
|
||||||
"@formily/shared": "2.x",
|
"@formily/shared": "2.x",
|
||||||
"antd": "5.x",
|
"antd": "5.x",
|
||||||
"antd-style": "3.4.5",
|
"antd-style": "3.7.1",
|
||||||
"cron-parser": "4.4.0",
|
"cron-parser": "4.4.0",
|
||||||
"dayjs": "^1.11.8",
|
"dayjs": "^1.11.8",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
@ -31,7 +31,6 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
if (!condition(collection.options)) {
|
if (!condition(collection.options)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tk = collection.filterTargetKey as string;
|
|
||||||
const name = `${dataSource.name}_${collection.name}_path`;
|
const name = `${dataSource.name}_${collection.name}_path`;
|
||||||
const parentForeignKey = collection.treeParentField?.foreignKey || 'parentId';
|
const parentForeignKey = collection.treeParentField?.foreignKey || 'parentId';
|
||||||
|
|
||||||
@ -51,8 +50,9 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
//afterCreate
|
//afterCreate
|
||||||
this.db.on(`${collection.name}.afterCreate`, async (model: Model, options) => {
|
this.db.on(`${collection.name}.afterCreate`, async (model: Model, options) => {
|
||||||
const { transaction } = options;
|
const { transaction } = options;
|
||||||
|
const tk = collection.filterTargetKey as string;
|
||||||
let path = `/${model.get(tk)}`;
|
let path = `/${model.get(tk)}`;
|
||||||
path = await this.getTreePath(model, path, collection, tk, name, transaction);
|
path = await this.getTreePath(model, path, collection, name, transaction);
|
||||||
const rootPk = path.split('/')[1];
|
const rootPk = path.split('/')[1];
|
||||||
await this.app.db.getRepository(name).create({
|
await this.app.db.getRepository(name).create({
|
||||||
values: {
|
values: {
|
||||||
@ -66,16 +66,18 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
|
|
||||||
//afterUpdate
|
//afterUpdate
|
||||||
this.db.on(`${collection.name}.afterUpdate`, async (model: Model, options) => {
|
this.db.on(`${collection.name}.afterUpdate`, async (model: Model, options) => {
|
||||||
|
const tk = collection.filterTargetKey;
|
||||||
// only update parentId and filterTargetKey
|
// only update parentId and filterTargetKey
|
||||||
if (!(model._changed.has(tk) || model._changed.has(parentForeignKey))) {
|
if (!(model._changed.has(tk) || model._changed.has(parentForeignKey))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { transaction } = options;
|
const { transaction } = options;
|
||||||
await this.updateTreePath(model, collection, tk, name, transaction);
|
await this.updateTreePath(model, collection, name, transaction);
|
||||||
});
|
});
|
||||||
|
|
||||||
// after remove
|
// after remove
|
||||||
this.db.on(`${collection.name}.afterBulkUpdate`, async (options) => {
|
this.db.on(`${collection.name}.afterBulkUpdate`, async (options) => {
|
||||||
|
const tk = collection.filterTargetKey as string;
|
||||||
if (!(options.where && options.where[tk])) {
|
if (!(options.where && options.where[tk])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -86,12 +88,13 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
transaction: options.transaction,
|
transaction: options.transaction,
|
||||||
});
|
});
|
||||||
for (const model of instances) {
|
for (const model of instances) {
|
||||||
await this.updateTreePath(model, collection, tk, name, options.transaction);
|
await this.updateTreePath(model, collection, name, options.transaction);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//afterDestroy
|
//afterDestroy
|
||||||
this.db.on(`${collection.name}.afterDestroy`, async (model: Model, options: DestroyOptions) => {
|
this.db.on(`${collection.name}.afterDestroy`, async (model: Model, options: DestroyOptions) => {
|
||||||
|
const tk = collection.filterTargetKey as string;
|
||||||
await this.app.db.getRepository(name).destroy({
|
await this.app.db.getRepository(name).destroy({
|
||||||
filter: {
|
filter: {
|
||||||
nodePk: model.get(tk),
|
nodePk: model.get(tk),
|
||||||
@ -139,10 +142,10 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
model: Model,
|
model: Model,
|
||||||
path: string,
|
path: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
tk: string,
|
|
||||||
pathCollectionName: string,
|
pathCollectionName: string,
|
||||||
transaction?: Transaction,
|
transaction?: Transaction,
|
||||||
) {
|
) {
|
||||||
|
const tk = collection.filterTargetKey as string;
|
||||||
const parentForeignKey = collection.treeParentField?.foreignKey || 'parentId';
|
const parentForeignKey = collection.treeParentField?.foreignKey || 'parentId';
|
||||||
if (model.get(parentForeignKey) && model.get(parentForeignKey) !== null) {
|
if (model.get(parentForeignKey) && model.get(parentForeignKey) !== null) {
|
||||||
const parent = await this.app.db.getRepository(collection.name).findOne({
|
const parent = await this.app.db.getRepository(collection.name).findOne({
|
||||||
@ -164,7 +167,7 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
});
|
});
|
||||||
const parentPath = lodash.get(parentPathData, 'path', null);
|
const parentPath = lodash.get(parentPathData, 'path', null);
|
||||||
if (parentPath == null) {
|
if (parentPath == null) {
|
||||||
path = await this.getTreePath(parent, path, collection, tk, pathCollectionName, transaction);
|
path = await this.getTreePath(parent, path, collection, pathCollectionName, transaction);
|
||||||
} else {
|
} else {
|
||||||
path = `${parentPath}/${model.get(tk)}`;
|
path = `${parentPath}/${model.get(tk)}`;
|
||||||
}
|
}
|
||||||
@ -177,12 +180,12 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
private async updateTreePath(
|
private async updateTreePath(
|
||||||
model: Model,
|
model: Model,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
tk: string,
|
|
||||||
pathCollectionName: string,
|
pathCollectionName: string,
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
) {
|
) {
|
||||||
|
const tk = collection.filterTargetKey as string;
|
||||||
let path = `/${model.get(tk)}`;
|
let path = `/${model.get(tk)}`;
|
||||||
path = await this.getTreePath(model, path, collection, tk, pathCollectionName, transaction);
|
path = await this.getTreePath(model, path, collection, pathCollectionName, transaction);
|
||||||
const collectionTreePath = this.db.getCollection(pathCollectionName);
|
const collectionTreePath = this.db.getCollection(pathCollectionName);
|
||||||
const nodePkColumnName = collectionTreePath.getField('nodePk').columnName();
|
const nodePkColumnName = collectionTreePath.getField('nodePk').columnName();
|
||||||
const pathData = await this.app.db.getRepository(pathCollectionName).findOne({
|
const pathData = await this.app.db.getRepository(pathCollectionName).findOne({
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"homepage": "https://docs.nocobase.com/handbook/block-gantt",
|
"homepage": "https://docs.nocobase.com/handbook/block-gantt",
|
||||||
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/block-gantt",
|
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/block-gantt",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"antd-style": "3.4.5"
|
"antd-style": "3.7.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@nocobase/client": "1.x",
|
"@nocobase/client": "1.x",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user