[CLAUDE] AwV2: Mig 25 +IsUserSelectable + Designer pin toggle + Workspace filter, bỏ "(clone)"
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Hai yêu cầu UAT 2026-05-08:
1. Bỏ "(clone)" auto-append khi clone version mới — version đã đủ phân biệt.
2. Thêm pin toggle để admin chọn workflows nào cho user pick lúc tạo phiếu.
Migration 25 AddIsUserSelectableToApprovalWorkflows:
- ALTER ApprovalWorkflows ADD IsUserSelectable bit NOT NULL DEFAULT 0
- UPDATE backfill SET IsUserSelectable=1 WHERE IsActive=1 (giữ behavior cũ
cho active versions, archived = false default — admin tự pin nếu cần)
BE:
- Domain ApprovalWorkflow +property IsUserSelectable
- DTO AwDefinitionDto +field
- CreateAwDefinitionCommandHandler set default true cho version mới
- New SetAwUserSelectableCommand + Handler
- API PATCH /api/approval-workflows-v2/{id}/user-selectable (Workflows.Create policy)
- DbInitializer SeedSampleApprovalWorkflowsV2Async set IsUserSelectable=true
FE Designer (fe-admin):
- DefinitionDto +isUserSelectable
- Badge amber "Pin Cho user chọn" khi true (cạnh Đang áp dụng/Archived)
- Button "Pin/PinOff Ghim cho user / Bỏ ghim" trong action group + mutation toggle
- Auto-fill name khi clone: bỏ "(clone)" suffix → giữ nguyên name
FE Workspace (fe-admin + fe-user):
- approvalWorkflows query filter w.isUserSelectable === true
- User dropdown chỉ thấy workflows admin đã pin
Verify: dotnet build pass · 81 test pass · npm build × 2 pass · Mig 25 apply LocalDB OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
BIN
.claude.zip
Normal file
BIN
.claude.zip
Normal file
Binary file not shown.
@ -73,17 +73,16 @@ export function PeWorkspaceCreateView({
|
|||||||
|
|
||||||
// Mig 23 — fetch list quy trình duyệt V2 cho User chọn (filter theo
|
// Mig 23 — fetch list quy trình duyệt V2 cho User chọn (filter theo
|
||||||
// ApplicableType khớp với defaultType: 1=DuyetNcc / 2=DuyetNccPhuongAn).
|
// ApplicableType khớp với defaultType: 1=DuyetNcc / 2=DuyetNccPhuongAn).
|
||||||
|
// Mig 25 — chỉ hiện workflows admin đã ghim "cho user chọn" (IsUserSelectable=true).
|
||||||
const approvalWorkflows = useQuery({
|
const approvalWorkflows = useQuery({
|
||||||
queryKey: ['approval-workflows-v2-active', defaultType],
|
queryKey: ['approval-workflows-v2-active', defaultType],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean }[] }[] }>(
|
const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean; isUserSelectable: boolean }[] }[] }>(
|
||||||
'/approval-workflows-v2',
|
'/approval-workflows-v2',
|
||||||
{ params: { applicableType: defaultType } },
|
{ params: { applicableType: defaultType } },
|
||||||
)
|
)
|
||||||
// Trả về tất cả version (active + archived) cho User pick — UAT cần
|
|
||||||
// flexibility chọn cả version cũ test compare.
|
|
||||||
const typeBucket = res.data.types.find(t => t.applicableType === defaultType)
|
const typeBucket = res.data.types.find(t => t.applicableType === defaultType)
|
||||||
return typeBucket?.history ?? []
|
return (typeBucket?.history ?? []).filter(w => w.isUserSelectable)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
import { useMemo, useState, type FormEvent } from 'react'
|
import { useMemo, useState, type FormEvent } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { GitBranch, Plus, Trash2, CheckCircle2, History, Workflow, ChevronUp, ChevronDown } from 'lucide-react'
|
import { GitBranch, Plus, Trash2, CheckCircle2, History, Workflow, ChevronUp, ChevronDown, Pin, PinOff } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
@ -59,6 +59,7 @@ type DefinitionDto = {
|
|||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
isUserSelectable: boolean // Mig 25 — admin toggle cho user pick
|
||||||
activatedAt: string | null
|
activatedAt: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
steps: StepDto[]
|
steps: StepDto[]
|
||||||
@ -214,6 +215,19 @@ function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => voi
|
|||||||
onError: err => toast.error(getErrorMessage(err)),
|
onError: err => toast.error(getErrorMessage(err)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Mig 25 — toggle "cho user pick lúc create phiếu" (stick/unstick)
|
||||||
|
const toggleSelectable = useMutation({
|
||||||
|
mutationFn: async ({ id, isUserSelectable }: { id: string; isUserSelectable: boolean }) =>
|
||||||
|
api.patch(`/approval-workflows-v2/${id}/user-selectable`, { isUserSelectable }),
|
||||||
|
onSuccess: (_data, vars) => {
|
||||||
|
toast.success(vars.isUserSelectable
|
||||||
|
? 'Đã ghim — user có thể chọn quy trình này'
|
||||||
|
: 'Đã bỏ ghim — user không thấy quy trình này')
|
||||||
|
qc.invalidateQueries({ queryKey: ['approval-workflow-v2-overview'] })
|
||||||
|
},
|
||||||
|
onError: err => toast.error(getErrorMessage(err)),
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{type.active ? (
|
{type.active ? (
|
||||||
@ -221,6 +235,10 @@ function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => voi
|
|||||||
def={type.active}
|
def={type.active}
|
||||||
isActive
|
isActive
|
||||||
onClone={d => { setCloneFrom(d); setDesignerOpen(true) }}
|
onClone={d => { setCloneFrom(d); setDesignerOpen(true) }}
|
||||||
|
onToggleSelectable={() => toggleSelectable.mutate({
|
||||||
|
id: type.active!.id,
|
||||||
|
isUserSelectable: !type.active!.isUserSelectable,
|
||||||
|
})}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
if (confirm(`Xoá version đang áp dụng "${type.active!.code} v${type.active!.version}"?`)) {
|
if (confirm(`Xoá version đang áp dụng "${type.active!.code} v${type.active!.version}"?`)) {
|
||||||
del.mutate(type.active!.id)
|
del.mutate(type.active!.id)
|
||||||
@ -254,6 +272,10 @@ function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => voi
|
|||||||
def={d}
|
def={d}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
onClone={dd => { setCloneFrom(dd); setDesignerOpen(true) }}
|
onClone={dd => { setCloneFrom(dd); setDesignerOpen(true) }}
|
||||||
|
onToggleSelectable={() => toggleSelectable.mutate({
|
||||||
|
id: d.id,
|
||||||
|
isUserSelectable: !d.isUserSelectable,
|
||||||
|
})}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
if (confirm(`Xoá version "${d.code} v${d.version}"?`)) del.mutate(d.id)
|
if (confirm(`Xoá version "${d.code} v${d.version}"?`)) del.mutate(d.id)
|
||||||
}}
|
}}
|
||||||
@ -280,11 +302,13 @@ function DefinitionCard({
|
|||||||
def,
|
def,
|
||||||
isActive,
|
isActive,
|
||||||
onClone,
|
onClone,
|
||||||
|
onToggleSelectable,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
def: DefinitionDto
|
def: DefinitionDto
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
onClone: (d: DefinitionDto) => void
|
onClone: (d: DefinitionDto) => void
|
||||||
|
onToggleSelectable: () => void
|
||||||
onDelete: () => void
|
onDelete: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -307,6 +331,13 @@ function DefinitionCard({
|
|||||||
Archived
|
Archived
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{/* Mig 25 — badge IsUserSelectable: ghim cho user pick */}
|
||||||
|
{def.isUserSelectable && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium text-amber-700" title="User có thể chọn quy trình này khi tạo phiếu">
|
||||||
|
<Pin className="h-3 w-3" />
|
||||||
|
Cho user chọn
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{def.description && <p className="mt-1 text-xs leading-relaxed text-slate-500">{def.description}</p>}
|
{def.description && <p className="mt-1 text-xs leading-relaxed text-slate-500">{def.description}</p>}
|
||||||
|
|
||||||
@ -368,6 +399,16 @@ function DefinitionCard({
|
|||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
Tạo từ bản này
|
Tạo từ bản này
|
||||||
</Button>
|
</Button>
|
||||||
|
{/* Mig 25 — toggle stick: cho user chọn quy trình này khi tạo phiếu */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onToggleSelectable}
|
||||||
|
title={def.isUserSelectable ? 'Bỏ ghim — user sẽ không thấy quy trình này' : 'Ghim — user có thể chọn quy trình này'}
|
||||||
|
>
|
||||||
|
{def.isUserSelectable ? <PinOff className="h-3.5 w-3.5" /> : <Pin className="h-3.5 w-3.5" />}
|
||||||
|
{def.isUserSelectable ? 'Bỏ ghim' : 'Ghim cho user'}
|
||||||
|
</Button>
|
||||||
<Button variant="outline" size="sm" onClick={onDelete}>
|
<Button variant="outline" size="sm" onClick={onDelete}>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
Xoá version
|
Xoá version
|
||||||
@ -400,7 +441,7 @@ function Designer({
|
|||||||
|
|
||||||
const defaultCode = DEFAULT_CODE_BY_TYPE[applicableType] ?? 'QT-V2-001'
|
const defaultCode = DEFAULT_CODE_BY_TYPE[applicableType] ?? 'QT-V2-001'
|
||||||
const [code, setCode] = useState(cloneFrom?.code ?? defaultCode)
|
const [code, setCode] = useState(cloneFrom?.code ?? defaultCode)
|
||||||
const [name, setName] = useState(cloneFrom ? `${cloneFrom.name} (clone)` : `Quy trình ${applicableTypeLabel}`)
|
const [name, setName] = useState(cloneFrom ? cloneFrom.name : `Quy trình ${applicableTypeLabel}`)
|
||||||
const [description, setDescription] = useState(cloneFrom?.description ?? '')
|
const [description, setDescription] = useState(cloneFrom?.description ?? '')
|
||||||
const [steps, setSteps] = useState<EditStep[]>(initialSteps)
|
const [steps, setSteps] = useState<EditStep[]>(initialSteps)
|
||||||
|
|
||||||
|
|||||||
@ -72,15 +72,16 @@ export function PeWorkspaceCreateView({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Mig 23 — fetch list quy trình duyệt V2 (filter ApplicableType khớp defaultType).
|
// Mig 23 — fetch list quy trình duyệt V2 (filter ApplicableType khớp defaultType).
|
||||||
|
// Mig 25 — chỉ hiện workflows admin đã ghim "cho user chọn" (IsUserSelectable=true).
|
||||||
const approvalWorkflows = useQuery({
|
const approvalWorkflows = useQuery({
|
||||||
queryKey: ['approval-workflows-v2-active', defaultType],
|
queryKey: ['approval-workflows-v2-active', defaultType],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean }[] }[] }>(
|
const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean; isUserSelectable: boolean }[] }[] }>(
|
||||||
'/approval-workflows-v2',
|
'/approval-workflows-v2',
|
||||||
{ params: { applicableType: defaultType } },
|
{ params: { applicableType: defaultType } },
|
||||||
)
|
)
|
||||||
const typeBucket = res.data.types.find(t => t.applicableType === defaultType)
|
const typeBucket = res.data.types.find(t => t.applicableType === defaultType)
|
||||||
return typeBucket?.history ?? []
|
return (typeBucket?.history ?? []).filter(w => w.isUserSelectable)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,17 @@ public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase
|
|||||||
return Ok(new { id });
|
return Ok(new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record SetUserSelectableBody(bool IsUserSelectable);
|
||||||
|
|
||||||
|
// Mig 25 — admin toggle stick/unstick "cho user pick lúc create phiếu".
|
||||||
|
[HttpPatch("{id:guid}/user-selectable")]
|
||||||
|
[Authorize(Policy = "Workflows.Create")]
|
||||||
|
public async Task<IActionResult> SetUserSelectable(Guid id, [FromBody] SetUserSelectableBody body, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await mediator.Send(new SetAwUserSelectableCommand(id, body.IsUserSelectable), ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Policy = "Workflows.Create")]
|
[Authorize(Policy = "Workflows.Create")]
|
||||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
|
|||||||
@ -45,6 +45,7 @@ public record AwDefinitionDto(
|
|||||||
string Name,
|
string Name,
|
||||||
string? Description,
|
string? Description,
|
||||||
bool IsActive,
|
bool IsActive,
|
||||||
|
bool IsUserSelectable,
|
||||||
DateTime? ActivatedAt,
|
DateTime? ActivatedAt,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
List<AwStepDto> Steps);
|
List<AwStepDto> Steps);
|
||||||
@ -126,6 +127,7 @@ public class GetAwAdminOverviewQueryHandler(
|
|||||||
d.Name,
|
d.Name,
|
||||||
d.Description,
|
d.Description,
|
||||||
d.IsActive,
|
d.IsActive,
|
||||||
|
d.IsUserSelectable,
|
||||||
d.ActivatedAt,
|
d.ActivatedAt,
|
||||||
d.CreatedAt,
|
d.CreatedAt,
|
||||||
d.Steps.OrderBy(s => s.Order).Select(s => new AwStepDto(
|
d.Steps.OrderBy(s => s.Order).Select(s => new AwStepDto(
|
||||||
@ -268,6 +270,7 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
|||||||
Name = request.Name,
|
Name = request.Name,
|
||||||
Description = request.Description,
|
Description = request.Description,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
IsUserSelectable = true, // Mig 25 — version mới mặc định cho user pick
|
||||||
ActivatedAt = DateTime.UtcNow,
|
ActivatedAt = DateTime.UtcNow,
|
||||||
Steps = request.Steps.OrderBy(s => s.Order)
|
Steps = request.Steps.OrderBy(s => s.Order)
|
||||||
.Select(s => new ApprovalWorkflowStep
|
.Select(s => new ApprovalWorkflowStep
|
||||||
@ -291,6 +294,27 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== PATCH user-selectable toggle (Mig 25 — admin pin/unpin cho user pick) ==========
|
||||||
|
// Independent với IsActive: cho phép multiple version cùng selectable. Default
|
||||||
|
// version mới IsUserSelectable=true (mirror IsActive default), admin có thể
|
||||||
|
// unstick để dấu khỏi Workspace dropdown, hoặc stick lại version cũ archived.
|
||||||
|
|
||||||
|
public record SetAwUserSelectableCommand(Guid Id, bool IsUserSelectable) : IRequest;
|
||||||
|
|
||||||
|
public class SetAwUserSelectableCommandHandler(IApplicationDbContext db)
|
||||||
|
: IRequestHandler<SetAwUserSelectableCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(SetAwUserSelectableCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var def = await db.ApprovalWorkflows
|
||||||
|
.FirstOrDefaultAsync(d => d.Id == request.Id, ct)
|
||||||
|
?? throw new KeyNotFoundException($"ApprovalWorkflow {request.Id} không tồn tại.");
|
||||||
|
|
||||||
|
def.IsUserSelectable = request.IsUserSelectable;
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== DELETE version (chỉ khi chưa có phiếu pin) ==========
|
// ========== DELETE version (chỉ khi chưa có phiếu pin) ==========
|
||||||
// Hiện chưa có phiếu nào pin schema mới → unconditional delete OK cho UAT.
|
// Hiện chưa có phiếu nào pin schema mới → unconditional delete OK cho UAT.
|
||||||
// Sau UAT khi link với PE/Contract thật cần check usage trước khi delete.
|
// Sau UAT khi link với PE/Contract thật cần check usage trước khi delete.
|
||||||
|
|||||||
@ -28,6 +28,12 @@ public class ApprovalWorkflow : BaseEntity
|
|||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
public DateTime? ActivatedAt { get; set; }
|
public DateTime? ActivatedAt { get; set; }
|
||||||
|
|
||||||
|
// Mig 25 — admin toggle "cho user pick lúc create phiếu". Workspace dropdown
|
||||||
|
// chỉ hiện workflow IsUserSelectable=true. Independent với IsActive: cho phép
|
||||||
|
// multiple version cùng selectable (vd v02+v03 cùng pickable). Default true
|
||||||
|
// khi tạo version mới (mirror IsActive default), admin có thể unstick.
|
||||||
|
public bool IsUserSelectable { get; set; }
|
||||||
|
|
||||||
public List<ApprovalWorkflowStep> Steps { get; set; } = new();
|
public List<ApprovalWorkflowStep> Steps { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -115,6 +115,7 @@ public static class DbInitializer
|
|||||||
Name = "Quy trình duyệt NCC và Giải pháp (mẫu UAT)",
|
Name = "Quy trình duyệt NCC và Giải pháp (mẫu UAT)",
|
||||||
Description = "Sample seed cho UAT B — 1 Bước Phòng CCM × 1 Cấp NV test. Admin có thể clone tạo version mới qua Designer.",
|
Description = "Sample seed cho UAT B — 1 Bước Phòng CCM × 1 Cấp NV test. Admin có thể clone tạo version mới qua Designer.",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
IsUserSelectable = true, // Mig 25 — sample mặc định cho user pick
|
||||||
ActivatedAt = DateTime.UtcNow,
|
ActivatedAt = DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
var step = new ApprovalWorkflowStep
|
var step = new ApprovalWorkflowStep
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddIsUserSelectableToApprovalWorkflows : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsUserSelectable",
|
||||||
|
table: "ApprovalWorkflows",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
// Backfill: workflow đang active = auto user-selectable (giữ behavior
|
||||||
|
// hiện tại — Workspace dropdown vẫn hiện active workflow). Archived
|
||||||
|
// versions default false, admin tự toggle khi muốn user pick.
|
||||||
|
migrationBuilder.Sql("UPDATE ApprovalWorkflows SET IsUserSelectable = 1 WHERE IsActive = 1;");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsUserSelectable",
|
||||||
|
table: "ApprovalWorkflows");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -155,6 +155,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<bool>("IsActive")
|
b.Property<bool>("IsActive")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUserSelectable")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
|
|||||||
Reference in New Issue
Block a user