diff --git a/src/app/components/workflow-diagram/workflow-diagram.component.html b/src/app/components/workflow-diagram/workflow-diagram.component.html index e0dfbfa..158e328 100644 --- a/src/app/components/workflow-diagram/workflow-diagram.component.html +++ b/src/app/components/workflow-diagram/workflow-diagram.component.html @@ -38,7 +38,7 @@ - @if(data.data.chatWorkflowBlockId !== chatWorkflowBlockTypeEnum.SendTextMessage){ + @if(data.data.chatWorkflowBlockId !== chatWorkflowBlockTypeEnum.SendTextMessage && data.data.chatWorkflowBlockId !== chatWorkflowBlockTypeEnum.GetCustomerDetails) {
} @switch (data.data.chatWorkflowBlockId){ @@ -51,6 +51,17 @@ } + @case(chatWorkflowBlockTypeEnum.GetCustomerDetails){ + +
+ @if (data.data.customerBlockFieldInfo.isNameEditorEnabled){ +
+ } + @if (data.data.customerBlockFieldInfo.isPhoneEditorEnabled){ +
+ } +
+ } @case(chatWorkflowBlockTypeEnum.GetPickerInput){ @switch (data.data.chatWorkflowEditorTypeId){ @case (chatWorkflowEditorTypeEnum.Boolean){ diff --git a/src/app/components/workflow-diagram/workflow-diagram.component.ts b/src/app/components/workflow-diagram/workflow-diagram.component.ts index 2331951..7362568 100644 --- a/src/app/components/workflow-diagram/workflow-diagram.component.ts +++ b/src/app/components/workflow-diagram/workflow-diagram.component.ts @@ -4,7 +4,7 @@ import { ComplexHierarchicalTree, ConnectionPointOrigin, ConnectorConstraints, C SelectorConstraints, SelectorModel, SnapSettingsModel, TextModel, UserHandleEventsArgs, UserHandleModel, DataSourceModel, DataBindingService} from '@syncfusion/ej2-angular-diagrams'; import { ChatWorkflowRulesData, FieldDetails, FieldOptionDetail, FieldValidation, MessageDetails, RuleData2 } from '../../models/appModel'; -import { RULE_DATA, RULE_DATA2, RULE_DATA3 } from '../../data/rule-data'; +import { RULE_DATA3 } from '../../data/rule-data'; import { DialogModule } from '@syncfusion/ej2-angular-popups'; import { BeforeOpenCloseMenuEventArgs, DropDownButtonComponent, DropDownButtonModule, ItemModel, OpenCloseMenuEventArgs } from '@syncfusion/ej2-angular-splitbuttons'; import { CommonModule } from '@angular/common'; @@ -293,8 +293,9 @@ export class WorkflowDiagramComponent implements AfterViewInit { this.isParentListItem = false; // The value is reset here, to handle document click case of dropdown // Reset ListView to its initial state before opening if (this.listView) { - this.listView.dataSource = this.listdata; // Reset data - this.listView.refresh(); + while ((this.listView as any).curDSLevel.length > 0) { + this.listView.back(); + } } } @@ -346,5 +347,4 @@ export class WorkflowDiagramComponent implements AfterViewInit { this.diagram.loadDiagram(jsonString); this.fileInput.nativeElement.value = ''; } - } \ No newline at end of file diff --git a/src/app/components/workflow-sidebar/workflow-sidebar.component.html b/src/app/components/workflow-sidebar/workflow-sidebar.component.html index d376206..ebff77f 100644 --- a/src/app/components/workflow-sidebar/workflow-sidebar.component.html +++ b/src/app/components/workflow-sidebar/workflow-sidebar.component.html @@ -3,7 +3,7 @@
{{sidebarHeader}}
-
+
@@ -166,6 +166,13 @@
} + @case(chatWorkflowBlockTypeEnum.GetCustomerDetails){ +
+ + + +
+ } }
diff --git a/src/app/components/workflow-sidebar/workflow-sidebar.component.ts b/src/app/components/workflow-sidebar/workflow-sidebar.component.ts index 170f9a3..f120a10 100644 --- a/src/app/components/workflow-sidebar/workflow-sidebar.component.ts +++ b/src/app/components/workflow-sidebar/workflow-sidebar.component.ts @@ -3,17 +3,17 @@ import { SidebarComponent, SidebarModule } from '@syncfusion/ej2-angular-navigat import { TextFormatEnum, ChatWorkflowEditorTypeEnum, ChatWorkflowBlockTypeEnum } from '../../models/enum'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; -import { FieldDetails, FieldOptionDetail, FieldValidation, MessageDetails, RuleData2 } from '../../models/appModel'; +import { CustomerBlockFieldDetails, FieldDetails, FieldOptionDetail, FieldValidation, MessageDetails, RuleData2 } from '../../models/appModel'; import { NodeModel } from '@syncfusion/ej2-angular-diagrams'; import { DropDownListComponent, DropDownListModule } from '@syncfusion/ej2-angular-dropdowns'; import { DatePickerModule, DateTimePickerModule } from '@syncfusion/ej2-angular-calendars'; -import { ButtonModule, SwitchModule } from '@syncfusion/ej2-angular-buttons'; +import { ButtonModule, CheckBoxModule, SwitchModule } from '@syncfusion/ej2-angular-buttons'; @Component({ selector: 'app-workflow-sidebar', standalone: true, - imports: [SidebarModule, FormsModule, CommonModule, DatePickerModule, DateTimePickerModule, ButtonModule, SwitchModule, DropDownListModule ], + imports: [SidebarModule, FormsModule, CommonModule, DatePickerModule, DateTimePickerModule, ButtonModule, CheckBoxModule, SwitchModule, DropDownListModule ], templateUrl: './workflow-sidebar.component.html', styleUrl: './workflow-sidebar.component.scss' }) @@ -59,6 +59,10 @@ export class WorkflowSidebarComponent { ddlTextFormatFields: Object = { text: 'text', value: 'value' }; public value = 1; + public getEmailInfo: boolean = true; + public getNameInfo: boolean = false; + public getPhoneNumberInfo: boolean = false; + @Input() nodeEditType!: number; @Input() nodeBlockType!: number; @Input() sidebarHeader!: string; @@ -208,27 +212,27 @@ export class WorkflowSidebarComponent { isPrivate: this.checkedIsPrivate, textFormat: this.ddlTextFormat.value as TextFormatEnum } - this.newNodeInfo = this.createNodeInfo(null, this.nodeBlockType, this.selectedWorkFlowId, null, null, messageInfo); + this.newNodeInfo = this.createNodeInfo(null, this.nodeBlockType, this.selectedWorkFlowId, null, null, messageInfo, null); break; } case (this.chatWorkflowBlockTypeEnum.GetPickerInput): { switch (this.nodeEditType) { case (this.chatWorkflowEditorTypeEnum.Boolean): { let fieldInfo = this.createFieldInfo(null); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null, null); break; } case (this.chatWorkflowEditorTypeEnum.Buttons): { let fieldOptionInfo = this.mapOptionsToFieldOptions(); let fieldInfo = this.createFieldInfo(null); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, fieldOptionInfo, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, fieldOptionInfo, null, null); this.newNodeHeight += (this.options.length * 25); break; } case this.chatWorkflowEditorTypeEnum.DropDown: { let fieldOptionInfo = this.mapOptionsToFieldOptions(); let fieldInfo = this.createFieldInfo(null); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, fieldOptionInfo, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, fieldOptionInfo, null, null); this.newNodeHeight += (this.options.length * 25); break; } @@ -236,14 +240,14 @@ export class WorkflowSidebarComponent { let fieldValidationInfo = this.createFieldValidationInfo(this.fieldOptionMinValue.toString(), this.fieldOptionMaxValue.toString(), null); let fieldOptionInfo = this.mapOptionsToFieldOptions(); let fieldInfo = this.createFieldInfo(fieldValidationInfo); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, fieldOptionInfo, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, fieldOptionInfo, null, null); this.newNodeHeight += (this.options.length * 25); break; } case (this.chatWorkflowEditorTypeEnum.List): { let fieldOptionInfo = this.mapOptionsToFieldOptions(); let fieldInfo = this.createFieldInfo(null); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, fieldOptionInfo, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, fieldOptionInfo, null, null); this.newNodeHeight += (this.options.length * 25); break; } @@ -255,13 +259,13 @@ export class WorkflowSidebarComponent { case (this.chatWorkflowEditorTypeEnum.Text): { let fieldValidationInfo = this.createFieldValidationInfo(null, this.fieldOptionMaxValue.toString(), null); let fieldInfo = this.createFieldInfo(fieldValidationInfo); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null, null); break; } case (this.chatWorkflowEditorTypeEnum.TextArea): { let fieldValidationInfo = this.createFieldValidationInfo(null, this.fieldOptionMaxValue.toString(), null); let fieldInfo = this.createFieldInfo(fieldValidationInfo); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null, null); break; } case (this.chatWorkflowEditorTypeEnum.Date): { @@ -272,7 +276,7 @@ export class WorkflowSidebarComponent { let fieldValidationInfo = this.createFieldValidationInfo(minDate, maxDate, null); let fieldInfo = this.createFieldInfo(fieldValidationInfo); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null, null); break; } case (this.chatWorkflowEditorTypeEnum.DateTime): { @@ -282,30 +286,39 @@ export class WorkflowSidebarComponent { let fieldValidationInfo = this.createFieldValidationInfo(minDateTime, maxDateTime, null); let fieldInfo = this.createFieldInfo(fieldValidationInfo); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null, null); break; } case (this.chatWorkflowEditorTypeEnum.Number): { let fieldValidationInfo = this.createFieldValidationInfo(this.fieldOptionMinValue.toString(), this.fieldOptionMaxValue.toString(), null); let fieldInfo = this.createFieldInfo(fieldValidationInfo); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null, null); break; } case (this.chatWorkflowEditorTypeEnum.Decimal): { let fieldValidationInfo = this.createFieldValidationInfo(this.fieldOptionMinValue.toString(), this.fieldOptionMaxValue.toString(), null); let fieldInfo = this.createFieldInfo(fieldValidationInfo); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null, null); break; } case (this.chatWorkflowEditorTypeEnum.Regex): { let fieldValidationInfo = this.createFieldValidationInfo(null, null, this.fieldOptionRegexValue); let fieldInfo = this.createFieldInfo(fieldValidationInfo); - this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null); + this.newNodeInfo = this.createNodeInfo(this.nodeEditType, this.nodeBlockType, this.selectedWorkFlowId, fieldInfo, null, null, null); break; } } break; } + case (this.chatWorkflowBlockTypeEnum.GetCustomerDetails): { + let customerBlockFieldInfo : CustomerBlockFieldDetails = { + isEmailEditorEnabled: this.getEmailInfo, + isNameEditorEnabled: this.getNameInfo, + isPhoneEditorEnabled: this.getPhoneNumberInfo, + } + this.newNodeInfo = this.createNodeInfo(null, this.nodeBlockType, this.selectedWorkFlowId, null, null, null, customerBlockFieldInfo); + break; + } } if (this.isEdit) { this.updateNode.emit([sourceNodeId, this.newNodeInfo]); @@ -316,7 +329,7 @@ export class WorkflowSidebarComponent { } } - public createNodeInfo(editorTypeId: number | null, blockId: number, workflowId: number, fieldInfo: FieldDetails | null, fieldOptionInfo: FieldOptionDetail[] | null, messageInfo: MessageDetails | null): RuleData2 { + public createNodeInfo(editorTypeId: number | null, blockId: number, workflowId: number, fieldInfo: FieldDetails | null, fieldOptionInfo: FieldOptionDetail[] | null, messageInfo: MessageDetails | null, customerBlockFieldInfo: CustomerBlockFieldDetails | null): RuleData2 { let ruleDataId = this.isEdit ? 0 : WorkflowSidebarComponent.nodeLength++; // Need to set value dynamically from db return { id: ruleDataId, @@ -329,7 +342,8 @@ export class WorkflowSidebarComponent { fieldDetails: fieldInfo, branchDetails: null, messageDetails: messageInfo, - fieldOptionDetails: fieldOptionInfo + fieldOptionDetails: fieldOptionInfo, + customerBlockFieldInfo: customerBlockFieldInfo }; } diff --git a/src/app/data/list-data.ts b/src/app/data/list-data.ts index 24a3a77..226a7e0 100644 --- a/src/app/data/list-data.ts +++ b/src/app/data/list-data.ts @@ -3,38 +3,13 @@ export const LIST_DATA: { [key: string]: any }[] = [ 'text': 'Action', 'id': '01', 'category': 'Block Type', - 'child': [{ - 'text': 'Identity', + 'child': [ + { + 'text': 'Customer Block', 'id': '1', + 'blockid': 3, + 'editerTypeId': null, 'category': 'Action', - 'child': [{ - 'text': 'Name', - 'id': '1001', - 'blockid': 3, - 'editerTypeId': 18, - 'category': 'Identity', - }, - { - 'text': 'Email', - 'id': '1002', - 'blockid': 3, - 'editerTypeId': 19, - 'category': 'Identity', - }, - { - 'text': 'Phone No', - 'id': '1003', - 'blockid': 3, - 'editerTypeId': 20, - 'category': 'Identity', - }, - { - 'text': 'Requester Linking', - 'id': '1004', - 'blockid': 3, - 'editerTypeId': 21, - 'category': 'Identity', - }] }, { 'text': 'Custom Message', diff --git a/src/app/data/rule-data.ts b/src/app/data/rule-data.ts index 0c97f69..0efbe09 100644 --- a/src/app/data/rule-data.ts +++ b/src/app/data/rule-data.ts @@ -1,155 +1,4 @@ -import { RuleData, RuleData2 } from "../models/appModel"; - - -export const RULE_DATA: RuleData[] = [ - { - // identity input request rule - "id" : 1, - "workflow_id" : 1, - "expression" : "Utils.IsValidEmail(input)", - "success_message" : "Email received", - "error_message" : "Invalid Email. Provide valid Email", - "operator" : null, - "success_action_name" : "EvaluateRule", - "success_expression" : null, - "success_workflow_id" : 1, - "success_rule_id" : 2, - "failure_action_name" : "OutputExpression", - "failure_expression" : "false", - "failure_workflow_id" : 1, - "failure_rule_id" : 1, - "child_rules" : null, - "custom_details" : "{\"Field\": \"visitorEmail\", \"Description\": \"Provide valid email\", \"Placeholder\": \"Enter your email\", \"IsInputStage\": true}", - "last_modified_on" : "2024-09-26T07:16:07.705Z", - "is_active" : true - }, - { - // identity verification rule - "id" : 2, - "workflow_id" : 1, - "expression" : "Utils.IsUserEmailExists(input)", - "success_message" : "", - "error_message" : "", - "operator" : null, - "success_action_name" : "EvaluateRule", - "success_expression" : null, - "success_workflow_id" : 1, - "success_rule_id" : 4, - "failure_action_name" : "EvaluateRule", - "failure_expression" : null, - "failure_workflow_id" : 1, - "failure_rule_id" : 3, - "child_rules" : null, - "custom_details" : null, - "last_modified_on" : "2024-09-16T12:17:20.768Z", - "is_active" : true - }, - { - // requesting and updating name field - "id" : 3, - "workflow_id" : 1, - "expression" : "Utils.SaveField(input)", - "success_message" : "Name received", - "error_message" : "Name should not be empty", - "operator" : null, - "success_action_name" : "EvaluateRule", - "success_expression" : null, - "success_workflow_id" : 1, - "success_rule_id" : 4, - "failure_action_name" : "OutputExpression", - "failure_expression" : "false", - "failure_workflow_id" : 1, - "failure_rule_id" : 3, - "child_rules" : null, - "custom_details" : "{\"Field\": \"visitorName\", \"Description\": \"Provide valid name\", \"Placeholder\": \"Enter your name\", \"IsInputStage\": true}", - "last_modified_on" : "2024-09-26T10:05:28.391Z", - "is_active" : true - }, - { - // branching rule cases - "id" : 4, - "workflow_id" : 1, - "expression" : "true", - "success_message" : "", - "error_message" : "", - "operator" : null, - "success_action_name" : "CustomAction", // validate the selected value in custom method action - "success_expression" : null, - "success_workflow_id" : null, - "success_rule_id" : null, - "failure_action_name" : null, - "failure_expression" : null, - "failure_workflow_id" : null, - "failure_rule_id" : null, - "child_rules" : null, - "custom_details" : "{\"values\":[{\"value\":\"Bolddesk\",\"SuccessWorkflowId\":1,\"SuccessRuleId\":5},{\"value\":\"Boldsign\",\"SuccessWorkflowId\":1,\"SuccessRuleId\":6}]}", - "last_modified_on" : "2024-09-16T13:10:50.543Z", - "is_active" : true - }, - { - //branch rule from rule id "4" - "id" : 5, - "workflow_id" : 1, - "expression" : "Utils.SendMessage(input)", // send message action for selected item - "success_message" : "Bold desk product selected", - "error_message" : "Send message action failed", - "operator" : null, - "success_action_name" : "EvaluateRule", - "success_expression" : null, - "success_workflow_id" : 1, - "success_rule_id" : 7, - "failure_action_name" : null, - "failure_expression" : null, - "failure_workflow_id" : null, - "failure_rule_id" : null, - "child_rules" : null, - "custom_details" : "{\"Message\": \"Bolddesk value selected\"}", - "last_modified_on" : "2024-09-16T13:10:50.543Z", - "is_active" : true - }, - { - //branch rule from rule id "4" - "id" : 6, - "workflow_id" : 1, - "expression" : "Utils.SendMessage(input)", // send message action for selected item - "success_message" : "Bold sign product selected", - "error_message" : "Send message action failed", - "operator" : null, - "success_action_name" : "EvaluateRule", - "success_expression" : null, - "success_workflow_id" : 7, - "success_rule_id" : 1, - "failure_action_name" : null, - "failure_expression" : null, - "failure_workflow_id" : null, - "failure_rule_id" : null, - "child_rules" : null, - "custom_details" : "{\"Message\": \"Boldsign value selected\"}", - "last_modified_on" : "2024-09-16T13:10:50.543Z", - "is_active" : true - }, - { - // flow completed - "id" : 7, - "workflow_id" : 1, - "expression" : "true", - "success_message" : "Flow completed", - "error_message" : "Flow completed action failed", - "operator" : null, - "success_action_name" : "OutputExpression", - "success_expression" : "true", - "success_workflow_id" : null, - "success_rule_id" : null, - "failure_action_name" : null, - "failure_expression" : null, - "failure_workflow_id" : null, - "failure_rule_id" : null, - "child_rules" : null, - "custom_details" : null, - "last_modified_on" : "2024-09-16T13:10:50.543Z", - "is_active" : true - } -]; +import { RuleData2 } from "../models/appModel"; export const RULE_DATA2: RuleData2[] = [ { diff --git a/src/app/models/appModel.ts b/src/app/models/appModel.ts index 40060c7..e2aad60 100644 --- a/src/app/models/appModel.ts +++ b/src/app/models/appModel.ts @@ -1,26 +1,5 @@ import { TextFormatEnum } from "./enum"; -export interface RuleData { - id: number; - workflow_id: number; - expression: string; - success_message: string; - error_message: string; - operator: any | null; // Use 'any' if the data type is unknown, else specify a more specific type - success_action_name: string | null; - success_expression: string | null; - success_workflow_id: number | null; - success_rule_id: number | null; - failure_action_name: string | null; - failure_expression: string | null; - failure_workflow_id: number | null; - failure_rule_id: number | null; - child_rules: any | null; // If child_rules has a specific type, replace 'any' with it - custom_details: string | null; // Assuming this is always a JSON string - last_modified_on: string; // Keep as a string, or use Date if you prefer converting it - is_active: boolean; -} - // Define FieldOptionDetails interface export interface FieldOptionDetail { label: string; @@ -37,17 +16,17 @@ export interface FieldValidation { // Define FieldDetails interface export interface FieldDetails { - description?: string | null; - label?: string | null; - placeholder?: string | null; - apiName?: string | null; - maskForAgent?: boolean | null; - isOptional?: boolean | null; - useAPI?: boolean | null; + description?: string; + label?: string; + placeholder?: string; + apiName?: string; + maskForAgent?: boolean; + isOptional?: boolean; + useAPI?: boolean; fieldValidation?: FieldValidation | null; - value?: string | null; - groupId?: number | null; - ruleType?: number | null; + value?: string; + groupId?: number; + ruleType?: number; } // Define MessageDetails interface @@ -67,6 +46,12 @@ export interface BranchDetail { value?: string | null; } +export interface CustomerBlockFieldDetails { + isEmailEditorEnabled?: boolean; + isNameEditorEnabled?: boolean; + isPhoneEditorEnabled?: boolean; +} + // Define the main Workflow interface export interface RuleData2 { id: number; @@ -80,6 +65,8 @@ export interface RuleData2 { branchDetails?: BranchDetail[] | null; messageDetails?: MessageDetails | null; fieldOptionDetails?: FieldOptionDetail[] | null; + customerBlockFieldInfo?: CustomerBlockFieldDetails | null; + parentId?: number | null; } // Define the main Workflow interface @@ -93,5 +80,6 @@ export interface ChatWorkflowRulesData { branchDetails?: BranchDetail[] | null; messageDetails?: MessageDetails | null; fieldOptionDetails?: FieldOptionDetail[] | null; + customerBlockFieldInfo?: CustomerBlockFieldDetails | null; parentId?: number | null; } \ No newline at end of file