├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── deployment.yaml │ ├── matrix_includes.json │ ├── merge-branches.yaml │ ├── typecheck-in-app.yaml │ ├── typecheck-next-api.yml │ └── typecheck-next-web.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .leanignore ├── .prettierignore ├── .prettierrc.yaml ├── CHANGELOG.md ├── Deploy.md ├── README.md ├── api ├── Category.js ├── OpsLog.js ├── Organization.js ├── Role.js ├── User.js ├── category │ ├── api.js │ └── utils.js ├── cloud.js ├── common.js ├── customerService │ ├── api.js │ └── utils.js ├── errorHandler.js ├── file │ ├── api.js │ └── utils.js ├── group │ └── utils.js ├── index.js ├── launch.js ├── middleware │ └── index.js ├── next-shim.js ├── notification.js ├── quick-reply │ └── api.js ├── rule │ ├── action │ │ ├── actions.js │ │ └── index.js │ ├── automation │ │ ├── api.js │ │ ├── conditions │ │ │ ├── created_at.js │ │ │ ├── index.js │ │ │ └── updated_at.js │ │ └── index.js │ ├── condition │ │ ├── conditions │ │ │ ├── assignee_id.js │ │ │ ├── content.js │ │ │ ├── current_user.js │ │ │ ├── index.js │ │ │ ├── latest_reply.js │ │ │ ├── status.js │ │ │ ├── title.js │ │ │ └── update_type.js │ │ └── index.js │ └── trigger │ │ ├── api.js │ │ └── index.js ├── ticket │ ├── api.js │ ├── model.js │ └── utils.js ├── ticketField │ ├── constant.js │ ├── fieldService.js │ ├── index.js │ └── variantService.js ├── ticketForm │ ├── Service.js │ └── index.js ├── user │ ├── api.js │ └── utils.js └── utils │ ├── cache.js │ ├── index.js │ ├── object.js │ └── search.js ├── clientGlobalVar.js ├── config.js ├── config.webapp.js ├── deploy ├── index.mjs └── utils.mjs ├── docs ├── api1.yml ├── api2.yml └── crm.md ├── in-app └── v1 │ ├── .env │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc.json │ ├── README.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── script │ ├── check_npm_version.sh │ └── generateEnv.js │ ├── src │ ├── App │ │ ├── Articles │ │ │ ├── index.tsx │ │ │ └── utils.tsx │ │ ├── Categories │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── Home │ │ │ ├── AiClassify.tsx │ │ │ ├── Help.tsx │ │ │ ├── Notices.tsx │ │ │ ├── Topics.tsx │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── LogIn │ │ │ └── index.tsx │ │ ├── NotFound │ │ │ └── index.tsx │ │ ├── Test │ │ │ └── index.tsx │ │ ├── Tickets │ │ │ ├── New │ │ │ │ ├── CustomForm │ │ │ │ │ ├── CustomField │ │ │ │ │ │ ├── CheckboxGroup │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Date │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Description.tsx │ │ │ │ │ │ ├── Input │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── NumberInput │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── RadioGroup │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Select │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Textarea │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Uploader │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── FormNote.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Form │ │ │ │ │ ├── Field │ │ │ │ │ │ ├── CheckboxGroup │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Dropdown │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ │ │ ├── Input │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── RadioGroup │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Textarea │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Uploader │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── Group │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── hooks │ │ │ │ │ └── useCannotCreateMore.ts │ │ │ │ ├── index.tsx │ │ │ │ └── usePersistFormData.ts │ │ │ ├── Ticket │ │ │ │ ├── Evaluation │ │ │ │ │ └── index.tsx │ │ │ │ ├── Replies │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── ReplyInput │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── hooks │ │ │ │ └── useTickets.ts │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── TopCategories │ │ │ └── index.tsx │ │ └── index.tsx │ ├── api │ │ ├── article.ts │ │ ├── category.ts │ │ ├── evaluation.ts │ │ ├── ticket-form.ts │ │ └── ticket.ts │ ├── auth │ │ └── index.tsx │ ├── components │ │ ├── APIError │ │ │ └── index.tsx │ │ ├── Alert │ │ │ └── index.tsx │ │ ├── Button │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── CategoryPath │ │ │ └── index.tsx │ │ ├── ControlButton │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── ErrorBoundary │ │ │ └── index.tsx │ │ ├── FileItem │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── Form │ │ │ ├── Checkbox │ │ │ │ └── index.tsx │ │ │ ├── Input │ │ │ │ └── index.tsx │ │ │ ├── Radio │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Loading │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── NewTicketButton │ │ │ └── index.tsx │ │ ├── NoData │ │ │ └── index.tsx │ │ ├── OpenInBrowser │ │ │ └── index.tsx │ │ ├── Page │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── Preview │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── QueryWrapper │ │ │ └── index.tsx │ │ ├── SDK │ │ │ ├── Context.tsx │ │ │ ├── api │ │ │ │ ├── button.ts │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── index.ts │ │ │ └── webView │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ ├── SpaceChinese │ │ │ └── index.tsx │ │ ├── Time │ │ │ └── index.tsx │ │ └── Uploader │ │ │ └── index.tsx │ ├── env │ │ ├── index.ts │ │ └── local-storage.ts │ ├── i18n │ │ ├── index.ts │ │ └── locales │ │ │ ├── ar.json │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es.json │ │ │ ├── fr.json │ │ │ ├── id.json │ │ │ ├── it.json │ │ │ ├── ja.json │ │ │ ├── ko.json │ │ │ ├── pt.json │ │ │ ├── ru.json │ │ │ ├── th.json │ │ │ ├── tr.json │ │ │ ├── vi.json │ │ │ ├── zh-cn.json │ │ │ ├── zh-hk.json │ │ │ └── zh-tw.json │ ├── icons │ │ ├── Back.tsx │ │ ├── Bell.tsx │ │ ├── Check.tsx │ │ ├── Clip.tsx │ │ ├── Done.tsx │ │ ├── EditableIcon.tsx │ │ ├── FailedIcon.tsx │ │ ├── Feedback.tsx │ │ ├── FileVideo.tsx │ │ ├── Fill.tsx │ │ ├── Help.tsx │ │ ├── Home.tsx │ │ ├── LoadingIcon.png │ │ ├── Plus.tsx │ │ ├── Speaker.tsx │ │ ├── ThumbDown.tsx │ │ ├── ThumbUp.tsx │ │ └── X.tsx │ ├── index.css │ ├── index.tsx │ ├── leancloud │ │ └── index.ts │ ├── states │ │ ├── app.ts │ │ ├── auth.ts │ │ ├── content.ts │ │ ├── root-category.ts │ │ └── ticket-info.ts │ ├── types │ │ └── index.ts │ ├── utils │ │ ├── base64.ts │ │ ├── screen.ts │ │ ├── url.ts │ │ ├── useIsMounted.ts │ │ ├── usePreview.tsx │ │ └── useUpload.ts │ └── vite.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.js ├── index.js ├── integrations ├── mailgun │ └── server.js ├── slack │ └── server.js └── zulip │ └── server.js ├── jsconfig.json ├── leanengine.yaml ├── lib ├── common.js └── leancloud.js ├── migration-store.js ├── migrations ├── 1511857499214-add-avatar.js ├── 1511873985379-add-user-name.js ├── 1546020348973-fix-reply-acl-for-organization-members.js └── 1551407562121-add-ticket-tag.js ├── modules ├── About.js ├── App.css ├── App.js ├── Avatar.js ├── CategoriesSelect.js ├── CustomerService │ ├── PreviewTip.js │ ├── Tickets.js │ ├── index.js │ ├── index.module.scss │ └── useCustomerServices.js ├── CustomerServiceTickets.css ├── Error.js ├── GlobalNav.js ├── Home.js ├── Login.css ├── Login.js ├── NewTicket.js ├── NewTicket.next.css ├── NewTicket.next.js ├── NotFound.js ├── Notification.js ├── Notifications.js ├── OrganizationSelect.js ├── Settings │ ├── AccountLink.js │ ├── Categories.js │ ├── Category.js │ ├── CategorySort.js │ ├── CustomerServiceProfile.js │ ├── FAQ.js │ ├── FAQs.js │ ├── Group.js │ ├── Groups.js │ ├── Members.js │ ├── OauthButton.js │ ├── Organization.js │ ├── OrganizationNew.js │ ├── Organizations.js │ ├── Profile.js │ ├── QuickReply │ │ ├── Add.js │ │ ├── Edit.js │ │ ├── index.js │ │ └── index.module.scss │ ├── Rule │ │ ├── Action │ │ │ ├── index.js │ │ │ └── types │ │ │ │ └── index.js │ │ ├── Automation │ │ │ ├── condition │ │ │ │ ├── fields │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── Condition │ │ │ ├── fields │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── Trigger │ │ │ ├── index.js │ │ │ └── index.module.scss │ │ ├── components │ │ │ ├── AssigneeSelect.js │ │ │ ├── CardContainer.js │ │ │ ├── CardContainer.module.scss │ │ │ ├── MapSelect.js │ │ │ ├── Select.js │ │ │ └── Value.js │ │ └── context.js │ ├── Tag.js │ ├── Tags.js │ ├── TicketField │ │ ├── Field.js │ │ ├── FieldList.js │ │ ├── LocaleManage.js │ │ ├── Preview.js │ │ ├── index.js │ │ ├── index.module.scss │ │ └── util.js │ ├── TicketForm │ │ ├── FormPage.js │ │ ├── Preview.js │ │ ├── index.js │ │ ├── index.module.scss │ │ └── useTicketForm.js │ ├── Vacation.js │ ├── index.css │ └── index.js ├── Subscriptions.js ├── Ticket │ ├── AccessControl │ │ ├── index.js │ │ └── index.module.scss │ ├── CSReplyEditor │ │ ├── MarkdownEditor.js │ │ ├── QuickReplySelector.js │ │ ├── index.js │ │ └── index.module.scss │ ├── Category │ │ ├── index.css │ │ └── index.js │ ├── EditReplyModal.js │ ├── Evaluation.js │ ├── LeanCloudApp.js │ ├── NextTicket.js │ ├── OpsLog.js │ ├── RecentTickets.js │ ├── ReplyCard.js │ ├── ReplyRevisionsModal.js │ ├── TagForm.js │ ├── TicketMetadata.js │ ├── TicketOperation.js │ ├── TicketReply.js │ ├── Time.js │ ├── index.css │ └── index.js ├── Tickets │ ├── CategoryTreeMenu.css │ ├── CategoryTreeMenu.js │ ├── TicketItem.js │ └── index.js ├── TicketsMoveButton.js ├── User.css ├── User.js ├── UserForm.js ├── UserLabel.css ├── UserLabel.js ├── category │ └── index.js ├── common.js ├── components │ ├── BlodSearchString │ │ └── index.js │ ├── Confirm │ │ ├── index.js │ │ └── index.module.scss │ ├── CustomField │ │ ├── index.js │ │ └── index.module.scss │ ├── DelayInputForm │ │ └── index.js │ ├── Divider │ │ ├── index.css │ │ └── index.js │ ├── EmptyBadge │ │ ├── index.css │ │ └── index.js │ ├── ErrorBoundary.js │ ├── FAQ │ │ ├── index.css │ │ └── index.js │ ├── Group │ │ └── index.js │ ├── InternalBadge │ │ └── index.js │ ├── NoData │ │ ├── index.js │ │ └── index.module.scss │ ├── Pagination │ │ ├── index.js │ │ └── index.module.scss │ ├── Radio │ │ ├── index.css │ │ └── index.js │ ├── Select │ │ ├── index.css │ │ └── index.js │ ├── TextareaWithPreview │ │ ├── index.css │ │ └── index.js │ ├── TicketStatusLabel │ │ ├── index.js │ │ └── index.module.scss │ ├── Uploader │ │ ├── index.js │ │ └── index.module.scss │ └── WeekendWarning │ │ └── index.js ├── config │ └── index.js ├── context │ └── index.js ├── custom │ └── element.js ├── i18n │ ├── index.js │ └── locales │ │ ├── cht.json │ │ ├── en.json │ │ ├── id.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── th.json │ │ └── zh.json ├── style │ ├── _custom.scss │ └── index.scss └── utils │ ├── AuthRoute.js │ ├── DocumentTitle.js │ ├── hooks.js │ ├── index.js │ ├── useAutoSave.js │ └── useUploader.js ├── next ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.json ├── api │ ├── .eslintrc.js │ ├── .swcrc │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── scripts │ │ ├── admin-migration.mjs │ │ ├── article-i18n-migration.mjs │ │ ├── create-es-index-ticket.mjs │ │ ├── migrate-private-article.js │ │ ├── set-reply-edited-flag.js │ │ ├── ticket-form-note-i18n.mjs │ │ └── view-conditions-migration.mjs │ ├── server.js │ ├── src │ │ ├── api-log │ │ │ ├── api-log.middleware.ts │ │ │ ├── api-log.service.ts │ │ │ ├── crm │ │ │ │ ├── crm.service.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── article │ │ │ ├── article.service.ts │ │ │ └── stats.ts │ │ ├── cache │ │ │ ├── cache.ts │ │ │ ├── index.ts │ │ │ ├── redis.ts │ │ │ ├── stores │ │ │ │ ├── lru-cache.store.ts │ │ │ │ └── redis.store.ts │ │ │ └── types.ts │ │ ├── category │ │ │ ├── category.service.ts │ │ │ ├── find-category.pipe.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── cloud │ │ │ ├── index.ts │ │ │ ├── ticketLog │ │ │ │ └── index.ts │ │ │ ├── ticketStats │ │ │ │ ├── index.ts │ │ │ │ ├── slack.ts │ │ │ │ └── utils.ts │ │ │ └── utils.ts │ │ ├── common │ │ │ ├── http │ │ │ │ ├── error.ts │ │ │ │ ├── handler │ │ │ │ │ ├── index.ts │ │ │ │ │ └── param │ │ │ │ │ │ ├── body.ts │ │ │ │ │ │ ├── current-user.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── locale.ts │ │ │ │ │ │ ├── pagination.ts │ │ │ │ │ │ ├── query.ts │ │ │ │ │ │ └── url-param.ts │ │ │ │ └── index.ts │ │ │ ├── pipe │ │ │ │ ├── FindModelPipe.ts │ │ │ │ ├── ParseBoolPipe.ts │ │ │ │ ├── ParseCsvPipe.ts │ │ │ │ ├── ParseDatePipe.ts │ │ │ │ ├── ParseIntPipe.ts │ │ │ │ ├── ParseOrderPipe.ts │ │ │ │ ├── TrimPipe.ts │ │ │ │ ├── ValidationPipe.ts │ │ │ │ └── index.ts │ │ │ └── template │ │ │ │ ├── index.ts │ │ │ │ ├── render.ts │ │ │ │ ├── string.template.ts │ │ │ │ └── types.ts │ │ ├── config │ │ │ └── index.ts │ │ ├── controller │ │ │ ├── article-topic.ts │ │ │ ├── category.ts │ │ │ ├── collaborator.ts │ │ │ ├── config.ts │ │ │ ├── customer-service-action-log.ts │ │ │ ├── customer-service.ts │ │ │ ├── dynamic-content.ts │ │ │ ├── email-notification.ts │ │ │ ├── evaluation-tag.ts │ │ │ ├── export-ticket-task.ts │ │ │ ├── file.ts │ │ │ ├── group.ts │ │ │ ├── index.ts │ │ │ ├── merge-user-task.ts │ │ │ ├── metrics.ts │ │ │ ├── quick-reply.ts │ │ │ ├── support-email.ts │ │ │ ├── tag-metadata.ts │ │ │ ├── ticket-field.ts │ │ │ ├── ticket.ts │ │ │ ├── translate.ts │ │ │ ├── user.ts │ │ │ ├── vacation.ts │ │ │ ├── verification.ts │ │ │ └── view.ts │ │ ├── declear.d.ts │ │ ├── dynamic-content │ │ │ ├── dynamic-content.service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── events │ │ │ └── index.ts │ │ ├── file │ │ │ └── services │ │ │ │ └── file.ts │ │ ├── i18n │ │ │ └── locales.ts │ │ ├── index.ts │ │ ├── integration │ │ │ ├── debug.ts │ │ │ ├── index.ts │ │ │ ├── intelligent-operation │ │ │ │ ├── README.md │ │ │ │ ├── aliyun.d.ts │ │ │ │ ├── desensitize.ts │ │ │ │ └── index.ts │ │ │ ├── jira.ts │ │ │ ├── mailgun.ts │ │ │ ├── slack-plus │ │ │ │ ├── SlackNotification.ts │ │ │ │ ├── index.ts │ │ │ │ └── message.ts │ │ │ ├── slack │ │ │ │ ├── index.ts │ │ │ │ └── message.ts │ │ │ └── taptap-dw │ │ │ │ └── index.ts │ │ ├── interfaces │ │ │ ├── email-notification.ts │ │ │ ├── support-email.ts │ │ │ └── ticket.ts │ │ ├── launch.ts │ │ ├── leancloud.ts │ │ ├── middleware │ │ │ ├── auth.ts │ │ │ ├── boolean.ts │ │ │ ├── error.ts │ │ │ ├── include.ts │ │ │ ├── index.ts │ │ │ ├── locale.ts │ │ │ ├── pagination.ts │ │ │ ├── parseRange.ts │ │ │ ├── search.ts │ │ │ └── sort.ts │ │ ├── model │ │ │ ├── Article.ts │ │ │ ├── ArticleFeedback.ts │ │ │ ├── ArticleRevision.ts │ │ │ ├── ArticleTopic.ts │ │ │ ├── ArticleTranslation.ts │ │ │ ├── Category.ts │ │ │ ├── Config.ts │ │ │ ├── DurationMetrics.ts │ │ │ ├── DynamicContent.ts │ │ │ ├── DynamicContentVariant.ts │ │ │ ├── ExportTicketTask.ts │ │ │ ├── File.ts │ │ │ ├── Group.ts │ │ │ ├── MergeUserTask.ts │ │ │ ├── Notification.ts │ │ │ ├── OpsLog.ts │ │ │ ├── Organization.ts │ │ │ ├── QuickReply.ts │ │ │ ├── Reply.ts │ │ │ ├── ReplyRevision.ts │ │ │ ├── Role.ts │ │ │ ├── SupportEmail.ts │ │ │ ├── SupportEmailMessage.ts │ │ │ ├── Tag.ts │ │ │ ├── TagMetadata.ts │ │ │ ├── Ticket.ts │ │ │ ├── TicketField.ts │ │ │ ├── TicketFieldValue.ts │ │ │ ├── TicketFieldVariant.ts │ │ │ ├── TicketForm.ts │ │ │ ├── TicketLog.ts │ │ │ ├── TicketStats.ts │ │ │ ├── TicketStatusStats.ts │ │ │ ├── TimeTrigger.ts │ │ │ ├── Trigger.ts │ │ │ ├── User.ts │ │ │ ├── Vacation.ts │ │ │ ├── View.ts │ │ │ └── Watch.ts │ │ ├── notification │ │ │ ├── index.ts │ │ │ └── migrate.ts │ │ ├── orm │ │ │ ├── acl.ts │ │ │ ├── clickhouse.ts │ │ │ ├── command.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── preloader.ts │ │ │ ├── query.ts │ │ │ ├── relation.ts │ │ │ └── utils.ts │ │ ├── queue │ │ │ └── index.ts │ │ ├── response │ │ │ ├── article-revision.ts │ │ │ ├── article-topic.ts │ │ │ ├── article.ts │ │ │ ├── category.ts │ │ │ ├── customer-service.ts │ │ │ ├── dynamic-content-variant.ts │ │ │ ├── dynamic-content.ts │ │ │ ├── email-notification.ts │ │ │ ├── export-ticket-task.ts │ │ │ ├── file.ts │ │ │ ├── group.ts │ │ │ ├── merge-user-task.ts │ │ │ ├── notification.ts │ │ │ ├── ops-log.ts │ │ │ ├── organization.ts │ │ │ ├── quick-reply.ts │ │ │ ├── reply-revision.ts │ │ │ ├── reply.ts │ │ │ ├── support-email.ts │ │ │ ├── tag-metadata.ts │ │ │ ├── ticket-field.ts │ │ │ ├── ticket-form.ts │ │ │ ├── ticket-stats.ts │ │ │ ├── ticket.ts │ │ │ ├── time-trigger.ts │ │ │ ├── trigger.ts │ │ │ ├── user.ts │ │ │ ├── vacation.ts │ │ │ └── view.ts │ │ ├── router │ │ │ ├── article.ts │ │ │ ├── index.ts │ │ │ ├── notification.ts │ │ │ ├── organization.ts │ │ │ ├── reply.ts │ │ │ ├── ticket-stats.ts │ │ │ ├── ticket.ts │ │ │ ├── time-trigger.ts │ │ │ ├── trigger.ts │ │ │ └── unread.ts │ │ ├── sentry.ts │ │ ├── service │ │ │ ├── collaborator.ts │ │ │ ├── customer-service-action-log.ts │ │ │ ├── email-notification.ts │ │ │ ├── email.ts │ │ │ ├── group.ts │ │ │ ├── openai.ts │ │ │ ├── organization.ts │ │ │ ├── role.ts │ │ │ ├── search-ticket.ts │ │ │ ├── support-email-message.ts │ │ │ ├── support-email.ts │ │ │ ├── ticket.ts │ │ │ └── translate.ts │ │ ├── tap-support │ │ │ ├── index.ts │ │ │ ├── tap-support.controller.ts │ │ │ ├── tap-support.service.ts │ │ │ └── types.ts │ │ ├── ticket-form │ │ │ ├── index.ts │ │ │ ├── schemas.ts │ │ │ ├── ticket-form-note │ │ │ │ ├── responses.ts │ │ │ │ ├── schemas.ts │ │ │ │ ├── ticket-form-note.controller.ts │ │ │ │ ├── ticket-form-note.entity.ts │ │ │ │ ├── ticket-form-note.service.ts │ │ │ │ └── types.ts │ │ │ ├── ticket-form.controller.ts │ │ │ ├── ticket-form.service.ts │ │ │ └── types.ts │ │ ├── ticket │ │ │ ├── TicketCreator.ts │ │ │ ├── TicketUpdater.ts │ │ │ ├── automation │ │ │ │ ├── action │ │ │ │ │ ├── addTag.ts │ │ │ │ │ ├── changeStatus.ts │ │ │ │ │ ├── closeTicket.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── updateAssigneeId.ts │ │ │ │ │ ├── updateCategoryId.ts │ │ │ │ │ └── updateGroupId.ts │ │ │ │ ├── condition │ │ │ │ │ ├── assigneeId.ts │ │ │ │ │ ├── authorId.ts │ │ │ │ │ ├── categoryId.ts │ │ │ │ │ ├── common │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── number.ts │ │ │ │ │ │ └── string.ts │ │ │ │ │ ├── content.ts │ │ │ │ │ ├── groupId.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── language.ts │ │ │ │ │ ├── metaData.ts │ │ │ │ │ ├── status.ts │ │ │ │ │ ├── tags.ts │ │ │ │ │ └── title.ts │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ ├── time-trigger │ │ │ │ │ ├── action │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── condition │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── sinceAssigned.ts │ │ │ │ │ │ ├── sinceCreated.ts │ │ │ │ │ │ └── sinceUpdated.ts │ │ │ │ │ ├── context.ts │ │ │ │ │ └── index.ts │ │ │ │ └── trigger │ │ │ │ │ ├── action │ │ │ │ │ ├── index.ts │ │ │ │ │ └── updateAssigneeId.ts │ │ │ │ │ ├── condition │ │ │ │ │ ├── assigneeId.ts │ │ │ │ │ ├── authorId.ts │ │ │ │ │ ├── currentUserId.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── replyContent.ts │ │ │ │ │ └── ticket.ts │ │ │ │ │ ├── context.ts │ │ │ │ │ └── index.ts │ │ │ ├── export │ │ │ │ ├── ExportStream.ts │ │ │ │ ├── ExportTicket.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── services │ │ │ │ ├── duration-metrics.ts │ │ │ │ └── ticket.ts │ │ │ └── view │ │ │ │ ├── conditions │ │ │ │ ├── ViewCondition.ts │ │ │ │ ├── assigneeId.ts │ │ │ │ ├── categoryId.ts │ │ │ │ ├── groupId.ts │ │ │ │ ├── index.ts │ │ │ │ ├── language.ts │ │ │ │ ├── sinceCreated.ts │ │ │ │ ├── sinceFulfilled.ts │ │ │ │ ├── sinceNew.ts │ │ │ │ ├── sincePreFulfilled.ts │ │ │ │ ├── sinceUpdated.ts │ │ │ │ ├── sinceWaitingCustomer.ts │ │ │ │ ├── sinceWaitingCustomerService.ts │ │ │ │ ├── status.ts │ │ │ │ └── tags.ts │ │ │ │ └── index.ts │ │ ├── user │ │ │ ├── services │ │ │ │ └── user.ts │ │ │ └── types.ts │ │ └── utils │ │ │ ├── conditions.ts │ │ │ ├── htmlify.ts │ │ │ ├── index.ts │ │ │ ├── ip.ts │ │ │ ├── jwt.ts │ │ │ ├── locale.ts │ │ │ ├── mem-promise.ts │ │ │ ├── promise-cache.ts │ │ │ ├── search.ts │ │ │ ├── textFilter.ts │ │ │ ├── trace.ts │ │ │ ├── types.ts │ │ │ ├── xss.ts │ │ │ ├── yup.ts │ │ │ └── zod.ts │ ├── tsconfig.json │ └── types.d.ts └── web │ ├── .eslintrc.js │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── css │ │ ├── docsearch-override.css │ │ └── docsearch.min.css │ └── js │ │ └── docsearch.min.js │ ├── script │ └── generateEnv.js │ ├── src │ ├── App │ │ ├── Admin │ │ │ ├── CurrentUserSection │ │ │ │ └── index.tsx │ │ │ ├── Feedback │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Settings │ │ │ │ ├── Articles │ │ │ │ │ ├── ArticleDetail.tsx │ │ │ │ │ ├── ArticleSelect.tsx │ │ │ │ │ ├── EditArticleForm.tsx │ │ │ │ │ ├── EditArticleTranslationForm.tsx │ │ │ │ │ ├── EditTranslation.tsx │ │ │ │ │ ├── FeedbackSummary.tsx │ │ │ │ │ ├── NewArticle.tsx │ │ │ │ │ ├── NewTranslation.tsx │ │ │ │ │ ├── Revision.tsx │ │ │ │ │ ├── components │ │ │ │ │ │ ├── ArticleForm.tsx │ │ │ │ │ │ ├── ArticleStatus.tsx │ │ │ │ │ │ ├── PreviewLink.tsx │ │ │ │ │ │ ├── SetDefaultButton.tsx │ │ │ │ │ │ └── TranslationForm.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── Automations │ │ │ │ │ ├── TimeTriggers │ │ │ │ │ │ ├── Detail.tsx │ │ │ │ │ │ ├── New.tsx │ │ │ │ │ │ ├── actions │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── conditions │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Triggers │ │ │ │ │ │ ├── Detail.tsx │ │ │ │ │ │ ├── New.tsx │ │ │ │ │ │ ├── actions │ │ │ │ │ │ │ ├── UpdateAssigneeId.tsx │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── conditions │ │ │ │ │ │ │ ├── AssigneeId.tsx │ │ │ │ │ │ │ ├── AuthorId.tsx │ │ │ │ │ │ │ ├── CurrentUserId.tsx │ │ │ │ │ │ │ ├── Ticket.tsx │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── actions │ │ │ │ │ │ ├── ChangeStatus.tsx │ │ │ │ │ │ ├── UpdateAssigneeId.tsx │ │ │ │ │ │ ├── UpdateCategoryId.tsx │ │ │ │ │ │ └── UpdateGroupId.tsx │ │ │ │ │ ├── components │ │ │ │ │ │ ├── TagSelect.tsx │ │ │ │ │ │ └── TriggerForm │ │ │ │ │ │ │ ├── Actions.tsx │ │ │ │ │ │ │ ├── Conditions.tsx │ │ │ │ │ │ │ ├── CustomField.tsx │ │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── conditions │ │ │ │ │ │ ├── AssigneeId.tsx │ │ │ │ │ │ ├── AuthorId.tsx │ │ │ │ │ │ ├── CategoryId.tsx │ │ │ │ │ │ ├── GroupId.tsx │ │ │ │ │ │ ├── Language.tsx │ │ │ │ │ │ ├── MetaData.tsx │ │ │ │ │ │ ├── NumberValue.tsx │ │ │ │ │ │ ├── Status.tsx │ │ │ │ │ │ ├── StringValue.tsx │ │ │ │ │ │ └── Tags.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── Categories │ │ │ │ │ ├── AiClassifyTest.tsx │ │ │ │ │ ├── CategoryFieldStats.tsx │ │ │ │ │ ├── CategoryForm.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── Collaborators │ │ │ │ │ ├── Privileges.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── DynamicContents │ │ │ │ │ ├── DynamicContentForm.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── EmailNotification │ │ │ │ │ └── index.tsx │ │ │ │ ├── Evaluation │ │ │ │ │ └── index.tsx │ │ │ │ ├── ExportTicket │ │ │ │ │ └── Tasks.tsx │ │ │ │ ├── Groups │ │ │ │ │ └── index.tsx │ │ │ │ ├── Members │ │ │ │ │ ├── components │ │ │ │ │ │ └── CustomerServiceForm.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Others │ │ │ │ │ ├── Weekday.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── QuickReplies │ │ │ │ │ ├── QuickReplyForm.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── SupportEmails │ │ │ │ │ ├── SupportEmailForm.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Tags │ │ │ │ │ ├── TagMetadataForm.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── TicketFields │ │ │ │ │ ├── TicketFieldForm.tsx │ │ │ │ │ ├── TicketFieldIcon.tsx │ │ │ │ │ ├── TicketFieldType.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── TicketFormNotes │ │ │ │ │ ├── EditTranslation.tsx │ │ │ │ │ ├── NewTicketFormNote.tsx │ │ │ │ │ ├── NewTicketFormNoteTranslation.tsx │ │ │ │ │ ├── TicketFormNoteDetail.tsx │ │ │ │ │ ├── TicketFormNoteForm.tsx │ │ │ │ │ ├── TicketFormNoteTranslationForm.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── TicketForms │ │ │ │ │ ├── EditTicketForm.tsx │ │ │ │ │ ├── components │ │ │ │ │ │ └── RefreshButton.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Topics │ │ │ │ │ └── index.tsx │ │ │ │ ├── Users │ │ │ │ │ ├── MergeUser │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ └── MergeUserForm.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Vacations │ │ │ │ │ └── index.tsx │ │ │ │ ├── Views │ │ │ │ │ ├── EditView.tsx │ │ │ │ │ ├── components │ │ │ │ │ │ ├── LanguageSelect.tsx │ │ │ │ │ │ ├── TagSelect.tsx │ │ │ │ │ │ ├── ViewConditions.tsx │ │ │ │ │ │ └── ViewSort.tsx │ │ │ │ │ ├── conditions │ │ │ │ │ │ ├── AssigneeId.tsx │ │ │ │ │ │ ├── GroupId.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Sidebar │ │ │ │ └── index.tsx │ │ │ ├── Stats │ │ │ │ ├── CustomerServiceAction │ │ │ │ │ ├── action-log-collector.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── Exporter.tsx │ │ │ │ │ │ ├── FilterForm.tsx │ │ │ │ │ │ └── SimpleModal.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── render.ts │ │ │ │ ├── Duration.tsx │ │ │ │ ├── ReplyDetails.tsx │ │ │ │ ├── StatsDetails.tsx │ │ │ │ ├── StatsPage.tsx │ │ │ │ ├── StatusPage.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── utils.tsx │ │ │ ├── Tickets │ │ │ │ ├── CustomFieldGrid.tsx │ │ │ │ ├── Filter │ │ │ │ │ ├── FilterForm │ │ │ │ │ │ ├── AssigneeSelect.tsx │ │ │ │ │ │ ├── CategorySelect.tsx │ │ │ │ │ │ ├── CreatedAtSelect.tsx │ │ │ │ │ │ ├── EvaluationStarSelect.tsx │ │ │ │ │ │ ├── FieldSelect.tsx │ │ │ │ │ │ ├── GroupSelect.tsx │ │ │ │ │ │ ├── MetadataList.tsx │ │ │ │ │ │ ├── OptionFieldValueSelect.tsx │ │ │ │ │ │ ├── PresetRangePicker.tsx │ │ │ │ │ │ ├── StatusSelect.tsx │ │ │ │ │ │ ├── TagSelect.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useTicketFilter.tsx │ │ │ │ ├── Ticket │ │ │ │ │ ├── TagForm.tsx │ │ │ │ │ ├── TicketDetail.tsx │ │ │ │ │ ├── Timeline │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── api1.tsx │ │ │ │ │ ├── components │ │ │ │ │ │ ├── CategoryCascader.tsx │ │ │ │ │ │ ├── CustomFields.tsx │ │ │ │ │ │ ├── EditReplyModal.tsx │ │ │ │ │ │ ├── Evaluation.tsx │ │ │ │ │ │ ├── FormField.tsx │ │ │ │ │ │ ├── LeanCloudApp.tsx │ │ │ │ │ │ ├── OpsLog.tsx │ │ │ │ │ │ ├── PrivateSelect.tsx │ │ │ │ │ │ ├── RecentTickets.tsx │ │ │ │ │ │ ├── ReplyCard.tsx │ │ │ │ │ │ ├── ReplyEditor.tsx │ │ │ │ │ │ ├── ReplyRevisionsModal.tsx │ │ │ │ │ │ ├── SubscribeButton.tsx │ │ │ │ │ │ ├── TicketCard.tsx │ │ │ │ │ │ ├── TicketViewers.tsx │ │ │ │ │ │ ├── Time.tsx │ │ │ │ │ │ └── useModal.tsx │ │ │ │ │ ├── hooks │ │ │ │ │ │ └── useTicketViewers.ts │ │ │ │ │ ├── mixed-ticket.tsx │ │ │ │ │ └── timeline-data.ts │ │ │ │ ├── TicketStats │ │ │ │ │ ├── StatsPie.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── utills.ts │ │ │ │ ├── TicketTable.tsx │ │ │ │ ├── Topbar │ │ │ │ │ ├── BatchOperateMenu │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── BatchUpdateDialog │ │ │ │ │ │ ├── AssigneeSelect.tsx │ │ │ │ │ │ ├── CategorySelect.tsx │ │ │ │ │ │ ├── GroupSelect.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Exporter.tsx │ │ │ │ │ ├── SortDropdown.tsx │ │ │ │ │ ├── TicketTableColumnsModal.tsx │ │ │ │ │ ├── batchUpdate.ts │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── hooks │ │ │ │ │ └── useTicketTableColumns.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── useTicketSwitchType.tsx │ │ │ ├── Verification │ │ │ │ └── index.tsx │ │ │ ├── Views │ │ │ │ ├── components │ │ │ │ │ ├── Count.tsx │ │ │ │ │ └── ViewForm.tsx │ │ │ │ ├── hooks │ │ │ │ │ └── useViewTickets.ts │ │ │ │ └── index.tsx │ │ │ ├── components │ │ │ │ ├── ArticleListFormItem.tsx │ │ │ │ ├── CategoryPath.tsx │ │ │ │ ├── HoverMenu.tsx │ │ │ │ ├── JSONTextarea.tsx │ │ │ │ ├── LocaleModal.tsx │ │ │ │ ├── LocaleSelect.tsx │ │ │ │ ├── MetaField.tsx │ │ │ │ ├── RoleCheckboxGroup.tsx │ │ │ │ ├── SortableListFormItem.tsx │ │ │ │ ├── TicketLink.tsx │ │ │ │ ├── TicketOverview.tsx │ │ │ │ ├── TicketStatus.tsx │ │ │ │ ├── TranslationList.tsx │ │ │ │ ├── Uploader.tsx │ │ │ │ ├── UserLabel.tsx │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── Login │ │ │ └── index.tsx │ │ ├── Tickets │ │ │ ├── New │ │ │ │ ├── TicketForm │ │ │ │ │ ├── CustomFields.tsx │ │ │ │ │ ├── Fields │ │ │ │ │ │ ├── CheckboxGroup.tsx │ │ │ │ │ │ ├── Date.tsx │ │ │ │ │ │ ├── Help │ │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Input.tsx │ │ │ │ │ │ ├── Number.tsx │ │ │ │ │ │ ├── RadioGroup.tsx │ │ │ │ │ │ ├── Select.tsx │ │ │ │ │ │ ├── Textarea.tsx │ │ │ │ │ │ └── Upload.tsx │ │ │ │ │ ├── FormItems.tsx │ │ │ │ │ ├── FormNote.tsx │ │ │ │ │ ├── LeanCloudAppSelect.tsx │ │ │ │ │ ├── OrganizationSelect.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── helpers │ │ │ └── lazy.tsx │ │ └── index.tsx │ ├── api │ │ ├── article-revision.ts │ │ ├── article.ts │ │ ├── category.ts │ │ ├── collaborator.ts │ │ ├── config.ts │ │ ├── customer-service-action-log.ts │ │ ├── customer-service.ts │ │ ├── dynamic-content.ts │ │ ├── email-notification.ts │ │ ├── file.ts │ │ ├── group.ts │ │ ├── metrics.ts │ │ ├── op-log.ts │ │ ├── organization.ts │ │ ├── query-client.ts │ │ ├── quick-reply.ts │ │ ├── reply.ts │ │ ├── support-email.ts │ │ ├── tag-metadata.ts │ │ ├── tds-support.ts │ │ ├── ticket-field.ts │ │ ├── ticket-form-note.ts │ │ ├── ticket-form.ts │ │ ├── ticket-stats.ts │ │ ├── ticket.ts │ │ ├── time-trigger.ts │ │ ├── topic.ts │ │ ├── trigger.ts │ │ ├── user.ts │ │ ├── vacation.ts │ │ └── view.ts │ ├── components │ │ ├── Chart │ │ │ ├── Interactions.ts │ │ │ └── index.tsx │ │ ├── DateTime │ │ │ └── index.tsx │ │ ├── ErrorPage │ │ │ └── index.tsx │ │ ├── MarkdownEditor │ │ │ └── index.tsx │ │ ├── Menu.tsx │ │ ├── Page │ │ │ ├── SubMenu │ │ │ │ ├── BaseMenu.tsx │ │ │ │ ├── SiderMenu.tsx │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── RequirePermission.tsx │ │ ├── Sentry │ │ │ ├── ErrorBoundary.tsx │ │ │ └── index.tsx │ │ ├── SortableList.tsx │ │ ├── antd │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── common │ │ │ ├── Category.tsx │ │ │ ├── CategorySelect.tsx │ │ │ ├── CustomerServiceSelect.tsx │ │ │ ├── GroupSelect.tsx │ │ │ ├── LoadingCover.tsx │ │ │ ├── QueryResult.tsx │ │ │ ├── RecentTickets.tsx │ │ │ ├── Retry.tsx │ │ │ ├── StatusSelect.tsx │ │ │ ├── UserSelect.tsx │ │ │ └── index.ts │ ├── config │ │ ├── config.ts │ │ └── index.tsx │ ├── global.d.ts │ ├── i18n │ │ └── locales.ts │ ├── icons │ │ └── DragIcon.tsx │ ├── index.css │ ├── index.tsx │ ├── leancloud │ │ └── index.ts │ ├── styles │ │ ├── antd-override.less │ │ ├── custom-antd-override.less │ │ ├── general │ │ │ └── variable.less │ │ └── tds-theme.less │ └── utils │ │ ├── date-range.ts │ │ ├── types.ts │ │ ├── useEffectEvent.ts │ │ ├── useGetCategoryPath.ts │ │ ├── useOrderBy.ts │ │ ├── usePage.ts │ │ └── useSearchParams.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.js ├── oauth ├── index.js ├── lc.js └── xd-cas.js ├── package-lock.json ├── package.json ├── plugin ├── client.js ├── jira.js └── server.js ├── postcss.config.js ├── public ├── css │ ├── docsearch-override.css │ ├── docsearch.min.css │ └── leancloud-compatible.css ├── favicon.ico ├── fonts │ ├── nootype_-_radikal_bold-webfont.eot │ ├── nootype_-_radikal_bold-webfont.ttf │ ├── nootype_-_radikal_bold-webfont.woff │ └── nootype_-_radikal_bold-webfont.woff2 ├── index.css └── maintenance-mode.html ├── resources ├── data │ ├── TicketField.jsonl │ ├── TicketFieldVariant.jsonl │ ├── TicketForm.jsonl │ └── View.jsonl └── schema │ ├── Category.json │ ├── Config.json │ ├── DurationMetrics.json │ ├── DynamicContent.json │ ├── DynamicContentVariant.json │ ├── ExportTicketTask.json │ ├── FAQ.json │ ├── FAQFeedback.json │ ├── FAQRevision.json │ ├── FAQTopic.json │ ├── FAQTranslation.json │ ├── Group.json │ ├── JiraIssue.json │ ├── MergeUserTask.json │ ├── Message.json │ ├── OpsLog.json │ ├── Organization.json │ ├── QuickReply.json │ ├── Reply.json │ ├── ReplyRevision.json │ ├── SlackNotification.json │ ├── SupportEmail.json │ ├── SupportEmailMessage.json │ ├── Tag.json │ ├── TagMetadata.json │ ├── Ticket.json │ ├── TicketField.json │ ├── TicketFieldValue.json │ ├── TicketFieldVariant.json │ ├── TicketForm.json │ ├── TicketFormNote.json │ ├── TicketFormNoteTranslation.json │ ├── TicketStats.json │ ├── TicketStatusStats.json │ ├── TimeTrigger.json │ ├── Trigger.json │ ├── Vacation.json │ ├── View.json │ ├── Watch.json │ ├── _User.json │ └── notification.json ├── server.js └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | node_modules/** -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | node: true, 7 | mocha: true, 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:react/recommended', 12 | 'plugin:promise/recommended', 13 | 'plugin:react-hooks/recommended', 14 | 'prettier', 15 | ], 16 | parserOptions: { 17 | ecmaFeatures: { 18 | experimentalObjectRestSpread: true, 19 | jsx: true, 20 | }, 21 | ecmaVersion: 2021, 22 | sourceType: 'module', 23 | }, 24 | plugins: ['react', 'promise', 'i18n'], 25 | rules: { 26 | 'linebreak-style': ['error', 'unix'], 27 | 'no-console': 0, 28 | 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 29 | 'promise/no-nesting': 0, 30 | 'promise/no-callback-in-promise': 0, 31 | 'i18n/no-chinese-character': 1, 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/matrix_includes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "name": "lc", "branch": "master", "region": "cn-n1", "group": "web" }, 3 | { "name": "lc-intl", "branch": "master", "region": "us-w1", "group": "web", "only": "prod" }, 4 | { "name": "dc", "branch": "dc", "region": "cn-n1", "group": "app" }, 5 | { "name": "dc-intl", "branch": "dc", "region": "us-w1", "group": "web" }, 6 | { "name": "xd", "branch": "xd", "region": "cn-n1", "group": "web" }, 7 | { "name": "xd-intl", "branch": "xd", "region": "ap-sg", "group": "web", "only": "prod" }, 8 | { "name": "tap", "branch": "tap", "region": "cn-n1", "group": "web" }, 9 | { "name": "tap-intl", "branch": "tap-intl", "region": "us-w1", "group": "web", "only": "prod" } 10 | ] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .leancloud/ 3 | server.bundle.js 4 | public/bundle.js* 5 | public/app.css* 6 | datas/ 7 | dist/ 8 | .idea/ -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.leanignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .DS_Store 3 | .avoscloud/ 4 | .leancloud/ 5 | node_modules/ 6 | datas/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /public 2 | /modules/i18n/locales.js 3 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | tabWidth: 2 3 | bracketSpacing: true 4 | semi: false 5 | printWidth: 100 -------------------------------------------------------------------------------- /api/Role.js: -------------------------------------------------------------------------------- 1 | const AV = require('leanengine') 2 | 3 | const getRoleUsersQuery = ({ params, currentUser }) => { 4 | const { roleId } = params 5 | return AV.Object.createWithoutData('_Role', roleId) 6 | .fetch({}, { user: currentUser }) 7 | .then((organization) => { 8 | if (!organization) { 9 | throw new AV.Cloud.Error('该角色不存在。') 10 | } 11 | 12 | return organization.getUsers().query() 13 | }) 14 | } 15 | 16 | AV.Cloud.define('getRoleUsers', async (req) => 17 | (await getRoleUsersQuery(req)).find({ useMasterKey: true }) 18 | ) 19 | 20 | AV.Cloud.define('getRoleUsersCount', async (req) => 21 | (await getRoleUsersQuery(req)).count({ useMasterKey: true }) 22 | ) 23 | -------------------------------------------------------------------------------- /api/cloud.js: -------------------------------------------------------------------------------- 1 | const AV = require('leanengine') 2 | 3 | /** 4 | * 一个简单的云代码方法 5 | */ 6 | AV.Cloud.define('hello', function (request, response) { 7 | response.success('Hello world!') 8 | }) 9 | -------------------------------------------------------------------------------- /api/customerService/utils.js: -------------------------------------------------------------------------------- 1 | const AV = require('leancloud-storage') 2 | 3 | const cache = require('../utils/cache') 4 | 5 | function fetchCustomerServiceRole() { 6 | return new AV.Query(AV.Role).equalTo('name', 'customerService').first() 7 | } 8 | 9 | function getCustomerServiceRole() { 10 | return cache.get('role:customerService', fetchCustomerServiceRole, 1000 * 60 * 10) 11 | } 12 | 13 | /** 14 | * @param {string | AV.User} user 15 | */ 16 | async function isCustomerService(user) { 17 | const userId = typeof user === 'string' ? user : user.id 18 | const role = await getCustomerServiceRole() 19 | const query = role.getUsers().query().equalTo('objectId', userId).select('objectId') 20 | return !!(await query.first({ useMasterKey: true })) 21 | } 22 | 23 | module.exports = { getCustomerServiceRole, isCustomerService } 24 | -------------------------------------------------------------------------------- /api/errorHandler.js: -------------------------------------------------------------------------------- 1 | const Raven = require('raven') 2 | 3 | exports.captureException = (message, err) => { 4 | if (message instanceof Error) { 5 | err = message 6 | message = '' 7 | } 8 | let extra = message 9 | if (typeof message == 'string') { 10 | extra = { message } 11 | } 12 | console.error(message, err.stack) 13 | Raven.captureException(err, { extra }) 14 | } 15 | -------------------------------------------------------------------------------- /api/group/utils.js: -------------------------------------------------------------------------------- 1 | const AV = require('leancloud-storage') 2 | 3 | function encodeGroupObject(group) { 4 | if (!group) { 5 | return null 6 | } 7 | return { 8 | id: group.id, 9 | name: group.get('name'), 10 | role_id: group.get('role').id, 11 | } 12 | } 13 | 14 | async function getTinyGroupInfo(groupId) { 15 | if (groupId === '') { 16 | return null 17 | } 18 | const group = await new AV.Query('Group').get(groupId, { useMasterKey: true }) 19 | return { 20 | objectId: group.id, 21 | name: group.get('name'), 22 | } 23 | } 24 | 25 | module.exports = { encodeGroupObject, getTinyGroupInfo } 26 | -------------------------------------------------------------------------------- /api/launch.js: -------------------------------------------------------------------------------- 1 | const { ready: nextReady } = require('./next-shim') 2 | 3 | let tasks = [] 4 | let launched = false 5 | 6 | function addTask(task) { 7 | if (launched) { 8 | throw new Error('app launched') 9 | } 10 | if (task && typeof task.then === 'function') { 11 | tasks.push(task) 12 | } 13 | } 14 | 15 | async function ready() { 16 | if (launched) { 17 | throw new Error('app launched') 18 | } 19 | launched = true 20 | await nextReady() 21 | await Promise.all(tasks) 22 | tasks = [] 23 | } 24 | 25 | module.exports = { 26 | addTask, 27 | ready, 28 | } 29 | -------------------------------------------------------------------------------- /api/rule/action/actions.js: -------------------------------------------------------------------------------- 1 | const { TICKET_ACTION } = require('../../../lib/common') 2 | 3 | module.exports = { 4 | update_assignee_id: (id) => { 5 | if (typeof id !== 'string') { 6 | throw new Error('The assignee_id muet ba a string') 7 | } 8 | if (id === '(current user)') { 9 | return (ctx) => { 10 | ctx.ticket.assignee_id = ctx.operator_id 11 | } 12 | } 13 | return (ctx) => { 14 | ctx.ticket.assignee_id = id 15 | } 16 | }, 17 | operate: (action) => { 18 | if (!Object.values(TICKET_ACTION).includes(action)) { 19 | throw new Error('Invalid action') 20 | } 21 | return (ctx) => { 22 | ctx.ticket.operate(action) 23 | } 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /api/rule/automation/conditions/created_at.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment') 2 | 3 | function assertValueIsValid(value) { 4 | if (typeof value !== 'number' || value < 0) { 5 | throw new Error('Value must be a postive number') 6 | } 7 | } 8 | 9 | module.exports = { 10 | is: (value) => { 11 | assertValueIsValid(value) 12 | return (ctx) => { 13 | return moment().diff(ctx.ticket.created_at, 'hour') === value 14 | } 15 | }, 16 | less_than: (value) => { 17 | assertValueIsValid(value) 18 | return (ctx) => { 19 | return moment().diff(ctx.ticket.created_at, 'hour') < value 20 | } 21 | }, 22 | greater_than: (value) => { 23 | assertValueIsValid(value) 24 | return (ctx) => { 25 | return moment().diff(ctx.ticket.created_at, 'hour') > value 26 | } 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /api/rule/automation/conditions/index.js: -------------------------------------------------------------------------------- 1 | const { assignee_id, content, status, title } = require('../../condition/conditions') 2 | 3 | module.exports = { 4 | assignee_id, 5 | content, 6 | status, 7 | title, 8 | created_at: require('./created_at'), 9 | updated_at: require('./updated_at'), 10 | } 11 | -------------------------------------------------------------------------------- /api/rule/automation/conditions/updated_at.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment') 2 | 3 | function assertValueIsValid(value) { 4 | if (typeof value !== 'number' || value < 0) { 5 | throw new Error('Value must be a postive number') 6 | } 7 | } 8 | 9 | module.exports = { 10 | is: (value) => { 11 | assertValueIsValid(value) 12 | return (ctx) => { 13 | return moment().diff(ctx.ticket.updated_at, 'hour') === value 14 | } 15 | }, 16 | less_than: (value) => { 17 | assertValueIsValid(value) 18 | return (ctx) => { 19 | return moment().diff(ctx.ticket.updated_at, 'hour') < value 20 | } 21 | }, 22 | greater_than: (value) => { 23 | assertValueIsValid(value) 24 | return (ctx) => { 25 | return moment().diff(ctx.ticket.updated_at, 'hour') > value 26 | } 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /api/rule/automation/index.js: -------------------------------------------------------------------------------- 1 | const { Conditions } = require('../condition') 2 | const { Actions } = require('../action') 3 | const conditions = require('./conditions') 4 | 5 | class Automation { 6 | constructor({ objectId, conditions, actions }) { 7 | this.id = objectId 8 | this.rawConditions = conditions 9 | this.conditions = Automation.parseConditions(conditions) 10 | this.rawActions = actions 11 | this.actions = Automation.parseActions(actions) 12 | } 13 | 14 | static parseConditions(data) { 15 | return new Conditions(data, conditions) 16 | } 17 | 18 | static parseActions(data) { 19 | return new Actions(data) 20 | } 21 | 22 | test(ctx) { 23 | return this.conditions.test(ctx) 24 | } 25 | 26 | exec(ctx) { 27 | return this.actions.exec(ctx) 28 | } 29 | } 30 | 31 | module.exports = { Automation } 32 | -------------------------------------------------------------------------------- /api/rule/condition/conditions/assignee_id.js: -------------------------------------------------------------------------------- 1 | function assertValueIsValid(value) { 2 | if (typeof value !== 'string') { 3 | throw new Error('Value must be a string') 4 | } 5 | } 6 | 7 | module.exports = { 8 | is: (value) => { 9 | assertValueIsValid(value) 10 | return (ctx) => ctx.ticket.assignee_id === value 11 | }, 12 | is_not: (value) => { 13 | assertValueIsValid(value) 14 | return (ctx) => ctx.ticket.assignee_id !== value 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /api/rule/condition/conditions/content.js: -------------------------------------------------------------------------------- 1 | function assertValueIsValid(value) { 2 | if (typeof value !== 'string') { 3 | throw new Error('Value must be a string') 4 | } 5 | } 6 | 7 | module.exports = { 8 | contains: (value) => { 9 | assertValueIsValid(value) 10 | return (ctx) => ctx.ticket.content.includes(value) 11 | }, 12 | not_contains: (value) => { 13 | assertValueIsValid(value) 14 | return (ctx) => !ctx.ticket.content.includes(value) 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /api/rule/condition/conditions/current_user.js: -------------------------------------------------------------------------------- 1 | const { isCustomerService } = require('../../../customerService/utils') 2 | 3 | function assertValueIsValid(value) { 4 | if (typeof value !== 'string') { 5 | throw new Error('Value must be a string') 6 | } 7 | } 8 | 9 | module.exports = { 10 | is: (id) => { 11 | assertValueIsValid(id) 12 | if (id === '(customer service)') { 13 | return (ctx) => { 14 | return isCustomerService(ctx.operator_id) 15 | } 16 | } 17 | return (ctx) => ctx.operator_id === id 18 | }, 19 | is_not: (id) => { 20 | assertValueIsValid(id) 21 | if (id === '(customer service)') { 22 | return async (ctx) => { 23 | return !(await isCustomerService(ctx.operator_id)) 24 | } 25 | } 26 | return (ctx) => ctx.operator_id !== id 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /api/rule/condition/conditions/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | update_type: require('./update_type'), 3 | status: require('./status'), 4 | assignee_id: require('./assignee_id'), 5 | title: require('./title'), 6 | content: require('./content'), 7 | latest_reply: require('./latest_reply'), 8 | current_user: require('./current_user'), 9 | } 10 | -------------------------------------------------------------------------------- /api/rule/condition/conditions/latest_reply.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | updated: () => { 3 | return (ctx) => ctx.ticket.isUpdated('latest_reply') 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /api/rule/condition/conditions/status.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | 3 | const { TICKET_STATUS } = require('../../../../lib/common') 4 | 5 | function assertValueIsValid(value) { 6 | if (typeof value !== 'number') { 7 | throw new Error('Value must be a number') 8 | } 9 | if (!(value in _.invert(TICKET_STATUS))) { 10 | throw new Error('Invalid value') 11 | } 12 | } 13 | 14 | module.exports = { 15 | is: (value) => { 16 | assertValueIsValid(value) 17 | return (ctx) => ctx.ticket.status === value 18 | }, 19 | is_not: (value) => { 20 | assertValueIsValid(value) 21 | return (ctx) => ctx.ticket.status !== value 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /api/rule/condition/conditions/title.js: -------------------------------------------------------------------------------- 1 | function assertValueIsValid(value) { 2 | if (typeof value !== 'string') { 3 | throw new Error('Value must be a string') 4 | } 5 | } 6 | 7 | module.exports = { 8 | contains: (value) => { 9 | assertValueIsValid(value) 10 | return (ctx) => ctx.ticket.title.includes(value) 11 | }, 12 | not_contains: (value) => { 13 | assertValueIsValid(value) 14 | return (ctx) => !ctx.ticket.title.includes(value) 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /api/rule/condition/conditions/update_type.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | is: (value) => { 3 | if (value !== 'create' && value !== 'update') { 4 | throw new Error('Value must be "create" or "update"') 5 | } 6 | return (ctx) => ctx.update_type === value 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /api/ticketField/constant.js: -------------------------------------------------------------------------------- 1 | const TYPES = ['dropdown', 'text', 'multi-line', 'multi-select', 'checkbox', 'radios', 'file'] 2 | const LOCALES = [ 3 | 'zh-cn', 4 | 'zh-tw', 5 | 'zh-hk', 6 | 'en', 7 | 'ja', 8 | 'ko', 9 | 'id', 10 | 'th', 11 | 'de', 12 | 'fr', 13 | 'ru', 14 | 'es', 15 | 'pt', 16 | 'tr', 17 | ] 18 | const REQUIRE_OPTIONS = ['dropdown', 'multi-select'] 19 | module.exports = { 20 | LOCALES, 21 | TYPES, 22 | REQUIRE_OPTIONS, 23 | } 24 | -------------------------------------------------------------------------------- /api/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {*} res 4 | * @param {*} count number 5 | * @returns res 6 | */ 7 | const TOTAL_COUNT_KEY = 'X-Total-Count' 8 | function responseAppendCount(res, count) { 9 | res.append(TOTAL_COUNT_KEY, count) 10 | res.append('Access-Control-Expose-Headers', TOTAL_COUNT_KEY) 11 | return res 12 | } 13 | 14 | module.exports = { 15 | responseAppendCount, 16 | } 17 | -------------------------------------------------------------------------------- /clientGlobalVar.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | 3 | const clientGlobalVars = {} 4 | 5 | /** 6 | * @param {string} key 7 | * @param {any} value 8 | */ 9 | function setClientGlobalVar(key, value) { 10 | if (clientGlobalVars[key] && typeof clientGlobalVars[key] === 'object') { 11 | _.merge(clientGlobalVars[key], value) 12 | } else { 13 | clientGlobalVars[key] = value 14 | } 15 | } 16 | 17 | /** 18 | * @param {Record} object 19 | */ 20 | function setClientGlobalVars(object) { 21 | _.merge(clientGlobalVars, object) 22 | } 23 | 24 | module.exports = { clientGlobalVars, setClientGlobalVar, setClientGlobalVars } 25 | -------------------------------------------------------------------------------- /config.webapp.js: -------------------------------------------------------------------------------- 1 | import { setConfig } from './modules/config' 2 | 3 | // Used in CustomerServiceStats. 4 | // 0/-1/-2/...: a week ends at 23:59:59 Sunday/Saturday/Friday/... 5 | setConfig('stats.offsetDays', 0) 6 | 7 | setConfig('weekendWarning.enabled', true) 8 | -------------------------------------------------------------------------------- /deploy/utils.mjs: -------------------------------------------------------------------------------- 1 | export const step = (name) => { 2 | console.log() 3 | console.log('---------------------------') 4 | console.log(name) 5 | console.log('---------------------------') 6 | } 7 | 8 | export const task = async (name, initializer) => { 9 | process.stdout.write(name) 10 | const result = await initializer() 11 | process.stdout.cursorTo(0) 12 | console.log(name + ' ✔') 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /docs/crm.md: -------------------------------------------------------------------------------- 1 | # CRM 2 | 3 | ## 开启 CRM 日志上报 4 | 5 | CRM 日志依赖 API 日志功能,需要同时设置以下环境变量 6 | 7 | ``` 8 | export ENABLE_API_LOG=1 9 | export ENABLE_CRM=1 10 | ``` 11 | -------------------------------------------------------------------------------- /in-app/v1/.env: -------------------------------------------------------------------------------- 1 | VITE_POLYFILL_SERVICE=https://polyfill.alicdn.com -------------------------------------------------------------------------------- /in-app/v1/.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js -------------------------------------------------------------------------------- /in-app/v1/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | extends: ['../../next/.eslintrc.js', '../../next/web/.eslintrc.js'], 5 | }; 6 | -------------------------------------------------------------------------------- /in-app/v1/.gitignore: -------------------------------------------------------------------------------- 1 | .env.local 2 | -------------------------------------------------------------------------------- /in-app/v1/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100 4 | } -------------------------------------------------------------------------------- /in-app/v1/README.md: -------------------------------------------------------------------------------- 1 | [设计稿](https://www.figma.com/file/NGvD8ipURqGG7mwJ7PYljQ/%E5%AE%A2%E6%9C%8D%E7%B3%BB%E7%BB%9F) 2 | 3 | ## 环境变量 4 | 5 | | Name | Required | Description | 6 | | ---------------------------------------------------- | -------- | ----------------------------------------------------------------------------- | 7 | | `VITE_LC_TICKET_HOST` | no | 用于检查是否为外链,用于在浏览器中打开等场景,默认为 `window.location.origin` | 8 | | `SENTRY_WEB_DSN`(for build) or `VITE_SENTRY_WEB_DSN` | no | Sentry DSN | 9 | -------------------------------------------------------------------------------- /in-app/v1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TapSupport 8 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /in-app/v1/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss/nesting'), 4 | require('tailwindcss'), 5 | require('autoprefixer'), 6 | require('postcss-preset-env')(), 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /in-app/v1/script/check_npm_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | version=$(npm -v) 4 | major_version=${version%.*.*} 5 | if [ "$major_version" != "8" ]; then 6 | echo 'Please install packages with npm@8' >&2 7 | echo '' >&2 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Categories/index.module.css: -------------------------------------------------------------------------------- 1 | .marker { 2 | &:before { 3 | content: ' '; 4 | @apply block m-auto bg-[#222] w-1 h-1 rounded-full; 5 | } 6 | 7 | @apply flex shrink-0 w-6 h-6 mr-1; 8 | } 9 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Home/index.module.css: -------------------------------------------------------------------------------- 1 | .topicItem{ 2 | display: inline-flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: space-between; 6 | text-decoration: none; 7 | @apply rounded mr-2 whitespace-nowrap focus:outline-none px-2 py-1 text-sm; 8 | } 9 | 10 | .topicItem::after{ 11 | content: attr(data-text); 12 | content: attr(data-text) / ""; 13 | height: 0; 14 | visibility: hidden; 15 | overflow: hidden; 16 | user-select: none; 17 | pointer-events: none; 18 | @apply font-bold; 19 | 20 | @media speech { 21 | display: none; 22 | } 23 | } -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/New/CustomForm/CustomField/Description.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | export interface DescriptionProps extends ComponentPropsWithoutRef<'div'> { 5 | error?: { 6 | message?: string; 7 | }; 8 | } 9 | 10 | export function Description({ children, className, error, ...props }: DescriptionProps) { 11 | if (!children && !error) { 12 | return null; 13 | } 14 | return ( 15 |
22 | {error?.message ?? children} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/New/CustomForm/CustomField/index.module.css: -------------------------------------------------------------------------------- 1 | .required { 2 | &::after { 3 | content: '*'; 4 | @apply text-red; 5 | position: absolute; 6 | line-height: 18px; 7 | padding-left: 2px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/New/CustomForm/FormNote.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown'; 2 | 3 | export interface FormNoteProps { 4 | content: string; 5 | } 6 | 7 | export function FormNote({ content }: FormNoteProps) { 8 | return ( 9 |
10 | 11 | {content} 12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/New/Form/Field/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | export type ErrorMessageProps = ComponentPropsWithoutRef<'div'>; 5 | 6 | export function ErrorMessage({ className, children, ...props }: ErrorMessageProps) { 7 | if (!children) { 8 | return null; 9 | } 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/New/Form/Field/utils.ts: -------------------------------------------------------------------------------- 1 | export function scrollIntoViewInNeeded(el: HTMLElement, headerId = 'page-header') { 2 | const header = document.getElementById(headerId); 3 | if (!header) { 4 | return; 5 | } 6 | const top = el.offsetTop - header.clientHeight; 7 | if (window.pageYOffset > top) { 8 | window.scrollTo({ top }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/New/Form/Group/index.module.css: -------------------------------------------------------------------------------- 1 | .required { 2 | &::after { 3 | content: '*'; 4 | @apply text-red; 5 | position: absolute; 6 | line-height: 18px; 7 | padding-left: 2px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/New/hooks/useCannotCreateMore.ts: -------------------------------------------------------------------------------- 1 | import { TicketResolvedStatus } from '../../Ticket'; 2 | import { useTickets } from '../../hooks/useTickets'; 3 | 4 | const MAX_OPEN_COUNT = import.meta.env.VITE_MAX_OPEN_COUNT; 5 | 6 | export function useCannotCreateMore() { 7 | const { data } = useTickets({ 8 | status: TicketResolvedStatus.unResolved, 9 | queryOptions: { 10 | enabled: !!MAX_OPEN_COUNT, 11 | suspense: true, 12 | }, 13 | }); 14 | 15 | const count = data?.pages[0]?.length ?? 0; 16 | 17 | return MAX_OPEN_COUNT ? count >= parseInt(MAX_OPEN_COUNT) : false; 18 | } 19 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/Ticket/Replies/index.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | @apply m-2 bg-transparent break-all; 3 | 4 | /* overwrite markdown-body changes */ 5 | font-size: unset; 6 | line-height: unset; 7 | color: unset; 8 | p { 9 | margin-bottom: 1em; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/Ticket/index.module.css: -------------------------------------------------------------------------------- 1 | .status { 2 | &.new, &.waitForStaff { 3 | @apply text-tapBlue; 4 | } 5 | &.waitForCustomer { 6 | @apply text-amber; 7 | } 8 | &.resolved { 9 | @apply text-green; 10 | } 11 | } 12 | 13 | .dataGrid { 14 | display: grid; 15 | grid-template-columns: auto 1fr; 16 | } 17 | -------------------------------------------------------------------------------- /in-app/v1/src/App/Tickets/index.module.css: -------------------------------------------------------------------------------- 1 | .tab { 2 | @apply mr-5 pb-3 relative; 3 | 4 | &.active { 5 | @apply text-[#222] font-bold; 6 | &::after { 7 | content:''; 8 | } 9 | } 10 | 11 | &::after { 12 | 13 | position: absolute; 14 | display:block; 15 | bottom: 0; 16 | left: 50%; 17 | transform:translate(-50%); 18 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); 19 | border-radius: 2px; 20 | @apply bg-tapBlue w-[24px] h-[3px]; 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /in-app/v1/src/api/article.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/leancloud'; 2 | import { Article } from '@/types'; 3 | import { useQuery, UseQueryOptions } from 'react-query'; 4 | 5 | async function getArticle(id: string, locale?: string) { 6 | return (await http.get
(`/api/2/articles/${id}`, { params: { locale } })).data; 7 | } 8 | 9 | export function useArticle(id: string, options?: UseQueryOptions) { 10 | return useQuery({ 11 | queryKey: ['article', id], 12 | queryFn: () => getArticle(id), 13 | staleTime: 60_000, 14 | ...options, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /in-app/v1/src/api/evaluation.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query'; 2 | 3 | import { http } from '@/leancloud'; 4 | import { EvaluationTag } from '@/types'; 5 | 6 | export async function getEvaluationTag() { 7 | const res = await http.get('/api/2/evaluation-tag'); 8 | return res.data; 9 | } 10 | 11 | export function useEvaluationTag() { 12 | return useQuery({ 13 | queryKey: ['EvaluationTag'], 14 | queryFn: getEvaluationTag, 15 | staleTime: Infinity, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /in-app/v1/src/components/Button/index.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | &:disabled { 3 | cursor: default; 4 | opacity: 0.6; 5 | } 6 | 7 | &.primary { 8 | box-shadow: inset 0px -3px 0px rgba(0, 0, 0, 0.1); 9 | 10 | &:active:enabled { 11 | box-shadow: none; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /in-app/v1/src/components/ControlButton/index.module.css: -------------------------------------------------------------------------------- 1 | .shadow { 2 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.12); 3 | } 4 | -------------------------------------------------------------------------------- /in-app/v1/src/components/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import * as Sentry from '@sentry/react'; 3 | 4 | export class ErrorBoundary extends Component { 5 | state: { error?: Error } = {}; 6 | 7 | static getDerivedStateFromError(error: Error) { 8 | return { error }; 9 | } 10 | 11 | componentDidCatch(error: Error) { 12 | Sentry.captureException(error); 13 | } 14 | 15 | render() { 16 | if (this.state.error) { 17 | return ( 18 |
19 |

Something went wrong.

20 |
{this.state.error.message}
21 |
22 | ); 23 | } 24 | return this.props.children; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /in-app/v1/src/components/FileItem/index.module.css: -------------------------------------------------------------------------------- 1 | .progress { 2 | height: 2px; 3 | } 4 | -------------------------------------------------------------------------------- /in-app/v1/src/components/Form/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithRef, forwardRef } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | export type InputProps = ComponentPropsWithRef<'input'>; 5 | 6 | export const Input = forwardRef(({ className, ...props }, ref) => ( 7 | 16 | )); 17 | -------------------------------------------------------------------------------- /in-app/v1/src/components/Form/Radio/index.module.css: -------------------------------------------------------------------------------- 1 | .radio { 2 | position: relative; 3 | 4 | &:checked:before { 5 | @apply absolute bg-tapBlue rounded-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2; 6 | content: ''; 7 | width: 6px; 8 | height: 6px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /in-app/v1/src/components/Form/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Input'; 2 | export * from './Radio'; 3 | export * from './Checkbox'; 4 | -------------------------------------------------------------------------------- /in-app/v1/src/components/Loading/index.module.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | animation-name: spin; 3 | animation-duration: 700ms; 4 | animation-iteration-count: infinite; 5 | animation-timing-function: linear; 6 | } 7 | 8 | @keyframes spin { 9 | from { 10 | transform:rotate(0deg); 11 | } 12 | to { 13 | transform:rotate(360deg); 14 | } 15 | } -------------------------------------------------------------------------------- /in-app/v1/src/components/NewTicketButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { Button } from '../Button'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | export function NewTicketButton({ categoryId }: { categoryId: string }) { 6 | const { t } = useTranslation(); 7 | return ( 8 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /in-app/v1/src/components/NoData/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | export interface NoDataProps { 5 | message?: string | ReactNode; 6 | } 7 | 8 | export function NoData({ message }: NoDataProps) { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 |
13 | {message || t('general.no_data')} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /in-app/v1/src/components/Page/index.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: -webkit-sticky; 3 | position: sticky; 4 | 5 | } 6 | 7 | .contentShadow { 8 | box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.08), inset 0px -2px 0px #F2F2F2; 9 | } -------------------------------------------------------------------------------- /in-app/v1/src/components/Preview/index.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.25), 0px 4px 12px rgba(0, 0, 0, 0.25); 3 | } 4 | -------------------------------------------------------------------------------- /in-app/v1/src/components/SDK/api/button.ts: -------------------------------------------------------------------------------- 1 | import { callHandlerPromise } from './utils'; 2 | 3 | /** 4 | * 检查间隔 5 | * */ 6 | const CHECK_INTERVAL = 3000; 7 | 8 | /** 9 | * 防止死循环检查定时器 10 | * 当程序出现task占满时将无法执行定时器,则释放关闭按钮 11 | * 防止用户无法退出webview 12 | * */ 13 | let checkTimer: number | null = null; 14 | 15 | const _showCloseButton = () => callHandlerPromise('showCloseButton'); 16 | const _hideCloseButton = (duration = 3000) => callHandlerPromise('hideCloseButton', { duration }); 17 | 18 | /** 19 | * 显示右上角关闭按钮 20 | * */ 21 | export function showCloseButton() { 22 | if (checkTimer) { 23 | clearTimeout(checkTimer); 24 | } 25 | _showCloseButton(); 26 | } 27 | 28 | /** 29 | * 隐藏右上角关闭按钮 30 | * */ 31 | export function hideCloseButton() { 32 | _hideCloseButton(CHECK_INTERVAL + 1000); 33 | clearTimeout(checkTimer as number); 34 | checkTimer = setTimeout(() => hideCloseButton(), CHECK_INTERVAL) as any; 35 | } 36 | -------------------------------------------------------------------------------- /in-app/v1/src/components/SDK/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './webView'; 3 | export * from './Context'; 4 | -------------------------------------------------------------------------------- /in-app/v1/src/components/SDK/webView/index.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | WVJBCallbacks?: any[]; 4 | WebViewJavascriptBridge?: Bridge; 5 | webViewJavascriptInterface?: { 6 | notice: (message: string) => void; 7 | }; 8 | } 9 | } 10 | 11 | export const registerHandler: (name: string, registerCallback: (data: any) => void) => void; 12 | 13 | export const callHandler: (name: string, params: any, callback: (data: any) => void) => void; 14 | -------------------------------------------------------------------------------- /in-app/v1/src/components/SpaceChinese/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | const CHINESE_REGEX = /[\u4E00-\u9FA5]+/; 4 | 5 | // @ts-ignore 6 | export const SpaceChinese = memo(({ children }: { children: string }) => { 7 | if (children.length === 2 && CHINESE_REGEX.test(children)) { 8 | return children.split('').join(' '); 9 | } 10 | return children; 11 | }); 12 | -------------------------------------------------------------------------------- /in-app/v1/src/components/Time/index.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef } from 'react'; 2 | 3 | function addZeroPrefix(value: number, width: number): string { 4 | const str = value + ''; 5 | if (str.length >= width) { 6 | return str; 7 | } 8 | return '0'.repeat(width - str.length) + str; 9 | } 10 | 11 | export interface TimeProps extends ComponentPropsWithoutRef<'span'> { 12 | value: Date; 13 | } 14 | 15 | export function Time({ value, ...props }: TimeProps) { 16 | const year = value.getFullYear(); 17 | const month = addZeroPrefix(value.getMonth() + 1, 2); 18 | const date = addZeroPrefix(value.getDate(), 2); 19 | const hour = addZeroPrefix(value.getHours(), 2); 20 | const minute = addZeroPrefix(value.getMinutes(), 2); 21 | const second = addZeroPrefix(value.getSeconds(), 2); 22 | return {`${year}-${month}-${date} ${hour}:${minute}:${second}`}; 23 | } 24 | -------------------------------------------------------------------------------- /in-app/v1/src/env/index.ts: -------------------------------------------------------------------------------- 1 | import { LocalStorage } from './local-storage'; 2 | 3 | export const localStorage = window.localStorage ?? new LocalStorage(); 4 | -------------------------------------------------------------------------------- /in-app/v1/src/env/local-storage.ts: -------------------------------------------------------------------------------- 1 | export class LocalStorage implements Storage { 2 | readonly data = new Map(); 3 | 4 | get length() { 5 | return this.data.size; 6 | } 7 | 8 | clear() { 9 | this.data.clear(); 10 | } 11 | 12 | getItem(key: string) { 13 | return this.data.get(key) ?? null; 14 | } 15 | 16 | key(index: number) { 17 | if (index < 0 || index >= this.data.size) { 18 | return null; 19 | } 20 | const keys = this.data.keys(); 21 | for (let i = 0; i < index; ++i) { 22 | keys.next(); 23 | } 24 | return keys.next().value; 25 | } 26 | 27 | removeItem(key: string) { 28 | this.data.delete(key); 29 | } 30 | 31 | setItem(key: string, value: string) { 32 | this.data.set(key, value); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/Back.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function BackIcon(props: React.SVGProps) { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | } 23 | 24 | export default BackIcon; 25 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/Check.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function CheckIcon(props: React.SVGProps) { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | } 23 | 24 | export default CheckIcon; 25 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/Clip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function ClipIcon(props: React.SVGProps) { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | } 23 | 24 | export default ClipIcon; 25 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/Done.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const DoneIcon = (props: SVGProps) => ( 4 | 12 | 13 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/EditableIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const EditableIcon = (props: SVGProps) => ( 4 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function HomeIcon(props: React.SVGProps) { 4 | return ( 5 | 13 | 19 | 26 | 27 | ); 28 | } 29 | 30 | export default HomeIcon; 31 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/LoadingIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leancloud/ticket/634cbb110ffdc99e58669eea53b33752f321e7ea/in-app/v1/src/icons/LoadingIcon.png -------------------------------------------------------------------------------- /in-app/v1/src/icons/Plus.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function PlusIcon(props: React.SVGProps) { 4 | return ( 5 | 13 | 14 | 20 | 21 | ); 22 | } 23 | 24 | export default PlusIcon; 25 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/Speaker.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const SpeakerIcon = (props: SVGProps) => ( 4 | 12 | 19 | 20 | ); 21 | 22 | export default SpeakerIcon; 23 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/ThumbDown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function ThumbDown(props: React.SVGProps) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | 21 | export default ThumbDown; 22 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/ThumbUp.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function ThumbUp(props: React.SVGProps) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | 21 | export default ThumbUp; 22 | -------------------------------------------------------------------------------- /in-app/v1/src/icons/X.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function XIcon(props: React.SVGProps) { 4 | return ( 5 | 13 | 20 | 21 | ); 22 | } 23 | 24 | export default XIcon; 25 | -------------------------------------------------------------------------------- /in-app/v1/src/states/app.ts: -------------------------------------------------------------------------------- 1 | import { atom, useRecoilState } from 'recoil'; 2 | 3 | interface AppState { 4 | topicIndex: number; 5 | historyIndex: number; 6 | } 7 | 8 | const appState = atom({ 9 | key: 'app', 10 | default: { 11 | topicIndex: 0, 12 | historyIndex: 0, 13 | }, 14 | }); 15 | 16 | export function useAppState() { 17 | return useRecoilState(appState); 18 | } 19 | -------------------------------------------------------------------------------- /in-app/v1/src/states/auth.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'open-leancloud-storage/auth'; 2 | import { atom, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | 4 | interface AuthState { 5 | user?: User; 6 | loading?: boolean; 7 | error?: Error; 8 | } 9 | 10 | const authState = atom({ 11 | key: 'auth', 12 | default: { 13 | loading: true, 14 | }, 15 | }); 16 | 17 | export function useSetAuth() { 18 | return useSetRecoilState(authState); 19 | } 20 | 21 | export function useAuth() { 22 | return useRecoilValue(authState); 23 | } 24 | -------------------------------------------------------------------------------- /in-app/v1/src/states/root-category.ts: -------------------------------------------------------------------------------- 1 | import { Category } from '@/api/category'; 2 | import { atom, useRecoilValue, useSetRecoilState } from 'recoil'; 3 | 4 | const rootCategoryState = atom({ 5 | key: 'rootCategory', 6 | }); 7 | 8 | export function useSetRootCategory() { 9 | return useSetRecoilState(rootCategoryState); 10 | } 11 | 12 | export function useRootCategory() { 13 | return useRecoilValue(rootCategoryState); 14 | } 15 | -------------------------------------------------------------------------------- /in-app/v1/src/states/ticket-info.ts: -------------------------------------------------------------------------------- 1 | import { atom, useRecoilValue, useSetRecoilState } from 'recoil'; 2 | 3 | interface TicketInfo { 4 | meta?: Record; 5 | fields?: Record; 6 | } 7 | 8 | const ticketInfoState = atom({ 9 | key: 'ticketInfo', 10 | default: {}, 11 | }); 12 | 13 | export function useSetTicketInfo() { 14 | return useSetRecoilState(ticketInfoState); 15 | } 16 | 17 | export function useTicketInfo() { 18 | return useRecoilValue(ticketInfoState); 19 | } 20 | -------------------------------------------------------------------------------- /in-app/v1/src/utils/base64.ts: -------------------------------------------------------------------------------- 1 | export function utf8_to_b64(str: string) { 2 | return window.btoa(unescape(encodeURIComponent(str))); 3 | } 4 | 5 | export function b64_to_utf8(str: string) { 6 | return decodeURIComponent(escape(window.atob(str))); 7 | } 8 | -------------------------------------------------------------------------------- /in-app/v1/src/utils/screen.ts: -------------------------------------------------------------------------------- 1 | export const checkIsLandScreen = () => { 2 | const orientation = window.screen.orientation?.type; 3 | if (orientation) { 4 | return orientation === 'landscape-primary'; 5 | } 6 | 7 | return window.innerHeight === window.innerWidth 8 | ? document.documentElement.clientHeight < document.documentElement.clientWidth 9 | : window.innerHeight <= window.innerWidth; 10 | }; 11 | -------------------------------------------------------------------------------- /in-app/v1/src/utils/url.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | export function useSearchParams(): Record { 5 | const { search } = useLocation(); 6 | const params = useMemo(() => { 7 | const params = new URLSearchParams(search); 8 | return Array.from(params.entries()).reduce((map, [key, value]) => { 9 | map[key] = value; 10 | return map; 11 | }, {} as Record); 12 | }, [search]); 13 | return params; 14 | } 15 | -------------------------------------------------------------------------------- /in-app/v1/src/utils/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function useIsMounted() { 4 | const $mounted = useRef(false); 5 | const $test = useRef(() => $mounted.current); 6 | useEffect(() => { 7 | $mounted.current = true; 8 | return () => { 9 | $mounted.current = false; 10 | }; 11 | }, []); 12 | return $test.current; 13 | } 14 | -------------------------------------------------------------------------------- /in-app/v1/src/vite.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | VITE_LEANCLOUD_APP_ID: string; 5 | VITE_LEANCLOUD_APP_KEY: string; 6 | VITE_LEANCLOUD_API_HOST: string; 7 | VITE_LC_TICKET_HOST?: string; 8 | VITE_ALLOW_MUTATE_EVALUATION: string; 9 | VITE_SENTRY_WEB_DSN?: string; 10 | VITE_MAX_OPEN_COUNT?: string; 11 | } 12 | -------------------------------------------------------------------------------- /in-app/v1/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 3 | theme: { 4 | extend: { 5 | colors: { 6 | tapBlue: { 7 | DEFAULT: '#00d9c5', 8 | 100: '#fafefe', 9 | 200: '#edfcfb', 10 | 300: '#e6fbf9', 11 | 400: '#c2f6f1', 12 | 500: '#9cf0e8', 13 | 600: '#33e1d1', 14 | 700: '#00d9c5', 15 | 800: '#00bfad', 16 | }, 17 | red: { 18 | DEFAULT: '#F64C4C', 19 | }, 20 | green: { 21 | DEFAULT: '#47b881', 22 | }, 23 | amber: { 24 | DEFAULT: '#ff7f4f', 25 | }, 26 | }, 27 | fontSize: { 28 | xs: ['12px', '15px'], 29 | sm: ['12px', '18px'], 30 | base: ['14px', '21px'], 31 | }, 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /in-app/v1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictNullChecks": true, 5 | "esModuleInterop": true, 6 | "module": "ES2020", 7 | "target": "ES2020", 8 | "moduleResolution": "Node", 9 | "jsx": "react-jsx", 10 | "isolatedModules": true, 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["src/*"], 14 | }, 15 | "typeRoots": ["./node_modules/@types"], 16 | "skipLibCheck": true, 17 | "resolveJsonModule": true 18 | }, 19 | "include": ["src/**/*"] 20 | } -------------------------------------------------------------------------------- /in-app/v1/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import legacy from '@vitejs/plugin-legacy'; 4 | import path from 'path'; 5 | 6 | export default defineConfig({ 7 | base: '/in-app/v1/', 8 | plugins: [ 9 | react(), 10 | legacy({ 11 | targets: ['defaults', 'not IE 11', 'Android 56', 'iOS 10'], 12 | }), 13 | ], 14 | server: { 15 | port: 8081, 16 | proxy: { 17 | '/api': 'http://127.0.0.1:3000', 18 | }, 19 | }, 20 | resolve: { 21 | alias: { 22 | '@': path.resolve('./src'), 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { BrowserRouter } from 'react-router-dom' 4 | import { QueryClient, QueryClientProvider } from 'react-query' 5 | 6 | import App from './modules/App' 7 | import './config.webapp' 8 | 9 | if (process.env.NODE_ENV !== 'production') { 10 | // window.ENABLE_LEANCLOUD_INTEGRATION = false 11 | window.USE_LC_OAUTH = false 12 | } 13 | 14 | const queryClient = new QueryClient({ 15 | defaultOptions: { 16 | queries: { 17 | refetchOnWindowFocus: false, 18 | }, 19 | }, 20 | }) 21 | 22 | render( 23 | 24 | 25 | 26 | 27 | , 28 | document.getElementById('app') 29 | ) 30 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "lib": ["lib/*"], 6 | "modules": ["modules/*"] 7 | } 8 | }, 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /leanengine.yaml: -------------------------------------------------------------------------------- 1 | exposeEnvironmentsOnBuild: true 2 | install: 3 | - use: default 4 | - require: 5 | - ./package.json 6 | - ./package-lock.json 7 | - ./in-app/v1/package.json 8 | - ./in-app/v1/package-lock.json 9 | - ./next/api/package.json 10 | - ./next/api/package-lock.json 11 | - ./next/web/package.json 12 | - ./next/web/package-lock.json 13 | - (cd in-app/v1/ && npm ci) 14 | - (cd next/api && npm ci) 15 | - (cd next/web && npm ci) 16 | build: 17 | - npm run build 18 | - (cd in-app/v1/ && npm run build) 19 | - (cd next/api && npm run build) 20 | - (cd next/web && npm run build) 21 | -------------------------------------------------------------------------------- /migrations/1511857499214-add-avatar.js: -------------------------------------------------------------------------------- 1 | const AV = require('leanengine') 2 | 3 | const { getGravatarHash } = require('../lib/common') 4 | const forEachAVObject = require('../api/common').forEachAVObject 5 | 6 | exports.up = function (next) { 7 | return forEachAVObject(new AV.Query('_User'), (user) => { 8 | return user 9 | .set('gravatarHash', getGravatarHash(user.get('email'))) 10 | .save(null, { useMasterKey: true }) 11 | }) 12 | .then(() => { 13 | return next() 14 | }) 15 | .catch(next) 16 | } 17 | 18 | exports.down = function (next) { 19 | return forEachAVObject(new AV.Query('_User'), (user) => { 20 | return user.unset('gravatarHash').save(null, { useMasterKey: true }) 21 | }) 22 | .then(() => { 23 | return next() 24 | }) 25 | .catch(next) 26 | } 27 | -------------------------------------------------------------------------------- /modules/About.js: -------------------------------------------------------------------------------- 1 | /*global BRAND_NAME */ 2 | import React from 'react' 3 | import { useTranslation } from 'react-i18next' 4 | 5 | import { useTitle } from './utils/hooks' 6 | 7 | export default function About() { 8 | const { t } = useTranslation() 9 | useTitle(t('about')) 10 | 11 | return ( 12 |
13 |

{BRAND_NAME}

14 |
15 |

16 | {t('lightweight')} {t('oss')} {t('intro')} 17 |

18 |

19 | {t('builtWith')} 20 | LeanCloud 21 | {t('builtWithEnding')} 22 |

23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /modules/App.css: -------------------------------------------------------------------------------- 1 | .main h1 { 2 | font-size: 32px; 3 | } 4 | 5 | .main h1 small { 6 | font-weight: 300; 7 | } 8 | -------------------------------------------------------------------------------- /modules/CustomerService/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Switch, useRouteMatch } from 'react-router-dom' 3 | 4 | import CSTickets from './Tickets' 5 | 6 | export { useCustomerServices } from './useCustomerServices' 7 | 8 | export default function CustomerService(props) { 9 | const { path } = useRouteMatch() 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /modules/CustomerService/index.module.scss: -------------------------------------------------------------------------------- 1 | .previewTip { 2 | position: fixed; 3 | right: 10px; 4 | bottom: 10px; 5 | 6 | :global { 7 | .close { 8 | position: absolute; 9 | top: 0; 10 | right: 0; 11 | width: 18px; 12 | height: 18px; 13 | line-height: 18px; 14 | font-size: 18px; 15 | } 16 | 17 | .card-text { 18 | margin-bottom: 10px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/CustomerService/useCustomerServices.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { auth } from '../../lib/leancloud' 3 | 4 | let getRoleTask 5 | export async function getCustomerServices() { 6 | if (!getRoleTask) { 7 | getRoleTask = auth.queryRole().where('name', '==', 'customerService').first() 8 | } 9 | const role = await getRoleTask 10 | const users = await role.queryUser().orderBy('username').find() 11 | return users.map((u) => u.toJSON()) 12 | } 13 | 14 | export function useCustomerServices() { 15 | const [customerServices, setCustomerServices] = useState([]) 16 | useEffect(() => { 17 | getCustomerServices().then(setCustomerServices).catch(console.error) 18 | }, []) 19 | return customerServices 20 | } 21 | -------------------------------------------------------------------------------- /modules/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react' 2 | import { auth } from '../lib/leancloud' 3 | import { useHistory } from 'react-router-dom' 4 | import { AppContext } from './context' 5 | 6 | export default function Home() { 7 | const history = useHistory() 8 | const { isUser } = useContext(AppContext) 9 | 10 | useEffect(() => { 11 | if (!auth.currentUser) { 12 | history.replace('/login') 13 | return 14 | } 15 | if (!isUser) { 16 | return window.location.replace(`/next/admin/`) 17 | } 18 | history.replace('/tickets') 19 | }, [history, isUser]) 20 | 21 | return
Home
22 | } 23 | -------------------------------------------------------------------------------- /modules/Login.css: -------------------------------------------------------------------------------- 1 | .wrap { 2 | max-width: 360px; 3 | margin: 20px auto; 4 | } 5 | -------------------------------------------------------------------------------- /modules/NewTicket.next.css: -------------------------------------------------------------------------------- 1 | :global(html), :global(body) { 2 | height: 100%; 3 | } 4 | 5 | :global(#app) { 6 | height: 100%; 7 | display: flex; 8 | } 9 | -------------------------------------------------------------------------------- /modules/NewTicket.next.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | 4 | import './NewTicket.next.css' 5 | 6 | export default function NewTicket() { 7 | const history = useHistory() 8 | const frame = useRef(null) 9 | 10 | useEffect(() => { 11 | const h = ({ data }) => { 12 | if (data === 'ticketCreated') { 13 | setTimeout(() => history.push('/tickets'), 1000) 14 | } else if (data === 'requireAuth') { 15 | history.push('/login') 16 | } 17 | } 18 | frame.current.contentWindow.addEventListener('message', h) 19 | }, []) 20 | 21 | return