This commit is contained in:
		
							parent
							
								
									986ab4d463
								
							
						
					
					
						commit
						415d35f6e2
					
				|  | @ -0,0 +1,71 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"myproject/tools" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/gorilla/sessions" | ||||
| 	"github.com/labstack/echo/v4" | ||||
| ) | ||||
| 
 | ||||
| func NewAuthStore() *sessions.CookieStore { | ||||
| 	auth := make([]byte, 32) | ||||
| 	_, _ = rand.Read(auth) | ||||
| 	enc := make([]byte, 16) | ||||
| 	_, _ = rand.Read(enc) | ||||
| 	s := sessions.NewCookieStore(auth, enc) | ||||
| 	s.Options.Secure = false | ||||
| 	s.Options.SameSite = http.SameSiteDefaultMode | ||||
| 	s.MaxAge(3600) | ||||
| 	return s | ||||
| } | ||||
| 
 | ||||
| func setAuth(onlyAdmin bool, g *echo.Group) *echo.Group { | ||||
| 	g.Use( | ||||
| 		func(next echo.HandlerFunc) echo.HandlerFunc { | ||||
| 			return func(c echo.Context) error { | ||||
| 				sess, err := c.Get("authStore").(*sessions.CookieStore).New(c.Request(), tools.SessionName) | ||||
| 				if err != nil { | ||||
| 					// journal.Debug(ctx, commerr.Trace(err).Error())
 | ||||
| 				} | ||||
| 
 | ||||
| 				userName := sess.Values[tools.UserNameSessionKey] | ||||
| 				if userName == nil { | ||||
| 					return echo.ErrUnauthorized | ||||
| 				} | ||||
| 
 | ||||
| 				email, lp, domain := SplitEmail(userName.(string)) | ||||
| 				c.Set(tools.UserCtxKey, email) | ||||
| 				c.Set(tools.LpCtxKey, lp) | ||||
| 				c.Set(tools.DomainCtxKey, domain) | ||||
| 
 | ||||
| 				return next(c) | ||||
| 			} | ||||
| 		}, | ||||
| 		func(next echo.HandlerFunc) echo.HandlerFunc { | ||||
| 			return func(c echo.Context) error { | ||||
| 				if onlyAdmin && tools.GetUser(c) != "admin" { | ||||
| 					return echo.ErrUnauthorized | ||||
| 				} | ||||
| 
 | ||||
| 				return next(c) | ||||
| 			} | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	return g | ||||
| } | ||||
| 
 | ||||
| func SplitEmail(toSplit string) (email, user, domain string) { | ||||
| 	email = strings.TrimSpace(toSplit) | ||||
| 	email = strings.ToLower(email) | ||||
| 	parts := strings.Split(email, "@") | ||||
| 	user = parts[0] | ||||
| 	if len(parts) > 1 { | ||||
| 		domain = parts[1] | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
|  | @ -0,0 +1,98 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"myproject/tools" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/labstack/echo/v4" | ||||
| 	"github.com/labstack/echo/v4/middleware" | ||||
| ) | ||||
| 
 | ||||
| func NewEcho(ctx context.Context, restartMode *bool) *echo.Echo { | ||||
| 	timeout := 5 * time.Minute | ||||
| 	e := echo.New() | ||||
| 	e.HideBanner = true | ||||
| 	e.Server.ReadTimeout = timeout | ||||
| 	e.Server.WriteTimeout = timeout | ||||
| 	e.Server.BaseContext = func(listener net.Listener) context.Context { | ||||
| 		return ctx | ||||
| 	} | ||||
| 	e.HTTPErrorHandler = func(err error, c echo.Context) { | ||||
| 		httpError, ok := err.(*echo.HTTPError) | ||||
| 		if ok { | ||||
| 			errorCode := httpError.Code | ||||
| 			switch errorCode { | ||||
| 			case http.StatusServiceUnavailable: | ||||
| 				tools.Serve503(c) | ||||
| 			case http.StatusTooManyRequests: | ||||
| 				tools.Serve429(c) | ||||
| 			case http.StatusForbidden: | ||||
| 				tools.Serve403(c) | ||||
| 			case http.StatusUnauthorized: | ||||
| 				tools.Serve401(c) | ||||
| 			case http.StatusNotFound, http.StatusMethodNotAllowed: | ||||
| 				if strings.HasPrefix(c.Request().RequestURI, "/backend") { | ||||
| 					switch errorCode { | ||||
| 					case http.StatusNotFound: | ||||
| 						tools.Serve404(c) | ||||
| 					case http.StatusMethodNotAllowed: | ||||
| 						tools.Serve405(c) | ||||
| 					} | ||||
| 					return | ||||
| 				} | ||||
| 				c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/?redirect=%s", c.Request().RequestURI)) | ||||
| 			default: | ||||
| 				tools.Serve500(c) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	authStore := NewAuthStore() | ||||
| 
 | ||||
| 	e.Use( | ||||
| 		middleware.CORSWithConfig(middleware.CORSConfig{ | ||||
| 			AllowOrigins: []string{"http://localhost:7777", "http://localhost:8808"}, | ||||
| 			AllowHeaders: []string{ | ||||
| 				echo.HeaderOrigin, | ||||
| 				echo.HeaderContentType, | ||||
| 				echo.HeaderAccept, | ||||
| 				echo.HeaderAccessControlAllowOrigin, | ||||
| 				echo.HeaderAccessControlAllowCredentials, | ||||
| 				echo.HeaderAccessControlAllowHeaders, | ||||
| 				echo.HeaderAccessControlRequestHeaders, | ||||
| 				echo.HeaderAuthorization, | ||||
| 			}, | ||||
| 			AllowCredentials: true, | ||||
| 		}), | ||||
| 		middleware.GzipWithConfig(middleware.GzipConfig{ | ||||
| 			Level: 6, | ||||
| 		}), | ||||
| 		func(next echo.HandlerFunc) echo.HandlerFunc { | ||||
| 			return func(c echo.Context) error { | ||||
| 				c.Set("authStore", authStore) | ||||
| 
 | ||||
| 				return next(c) | ||||
| 			} | ||||
| 		}, | ||||
| 		func(next echo.HandlerFunc) echo.HandlerFunc { | ||||
| 			return func(c echo.Context) error { | ||||
| 
 | ||||
| 				return next(c) | ||||
| 			} | ||||
| 		}, | ||||
| 		func(next echo.HandlerFunc) echo.HandlerFunc { | ||||
| 			return func(c echo.Context) error { | ||||
| 				c.Set(tools.RootCtxKey, ctx) | ||||
| 
 | ||||
| 				return next(c) | ||||
| 			} | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	return e | ||||
| } | ||||
|  | @ -0,0 +1 @@ | |||
| VITE_API_URL=http://localhost:8888 | ||||
|  | @ -0,0 +1 @@ | |||
| VITE_API_URL= | ||||
|  | @ -0,0 +1,29 @@ | |||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
| 
 | ||||
| node_modules | ||||
| .DS_Store | ||||
| dist-ssr | ||||
| coverage | ||||
| *.local | ||||
| 
 | ||||
| /cypress/videos/ | ||||
| /cypress/screenshots/ | ||||
| 
 | ||||
| # Editor directories and files | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| .idea | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
| 
 | ||||
| *.tsbuildinfo | ||||
|  | @ -1,23 +1,33 @@ | |||
| # Vue 3 + TypeScript + Vite | ||||
| # js | ||||
| 
 | ||||
| This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue | ||||
| 3 `<script setup>` SFCs, check out | ||||
| the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. | ||||
| This template should help get you started developing with Vue 3 in Vite. | ||||
| 
 | ||||
| ## Recommended IDE Setup | ||||
| 
 | ||||
| - [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) | ||||
| [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). | ||||
| 
 | ||||
| ## Type Support For `.vue` Imports in TS | ||||
| ## Type Support for `.vue` Imports in TS | ||||
| 
 | ||||
| Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type | ||||
| by default. In most cases this is fine if you don't really care about component prop types outside of templates. | ||||
| However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using | ||||
| manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps: | ||||
| TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. | ||||
| 
 | ||||
| 1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look | ||||
|    for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, | ||||
|    Take Over mode will enable itself if the default TypeScript extension is disabled. | ||||
| 2. Reload the VS Code window by running `Developer: Reload Window` from the command palette. | ||||
| ## Customize configuration | ||||
| 
 | ||||
| You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471). | ||||
| See [Vite Configuration Reference](https://vitejs.dev/config/). | ||||
| 
 | ||||
| ## Project Setup | ||||
| 
 | ||||
| ```sh | ||||
| npm install | ||||
| ``` | ||||
| 
 | ||||
| ### Compile and Hot-Reload for Development | ||||
| 
 | ||||
| ```sh | ||||
| npm run dev | ||||
| ``` | ||||
| 
 | ||||
| ### Type-Check, Compile and Minify for Production | ||||
| 
 | ||||
| ```sh | ||||
| npm run build | ||||
| ``` | ||||
|  |  | |||
|  | @ -0,0 +1,115 @@ | |||
| /* eslint-disable */ | ||||
| // @ts-nocheck
 | ||||
| // Generated by unplugin-vue-components
 | ||||
| // Read more: https://github.com/vuejs/core/pull/3399
 | ||||
| export {} | ||||
| 
 | ||||
| /* prettier-ignore */ | ||||
| declare module 'vue' { | ||||
|   export interface GlobalComponents { | ||||
|     AAlert: typeof import('ant-design-vue/es')['Alert'] | ||||
|     ABadge: typeof import('ant-design-vue/es')['Badge'] | ||||
|     AButton: typeof import('ant-design-vue/es')['Button'] | ||||
|     ACalendar: typeof import('ant-design-vue/es')['Calendar'] | ||||
|     ACard: typeof import('ant-design-vue/es')['Card'] | ||||
|     ACardGrid: typeof import('ant-design-vue/es')['CardGrid'] | ||||
|     ACol: typeof import('ant-design-vue/es')['Col'] | ||||
|     ACollapse: typeof import('ant-design-vue/es')['Collapse'] | ||||
|     ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] | ||||
|     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] | ||||
|     Actions: typeof import('./src/components/common/rules/Actions.vue')['default'] | ||||
|     ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] | ||||
|     Add: typeof import('./src/components/admin/domains/Add.vue')['default'] | ||||
|     AddressBooks: typeof import('./src/components/common/AddressBooks.vue')['default'] | ||||
|     AddressRules: typeof import('./src/components/admin/settings/AddressRules.vue')['default'] | ||||
|     ADescriptions: typeof import('ant-design-vue/es')['Descriptions'] | ||||
|     ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem'] | ||||
|     ADivider: typeof import('ant-design-vue/es')['Divider'] | ||||
|     ADropdown: typeof import('ant-design-vue/es')['Dropdown'] | ||||
|     AFlex: typeof import('ant-design-vue/es')['Flex'] | ||||
|     AForm: typeof import('ant-design-vue/es')['Form'] | ||||
|     AFormItem: typeof import('ant-design-vue/es')['FormItem'] | ||||
|     AInput: typeof import('ant-design-vue/es')['Input'] | ||||
|     AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] | ||||
|     AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] | ||||
|     AInputSearch: typeof import('ant-design-vue/es')['InputSearch'] | ||||
|     AList: typeof import('ant-design-vue/es')['List'] | ||||
|     AListItem: typeof import('ant-design-vue/es')['ListItem'] | ||||
|     AMenu: typeof import('ant-design-vue/es')['Menu'] | ||||
|     AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] | ||||
|     AModal: typeof import('ant-design-vue/es')['Modal'] | ||||
|     APagination: typeof import('ant-design-vue/es')['Pagination'] | ||||
|     APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] | ||||
|     AProgress: typeof import('ant-design-vue/es')['Progress'] | ||||
|     ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] | ||||
|     ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] | ||||
|     ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] | ||||
|     AResult: typeof import('ant-design-vue/es')['Result'] | ||||
|     ARow: typeof import('ant-design-vue/es')['Row'] | ||||
|     ASelect: typeof import('ant-design-vue/es')['Select'] | ||||
|     ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] | ||||
|     ASpace: typeof import('ant-design-vue/es')['Space'] | ||||
|     ASpin: typeof import('ant-design-vue/es')['Spin'] | ||||
|     AStatistic: typeof import('ant-design-vue/es')['Statistic'] | ||||
|     ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] | ||||
|     ASwitch: typeof import('ant-design-vue/es')['Switch'] | ||||
|     ATable: typeof import('ant-design-vue/es')['Table'] | ||||
|     ATextarea: typeof import('ant-design-vue/es')['Textarea'] | ||||
|     ATimePicker: typeof import('ant-design-vue/es')['TimePicker'] | ||||
|     ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker'] | ||||
|     ATooltip: typeof import('ant-design-vue/es')['Tooltip'] | ||||
|     ATransfer: typeof import('ant-design-vue/es')['Transfer'] | ||||
|     AUpload: typeof import('ant-design-vue/es')['Upload'] | ||||
|     AvaliableToMe: typeof import('./src/components/user/AddressBooks/AvaliableToMe.vue')['default'] | ||||
|     BlockedIPs: typeof import('./src/components/admin/security/BlockedIPs.vue')['default'] | ||||
|     Calendars: typeof import('./src/components/common/Calendars.vue')['default'] | ||||
|     CalendarsAccess: typeof import('./src/components/common/CalendarsAccess.vue')['default'] | ||||
|     Categories: typeof import('./src/components/admin/domains/resources/Categories.vue')['default'] | ||||
|     Conditions: typeof import('./src/components/common/rules/Conditions.vue')['default'] | ||||
|     Dashboard: typeof import('./src/components/admin/Dashboard.vue')['default'] | ||||
|     Day: typeof import('./src/components/common/approval/Day.vue')['default'] | ||||
|     DKIM: typeof import('./src/components/admin/domains/DKIM.vue')['default'] | ||||
|     Email: typeof import('./src/components/admin/security/black_list/Email.vue')['default'] | ||||
|     Export: typeof import('./src/components/admin/domains/mailstorage/Export.vue')['default'] | ||||
|     Groups: typeof import('./src/components/admin/domains/userdb/Groups.vue')['default'] | ||||
|     IncomingRules: typeof import('./src/components/user/IncomingRules.vue')['default'] | ||||
|     Interval: typeof import('./src/components/common/approval/Interval.vue')['default'] | ||||
|     IP: typeof import('./src/components/admin/security/black_list/IP.vue')['default'] | ||||
|     License: typeof import('./src/components/admin/settings/License.vue')['default'] | ||||
|     List: typeof import('./src/components/user/Calendars/EventsPlanner/List.vue')['default'] | ||||
|     Logo: typeof import('./src/components/Logo.vue')['default'] | ||||
|     Mailboxes: typeof import('./src/components/admin/domains/mailstorage/Mailboxes.vue')['default'] | ||||
|     MailboxSharedAccess: typeof import('./src/components/user/MailboxSharedAccess.vue')['default'] | ||||
|     MailsPermDeletion: typeof import('./src/components/admin/domains/MailsPermDeletion.vue')['default'] | ||||
|     Main: typeof import('./src/components/admin/settings/Main.vue')['default'] | ||||
|     Manage: typeof import('./src/components/admin/settings/smtp_queue/Manage.vue')['default'] | ||||
|     Migration: typeof import('./src/components/admin/domains/Migration.vue')['default'] | ||||
|     MyBooks: typeof import('./src/components/user/AddressBooks/MyBooks.vue')['default'] | ||||
|     MyCalendars: typeof import('./src/components/user/Calendars/MyCalendars.vue')['default'] | ||||
|     NewOrEdit: typeof import('./src/components/user/Calendars/EventsPlanner/NewOrEdit.vue')['default'] | ||||
|     NotImplemented: typeof import('./src/components/NotImplemented.vue')['default'] | ||||
|     Offices: typeof import('./src/components/admin/domains/resources/Offices.vue')['default'] | ||||
|     OutgoingRules: typeof import('./src/components/admin/domains/OutgoingRules.vue')['default'] | ||||
|     PageNotFound: typeof import('./src/components/PageNotFound.vue')['default'] | ||||
|     Policy: typeof import('./src/components/admin/domains/cidr_access/Policy.vue')['default'] | ||||
|     Pools: typeof import('./src/components/admin/domains/cidr_access/Pools.vue')['default'] | ||||
|     Profile: typeof import('./src/components/user/Profile.vue')['default'] | ||||
|     RecoveryFolder: typeof import('./src/components/user/RecoveryFolder.vue')['default'] | ||||
|     Redirects: typeof import('./src/components/admin/domains/userdb/Redirects.vue')['default'] | ||||
|     Resources: typeof import('./src/components/admin/domains/resources/Resources.vue')['default'] | ||||
|     RouterLink: typeof import('vue-router')['RouterLink'] | ||||
|     RouterView: typeof import('vue-router')['RouterView'] | ||||
|     Rules: typeof import('./src/components/common/rules/Rules.vue')['default'] | ||||
|     ScriptErrorNotify: typeof import('./src/components/user/Calendars/EventsPlanner/ScriptErrorNotify.vue')['default'] | ||||
|     Settings: typeof import('./src/components/admin/domains/mailstorage/Settings.vue')['default'] | ||||
|     SettingsDB: typeof import('./src/components/admin/settings/SettingsDB.vue')['default'] | ||||
|     SharedAddressBooks: typeof import('./src/components/admin/domains/SharedAddressBooks.vue')['default'] | ||||
|     SharedCalendars: typeof import('./src/components/admin/domains/SharedCalendars.vue')['default'] | ||||
|     SharedFolders: typeof import('./src/components/common/SharedFolders.vue')['default'] | ||||
|     ShareFreeTime: typeof import('./src/components/user/Calendars/ShareFreeTime.vue')['default'] | ||||
|     TimezoneSelect: typeof import('./src/components/common/TimezoneSelect.vue')['default'] | ||||
|     Users: typeof import('./src/components/admin/domains/userdb/Users.vue')['default'] | ||||
|     WorkDaysSelect: typeof import('./src/components/common/WorkDaysSelect.vue')['default'] | ||||
|     WorkHoursRangePicker: typeof import('./src/components/common/WorkHoursRangePicker.vue')['default'] | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1 @@ | |||
| /// <reference types="vite/client" />
 | ||||
|  | @ -1,13 +1,13 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"/> | ||||
|     <meta content="width=device-width, initial-scale=1.0" name="viewport"/> | ||||
|     <title>myproject</title> | ||||
| </head> | ||||
| <body> | ||||
| <div id="app"></div> | ||||
| <script src="./src/main.ts" type="module"></script> | ||||
| </body> | ||||
|   <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <link rel="icon" href="/favicon.ico"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>Tegu</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|     <script type="module" src="/src/main.ts"></script> | ||||
|   </body> | ||||
| </html> | ||||
| 
 | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -1,21 +1,34 @@ | |||
| { | ||||
|   "name": "frontend", | ||||
|   "private": true, | ||||
|   "name": "js", | ||||
|   "version": "0.0.0", | ||||
|   "private": true, | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "dev": "vite", | ||||
|     "build": "vue-tsc --noEmit && vite build", | ||||
|     "preview": "vite preview" | ||||
|     "dev": "vite --port 7777", | ||||
|     "build": "run-p type-check \"build-only {@}\" --", | ||||
|     "preview": "vite preview", | ||||
|     "build-only": "vite build", | ||||
|     "type-check": "vue-tsc --build --force" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "vue": "^3.2.37" | ||||
|     "@ant-design/icons-vue": "^7.0.1", | ||||
|     "ant-design-vue": "^4.2.3", | ||||
|     "pinia": "^2.1.7", | ||||
|     "pinia-plugin-persistedstate": "^3.2.1", | ||||
|     "vue": "^3.4.21", | ||||
|     "vue-i18n": "^9.13.1", | ||||
|     "vue-router": "^4.3.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@vitejs/plugin-vue": "^3.0.3", | ||||
|     "typescript": "^4.6.4", | ||||
|     "vite": "^3.0.7", | ||||
|     "vue-tsc": "^1.8.27", | ||||
|     "@babel/types": "^7.18.10" | ||||
|     "@tsconfig/node20": "^20.1.4", | ||||
|     "@types/node": "^20.12.5", | ||||
|     "@vitejs/plugin-vue": "^5.0.4", | ||||
|     "@vue/tsconfig": "^0.5.1", | ||||
|     "less": "^4.3.0", | ||||
|     "less-loader": "^12.2.0", | ||||
|     "npm-run-all2": "^6.1.2", | ||||
|     "typescript": "~5.4.0", | ||||
|     "vite": "^5.2.8", | ||||
|     "vue-tsc": "^2.0.11" | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| bb7ffb87329c9ad4990374471d4ce9a4 | ||||
| 5b1aa7f7c882c7e08d0d390c73602d71 | ||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.9 KiB | 
|  | @ -1,22 +1,83 @@ | |||
| <script lang="ts" setup> | ||||
| import HelloWorld from "./components/HelloWorld.vue"; | ||||
| <script setup lang="ts"> | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { RouterLink, RouterView } from "vue-router"; | ||||
| import zhCN from "ant-design-vue/es/locale/zh_CN"; | ||||
| import ru from "ant-design-vue/es/locale/ru_RU"; | ||||
| import en from "ant-design-vue/es/locale/en_GB"; | ||||
| import dayjs from "dayjs"; | ||||
| import "dayjs/locale/ru"; | ||||
| import "dayjs/locale/en"; | ||||
| import "dayjs/locale/zh-cn"; | ||||
| import updateLocale from "dayjs/plugin/updateLocale"; | ||||
| import localeData from "dayjs/plugin/localeData"; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   locale: any; | ||||
| }>({ | ||||
|   locale: undefined, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   dayjs.extend(updateLocale); | ||||
|   dayjs.extend(localeData); | ||||
| 
 | ||||
|   let locales = getBrowserLocales({ languageCodeOnly: true }); | ||||
|   let locale = "en"; | ||||
|   if (locales && locales.length > 0) { | ||||
|     locale = locales[0]; | ||||
|   } | ||||
| 
 | ||||
|   dayjs.updateLocale(locale, { | ||||
|     weekStart: 1, | ||||
|   }); | ||||
|   dayjs.locale(locale); | ||||
|   switch (locale) { | ||||
|     case "ru": | ||||
|       page.locale = ru; | ||||
|       break; | ||||
|     case "zh": | ||||
|       page.locale = zhCN; | ||||
|       break; | ||||
|     default: | ||||
|       page.locale = en; | ||||
|       break; | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| function getBrowserLocales(options = {}) { | ||||
|   const defaultOptions = { | ||||
|     languageCodeOnly: false, | ||||
|   }; | ||||
| 
 | ||||
|   const opt = { | ||||
|     ...defaultOptions, | ||||
| 
 | ||||
|     ...options, | ||||
|   }; | ||||
| 
 | ||||
|   const browserLocales = | ||||
|     navigator.languages === undefined | ||||
|       ? [navigator.language] | ||||
|       : navigator.languages; | ||||
| 
 | ||||
|   if (!browserLocales) { | ||||
|     return undefined; | ||||
|   } | ||||
| 
 | ||||
|   return browserLocales.map((locale) => { | ||||
|     const trimmedLocale = locale.trim(); | ||||
| 
 | ||||
|     return opt.languageCodeOnly ? trimmedLocale.split(/-|_/)[0] : trimmedLocale; | ||||
|   }); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <img id="logo" alt="Wails logo" src="./assets/images/logo-universal.png" /> | ||||
|   <HelloWorld /> | ||||
|   <a-config-provider :locale="page.locale"> | ||||
|     <div class="root-view"> | ||||
|       <RouterView /> | ||||
|     </div> | ||||
|   </a-config-provider> | ||||
| </template> | ||||
| 
 | ||||
| <style> | ||||
| #logo { | ||||
|   display: block; | ||||
|   width: 50%; | ||||
|   height: 50%; | ||||
|   margin: auto; | ||||
|   padding: 10% 0 0; | ||||
|   background-position: center; | ||||
|   background-repeat: no-repeat; | ||||
|   background-size: 100% 100%; | ||||
|   background-origin: content-box; | ||||
| } | ||||
| </style> | ||||
| <style scoped></style> | ||||
|  |  | |||
|  | @ -0,0 +1,36 @@ | |||
| /* color palette from <https://github.com/vuejs/theme> */ | ||||
| 
 | ||||
| 
 | ||||
| *, | ||||
| *::before, | ||||
| *::after { | ||||
|   box-sizing: border-box; | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|   min-height: 100vh; | ||||
|   color: var(--color-text); | ||||
|   background: var(--color-background); | ||||
|   transition: | ||||
|     color 0.5s, | ||||
|     background-color 0.5s; | ||||
|   line-height: 1.6; | ||||
|   font-family: | ||||
|     Inter, | ||||
|     -apple-system, | ||||
|     BlinkMacSystemFont, | ||||
|     'Segoe UI', | ||||
|     Roboto, | ||||
|     Oxygen, | ||||
|     Ubuntu, | ||||
|     Cantarell, | ||||
|     'Fira Sans', | ||||
|     'Droid Sans', | ||||
|     'Helvetica Neue', | ||||
|     sans-serif; | ||||
|   font-size: 15px; | ||||
|   text-rendering: optimizeLegibility; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
| } | ||||
|  | @ -1,93 +0,0 @@ | |||
| Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), | ||||
| 
 | ||||
| This Font Software is licensed under the SIL Open Font License, Version 1.1. | ||||
| This license is copied below, and is also available with a FAQ at: | ||||
| http://scripts.sil.org/OFL | ||||
| 
 | ||||
| 
 | ||||
| ----------------------------------------------------------- | ||||
| SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | ||||
| ----------------------------------------------------------- | ||||
| 
 | ||||
| PREAMBLE | ||||
| The goals of the Open Font License (OFL) are to stimulate worldwide | ||||
| development of collaborative font projects, to support the font creation | ||||
| efforts of academic and linguistic communities, and to provide a free and | ||||
| open framework in which fonts may be shared and improved in partnership | ||||
| with others. | ||||
| 
 | ||||
| The OFL allows the licensed fonts to be used, studied, modified and | ||||
| redistributed freely as long as they are not sold by themselves. The | ||||
| fonts, including any derivative works, can be bundled, embedded,  | ||||
| redistributed and/or sold with any software provided that any reserved | ||||
| names are not used by derivative works. The fonts and derivatives, | ||||
| however, cannot be released under any other type of license. The | ||||
| requirement for fonts to remain under this license does not apply | ||||
| to any document created using the fonts or their derivatives. | ||||
| 
 | ||||
| DEFINITIONS | ||||
| "Font Software" refers to the set of files released by the Copyright | ||||
| Holder(s) under this license and clearly marked as such. This may | ||||
| include source files, build scripts and documentation. | ||||
| 
 | ||||
| "Reserved Font Name" refers to any names specified as such after the | ||||
| copyright statement(s). | ||||
| 
 | ||||
| "Original Version" refers to the collection of Font Software components as | ||||
| distributed by the Copyright Holder(s). | ||||
| 
 | ||||
| "Modified Version" refers to any derivative made by adding to, deleting, | ||||
| or substituting -- in part or in whole -- any of the components of the | ||||
| Original Version, by changing formats or by porting the Font Software to a | ||||
| new environment. | ||||
| 
 | ||||
| "Author" refers to any designer, engineer, programmer, technical | ||||
| writer or other person who contributed to the Font Software. | ||||
| 
 | ||||
| PERMISSION & CONDITIONS | ||||
| Permission is hereby granted, free of charge, to any person obtaining | ||||
| a copy of the Font Software, to use, study, copy, merge, embed, modify, | ||||
| redistribute, and sell modified and unmodified copies of the Font | ||||
| Software, subject to the following conditions: | ||||
| 
 | ||||
| 1) Neither the Font Software nor any of its individual components, | ||||
| in Original or Modified Versions, may be sold by itself. | ||||
| 
 | ||||
| 2) Original or Modified Versions of the Font Software may be bundled, | ||||
| redistributed and/or sold with any software, provided that each copy | ||||
| contains the above copyright notice and this license. These can be | ||||
| included either as stand-alone text files, human-readable headers or | ||||
| in the appropriate machine-readable metadata fields within text or | ||||
| binary files as long as those fields can be easily viewed by the user. | ||||
| 
 | ||||
| 3) No Modified Version of the Font Software may use the Reserved Font | ||||
| Name(s) unless explicit written permission is granted by the corresponding | ||||
| Copyright Holder. This restriction only applies to the primary font name as | ||||
| presented to the users. | ||||
| 
 | ||||
| 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | ||||
| Software shall not be used to promote, endorse or advertise any | ||||
| Modified Version, except to acknowledge the contribution(s) of the | ||||
| Copyright Holder(s) and the Author(s) or with their explicit written | ||||
| permission. | ||||
| 
 | ||||
| 5) The Font Software, modified or unmodified, in part or in whole, | ||||
| must be distributed entirely under this license, and must not be | ||||
| distributed under any other license. The requirement for fonts to | ||||
| remain under this license does not apply to any document created | ||||
| using the Font Software. | ||||
| 
 | ||||
| TERMINATION | ||||
| This license becomes null and void if any of the above conditions are | ||||
| not met. | ||||
| 
 | ||||
| DISCLAIMER | ||||
| THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | ||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | ||||
| OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | ||||
| COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | ||||
| INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | ||||
| DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||||
| FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | ||||
| OTHER DEALINGS IN THE FONT SOFTWARE. | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 13 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 136 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 16 KiB | 
|  | @ -0,0 +1,47 @@ | |||
| @import './base.css'; | ||||
| 
 | ||||
| 
 | ||||
| #nav.ant-menu-horizontal>.ant-menu-item:after,#nav.ant-menu-horizontal { | ||||
|   bottom: auto; | ||||
| } | ||||
| 
 | ||||
| .root-view { | ||||
|   padding: 8px; | ||||
| } | ||||
| 
 | ||||
| .form-input-number { | ||||
|   width: 50%; | ||||
|   min-width: 80px; | ||||
| } | ||||
| 
 | ||||
| .disabled-router-link { | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .main-view { | ||||
|   padding-right: 8px; | ||||
|   padding-left: 8px; | ||||
|   max-height: calc(100vh - 70px); | ||||
|   overflow: auto; | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| .bw-list-panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| .disabled-div { | ||||
|   pointer-events: none; | ||||
|   opacity: 0.4; | ||||
| } | ||||
| 
 | ||||
| .middle-of-screen { | ||||
|   margin: 0; | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
|   left: 50%; | ||||
|   -ms-transform: translate(-50%, -50%); | ||||
|   transform: translate(-50%, -50%); | ||||
| } | ||||
|  | @ -1,71 +0,0 @@ | |||
| <script lang="ts" setup> | ||||
| import {reactive} from 'vue' | ||||
| import {Greet} from '../../wailsjs/go/main/App' | ||||
| 
 | ||||
| const data = reactive({ | ||||
|   name: "", | ||||
|   resultText: "Please enter your name below 👇", | ||||
| }) | ||||
| 
 | ||||
| function greet() { | ||||
|   Greet(data.name).then(result => { | ||||
|     data.resultText = result | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <main> | ||||
|     <div id="result" class="result">{{ data.resultText }}</div> | ||||
|     <div id="input" class="input-box"> | ||||
|       <input id="name" v-model="data.name" autocomplete="off" class="input" type="text"/> | ||||
|       <button class="btn" @click="greet">Greet</button> | ||||
|     </div> | ||||
|   </main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| .result { | ||||
|   height: 20px; | ||||
|   line-height: 20px; | ||||
|   margin: 1.5rem auto; | ||||
| } | ||||
| 
 | ||||
| .input-box .btn { | ||||
|   width: 60px; | ||||
|   height: 30px; | ||||
|   line-height: 30px; | ||||
|   border-radius: 3px; | ||||
|   border: none; | ||||
|   margin: 0 0 0 20px; | ||||
|   padding: 0 8px; | ||||
|   cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .input-box .btn:hover { | ||||
|   background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); | ||||
|   color: #333333; | ||||
| } | ||||
| 
 | ||||
| .input-box .input { | ||||
|   border: none; | ||||
|   border-radius: 3px; | ||||
|   outline: none; | ||||
|   height: 30px; | ||||
|   line-height: 30px; | ||||
|   padding: 0 10px; | ||||
|   background-color: rgba(240, 240, 240, 1); | ||||
|   -webkit-font-smoothing: antialiased; | ||||
| } | ||||
| 
 | ||||
| .input-box .input:hover { | ||||
|   border: none; | ||||
|   background-color: rgba(255, 255, 255, 1); | ||||
| } | ||||
| 
 | ||||
| .input-box .input:focus { | ||||
|   border: none; | ||||
|   background-color: rgba(255, 255, 255, 1); | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,35 @@ | |||
| <template> | ||||
|   <a href="https://mbk-lab.ru/en/" rel="home"> | ||||
|     <a-flex style="height: 45px" justify="center" align="center"> | ||||
|       <div> | ||||
|         <img | ||||
|           style="margin-right: 4px; margin-top: 4px" | ||||
|           height="51px" | ||||
|           src="@/assets/logo-200x200.png" | ||||
|         /> | ||||
|       </div> | ||||
|       <div style="color: #2a4861"> | ||||
|         <div | ||||
|           style=" | ||||
|             font-size: 28px; | ||||
|             font-weight: 600; | ||||
|             height: 51px; | ||||
|             margin-top: -8px; | ||||
|             margin-bottom: -16px; | ||||
|           " | ||||
|         > | ||||
|           TEGU | ||||
|         </div> | ||||
|         <div style="font-size: small">{{ props.version }}</div> | ||||
|       </div> | ||||
|     </a-flex> | ||||
|   </a> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { Flex } from "ant-design-vue"; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   version: string; | ||||
| }>(); | ||||
| </script> | ||||
|  | @ -0,0 +1,53 @@ | |||
| <template> | ||||
|   <div class="not-implemented"> | ||||
|     <div class="content"> | ||||
|       <div class="icon">🚧</div> | ||||
|       <h1>Not Implemented Yet</h1> | ||||
|       <p class="message">This feature is currently under development.</p> | ||||
|       <p class="sub-message">Please check back later for updates.</p> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| .not-implemented { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   min-height: 60vh; | ||||
|   background-color: #f8f9fa; | ||||
| } | ||||
| 
 | ||||
| .content { | ||||
|   text-align: center; | ||||
|   padding: 2rem; | ||||
|   background: white; | ||||
|   border-radius: 12px; | ||||
|   box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | ||||
|   max-width: 400px; | ||||
|   width: 90%; | ||||
| } | ||||
| 
 | ||||
| .icon { | ||||
|   font-size: 4rem; | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| h1 { | ||||
|   color: #2c3e50; | ||||
|   font-size: 1.8rem; | ||||
|   margin-bottom: 1rem; | ||||
|   font-weight: 600; | ||||
| } | ||||
| 
 | ||||
| .message { | ||||
|   color: #34495e; | ||||
|   font-size: 1.1rem; | ||||
|   margin-bottom: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .sub-message { | ||||
|   color: #7f8c8d; | ||||
|   font-size: 0.9rem; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,44 @@ | |||
| <template> | ||||
|   <a-result | ||||
|     status="404" | ||||
|     title="404" | ||||
|     :sub-title="t('common.not_found.page_not_found')" | ||||
|   > | ||||
|     <template #extra> | ||||
|       <a-button v-if="page.authed" type="primary" @click="backHome">{{ | ||||
|         t("common.not_found.return") | ||||
|       }}</a-button> | ||||
|     </template> | ||||
|   </a-result> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { useAuthStore } from "@/stores/auth"; | ||||
| import router from "@/router"; | ||||
| import { | ||||
|   RouteAdminDashboard, | ||||
|   RouteLogin, | ||||
|   RouteUserProfile, | ||||
| } from "@/router/consts"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   authed: boolean; | ||||
| }>({ | ||||
|   authed: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.authed = useAuthStore().authed; | ||||
| }); | ||||
| 
 | ||||
| function backHome() { | ||||
|   if (useAuthStore().isAdmin) { | ||||
|     return router.push(RouteAdminDashboard); | ||||
|   } else { | ||||
|     return router.push(RouteUserProfile); | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,465 @@ | |||
| <template> | ||||
|   <div style="width: inherit"> | ||||
|     <a-row :gutter="[16, 16]" justify="space-around"> | ||||
|       <a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6"> | ||||
|         <a-card hoverable style="height: 100%"> | ||||
|           <template #title> | ||||
|             {{ t("components.admin.dashboard.license.title") }} | ||||
|           </template> | ||||
| 
 | ||||
|           <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.license.developer')" | ||||
|               >{{ page.licenseBlock.Developer }}</a-descriptions-item | ||||
|             > | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.license.version')" | ||||
|             > | ||||
|               {{ page.licenseBlock.Version }} | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.license.license')" | ||||
|             > | ||||
|               <div | ||||
|                 :class=" | ||||
|                   page.contentLoaded && | ||||
|                   (page.licenseBlock.Expired || | ||||
|                     page.licenseBlock.Mismatch || | ||||
|                     !page.licenseBlock.License) | ||||
|                     ? 'warn' | ||||
|                     : '' | ||||
|                 " | ||||
|               > | ||||
|                 {{ | ||||
|                   page.licenseBlock.Mismatch | ||||
|                     ? t("components.admin.dashboard.license.license_mismatch") | ||||
|                     : !page.licenseBlock.License | ||||
|                     ? t("components.admin.dashboard.license.license_empty") | ||||
|                     : page.licenseBlock.License | ||||
|                 }} | ||||
|               </div> | ||||
|             </a-descriptions-item> | ||||
|           </a-descriptions> | ||||
|         </a-card> | ||||
|       </a-col> | ||||
| 
 | ||||
|       <a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6"> | ||||
|         <a-card hoverable style="height: 100%"> | ||||
|           <template #title> | ||||
|             {{ t("components.admin.dashboard.mailboxes.title") }} | ||||
|           </template> | ||||
| 
 | ||||
|           <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.mailboxes.license_allowed')" | ||||
|               >{{ page.mailboxBlock.Total }}</a-descriptions-item | ||||
|             > | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.mailboxes.used')" | ||||
|               >{{ page.mailboxBlock.Used }} | ||||
|             </a-descriptions-item> | ||||
| 
 | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.mailboxes.remains')" | ||||
|             > | ||||
|               <div :class="page.mailboxBlock.LowMailboxes ? 'warn' : ''"> | ||||
|                 {{ page.mailboxBlock.Remains }} | ||||
|               </div> | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.mailboxes.archived')" | ||||
|             > | ||||
|               {{ page.mailboxBlock.Archived }} | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               v-if="page.mailboxBlock.UsedLastMonth" | ||||
|               :label="t('components.admin.dashboard.mailboxes.used_last_month')" | ||||
|               >{{ page.mailboxBlock.UsedLastMonth }} | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               v-if="page.mailboxBlock.UsedCurrentMonth" | ||||
|               :label=" | ||||
|                 t('components.admin.dashboard.mailboxes.used_current_month') | ||||
|               " | ||||
|               >{{ page.mailboxBlock.UsedCurrentMonth }} | ||||
|             </a-descriptions-item> | ||||
|           </a-descriptions> | ||||
|         </a-card> | ||||
|       </a-col> | ||||
| 
 | ||||
|       <a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="12"> | ||||
|         <a-card | ||||
|           hoverable | ||||
|           style="height: 100%" | ||||
|           :tab-list="tabListNoTitle" | ||||
|           :active-tab-key="page.detailsKey" | ||||
|           @tabChange="(key: string) =>page.detailsKey = key" | ||||
|         > | ||||
|           <div v-if="page.detailsKey === 'imap'"> | ||||
|             <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.imap.server')" | ||||
|               > | ||||
|                 <a-space> | ||||
|                   {{ page.imapBlock.Server }} | ||||
|                   <a-button | ||||
|                     @click="copyToClipboard(page.imapBlock.Server)" | ||||
|                     size="small" | ||||
|                   > | ||||
|                     <CopyOutlined /> | ||||
|                   </a-button> | ||||
|                 </a-space> | ||||
|               </a-descriptions-item> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.imap.port')" | ||||
|               > | ||||
|                 <a-space> | ||||
|                   {{ page.imapBlock.Port }} | ||||
|                   <a-button | ||||
|                     @click="copyToClipboard(page.imapBlock.Port.toString())" | ||||
|                     size="small" | ||||
|                   > | ||||
|                     <CopyOutlined /> | ||||
|                   </a-button> | ||||
|                 </a-space> | ||||
|               </a-descriptions-item> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.imap.encryption')" | ||||
|                 >{{ page.imapBlock.SSL }} | ||||
|               </a-descriptions-item> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.imap.auth')" | ||||
|                 >{{ t("components.admin.dashboard.imap.auth_text") }} | ||||
|               </a-descriptions-item> | ||||
|             </a-descriptions> | ||||
|           </div> | ||||
|           <div v-if="page.detailsKey === 'smtp'"> | ||||
|             <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.smtp.server')" | ||||
|               > | ||||
|                 <a-space> | ||||
|                   {{ page.smtpBlock.Server }} | ||||
|                   <a-button | ||||
|                     @click="copyToClipboard(page.smtpBlock.Server)" | ||||
|                     size="small" | ||||
|                   > | ||||
|                     <CopyOutlined /> | ||||
|                   </a-button> | ||||
|                 </a-space> | ||||
|               </a-descriptions-item> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.smtp.port')" | ||||
|               > | ||||
|                 <a-space> | ||||
|                   {{ page.smtpBlock.Port }} | ||||
|                   <a-button | ||||
|                     @click="copyToClipboard(page.smtpBlock.Port.toString())" | ||||
|                     size="small" | ||||
|                   > | ||||
|                     <CopyOutlined /> | ||||
|                   </a-button> | ||||
|                 </a-space> | ||||
|               </a-descriptions-item> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.smtp.encryption')" | ||||
|                 >{{ page.smtpBlock.SSL }} | ||||
|               </a-descriptions-item> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.smtp.auth')" | ||||
|                 >{{ t("components.admin.dashboard.smtp.auth_text") }} | ||||
|               </a-descriptions-item> | ||||
|             </a-descriptions> | ||||
|           </div> | ||||
|           <div v-if="page.detailsKey === 'queue'"> | ||||
|             <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.smtp_queue.type')" | ||||
|                 >{{ page.smtpQueueBlock.Type }}</a-descriptions-item | ||||
|               > | ||||
|               <a-descriptions-item | ||||
|                 v-if="page.smtpQueueBlock.Host" | ||||
|                 :label="t('components.admin.dashboard.smtp_queue.host')" | ||||
|               > | ||||
|                 {{ page.smtpQueueBlock.Host }} | ||||
|               </a-descriptions-item> | ||||
|             </a-descriptions> | ||||
|           </div> | ||||
|           <div v-if="page.detailsKey === 'antispam'"> | ||||
|             <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.antispam.type')" | ||||
|               > | ||||
|                 {{ page.antispamBlock.Type }} | ||||
|               </a-descriptions-item> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.antispam.status')" | ||||
|               > | ||||
|                 {{ | ||||
|                   page.antispamBlock.Enabled | ||||
|                     ? t("components.admin.dashboard.antispam.status_enabled") | ||||
|                     : t("components.admin.dashboard.antispam.status_disabled") | ||||
|                 }} | ||||
|               </a-descriptions-item> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.antispam.host')" | ||||
|               > | ||||
|                 {{ page.antispamBlock.Host }} | ||||
|               </a-descriptions-item> | ||||
|             </a-descriptions> | ||||
|           </div> | ||||
| 
 | ||||
|           <div v-if="page.detailsKey === 'dav'"> | ||||
|             <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.dav.books')" | ||||
|               > | ||||
|                 <a-space> | ||||
|                   {{ page.davBlock.BooksUrl }} | ||||
|                   <a-button | ||||
|                     v-if="page.davBlock.BooksUrl.startsWith('http')" | ||||
|                     @click="copyToClipboard(page.davBlock.BooksUrl)" | ||||
|                     size="small" | ||||
|                   > | ||||
|                     <CopyOutlined /> | ||||
|                   </a-button> | ||||
|                 </a-space> | ||||
|               </a-descriptions-item> | ||||
|               <a-descriptions-item | ||||
|                 :label="t('components.admin.dashboard.dav.calendars')" | ||||
|               > | ||||
|                 <a-space> | ||||
|                   {{ page.davBlock.CalsUrl }} | ||||
|                   <a-button | ||||
|                     v-if="page.davBlock.CalsUrl.startsWith('http')" | ||||
|                     @click="copyToClipboard(page.davBlock.CalsUrl)" | ||||
|                     size="small" | ||||
|                   > | ||||
|                     <CopyOutlined /> | ||||
|                   </a-button> | ||||
|                 </a-space> | ||||
|               </a-descriptions-item> | ||||
|             </a-descriptions> | ||||
|           </div> | ||||
|         </a-card> | ||||
|       </a-col> | ||||
|     </a-row> | ||||
|     <br /> | ||||
|     <a-row :gutter="[16, 16]"> | ||||
|       <a-col | ||||
|         v-for="domain in page.domains" | ||||
|         :xs="24" | ||||
|         :sm="24" | ||||
|         :md="24" | ||||
|         :lg="12" | ||||
|         :xl="8" | ||||
|       > | ||||
|         <a-card hoverable style="height: 100%"> | ||||
|           <template #title> {{ domain.Domain }} </template> | ||||
|           <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.domain.mailstorage')" | ||||
|               >{{ domain.MailStorage }} | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.domain.userdb_providers')" | ||||
|             > | ||||
|               <a-descriptions size="small" :column="1"> | ||||
|                 <a-descriptions-item v-for="prov in domain.UserDBProviders"> | ||||
|                   {{ prov }} | ||||
|                 </a-descriptions-item> | ||||
|               </a-descriptions> | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.admin.dashboard.domain.dns_record')" | ||||
|               ><a @click="page.dnsModal = { show: true, text: domain.DNS }">{{ | ||||
|                 t("components.admin.dashboard.domain.dns_show") | ||||
|               }}</a> | ||||
|             </a-descriptions-item> | ||||
|           </a-descriptions> | ||||
|         </a-card> | ||||
|       </a-col> | ||||
|     </a-row> | ||||
|     <a-row justify="space-around"> | ||||
|       <a-col> </a-col> | ||||
|     </a-row> | ||||
|   </div> | ||||
|   <a-modal style="width: 70%" v-model:open="page.dnsModal.show"> | ||||
|     <a-textarea | ||||
|       style="text-wrap: nowrap; height: 200px" | ||||
|       v-model:value="page.dnsModal.text" | ||||
|     > | ||||
|     </a-textarea> | ||||
|     <template #footer> | ||||
|       <a-button @click="copyToClipboard(page.dnsModal.text)"> | ||||
|         <CopyOutlined /> | ||||
|         {{ t("components.admin.dashboard.domain.dns_copy") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { copyToClipboard } from "@/composables/misc"; | ||||
| import { CopyOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| 
 | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const tabListNoTitle = [ | ||||
|   { | ||||
|     key: "imap", | ||||
|     tab: t("components.admin.dashboard.imap.title"), | ||||
|   }, | ||||
|   { | ||||
|     key: "smtp", | ||||
|     tab: t("components.admin.dashboard.smtp.title"), | ||||
|   }, | ||||
|   { | ||||
|     key: "queue", | ||||
|     tab: t("components.admin.dashboard.smtp_queue.title"), | ||||
|   }, | ||||
|   { | ||||
|     key: "antispam", | ||||
|     tab: t("components.admin.dashboard.antispam.title"), | ||||
|   }, | ||||
| 
 | ||||
|   { | ||||
|     key: "dav", | ||||
|     tab: t("components.admin.dashboard.dav.title"), | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   dnsModal: { | ||||
|     show: boolean; | ||||
|     text: string; | ||||
|   }; | ||||
|   contentLoaded: boolean; | ||||
|   detailsKey: string; | ||||
|   licenseBlock: { | ||||
|     Developer: string; | ||||
|     License: string; | ||||
|     Expired: boolean; | ||||
|     Mismatch: boolean; | ||||
|     Version: string; | ||||
|   }; | ||||
|   mailboxBlock: { | ||||
|     Total: number; | ||||
|     Used: number; | ||||
|     Remains: number; | ||||
|     Archived: number; | ||||
|     LowMailboxes: boolean; | ||||
|     UsedLastMonth: number; | ||||
|     UsedCurrentMonth: number; | ||||
|   }; | ||||
|   smtpQueueBlock: { | ||||
|     Type: string; | ||||
|     Host: string; | ||||
|   }; | ||||
|   antispamBlock: { | ||||
|     Type: string; | ||||
|     Enabled: boolean; | ||||
|     Host: string; | ||||
|   }; | ||||
|   davBlock: { | ||||
|     BooksUrl: string; | ||||
|     CalsUrl: string; | ||||
|   }; | ||||
|   imapBlock: { | ||||
|     Server: string; | ||||
|     Port: number; | ||||
|     SSL: string; | ||||
|   }; | ||||
|   smtpBlock: { | ||||
|     Server: string; | ||||
|     Port: number; | ||||
|     SSL: string; | ||||
|   }; | ||||
|   domains: { | ||||
|     Domain: string; | ||||
|     MailStorage: string; | ||||
|     UserDBProviders: string[]; | ||||
|     DNS: string; | ||||
|   }[]; | ||||
| }>({ | ||||
|   detailsKey: "imap", | ||||
|   licenseBlock: { | ||||
|     Developer: "", | ||||
|     License: "", | ||||
|     Expired: false, | ||||
|     Mismatch: false, | ||||
|     Version: "", | ||||
|   }, | ||||
| 
 | ||||
|   mailboxBlock: { | ||||
|     Total: 0, | ||||
|     Used: 0, | ||||
|     Remains: 0, | ||||
|     Archived: 0, | ||||
|     LowMailboxes: false, | ||||
|     UsedLastMonth: 0, | ||||
|     UsedCurrentMonth: 0, | ||||
|   }, | ||||
|   smtpQueueBlock: { | ||||
|     Type: "", | ||||
|     Host: "", | ||||
|   }, | ||||
|   antispamBlock: { | ||||
|     Type: "", | ||||
|     Enabled: false, | ||||
|     Host: "", | ||||
|   }, | ||||
|   davBlock: { | ||||
|     BooksUrl: "", | ||||
|     CalsUrl: "", | ||||
|   }, | ||||
|   imapBlock: { | ||||
|     Server: "", | ||||
|     Port: 0, | ||||
|     SSL: "", | ||||
|   }, | ||||
|   smtpBlock: { | ||||
|     Server: "", | ||||
|     Port: 0, | ||||
|     SSL: "", | ||||
|   }, | ||||
|   contentLoaded: false, | ||||
|   domains: [], | ||||
|   dnsModal: { | ||||
|     show: false, | ||||
|     text: "", | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch("/admin/dashboard"); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.contentLoaded = true; | ||||
| 
 | ||||
|   page.licenseBlock = res.data.LicenseBlock; | ||||
|   page.mailboxBlock = res.data.MailboxBlock; | ||||
|   page.smtpQueueBlock = res.data.SmtpQueueBlock; | ||||
|   page.antispamBlock = res.data.AntispamBlock; | ||||
|   page.davBlock = res.data.DavBlock; | ||||
|   page.smtpBlock = res.data.SmtpBlock; | ||||
|   page.imapBlock = res.data.ImapBlock; | ||||
|   page.domains = res.data.Domains; | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .warn { | ||||
|   color: red; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,120 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|         <a-form-item | ||||
|           :label="$t('components.admin.domains.add.Domain')" | ||||
|           :rules="[{ required: true }]" | ||||
|         > | ||||
|           <a-input v-model:value="page.new.Domain"> </a-input> | ||||
|         </a-form-item> | ||||
| 
 | ||||
|         <a-form-item | ||||
|           :label="$t('components.admin.domains.add.Type')" | ||||
|           :rules="[{ required: true }]" | ||||
|         > | ||||
|           <a-select v-model:value="page.new.Type"> | ||||
|             <a-select-option value="pgStorage">PostgreSQL</a-select-option> | ||||
|             <a-select-option value="maildirStorage">Maildir</a-select-option> | ||||
|           </a-select> | ||||
|         </a-form-item> | ||||
|         <a-button | ||||
|           :disabled="!page.new.Type || !page.new.Domain" | ||||
|           class="save-button" | ||||
|           type="primary" | ||||
|           size="large" | ||||
|           @click="save" | ||||
|         > | ||||
|           {{ $t("components.admin.domains.add.create") }} | ||||
|         </a-button> | ||||
|       </a-form> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { saveAndRestart } from "@/composables/restart"; | ||||
| import { message } from "ant-design-vue"; | ||||
| 
 | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import router from "@/router"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageSettings, | ||||
| } from "@/router/consts"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| interface ConnParams { | ||||
|   Host: string; | ||||
|   Port: number; | ||||
|   Db: string; | ||||
|   User: string; | ||||
|   Pass: string; | ||||
|   MaxConn: number; | ||||
| } | ||||
| 
 | ||||
| interface PathParams { | ||||
|   DatabaseDir: string; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   new: { | ||||
|     Type: string; | ||||
|     Domain: string; | ||||
|   }; | ||||
| 
 | ||||
|   loading: boolean; | ||||
| }>({ | ||||
|   loading: false, | ||||
|   new: { | ||||
|     Type: "", | ||||
|     Domain: "", | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const labelCol = { span: 8 }; | ||||
| const wrapperCol = { span: 16 }; | ||||
| 
 | ||||
| onMounted(() => {}); | ||||
| 
 | ||||
| async function save() { | ||||
|   const res = await apiFetch("/admin/domains/add", { | ||||
|     method: "POST", | ||||
|     body: page.new, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.admin.domains.add.success")); | ||||
| 
 | ||||
|   router.push({ | ||||
|     path: RouteAdminDomainsDomainMailStorageSettings.replace( | ||||
|       DomainPlaceholder, | ||||
|       res.data | ||||
|     ), | ||||
|     hash: "#refresh", | ||||
|   }); | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   width: 500px; | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| .save-button { | ||||
|   display: block; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,156 @@ | |||
| <template style="max-width: 100vw"> | ||||
|   <div> | ||||
|     <a-card v-if="page.contentLoaded" class="panel-content"> | ||||
|       <template v-if="page.dkim !== ''"> | ||||
|         <a-card | ||||
|           style="line-break: anywhere" | ||||
|           :title="$t('components.admin.domains.dkim.title')" | ||||
|         > | ||||
|           {{ page.dkim }} | ||||
|         </a-card> | ||||
|         <br /> | ||||
|         <a-button size="large" class="save-button" danger @click="deleteDKIM"> | ||||
|           {{ $t("components.admin.domains.dkim.delete") }} | ||||
|         </a-button> | ||||
|       </template> | ||||
|       <template v-else> | ||||
|         <a-form | ||||
|           :label-col="labelCol" | ||||
|           :wrapper-col="wrapperCol" | ||||
|           :labelWrap="true" | ||||
|         > | ||||
|           <a-form-item :label="$t('components.admin.domains.dkim.selector')"> | ||||
|             <a-input v-model:value="page.new.Selector"> </a-input> | ||||
|           </a-form-item> | ||||
| 
 | ||||
|           <a-form-item :label="$t('components.admin.domains.dkim.key_size')"> | ||||
|             <a-input-number | ||||
|               :min="0" | ||||
|               :addon-after="$t('common.suffixes.bits')" | ||||
|               class="form-input-number" | ||||
|               v-model:value="page.new.Bits" | ||||
|             > | ||||
|             </a-input-number> | ||||
|           </a-form-item> | ||||
|           <a-button | ||||
|             class="save-button" | ||||
|             type="primary" | ||||
|             size="large" | ||||
|             @click="save" | ||||
|             :loading="page.loading" | ||||
|           > | ||||
|             {{ $t("components.admin.domains.dkim.create") }} | ||||
|           </a-button> | ||||
|         </a-form> | ||||
|       </template> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { saveAndRestart } from "@/composables/restart"; | ||||
| import { message } from "ant-design-vue"; | ||||
| 
 | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import router from "@/router"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageSettings, | ||||
| } from "@/router/consts"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   contentLoaded: boolean; | ||||
|   domain: string; | ||||
|   dkim: string; | ||||
|   new: { | ||||
|     Selector: string; | ||||
|     Bits: number; | ||||
|   }; | ||||
| 
 | ||||
|   loading: boolean; | ||||
| }>({ | ||||
|   contentLoaded: false, | ||||
|   domain: "", | ||||
|   dkim: "", | ||||
|   loading: false, | ||||
|   new: { | ||||
|     Selector: "mail", | ||||
|     Bits: 1024, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const labelCol = { span: 8 }; | ||||
| const wrapperCol = { span: 16 }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch(`/admin/domains/${page.domain}/dkim`); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.dkim = res.data; | ||||
|   page.contentLoaded = true; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function save() { | ||||
|   const res = await apiFetch(`/admin/domains/${page.domain}/dkim`, { | ||||
|     method: "POST", | ||||
|     body: page.new, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.admin.domains.dkim.success")); | ||||
| 
 | ||||
|   page.dkim = res.data; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function deleteDKIM() { | ||||
|   const res = await apiFetch(`/admin/domains/${page.domain}/dkim`, { | ||||
|     method: "DELETE", | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.dkim = res.data; | ||||
|   return; | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| .save-button { | ||||
|   width: 30%; | ||||
|   min-width: 150px; | ||||
|   display: block; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,61 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <Rules | ||||
|       :get-get-url="getUrl" | ||||
|       :get-delete-url="getUrl" | ||||
|       :get-save-url="getUrl" | ||||
|       :get-move-url="moveUrl" | ||||
|       :get-enable-url="enableUrl" | ||||
|       :conditions="conditionsList" | ||||
|       :actions="actionsList" | ||||
|     > | ||||
|       <template #condition-value="valueProps"> | ||||
|         <ConditionValue | ||||
|           :change="valueProps.change" | ||||
|           :value="valueProps.value" | ||||
|           :type="valueProps.type" | ||||
|         > | ||||
|         </ConditionValue> | ||||
|       </template> | ||||
|       <template #action-value="valueProps"> | ||||
|         <ActionValue | ||||
|           :change="valueProps.change" | ||||
|           :value="valueProps.value" | ||||
|           :type="valueProps.type" | ||||
|           :get-folders-url="foldersUrl" | ||||
|         > | ||||
|         </ActionValue> | ||||
|       </template> | ||||
|     </Rules> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import ConditionValue from "@/components/common/rules/incoming/Conditions.vue"; | ||||
| import ActionValue from "@/components/common/rules/incoming/Actions.vue"; | ||||
| import { conditionsList } from "@/components/common/rules/incoming/Conditions.vue"; | ||||
| import { actionsList } from "@/components/common/rules/incoming/Actions.vue"; | ||||
| import Rules from "@/components/common/rules/Rules.vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| function getUrl(): string { | ||||
|   return `/admin/domains/${route.params.domain as string}/incoming_rules`; | ||||
| } | ||||
| 
 | ||||
| function moveUrl(): string { | ||||
|   return `/admin/domains/${route.params.domain as string}/incoming_rules/move`; | ||||
| } | ||||
| 
 | ||||
| function foldersUrl(): string { | ||||
|   return `/admin/domains/${ | ||||
|     route.params.domain as string | ||||
|   }/incoming_rules/folders`; | ||||
| } | ||||
| 
 | ||||
| function enableUrl(): string { | ||||
|   return `/admin/domains/${ | ||||
|     route.params.domain as string | ||||
|   }/incoming_rules/enable`; | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,344 @@ | |||
| <template style="max-width: 100vw"> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form | ||||
|         :class="page.status.Active ? 'disabled-div' : ''" | ||||
|         :label-col="labelCol" | ||||
|         :wrapper-col="wrapperCol" | ||||
|       > | ||||
|         <a-form-item | ||||
|           :label="t(`components.admin.domains.mails_perm_deletion.filter`)" | ||||
|         > | ||||
|           <div style="text-align: center"> | ||||
|             <a-flex gap="middle" vertical> | ||||
|               <a-row> | ||||
|                 <a-col flex="auto"> | ||||
|                   <a-radio-group v-model:value="page.status.Condition.Op"> | ||||
|                     <a-radio-button value="and">{{ | ||||
|                       t("components.common.rules.edit_conditions_all") | ||||
|                     }}</a-radio-button> | ||||
|                     <a-radio-button value="or">{{ | ||||
|                       t("components.common.rules.edit_conditions_any") | ||||
|                     }}</a-radio-button> | ||||
|                   </a-radio-group> | ||||
|                 </a-col> | ||||
|                 <a-col flex="100px"> </a-col> | ||||
|               </a-row> | ||||
|               <template v-if="page.renderConditions"> | ||||
|                 <div v-for="(condition, i) in page.status.Condition.Conditions"> | ||||
|                   <a-row> | ||||
|                     <a-col flex="auto"> | ||||
|                       <Conditions | ||||
|                         :value="condition" | ||||
|                         :update="(newVal: string[]) => updateCondition(i, newVal)" | ||||
|                         :conditions="conditionsListForMailDeletion" | ||||
|                       > | ||||
|                         <template #condition-value="valueProps"> | ||||
|                           <ConditionValue | ||||
|                             :change="valueProps.change" | ||||
|                             :value="valueProps.value" | ||||
|                             :type="valueProps.type" | ||||
|                           > | ||||
|                           </ConditionValue> | ||||
|                         </template> | ||||
|                       </Conditions> | ||||
|                     </a-col> | ||||
|                     <a-col flex="100px"> | ||||
|                       <a-button | ||||
|                         :disabled="i === 0" | ||||
|                         danger | ||||
|                         @click="deleteCondition(i)" | ||||
|                       > | ||||
|                         {{ t("components.common.rules.delete") }} | ||||
|                       </a-button> | ||||
|                     </a-col> | ||||
|                   </a-row> | ||||
|                 </div> | ||||
|               </template> | ||||
|               <a-row> | ||||
|                 <a-col flex="auto"> | ||||
|                   <a-button | ||||
|                     style="width: fit-content; margin: auto" | ||||
|                     @click="addCondition" | ||||
|                   > | ||||
|                     <PlusOutlined> </PlusOutlined> | ||||
|                     {{ t("components.common.rules.add_condition") }} | ||||
|                   </a-button> | ||||
|                 </a-col> | ||||
|                 <a-col flex="100px"> </a-col> | ||||
|               </a-row> | ||||
|             </a-flex> | ||||
|           </div> | ||||
|         </a-form-item> | ||||
|       </a-form> | ||||
|       <a-divider></a-divider> | ||||
|       <div> | ||||
|         <a-space style="width: 100%" direction="vertical"> | ||||
|           <a-alert | ||||
|             v-if="page.status.Error" | ||||
|             type="error" | ||||
|             showIcon | ||||
|             :message="page.status.Error" | ||||
|           /> | ||||
|           <a-alert | ||||
|             v-if="page.status.Percent === 100 && !page.status.OnlyFind" | ||||
|             type="success" | ||||
|             showIcon | ||||
|             :message="t(`components.admin.domains.mails_perm_deletion.success`)" | ||||
|           /> | ||||
|           <a-progress | ||||
|             v-if=" | ||||
|               (page.status.Count || page.status.Percent) && | ||||
|               !page.status.OnlyFind | ||||
|             " | ||||
|             :percent="page.status.Percent" | ||||
|           /> | ||||
| 
 | ||||
|           <a-row> | ||||
|             <a-col flex="2"> </a-col> | ||||
|             <a-col> | ||||
|               <a-space> | ||||
|                 <a-button | ||||
|                   :type="page.status.Count === 0 ? `primary` : undefined" | ||||
|                   :loading="page.status.OnlyFind && page.status.Active" | ||||
|                   :disabled="page.status.Active" | ||||
|                   @click="start(true)" | ||||
|                 > | ||||
|                   {{ t("components.admin.domains.mails_perm_deletion.find") }} | ||||
|                 </a-button> | ||||
|                 <a-button | ||||
|                   danger | ||||
|                   :type="`primary`" | ||||
|                   :disabled="page.status.Active || !page.status.Count" | ||||
|                   :loading="!page.status.OnlyFind && page.status.Active" | ||||
|                   @click="confirmDelete" | ||||
|                 > | ||||
|                   {{ t("components.admin.domains.mails_perm_deletion.delete") }} | ||||
|                   {{ page.status.Count }} | ||||
|                 </a-button> | ||||
|               </a-space> | ||||
|             </a-col> | ||||
|           </a-row> | ||||
|         </a-space> | ||||
|       </div> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| 
 | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { createVNode, onMounted, onUnmounted, reactive } from "vue"; | ||||
| import { saveAndRestart } from "@/composables/restart"; | ||||
| import { message } from "ant-design-vue"; | ||||
| import Conditions from "@/components/common/rules/Conditions.vue"; | ||||
| import ConditionValue from "@/components/common/rules/incoming/Conditions.vue"; | ||||
| import { conditionsListForMailDeletion } from "@/components/common/rules/incoming/Conditions.vue"; | ||||
| 
 | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import router from "@/router"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageSettings, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
|   UpOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import type { Condition } from "@/components/common/rules/Conditions.vue"; | ||||
| import { nextTick } from "vue"; | ||||
| import { getStatusClassNames } from "ant-design-vue/es/_util/statusUtils"; | ||||
| 
 | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 3, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 21, style: { "text-align": "center" } }; | ||||
| 
 | ||||
| interface ConditionStruct { | ||||
|   Op: string; | ||||
|   Conditions: string[][]; | ||||
| } | ||||
| 
 | ||||
| interface DeleteStatus { | ||||
|   Condition: ConditionStruct; | ||||
|   OnlyFind: boolean; | ||||
|   Active: boolean; | ||||
|   Count: number; | ||||
|   Percent: number; | ||||
|   Error: string; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   // @ts-ignore | ||||
|   timeout: NodeJS.Timeout | undefined; | ||||
|   interval: number | undefined; | ||||
|   domain: string; | ||||
|   loading: boolean; | ||||
|   valid: boolean; | ||||
|   renderConditions: boolean; | ||||
| 
 | ||||
|   status: DeleteStatus; | ||||
| }>({ | ||||
|   interval: 1000, | ||||
|   timeout: undefined, | ||||
|   domain: "", | ||||
|   loading: false, | ||||
| 
 | ||||
|   renderConditions: true, | ||||
|   valid: true, | ||||
| 
 | ||||
|   status: { | ||||
|     Condition: { | ||||
|       Op: "and", | ||||
|       Conditions: [[...conditionsListForMailDeletion[0].Args]], | ||||
|     }, | ||||
|     OnlyFind: true, | ||||
|     Active: false, | ||||
|     Count: 0, | ||||
|     Percent: 0, | ||||
|     Error: "", | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| onUnmounted(() => { | ||||
|   page.interval = undefined; | ||||
|   clearTimeout(page.timeout); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   clearTimeout(page.timeout); | ||||
| 
 | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mails_perm_deletion` | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   page.status = res.data as DeleteStatus; | ||||
|   page.renderConditions = false; | ||||
|   validate(); | ||||
|   nextTick(() => { | ||||
|     page.renderConditions = true; | ||||
|   }); | ||||
| 
 | ||||
|   if (page.status.Active && page.interval) { | ||||
|     page.timeout = setTimeout(get, page.interval); | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function start(onlyFind: boolean) { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   page.status.OnlyFind = onlyFind; | ||||
|   if (page.status.OnlyFind) { | ||||
|     page.status.Count = 0; | ||||
|   } | ||||
|   page.status.Active = true; | ||||
|   page.status.Error = ""; | ||||
|   page.status.Percent = 0; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mails_perm_deletion`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.status, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| function updateCondition(i: number, newVal: string[]) { | ||||
|   page.status.Condition.Conditions[i] = newVal; | ||||
|   validate(); | ||||
| } | ||||
| 
 | ||||
| function deleteCondition(i: number) { | ||||
|   page.status.Condition.Conditions.splice(i, 1); | ||||
|   page.renderConditions = false; | ||||
|   validate(); | ||||
|   nextTick(() => { | ||||
|     page.renderConditions = true; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function validate() { | ||||
|   let ok = true; | ||||
| 
 | ||||
|   page.status.Condition.Conditions.forEach((args) => { | ||||
|     let cond = conditionsListForMailDeletion.find( | ||||
|       (a: Condition) => a.Value === args[0] | ||||
|     ) as Condition; | ||||
|     if (cond.ValIdx != -1 && args[cond.ValIdx] === "") { | ||||
|       ok = false; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   page.valid = ok; | ||||
| } | ||||
| 
 | ||||
| function addCondition() { | ||||
|   page.status.Condition.Conditions.push(conditionsListForMailDeletion[0].Args); | ||||
|   validate(); | ||||
| } | ||||
| 
 | ||||
| function confirmDelete() { | ||||
|   Modal.confirm({ | ||||
|     title: | ||||
|       t("components.admin.domains.mails_perm_deletion.delete") + | ||||
|       page.status.Count, | ||||
|     icon: createVNode(ExclamationCircleOutlined), | ||||
|     content: t("components.admin.domains.mails_perm_deletion.delete_text"), | ||||
|     okText: t("components.admin.domains.mails_perm_deletion.ok"), | ||||
|     okType: "danger", | ||||
|     cancelText: t("components.admin.domains.mails_perm_deletion.cancel"), | ||||
|     async onOk() { | ||||
|       start(false); | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 800px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| .save-button { | ||||
|   width: 30%; | ||||
|   min-width: 150px; | ||||
|   display: block; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,151 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|         <a-form-item :label="$t('components.admin.domains.migration.enabled')"> | ||||
|           <a-switch v-model:checked="page.settings.Enabled"> </a-switch> | ||||
|         </a-form-item> | ||||
|         <template v-if="page.settings.Enabled"> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.domains.migration.virtual_domain')" | ||||
|           > | ||||
|             <a-input v-model:value="page.settings.VirtualDomain"> </a-input> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.domains.migration.other_servers')" | ||||
|           > | ||||
|             <a-textarea v-model:value="page.settings.OtherServersStr"> | ||||
|             </a-textarea> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.domains.migration.local_emails')" | ||||
|           > | ||||
|             <a-textarea v-model:value="page.settings.LocalEmailsStr"> | ||||
|             </a-textarea> | ||||
|           </a-form-item> | ||||
|         </template> | ||||
| 
 | ||||
|         <a-space style="display: flex; justify-content: center"> | ||||
|           <a-button | ||||
|             :disabled=" | ||||
|               (page.settings.Enabled && | ||||
|                 (!page.settings.VirtualDomain || | ||||
|                   !page.settings.OtherServersStr)) || | ||||
|               !page.contentLoaded | ||||
|             " | ||||
|             class="save-button" | ||||
|             type="primary" | ||||
|             size="large" | ||||
|             style="width: fit-content" | ||||
|             @click="save" | ||||
|           > | ||||
|             {{ $t("components.admin.domains.migration.save") }} | ||||
|           </a-button> | ||||
|         </a-space> | ||||
|       </a-form> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { saveAndRestart } from "@/composables/restart"; | ||||
| import { message } from "ant-design-vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   contentLoaded: boolean; | ||||
|   settings: { | ||||
|     Enabled: boolean; | ||||
|     VirtualDomain: string; | ||||
|     OtherServers: string[]; | ||||
|     OtherServersStr: string; | ||||
|     LocalEmails: string[]; | ||||
|     LocalEmailsStr: string; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   contentLoaded: false, | ||||
|   settings: { | ||||
|     Enabled: false, | ||||
|     VirtualDomain: "", | ||||
|     OtherServers: [], | ||||
|     OtherServersStr: "", | ||||
|     LocalEmails: [], | ||||
|     LocalEmailsStr: "", | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const labelCol = { span: 16, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 8, style: { "text-align": "right" } }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| function loadTextArea() { | ||||
|   page.settings.OtherServersStr = page.settings.OtherServers.join("\n"); | ||||
|   page.settings.LocalEmailsStr = page.settings.LocalEmails.join("\n"); | ||||
| } | ||||
| 
 | ||||
| function saveTextArea() { | ||||
|   page.settings.OtherServers = page.settings.OtherServersStr.split("\n"); | ||||
|   page.settings.LocalEmails = page.settings.LocalEmailsStr.split("\n"); | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch(`/admin/domains/${page.domain}/migration`); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.settings = res.data; | ||||
|   page.contentLoaded = true; | ||||
| 
 | ||||
|   loadTextArea(); | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function save() { | ||||
|   saveTextArea(); | ||||
|   const res = await apiFetch(`/admin/domains/${page.domain}/migration`, { | ||||
|     method: "POST", | ||||
|     body: page.settings, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.admin.domains.migration.success")); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   width: 600px; | ||||
|   max-width: 40%; | ||||
|   margin: auto; | ||||
| } | ||||
| .input-number { | ||||
|   width: 50%; | ||||
| } | ||||
| .save-button { | ||||
|   width: 30%; | ||||
|   min-width: 150px; | ||||
|   display: block; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,68 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <Rules | ||||
|       :get-get-url="getUrl" | ||||
|       :get-delete-url="getUrl" | ||||
|       :get-save-url="getUrl" | ||||
|       :get-move-url="moveUrl" | ||||
|       :get-enable-url="enableUrl" | ||||
|       :conditions="conditionsList" | ||||
|       :actions="actionsList" | ||||
|     > | ||||
|       <template #condition-value="valueProps"> | ||||
|         <ConditionValue | ||||
|           :change="valueProps.change" | ||||
|           :value="valueProps.value" | ||||
|           :type="valueProps.type" | ||||
|           :get-groups-url="groupsUrl" | ||||
|         > | ||||
|         </ConditionValue> | ||||
|       </template> | ||||
|       <template #action-value="valueProps"> | ||||
|         <ActionValue | ||||
|           :change="valueProps.change" | ||||
|           :value="valueProps.value" | ||||
|           :type="valueProps.type" | ||||
|           :get-groups-url="groupsUrl" | ||||
|           :get-group-emails-url="groupEmailsUrl" | ||||
|         > | ||||
|         </ActionValue> | ||||
|       </template> | ||||
|     </Rules> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import ConditionValue from "@/components/common/rules/outgoing/Conditions.vue"; | ||||
| import ActionValue from "@/components/common/rules/outgoing/Actions.vue"; | ||||
| import { conditionsList } from "@/components/common/rules/outgoing/Conditions.vue"; | ||||
| import { actionsList } from "@/components/common/rules/outgoing/Actions.vue"; | ||||
| import Rules from "@/components/common/rules/Rules.vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| function getUrl(): string { | ||||
|   return `/admin/domains/${route.params.domain as string}/outgoing_rules`; | ||||
| } | ||||
| 
 | ||||
| function moveUrl(): string { | ||||
|   return `/admin/domains/${route.params.domain as string}/outgoing_rules/move`; | ||||
| } | ||||
| 
 | ||||
| function groupsUrl(): string { | ||||
|   return `/admin/domains/${ | ||||
|     route.params.domain as string | ||||
|   }/outgoing_rules/groups`; | ||||
| } | ||||
| function groupEmailsUrl(): string { | ||||
|   return `/admin/domains/${ | ||||
|     route.params.domain as string | ||||
|   }/outgoing_rules/group_emails`; | ||||
| } | ||||
| 
 | ||||
| function enableUrl(): string { | ||||
|   return `/admin/domains/${ | ||||
|     route.params.domain as string | ||||
|   }/outgoing_rules/enable`; | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,25 @@ | |||
| <template> | ||||
|   <AddressBooks | ||||
|     v-if="page.loaded" | ||||
|     :domain="page.domain" | ||||
|     user="admin" | ||||
|   ></AddressBooks> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import AddressBooks from "@/components/common/AddressBooks.vue"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   loaded: boolean; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   loaded: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   page.loaded = true; | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,21 @@ | |||
| <template> | ||||
|   <Calendars v-if="page.loaded" :domain="page.domain" user="admin"></Calendars> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import Calendars from "@/components/common/Calendars.vue"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   loaded: boolean; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   loaded: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   page.loaded = true; | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,25 @@ | |||
| <template> | ||||
|   <SharedFolders | ||||
|     v-if="page.loaded" | ||||
|     :domain="page.domain" | ||||
|     user="admin" | ||||
|   ></SharedFolders> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import SharedFolders from "@/components/common/SharedFolders.vue"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   loaded: boolean; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   loaded: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   page.loaded = true; | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,471 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form | ||||
|         style="margin-bottom: -24px" | ||||
|         :label-col="labelCol" | ||||
|         :wrapper-col="wrapperCol" | ||||
|         :labelWrap="true" | ||||
|       > | ||||
|         <a-form-item | ||||
|           :label="$t('components.admin.domains.cidr_access.policy.enabled')" | ||||
|         > | ||||
|           <a-switch | ||||
|             :disabled="page.enabled === undefined" | ||||
|             @change="setEnabled" | ||||
|             v-model:checked="page.enabled" | ||||
|           > | ||||
|           </a-switch> | ||||
|         </a-form-item> | ||||
|       </a-form> | ||||
|     </a-card> | ||||
|     <br /> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-input-search | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder=" | ||||
|                   t('components.admin.domains.cidr_access.policy.search') | ||||
|                 " | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col :span="6"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button type="primary" @click="showAddModal(undefined)"> | ||||
|                 <PlusOutlined /> | ||||
|                 {{ | ||||
|                   t("components.admin.domains.cidr_access.policy.add_policy") | ||||
|                 }} | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 v-if="page.withSearch" | ||||
|                 :title=" | ||||
|                   t( | ||||
|                     'components.admin.domains.cidr_access.policy.delete_found' | ||||
|                   ) + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.admin.domains.cidr_access.policy.ok')" | ||||
|                 :cancel-text=" | ||||
|                   t('components.admin.domains.cidr_access.policy.cancel') | ||||
|                 " | ||||
|                 @confirm="deletePolicy(undefined)" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.admin.domains.cidr_access.policy.delete_found") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|               <a-popconfirm | ||||
|                 v-else | ||||
|                 :title=" | ||||
|                   t('components.admin.domains.cidr_access.policy.delete_all') + | ||||
|                   '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.admin.domains.cidr_access.policy.ok')" | ||||
|                 :cancel-text=" | ||||
|                   t('components.admin.domains.cidr_access.policy.cancel') | ||||
|                 " | ||||
|                 @confirm="deletePolicy(undefined)" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.admin.domains.cidr_access.policy.delete_all") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button type="primary" @click="showAddModal(record)">{{ | ||||
|               t("components.admin.domains.cidr_access.policy.properties") | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title=" | ||||
|                 t('components.admin.domains.cidr_access.policy.delete') + '?' | ||||
|               " | ||||
|               :ok-text="t('components.admin.domains.cidr_access.policy.ok')" | ||||
|               :cancel-text=" | ||||
|                 t('components.admin.domains.cidr_access.policy.cancel') | ||||
|               " | ||||
|               @confirm="deletePolicy(record.GroupName)" | ||||
|             > | ||||
|               <a-button danger>{{ | ||||
|                 t("components.admin.domains.cidr_access.policy.delete") | ||||
|               }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     style="width: 900px" | ||||
|     v-model:open="page.add.show" | ||||
|     :title=" | ||||
|       page.add.isUpdate | ||||
|         ? t('components.admin.domains.cidr_access.policy.update_policy') + | ||||
|           page.add.policy.GroupName | ||||
|         : t('components.admin.domains.cidr_access.policy.add_policy') | ||||
|     " | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-space direction="vertical"> | ||||
|       <a-select | ||||
|         v-if="!page.add.isUpdate" | ||||
|         :placeholder=" | ||||
|           t('components.admin.domains.cidr_access.policy.select_group') | ||||
|         " | ||||
|         style="width: 100%" | ||||
|         :disabled="page.add.avaliableGroups.length === 0" | ||||
|         v-model:value="page.add.policy.GroupName" | ||||
|         :options="page.add.avaliableGroups" | ||||
|       > | ||||
|       </a-select> | ||||
|       <a-transfer | ||||
|         v-model:target-keys="page.add.pools_ids" | ||||
|         :data-source="page.pools" | ||||
|         :render="(item: any) => item.name" | ||||
|         show-search | ||||
|         :titles="[' (avaliable pools)', ' (active pools)']" | ||||
|         :list-style="{ | ||||
|           width: '405px', | ||||
|           height: '320px', | ||||
|         }" | ||||
|       /> | ||||
|     </a-space> | ||||
|     <a-divider></a-divider> | ||||
| 
 | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         type="primary" | ||||
|         :disabled="!page.add.policy.GroupName || page.add.pools_ids.length == 0" | ||||
|         @click="add" | ||||
|       > | ||||
|         {{ | ||||
|           page.add.isUpdate | ||||
|             ? t("components.admin.domains.cidr_access.policy.update_policy") + | ||||
|               page.add.policy.GroupName | ||||
|             : t("components.admin.domains.cidr_access.policy.add_policy") | ||||
|         }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 16, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 8, style: { "text-align": "right" } }; | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.cidr_access.policy.col_name"), | ||||
|     dataIndex: "GroupName", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.cidr_access.policy.col_count"), | ||||
|     dataIndex: "Count", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| interface Pool { | ||||
|   ID: number; | ||||
|   Name: string; | ||||
| } | ||||
| 
 | ||||
| interface Policy { | ||||
|   GroupName?: string; | ||||
|   Pools: Pool[]; | ||||
| } | ||||
| 
 | ||||
| interface TransferElem { | ||||
|   key: string; | ||||
|   name: string; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   enabled?: boolean; | ||||
|   pools: TransferElem[]; | ||||
| 
 | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   add: { | ||||
|     show: boolean; | ||||
|     pools_ids: string[]; | ||||
|     isUpdate: boolean; | ||||
|     avaliableGroups: any[]; | ||||
|     policy: Policy; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   enabled: undefined, | ||||
|   pools: [], | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   add: { | ||||
|     show: false, | ||||
|     pools_ids: [], | ||||
|     isUpdate: false, | ||||
|     avaliableGroups: [], | ||||
|     policy: { | ||||
|       GroupName: "", | ||||
|       Pools: [], | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
|   getEnabled(); | ||||
|   getPools(); | ||||
| }); | ||||
| 
 | ||||
| async function getPools() { | ||||
|   const res = await apiFetch(`/admin/domains/${page.domain}/cidr_access/pools`); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.pools = []; | ||||
|   page.pools = (res.data as Array<any>).map((e) => { | ||||
|     return { | ||||
|       key: String(e.ID), | ||||
|       name: e.Name, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function getEnabled() { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/cidr_access/policy/enabled` | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.enabled = res.data; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function setEnabled() { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/cidr_access/policy/enabled`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: { | ||||
|         Enabled: page.enabled, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/cidr_access/policy?` + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = (res.data as any[]).map((e) => { | ||||
|     return { | ||||
|       Count: (e.Pools as any[]).length, | ||||
|       ...e, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
|   page.add.policy.Pools = page.add.pools_ids.map((e) => { | ||||
|     return { | ||||
|       ID: Number(e), | ||||
|       Name: "", | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/cidr_access/policy`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.add.policy, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function showAddModal(policy: Policy | undefined) { | ||||
|   if (policy) { | ||||
|     page.add.policy.GroupName = policy.GroupName; | ||||
|     page.add.policy.Pools = policy.Pools; | ||||
|     page.add.isUpdate = true; | ||||
|     page.add.avaliableGroups = [{ value: policy.GroupName }]; | ||||
|     page.add.pools_ids = policy.Pools.map((e) => { | ||||
|       return String(e.ID); | ||||
|     }); | ||||
|   } else { | ||||
|     page.add.policy.GroupName = undefined; | ||||
|     page.add.policy.Pools = []; | ||||
|     page.add.pools_ids = []; | ||||
|     page.add.isUpdate = false; | ||||
| 
 | ||||
|     const res = await apiFetch( | ||||
|       `/admin/domains/${page.domain}/cidr_access/policy/avaliable_groups` | ||||
|     ); | ||||
| 
 | ||||
|     if (res.error) { | ||||
|       notifyError(res.error); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     page.add.avaliableGroups = []; | ||||
|     page.add.avaliableGroups = (res.data as string[]).map((e: string) => { | ||||
|       return { | ||||
|         value: e, | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = true; | ||||
| } | ||||
| 
 | ||||
| async function deletePolicy(group: string | undefined) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/cidr_access/policy?` + params.toString(), | ||||
|     { | ||||
|       method: "DELETE", | ||||
|       body: { | ||||
|         GroupName: group, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,397 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-input-search | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder=" | ||||
|                   t('components.admin.domains.cidr_access.pools.search') | ||||
|                 " | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col :span="6"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button type="primary" @click="showAddModal"> | ||||
|                 <PlusOutlined /> | ||||
|                 {{ t("components.admin.domains.cidr_access.pools.add_pool") }} | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 v-if="page.withSearch" | ||||
|                 :title=" | ||||
|                   t('components.admin.domains.cidr_access.pools.delete_found') + | ||||
|                   '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.admin.domains.cidr_access.pools.ok')" | ||||
|                 :cancel-text=" | ||||
|                   t('components.admin.domains.cidr_access.pools.cancel') | ||||
|                 " | ||||
|                 @confirm="deletePool(undefined)" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.admin.domains.cidr_access.pools.delete_found") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|               <a-popconfirm | ||||
|                 v-else | ||||
|                 :title=" | ||||
|                   t('components.admin.domains.cidr_access.pools.delete_all') + | ||||
|                   '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.admin.domains.cidr_access.pools.ok')" | ||||
|                 :cancel-text=" | ||||
|                   t('components.admin.domains.cidr_access.pools.cancel') | ||||
|                 " | ||||
|                 @confirm="deletePool(undefined)" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.admin.domains.cidr_access.pools.delete_all") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button type="primary" @click="showPropModal(record)">{{ | ||||
|               t("components.admin.domains.cidr_access.pools.properties") | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title=" | ||||
|                 t('components.admin.domains.cidr_access.pools.delete') + '?' | ||||
|               " | ||||
|               :ok-text="t('components.admin.domains.cidr_access.pools.ok')" | ||||
|               :cancel-text=" | ||||
|                 t('components.admin.domains.cidr_access.pools.cancel') | ||||
|               " | ||||
|               @confirm="deletePool(record.ID)" | ||||
|             > | ||||
|               <a-button danger>{{ | ||||
|                 t("components.admin.domains.cidr_access.pools.delete") | ||||
|               }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     style="width: 500px" | ||||
|     v-model:open="page.add.show" | ||||
|     :title="t('components.admin.domains.cidr_access.pools.add_pool')" | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.cidr_access.pools.add_name')" | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.add.pool.Name"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.cidr_access.pools.add_ips')" | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-textarea | ||||
|           :placeholder=" | ||||
|             $t('components.admin.domains.cidr_access.pools.add_ips_placeholder') | ||||
|           " | ||||
|           v-model:value="page.add.pool.IPsString" | ||||
|         > | ||||
|         </a-textarea> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
| 
 | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         type="primary" | ||||
|         :disabled="!page.add.pool.Name || page.add.pool.IPsString == ''" | ||||
|         @click="add" | ||||
|       > | ||||
|         {{ t("components.admin.domains.cidr_access.pools.add_pool") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
|   <a-modal | ||||
|     style="width: 500px" | ||||
|     v-model:open="page.props.show" | ||||
|     :title="t('components.admin.domains.cidr_access.pools.properties')" | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.cidr_access.pools.add_name')" | ||||
|       > | ||||
|         <a-input disabled v-model:value="page.props.pool.Name"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.cidr_access.pools.add_ips')" | ||||
|       > | ||||
|         <a-textarea | ||||
|           :placeholder=" | ||||
|             $t('components.admin.domains.cidr_access.pools.add_ips_placeholder') | ||||
|           " | ||||
|           v-model:value="page.props.pool.IPsString" | ||||
|         > | ||||
|         </a-textarea> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         type="primary" | ||||
|         :disabled="!page.props.pool.Name || page.props.pool.IPsString == ''" | ||||
|         @click="update" | ||||
|       > | ||||
|         {{ t("components.admin.domains.cidr_access.pools.update_pool") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 8 }; | ||||
| const wrapperCol = { span: 16 }; | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.cidr_access.pools.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| interface Pool { | ||||
|   Name: string; | ||||
|   IPs: string[]; | ||||
|   IPsString: string; | ||||
|   ID: number; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
| 
 | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   add: { | ||||
|     show: boolean; | ||||
|     pool: Pool; | ||||
|   }; | ||||
|   props: { | ||||
|     show: boolean; | ||||
|     pool: Pool; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
| 
 | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   add: { | ||||
|     show: false, | ||||
|     pool: { | ||||
|       ID: 0, | ||||
|       Name: "", | ||||
|       IPs: [], | ||||
|       IPsString: "", | ||||
|     }, | ||||
|   }, | ||||
|   props: { | ||||
|     show: false, | ||||
|     pool: { | ||||
|       ID: 0, | ||||
|       Name: "", | ||||
|       IPs: [], | ||||
|       IPsString: "", | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/cidr_access/pools?` + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
|   page.add.pool.IPs = page.add.pool.IPsString.split("\n"); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/cidr_access/pools`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.add.pool, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function update() { | ||||
|   page.props.pool.IPs = page.props.pool.IPsString.split("\n"); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/cidr_access/pools`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.props.pool, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.props.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function showPropModal(record: any) { | ||||
|   page.props.pool = { ...record }; | ||||
|   page.props.pool.IPsString = page.props.pool.IPs.join("\n"); | ||||
|   page.props.show = true; | ||||
| } | ||||
| 
 | ||||
| async function showAddModal() { | ||||
|   page.add.pool.Name = ""; | ||||
|   page.add.pool.ID = 0; | ||||
|   page.add.pool.IPsString = ""; | ||||
|   page.add.pool.IPs = []; | ||||
|   page.add.show = true; | ||||
| } | ||||
| 
 | ||||
| async function deletePool(id: number | undefined) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/cidr_access/pools?` + params.toString(), | ||||
|     { | ||||
|       method: "DELETE", | ||||
|       body: { | ||||
|         ID: id, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,158 @@ | |||
| <template> | ||||
|   <a-modal | ||||
|     v-model:open="open" | ||||
|     :title=" | ||||
|       t('components.admin.domains.mailstorage.export.title') + ' ' + props.email | ||||
|     " | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form-item v-if="page.error"> | ||||
|       <a-alert type="error" showIcon :message="page.error" /> | ||||
|     </a-form-item> | ||||
|     <a-form-item v-if="page.duration"> | ||||
|       <a-alert | ||||
|         type="success" | ||||
|         showIcon | ||||
|         :message=" | ||||
|           t('components.admin.domains.mailstorage.export.success') + | ||||
|           page.duration | ||||
|         " | ||||
|       /> | ||||
|     </a-form-item> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|       <a-form-item | ||||
|         v-if="page.hostname" | ||||
|         :label="$t('components.admin.domains.mailstorage.export.hostname')" | ||||
|       > | ||||
|         <a-input :disabled="true" v-model:value="page.hostname"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.mailstorage.export.catalog')" | ||||
|       > | ||||
|         <a-input | ||||
|           :disabled="page.active || page.loading" | ||||
|           v-model:value="page.targetFolder" | ||||
|         > | ||||
|         </a-input> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <a-progress | ||||
|       v-if="page.active || page.percent !== 0" | ||||
|       :percent="page.percent" | ||||
|     /> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         @click="start" | ||||
|         v-if="!page.active && !page.duration" | ||||
|         :disabled="page.loading" | ||||
|         type="primary" | ||||
|       > | ||||
|         {{ t("components.admin.domains.mailstorage.export.start_export") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { onMounted, onUnmounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 8 }; | ||||
| const wrapperCol = { span: 16 }; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   email: string; | ||||
| }>(); | ||||
| 
 | ||||
| const open = defineModel("open"); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   // @ts-ignore | ||||
|   timeout: NodeJS.Timeout | undefined; | ||||
|   interval: number | undefined; | ||||
|   loading: boolean; | ||||
|   domain: string; | ||||
|   targetFolder: string; | ||||
|   hostname: string; | ||||
|   active: boolean; | ||||
|   percent: number; | ||||
|   error: string; | ||||
|   duration: string; | ||||
| }>({ | ||||
|   interval: 1000, | ||||
|   timeout: undefined, | ||||
|   loading: false, | ||||
|   percent: 0, | ||||
|   active: false, | ||||
|   domain: "", | ||||
|   targetFolder: "", | ||||
|   hostname: "", | ||||
|   error: "", | ||||
|   duration: "", | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| onUnmounted(() => { | ||||
|   page.interval = undefined; | ||||
|   clearTimeout(page.timeout); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   clearTimeout(page.timeout); | ||||
| 
 | ||||
|   page.loading = true; | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mailstorage/mailboxes/${props.email}/export` | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   page.active = res.data.Active; | ||||
|   page.percent = res.data.Percent; | ||||
|   page.hostname = res.data.Hostname; | ||||
|   page.targetFolder = res.data.TargetFolder; | ||||
|   page.error = res.data.Error; | ||||
|   page.duration = res.data.Duration; | ||||
| 
 | ||||
|   if (page.active && page.interval) { | ||||
|     page.timeout = setTimeout(get, page.interval); | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function start() { | ||||
|   page.loading = true; | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mailstorage/mailboxes/${props.email}/export`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: { | ||||
|         TargetFolder: page.targetFolder, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,747 @@ | |||
| <template> | ||||
|   <a-table | ||||
|     class="ant-table-striped" | ||||
|     :columns="page.columns" | ||||
|     :data-source="page.data" | ||||
|     :loading="page.loading" | ||||
|     bordered | ||||
|     style="min-width: 830px" | ||||
|     size="small" | ||||
|     :pagination="false" | ||||
|     :rowClassName=" | ||||
|         (record: any, index: any) => (record.Archived ? 'striped' : undefined) | ||||
|       " | ||||
|   > | ||||
|     <template #bodyCell="{ column, record }"> | ||||
|       <template v-if="column.key === 'email'"> | ||||
|         <a-space> | ||||
|           <a-tooltip | ||||
|             :title=" | ||||
|               t(`components.admin.domains.mailstorage.mailboxes.in_archive`) | ||||
|             " | ||||
|           > | ||||
|             <LockOutlined v-if="record.Archived" /> | ||||
|           </a-tooltip> | ||||
|           {{ record.Email }} | ||||
|         </a-space> | ||||
|       </template> | ||||
|       <template v-if="column.key === 'action'"> | ||||
|         <a-dropdown> | ||||
|           <template #overlay> | ||||
|             <a-menu> | ||||
|               <a-menu-item key="1"> | ||||
|                 <a-button | ||||
|                   style="width: 100%" | ||||
|                   @click="showRenameWindow(record.Email as string)" | ||||
|                   >{{ | ||||
|                     t("components.admin.domains.mailstorage.mailboxes.rename") | ||||
|                   }}</a-button | ||||
|                 > | ||||
|               </a-menu-item> | ||||
|               <a-menu-item key="2"> | ||||
|                 <a-button | ||||
|                   style="width: 100%" | ||||
|                   @click=" | ||||
|                     page.export.email = record.Email; | ||||
|                     page.export.show = true; | ||||
|                   " | ||||
|                   >{{ | ||||
|                     t("components.admin.domains.mailstorage.mailboxes.export") | ||||
|                   }}</a-button | ||||
|                 > | ||||
|               </a-menu-item> | ||||
|               <a-menu-item v-if="page.type !== 'maildirStorage'" key="3"> | ||||
|                 <a-button | ||||
|                   style="width: 100%" | ||||
|                   @click="clearRecovery(record.Email)" | ||||
|                   >{{ | ||||
|                     t( | ||||
|                       "components.admin.domains.mailstorage.mailboxes.clear_recovery" | ||||
|                     ) | ||||
|                   }}</a-button | ||||
|                 > | ||||
|               </a-menu-item> | ||||
|               <a-menu-item key="4"> | ||||
|                 <a-button | ||||
|                   :disabled="page.multistorageAccessKeys.length < 2" | ||||
|                   style="width: 100%" | ||||
|                   @click=" | ||||
|                     showChangeStorage( | ||||
|                       record.Email, | ||||
|                       record.MultistorageAccessKey | ||||
|                     ) | ||||
|                   " | ||||
|                   >{{ | ||||
|                     t( | ||||
|                       "components.admin.domains.mailstorage.mailboxes.change_storage" | ||||
|                     ) | ||||
|                   }}</a-button | ||||
|                 > | ||||
|               </a-menu-item> | ||||
|               <a-menu-item key="5"> | ||||
|                 <a-button | ||||
|                   v-if="record.Archived" | ||||
|                   style="width: 100%" | ||||
|                   @click="setArchiveMailbox(record.Email, false)" | ||||
|                   >{{ | ||||
|                     t( | ||||
|                       "components.admin.domains.mailstorage.mailboxes.unarchive" | ||||
|                     ) | ||||
|                   }}</a-button | ||||
|                 > | ||||
|                 <a-button | ||||
|                   v-else | ||||
|                   style="width: 100%" | ||||
|                   @click="setArchiveMailbox(record.Email, true)" | ||||
|                   >{{ | ||||
|                     t("components.admin.domains.mailstorage.mailboxes.archive") | ||||
|                   }}</a-button | ||||
|                 > | ||||
|               </a-menu-item> | ||||
|               <a-menu-item key="6"> | ||||
|                 <a-button | ||||
|                   @click=" | ||||
|                     deleteMailbox( | ||||
|                       t( | ||||
|                         'components.admin.domains.mailstorage.mailboxes.delete' | ||||
|                       ) + | ||||
|                         ' ' + | ||||
|                         record.Email + | ||||
|                         '?', | ||||
|                       record.Email | ||||
|                     ) | ||||
|                   " | ||||
|                   style="width: 100%" | ||||
|                   danger | ||||
|                   :disabled="record.Archived" | ||||
|                   >{{ | ||||
|                     t("components.admin.domains.mailstorage.mailboxes.delete") | ||||
|                   }}</a-button | ||||
|                 > | ||||
|               </a-menu-item> | ||||
|             </a-menu> | ||||
|           </template> | ||||
|           <a> | ||||
|             {{ | ||||
|               t("components.admin.domains.mailstorage.mailboxes.col_actions") | ||||
|             }} | ||||
|             <DownOutlined /> | ||||
|           </a> | ||||
|         </a-dropdown> | ||||
|       </template> | ||||
|     </template> | ||||
|     <template #title> | ||||
|       <a-row> | ||||
|         <a-col style="display: flex; align-items: flex-end" :span="12"> | ||||
|           <a-space> | ||||
|             <a-input-search | ||||
|               v-model:value="page.pagination.search" | ||||
|               :placeholder=" | ||||
|                 t('components.admin.domains.mailstorage.mailboxes.search') | ||||
|               " | ||||
|               enter-button | ||||
|               allow-clear | ||||
|               @search="applySearch" | ||||
|             /> | ||||
|             <a-select | ||||
|               style="width: 140px" | ||||
|               v-model:value="page.pagination.access_key" | ||||
|               v-if="page.multistorageAccessKeys.length > 1" | ||||
|               @change="applySearch" | ||||
|             > | ||||
|               <a-select-option value="">{{ | ||||
|                 t( | ||||
|                   "components.admin.domains.mailstorage.mailboxes.storage_filter" | ||||
|                 ) | ||||
|               }}</a-select-option> | ||||
|               <a-select-option | ||||
|                 v-for="key in page.multistorageAccessKeys" | ||||
|                 :value="key" | ||||
|                 >{{ key }}</a-select-option | ||||
|               > | ||||
|             </a-select> | ||||
|           </a-space> | ||||
|         </a-col> | ||||
|         <a-col :span="12"> | ||||
|           <a-space | ||||
|             v-if="page.withSearch" | ||||
|             style="display: flex; justify-content: end" | ||||
|           > | ||||
|             <a-button | ||||
|               v-if="page.type !== 'maildirStorage'" | ||||
|               @click="clearRecovery('')" | ||||
|               >{{ | ||||
|                 $t( | ||||
|                   "components.admin.domains.mailstorage.mailboxes.clear_recovery_found" | ||||
|                 ) | ||||
|               }}</a-button | ||||
|             > | ||||
|             <a-button | ||||
|               @click=" | ||||
|                 deleteMailbox( | ||||
|                   t( | ||||
|                     'components.admin.domains.mailstorage.mailboxes.delete_found' | ||||
|                   ) + '?', | ||||
|                   '' | ||||
|                 ) | ||||
|               " | ||||
|               danger | ||||
|               >{{ | ||||
|                 $t( | ||||
|                   "components.admin.domains.mailstorage.mailboxes.delete_found" | ||||
|                 ) | ||||
|               }}</a-button | ||||
|             > | ||||
|           </a-space> | ||||
|           <a-space v-else style="display: flex; justify-content: end"> | ||||
|             <a-button | ||||
|               v-if="page.type !== 'maildirStorage'" | ||||
|               @click="clearRecovery('')" | ||||
|               >{{ | ||||
|                 $t( | ||||
|                   "components.admin.domains.mailstorage.mailboxes.clear_recovery_all" | ||||
|                 ) | ||||
|               }}</a-button | ||||
|             > | ||||
| 
 | ||||
|             <a-button | ||||
|               @click=" | ||||
|                 deleteMailbox( | ||||
|                   t( | ||||
|                     'components.admin.domains.mailstorage.mailboxes.delete_all' | ||||
|                   ) + '?', | ||||
|                   '' | ||||
|                 ) | ||||
|               " | ||||
|               danger | ||||
|               >{{ | ||||
|                 $t("components.admin.domains.mailstorage.mailboxes.delete_all") | ||||
|               }}</a-button | ||||
|             > | ||||
|           </a-space> | ||||
|         </a-col> | ||||
|       </a-row> | ||||
|     </template> | ||||
|     <template #footer> | ||||
|       <a-flex justify="flex-end"> | ||||
|         <a-pagination | ||||
|           v-model:value="page.pagination.current" | ||||
|           :defaultPageSize="page.pagination.size" | ||||
|           :total="page.pagination.total" | ||||
|           @change="pageChange" | ||||
|           :pageSizeOptions="['10', '20', '50', '100']" | ||||
|           v-model:pageSize="page.pagination.size" | ||||
|           :showSizeChanger="true" | ||||
|         /> | ||||
|       </a-flex> | ||||
|     </template> | ||||
|   </a-table> | ||||
|   <Export | ||||
|     v-if="page.export.show" | ||||
|     v-model:open="page.export.show" | ||||
|     :email="page.export.email" | ||||
|   ></Export> | ||||
|   <a-modal | ||||
|     :title=" | ||||
|       $t('components.admin.domains.mailstorage.mailboxes.rename') + | ||||
|       ' ' + | ||||
|       page.rename.oldEmail | ||||
|     " | ||||
|     v-model:open="page.rename.show" | ||||
|     style="width: 350px" | ||||
|   > | ||||
|     <a-space style="width: 100%" direction="vertical"> | ||||
|       <a-alert | ||||
|         v-if="page.rename.error" | ||||
|         type="error" | ||||
|         showIcon | ||||
|         :message="page.rename.error" | ||||
|         closable | ||||
|       /> | ||||
|       <a-input | ||||
|         v-model:value="page.rename.newLp" | ||||
|         :addon-after="'@' + page.domain" | ||||
|       > | ||||
|       </a-input> | ||||
|     </a-space> | ||||
| 
 | ||||
|     <template #footer> | ||||
|       <a-space> | ||||
|         <a-button @click="page.rename.show = false"> | ||||
|           {{ | ||||
|             $t("components.admin.domains.mailstorage.mailboxes.cancel") | ||||
|           }}</a-button | ||||
|         > | ||||
|         <a-button | ||||
|           :disabled="page.rename.newLp === ''" | ||||
|           :loading="page.rename.loading" | ||||
|           @click="renameMailbox" | ||||
|           type="primary" | ||||
|         > | ||||
|           {{ | ||||
|             $t("components.admin.domains.mailstorage.mailboxes.rename") | ||||
|           }}</a-button | ||||
|         > | ||||
|       </a-space> | ||||
|     </template> | ||||
|   </a-modal> | ||||
|   <a-modal | ||||
|     :title=" | ||||
|       $t('components.admin.domains.mailstorage.mailboxes.change_storage_for') + | ||||
|       page.change_storage.email | ||||
|     " | ||||
|     v-model:open="page.change_storage.show" | ||||
|     style="width: 350px" | ||||
|     :maskClosable=" | ||||
|       !page.change_storage.status.Active && !page.change_storage.loading | ||||
|     " | ||||
|     :closable=" | ||||
|       !page.change_storage.status.Active && !page.change_storage.loading | ||||
|     " | ||||
|   > | ||||
|     <a-alert | ||||
|       v-if="page.change_storage.status.Error" | ||||
|       type="error" | ||||
|       showIcon | ||||
|       :message="page.change_storage.status.Error" | ||||
|     /> | ||||
|     <a-progress | ||||
|       v-if="page.change_storage.status.Active" | ||||
|       :percent="page.change_storage.status.Percent" | ||||
|     /> | ||||
|     <a-select | ||||
|       v-if="!page.change_storage.status.Active" | ||||
|       style="width: 100%" | ||||
|       v-model:value="page.change_storage.new_access_key" | ||||
|     > | ||||
|       <a-select-option | ||||
|         v-for="key in page.change_storage.accessKeysVariants" | ||||
|         :value="key" | ||||
|         >{{ key }}</a-select-option | ||||
|       > | ||||
|     </a-select> | ||||
| 
 | ||||
|     <template #footer> | ||||
|       <a-space v-if="!page.change_storage.status.Active"> | ||||
|         <a-button @click="page.change_storage.show = false"> | ||||
|           {{ | ||||
|             $t("components.admin.domains.mailstorage.mailboxes.cancel") | ||||
|           }}</a-button | ||||
|         > | ||||
|         <a-button | ||||
|           :loading="page.change_storage.loading" | ||||
|           @click="changeStorage" | ||||
|           type="primary" | ||||
|         > | ||||
|           {{ | ||||
|             $t("components.admin.domains.mailstorage.mailboxes.change") | ||||
|           }}</a-button | ||||
|         > | ||||
|       </a-space> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HddOutlined, | ||||
|   LockOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const sqlColumns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.mailstorage.mailboxes.col_email"), | ||||
|     dataIndex: "Email", | ||||
|     key: "email", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.mailstorage.mailboxes.col_msg_count"), | ||||
|     dataIndex: "Count", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.mailstorage.mailboxes.col_size"), | ||||
|     dataIndex: "Size", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.mailstorage.mailboxes.col_quota"), | ||||
|     dataIndex: "Quota", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.mailstorage.mailboxes.col_quota_used"), | ||||
|     dataIndex: "QuotaUsed", | ||||
|   }, | ||||
|   { | ||||
|     title: t( | ||||
|       "components.admin.domains.mailstorage.mailboxes.col_recovery_size" | ||||
|     ), | ||||
|     dataIndex: "RecoverySize", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.mailstorage.mailboxes.col_table_prefix"), | ||||
|     dataIndex: "Prefix", | ||||
|   }, | ||||
|   { | ||||
|     title: t( | ||||
|       "components.admin.domains.mailstorage.mailboxes.col_table_access_key" | ||||
|     ), | ||||
|     dataIndex: "MultistorageAccessKey", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "center", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const maildirColumns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.mailstorage.mailboxes.col_email"), | ||||
|     dataIndex: "Email", | ||||
|     key: "email", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "center", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| interface ChangeStorageStatus { | ||||
|   Active: boolean; | ||||
|   Error: string; | ||||
|   Percent: number; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   rename: { | ||||
|     show: boolean; | ||||
|     oldEmail: string; | ||||
|     newLp: string; | ||||
|     error: string; | ||||
|     loading: boolean; | ||||
|   }; | ||||
|   export: { | ||||
|     show: boolean; | ||||
|     email: string; | ||||
|   }; | ||||
|   change_storage: { | ||||
|     started: boolean; | ||||
|     show: boolean; | ||||
|     loading: boolean; | ||||
|     email: string; | ||||
|     new_access_key: string; | ||||
|     accessKeysVariants: string[]; | ||||
|     status: ChangeStorageStatus; | ||||
|     timeout: number | undefined; | ||||
|     interval: number | undefined; | ||||
|   }; | ||||
|   columns: ColumnType<any>[]; | ||||
|   type: string; | ||||
|   multistorageAccessKeys: string[]; | ||||
|   domain: string; | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|     access_key: string; | ||||
|   }; | ||||
| }>({ | ||||
|   rename: { | ||||
|     show: false, | ||||
|     oldEmail: "", | ||||
|     newLp: "", | ||||
|     error: "", | ||||
|     loading: false, | ||||
|   }, | ||||
|   export: { | ||||
|     show: false, | ||||
|     email: "", | ||||
|   }, | ||||
|   change_storage: { | ||||
|     started: false, | ||||
|     interval: 1000, | ||||
|     timeout: undefined, | ||||
|     show: false, | ||||
|     loading: false, | ||||
|     email: "", | ||||
|     new_access_key: "", | ||||
|     accessKeysVariants: [], | ||||
|     status: { | ||||
|       Active: false, | ||||
|       Error: "", | ||||
|       Percent: 0, | ||||
|     }, | ||||
|   }, | ||||
|   columns: [], | ||||
|   type: "", | ||||
|   multistorageAccessKeys: [], | ||||
|   domain: "", | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|     access_key: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("access_key", page.pagination.access_key); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mailstorage/mailboxes?` + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.type = res.data.Type; | ||||
|   page.multistorageAccessKeys = res.data.MultistorageAccessKeys; | ||||
| 
 | ||||
|   if (page.type === "maildirStorage") { | ||||
|     page.columns = maildirColumns as []; | ||||
|   } else { | ||||
|     page.columns = sqlColumns as []; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data.Mailboxes; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function applySearch() { | ||||
|   page.pagination.current = 1; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function clearRecovery(email: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("email", email); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mailstorage/mailboxes/clear_recovery?` + | ||||
|       params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function setArchiveMailbox(email: string, archive: boolean) { | ||||
|   const params = new URLSearchParams(); | ||||
| 
 | ||||
|   params.append("email", email); | ||||
|   params.append("archive", String(archive)); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mailstorage/mailboxes/set_archive?` + | ||||
|       params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| function showRenameWindow(email: string) { | ||||
|   page.rename.oldEmail = email; | ||||
|   page.rename.newLp = email.split("@")[0]; | ||||
|   page.rename.show = true; | ||||
|   page.rename.error = ""; | ||||
| } | ||||
| 
 | ||||
| async function renameMailbox() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("to", page.rename.newLp); | ||||
| 
 | ||||
|   page.rename.loading = true; | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mailstorage/mailboxes/${page.rename.oldEmail}/rename?` + | ||||
|       params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
|   page.rename.loading = false; | ||||
|   if (res.error) { | ||||
|     if (res.error.includes("exists")) { | ||||
|       page.rename.error = t( | ||||
|         "components.admin.domains.mailstorage.mailboxes.rename_exists" | ||||
|       ); | ||||
|     } else { | ||||
|       notifyError(res.error); | ||||
|     } | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.rename.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function deleteMailbox(title: string, email: string) { | ||||
|   Modal.confirm({ | ||||
|     title: title, | ||||
|     icon: createVNode(ExclamationCircleOutlined), | ||||
|     async onOk() { | ||||
|       const params = new URLSearchParams(); | ||||
|       params.append("search", page.pagination.search); | ||||
|       params.append("email", email); | ||||
| 
 | ||||
|       const res = await apiFetch( | ||||
|         `/admin/domains/${page.domain}/mailstorage/mailboxes?` + | ||||
|           params.toString(), | ||||
|         { | ||||
|           method: "DELETE", | ||||
|         } | ||||
|       ); | ||||
| 
 | ||||
|       if (res.error) { | ||||
|         notifyError(res.error); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       return get(); | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function showChangeStorage(email: string, mailboxAccessKey: string) { | ||||
|   page.change_storage.loading = false; | ||||
|   page.change_storage.email = email; | ||||
|   page.change_storage.new_access_key = ""; | ||||
|   page.change_storage.accessKeysVariants = []; | ||||
|   for (let i = 0; i < page.multistorageAccessKeys.length; i++) { | ||||
|     const element = page.multistorageAccessKeys[i]; | ||||
|     if (element != mailboxAccessKey) { | ||||
|       if (page.change_storage.new_access_key === "") { | ||||
|         page.change_storage.new_access_key = element; | ||||
|       } | ||||
|       page.change_storage.accessKeysVariants.push(element); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   page.change_storage.show = await getChangeStorage(); | ||||
| } | ||||
| 
 | ||||
| async function getChangeStorage(): Promise<boolean> { | ||||
|   clearTimeout(page.change_storage.timeout); | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mailstorage/mailboxes/${page.change_storage.email}/change_storage` | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   if (res.data.Percent === 100 && res.data.Error === "") { | ||||
|     router.go(0); | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   page.change_storage.status = res.data; | ||||
| 
 | ||||
|   if (page.change_storage.status.Active && page.change_storage.interval) { | ||||
|     page.change_storage.timeout = setTimeout( | ||||
|       getChangeStorage, | ||||
|       page.change_storage.interval | ||||
|     ); | ||||
|   } else if (page.change_storage.started) { | ||||
|     router.go(0); | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| async function changeStorage() { | ||||
|   const params = new URLSearchParams(); | ||||
| 
 | ||||
|   params.append("new_access_key", page.change_storage.new_access_key); | ||||
| 
 | ||||
|   page.change_storage.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/mailstorage/mailboxes/${page.change_storage.email}/change_storage?` + | ||||
|       params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.change_storage.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
|   page.change_storage.started = true; | ||||
| 
 | ||||
|   getChangeStorage(); | ||||
|   return; | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .ant-table-striped :deep(.striped) td { | ||||
|   opacity: 1; | ||||
| } | ||||
| </style> | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,328 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       small | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-input-search | ||||
|                 style="width: 300px" | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder="t('common.misc.search')" | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col :span="6"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button type="primary" @click="newCategory"> | ||||
|                 <PlusOutlined /> | ||||
|                 {{ | ||||
|                   t( | ||||
|                     "components.admin.domains.resources.categories.add_category" | ||||
|                   ) | ||||
|                 }} | ||||
|               </a-button> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button @click="editCategory(record)">{{ | ||||
|               t("common.misc.edit") | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title="t('common.misc.delete') + '?'" | ||||
|               :ok-text="t('common.misc.ok')" | ||||
|               :cancel-text="t('common.misc.cancel')" | ||||
|               @confirm="deleteCategory(record.Id)" | ||||
|             > | ||||
|               <a-button danger>{{ t("common.misc.delete") }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'office_bind'"> | ||||
|           {{ record.OfficeBind ? t("common.misc.yes") : t("common.misc.no") }} | ||||
|         </template> | ||||
|         <template v-if="column.key === 'script'"> | ||||
|           {{ record.Script ? t("common.misc.yes") : t("common.misc.no") }} | ||||
|         </template> | ||||
|       </template> | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     style="width: 400px" | ||||
|     v-model:open="page.addUpdate.show" | ||||
|     :title=" | ||||
|       page.addUpdate.Id | ||||
|         ? t(`components.admin.domains.resources.categories.edit_category`) | ||||
|         : t(`components.admin.domains.resources.categories.add_category`) | ||||
|     " | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.resources.categories.col_name')" | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.addUpdate.Name"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label=" | ||||
|           t('components.admin.domains.resources.categories.col_office_bind') | ||||
|         " | ||||
|       > | ||||
|         <a-switch v-model:checked="page.addUpdate.OfficeBind"></a-switch> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="t('components.admin.domains.resources.categories.col_script')" | ||||
|       > | ||||
|         <a-switch v-model:checked="page.addUpdate.Script"></a-switch> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
| 
 | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         type="primary" | ||||
|         :disabled="!page.addUpdate.Name" | ||||
|         @click="addUpdateCategory" | ||||
|         :loading="page.addUpdate.loading" | ||||
|       > | ||||
|         {{ | ||||
|           page.addUpdate.Id | ||||
|             ? t("common.misc.save_changes") | ||||
|             : t("components.admin.domains.resources.categories.add_category") | ||||
|         }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import dayjs, { Dayjs } from "dayjs"; | ||||
| 
 | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   EventIdPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
|   RouteUserCalendarsEventsPlannerEdit, | ||||
|   RouteUserCalendarsEventsPlannerNew, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   CheckOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HddOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import { timeToDateTime } from "@/composables/misc"; | ||||
| import TimezoneSelect from "@/components/common/TimezoneSelect.vue"; | ||||
| import WorkDaysSelect from "@/components/common/WorkDaysSelect.vue"; | ||||
| import WorkHoursRangePicker from "@/components/common/WorkHoursRangePicker.vue"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 10, style: {} }; | ||||
| const wrapperCol = { span: 14, style: {} }; | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.resources.categories.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.resources.categories.col_office_bind"), | ||||
|     key: "office_bind", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.resources.categories.col_script"), | ||||
|     key: "script", | ||||
|   }, | ||||
|   { | ||||
|     title: "", | ||||
|     key: "action", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   loading: boolean; | ||||
|   data: any[]; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   addUpdate: { | ||||
|     show: boolean; | ||||
|     Id: number; | ||||
|     Name: string; | ||||
|     OfficeBind: boolean; | ||||
|     Script: boolean; | ||||
|     loading: boolean; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   addUpdate: { | ||||
|     loading: false, | ||||
|     show: false, | ||||
|     Id: 0, | ||||
|     Name: "", | ||||
|     OfficeBind: false, | ||||
|     Script: false, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/categories?` + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function deleteCategory(id: number) { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/categories/${id}`, | ||||
|     { | ||||
|       method: "DELETE", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess( | ||||
|     t("components.admin.domains.resources.categories.delete_success") | ||||
|   ); | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function editCategory(record: any) { | ||||
|   page.addUpdate = { ...record }; | ||||
|   page.addUpdate.show = true; | ||||
| } | ||||
| 
 | ||||
| async function addUpdateCategory() { | ||||
|   page.addUpdate.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/categories`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.addUpdate, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.addUpdate.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (page.addUpdate.Id !== 0) { | ||||
|     notifySuccess( | ||||
|       t("components.admin.domains.resources.categories.update_success") | ||||
|     ); | ||||
|   } else { | ||||
|     notifySuccess( | ||||
|       t("components.admin.domains.resources.categories.create_success") | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   page.addUpdate.show = false; | ||||
|   get(); | ||||
| } | ||||
| 
 | ||||
| async function newCategory() { | ||||
|   page.addUpdate.Id = 0; | ||||
|   page.addUpdate.Name = ""; | ||||
|   page.addUpdate.OfficeBind = false; | ||||
|   page.addUpdate.Script = false; | ||||
|   page.addUpdate.show = true; | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,352 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       small | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-input-search | ||||
|                 style="width: 300px" | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder="t('common.misc.search')" | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col :span="6"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button type="primary" @click="newOffice"> | ||||
|                 <PlusOutlined /> | ||||
|                 {{ t("components.admin.domains.resources.offices.add_office") }} | ||||
|               </a-button> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button @click="editOffice(record)">{{ | ||||
|               t("common.misc.edit") | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title="t('common.misc.delete') + '?'" | ||||
|               :ok-text="t('common.misc.ok')" | ||||
|               :cancel-text="t('common.misc.cancel')" | ||||
|               @confirm="deleteOffice(record.Id)" | ||||
|             > | ||||
|               <a-button danger>{{ t("common.misc.delete") }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'timezone'"> | ||||
|           UTC{{ record.Timezone }} | ||||
|         </template> | ||||
|       </template> | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     style="width: 400px" | ||||
|     v-model:open="page.addUpdate.show" | ||||
|     :title=" | ||||
|       page.addUpdate.Id | ||||
|         ? t(`components.admin.domains.resources.offices.edit_office`) | ||||
|         : t(`components.admin.domains.resources.offices.add_office`) | ||||
|     " | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.resources.offices.col_name')" | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.addUpdate.Name"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="t('components.user.calendars.share_free_time.timezone')" | ||||
|       > | ||||
|         <TimezoneSelect v-model:value="page.addUpdate.Timezone"> | ||||
|         </TimezoneSelect> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="t('components.user.calendars.share_free_time.work_days')" | ||||
|       > | ||||
|         <WorkDaysSelect v-model:value="page.addUpdate.WorkDays"> | ||||
|         </WorkDaysSelect> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="t('components.user.calendars.share_free_time.work_hours')" | ||||
|       > | ||||
|         <WorkHoursRangePicker v-model:value="page.addUpdate.workTimeValues"> | ||||
|         </WorkHoursRangePicker> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
| 
 | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         type="primary" | ||||
|         :disabled="!page.addUpdate.Name" | ||||
|         @click="addUpdateOffice" | ||||
|         :loading="page.addUpdate.loading" | ||||
|       > | ||||
|         {{ | ||||
|           page.addUpdate.Id | ||||
|             ? t("common.misc.save_changes") | ||||
|             : t("components.admin.domains.resources.offices.add_office") | ||||
|         }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import dayjs, { Dayjs } from "dayjs"; | ||||
| 
 | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   EventIdPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
|   RouteUserCalendarsEventsPlannerEdit, | ||||
|   RouteUserCalendarsEventsPlannerNew, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HddOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import { timeToDateTime } from "@/composables/misc"; | ||||
| import TimezoneSelect from "@/components/common/TimezoneSelect.vue"; | ||||
| import WorkDaysSelect from "@/components/common/WorkDaysSelect.vue"; | ||||
| import WorkHoursRangePicker from "@/components/common/WorkHoursRangePicker.vue"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 8, style: {} }; | ||||
| const wrapperCol = { span: 16, style: {} }; | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.resources.offices.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.resources.offices.col_timezone"), | ||||
|     key: "timezone", | ||||
|   }, | ||||
|   { | ||||
|     title: "", | ||||
|     key: "action", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   loading: boolean; | ||||
|   data: any[]; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   addUpdate: { | ||||
|     show: boolean; | ||||
|     Id: number; | ||||
|     Name: string; | ||||
|     Timezone: string; | ||||
|     WorkDays: string[]; | ||||
|     WorkTime: string[]; | ||||
|     workTimeValues: any[]; | ||||
|     loading: boolean; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   addUpdate: { | ||||
|     loading: false, | ||||
|     show: false, | ||||
|     Id: 0, | ||||
|     Name: "", | ||||
|     Timezone: "", | ||||
|     WorkDays: [], | ||||
|     WorkTime: [], | ||||
|     workTimeValues: [], | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/offices?` + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function deleteOffice(id: number) { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/offices/${id}`, | ||||
|     { | ||||
|       method: "DELETE", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.admin.domains.resources.offices.delete_success")); | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function editOffice(record: any) { | ||||
|   page.addUpdate = { ...record }; | ||||
|   page.addUpdate.workTimeValues = [ | ||||
|     dayjs(Date.parse("1970-01-01 " + page.addUpdate.WorkTime[0])), | ||||
|     dayjs(Date.parse("1970-01-01 " + page.addUpdate.WorkTime[1])), | ||||
|   ]; | ||||
|   page.addUpdate.show = true; | ||||
| } | ||||
| 
 | ||||
| async function addUpdateOffice() { | ||||
|   page.addUpdate.WorkTime[0] = page.addUpdate.workTimeValues[0].format("HH:mm"); | ||||
|   page.addUpdate.WorkTime[1] = page.addUpdate.workTimeValues[1].format("HH:mm"); | ||||
| 
 | ||||
|   page.addUpdate.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/offices`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.addUpdate, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.addUpdate.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (page.addUpdate.Id !== 0) { | ||||
|     notifySuccess( | ||||
|       t("components.admin.domains.resources.offices.update_success") | ||||
|     ); | ||||
|   } else { | ||||
|     notifySuccess( | ||||
|       t("components.admin.domains.resources.offices.create_success") | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   page.addUpdate.show = false; | ||||
|   get(); | ||||
| } | ||||
| 
 | ||||
| function getStringTimezone(): string { | ||||
|   let tz = new Date().getTimezoneOffset(); | ||||
|   tz = tz / -60; | ||||
|   let str = "+"; | ||||
|   if (tz < 0) { | ||||
|     str = "-"; | ||||
|     tz *= -1; | ||||
|   } | ||||
| 
 | ||||
|   if (tz < 10) { | ||||
|     str += "0"; | ||||
|   } | ||||
| 
 | ||||
|   return str + tz.toString(); | ||||
| } | ||||
| 
 | ||||
| async function newOffice() { | ||||
|   page.addUpdate.Id = 0; | ||||
|   page.addUpdate.Name = ""; | ||||
|   page.addUpdate.Timezone = getStringTimezone(); | ||||
|   page.addUpdate.WorkDays = ["1", "2", "3", "4", "5"]; | ||||
|   page.addUpdate.WorkTime = ["10:00", "19:00"]; | ||||
|   page.addUpdate.workTimeValues = [ | ||||
|     dayjs(Date.parse("1970-01-01 " + page.addUpdate.WorkTime[0])), | ||||
|     dayjs(Date.parse("1970-01-01 " + page.addUpdate.WorkTime[1])), | ||||
|   ]; | ||||
|   page.addUpdate.show = true; | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 400px; | ||||
|   max-width: 40%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,608 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       small | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-input-search | ||||
|                 style="width: 300px" | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder="t('common.misc.search')" | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|               <a-select v-model:value="page.pagination.category" @change="get"> | ||||
|                 <a-select-option :value="''">{{ | ||||
|                   t("components.admin.domains.resources.resources.any_category") | ||||
|                 }}</a-select-option> | ||||
|                 <a-select-option | ||||
|                   v-for="cat in page.categories" | ||||
|                   :value="cat.Id.toString()" | ||||
|                   >{{ cat.Name }}</a-select-option | ||||
|                 > | ||||
|               </a-select> | ||||
|               <a-select v-model:value="page.pagination.office" @change="get"> | ||||
|                 <a-select-option :value="''">{{ | ||||
|                   t("components.admin.domains.resources.resources.any_office") | ||||
|                 }}</a-select-option> | ||||
|                 <a-select-option | ||||
|                   v-for="office in page.offices" | ||||
|                   :value="office.Id.toString()" | ||||
|                   >{{ office.Name }}</a-select-option | ||||
|                 > | ||||
|               </a-select> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col :span="6"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button type="primary" @click="newResource"> | ||||
|                 <PlusOutlined /> | ||||
|                 {{ | ||||
|                   t("components.admin.domains.resources.resources.add_resource") | ||||
|                 }} | ||||
|               </a-button> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button type="primary" @click="showAccessModal(record.Id)">{{ | ||||
|               t("components.common.shared_calendars.edit_cal_access") | ||||
|             }}</a-button> | ||||
|             <a-button @click="editResource(record)">{{ | ||||
|               t("common.misc.edit") | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title="t('common.misc.delete') + '?'" | ||||
|               :ok-text="t('common.misc.ok')" | ||||
|               :cancel-text="t('common.misc.cancel')" | ||||
|               @confirm="deleteResource(record.Id)" | ||||
|             > | ||||
|               <a-button danger>{{ t("common.misc.delete") }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     :width="page.addUpdate.hasScript ? 800 : 500" | ||||
|     v-model:open="page.addUpdate.show" | ||||
|     :title=" | ||||
|       page.addUpdate.Id | ||||
|         ? t(`components.admin.domains.resources.resources.edit_resource`) | ||||
|         : t(`components.admin.domains.resources.resources.add_resource`) | ||||
|     " | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap> | ||||
|       <a-form-item | ||||
|         :label="t('components.admin.domains.resources.resources.col_category')" | ||||
|       > | ||||
|         <a-select | ||||
|           v-model:value="page.addUpdate.categoryIdStr" | ||||
|           @change="updateCatMeta" | ||||
|         > | ||||
|           <a-select-option | ||||
|             v-for="cat in page.categories" | ||||
|             :value="cat.Id.toString()" | ||||
|             >{{ cat.Name }}</a-select-option | ||||
|           > | ||||
|         </a-select> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         v-if="page.addUpdate.officeBind" | ||||
|         :label="t('components.admin.domains.resources.resources.col_office')" | ||||
|       > | ||||
|         <a-select v-model:value="page.addUpdate.officeIdStr"> | ||||
|           <a-select-option | ||||
|             v-for="office in page.offices" | ||||
|             :value="office.Id.toString()" | ||||
|             >{{ office.Name }}</a-select-option | ||||
|           > | ||||
|         </a-select> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         v-if="page.addUpdate.hasScript" | ||||
|         :label="$t('components.admin.domains.resources.resources.col_script')" | ||||
|       > | ||||
|         <a-flex gap="middle" vertical> | ||||
|           <a-alert | ||||
|             v-if="page.addUpdate.checkScript.error !== null" | ||||
|             :type="page.addUpdate.checkScript.error ? 'error' : 'success'" | ||||
|             show-icon | ||||
|           > | ||||
|             <template #message> | ||||
|               <div style="white-space: pre-line; font-family: monospace"> | ||||
|                 {{ | ||||
|                   page.addUpdate.checkScript.error | ||||
|                     ? page.addUpdate.checkScript.error | ||||
|                     : $t( | ||||
|                         "components.admin.domains.resources.resources.check_script_good" | ||||
|                       ) | ||||
|                 }} | ||||
|               </div> | ||||
|             </template> | ||||
|           </a-alert> | ||||
|           <a-textarea v-model:value="page.addUpdate.Script"> </a-textarea> | ||||
|         </a-flex> | ||||
|       </a-form-item> | ||||
|       <template | ||||
|         v-if=" | ||||
|           page.addUpdate.categoryIdStr && | ||||
|           (!page.addUpdate.officeBind || page.addUpdate.officeIdStr) | ||||
|         " | ||||
|       > | ||||
|         <a-form-item | ||||
|           :label=" | ||||
|             $t('components.admin.domains.resources.resources.col_show_time_as') | ||||
|           " | ||||
|         > | ||||
|           <a-select v-model:value="page.addUpdate.transparentCalObjectsStr"> | ||||
|             <a-select-option value="false">{{ | ||||
|               $t("components.admin.domains.resources.resources.time_busy") | ||||
|             }}</a-select-option> | ||||
|             <a-select-option value="true">{{ | ||||
|               $t("components.admin.domains.resources.resources.time_free") | ||||
|             }}</a-select-option> | ||||
|           </a-select> | ||||
|         </a-form-item> | ||||
|         <a-form-item | ||||
|           :label="$t('components.admin.domains.resources.resources.col_name')" | ||||
|           :rules="[{ required: true }]" | ||||
|         > | ||||
|           <a-input v-model:value="page.addUpdate.Name"> </a-input> | ||||
|         </a-form-item> | ||||
|       </template> | ||||
|     </a-form> | ||||
|     <template #footer> | ||||
|       <a-space> | ||||
|         <a-button | ||||
|           v-if="page.addUpdate.hasScript" | ||||
|           @click="checkScript" | ||||
|           :loading="page.addUpdate.checkScript.loading" | ||||
|         > | ||||
|           {{ t("components.admin.domains.resources.resources.check_script") }} | ||||
|         </a-button> | ||||
|         <a-button | ||||
|           type="primary" | ||||
|           :disabled=" | ||||
|             !page.addUpdate.Name || | ||||
|             !page.addUpdate.categoryIdStr || | ||||
|             (page.addUpdate.officeBind && !page.addUpdate.officeIdStr) | ||||
|           " | ||||
|           @click="addUpdateResource" | ||||
|           :loading="page.addUpdate.loading" | ||||
|         > | ||||
|           {{ | ||||
|             page.addUpdate.Id | ||||
|               ? t("common.misc.save_changes") | ||||
|               : t("components.admin.domains.resources.resources.add_resource") | ||||
|           }} | ||||
|         </a-button> | ||||
|       </a-space> | ||||
|     </template> | ||||
|   </a-modal> | ||||
|   <CalendarsAccess | ||||
|     v-if="page.access.show" | ||||
|     v-model:open="page.access.show" | ||||
|     :id="page.access.Id" | ||||
|     :domain="page.domain" | ||||
|     :type="'group'" | ||||
|     :resources="true" | ||||
|   > | ||||
|   </CalendarsAccess> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import dayjs, { Dayjs } from "dayjs"; | ||||
| 
 | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   EventIdPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
|   RouteUserCalendarsEventsPlannerEdit, | ||||
|   RouteUserCalendarsEventsPlannerNew, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   CheckOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HddOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import { timeToDateTime } from "@/composables/misc"; | ||||
| import TimezoneSelect from "@/components/common/TimezoneSelect.vue"; | ||||
| import WorkDaysSelect from "@/components/common/WorkDaysSelect.vue"; | ||||
| import WorkHoursRangePicker from "@/components/common/WorkHoursRangePicker.vue"; | ||||
| import { _cf } from "ant-design-vue/es/_util/cssinjs/hooks/useStyleRegister"; | ||||
| import CalendarsAccess from "@/components/common/CalendarsAccess.vue"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 6, style: {} }; | ||||
| const wrapperCol = { span: 18, style: {} }; | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.resources.resources.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.resources.resources.col_category"), | ||||
|     dataIndex: "CategoryName", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.resources.resources.col_office"), | ||||
|     dataIndex: "OfficeName", | ||||
|   }, | ||||
|   { | ||||
|     title: "", | ||||
|     key: "action", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| interface Category { | ||||
|   Id: number; | ||||
|   Name: string; | ||||
|   OfficeBind: boolean; | ||||
|   Script: boolean; | ||||
| } | ||||
| 
 | ||||
| interface Office { | ||||
|   Id: number; | ||||
|   Name: string; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   loading: boolean; | ||||
|   data: any[]; | ||||
|   categories: Category[]; | ||||
|   catId2Name: Map<number, string>; | ||||
|   offices: Office[]; | ||||
|   officeId2Name: Map<number, string>; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|     category: string; | ||||
|     office: string; | ||||
|   }; | ||||
|   addUpdate: { | ||||
|     show: boolean; | ||||
|     Id: number; | ||||
|     Name: string; | ||||
|     CategoryId?: number; | ||||
|     categoryIdStr: string; | ||||
|     OfficeId?: number; | ||||
|     officeIdStr: string; | ||||
|     Script: string | null; | ||||
|     TransparentCalObjects: boolean; | ||||
|     transparentCalObjectsStr: string; | ||||
|     loading: boolean; | ||||
|     officeBind: boolean; | ||||
|     hasScript: boolean; | ||||
|     checkScript: { | ||||
|       loading: boolean; | ||||
|       error: string | null; | ||||
|     }; | ||||
|   }; | ||||
|   access: { | ||||
|     show: boolean; | ||||
|     Id: number; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|     category: "", | ||||
|     office: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   categories: [], | ||||
|   catId2Name: new Map(), | ||||
|   officeId2Name: new Map(), | ||||
|   offices: [], | ||||
|   addUpdate: { | ||||
|     show: false, | ||||
|     Id: 0, | ||||
|     Name: "", | ||||
|     CategoryId: 0, | ||||
|     categoryIdStr: "", | ||||
|     OfficeId: 0, | ||||
|     officeIdStr: "", | ||||
|     Script: "", | ||||
|     loading: false, | ||||
|     officeBind: false, | ||||
|     hasScript: false, | ||||
|     TransparentCalObjects: false, | ||||
|     transparentCalObjectsStr: "false", | ||||
|     checkScript: { | ||||
|       loading: false, | ||||
|       error: null, | ||||
|     }, | ||||
|   }, | ||||
|   access: { | ||||
|     show: false, | ||||
|     Id: 0, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   load(); | ||||
| }); | ||||
| 
 | ||||
| async function load() { | ||||
|   await loadCategoriesAndOffices(); | ||||
|   get(); | ||||
| } | ||||
| 
 | ||||
| async function loadCategoriesAndOffices() { | ||||
|   loadCategories(); | ||||
|   loadOffices(); | ||||
| } | ||||
| 
 | ||||
| async function loadCategories() { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/categories` | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.categories = res.data; | ||||
|   page.categories.forEach((cat) => { | ||||
|     page.catId2Name.set(cat.Id, cat.Name); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function loadOffices() { | ||||
|   const res = await apiFetch(`/admin/domains/${page.domain}/resources/offices`); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.offices = res.data; | ||||
|   page.offices.forEach((office) => { | ||||
|     page.officeId2Name.set(office.Id, office.Name); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("category", page.pagination.category); | ||||
|   params.append("office", page.pagination.office); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/resources?` + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   page.data.forEach((e) => { | ||||
|     e.CategoryName = page.catId2Name.get(e.CategoryId); | ||||
|     e.OfficeName = page.officeId2Name.get(e.OfficeId); | ||||
|   }); | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function deleteResource(id: number) { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/resources/${id}`, | ||||
|     { | ||||
|       method: "DELETE", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess( | ||||
|     t("components.admin.domains.resources.resources.delete_success") | ||||
|   ); | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function editResource(record: any) { | ||||
|   page.addUpdate = { ...record }; | ||||
|   page.addUpdate.checkScript = { | ||||
|     loading: false, | ||||
|     error: null, | ||||
|   }; | ||||
| 
 | ||||
|   page.addUpdate.categoryIdStr = ""; | ||||
|   if (page.addUpdate.CategoryId) { | ||||
|     page.addUpdate.categoryIdStr = page.addUpdate.CategoryId.toString(); | ||||
|   } | ||||
|   page.addUpdate.officeIdStr = ""; | ||||
|   if (page.addUpdate.OfficeId) { | ||||
|     page.addUpdate.officeIdStr = page.addUpdate.OfficeId.toString(); | ||||
|   } | ||||
|   page.addUpdate.transparentCalObjectsStr = String( | ||||
|     page.addUpdate.TransparentCalObjects | ||||
|   ); | ||||
| 
 | ||||
|   updateCatMeta(); | ||||
|   page.addUpdate.show = true; | ||||
| } | ||||
| 
 | ||||
| async function addUpdateResource() { | ||||
|   page.addUpdate.CategoryId = undefined; | ||||
|   if (page.addUpdate.categoryIdStr) { | ||||
|     page.addUpdate.CategoryId = Number(page.addUpdate.categoryIdStr); | ||||
|   } | ||||
|   page.addUpdate.OfficeId = undefined; | ||||
|   if (page.addUpdate.officeIdStr) { | ||||
|     page.addUpdate.OfficeId = Number(page.addUpdate.officeIdStr); | ||||
|   } | ||||
|   if (!page.addUpdate.hasScript) { | ||||
|     page.addUpdate.Script = null; | ||||
|   } | ||||
|   page.addUpdate.TransparentCalObjects = Boolean( | ||||
|     page.addUpdate.transparentCalObjectsStr | ||||
|   ); | ||||
| 
 | ||||
|   page.addUpdate.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/resources`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.addUpdate, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.addUpdate.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (page.addUpdate.Id !== 0) { | ||||
|     notifySuccess( | ||||
|       t("components.admin.domains.resources.resources.update_success") | ||||
|     ); | ||||
|   } else { | ||||
|     notifySuccess( | ||||
|       t("components.admin.domains.resources.resources.create_success") | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   page.addUpdate.show = false; | ||||
|   get(); | ||||
| } | ||||
| 
 | ||||
| async function checkScript() { | ||||
|   page.addUpdate.checkScript.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/resources/resources/check_script`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.addUpdate.Script, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.addUpdate.checkScript.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.addUpdate.checkScript.error = res.data.error; | ||||
| } | ||||
| 
 | ||||
| function updateCatMeta() { | ||||
|   page.addUpdate.officeBind = false; | ||||
|   page.addUpdate.hasScript = false; | ||||
| 
 | ||||
|   for (let i = 0; i < page.categories.length; i++) { | ||||
|     const cat = page.categories[i]; | ||||
|     if (cat.Id.toString() === page.addUpdate.categoryIdStr) { | ||||
|       page.addUpdate.officeBind = cat.OfficeBind; | ||||
|       page.addUpdate.hasScript = cat.Script; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function newResource() { | ||||
|   page.addUpdate.Id = 0; | ||||
|   page.addUpdate.Name = ""; | ||||
|   page.addUpdate.CategoryId = undefined; | ||||
|   page.addUpdate.categoryIdStr = ""; | ||||
|   page.addUpdate.OfficeId = undefined; | ||||
|   page.addUpdate.officeIdStr = ""; | ||||
|   page.addUpdate.officeBind = false; | ||||
|   page.addUpdate.hasScript = false; | ||||
|   page.addUpdate.Script = null; | ||||
|   page.addUpdate.show = true; | ||||
|   page.addUpdate.checkScript.loading = false; | ||||
|   page.addUpdate.checkScript.error = null; | ||||
|   page.addUpdate.TransparentCalObjects = false; | ||||
|   page.addUpdate.transparentCalObjectsStr = "false"; | ||||
| } | ||||
| 
 | ||||
| async function showAccessModal(id: number) { | ||||
|   page.access.Id = id; | ||||
|   page.access.show = true; | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 800px; | ||||
|   max-width: 80%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,115 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|         <a-form-item | ||||
|           :label="$t('components.admin.domains.userdb.add.Name')" | ||||
|           :rules="[{ required: true }]" | ||||
|         > | ||||
|           <a-input v-model:value="page.new.Name"> </a-input> | ||||
|         </a-form-item> | ||||
| 
 | ||||
|         <a-form-item | ||||
|           :label="$t('components.admin.domains.userdb.add.Type')" | ||||
|           :rules="[{ required: true }]" | ||||
|         > | ||||
|           <a-select v-model:value="page.new.Type"> | ||||
|             <a-select-option value="ldapUserDB">LDAP</a-select-option> | ||||
|             <a-select-option value="jsonFileUserDB">JSON File</a-select-option> | ||||
|           </a-select> | ||||
|         </a-form-item> | ||||
|         <a-button | ||||
|           :disabled="!page.new.Type || !page.new.Name" | ||||
|           class="save-button" | ||||
|           type="primary" | ||||
|           size="large" | ||||
|           @click="save" | ||||
|         > | ||||
|           {{ $t("components.admin.domains.userdb.add.create") }} | ||||
|         </a-button> | ||||
|       </a-form> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { saveAndRestart } from "@/composables/restart"; | ||||
| import { message } from "ant-design-vue"; | ||||
| 
 | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import router from "@/router"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageSettings, | ||||
|   RouteAdminDomainsDomainUserDBProviderSettings, | ||||
|   UserdbPlaceholder, | ||||
| } from "@/router/consts"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   new: { | ||||
|     Type: string; | ||||
|     Name: string; | ||||
|   }; | ||||
|   domain: string; | ||||
|   loading: boolean; | ||||
| }>({ | ||||
|   loading: false, | ||||
|   new: { | ||||
|     Type: "", | ||||
|     Name: "", | ||||
|   }, | ||||
|   domain: "", | ||||
| }); | ||||
| 
 | ||||
| const labelCol = { span: 8 }; | ||||
| const wrapperCol = { span: 16 }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
| }); | ||||
| 
 | ||||
| async function save() { | ||||
|   const res = await apiFetch(`/admin/domains/${page.domain}/userdb/add`, { | ||||
|     method: "POST", | ||||
|     body: page.new, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.admin.domains.userdb.add.success")); | ||||
| 
 | ||||
|   router.push({ | ||||
|     path: RouteAdminDomainsDomainUserDBProviderSettings.replace( | ||||
|       DomainPlaceholder, | ||||
|       page.domain | ||||
|     ).replace(UserdbPlaceholder, res.data), | ||||
|     hash: "#refresh", | ||||
|   }); | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   width: 500px; | ||||
|   max-width: 50%; | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| .save-button { | ||||
|   display: block; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,434 @@ | |||
| <template> | ||||
|   <a-table | ||||
|     class="panel-content" | ||||
|     :columns="columns" | ||||
|     :data-source="page.data" | ||||
|     :loading="page.loading" | ||||
|     bordered | ||||
|     size="small" | ||||
|     :pagination="false" | ||||
|   > | ||||
|     <template #bodyCell="{ column, record }"> | ||||
|       <template v-if="column.key === 'action'"> | ||||
|         <a-space> | ||||
|           <a-button type="primary" @click="showMembersModal(record)">{{ | ||||
|             t("components.admin.domains.userdb.groups.members") | ||||
|           }}</a-button> | ||||
|           <a-button @click="showAddModal(record)">{{ | ||||
|             t("components.admin.domains.userdb.groups.edit") | ||||
|           }}</a-button> | ||||
|           <a-popconfirm | ||||
|             :title="t('components.admin.domains.userdb.groups.delete') + '?'" | ||||
|             :ok-text="t('components.admin.domains.userdb.groups.ok')" | ||||
|             :cancel-text="t('components.admin.domains.userdb.groups.cancel')" | ||||
|             @confirm="deleteGroup(record.Gid)" | ||||
|           > | ||||
|             <a-button danger>{{ | ||||
|               t("components.admin.domains.userdb.groups.delete") | ||||
|             }}</a-button> | ||||
|           </a-popconfirm> | ||||
|         </a-space> | ||||
|       </template> | ||||
|       <template v-if="column.key === 'users_count'"> | ||||
|        {{ record.MemberUsers ?  (record.MemberUsers as Array<any>).length : 0 }} | ||||
|       </template> | ||||
|       <template v-if="column.key === 'groups_count'"> | ||||
|        {{ record.MemberGroups ? (record.MemberGroups as Array<any>).length : 0 }} | ||||
|       </template> | ||||
|     </template> | ||||
|     <template #title> | ||||
|       <div style="display: flex; justify-content: end"> | ||||
|         <a-button type="primary" @click="showAddModal(undefined)">{{ | ||||
|           $t("components.admin.domains.userdb.groups.add") | ||||
|         }}</a-button> | ||||
|       </div> | ||||
|     </template> | ||||
|   </a-table> | ||||
|   <a-modal | ||||
|     v-model:open="page.add.show" | ||||
|     :title=" | ||||
|       page.add.Gid | ||||
|         ? t(`components.admin.domains.userdb.groups.edit`) + ' ' + page.add.Name | ||||
|         : t(`components.admin.domains.userdb.groups.add`) | ||||
|     " | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.userdb.groups.add_modal.name') | ||||
|          | ||||
|         " | ||||
|          :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.add.Name"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.userdb.groups.add_modal.email')" | ||||
|                :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.add.Email"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label=" | ||||
|           $t('components.admin.domains.userdb.groups.add_modal.alt_emails') | ||||
|         " | ||||
|       > | ||||
|         <a-textarea v-model:value="page.add.AlternateEmailsStr"> </a-textarea> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <template #footer> | ||||
|       <a-space> | ||||
|         <a-button @click="page.add.show = false"> | ||||
|           {{ $t("components.admin.domains.userdb.groups.add_modal.cancel") }} | ||||
|         </a-button> | ||||
|         <a-button | ||||
|           :disabled="!page.add.Name || !page.add.Email" | ||||
|           type="primary" | ||||
|           @click="add" | ||||
|         > | ||||
|           {{ $t("components.admin.domains.userdb.groups.add_modal.save") }} | ||||
|         </a-button> | ||||
|       </a-space> | ||||
|     </template> | ||||
|   </a-modal> | ||||
|   <a-modal v-model:open="page.members.show" style="width: 70%" @cancel="get"> | ||||
|     <a-transfer | ||||
|       :disabled="page.members.disabled" | ||||
|       v-model:target-keys="page.members.users_ids" | ||||
|       :data-source="page.all_users" | ||||
|       :render="(item: any) => item.name" | ||||
|       show-search | ||||
|       :titles="[' (all users)', ' (users in group)']" | ||||
|       :list-style="{ | ||||
|         width: '100%', | ||||
|         height: '320px', | ||||
|       }" | ||||
|       @change="changeUserMember" | ||||
|     /> | ||||
|     <br /> | ||||
|     <a-transfer | ||||
|       :disabled="page.members.disabled" | ||||
|       v-model:target-keys="page.members.groups_ids" | ||||
|       :data-source="page.all_groups" | ||||
|       :render="(item: any) => item.name" | ||||
|       show-search | ||||
|       :titles="[' (all groups)', ' (groups in group)']" | ||||
|       :list-style="{ | ||||
|         width: '100%', | ||||
|         height: '320px', | ||||
|       }" | ||||
|       @change="changeGroupMember" | ||||
|     /> | ||||
|     <template #footer> </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 8,  }; | ||||
| const wrapperCol = { span: 16 }; | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.userdb.groups.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.userdb.groups.col_email"), | ||||
|     dataIndex: "Email", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.userdb.groups.col_users_count"), | ||||
|     key: "users_count", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.userdb.groups.col_group_count"), | ||||
|     key: "groups_count", | ||||
| 
 | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| interface TransferElem { | ||||
|   key: string; | ||||
|   name: string; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   sid: string; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   all_users: TransferElem[]; | ||||
|   all_groups: TransferElem[]; | ||||
|   members: { | ||||
|     show: boolean; | ||||
|     Name: string; | ||||
|     Gid: string; | ||||
|     users_ids: string[]; | ||||
|     groups_ids: string[]; | ||||
|     disabled: boolean; | ||||
|   }; | ||||
|   add: { | ||||
|     show: boolean; | ||||
|     Gid: string; | ||||
|     Name: string; | ||||
|     Email: string; | ||||
|     AlternateEmails: string[]; | ||||
|     AlternateEmailsStr: string; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   sid: "", | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   add: { | ||||
|     show: false, | ||||
|     Name: "", | ||||
|     Email: "", | ||||
|     AlternateEmails: [], | ||||
|     AlternateEmailsStr: "", | ||||
|     Gid: "", | ||||
|   }, | ||||
|   members: { | ||||
|     show: false, | ||||
|     Name: "", | ||||
|     Gid: "", | ||||
|     users_ids: [], | ||||
|     groups_ids: [], | ||||
|     disabled: false, | ||||
|   }, | ||||
|   all_users: [], | ||||
|   all_groups: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.sid = route.params.sid as string; | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/groups` | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function getUsers() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/users` | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.all_users = []; | ||||
|   page.all_users = (res.data as Array<any>).map((e) => { | ||||
|     return { | ||||
|       key: e.Uid, | ||||
|       name: e.DisplayName + " (" + e.Email + ")", | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function getGroups() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/groups` | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.all_groups = []; | ||||
|   page.all_groups = (res.data as Array<any>).map((e) => { | ||||
|     return { | ||||
|       key: e.Gid, | ||||
|       name: e.Name + " (" + e.Email + ")", | ||||
|     }; | ||||
|   }); | ||||
|   page.all_groups = page.all_groups.filter((e) => e.key !== page.members.Gid); | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function showAddModal(record: any) { | ||||
|   if (record) { | ||||
|     page.add.Gid = record.Gid; | ||||
|     page.add.Email = record.Email; | ||||
|     page.add.Name = record.Name; | ||||
|     page.add.AlternateEmails = record.AlternateEmails; | ||||
|     page.add.AlternateEmailsStr = page.add.AlternateEmails.join("\n"); | ||||
|   } else { | ||||
|     page.add.Gid = ""; | ||||
|     page.add.Email = ""; | ||||
|     page.add.Name = ""; | ||||
|     page.add.AlternateEmails = []; | ||||
|     page.add.AlternateEmailsStr = ""; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = true; | ||||
| } | ||||
| 
 | ||||
| async function showMembersModal(record: any) { | ||||
|   page.members.Gid = record.Gid; | ||||
|   page.members.Name = record.Name; | ||||
|   page.members.show = true; | ||||
| 
 | ||||
|   page.members.groups_ids = record.MemberGroups; | ||||
|   if (!page.members.groups_ids) { | ||||
|     page.members.groups_ids = []; | ||||
|   } | ||||
|   page.members.users_ids = record.MemberUsers; | ||||
|   if (!page.members.users_ids) { | ||||
|     page.members.users_ids = []; | ||||
|   } | ||||
| 
 | ||||
|   await getUsers(); | ||||
|   await getGroups(); | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
|   page.add.AlternateEmails = page.add.AlternateEmailsStr.split("\n"); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/groups`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.add, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| async function deleteGroup(gid: string) { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/groups`, | ||||
|     { | ||||
|       method: "DELETE", | ||||
|       body: { | ||||
|         Gid: gid, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function changeUserMember( | ||||
|   nextTargetKeys: string[], | ||||
|   direction: string, | ||||
|   moveKeys: string[] | ||||
| ) { | ||||
|   page.members.disabled = true; | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/groups/${page.members.Gid}/user`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: { | ||||
|         Action: direction === "right" ? "add" : "delete", | ||||
|         Ids: moveKeys, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
|   page.members.disabled = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function changeGroupMember( | ||||
|   nextTargetKeys: string[], | ||||
|   direction: string, | ||||
|   moveKeys: string[] | ||||
| ) { | ||||
|   page.members.disabled = true; | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/groups/${page.members.Gid}/group`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: { | ||||
|         Action: direction === "right" ? "add" : "delete", | ||||
|         Ids: moveKeys, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
|   page.members.disabled = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 700px; | ||||
|   max-width: 70%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,245 @@ | |||
| <template> | ||||
|   <a-table | ||||
|     class="panel-content" | ||||
|     :columns="columns" | ||||
|     :data-source="page.data" | ||||
|     :loading="page.loading" | ||||
|     bordered | ||||
|     size="small" | ||||
|     :pagination="false" | ||||
|   > | ||||
|     <template #bodyCell="{ column, record }"> | ||||
|       <template v-if="column.key === 'action'"> | ||||
|         <a-space> | ||||
|           <a-button @click="showAddModal(record)">{{ | ||||
|             t("components.admin.domains.userdb.redirects.edit") | ||||
|           }}</a-button> | ||||
|           <a-popconfirm | ||||
|             :title="t('components.admin.domains.userdb.redirects.delete') + '?'" | ||||
|             :ok-text="t('components.admin.domains.userdb.redirects.ok')" | ||||
|             :cancel-text="t('components.admin.domains.userdb.redirects.cancel')" | ||||
|             @confirm="deleteRedirect(record.Rid)" | ||||
|           > | ||||
|             <a-button danger>{{ | ||||
|               t("components.admin.domains.userdb.redirects.delete") | ||||
|             }}</a-button> | ||||
|           </a-popconfirm> | ||||
|         </a-space> | ||||
|       </template> | ||||
|     </template> | ||||
|     <template #title> | ||||
|       <div style="display: flex; justify-content: end"> | ||||
|         <a-button type="primary" @click="showAddModal(undefined)">{{ | ||||
|           $t("components.admin.domains.userdb.redirects.add") | ||||
|         }}</a-button> | ||||
|       </div> | ||||
|     </template> | ||||
|   </a-table> | ||||
|   <a-modal | ||||
|     v-model:open="page.add.show" | ||||
|     :title=" | ||||
|       page.add.Rid | ||||
|         ? t(`components.admin.domains.userdb.redirects.edit`) + | ||||
|           ' ' + | ||||
|           page.add.Name | ||||
|         : t(`components.admin.domains.userdb.redirects.add`) | ||||
|     " | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.userdb.redirects.add_modal.name')" | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.add.Name"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.userdb.redirects.add_modal.email')" | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.add.Email"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label=" | ||||
|           $t('components.admin.domains.userdb.redirects.add_modal.destinations') | ||||
|         " | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-textarea v-model:value="page.add.DestinationsStr"> </a-textarea> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <template #footer> | ||||
|       <a-space> | ||||
|         <a-button @click="page.add.show = false"> | ||||
|           {{ $t("components.admin.domains.userdb.redirects.add_modal.cancel") }} | ||||
|         </a-button> | ||||
|         <a-button | ||||
|           :disabled=" | ||||
|             !page.add.Name || !page.add.Email || !page.add.DestinationsStr | ||||
|           " | ||||
|           type="primary" | ||||
|           @click="add" | ||||
|         > | ||||
|           {{ $t("components.admin.domains.userdb.redirects.add_modal.save") }} | ||||
|         </a-button> | ||||
|       </a-space> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 8 }; | ||||
| const wrapperCol = { span: 16 }; | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.userdb.redirects.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.userdb.redirects.col_email"), | ||||
|     dataIndex: "Email", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   sid: string; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   add: { | ||||
|     show: boolean; | ||||
|     Rid: string; | ||||
|     Name: string; | ||||
|     Email: string; | ||||
|     Destinations: string[]; | ||||
|     DestinationsStr: string; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   sid: "", | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   add: { | ||||
|     show: false, | ||||
|     Name: "", | ||||
|     Email: "", | ||||
|     Destinations: [], | ||||
|     DestinationsStr: "", | ||||
|     Rid: "", | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.sid = route.params.sid as string; | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/redirects` | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function showAddModal(record: any) { | ||||
|   if (record) { | ||||
|     page.add.Rid = record.Rid; | ||||
|     page.add.Email = record.Email; | ||||
|     page.add.Name = record.Name; | ||||
|     page.add.Destinations = record.Destinations; | ||||
|     page.add.DestinationsStr = page.add.Destinations.join("\n"); | ||||
|   } else { | ||||
|     page.add.Rid = ""; | ||||
|     page.add.Email = ""; | ||||
|     page.add.Name = ""; | ||||
|     page.add.Destinations = []; | ||||
|     page.add.DestinationsStr = ""; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = true; | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
|   page.add.Destinations = page.add.DestinationsStr.split("\n"); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/redirects`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.add, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| async function deleteRedirect(rid: string) { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/redirects`, | ||||
|     { | ||||
|       method: "DELETE", | ||||
|       body: { | ||||
|         Rid: rid, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,812 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|         <a-form-item | ||||
|           :label="$t('components.admin.domains.userdb.settings.Name')" | ||||
|         > | ||||
|           <a-input v-model:value="page.settings.Name"> </a-input> | ||||
|         </a-form-item> | ||||
| 
 | ||||
|         <a-form-item | ||||
|           :label="$t('components.admin.domains.userdb.settings.UseInGal')" | ||||
|         > | ||||
|           <a-switch v-model:checked="page.settings.UseInGal"> </a-switch> | ||||
|         </a-form-item> | ||||
| 
 | ||||
|         <template v-if="page.type === 'jsonFileUserDB'"> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.domains.userdb.settings.Dir')" | ||||
|           > | ||||
|             <a-input v-model:value="page.settings.Params.Dir"> </a-input> | ||||
|           </a-form-item> | ||||
| 
 | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t('components.admin.domains.userdb.settings.MasterUsersGroup') | ||||
|             " | ||||
|           > | ||||
|             <a-input v-model:value="page.settings.Params.MasterUsersGroup"> | ||||
|             </a-input> | ||||
|           </a-form-item> | ||||
|         </template> | ||||
|         <template v-else> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.domains.userdb.settings.CacheTTL')" | ||||
|           > | ||||
|             <a-input-number | ||||
|               class="form-input-number" | ||||
|               :addon-after="$t('common.suffixes.sec')" | ||||
|               v-model:value="page.settings.Params.CacheTTL" | ||||
|             > | ||||
|             </a-input-number> | ||||
|           </a-form-item> | ||||
|           <a-collapse> | ||||
|             <a-collapse-panel | ||||
|               :header=" | ||||
|                 $t('components.admin.domains.userdb.settings.ConnectionGroup') | ||||
|               " | ||||
|             > | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.ConnectURIs') | ||||
|                 " | ||||
|               > | ||||
|                 <a-textarea | ||||
|                   v-model:value="page.settings.Params.ConnectURIsString" | ||||
|                 > | ||||
|                 </a-textarea> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.ConnectBindDN') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.ConnectBindDN"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.ConnectPassword') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input-password | ||||
|                   v-model:value="page.settings.Params.ConnectPassword" | ||||
|                   autocomplete="off" | ||||
|                 > | ||||
|                 </a-input-password> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.ConnectBaseDN') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.ConnectBaseDN"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t( | ||||
|                     'components.admin.domains.userdb.settings.ConnectTLSMinVer' | ||||
|                   ) | ||||
|                 " | ||||
|               > | ||||
|                 <a-select v-model:value="page.settings.Params.ConnectTLSMinVer"> | ||||
|                   <a-select-option value="1.3"></a-select-option> | ||||
|                   <a-select-option value="1.2"></a-select-option> | ||||
|                   <a-select-option value="1.0"></a-select-option> | ||||
|                 </a-select> | ||||
|               </a-form-item> | ||||
|             </a-collapse-panel> | ||||
|             <a-collapse-panel | ||||
|               :header="$t('components.admin.domains.userdb.settings.UserGroup')" | ||||
|             > | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.UserObjectclass') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.UserObjectclass"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.UserMailAttr') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.UserMailAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.UserQuotaAttr') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.UserQuotaAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.MemberofAttr') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.MemberofAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t( | ||||
|                     'components.admin.domains.userdb.settings.UserAddSearchFilter' | ||||
|                   ) | ||||
|                 " | ||||
|               > | ||||
|                 <a-input | ||||
|                   v-model:value="page.settings.Params.UserAddSearchFilter" | ||||
|                 > | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|             </a-collapse-panel> | ||||
|             <a-collapse-panel | ||||
|               :header=" | ||||
|                 $t('components.admin.domains.userdb.settings.GroupsGroup') | ||||
|               " | ||||
|             > | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.EmailGroups') | ||||
|                 " | ||||
|               > | ||||
|                 <a-switch v-model:checked="page.settings.Params.EmailGroups"> | ||||
|                 </a-switch> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t( | ||||
|                     'components.admin.domains.userdb.settings.GroupObjectclass' | ||||
|                   ) | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.GroupObjectclass"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.GroupNameAttr') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.GroupNameAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.GroupMailAttr') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.GroupMailAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.GroupMemberAttr') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.GroupMemberAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.GroupMemberDn') | ||||
|                 " | ||||
|               > | ||||
|                 <a-switch v-model:checked="page.settings.Params.GroupMemberDn"> | ||||
|                 </a-switch> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.UserRDNAttr') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.UserRDNAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t( | ||||
|                     'components.admin.domains.userdb.settings.GroupAddSearchFilter' | ||||
|                   ) | ||||
|                 " | ||||
|               > | ||||
|                 <a-input | ||||
|                   v-model:value="page.settings.Params.GroupAddSearchFilter" | ||||
|                 > | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|             </a-collapse-panel> | ||||
|             <a-collapse-panel | ||||
|               :header=" | ||||
|                 $t('components.admin.domains.userdb.settings.RedirectGroup') | ||||
|               " | ||||
|             > | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.EmailRedirect') | ||||
|                 " | ||||
|               > | ||||
|                 <a-switch v-model:checked="page.settings.Params.EmailRedirect"> | ||||
|                 </a-switch> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t( | ||||
|                     'components.admin.domains.userdb.settings.AliasObjectclass' | ||||
|                   ) | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.AliasObjectclass"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.AliasMailAttr') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.AliasMailAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t( | ||||
|                     'components.admin.domains.userdb.settings.AliasRedirectAttr' | ||||
|                   ) | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.AliasRedirectAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|             </a-collapse-panel> | ||||
|             <a-collapse-panel | ||||
|               :header=" | ||||
|                 $t('components.admin.domains.userdb.settings.AltEmailGroup') | ||||
|               " | ||||
|             > | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.EmailAlternate') | ||||
|                 " | ||||
|               > | ||||
|                 <a-switch v-model:checked="page.settings.Params.EmailAlternate"> | ||||
|                 </a-switch> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t( | ||||
|                     'components.admin.domains.userdb.settings.AlternateMailAttr' | ||||
|                   ) | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.AlternateMailAttr"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|             </a-collapse-panel> | ||||
|             <a-collapse-panel | ||||
|               :header=" | ||||
|                 $t('components.admin.domains.userdb.settings.MasterGroup') | ||||
|               " | ||||
|             > | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t( | ||||
|                     'components.admin.domains.userdb.settings.MasterUserGroupNameAttr' | ||||
|                   ) | ||||
|                 " | ||||
|               > | ||||
|                 <a-input | ||||
|                   v-model:value="page.settings.Params.MasterUserGroupNameAttr" | ||||
|                 > | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.MasterUserGroup') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.MasterUserGroup"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|             </a-collapse-panel> | ||||
|             <a-collapse-panel | ||||
|               :header="$t('components.admin.domains.userdb.settings.GALGroup')" | ||||
|             > | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t('components.admin.domains.userdb.settings.GALCacheTTL') | ||||
|                 " | ||||
|               > | ||||
|                 <a-input-number | ||||
|                   class="form-input-number" | ||||
|                   :addon-after="$t('common.suffixes.sec')" | ||||
|                   v-model:value="page.settings.Params.GALCacheTTL" | ||||
|                 > | ||||
|                 </a-input-number> | ||||
|               </a-form-item> | ||||
|               <a-form-item | ||||
|                 :label=" | ||||
|                   $t( | ||||
|                     'components.admin.domains.userdb.settings.GALObjectsFilter' | ||||
|                   ) | ||||
|                 " | ||||
|               > | ||||
|                 <a-input v-model:value="page.settings.Params.GALObjectsFilter"> | ||||
|                 </a-input> | ||||
|               </a-form-item> | ||||
|               <a-collapse> | ||||
|                 <a-collapse-panel | ||||
|                   :header=" | ||||
|                     $t('components.admin.domains.userdb.settings.GALUserGroup') | ||||
|                   " | ||||
|                 > | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALUserNameAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALUserNameAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALUserEmailAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALUserEmailAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALUserAltEmailAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALUserAltEmailAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALUserPhoneAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALUserPhoneAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALUserPhotoAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALUserPhotoAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALUserBirthdayAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALUserBirthdayAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALUserAddressAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALUserAddressAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALUserOrganizationAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value=" | ||||
|                         page.settings.Params.GALUserOrganizationAttr | ||||
|                       " | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALUserRoleAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALUserRoleAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                 </a-collapse-panel> | ||||
|                 <a-collapse-panel | ||||
|                   :header=" | ||||
|                     $t( | ||||
|                       'components.admin.domains.userdb.settings.GALGroupsGroup' | ||||
|                     ) | ||||
|                   " | ||||
|                 > | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALGroupNameAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALGroupNameAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALGroupEmailAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALGroupEmailAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                 </a-collapse-panel> | ||||
|                 <a-collapse-panel | ||||
|                   :header=" | ||||
|                     $t( | ||||
|                       'components.admin.domains.userdb.settings.GALAliasesGroup' | ||||
|                     ) | ||||
|                   " | ||||
|                 > | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALAliasNameAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALAliasNameAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                   <a-form-item | ||||
|                     :label=" | ||||
|                       $t( | ||||
|                         'components.admin.domains.userdb.settings.GALAliasEmailAttr' | ||||
|                       ) | ||||
|                     " | ||||
|                   > | ||||
|                     <a-input | ||||
|                       v-model:value="page.settings.Params.GALAliasEmailAttr" | ||||
|                     > | ||||
|                     </a-input> | ||||
|                   </a-form-item> | ||||
|                 </a-collapse-panel> | ||||
|               </a-collapse> | ||||
|             </a-collapse-panel> | ||||
|           </a-collapse> | ||||
|         </template> | ||||
| 
 | ||||
|         <br /> | ||||
|         <a-space style="display: flex; justify-content: center"> | ||||
|           <a-input-search | ||||
|             v-model:value="page.settings.Email" | ||||
|             :placeholder=" | ||||
|               t('components.admin.domains.userdb.settings.check_email') | ||||
|             " | ||||
|             style="width: 370px" | ||||
|             size="large" | ||||
|             allowClear | ||||
|             @search="check" | ||||
|           > | ||||
|             <template #enterButton> | ||||
|               <a-button | ||||
|                 type="primary" | ||||
|                 :disabled="!page.settings.Email" | ||||
|                 :loading="page.checkLoading" | ||||
|                 >{{ | ||||
|                   $t("components.admin.domains.userdb.settings.check") | ||||
|                 }}</a-button | ||||
|               > | ||||
|             </template> | ||||
|           </a-input-search> | ||||
| 
 | ||||
|           <a-button | ||||
|             :disabled="!page.contentLoaded" | ||||
|             type="primary" | ||||
|             size="large" | ||||
|             class="save-button" | ||||
|             style="width: fit-content" | ||||
|             @click="save" | ||||
|           > | ||||
|             {{ $t("components.admin.domains.userdb.settings.save") }} | ||||
|           </a-button> | ||||
|         </a-space> | ||||
|       </a-form> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { saveAndRestart } from "@/composables/restart"; | ||||
| import { message } from "ant-design-vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import router from "@/router"; | ||||
| import { RouteAdminDashboard } from "@/router/consts"; | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   sid: string; | ||||
|   domain: string; | ||||
|   contentLoaded: boolean; | ||||
|   checkLoading: boolean; | ||||
|   type: string; | ||||
|   settings: { | ||||
|     Name: string; | ||||
|     UseInGal: boolean; | ||||
|     Email: string; | ||||
|     Params: { | ||||
|       //Maildir | ||||
|       Dir: string; | ||||
|       MasterUsersGroup: string; | ||||
| 
 | ||||
|       //Ldap | ||||
| 
 | ||||
|       CacheTTL: number; | ||||
|       ConnectURIs: string[]; | ||||
|       ConnectURIsString: string; | ||||
|       ConnectBindDN: string; | ||||
|       ConnectPassword: string; | ||||
|       ConnectBaseDN: string; | ||||
|       ConnectTLSMinVer: string; | ||||
| 
 | ||||
|       UserObjectclass: string; | ||||
|       UserMailAttr: string; | ||||
|       UserQuotaAttr: string; | ||||
|       MemberofAttr: string; | ||||
|       UserAddSearchFilter: string; | ||||
| 
 | ||||
|       EmailGroups: boolean; | ||||
|       GroupObjectclass: string; | ||||
|       GroupNameAttr: string; | ||||
|       GroupMailAttr: string; | ||||
|       GroupMemberAttr: string; | ||||
|       GroupMemberDn: boolean; | ||||
|       UserRDNAttr: string; | ||||
|       GroupAddSearchFilter: string; | ||||
| 
 | ||||
|       EmailRedirect: boolean; | ||||
|       AliasObjectclass: string; | ||||
|       AliasMailAttr: string; | ||||
|       AliasRedirectAttr: string; | ||||
| 
 | ||||
|       EmailAlternate: boolean; | ||||
|       AlternateMailAttr: string; | ||||
| 
 | ||||
|       MasterUserGroupNameAttr: string; | ||||
|       MasterUserGroup: string; | ||||
| 
 | ||||
|       GALCacheTTL: number; | ||||
| 
 | ||||
|       GALObjectsFilter: string; | ||||
|       GALUserNameAttr: string; | ||||
|       GALUserEmailAttr: string; | ||||
|       GALUserAltEmailAttr: string; | ||||
|       GALUserPhoneAttr: string; | ||||
|       GALUserPhotoAttr: string; | ||||
|       GALUserBirthdayAttr: string; | ||||
|       GALUserAddressAttr: string; | ||||
|       GALUserOrganizationAttr: string; | ||||
|       GALUserRoleAttr: string; | ||||
| 
 | ||||
|       GALGroupNameAttr: string; | ||||
|       GALGroupEmailAttr: string; | ||||
| 
 | ||||
|       GALAliasNameAttr: string; | ||||
|       GALAliasEmailAttr: string; | ||||
|     }; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   sid: "", | ||||
|   contentLoaded: false, | ||||
|   checkLoading: false, | ||||
|   type: "", | ||||
|   settings: { | ||||
|     Name: "", | ||||
|     UseInGal: false, | ||||
|     Email: "", | ||||
|     Params: { | ||||
|       Dir: "", | ||||
|       MasterUsersGroup: "", | ||||
|       CacheTTL: 0, | ||||
|       ConnectURIs: [], | ||||
|       ConnectURIsString: "", | ||||
|       ConnectBindDN: "", | ||||
|       ConnectPassword: "", | ||||
|       ConnectBaseDN: "", | ||||
|       ConnectTLSMinVer: "", | ||||
|       UserObjectclass: "", | ||||
|       UserMailAttr: "", | ||||
|       UserQuotaAttr: "", | ||||
|       MemberofAttr: "", | ||||
|       UserAddSearchFilter: "", | ||||
|       EmailGroups: false, | ||||
|       GroupObjectclass: "", | ||||
|       GroupNameAttr: "", | ||||
|       GroupMailAttr: "", | ||||
|       GroupMemberAttr: "", | ||||
|       GroupMemberDn: false, | ||||
|       UserRDNAttr: "", | ||||
|       GroupAddSearchFilter: "", | ||||
|       EmailRedirect: false, | ||||
|       AliasObjectclass: "", | ||||
|       AliasMailAttr: "", | ||||
|       AliasRedirectAttr: "", | ||||
|       EmailAlternate: false, | ||||
|       AlternateMailAttr: "", | ||||
|       MasterUserGroupNameAttr: "", | ||||
|       MasterUserGroup: "", | ||||
|       GALCacheTTL: 0, | ||||
|       GALObjectsFilter: "", | ||||
|       GALUserNameAttr: "", | ||||
|       GALUserEmailAttr: "", | ||||
|       GALUserAltEmailAttr: "", | ||||
|       GALUserPhoneAttr: "", | ||||
|       GALUserPhotoAttr: "", | ||||
|       GALUserBirthdayAttr: "", | ||||
|       GALUserAddressAttr: "", | ||||
|       GALUserOrganizationAttr: "", | ||||
|       GALUserRoleAttr: "", | ||||
|       GALGroupNameAttr: "", | ||||
|       GALGroupEmailAttr: "", | ||||
|       GALAliasNameAttr: "", | ||||
|       GALAliasEmailAttr: "", | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const labelCol = { span: 16, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 8 }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.domain = route.params.domain as string; | ||||
|   page.sid = route.params.sid as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/settings` | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.contentLoaded = true; | ||||
| 
 | ||||
|   page.type = res.data.Type; | ||||
|   page.settings = res.data; | ||||
|   loadTextArea(); | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function save() { | ||||
|   saveTextArea(); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/settings`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.settings, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.admin.domains.userdb.settings.save_success")); | ||||
| 
 | ||||
|   router.go(0); | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function check() { | ||||
|   if (!page.settings.Email) { | ||||
|     return; | ||||
|   } | ||||
|   saveTextArea(); | ||||
|   page.checkLoading = true; | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/settings/check`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.settings, | ||||
|     } | ||||
|   ); | ||||
|   page.checkLoading = false; | ||||
|   if (res.error) { | ||||
|     notifyError( | ||||
|       t("components.admin.domains.userdb.settings.check_error") + res.error | ||||
|     ); | ||||
| 
 | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.admin.domains.userdb.settings.check_success")); | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function loadTextArea() { | ||||
|   if (page.settings.Params.ConnectURIs) { | ||||
|     page.settings.Params.ConnectURIsString = | ||||
|       page.settings.Params.ConnectURIs.join("\n"); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function saveTextArea() { | ||||
|   if (page.settings.Params.ConnectURIsString) { | ||||
|     page.settings.Params.ConnectURIs = | ||||
|       page.settings.Params.ConnectURIsString.split("\n"); | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,294 @@ | |||
| <template> | ||||
|   <a-table | ||||
|     class="panel-content" | ||||
|     :columns="columns" | ||||
|     :data-source="page.data" | ||||
|     :loading="page.loading" | ||||
|     bordered | ||||
|     size="small" | ||||
|     :pagination="false" | ||||
|   > | ||||
|     <template #bodyCell="{ column, record }"> | ||||
|       <template v-if="column.key === 'action'"> | ||||
|         <a-space> | ||||
|           <a-button @click="showAddModal(record)">{{ | ||||
|             t("components.admin.domains.userdb.users.edit") | ||||
|           }}</a-button> | ||||
|           <a-popconfirm | ||||
|             :title="t('components.admin.domains.userdb.users.delete') + '?'" | ||||
|             :ok-text="t('components.admin.domains.userdb.users.ok')" | ||||
|             :cancel-text="t('components.admin.domains.userdb.users.cancel')" | ||||
|             @confirm="deleteUser(record.Uid)" | ||||
|           > | ||||
|             <a-button danger>{{ | ||||
|               t("components.admin.domains.userdb.users.delete") | ||||
|             }}</a-button> | ||||
|           </a-popconfirm> | ||||
|         </a-space> | ||||
|       </template> | ||||
|     </template> | ||||
|     <template #title> | ||||
|       <div style="display: flex; justify-content: end"> | ||||
|         <a-button type="primary" @click="showAddModal(undefined)">{{ | ||||
|           $t("components.admin.domains.userdb.users.add") | ||||
|         }}</a-button> | ||||
|       </div> | ||||
|     </template> | ||||
|   </a-table> | ||||
|   <a-modal | ||||
|     v-model:open="page.add.show" | ||||
|     :title=" | ||||
|       page.add.Uid | ||||
|         ? t(`components.admin.domains.userdb.users.edit`) + | ||||
|           ' ' + | ||||
|           page.add.DisplayName | ||||
|         : t(`components.admin.domains.userdb.users.add`) | ||||
|     " | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|       <a-form-item | ||||
|         :label=" | ||||
|           $t('components.admin.domains.userdb.users.add_modal.display_name') | ||||
|         " | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.add.DisplayName"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.userdb.users.add_modal.email')" | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.add.Email"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label=" | ||||
|           $t('components.admin.domains.userdb.users.add_modal.alt_emails') | ||||
|         " | ||||
|       > | ||||
|         <a-textarea v-model:value="page.add.AlternateEmailsStr"> </a-textarea> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.userdb.users.add_modal.quota')" | ||||
|       > | ||||
|         <a-input-number | ||||
|           class="form-input-number" | ||||
|           v-model:value="page.add.Quota" | ||||
|         > | ||||
|         </a-input-number> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.admin.domains.userdb.users.add_modal.password')" | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input-password autocomplete="off" v-model:value="page.add.Password"> | ||||
|         </a-input-password> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label=" | ||||
|           $t('components.admin.domains.userdb.users.add_modal.password_again') | ||||
|         " | ||||
|         :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input-password | ||||
|           autocomplete="off" | ||||
|           v-model:value="page.add.PasswordAgain" | ||||
|         > | ||||
|         </a-input-password> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <template #footer> | ||||
|       <a-space> | ||||
|         <a-button @click="page.add.show = false"> | ||||
|           {{ $t("components.admin.domains.userdb.users.add_modal.cancel") }} | ||||
|         </a-button> | ||||
|         <a-button | ||||
|           :disabled=" | ||||
|             !page.add.DisplayName || | ||||
|             !page.add.Email || | ||||
|             (!page.add.Uid && (!page.add.Password || !page.add.PasswordAgain)) | ||||
|           " | ||||
|           type="primary" | ||||
|           @click="add" | ||||
|         > | ||||
|           {{ $t("components.admin.domains.userdb.users.add_modal.save") }} | ||||
|         </a-button> | ||||
|       </a-space> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 8 }; | ||||
| const wrapperCol = { span: 16 }; | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.domains.userdb.users.col_name"), | ||||
|     dataIndex: "DisplayName", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.domains.userdb.users.col_email"), | ||||
|     dataIndex: "Email", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   domain: string; | ||||
|   sid: string; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   add: { | ||||
|     show: boolean; | ||||
|     Uid: string; | ||||
|     DisplayName: string; | ||||
|     Email: string; | ||||
|     AlternateEmails: string[]; | ||||
|     AlternateEmailsStr: string; | ||||
|     Quota: number; | ||||
|     Password: string; | ||||
|     PasswordAgain: string; | ||||
|   }; | ||||
| }>({ | ||||
|   domain: "", | ||||
|   sid: "", | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   add: { | ||||
|     show: false, | ||||
|     DisplayName: "", | ||||
|     Email: "", | ||||
|     AlternateEmails: [], | ||||
|     AlternateEmailsStr: "", | ||||
|     Quota: -1, | ||||
|     Password: "", | ||||
|     PasswordAgain: "", | ||||
|     Uid: "", | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.sid = route.params.sid as string; | ||||
|   page.domain = route.params.domain as string; | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/users` | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function showAddModal(record: any) { | ||||
|   if (record) { | ||||
|     page.add.Uid = record.Uid; | ||||
|     page.add.Email = record.Email; | ||||
|     page.add.DisplayName = record.DisplayName; | ||||
|     page.add.AlternateEmails = record.AlternateEmails; | ||||
|     page.add.AlternateEmailsStr = page.add.AlternateEmails.join("\n"); | ||||
|     page.add.Quota = record.Quota; | ||||
|     page.add.Password = record.Password; | ||||
|     page.add.PasswordAgain = record.Password; | ||||
|   } else { | ||||
|     page.add.Uid = ""; | ||||
|     page.add.Email = ""; | ||||
|     page.add.DisplayName = ""; | ||||
|     page.add.AlternateEmails = []; | ||||
|     page.add.AlternateEmailsStr = ""; | ||||
|     page.add.Quota = -1; | ||||
|   } | ||||
| 
 | ||||
|   page.add.Password = ""; | ||||
|   page.add.PasswordAgain = ""; | ||||
| 
 | ||||
|   page.add.show = true; | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
|   if (page.add.Password !== page.add.PasswordAgain) { | ||||
|     notifyError("Passwords mismatch"); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.AlternateEmails = page.add.AlternateEmailsStr.split("\n"); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/users`, | ||||
|     { | ||||
|       method: "POST", | ||||
|       body: page.add, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| async function deleteUser(uid: string) { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/domains/${page.domain}/userdb/${page.sid}/users`, | ||||
|     { | ||||
|       method: "DELETE", | ||||
|       body: { | ||||
|         Uid: uid, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,164 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       size="small" | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-button @click="unban(record.ip)"> | ||||
|             {{ t("components.admin.security.blocked_ips.unblock") }} | ||||
|           </a-button> | ||||
|         </template> | ||||
|       </template> | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="8"> | ||||
|             <a-input-search | ||||
|               v-model:value="page.pagination.search" | ||||
|               :placeholder="t('components.admin.security.blocked_ips.search')" | ||||
|               enter-button | ||||
|               allow-clear | ||||
|               @search="get" | ||||
|             /> | ||||
|           </a-col> | ||||
|           <a-col :span="16"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button @click="unban('')">{{ | ||||
|                 page.withSearch | ||||
|                   ? $t("components.admin.security.blocked_ips.unblock_found") | ||||
|                   : $t("components.admin.security.blocked_ips.unblock_all") | ||||
|               }}</a-button> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['2', '10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.security.blocked_ips.ip"), | ||||
|     dataIndex: "ip", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
| }>({ | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/blocked_ips?" + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = (res.data as Array<any>).map((i, idx) => { | ||||
|     return { | ||||
|       ip: i, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   page.pagination.total = res.total; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function unban(ip: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("ip", ip); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/blocked_ips?" + params.toString(), | ||||
|     { | ||||
|       method: "DELETE", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,215 @@ | |||
| <template> | ||||
|   <a-table | ||||
|     class="bw-list-panel-content" | ||||
|     :columns="columns" | ||||
|     :data-source="page.data" | ||||
|     :loading="page.loading" | ||||
|     bordered | ||||
|     size="small" | ||||
|     :pagination="false" | ||||
|   > | ||||
|     <template #bodyCell="{ column, record }"> | ||||
|       <template v-if="column.key === 'action'"> | ||||
|         <a-button @click="del(record.email)"> | ||||
|           {{ t("components.admin.security.bw_list_email.delete") }} | ||||
|         </a-button> | ||||
|       </template> | ||||
|     </template> | ||||
|     <template #title> | ||||
|       <a-row> | ||||
|         <a-col style="display: flex; align-items: flex-end" :span="8"> | ||||
|           <a-input-search | ||||
|             v-model:value="page.pagination.search" | ||||
|             :placeholder="t('components.admin.security.bw_list_email.search')" | ||||
|             enter-button | ||||
|             allow-clear | ||||
|             @search="get" | ||||
|           /> | ||||
|         </a-col> | ||||
|         <a-col :span="16"> | ||||
|           <a-space style="display: flex; justify-content: end"> | ||||
|             <a-button @click="del('')">{{ | ||||
|               page.withSearch | ||||
|                 ? $t("components.admin.security.bw_list_email.delete_found") | ||||
|                 : $t("components.admin.security.bw_list_email.delete_all") | ||||
|             }}</a-button> | ||||
|             <a-button type="primary" @click="page.showAddModal = true"> | ||||
|               <PlusOutlined /> | ||||
|               {{ | ||||
|                 $t("components.admin.security.bw_list_email.add_email") | ||||
|               }}</a-button | ||||
|             > | ||||
|           </a-space> | ||||
|         </a-col> | ||||
|       </a-row> | ||||
|     </template> | ||||
|     <template #footer> | ||||
|       <a-flex justify="flex-end"> | ||||
|         <a-pagination | ||||
|           v-model:value="page.pagination.current" | ||||
|           :defaultPageSize="page.pagination.size" | ||||
|           :total="page.pagination.total" | ||||
|           @change="pageChange" | ||||
|           :pageSizeOptions="['10', '20', '50', '100']" | ||||
|           v-model:pageSize="page.pagination.size" | ||||
|           :showSizeChanger="true" | ||||
|         /> | ||||
|       </a-flex> | ||||
|     </template> | ||||
|   </a-table> | ||||
|   <a-modal | ||||
|     v-model:open="page.showAddModal" | ||||
|     :title="$t(`components.admin.security.bw_list_email.add_email`)" | ||||
|   > | ||||
|     <a-input | ||||
|       v-model:value="page.toAdd" | ||||
|       :placeholder=" | ||||
|         $t(`components.admin.security.bw_list_email.add_email_placeholder`) | ||||
|       " | ||||
|     > | ||||
|     </a-input> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         key="submit" | ||||
|         type="primary" | ||||
|         :loading="page.addLoading" | ||||
|         @click="add" | ||||
|         >{{ $t(`components.admin.security.bw_list_email.add`) }}</a-button | ||||
|       > | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { PlusOutlined } from "@ant-design/icons-vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.security.bw_list_email.email"), | ||||
|     dataIndex: "email", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   showAddModal: boolean; | ||||
|   addLoading: boolean; | ||||
|   toAdd: string; | ||||
| }>({ | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   showAddModal: false, | ||||
|   addLoading: false, | ||||
|   toAdd: "", | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/black_list/email?" + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = (res.data as Array<any>).map((i, idx) => { | ||||
|     return { | ||||
|       email: i, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   page.pagination.total = res.total; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function del(email: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("email", email); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/black_list/email?" + params.toString(), | ||||
|     { | ||||
|       method: "DELETE", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("email", page.toAdd); | ||||
| 
 | ||||
|   page.addLoading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/black_list/email?" + params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.addLoading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.showAddModal = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  | @ -0,0 +1,213 @@ | |||
| <template> | ||||
|   <a-table | ||||
|     class="bw-list-panel-content" | ||||
|     :columns="columns" | ||||
|     :data-source="page.data" | ||||
|     :loading="page.loading" | ||||
|     bordered | ||||
|     size="small" | ||||
|     :pagination="false" | ||||
|   > | ||||
|     <template #bodyCell="{ column, record }"> | ||||
|       <template v-if="column.key === 'action'"> | ||||
|         <a-button @click="del(record.ip)"> | ||||
|           {{ t("components.admin.security.bw_list_ip.delete") }} | ||||
|         </a-button> | ||||
|       </template> | ||||
|     </template> | ||||
|     <template #title> | ||||
|       <a-row> | ||||
|         <a-col style="display: flex; align-items: flex-end" :span="8"> | ||||
|           <a-input-search | ||||
|             v-model:value="page.pagination.search" | ||||
|             :placeholder="t('components.admin.security.bw_list_ip.search')" | ||||
|             enter-button | ||||
|             allow-clear | ||||
|             @search="get" | ||||
|           /> | ||||
|         </a-col> | ||||
|         <a-col :span="16"> | ||||
|           <a-space style="display: flex; justify-content: end"> | ||||
|             <a-button @click="del('')">{{ | ||||
|               page.withSearch | ||||
|                 ? $t("components.admin.security.bw_list_ip.delete_found") | ||||
|                 : $t("components.admin.security.bw_list_ip.delete_all") | ||||
|             }}</a-button> | ||||
|             <a-button type="primary" @click="page.showAddModal = true"> | ||||
|               <PlusOutlined /> | ||||
|               {{ $t("components.admin.security.bw_list_ip.add_ip") }}</a-button | ||||
|             > | ||||
|           </a-space> | ||||
|         </a-col> | ||||
|       </a-row> | ||||
|     </template> | ||||
|     <template #footer> | ||||
|       <a-flex justify="flex-end"> | ||||
|         <a-pagination | ||||
|           v-model:value="page.pagination.current" | ||||
|           :defaultPageSize="page.pagination.size" | ||||
|           :total="page.pagination.total" | ||||
|           @change="pageChange" | ||||
|           :pageSizeOptions="['10', '20', '50', '100']" | ||||
|           v-model:pageSize="page.pagination.size" | ||||
|           :showSizeChanger="true" | ||||
|         /> | ||||
|       </a-flex> | ||||
|     </template> | ||||
|   </a-table> | ||||
|   <a-modal | ||||
|     v-model:open="page.showAddModal" | ||||
|     :title="$t(`components.admin.security.bw_list_ip.add_ip`)" | ||||
|   > | ||||
|     <a-input | ||||
|       v-model:value="page.toAdd" | ||||
|       :placeholder=" | ||||
|         $t(`components.admin.security.bw_list_ip.add_ip_placeholder`) | ||||
|       " | ||||
|     > | ||||
|     </a-input> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         key="submit" | ||||
|         type="primary" | ||||
|         :loading="page.addLoading" | ||||
|         @click="add" | ||||
|         >{{ $t(`components.admin.security.bw_list_ip.add`) }}</a-button | ||||
|       > | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { PlusOutlined } from "@ant-design/icons-vue"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.security.bw_list_ip.ip"), | ||||
|     dataIndex: "ip", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   showAddModal: boolean; | ||||
|   addLoading: boolean; | ||||
|   toAdd: string; | ||||
| }>({ | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   showAddModal: false, | ||||
|   addLoading: false, | ||||
|   toAdd: "", | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/black_list/ip?" + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = (res.data as Array<any>).map((i, idx) => { | ||||
|     return { | ||||
|       ip: i, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   page.pagination.total = res.total; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function del(ip: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("ip", ip); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/black_list/ip?" + params.toString(), | ||||
|     { | ||||
|       method: "DELETE", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("ip", page.toAdd); | ||||
| 
 | ||||
|   page.addLoading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/black_list/ip?" + params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.addLoading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.showAddModal = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  | @ -0,0 +1,215 @@ | |||
| <template> | ||||
|   <a-table | ||||
|     class="bw-list-panel-content" | ||||
|     :columns="columns" | ||||
|     :data-source="page.data" | ||||
|     :loading="page.loading" | ||||
|     bordered | ||||
|     size="small" | ||||
|     :pagination="false" | ||||
|   > | ||||
|     <template #bodyCell="{ column, record }"> | ||||
|       <template v-if="column.key === 'action'"> | ||||
|         <a-button @click="del(record.email)"> | ||||
|           {{ t("components.admin.security.bw_list_email.delete") }} | ||||
|         </a-button> | ||||
|       </template> | ||||
|     </template> | ||||
|     <template #title> | ||||
|       <a-row> | ||||
|         <a-col style="display: flex; align-items: flex-end" :span="8"> | ||||
|           <a-input-search | ||||
|             v-model:value="page.pagination.search" | ||||
|             :placeholder="t('components.admin.security.bw_list_email.search')" | ||||
|             enter-button | ||||
|             allow-clear | ||||
|             @search="get" | ||||
|           /> | ||||
|         </a-col> | ||||
|         <a-col :span="16"> | ||||
|           <a-space style="display: flex; justify-content: end"> | ||||
|             <a-button @click="del('')">{{ | ||||
|               page.withSearch | ||||
|                 ? $t("components.admin.security.bw_list_email.delete_found") | ||||
|                 : $t("components.admin.security.bw_list_email.delete_all") | ||||
|             }}</a-button> | ||||
|             <a-button type="primary" @click="page.showAddModal = true"> | ||||
|               <PlusOutlined /> | ||||
|               {{ | ||||
|                 $t("components.admin.security.bw_list_email.add_email") | ||||
|               }}</a-button | ||||
|             > | ||||
|           </a-space> | ||||
|         </a-col> | ||||
|       </a-row> | ||||
|     </template> | ||||
|     <template #footer> | ||||
|       <a-flex justify="flex-end"> | ||||
|         <a-pagination | ||||
|           v-model:value="page.pagination.current" | ||||
|           :defaultPageSize="page.pagination.size" | ||||
|           :total="page.pagination.total" | ||||
|           @change="pageChange" | ||||
|           :pageSizeOptions="['10', '20', '50', '100']" | ||||
|           v-model:pageSize="page.pagination.size" | ||||
|           :showSizeChanger="true" | ||||
|         /> | ||||
|       </a-flex> | ||||
|     </template> | ||||
|   </a-table> | ||||
|   <a-modal | ||||
|     v-model:open="page.showAddModal" | ||||
|     :title="$t(`components.admin.security.bw_list_email.add_email`)" | ||||
|   > | ||||
|     <a-input | ||||
|       v-model:value="page.toAdd" | ||||
|       :placeholder=" | ||||
|         $t(`components.admin.security.bw_list_email.add_email_placeholder`) | ||||
|       " | ||||
|     > | ||||
|     </a-input> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         key="submit" | ||||
|         type="primary" | ||||
|         :loading="page.addLoading" | ||||
|         @click="add" | ||||
|         >{{ $t(`components.admin.security.bw_list_email.add`) }}</a-button | ||||
|       > | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { PlusOutlined } from "@ant-design/icons-vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.security.bw_list_email.email"), | ||||
|     dataIndex: "email", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   showAddModal: boolean; | ||||
|   addLoading: boolean; | ||||
|   toAdd: string; | ||||
| }>({ | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   showAddModal: false, | ||||
|   addLoading: false, | ||||
|   toAdd: "", | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/white_list/email?" + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = (res.data as Array<any>).map((i, idx) => { | ||||
|     return { | ||||
|       email: i, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   page.pagination.total = res.total; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function del(email: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("email", email); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/white_list/email?" + params.toString(), | ||||
|     { | ||||
|       method: "DELETE", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("email", page.toAdd); | ||||
| 
 | ||||
|   page.addLoading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/white_list/email?" + params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.addLoading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.showAddModal = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  | @ -0,0 +1,213 @@ | |||
| <template> | ||||
|   <a-table | ||||
|     class="bw-list-panel-content" | ||||
|     :columns="columns" | ||||
|     :data-source="page.data" | ||||
|     :loading="page.loading" | ||||
|     bordered | ||||
|     size="small" | ||||
|     :pagination="false" | ||||
|   > | ||||
|     <template #bodyCell="{ column, record }"> | ||||
|       <template v-if="column.key === 'action'"> | ||||
|         <a-button @click="del(record.ip)"> | ||||
|           {{ t("components.admin.security.bw_list_ip.delete") }} | ||||
|         </a-button> | ||||
|       </template> | ||||
|     </template> | ||||
|     <template #title> | ||||
|       <a-row> | ||||
|         <a-col style="display: flex; align-items: flex-end" :span="8"> | ||||
|           <a-input-search | ||||
|             v-model:value="page.pagination.search" | ||||
|             :placeholder="t('components.admin.security.bw_list_ip.search')" | ||||
|             enter-button | ||||
|             allow-clear | ||||
|             @search="get" | ||||
|           /> | ||||
|         </a-col> | ||||
|         <a-col :span="16"> | ||||
|           <a-space style="display: flex; justify-content: end"> | ||||
|             <a-button @click="del('')">{{ | ||||
|               page.withSearch | ||||
|                 ? $t("components.admin.security.bw_list_ip.delete_found") | ||||
|                 : $t("components.admin.security.bw_list_ip.delete_all") | ||||
|             }}</a-button> | ||||
|             <a-button type="primary" @click="page.showAddModal = true"> | ||||
|               <PlusOutlined /> | ||||
|               {{ $t("components.admin.security.bw_list_ip.add_ip") }}</a-button | ||||
|             > | ||||
|           </a-space> | ||||
|         </a-col> | ||||
|       </a-row> | ||||
|     </template> | ||||
|     <template #footer> | ||||
|       <a-flex justify="flex-end"> | ||||
|         <a-pagination | ||||
|           v-model:value="page.pagination.current" | ||||
|           :defaultPageSize="page.pagination.size" | ||||
|           :total="page.pagination.total" | ||||
|           @change="pageChange" | ||||
|           :pageSizeOptions="['10', '20', '50', '100']" | ||||
|           v-model:pageSize="page.pagination.size" | ||||
|           :showSizeChanger="true" | ||||
|         /> | ||||
|       </a-flex> | ||||
|     </template> | ||||
|   </a-table> | ||||
|   <a-modal | ||||
|     v-model:open="page.showAddModal" | ||||
|     :title="$t(`components.admin.security.bw_list_ip.add_ip`)" | ||||
|   > | ||||
|     <a-input | ||||
|       v-model:value="page.toAdd" | ||||
|       :placeholder=" | ||||
|         $t(`components.admin.security.bw_list_ip.add_ip_placeholder`) | ||||
|       " | ||||
|     > | ||||
|     </a-input> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         key="submit" | ||||
|         type="primary" | ||||
|         :loading="page.addLoading" | ||||
|         @click="add" | ||||
|         >{{ $t(`components.admin.security.bw_list_ip.add`) }}</a-button | ||||
|       > | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { PlusOutlined } from "@ant-design/icons-vue"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.security.bw_list_ip.ip"), | ||||
|     dataIndex: "ip", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   showAddModal: boolean; | ||||
|   addLoading: boolean; | ||||
|   toAdd: string; | ||||
| }>({ | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   showAddModal: false, | ||||
|   addLoading: false, | ||||
|   toAdd: "", | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/white_list/ip?" + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = (res.data as Array<any>).map((i, idx) => { | ||||
|     return { | ||||
|       ip: i, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   page.pagination.total = res.total; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function del(ip: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("ip", ip); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/white_list/ip?" + params.toString(), | ||||
|     { | ||||
|       method: "DELETE", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("ip", page.toAdd); | ||||
| 
 | ||||
|   page.addLoading = true; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/security/white_list/ip?" + params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   page.addLoading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.showAddModal = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  | @ -0,0 +1,50 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <Rules | ||||
|       :get-get-url="getUrl" | ||||
|       :get-delete-url="getUrl" | ||||
|       :get-save-url="getUrl" | ||||
|       :get-move-url="moveUrl" | ||||
|       :get-enable-url="enableUrl" | ||||
|       :conditions="conditionsList" | ||||
|       :actions="actionsList" | ||||
|     > | ||||
|       <template #condition-value="valueProps"> | ||||
|         <ConditionValue | ||||
|           :change="valueProps.change" | ||||
|           :value="valueProps.value" | ||||
|           :type="valueProps.type" | ||||
|         > | ||||
|         </ConditionValue> | ||||
|       </template> | ||||
|       <template #action-value="valueProps"> | ||||
|         <ActionValue | ||||
|           :change="valueProps.change" | ||||
|           :value="valueProps.value" | ||||
|           :type="valueProps.type" | ||||
|         > | ||||
|         </ActionValue> | ||||
|       </template> | ||||
|     </Rules> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import ConditionValue from "@/components/common/rules/address/Conditions.vue"; | ||||
| import ActionValue from "@/components/common/rules/address/Actions.vue"; | ||||
| import { conditionsList } from "@/components/common/rules/address/Conditions.vue"; | ||||
| import { actionsList } from "@/components/common/rules/address/Actions.vue"; | ||||
| import Rules from "@/components/common/rules/Rules.vue"; | ||||
| 
 | ||||
| function getUrl(): string { | ||||
|   return `/admin/settings/address_rules`; | ||||
| } | ||||
| 
 | ||||
| function moveUrl(): string { | ||||
|   return `/admin/settings/address_rules/move`; | ||||
| } | ||||
| 
 | ||||
| function enableUrl(): string { | ||||
|   return `/admin/settings/address_rules/enable`; | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,110 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|         <a-form-item :label="t('components.admin.settings.calendars.freebusy')"> | ||||
|           <a-switch v-model:checked="page.AllowFreebusy"> </a-switch> | ||||
|         </a-form-item> | ||||
|         <a-form-item :label="t('components.admin.settings.calendars.showfrom')"> | ||||
|           <a-input-number | ||||
|             class="form-input-number" | ||||
|             :min="0" | ||||
|             v-model:value="page.ShowFromNWeeks" | ||||
|           > | ||||
|           </a-input-number> | ||||
|         </a-form-item> | ||||
|         <a-form-item :label="t('components.admin.settings.calendars.showto')"> | ||||
|           <a-input-number | ||||
|             class="form-input-number" | ||||
|             :min="0" | ||||
|             v-model:value="page.ShowToNWeeks" | ||||
|           > | ||||
|           </a-input-number> | ||||
|         </a-form-item> | ||||
|         <a-form-item | ||||
|           :label="t('components.admin.settings.calendars.localpart')" | ||||
|         > | ||||
|           <a-input v-model:value="page.SenderLP"> </a-input> | ||||
|         </a-form-item> | ||||
|       </a-form> | ||||
|       <a-button | ||||
|         :disabled="!page.contentLoaded" | ||||
|         class="save-button" | ||||
|         type="primary" | ||||
|         size="large" | ||||
|         @click="update" | ||||
|       > | ||||
|         {{ $t("components.admin.settings.calendars.save") }} | ||||
|       </a-button> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 16, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 8, style: { "text-align": "left" } }; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   contentLoaded: boolean; | ||||
|   AllowFreebusy: boolean; | ||||
|   ShowFromNWeeks: number; | ||||
|   ShowToNWeeks: number; | ||||
|   SenderLP: string; | ||||
| }>({ | ||||
|   contentLoaded: false, | ||||
|   AllowFreebusy: false, | ||||
|   ShowFromNWeeks: 0, | ||||
|   ShowToNWeeks: 0, | ||||
|   SenderLP: "", | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch("/admin/settings/calendars"); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.contentLoaded = true; | ||||
|   page.AllowFreebusy = res.data.AllowFreebusy; | ||||
|   page.ShowFromNWeeks = res.data.ShowFromNWeeks; | ||||
|   page.ShowToNWeeks = res.data.ShowToNWeeks; | ||||
|   page.SenderLP = res.data.SenderLP; | ||||
| } | ||||
| 
 | ||||
| async function update() { | ||||
|   const res = await apiFetch("/admin/settings/calendars", { | ||||
|     method: "POST", | ||||
|     body: page, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.admin.settings.calendars.success")); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| .save-button { | ||||
|   display: block; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,272 @@ | |||
| <template> | ||||
|   <a-card class="panel-content" v-if="page.contentLoaded"> | ||||
|     <div v-if="page.accepted"> | ||||
|       <a-descriptions | ||||
|         v-if="page.license" | ||||
|         :column="1" | ||||
|         :title="t(`components.admin.settings.license.title`)" | ||||
|       > | ||||
|         <a-descriptions-item :label="t(`components.admin.settings.license.id`)" | ||||
|           ><div style="font-weight: bold">{{ page.license.ID }}</div> | ||||
|         </a-descriptions-item> | ||||
|         <a-descriptions-item | ||||
|           :label="t(`components.admin.settings.license.edition`)" | ||||
|         > | ||||
|           <div style="font-weight: bold">{{ page.license.Edition }}</div> | ||||
|         </a-descriptions-item> | ||||
|         <a-descriptions-item | ||||
|           :label="t(`components.admin.settings.license.valid_till`)" | ||||
|         > | ||||
|           <div style="font-weight: bold"> | ||||
|             {{ | ||||
|               page.license.ExpiresUnix > 0 | ||||
|                 ? timeToFullDate(page.license.ExpiresUnix * 1000) | ||||
|                 : $t("components.admin.settings.license.valid_forever") | ||||
|             }} | ||||
|           </div> | ||||
|         </a-descriptions-item> | ||||
|         <a-descriptions-item | ||||
|           :label="t(`components.admin.settings.license.max_mailbox_count`)" | ||||
|         > | ||||
|           <div style="font-weight: bold"> | ||||
|             {{ page.license.MailboxCount }} | ||||
|           </div></a-descriptions-item | ||||
|         > | ||||
|         <a-descriptions-item | ||||
|           :label="t(`components.admin.settings.license.send_report`)" | ||||
|         > | ||||
|           <div style="font-weight: bold"> | ||||
|             {{ | ||||
|               page.license.SendReport && page.license.SendReportEmail !== "" | ||||
|                 ? page.license.SendReportEmail | ||||
|                 : $t("components.admin.settings.license.send_report_no") | ||||
|             }} | ||||
|           </div> | ||||
|         </a-descriptions-item> | ||||
|         <a-descriptions-item | ||||
|           :label="t(`components.admin.settings.license.customer_name`)" | ||||
|         > | ||||
|           <div style="font-weight: bold"> | ||||
|             {{ page.license.CustomerName }} | ||||
|           </div> | ||||
|         </a-descriptions-item> | ||||
|         <a-descriptions-item | ||||
|           :label="t(`components.admin.settings.license.customer_type`)" | ||||
|         > | ||||
|           <div style="font-weight: bold"> | ||||
|             {{ page.license.CustomerTypeStr }} | ||||
|           </div> | ||||
|         </a-descriptions-item> | ||||
|         <a-descriptions-item | ||||
|           :label="t(`components.admin.settings.license.customer_data`)" | ||||
|         > | ||||
|           <a-descriptions :column="1"> | ||||
|             <a-descriptions-item | ||||
|               v-for="{ Title, Value } in page.license.CustomerReqs" | ||||
|               :label="Title" | ||||
|             > | ||||
|               <div style="font-weight: bold"> | ||||
|                 {{ Value }} | ||||
|               </div> | ||||
|             </a-descriptions-item> | ||||
|           </a-descriptions> | ||||
|         </a-descriptions-item> | ||||
|       </a-descriptions> | ||||
|       <a-alert | ||||
|         style="margin-bottom: 16px" | ||||
|         v-else | ||||
|         type="error" | ||||
|         showIcon | ||||
|         :message="t(`components.admin.settings.license.no_install`)" | ||||
|       /> | ||||
|       <a-space style="display: flex; justify-content: center"> | ||||
|         <a-button size="large" @click="page.showEula = true"> | ||||
|           {{ $t("components.admin.settings.license.show_eula") }} | ||||
|         </a-button> | ||||
|         <a-upload | ||||
|           :file-list="page.files" | ||||
|           :before-upload=" | ||||
|           (file: any) => { | ||||
|             upload(file) | ||||
|             return false | ||||
|           } | ||||
|         " | ||||
|         > | ||||
|           <a-button size="large" :loading="page.uploading" type="primary"> | ||||
|             <UploadOutlined /> | ||||
|             {{ $t("components.admin.settings.license.load") }} | ||||
|           </a-button> | ||||
|         </a-upload> | ||||
|       </a-space> | ||||
|     </div> | ||||
|     <div v-else> | ||||
|       <pre | ||||
|         style=" | ||||
|           white-space: -moz-pre-wrap; | ||||
|           white-space: -pre-wrap; | ||||
|           white-space: -o-pre-wrap; | ||||
|           white-space: pre-wrap; | ||||
|         " | ||||
|       > | ||||
|       {{ $t("eula") }} | ||||
|       </pre> | ||||
|       <br /> | ||||
|       <a-button | ||||
|         class="save-button" | ||||
|         type="primary" | ||||
|         size="large" | ||||
|         style="width: fit-content" | ||||
|         @click="accept" | ||||
|       > | ||||
|         {{ $t("components.admin.settings.license.accept") }} | ||||
|       </a-button> | ||||
|     </div> | ||||
|   </a-card> | ||||
|   <a-modal v-model:open="page.showEula" width="80%"> | ||||
|     <template #footer></template> | ||||
|     <pre | ||||
|       style=" | ||||
|         white-space: -moz-pre-wrap; | ||||
|         white-space: -pre-wrap; | ||||
|         white-space: -o-pre-wrap; | ||||
|         white-space: pre-wrap; | ||||
|       " | ||||
|     > | ||||
|       {{ $t("eula") }} | ||||
|       </pre | ||||
|     > | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal, type UploadChangeParam } from "ant-design-vue"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { saveAndRestart } from "@/composables/restart"; | ||||
| import { message } from "ant-design-vue"; | ||||
| import { UploadOutlined } from "@ant-design/icons-vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { timeToDate, timeToDateTime, timeToFullDate } from "@/composables/misc"; | ||||
| import { uploadListProps } from "ant-design-vue/es/upload/interface"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   uploading: boolean; | ||||
|   contentLoaded: boolean; | ||||
|   accepted: boolean; | ||||
|   showEula: boolean; | ||||
|   files: any; | ||||
|   license: { | ||||
|     ID: string; | ||||
|     Edition: string; | ||||
|     ExpiresUnix: number; | ||||
|     MailboxCount: number; | ||||
|     SendReport: boolean; | ||||
|     SendReportEmail: string; | ||||
|     CustomerName: string; | ||||
|     CustomerType: number; | ||||
|     CustomerTypeStr: string; | ||||
|     CustomerReqs: { | ||||
|       Title: string; | ||||
|       Value: string; | ||||
|     }[]; | ||||
|   }; | ||||
| }>({ | ||||
|   uploading: false, | ||||
|   files: [], | ||||
|   showEula: false, | ||||
|   contentLoaded: false, | ||||
|   accepted: false, | ||||
|   license: { | ||||
|     ID: "", | ||||
|     Edition: "", | ||||
|     ExpiresUnix: 0, | ||||
|     MailboxCount: 0, | ||||
|     SendReport: false, | ||||
|     SendReportEmail: "", | ||||
|     CustomerName: "", | ||||
|     CustomerType: 0, | ||||
|     CustomerTypeStr: "", | ||||
|     CustomerReqs: [], | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch("/admin/settings/license"); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.accepted = res.data.EulaAccepted; | ||||
|   page.license = res.data.License; | ||||
|   if (page.license) | ||||
|     if (page.license.CustomerType === 1) { | ||||
|       page.license.CustomerTypeStr = t( | ||||
|         "components.admin.settings.license.fiz_type" | ||||
|       ); | ||||
|     } else if (page.license.CustomerType === 2) { | ||||
|       page.license.CustomerTypeStr = t( | ||||
|         "components.admin.settings.license.ur_type" | ||||
|       ); | ||||
|     } | ||||
|   page.contentLoaded = true; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function accept() { | ||||
|   const res = await apiFetch("/admin/settings/license/accept", { | ||||
|     method: "POST", | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   get(); | ||||
| } | ||||
| 
 | ||||
| async function upload(file: any) { | ||||
|   page.uploading = true; | ||||
|   const formData = new FormData(); | ||||
|   formData.append("file", file); | ||||
| 
 | ||||
|   const res = await apiFetch("/admin/settings/license", { | ||||
|     method: "POST", | ||||
|     body: formData, | ||||
|     isFormData: true, | ||||
|   }); | ||||
| 
 | ||||
|   page.uploading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   get(); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| .save-button { | ||||
|   width: 30%; | ||||
|   min-width: 150px; | ||||
|   display: block; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,257 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|         <a-form-item :label="$t('components.admin.settings.settings_db.Type')"> | ||||
|           <a-select v-model:value="page.new.Type" @change="getNew"> | ||||
|             <a-select-option value="pgsqlSettingsDB" | ||||
|               >PostgreSQL</a-select-option | ||||
|             > | ||||
|             <a-select-option value="sqliteSettingsDB">SQLite</a-select-option> | ||||
|           </a-select> | ||||
|         </a-form-item> | ||||
|         <template v-if="page.new.Type === 'pgsqlSettingsDB'"> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.settings.settings_db.ConnParams.Host')" | ||||
|           > | ||||
|             <a-input v-model:value="page.new.ConnParams.Host"> </a-input> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.settings.settings_db.ConnParams.Port')" | ||||
|           > | ||||
|             <a-input-number | ||||
|               class="form-input-number" | ||||
|               v-model:value="page.new.ConnParams.Port" | ||||
|             > | ||||
|             </a-input-number> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.settings.settings_db.ConnParams.Db')" | ||||
|           > | ||||
|             <a-input v-model:value="page.new.ConnParams.Db"> </a-input> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.settings.settings_db.ConnParams.User')" | ||||
|           > | ||||
|             <a-input v-model:value="page.new.ConnParams.User"> </a-input> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label="$t('components.admin.settings.settings_db.ConnParams.Pass')" | ||||
|           > | ||||
|             <a-input-password | ||||
|               autocomplete="off" | ||||
|               v-model:value="page.new.ConnParams.Pass" | ||||
|             > | ||||
|             </a-input-password> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t('components.admin.settings.settings_db.ConnParams.MaxConn') | ||||
|             " | ||||
|           > | ||||
|             <a-input-number | ||||
|               class="form-input-number" | ||||
|               :min="1" | ||||
|               v-model:value="page.new.ConnParams.MaxConn" | ||||
|             > | ||||
|             </a-input-number> | ||||
|           </a-form-item> | ||||
|         </template> | ||||
|         <template v-if="page.new.Type === 'sqliteSettingsDB'"> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t('components.admin.settings.settings_db.PathParams.DatabaseDir') | ||||
|             " | ||||
|           > | ||||
|             <a-input v-model:value="page.new.PathParams.DatabaseDir"> </a-input> | ||||
|           </a-form-item> | ||||
|         </template> | ||||
| 
 | ||||
|         <a-space style="display: flex; justify-content: center"> | ||||
|           <a-button | ||||
|             :loading="page.checkLoading" | ||||
|             :disabled="!page.new.Type" | ||||
|             class="save-button" | ||||
|             size="large" | ||||
|             @click="check" | ||||
|           > | ||||
|             {{ $t("components.admin.settings.settings_db.check") }} | ||||
|           </a-button> | ||||
|           <a-button | ||||
|             :disabled="!page.new.Type || !page.contentLoaded" | ||||
|             class="save-button" | ||||
|             type="primary" | ||||
|             size="large" | ||||
|             style="width: fit-content" | ||||
|             @click="saveAndRestart(save)" | ||||
|           > | ||||
|             {{ | ||||
|               page.old.Type != page.new.Type | ||||
|                 ? $t("components.admin.settings.settings_db.change") | ||||
|                 : $t("components.admin.settings.settings_db.save") | ||||
|             }} | ||||
|           </a-button> | ||||
|         </a-space> | ||||
|       </a-form> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { saveAndRestart } from "@/composables/restart"; | ||||
| import { message } from "ant-design-vue"; | ||||
| 
 | ||||
| import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| interface ConnParams { | ||||
|   Host: string; | ||||
|   Port: number; | ||||
|   Db: string; | ||||
|   User: string; | ||||
|   Pass: string; | ||||
|   MaxConn: number; | ||||
| } | ||||
| 
 | ||||
| interface PathParams { | ||||
|   DatabaseDir: string; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   contentLoaded: boolean; | ||||
|   old: { | ||||
|     Type: string; | ||||
|     ConnParams: ConnParams; | ||||
|     PathParams: PathParams; | ||||
|   }; | ||||
|   new: { | ||||
|     Type: string; | ||||
|     ConnParams: ConnParams; | ||||
|     PathParams: PathParams; | ||||
|   }; | ||||
| 
 | ||||
|   checkLoading: boolean; | ||||
| }>({ | ||||
|   contentLoaded: false, | ||||
|   new: { | ||||
|     Type: "", | ||||
|     ConnParams: { | ||||
|       Host: "", | ||||
|       Port: 0, | ||||
|       Db: "", | ||||
|       User: "", | ||||
|       Pass: "", | ||||
|       MaxConn: 0, | ||||
|     }, | ||||
|     PathParams: { | ||||
|       DatabaseDir: "", | ||||
|     }, | ||||
|   }, | ||||
|   checkLoading: false, | ||||
|   old: { | ||||
|     Type: "", | ||||
|     ConnParams: { | ||||
|       Host: "", | ||||
|       Port: 0, | ||||
|       Db: "", | ||||
|       User: "", | ||||
|       Pass: "", | ||||
|       MaxConn: 0, | ||||
|     }, | ||||
|     PathParams: { | ||||
|       DatabaseDir: "", | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const labelCol = { span: 16, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 8 }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch("/admin/settings/settings_db"); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.new = { ...res.data }; | ||||
|   page.old = { ...res.data }; | ||||
|   page.contentLoaded = true; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function getNew() { | ||||
|   if (page.new.Type === page.old.Type) { | ||||
|     page.new = { ...page.old }; | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/settings/settings_db/new?type=${page.new.Type}` | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.new = res.data; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function save(): Promise<boolean> { | ||||
|   const res = await apiFetch("/admin/settings/settings_db", { | ||||
|     method: "POST", | ||||
|     body: page.new, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| async function check() { | ||||
|   page.checkLoading = true; | ||||
|   const res = await apiFetch("/admin/settings/settings_db/check", { | ||||
|     method: "POST", | ||||
|     body: page.new, | ||||
|   }); | ||||
|   page.checkLoading = false; | ||||
|   if (res.error) { | ||||
|     notifyError( | ||||
|       t("components.admin.settings.settings_db.check_error") + res.error | ||||
|     ); | ||||
| 
 | ||||
|     return; | ||||
|   } | ||||
|   notifySuccess(t("components.admin.settings.settings_db.check_success")); | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| .input-number { | ||||
|   width: 50%; | ||||
| } | ||||
| .save-button { | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,388 @@ | |||
| <template> | ||||
|   <a-flex wrap justify="center" gap="large"> | ||||
|     <a-card> | ||||
|       <a-statistic | ||||
|         :title="t('components.admin.settings.smtp_queue.manage.total')" | ||||
|         :value="page.total" | ||||
|         style="margin-right: 50px" | ||||
|       /> | ||||
|     </a-card> | ||||
|     <a-card> | ||||
|       <a-statistic | ||||
|         :title="t('components.admin.settings.smtp_queue.manage.new')" | ||||
|         :value="page.new" | ||||
|         style="margin-right: 50px" | ||||
|       /> | ||||
|     </a-card> | ||||
| 
 | ||||
|     <a-card> | ||||
|       <a-statistic | ||||
|         :title="t('components.admin.settings.smtp_queue.manage.resend')" | ||||
|         :value="page.awaited" | ||||
|         style="margin-right: 50px" | ||||
|       /> | ||||
|     </a-card> | ||||
|   </a-flex> | ||||
|   <br /> | ||||
|   <a-table | ||||
|     :columns="columns" | ||||
|     :data-source="page.data" | ||||
|     :loading="page.loading" | ||||
|     bordered | ||||
|     size="small" | ||||
|     :pagination="false" | ||||
|   > | ||||
|     <template #bodyCell="{ column, record }"> | ||||
|       <template v-if="column.key === 'action'"> | ||||
|         <a-space> | ||||
|           <a-button @click="processMsg(record.ID)">{{ | ||||
|             t("components.admin.settings.smtp_queue.manage.process_now") | ||||
|           }}</a-button> | ||||
|           <a-popconfirm | ||||
|             :title=" | ||||
|               t('components.admin.settings.smtp_queue.manage.delete') + '?' | ||||
|             " | ||||
|             :ok-text="t('components.admin.settings.smtp_queue.manage.ok')" | ||||
|             :cancel-text=" | ||||
|               t('components.admin.settings.smtp_queue.manage.cancel') | ||||
|             " | ||||
|             @confirm="deleteMsg(record.ID)" | ||||
|           > | ||||
|             <a-button danger> | ||||
|               {{ t("components.admin.settings.smtp_queue.manage.delete") }} | ||||
|             </a-button> | ||||
|           </a-popconfirm> | ||||
|         </a-space> | ||||
|       </template> | ||||
|       <template v-if="column.key === 'WillResend'"> | ||||
|         {{ | ||||
|           record.WillResend === "" | ||||
|             ? t( | ||||
|                 "components.admin.settings.smtp_queue.manage.column_will_resend_now" | ||||
|               ) | ||||
|             : record.WillResend | ||||
|         }} | ||||
|       </template> | ||||
|     </template> | ||||
|     <template #title> | ||||
|       <a-row> | ||||
|         <a-col style="display: flex; align-items: flex-end" :span="6"> | ||||
|           <a-input-search | ||||
|             v-model:value="page.pagination.search" | ||||
|             :placeholder=" | ||||
|               t('components.admin.settings.smtp_queue.manage.search') | ||||
|             " | ||||
|             enter-button | ||||
|             allow-clear | ||||
|             @search="get" | ||||
|           /> | ||||
|         </a-col> | ||||
|         <a-col :span="18"> | ||||
|           <a-space | ||||
|             v-if="page.withSearch" | ||||
|             wrap | ||||
|             style="display: flex; justify-content: end" | ||||
|           > | ||||
|             <a-button @click="processAwaiting">{{ | ||||
|               $t("components.admin.settings.smtp_queue.manage.process_filtered") | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title=" | ||||
|                 t( | ||||
|                   'components.admin.settings.smtp_queue.manage.clear_filtered' | ||||
|                 ) + '?' | ||||
|               " | ||||
|               :ok-text="t('components.admin.settings.smtp_queue.manage.ok')" | ||||
|               :cancel-text=" | ||||
|                 t('components.admin.settings.smtp_queue.manage.cancel') | ||||
|               " | ||||
|               @confirm="clearAll" | ||||
|             > | ||||
|               <a-button danger>{{ | ||||
|                 $t("components.admin.settings.smtp_queue.manage.clear_filtered") | ||||
|               }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|           <a-space v-else wrap style="display: flex; justify-content: end"> | ||||
|             <a-button @click="processAwaiting">{{ | ||||
|               $t( | ||||
|                 "components.admin.settings.smtp_queue.manage.process_awaiting_resend" | ||||
|               ) | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title=" | ||||
|                 t( | ||||
|                   'components.admin.settings.smtp_queue.manage.delete_awaiting_resend' | ||||
|                 ) + '?' | ||||
|               " | ||||
|               :ok-text="t('components.admin.settings.smtp_queue.manage.ok')" | ||||
|               :cancel-text=" | ||||
|                 t('components.admin.settings.smtp_queue.manage.cancel') | ||||
|               " | ||||
|               @confirm="deleteAwaiting" | ||||
|             > | ||||
|               <a-button danger>{{ | ||||
|                 $t( | ||||
|                   "components.admin.settings.smtp_queue.manage.delete_awaiting_resend" | ||||
|                 ) | ||||
|               }}</a-button> | ||||
|             </a-popconfirm> | ||||
| 
 | ||||
|             <a-popconfirm | ||||
|               :title=" | ||||
|                 t('components.admin.settings.smtp_queue.manage.clear_all') + '?' | ||||
|               " | ||||
|               :ok-text="t('components.admin.settings.smtp_queue.manage.ok')" | ||||
|               :cancel-text=" | ||||
|                 t('components.admin.settings.smtp_queue.manage.cancel') | ||||
|               " | ||||
|               @confirm="clearAll" | ||||
|             > | ||||
|               <a-button danger>{{ | ||||
|                 $t("components.admin.settings.smtp_queue.manage.clear_all") | ||||
|               }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </a-col> | ||||
|       </a-row> | ||||
|     </template> | ||||
|     <template #footer> | ||||
|       <a-flex justify="flex-end"> | ||||
|         <!-- <a-space> | ||||
|           <a-popconfirm | ||||
|             :title=" | ||||
|               t('components.admin.settings.smtp_queue.manage.clear_all') + '?' | ||||
|             " | ||||
|             :ok-text="t('components.admin.settings.smtp_queue.manage.ok')" | ||||
|             :cancel-text=" | ||||
|               t('components.admin.settings.smtp_queue.manage.cancel') | ||||
|             " | ||||
|             @confirm="clearAll" | ||||
|           > | ||||
|             <a-button danger>{{ | ||||
|               $t("components.admin.settings.smtp_queue.manage.clear_all") | ||||
|             }}</a-button> | ||||
|           </a-popconfirm> | ||||
|           <a-popconfirm | ||||
|             :title=" | ||||
|               t( | ||||
|                 'components.admin.settings.smtp_queue.manage.delete_awaiting_resend' | ||||
|               ) + '?' | ||||
|             " | ||||
|             :ok-text="t('components.admin.settings.smtp_queue.manage.ok')" | ||||
|             :cancel-text=" | ||||
|               t('components.admin.settings.smtp_queue.manage.cancel') | ||||
|             " | ||||
|             @confirm="deleteAwaiting" | ||||
|           > | ||||
|             <a-button danger>{{ | ||||
|               $t( | ||||
|                 "components.admin.settings.smtp_queue.manage.delete_awaiting_resend" | ||||
|               ) | ||||
|             }}</a-button> | ||||
|           </a-popconfirm> | ||||
| 
 | ||||
|           <a-button @click="processAwaiting">{{ | ||||
|             $t( | ||||
|               "components.admin.settings.smtp_queue.manage.process_awaiting_resend" | ||||
|             ) | ||||
|           }}</a-button> | ||||
|         </a-space> --> | ||||
|         <a-pagination | ||||
|           v-model:value="page.pagination.current" | ||||
|           :defaultPageSize="page.pagination.size" | ||||
|           :total="page.pagination.total" | ||||
|           @change="pageChange" | ||||
|           :pageSizeOptions="['10', '20', '50', '100']" | ||||
|           v-model:pageSize="page.pagination.size" | ||||
|           :showSizeChanger="true" | ||||
|         /> | ||||
|       </a-flex> | ||||
|     </template> | ||||
|   </a-table> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { DeleteOutlined } from "@ant-design/icons-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.admin.settings.smtp_queue.manage.column_sender"), | ||||
|     dataIndex: "Sender", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.settings.smtp_queue.manage.column_receiver"), | ||||
|     dataIndex: "Receiver", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.settings.smtp_queue.manage.column_added"), | ||||
|     dataIndex: "Added", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.admin.settings.smtp_queue.manage.column_will_resend"), | ||||
|     dataIndex: "WillResend", | ||||
|     key: "WillResend", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   total: number; | ||||
|   new: number; | ||||
|   awaited: number; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
| }>({ | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   total: 0, | ||||
|   new: 0, | ||||
|   awaited: 0, | ||||
|   data: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/settings/smtp_queue/messages?" + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.total = res.data.Total; | ||||
|   page.new = res.data.New; | ||||
|   page.awaited = res.data.Awaited; | ||||
|   page.data = res.data.Messages; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function clearAll() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/settings/smtp_queue/messages/delete?" + params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| async function deleteMsg(id: string) { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/settings/smtp_queue/messages/delete/${id}`, | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function processMsg(id: string) { | ||||
|   const res = await apiFetch( | ||||
|     `/admin/settings/smtp_queue/messages/retry/${id}`, | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| async function deleteAwaiting() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/settings/smtp_queue/messages/delete_awaiting?" + params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| async function processAwaiting() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     "/admin/settings/smtp_queue/messages/retry?" + params.toString(), | ||||
|     { | ||||
|       method: "POST", | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,304 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|         <a-form-item | ||||
|           :label="$t('components.admin.settings.smtp_queue.settings.Type')" | ||||
|         > | ||||
|           <a-select v-model:value="page.new.Type" @change="getNew"> | ||||
|             <a-select-option value="pgsqlSmtpQueue">PostgreSQL</a-select-option> | ||||
|             <a-select-option value="sqliteFilesSmtpQueue" | ||||
|               >SQLite</a-select-option | ||||
|             > | ||||
|           </a-select> | ||||
|         </a-form-item> | ||||
|         <template | ||||
|           v-if=" | ||||
|             page.new.Type === 'pgsqlSmtpQueue' | ||||
|           " | ||||
|         > | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t( | ||||
|                 'components.admin.settings.smtp_queue.settings.ConnParams.Host' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             <a-input v-model:value="page.new.ConnParams.Host"> </a-input> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t( | ||||
|                 'components.admin.settings.smtp_queue.settings.ConnParams.Port' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             <a-input-number | ||||
|               class="form-input-number" | ||||
|               v-model:value="page.new.ConnParams.Port" | ||||
|             > | ||||
|             </a-input-number> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t('components.admin.settings.smtp_queue.settings.ConnParams.Db') | ||||
|             " | ||||
|           > | ||||
|             <a-input v-model:value="page.new.ConnParams.Db"> </a-input> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t( | ||||
|                 'components.admin.settings.smtp_queue.settings.ConnParams.User' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             <a-input v-model:value="page.new.ConnParams.User"> </a-input> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t( | ||||
|                 'components.admin.settings.smtp_queue.settings.ConnParams.Pass' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             <a-input-password | ||||
|               autocomplete="off" | ||||
|               v-model:value="page.new.ConnParams.Pass" | ||||
|             > | ||||
|             </a-input-password> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t( | ||||
|                 'components.admin.settings.smtp_queue.settings.ConnParams.MaxConn' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             <a-input-number | ||||
|               :min="1" | ||||
|               class="form-input-number" | ||||
|               v-model:value="page.new.ConnParams.MaxConn" | ||||
|             > | ||||
|             </a-input-number> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t( | ||||
|                 'components.admin.settings.smtp_queue.settings.ConnParams.MaxQueueItems' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             <a-input-number | ||||
|               :min="20" | ||||
|               class="form-input-number" | ||||
|               v-model:value="page.new.ConnParams.MaxQueueItems" | ||||
|             > | ||||
|             </a-input-number> | ||||
|           </a-form-item> | ||||
|         </template> | ||||
|         <template v-if="page.new.Type === 'sqliteFilesSmtpQueue'"> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               $t( | ||||
|                 'components.admin.settings.smtp_queue.settings.PathParams.DatabaseDir' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             <a-input v-model:value="page.new.PathParams.SMTPQueueDir"> | ||||
|             </a-input> | ||||
|           </a-form-item> | ||||
|         </template> | ||||
| 
 | ||||
|         <a-space style="display: flex; justify-content: center"> | ||||
|           <a-button | ||||
|             :loading="page.checkLoading" | ||||
|             :disabled="!page.new.Type" | ||||
|             class="save-button" | ||||
|             size="large" | ||||
|             @click="check" | ||||
|           > | ||||
|             {{ $t("components.admin.settings.smtp_queue.settings.check") }} | ||||
|           </a-button> | ||||
|           <a-button | ||||
|             :disabled="!page.new.Type || !page.contentLoaded" | ||||
|             class="save-button" | ||||
|             type="primary" | ||||
|             size="large" | ||||
|             style="width: fit-content" | ||||
|             @click="saveAndRestart(save)" | ||||
|           > | ||||
|             {{ | ||||
|               page.old.Type != page.new.Type | ||||
|                 ? $t("components.admin.settings.smtp_queue.settings.change") | ||||
|                 : $t("components.admin.settings.smtp_queue.settings.save") | ||||
|             }} | ||||
|           </a-button> | ||||
|         </a-space> | ||||
|       </a-form> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { saveAndRestart } from "@/composables/restart"; | ||||
| import { message } from "ant-design-vue"; | ||||
| 
 | ||||
| import { useI18n } from "vue-i18n"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| interface ConnParams { | ||||
|   Host: string; | ||||
|   Port: number; | ||||
|   Db: string; | ||||
|   User: string; | ||||
|   Pass: string; | ||||
|   MaxConn: number; | ||||
|   MaxQueueItems: number; | ||||
| } | ||||
| 
 | ||||
| interface PathParams { | ||||
|   SMTPQueueDir: string; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   contentLoaded: boolean; | ||||
|   old: { | ||||
|     Type: string; | ||||
|     ConnParams: ConnParams; | ||||
|     PathParams: PathParams; | ||||
|   }; | ||||
|   new: { | ||||
|     Type: string; | ||||
|     ConnParams: ConnParams; | ||||
|     PathParams: PathParams; | ||||
|   }; | ||||
| 
 | ||||
|   checkLoading: boolean; | ||||
| }>({ | ||||
|   contentLoaded: false, | ||||
|   new: { | ||||
|     Type: "", | ||||
|     ConnParams: { | ||||
|       Host: "", | ||||
|       Port: 0, | ||||
|       Db: "", | ||||
|       User: "", | ||||
|       Pass: "", | ||||
|       MaxConn: 0, | ||||
|       MaxQueueItems: 0, | ||||
|     }, | ||||
|     PathParams: { | ||||
|       SMTPQueueDir: "", | ||||
|     }, | ||||
|   }, | ||||
|   checkLoading: false, | ||||
|   old: { | ||||
|     Type: "", | ||||
|     ConnParams: { | ||||
|       Host: "", | ||||
|       Port: 0, | ||||
|       Db: "", | ||||
|       User: "", | ||||
|       Pass: "", | ||||
|       MaxConn: 0, | ||||
|       MaxQueueItems: 0, | ||||
|     }, | ||||
|     PathParams: { | ||||
|       SMTPQueueDir: "", | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const labelCol = { span: 16, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 8 }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch("/admin/settings/smtp_queue/settings"); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.new = { ...res.data }; | ||||
|   page.old = { ...res.data }; | ||||
| 
 | ||||
|   page.contentLoaded = true; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function getNew() { | ||||
|   if (page.new.Type === page.old.Type) { | ||||
|     page.new = { ...page.old }; | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/admin/settings/smtp_queue/settings/new?type=${page.new.Type}` | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.new = res.data; | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function save(): Promise<boolean> { | ||||
|   const res = await apiFetch("/admin/settings/smtp_queue/settings", { | ||||
|     method: "POST", | ||||
|     body: page.new, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| async function check() { | ||||
|   page.checkLoading = true; | ||||
|   const res = await apiFetch("/admin/settings/smtp_queue/settings/check", { | ||||
|     method: "POST", | ||||
|     body: page.new, | ||||
|   }); | ||||
|   page.checkLoading = false; | ||||
|   if (res.error) { | ||||
|     notifyError( | ||||
|       t("components.admin.settings.smtp_queue.settings.check_error") + res.error | ||||
|     ); | ||||
| 
 | ||||
|     return; | ||||
|   } | ||||
|   notifySuccess( | ||||
|     t("components.admin.settings.smtp_queue.settings.check_success") | ||||
|   ); | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 600px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| 
 | ||||
| .save-button { | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,815 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-input-search | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder=" | ||||
|                   t('components.common.shared_address_books.search') | ||||
|                 " | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col :span="6"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button type="primary" @click="showAddModal"> | ||||
|                 <PlusOutlined /> | ||||
|                 {{ t("components.common.shared_address_books.add_book") }} | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 v-if="page.withSearch" | ||||
|                 :title=" | ||||
|                   t('components.common.shared_address_books.delete_found') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_address_books.ok')" | ||||
|                 :cancel-text=" | ||||
|                   t('components.common.shared_address_books.cancel') | ||||
|                 " | ||||
|                 @confirm="deleteBook(undefined)" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_address_books.delete_found") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|               <a-popconfirm | ||||
|                 v-else | ||||
|                 :title=" | ||||
|                   t('components.common.shared_address_books.delete_all') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_address_books.ok')" | ||||
|                 :cancel-text=" | ||||
|                   t('components.common.shared_address_books.cancel') | ||||
|                 " | ||||
|                 @confirm="deleteBook(undefined)" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_address_books.delete_all") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button type="primary" @click="showAccessModal(record.Id)">{{ | ||||
|               t("components.common.shared_address_books.edit_access") | ||||
|             }}</a-button> | ||||
|             <a-button @click="showPropModal(record.Id, record.Name)">{{ | ||||
|               t("components.common.shared_address_books.properties") | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title="t('components.common.shared_address_books.delete') + '?'" | ||||
|               :ok-text="t('components.common.shared_address_books.ok')" | ||||
|               :cancel-text="t('components.common.shared_address_books.cancel')" | ||||
|               @confirm="deleteBook(record.Id)" | ||||
|             > | ||||
|               <a-button danger>{{ | ||||
|                 t("components.common.shared_address_books.delete") | ||||
|               }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     style="width: 300px" | ||||
|     v-model:open="page.add.show" | ||||
|     :title="t('components.common.shared_address_books.add_book')" | ||||
|   > | ||||
|     <a-input | ||||
|       style="margin-top: 8px" | ||||
|       :placeholder="$t('components.common.shared_address_books.add_name')" | ||||
|       v-model:value="page.add.Name" | ||||
|     > | ||||
|     </a-input> | ||||
|     <template #footer> | ||||
|       <a-button type="primary" :disabled="!page.add.Name" @click="addBook"> | ||||
|         {{ t("components.common.shared_address_books.add_book") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
|   <a-modal | ||||
|     style="width: 300px" | ||||
|     v-model:open="page.props.show" | ||||
|     :title="t('components.common.shared_address_books.properties')" | ||||
|   > | ||||
|     <a-input | ||||
|       :placeholder="$t('components.common.shared_address_books.add_name')" | ||||
|       v-model:value="page.props.Name" | ||||
|     > | ||||
|     </a-input> | ||||
|     <template #footer> | ||||
|       <a-button type="primary" :disabled="!page.props.Name" @click="updateBook"> | ||||
|         {{ t("components.common.shared_address_books.update_book") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
|   <a-modal | ||||
|     v-model:open="page.access.show" | ||||
|     style="width: 50%; min-width: 720px" | ||||
|     @cancel="get" | ||||
|   > | ||||
|     <a-table | ||||
|       :columns="accessColumns" | ||||
|       :data-source="page.access.data" | ||||
|       :loading="page.access.loading" | ||||
|       bordered | ||||
|       size="small" | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-radio-group | ||||
|                 @change="getAccess()" | ||||
|                 v-model:value="page.access.type" | ||||
|               > | ||||
|                 <a-radio-button value="email">{{ | ||||
|                   t("components.common.shared_address_books.access_type_email") | ||||
|                 }}</a-radio-button> | ||||
|                 <a-radio-button value="group">{{ | ||||
|                   t("components.common.shared_address_books.access_type_group") | ||||
|                 }}</a-radio-button> | ||||
|               </a-radio-group> | ||||
| 
 | ||||
|               <a-input-search | ||||
|                 v-model:value="page.access.pagination.search" | ||||
|                 :placeholder=" | ||||
|                   t('components.common.shared_address_books.access_search') | ||||
|                 " | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="getAccess" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col | ||||
|             style="display: flex; align-items: flex-end; justify-content: end" | ||||
|             :span="6" | ||||
|           > | ||||
|             <a-space> | ||||
|               <a-button | ||||
|                 v-if="page.access.type === 'email'" | ||||
|                 type="primary" | ||||
|                 @click="page.access.showAdd = true" | ||||
|               > | ||||
|                 {{ | ||||
|                   t( | ||||
|                     "components.common.shared_address_books.access_type_email_add" | ||||
|                   ) | ||||
|                 }} | ||||
|               </a-button> | ||||
|               <a-button | ||||
|                 v-if="page.access.type === 'group'" | ||||
|                 type="primary" | ||||
|                 @click="page.access.showAdd = true" | ||||
|               > | ||||
|                 {{ | ||||
|                   t( | ||||
|                     "components.common.shared_address_books.access_type_group_add" | ||||
|                   ) | ||||
|                 }} | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 v-if="page.access.withSearch" | ||||
|                 :title=" | ||||
|                   t('components.common.shared_address_books.delete_found') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_address_books.ok')" | ||||
|                 :cancel-text=" | ||||
|                   t('components.common.shared_address_books.cancel') | ||||
|                 " | ||||
|                 @confirm="deleteAccess('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_address_books.delete_found") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|               <a-popconfirm | ||||
|                 v-else | ||||
|                 :title=" | ||||
|                   t('components.common.shared_address_books.delete_all') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_address_books.ok')" | ||||
|                 :cancel-text=" | ||||
|                   t('components.common.shared_address_books.cancel') | ||||
|                 " | ||||
|                 @confirm="deleteAccess('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_address_books.delete_all") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-popconfirm | ||||
|             :title="t('components.common.shared_address_books.delete') + '?'" | ||||
|             :ok-text="t('components.common.shared_address_books.ok')" | ||||
|             :cancel-text="t('components.common.shared_address_books.cancel')" | ||||
|             @confirm="deleteAccess(record.Name)" | ||||
|           > | ||||
|             <a-button danger>{{ | ||||
|               t("components.common.shared_address_books.delete") | ||||
|             }}</a-button> | ||||
|           </a-popconfirm> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'perm'"> | ||||
|           <a-select | ||||
|             v-model:value="record.Perm" | ||||
|             style="min-width: 150px" | ||||
|             @change="updateAccess(record.Name, record.Perm)" | ||||
|           > | ||||
|             <a-select-option value="read"> | ||||
|               {{ | ||||
|                 t("components.common.shared_address_books.access_perm_read") | ||||
|               }}</a-select-option | ||||
|             > | ||||
|             <a-select-option value="all"> | ||||
|               {{ | ||||
|                 t("components.common.shared_address_books.access_perm_all") | ||||
|               }}</a-select-option | ||||
|             > | ||||
|           </a-select> | ||||
|         </template> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.access.pagination.current" | ||||
|             :defaultPageSize="page.access.pagination.size" | ||||
|             :total="page.access.pagination.total" | ||||
|             @change="accessPageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.access.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|     <template #footer> </template> | ||||
|   </a-modal> | ||||
|   <a-modal | ||||
|     style="width: 300px" | ||||
|     v-model:open="page.access.showAdd" | ||||
|     :title=" | ||||
|       page.access.type === 'email' | ||||
|         ? t('components.common.shared_address_books.access_type_email_add') | ||||
|         : t('components.common.shared_address_books.access_type_group_add') | ||||
|     " | ||||
|   > | ||||
|     <br /> | ||||
|     <a-form> | ||||
|       <a-form-item> | ||||
|         <template v-if="page.access.type === 'email'"> | ||||
|           <a-input | ||||
|             v-model:value="page.access.inputValue" | ||||
|             :placeholder=" | ||||
|               t( | ||||
|                 'components.common.shared_address_books.access_type_email_placeholder' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|           </a-input> | ||||
|         </template> | ||||
|         <template v-if="page.access.type === 'group'"> | ||||
|           <a-select | ||||
|             style="width: 100%" | ||||
|             :disabled="page.access.groups.length === 0" | ||||
|             v-model:value="page.access.selectValue" | ||||
|             :options="page.access.groups" | ||||
|           > | ||||
|           </a-select> | ||||
|         </template> | ||||
|       </a-form-item> | ||||
|       <a-form-item> | ||||
|         <a-select v-model:value="page.access.perm"> | ||||
|           <a-select-option value="read"> | ||||
|             {{ | ||||
|               t("components.common.shared_address_books.access_perm_read") | ||||
|             }}</a-select-option | ||||
|           > | ||||
|           <a-select-option value="all"> | ||||
|             {{ | ||||
|               t("components.common.shared_address_books.access_perm_all") | ||||
|             }}</a-select-option | ||||
|           > | ||||
|         </a-select> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         :disabled=" | ||||
|           (page.access.type === 'email' && page.access.inputValue === '') || | ||||
|           (page.access.type === 'group' && page.access.selectValue === '') | ||||
|         " | ||||
|         type="primary" | ||||
|         @click=" | ||||
|           addAccess( | ||||
|             page.access.type === 'email' | ||||
|               ? page.access.inputValue | ||||
|               : page.access.selectValue | ||||
|           ) | ||||
|         " | ||||
|       > | ||||
|         {{ t("components.common.shared_address_books.access_add") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.common.shared_address_books.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const accessColumns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.common.shared_address_books.access_col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.common.shared_address_books.access_col_permissions"), | ||||
|     dataIndex: "Perm", | ||||
|     key: "perm", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   domain?: string; | ||||
|   user: string; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   all_groups: string[]; | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   add: { | ||||
|     show: boolean; | ||||
|     Name: string; | ||||
|   }; | ||||
|   props: { | ||||
|     show: boolean; | ||||
|     Name: string; | ||||
|     Id: number; | ||||
|   }; | ||||
|   access: { | ||||
|     inputValue: string; | ||||
|     selectValue: string; | ||||
|     perm: string; | ||||
|     type: string; | ||||
|     data: any[]; | ||||
|     loading: boolean; | ||||
|     show: boolean; | ||||
|     Id: number; | ||||
|     groups: any[]; | ||||
|     pagination: { | ||||
|       current: number; | ||||
|       total: number; | ||||
|       size: number; | ||||
|       search: string; | ||||
|     }; | ||||
|     showAdd: boolean; | ||||
|     withSearch: boolean; | ||||
|   }; | ||||
| }>({ | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   add: { | ||||
|     show: false, | ||||
|     Name: "", | ||||
|   }, | ||||
|   all_groups: [], | ||||
|   access: { | ||||
|     withSearch: false, | ||||
|     inputValue: "", | ||||
|     selectValue: "", | ||||
|     perm: "read", | ||||
|     type: "users", | ||||
|     data: [], | ||||
|     loading: false, | ||||
|     show: false, | ||||
|     Id: 0, | ||||
|     groups: [], | ||||
|     pagination: { | ||||
|       current: 1, | ||||
|       total: 0, | ||||
|       size: 50, | ||||
|       search: "", | ||||
|     }, | ||||
|     showAdd: false, | ||||
|   }, | ||||
|   props: { | ||||
|     show: false, | ||||
|     Name: "", | ||||
|     Id: 0, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
|   getGroups(); | ||||
| }); | ||||
| 
 | ||||
| async function getGroups() { | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_address_books/groups`; | ||||
|   } else { | ||||
|     url = `/user/address_books/groups`; | ||||
|   } | ||||
|   const res = await apiFetch(url); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.all_groups = res.data; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_address_books?`; | ||||
|   } else { | ||||
|     url = `/user/address_books?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString()); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| function accessPageChange(current: number) { | ||||
|   page.access.pagination.current = current; | ||||
|   return getAccess(); | ||||
| } | ||||
| 
 | ||||
| async function addBook() { | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_address_books`; | ||||
|   } else { | ||||
|     url = `/user/address_books`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url, { | ||||
|     method: "POST", | ||||
|     body: { | ||||
|       Name: page.add.Name, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function updateBook() { | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_address_books`; | ||||
|   } else { | ||||
|     url = `/user/address_books`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url, { | ||||
|     method: "PATCH", | ||||
|     body: { | ||||
|       Name: page.props.Name, | ||||
|       Id: page.props.Id, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.props.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| async function showAccessModal(id: number) { | ||||
|   page.access.Id = id; | ||||
|   page.access.show = true; | ||||
|   page.access.type = "email"; | ||||
|   page.access.inputValue = ""; | ||||
|   getAccess(); | ||||
| } | ||||
| 
 | ||||
| async function showPropModal(id: number, name: string) { | ||||
|   page.props.Name = name; | ||||
|   page.props.show = true; | ||||
|   page.props.Id = id; | ||||
| } | ||||
| 
 | ||||
| async function showAddModal() { | ||||
|   page.add.show = true; | ||||
|   page.add.Name = ""; | ||||
| } | ||||
| 
 | ||||
| async function deleteBook(id: number | undefined) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_address_books?`; | ||||
|   } else { | ||||
|     url = `/user/address_books?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "DELETE", | ||||
|     body: { | ||||
|       Id: id, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function getAccess() { | ||||
|   page.access.withSearch = page.access.pagination.search !== ""; | ||||
|   page.access.loading = true; | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("id", page.access.Id.toString()); | ||||
|   params.append("type", page.access.type); | ||||
|   params.append("page", String(page.access.pagination.current)); | ||||
|   params.append("size", String(page.access.pagination.size)); | ||||
|   params.append("search", page.access.pagination.search); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_address_books/access?`; | ||||
|   } else { | ||||
|     url = `/user/address_books/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString()); | ||||
| 
 | ||||
|   page.access.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.access.data = []; | ||||
| 
 | ||||
|   if (res.data) { | ||||
|     for (let index = 0; index < res.data.length; index++) { | ||||
|       const element = res.data[index]; | ||||
| 
 | ||||
|       page.access.data.push({ | ||||
|         Name: element.AccessTo, | ||||
|         Perm: element.Permissions, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   page.access.pagination.total = res.total; | ||||
| 
 | ||||
|   page.access.groups = []; | ||||
|   page.access.selectValue = ""; | ||||
| 
 | ||||
|   if (page.access.type === "group") { | ||||
|     for (let j = 0; j < page.all_groups.length; j++) { | ||||
|       const group = page.all_groups[j]; | ||||
|       let skip = false; | ||||
|       for (let i = 0; i < page.access.data.length; i++) { | ||||
|         const alreadyHas = page.access.data[i]; | ||||
|         if (alreadyHas.Name === group) { | ||||
|           skip = true; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (!skip) { | ||||
|         page.access.groups.push({ value: group }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (page.access.groups.length) { | ||||
|     page.access.selectValue = page.access.groups[0].value; | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function deleteAccess(access: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("id", page.access.Id.toString()); | ||||
|   params.append("type", page.access.type); | ||||
|   params.append("page", String(page.access.pagination.current)); | ||||
|   params.append("size", String(page.access.pagination.size)); | ||||
|   params.append("search", page.access.pagination.search); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_address_books/access?`; | ||||
|   } else { | ||||
|     url = `/user/address_books/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "DELETE", | ||||
|     body: { | ||||
|       Access: access, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return getAccess(); | ||||
| } | ||||
| 
 | ||||
| async function updateAccess(access: string, perm: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("id", page.access.Id.toString()); | ||||
|   params.append("type", page.access.type); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_address_books/access?`; | ||||
|   } else { | ||||
|     url = `/user/address_books/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "PATCH", | ||||
|     body: { | ||||
|       Access: access, | ||||
|       Perm: perm, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return getAccess(); | ||||
| } | ||||
| 
 | ||||
| async function addAccess(access: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("id", page.access.Id.toString()); | ||||
|   params.append("type", page.access.type); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_address_books/access?`; | ||||
|   } else { | ||||
|     url = `/user/address_books/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "POST", | ||||
|     body: { | ||||
|       Access: access, | ||||
|       Perm: page.access.perm, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.access.showAdd = false; | ||||
| 
 | ||||
|   return getAccess(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 700px; | ||||
|   max-width: 70%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,475 @@ | |||
| <template> | ||||
|   <div > | ||||
|     <a-table class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-input-search | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder="t('components.common.shared_calendars.search')" | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col :span="6"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button type="primary" @click="showAddModal"> | ||||
|                 <PlusOutlined /> | ||||
|                 {{ t("components.common.shared_calendars.add_calendar") }} | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 v-if="page.withSearch" | ||||
|                 :title=" | ||||
|                   t('components.common.shared_calendars.delete_found') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_calendars.ok')" | ||||
|                 :cancel-text="t('components.common.shared_calendars.cancel')" | ||||
|                 @confirm="deleteCalendar(undefined)" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_calendars.delete_found") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|               <a-popconfirm | ||||
|                 v-else | ||||
|                 :title=" | ||||
|                   t('components.common.shared_calendars.delete_all') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_calendars.ok')" | ||||
|                 :cancel-text="t('components.common.shared_calendars.cancel')" | ||||
|                 @confirm="deleteCalendar(undefined)" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_calendars.delete_all") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button type="primary" @click="showAccessModal(record.Id)">{{ | ||||
|               t("components.common.shared_calendars.edit_access") | ||||
|             }}</a-button> | ||||
|               <a-button  @click="showPropModal(record)">{{ | ||||
|               t("components.common.shared_calendars.properties") | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title="t('components.common.shared_calendars.delete') + '?'" | ||||
|               :ok-text="t('components.common.shared_calendars.ok')" | ||||
|               :cancel-text="t('components.common.shared_calendars.cancel')" | ||||
|               @confirm="deleteCalendar(record.Id)" | ||||
|             > | ||||
|               <a-button danger>{{ | ||||
|                 t("components.common.shared_calendars.delete") | ||||
|               }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     style="width: 600px" | ||||
|     v-model:open="page.add.show" | ||||
|     :title="t('components.common.shared_calendars.add_calendar')" | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap> | ||||
|       <a-form-item :label="$t('components.common.shared_calendars.add_name')" | ||||
|        :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.add.cal.Name"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.common.shared_calendars.add_freebusy')" | ||||
|       > | ||||
|         <a-switch v-model:checked="page.add.cal.AllowFreebusy"> </a-switch> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.common.shared_calendars.add_showfrom')" | ||||
|       > | ||||
|         <a-input-number :min="0" v-model:value="page.add.cal.ShowFromNWeeks"> | ||||
|         </a-input-number> | ||||
|       </a-form-item> | ||||
|       <a-form-item :label="$t('components.common.shared_calendars.add_showto')"> | ||||
|         <a-input-number :min="0" v-model:value="page.add.cal.ShowToNWeeks"> | ||||
|         </a-input-number> | ||||
|       </a-form-item> | ||||
|       <a-form-item :label="$t('components.common.shared_calendars.add_color')"> | ||||
|         <input type="color" :value="page.add.cal.Color" @input="(e: any) => (page.add.cal.Color = e.target.value)"> | ||||
|         </input> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
| 
 | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         type="primary" | ||||
|         :disabled="!page.add.cal.Name" | ||||
|         @click="addCalendar" | ||||
|       > | ||||
|         {{ t("components.common.shared_calendars.add_calendar") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
|   <a-modal | ||||
|     style="width: 600px" | ||||
|     v-model:open="page.props.show" | ||||
|     :title="t('components.common.shared_calendars.properties')" | ||||
|   > | ||||
|   <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap> | ||||
|       <a-form-item :label="$t('components.common.shared_calendars.add_name')" | ||||
|        :rules="[{ required: true }]" | ||||
|       > | ||||
|         <a-input v-model:value="page.props.cal.Name"> </a-input> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.common.shared_calendars.add_freebusy')" | ||||
|       > | ||||
|         <a-switch v-model:checked="page.props.cal.AllowFreebusy"> </a-switch> | ||||
|       </a-form-item> | ||||
|       <a-form-item | ||||
|         :label="$t('components.common.shared_calendars.add_showfrom')" | ||||
|       > | ||||
|         <a-input-number :min="0" v-model:value="page.props.cal.ShowFromNWeeks"> | ||||
|         </a-input-number> | ||||
|       </a-form-item> | ||||
|       <a-form-item :label="$t('components.common.shared_calendars.add_showto')"> | ||||
|         <a-input-number :min="0" v-model:value="page.props.cal.ShowToNWeeks"> | ||||
|         </a-input-number> | ||||
|       </a-form-item> | ||||
|       <a-form-item :label="$t('components.common.shared_calendars.add_color')"> | ||||
|         <input type="color" :value="page.props.cal.Color" @input="(e: any) => (page.props.cal.Color = e.target.value)"> | ||||
|         </input> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         type="primary" | ||||
|         :disabled="!page.props.cal.Name" | ||||
|         @click="updateCalendar" | ||||
|       > | ||||
|         {{ t("components.common.shared_calendars.update_calendar") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
|   <CalendarsAccess v-if="page.access.show" | ||||
|   v-model:open="page.access.show" | ||||
|   :type="page.access.type" | ||||
|   :id="page.access.Id" | ||||
|   :domain="props.domain" | ||||
|   :resources="false" | ||||
|   > | ||||
| 
 | ||||
|   </CalendarsAccess> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import CalendarsAccess from "./CalendarsAccess.vue"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 16, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 8 }; | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.common.shared_calendars.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   domain?: string; | ||||
|   user: string; | ||||
| }>(); | ||||
| 
 | ||||
| interface Calendar { | ||||
|   Id: number; | ||||
|   Name: string; | ||||
|   ShowFromNWeeks: string; | ||||
|   ShowToNWeeks: string; | ||||
|   AllowFreebusy: boolean; | ||||
|   Color: string; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   all_groups: string[]; | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   add: { | ||||
|     show: boolean; | ||||
|     cal: Calendar; | ||||
|   }; | ||||
|   props: { | ||||
|     show: boolean; | ||||
|     cal: Calendar; | ||||
|   }; | ||||
|   access: { | ||||
|     perm: string; | ||||
|     type: string; | ||||
|     show: boolean; | ||||
|     Id: number; | ||||
|   }; | ||||
| }>({ | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   add: { | ||||
|     show: false, | ||||
|     cal: { | ||||
|       Id: 0, | ||||
|       Name: "", | ||||
|       ShowFromNWeeks: "", | ||||
|       ShowToNWeeks: "", | ||||
|       AllowFreebusy: false, | ||||
|       Color: "", | ||||
|     }, | ||||
|   }, | ||||
|   all_groups: [], | ||||
|   access: { | ||||
|     perm: "read", | ||||
|     type: "users", | ||||
|     show: false, | ||||
|     Id: 0, | ||||
|   }, | ||||
|   props: { | ||||
|     show: false, | ||||
|     cal: { | ||||
|       Id: 0, | ||||
|       Name: "", | ||||
|       ShowFromNWeeks: "", | ||||
|       ShowToNWeeks: "", | ||||
|       AllowFreebusy: false, | ||||
|       Color: "", | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_calendars?`; | ||||
|   } else { | ||||
|     url = `/user/calendars?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString()); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| async function addCalendar() { | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_calendars`; | ||||
|   } else { | ||||
|     url = `/user/calendars`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url, { | ||||
|     method: "POST", | ||||
|     body: page.add.cal, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function updateCalendar() { | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_calendars`; | ||||
|   } else { | ||||
|     url = `/user/calendars`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url, { | ||||
|     method: "PATCH", | ||||
|     body: page.props.cal, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.props.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| async function showAccessModal(id: number) { | ||||
|   page.access.Id = id; | ||||
|   page.access.show = true; | ||||
|   page.access.type = "email"; | ||||
| } | ||||
| 
 | ||||
| async function showPropModal(record: any) { | ||||
|   page.props.cal = {...record}; | ||||
|   page.props.show = true; | ||||
| } | ||||
| 
 | ||||
| async function showAddModal() { | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_calendars/default`; | ||||
|   } else { | ||||
|     url = `/user/calendars/default`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = true; | ||||
|   page.add.cal.Name = ""; | ||||
|   page.add.cal.ShowFromNWeeks = res.data.ShowFromNWeeks; | ||||
|   page.add.cal.ShowToNWeeks = res.data.ShowToNWeeks; | ||||
|   page.add.cal.AllowFreebusy = res.data.AllowFreebusy; | ||||
|   page.add.cal.Color = res.data.Color; | ||||
| } | ||||
| 
 | ||||
| async function deleteCalendar(id: number | undefined) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_calendars?`; | ||||
|   } else { | ||||
|     url = `/user/calendars?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "DELETE", | ||||
|     body: { | ||||
|       Id: id, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 700px; | ||||
|   max-width: 70%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,504 @@ | |||
| <template> | ||||
|   <a-modal style="width: 50%; min-width: 720px" v-model:open="openModel"> | ||||
|     <a-table | ||||
|       :columns="accessColumns" | ||||
|       :data-source="page.access.data" | ||||
|       :loading="page.access.loading" | ||||
|       bordered | ||||
|       size="small" | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-radio-group | ||||
|                 v-if="!props.resources" | ||||
|                 @change="getAccess()" | ||||
|                 v-model:value="page.access.type" | ||||
|               > | ||||
|                 <a-radio-button value="email">{{ | ||||
|                   t("components.common.shared_calendars.access_type_email") | ||||
|                 }}</a-radio-button> | ||||
|                 <a-radio-button value="group">{{ | ||||
|                   t("components.common.shared_calendars.access_type_group") | ||||
|                 }}</a-radio-button> | ||||
|               </a-radio-group> | ||||
| 
 | ||||
|               <a-input-search | ||||
|                 v-model:value="page.access.pagination.search" | ||||
|                 :placeholder=" | ||||
|                   t('components.common.shared_calendars.access_search') | ||||
|                 " | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="getAccess" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col | ||||
|             style="display: flex; align-items: flex-end; justify-content: end" | ||||
|             :span="6" | ||||
|           > | ||||
|             <a-space> | ||||
|               <a-button | ||||
|                 v-if="page.access.type === 'email'" | ||||
|                 type="primary" | ||||
|                 @click="page.access.showAdd = true" | ||||
|               > | ||||
|                 {{ | ||||
|                   t("components.common.shared_calendars.access_type_email_add") | ||||
|                 }} | ||||
|               </a-button> | ||||
|               <a-button | ||||
|                 v-if="page.access.type === 'group'" | ||||
|                 type="primary" | ||||
|                 @click="page.access.showAdd = true" | ||||
|               > | ||||
|                 {{ | ||||
|                   t("components.common.shared_calendars.access_type_group_add") | ||||
|                 }} | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 v-if="page.access.withSearch" | ||||
|                 :title=" | ||||
|                   t('components.common.shared_calendars.delete_found') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_calendars.ok')" | ||||
|                 :cancel-text="t('components.common.shared_calendars.cancel')" | ||||
|                 @confirm="deleteAccess('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_calendars.delete_found") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|               <a-popconfirm | ||||
|                 v-else | ||||
|                 :title=" | ||||
|                   t('components.common.shared_calendars.delete_all') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_calendars.ok')" | ||||
|                 :cancel-text="t('components.common.shared_calendars.cancel')" | ||||
|                 @confirm="deleteAccess('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_calendars.delete_all") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-popconfirm | ||||
|             :title="t('components.common.shared_calendars.delete') + '?'" | ||||
|             :ok-text="t('components.common.shared_calendars.ok')" | ||||
|             :cancel-text="t('components.common.shared_calendars.cancel')" | ||||
|             @confirm="deleteAccess(record.Name)" | ||||
|           > | ||||
|             <a-button danger>{{ | ||||
|               t("components.common.shared_calendars.delete") | ||||
|             }}</a-button> | ||||
|           </a-popconfirm> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'perm'"> | ||||
|           <a-select | ||||
|             v-if="!props.resources" | ||||
|             v-model:value="record.Perm" | ||||
|             style="min-width: 150px" | ||||
|             @change="updateAccess(record.Name, record.Perm)" | ||||
|           > | ||||
|             <a-select-option value="read"> | ||||
|               {{ | ||||
|                 t("components.common.shared_calendars.access_perm_read") | ||||
|               }}</a-select-option | ||||
|             > | ||||
|             <a-select-option value="all"> | ||||
|               {{ | ||||
|                 t("components.common.shared_calendars.access_perm_all") | ||||
|               }}</a-select-option | ||||
|             > | ||||
|           </a-select> | ||||
|           <div v-else> | ||||
|             {{ | ||||
|               record.Perm === "read" | ||||
|                 ? t("components.common.shared_calendars.access_perm_read") | ||||
|                 : t("components.common.shared_calendars.access_perm_all") | ||||
|             }} | ||||
|           </div> | ||||
|         </template> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.access.pagination.current" | ||||
|             :defaultPageSize="page.access.pagination.size" | ||||
|             :total="page.access.pagination.total" | ||||
|             @change="accessPageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.access.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|     <template #footer> </template> | ||||
|   </a-modal> | ||||
|   <a-modal | ||||
|     style="width: 300px" | ||||
|     v-model:open="page.access.showAdd" | ||||
|     :title=" | ||||
|       page.access.type === 'email' | ||||
|         ? t('components.common.shared_calendars.access_type_email_add') | ||||
|         : t('components.common.shared_calendars.access_type_group_add') | ||||
|     " | ||||
|   > | ||||
|     <br /> | ||||
|     <a-form> | ||||
|       <a-form-item> | ||||
|         <template v-if="page.access.type === 'email'"> | ||||
|           <a-input | ||||
|             v-model:value="page.access.inputValue" | ||||
|             :placeholder=" | ||||
|               t( | ||||
|                 'components.common.shared_calendars.access_type_email_placeholder' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|           </a-input> | ||||
|         </template> | ||||
|         <template v-if="page.access.type === 'group'"> | ||||
|           <a-select | ||||
|             style="width: 100%" | ||||
|             :disabled="page.access.groups.length === 0" | ||||
|             v-model:value="page.access.selectValue" | ||||
|             :options="page.access.groups" | ||||
|           > | ||||
|           </a-select> | ||||
|         </template> | ||||
|       </a-form-item> | ||||
|       <a-form-item v-if="!props.resources"> | ||||
|         <a-select v-model:value="page.access.perm"> | ||||
|           <a-select-option value="read"> | ||||
|             {{ | ||||
|               t("components.common.shared_calendars.access_perm_read") | ||||
|             }}</a-select-option | ||||
|           > | ||||
|           <a-select-option value="all"> | ||||
|             {{ | ||||
|               t("components.common.shared_calendars.access_perm_all") | ||||
|             }}</a-select-option | ||||
|           > | ||||
|         </a-select> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         :disabled=" | ||||
|           (page.access.type === 'email' && page.access.inputValue === '') || | ||||
|           (page.access.type === 'group' && page.access.selectValue === '') | ||||
|         " | ||||
|         type="primary" | ||||
|         @click=" | ||||
|           addAccess( | ||||
|             page.access.type === 'email' | ||||
|               ? page.access.inputValue | ||||
|               : page.access.selectValue | ||||
|           ) | ||||
|         " | ||||
|       > | ||||
|         {{ t("components.common.shared_calendars.access_add") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const openModel = defineModel<boolean>("open"); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   type: string; | ||||
|   id: number; | ||||
|   domain?: string; | ||||
|   resources: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| const accessColumns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.common.shared_calendars.access_col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.common.shared_calendars.access_col_permissions"), | ||||
|     dataIndex: "Perm", | ||||
|     key: "perm", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   all_groups: string[]; | ||||
|   access: { | ||||
|     inputValue: string; | ||||
|     selectValue: string; | ||||
|     perm: string; | ||||
|     type: string; | ||||
|     data: any[]; | ||||
|     loading: boolean; | ||||
|     show: boolean; | ||||
|     groups: any[]; | ||||
|     pagination: { | ||||
|       current: number; | ||||
|       total: number; | ||||
|       size: number; | ||||
|       search: string; | ||||
|     }; | ||||
|     showAdd: boolean; | ||||
|     withSearch: boolean; | ||||
|   }; | ||||
| }>({ | ||||
|   all_groups: [], | ||||
|   access: { | ||||
|     withSearch: false, | ||||
|     inputValue: "", | ||||
|     selectValue: "", | ||||
|     perm: "read", | ||||
|     type: "users", | ||||
|     data: [], | ||||
|     loading: false, | ||||
|     show: false, | ||||
|     groups: [], | ||||
|     pagination: { | ||||
|       current: 1, | ||||
|       total: 0, | ||||
|       size: 50, | ||||
|       search: "", | ||||
|     }, | ||||
|     showAdd: false, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.access.inputValue = ""; | ||||
|   page.access.type = props.type; | ||||
| 
 | ||||
|   init(); | ||||
| }); | ||||
| 
 | ||||
| async function init() { | ||||
|   await getGroups(); | ||||
|   getAccess(); | ||||
| } | ||||
| 
 | ||||
| async function getGroups() { | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/${ | ||||
|       props.resources ? `resources/resources` : `shared_calendars` | ||||
|     }/groups`; | ||||
|   } else { | ||||
|     url = `/user/calendars/groups`; | ||||
|   } | ||||
|   const res = await apiFetch(url); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.all_groups = res.data; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function getAccess() { | ||||
|   page.access.withSearch = page.access.pagination.search !== ""; | ||||
|   page.access.loading = true; | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("id", props.id.toString()); | ||||
|   params.append("type", page.access.type); | ||||
|   params.append("page", String(page.access.pagination.current)); | ||||
|   params.append("size", String(page.access.pagination.size)); | ||||
|   params.append("search", page.access.pagination.search); | ||||
| 
 | ||||
|   let url = ""; | ||||
| 
 | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/${ | ||||
|       props.resources ? `resources/resources` : `shared_calendars` | ||||
|     }/access?`; | ||||
|   } else { | ||||
|     url = `/user/calendars/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString()); | ||||
| 
 | ||||
|   page.access.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.access.data = []; | ||||
| 
 | ||||
|   if (res.data) { | ||||
|     for (let index = 0; index < res.data.length; index++) { | ||||
|       const element = res.data[index]; | ||||
| 
 | ||||
|       page.access.data.push({ | ||||
|         Name: element.AccessTo, | ||||
|         Perm: element.Permissions, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   page.access.pagination.total = res.total; | ||||
| 
 | ||||
|   page.access.groups = []; | ||||
|   page.access.selectValue = ""; | ||||
| 
 | ||||
|   if (page.access.type === "group") { | ||||
|     for (let j = 0; j < page.all_groups.length; j++) { | ||||
|       const group = page.all_groups[j]; | ||||
|       let skip = false; | ||||
|       for (let i = 0; i < page.access.data.length; i++) { | ||||
|         const alreadyHas = page.access.data[i]; | ||||
|         if (alreadyHas.Name === group) { | ||||
|           skip = true; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (!skip) { | ||||
|         page.access.groups.push({ value: group }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (page.access.groups.length) { | ||||
|     page.access.selectValue = page.access.groups[0].value; | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function deleteAccess(access: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("id", props.id.toString()); | ||||
|   params.append("type", page.access.type); | ||||
|   params.append("page", String(page.access.pagination.current)); | ||||
|   params.append("size", String(page.access.pagination.size)); | ||||
|   params.append("search", page.access.pagination.search); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/${ | ||||
|       props.resources ? `resources/resources` : `shared_calendars` | ||||
|     }/access?`; | ||||
|   } else { | ||||
|     url = `/user/calendars/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "DELETE", | ||||
|     body: { | ||||
|       Access: access, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.common.shared_calendars.remove_success")); | ||||
| 
 | ||||
|   return getAccess(); | ||||
| } | ||||
| 
 | ||||
| async function updateAccess(access: string, perm: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("id", props.id.toString()); | ||||
|   params.append("type", page.access.type); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/${ | ||||
|       props.resources ? `resources/resources` : `shared_calendars` | ||||
|     }/access?`; | ||||
|   } else { | ||||
|     url = `/user/calendars/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "PATCH", | ||||
|     body: { | ||||
|       Access: access, | ||||
|       Perm: perm, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.common.shared_calendars.update_success")); | ||||
| 
 | ||||
|   return getAccess(); | ||||
| } | ||||
| 
 | ||||
| async function addAccess(access: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("id", props.id.toString()); | ||||
|   params.append("type", page.access.type); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/${ | ||||
|       props.resources ? `resources/resources` : `shared_calendars` | ||||
|     }/access?`; | ||||
|   } else { | ||||
|     url = `/user/calendars/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "POST", | ||||
|     body: { | ||||
|       Access: access, | ||||
|       Perm: page.access.perm, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.common.shared_calendars.add_success")); | ||||
| 
 | ||||
|   page.access.showAdd = false; | ||||
| 
 | ||||
|   return getAccess(); | ||||
| } | ||||
| 
 | ||||
| function accessPageChange(current: number) { | ||||
|   page.access.pagination.current = current; | ||||
|   return getAccess(); | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,755 @@ | |||
| <template> | ||||
|   <div class="panel-content"> | ||||
|     <a-table | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-button | ||||
|                 @click=" | ||||
|                   page.currBaseDir = rootBaseDir; | ||||
|                   page.lastBaseDir = []; | ||||
|                   get(); | ||||
|                 " | ||||
|               > | ||||
|                 <HomeOutlined /> | ||||
|               </a-button> | ||||
|               <a-button | ||||
|                 :disabled="page.lastBaseDir.length === 0" | ||||
|                 @click=" | ||||
|                   page.currBaseDir = page.lastBaseDir.pop() as BaseDir; | ||||
|                   get(); | ||||
|                 " | ||||
|               > | ||||
|                 <ArrowLeftOutlined /> | ||||
|               </a-button> | ||||
| 
 | ||||
|               <a-input-search | ||||
|                 style="width: 250px" | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder=" | ||||
|                   t('components.common.shared_folders.search') + | ||||
|                   page.currBaseDir.CName | ||||
|                 " | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col :span="6"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button type="primary" @click="showAddModal"> | ||||
|                 <PlusOutlined /> | ||||
|                 {{ t("components.common.shared_folders.add_folder") }} | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 v-if="page.withSearch" | ||||
|                 :title=" | ||||
|                   t('components.common.shared_folders.delete_found') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_folders.ok')" | ||||
|                 :cancel-text="t('components.common.shared_folders.cancel')" | ||||
|                 @confirm="deleteFolder('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_folders.delete_found") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|               <a-popconfirm | ||||
|                 v-else | ||||
|                 :title="t('components.common.shared_folders.delete_all') + '?'" | ||||
|                 :ok-text="t('components.common.shared_folders.ok')" | ||||
|                 :cancel-text="t('components.common.shared_folders.cancel')" | ||||
|                 @confirm="deleteFolder('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_folders.delete_all") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button | ||||
|               :disabled=" | ||||
|                 props.user !== 'admin' && props.user !== record.Creator | ||||
|               " | ||||
|               type="primary" | ||||
|               @click="showAccessModal(record.Name)" | ||||
|               >{{ t("components.common.shared_folders.edit_access") }}</a-button | ||||
|             > | ||||
|             <a-popconfirm | ||||
|               :title="t('components.common.shared_folders.delete') + '?'" | ||||
|               :ok-text="t('components.common.shared_folders.ok')" | ||||
|               :cancel-text="t('components.common.shared_folders.cancel')" | ||||
|               @confirm="deleteFolder(record.Name)" | ||||
|             > | ||||
|               <a-button | ||||
|                 :disabled=" | ||||
|                   props.user !== 'admin' && props.user !== record.Creator | ||||
|                 " | ||||
|                 danger | ||||
|                 >{{ t("components.common.shared_folders.delete") }}</a-button | ||||
|               > | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'cname'"> | ||||
|           <a | ||||
|             @click=" | ||||
|               page.lastBaseDir = [...page.lastBaseDir, page.currBaseDir]; | ||||
|               page.currBaseDir = { | ||||
|                 Name: record.Name, | ||||
|                 CName: record.CName, | ||||
|               }; | ||||
|               get(); | ||||
|             " | ||||
|           > | ||||
|             {{ record.CName }} | ||||
|           </a> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'creator'"> | ||||
|           <a-space> | ||||
|             <template v-if="record.CreatorStatus === 'archived'"> | ||||
|               <a-tooltip | ||||
|                 :title=" | ||||
|                   t(`components.admin.domains.mailstorage.mailboxes.in_archive`) | ||||
|                 " | ||||
|               > | ||||
|                 <LockOutlined /> | ||||
|               </a-tooltip> | ||||
|             </template> | ||||
|             <template v-if="record.CreatorStatus === 'deleted'"> | ||||
|               <a-tooltip | ||||
|                 :title=" | ||||
|                   t(`components.admin.domains.mailstorage.mailboxes.deleted`) | ||||
|                 " | ||||
|               > | ||||
|                 <DeleteOutlined /> | ||||
|               </a-tooltip> | ||||
|             </template> | ||||
|             {{ record.Creator }} | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     style="width: 300px" | ||||
|     v-model:open="page.add.show" | ||||
|     :title="t('components.common.shared_folders.add_folder')" | ||||
|   > | ||||
|     <a-input | ||||
|       style="margin-top: 8px" | ||||
|       :placeholder="$t('components.common.shared_folders.add_name')" | ||||
|       v-model:value="page.add.Name" | ||||
|     > | ||||
|     </a-input> | ||||
|     <template #footer> | ||||
|       <a-button type="primary" :disabled="!page.add.Name" @click="addFolder"> | ||||
|         {{ t("components.common.shared_folders.add_folder") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
|   <a-modal | ||||
|     v-model:open="page.access.show" | ||||
|     style="width: 50%; min-width: 700px" | ||||
|     @cancel="get" | ||||
|   > | ||||
|     <a-table | ||||
|       :columns="accessColumns" | ||||
|       :data-source="page.access.data" | ||||
|       :loading="page.access.loading" | ||||
|       bordered | ||||
|       size="small" | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-radio-group | ||||
|                 @change="getAccess()" | ||||
|                 v-model:value="page.access.type" | ||||
|               > | ||||
|                 <a-radio-button value="email">{{ | ||||
|                   t("components.common.shared_folders.access_type_email") | ||||
|                 }}</a-radio-button> | ||||
|                 <a-radio-button value="group">{{ | ||||
|                   t("components.common.shared_folders.access_type_group") | ||||
|                 }}</a-radio-button> | ||||
|               </a-radio-group> | ||||
| 
 | ||||
|               <a-input-search | ||||
|                 v-model:value="page.access.pagination.search" | ||||
|                 :placeholder=" | ||||
|                   t('components.common.shared_folders.access_search') | ||||
|                 " | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="getAccess" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col | ||||
|             style="display: flex; align-items: flex-end; justify-content: end" | ||||
|             :span="6" | ||||
|           > | ||||
|             <a-space> | ||||
|               <a-button | ||||
|                 v-if="page.access.type === 'email'" | ||||
|                 type="primary" | ||||
|                 @click="page.access.showAdd = true" | ||||
|               > | ||||
|                 {{ | ||||
|                   t("components.common.shared_folders.access_type_email_add") | ||||
|                 }} | ||||
|               </a-button> | ||||
|               <a-button | ||||
|                 v-if="page.access.type === 'group'" | ||||
|                 type="primary" | ||||
|                 @click="page.access.showAdd = true" | ||||
|               > | ||||
|                 {{ | ||||
|                   t("components.common.shared_folders.access_type_group_add") | ||||
|                 }} | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 v-if="page.access.withSearch" | ||||
|                 :title=" | ||||
|                   t('components.common.shared_folders.delete_found') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.common.shared_folders.ok')" | ||||
|                 :cancel-text="t('components.common.shared_folders.cancel')" | ||||
|                 @confirm="deleteAccess('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_folders.delete_found") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|               <a-popconfirm | ||||
|                 v-else | ||||
|                 :title="t('components.common.shared_folders.delete_all') + '?'" | ||||
|                 :ok-text="t('components.common.shared_folders.ok')" | ||||
|                 :cancel-text="t('components.common.shared_folders.cancel')" | ||||
|                 @confirm="deleteAccess('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.common.shared_folders.delete_all") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-popconfirm | ||||
|             :title="t('components.common.shared_folders.delete') + '?'" | ||||
|             :ok-text="t('components.common.shared_folders.ok')" | ||||
|             :cancel-text="t('components.common.shared_folders.cancel')" | ||||
|             @confirm="deleteAccess(record.Name)" | ||||
|           > | ||||
|             <a-button danger>{{ | ||||
|               t("components.common.shared_folders.delete") | ||||
|             }}</a-button> | ||||
|           </a-popconfirm> | ||||
|         </template> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.access.pagination.current" | ||||
|             :defaultPageSize="page.access.pagination.size" | ||||
|             :total="page.access.pagination.total" | ||||
|             @change="accessPageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.access.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|     <template #footer> </template> | ||||
|   </a-modal> | ||||
|   <a-modal | ||||
|     style="width: 300px" | ||||
|     v-model:open="page.access.showAdd" | ||||
|     :title=" | ||||
|       page.access.type === 'email' | ||||
|         ? t('components.common.shared_folders.access_type_email_add') | ||||
|         : t('components.common.shared_folders.access_type_group_add') | ||||
|     " | ||||
|   > | ||||
|     <template v-if="page.access.type === 'email'"> | ||||
|       <a-input | ||||
|         v-model:value="page.access.inputValue" | ||||
|         :placeholder=" | ||||
|           t('components.common.shared_folders.access_type_email_placeholder') | ||||
|         " | ||||
|       > | ||||
|       </a-input> | ||||
|     </template> | ||||
|     <template v-if="page.access.type === 'group'"> | ||||
|       <a-select | ||||
|         style="width: 100%" | ||||
|         :disabled="page.access.groups.length === 0" | ||||
|         v-model:value="page.access.selectValue" | ||||
|         :options="page.access.groups" | ||||
|       > | ||||
|       </a-select> | ||||
|     </template> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         :disabled=" | ||||
|           (page.access.type === 'email' && page.access.inputValue === '') || | ||||
|           (page.access.type === 'group' && page.access.selectValue === '') | ||||
|         " | ||||
|         type="primary" | ||||
|         @click=" | ||||
|           addAccess( | ||||
|             page.access.type === 'email' | ||||
|               ? page.access.inputValue | ||||
|               : page.access.selectValue | ||||
|           ) | ||||
|         " | ||||
|       > | ||||
|         {{ t("components.common.shared_folders.access_add") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HddOutlined, | ||||
|   HomeOutlined, | ||||
|   LockOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.common.shared_folders.col_folder"), | ||||
|     dataIndex: "CName", | ||||
|     key: "cname", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.common.shared_folders.col_owner"), | ||||
|     dataIndex: "Creator", | ||||
|     key: "creator", | ||||
|   }, | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const accessColumns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.common.shared_folders.access_col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
| 
 | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   domain?: string; | ||||
|   user: string; | ||||
| }>(); | ||||
| 
 | ||||
| interface BaseDir { | ||||
|   Name: string; | ||||
|   CName: string; | ||||
| } | ||||
| 
 | ||||
| const rootBaseDir: BaseDir = { | ||||
|   Name: "", | ||||
|   CName: t("components.common.shared_folders.root_folder"), | ||||
| }; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   all_groups: string[]; | ||||
|   currBaseDir: BaseDir; | ||||
|   lastBaseDir: BaseDir[]; | ||||
|   withSearch: boolean; | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   add: { | ||||
|     show: boolean; | ||||
|     Name: string; | ||||
|   }; | ||||
|   access: { | ||||
|     inputValue: string; | ||||
|     selectValue: string; | ||||
|     type: string; | ||||
|     data: any[]; | ||||
|     loading: boolean; | ||||
|     show: boolean; | ||||
|     Name: string; | ||||
|     groups: any[]; | ||||
|     pagination: { | ||||
|       current: number; | ||||
|       total: number; | ||||
|       size: number; | ||||
|       search: string; | ||||
|     }; | ||||
|     showAdd: boolean; | ||||
|     withSearch: boolean; | ||||
|   }; | ||||
| }>({ | ||||
|   currBaseDir: rootBaseDir, | ||||
|   lastBaseDir: [], | ||||
|   withSearch: false, | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   add: { | ||||
|     show: false, | ||||
|     Name: "", | ||||
|   }, | ||||
|   all_groups: [], | ||||
|   access: { | ||||
|     withSearch: false, | ||||
|     inputValue: "", | ||||
|     selectValue: "", | ||||
|     type: "email", | ||||
|     data: [], | ||||
|     loading: false, | ||||
|     show: false, | ||||
|     Name: "", | ||||
|     groups: [], | ||||
|     pagination: { | ||||
|       current: 1, | ||||
|       total: 0, | ||||
|       size: 50, | ||||
|       search: "", | ||||
|     }, | ||||
|     showAdd: false, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
|   getGroups(); | ||||
| }); | ||||
| 
 | ||||
| async function getGroups() { | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_folders/groups`; | ||||
|   } else { | ||||
|     url = `/user/shared_folders/groups`; | ||||
|   } | ||||
|   const res = await apiFetch(url); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.all_groups = res.data; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("base_dir", page.currBaseDir.Name); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_folders?`; | ||||
|   } else { | ||||
|     url = `/user/shared_folders?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString()); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| function accessPageChange(current: number) { | ||||
|   page.access.pagination.current = current; | ||||
|   return getAccess(); | ||||
| } | ||||
| 
 | ||||
| async function addFolder() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("base_dir", page.currBaseDir.Name); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_folders?`; | ||||
|   } else { | ||||
|     url = `/user/shared_folders?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "POST", | ||||
|     body: { | ||||
|       Name: page.add.Name, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.add.show = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| async function showAccessModal(name: string) { | ||||
|   page.access.Name = name; | ||||
|   page.access.show = true; | ||||
|   page.access.type = "email"; | ||||
|   page.access.inputValue = ""; | ||||
|   getAccess(); | ||||
| } | ||||
| async function showAddModal() { | ||||
|   page.add.show = true; | ||||
|   page.add.Name = ""; | ||||
| } | ||||
| 
 | ||||
| async function deleteFolder(name: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
|   params.append("base_dir", page.currBaseDir.Name); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_folders?`; | ||||
|   } else { | ||||
|     url = `/user/shared_folders?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "DELETE", | ||||
|     body: { | ||||
|       Name: name, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function getAccess() { | ||||
|   page.access.withSearch = page.access.pagination.search !== ""; | ||||
|   page.access.loading = true; | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("name", page.access.Name); | ||||
|   params.append("type", page.access.type); | ||||
|   params.append("page", String(page.access.pagination.current)); | ||||
|   params.append("size", String(page.access.pagination.size)); | ||||
|   params.append("search", page.access.pagination.search); | ||||
|   params.append("base_dir", page.currBaseDir.Name); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_folders/access?`; | ||||
|   } else { | ||||
|     url = `/user/shared_folders/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString()); | ||||
| 
 | ||||
|   page.access.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.access.data = []; | ||||
| 
 | ||||
|   if (res.data) { | ||||
|     for (let index = 0; index < res.data.length; index++) { | ||||
|       const element = res.data[index]; | ||||
|       page.access.data.push({ Name: element }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   page.access.pagination.total = res.total; | ||||
| 
 | ||||
|   page.access.groups = []; | ||||
|   page.access.selectValue = ""; | ||||
| 
 | ||||
|   if (page.access.type === "group") { | ||||
|     for (let j = 0; j < page.all_groups.length; j++) { | ||||
|       const group = page.all_groups[j]; | ||||
|       let skip = false; | ||||
|       for (let i = 0; i < page.access.data.length; i++) { | ||||
|         const alreadyHas = page.access.data[i]; | ||||
|         if (alreadyHas.Name === group) { | ||||
|           skip = true; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (!skip) { | ||||
|         page.access.groups.push({ value: group }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (page.access.groups.length) { | ||||
|     page.access.selectValue = page.access.groups[0].value; | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function deleteAccess(access: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("name", page.access.Name); | ||||
|   params.append("type", page.access.type); | ||||
|   params.append("page", String(page.access.pagination.current)); | ||||
|   params.append("size", String(page.access.pagination.size)); | ||||
|   params.append("search", page.access.pagination.search); | ||||
|   params.append("base_dir", page.currBaseDir.Name); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_folders/access?`; | ||||
|   } else { | ||||
|     url = `/user/shared_folders/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "DELETE", | ||||
|     body: { | ||||
|       Access: access, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return getAccess(); | ||||
| } | ||||
| 
 | ||||
| async function addAccess(access: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("name", page.access.Name); | ||||
|   params.append("type", page.access.type); | ||||
|   params.append("base_dir", page.currBaseDir.Name); | ||||
| 
 | ||||
|   let url = ""; | ||||
|   if (props.domain) { | ||||
|     url = `/admin/domains/${props.domain}/shared_folders/access?`; | ||||
|   } else { | ||||
|     url = `/user/shared_folders/access?`; | ||||
|   } | ||||
| 
 | ||||
|   const res = await apiFetch(url + params.toString(), { | ||||
|     method: "POST", | ||||
|     body: { | ||||
|       Access: access, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.access.showAdd = false; | ||||
| 
 | ||||
|   return getAccess(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 700px; | ||||
|   max-width: 70%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,40 @@ | |||
| <template> | ||||
|   <a-select v-model="model"> | ||||
|     <a-select-option v-for="tz in timezones" :value="tz" | ||||
|       >UTC{{ tz }}</a-select-option | ||||
|     > | ||||
|   </a-select> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| const model = defineModel(); | ||||
| 
 | ||||
| const timezones = [ | ||||
|   "-12", | ||||
|   "-11", | ||||
|   "-10", | ||||
|   "-09", | ||||
|   "-08", | ||||
|   "-07", | ||||
|   "-06", | ||||
|   "-05", | ||||
|   "-04", | ||||
|   "-03", | ||||
|   "-02", | ||||
|   "-01", | ||||
|   "+00", | ||||
|   "+01", | ||||
|   "+02", | ||||
|   "+03", | ||||
|   "+04", | ||||
|   "+05", | ||||
|   "+06", | ||||
|   "+07", | ||||
|   "+08", | ||||
|   "+09", | ||||
|   "+10", | ||||
|   "+11", | ||||
|   "+12", | ||||
|   "+13", | ||||
|   "+14", | ||||
| ]; | ||||
| </script> | ||||
|  | @ -0,0 +1,36 @@ | |||
| <template> | ||||
|   <a-select | ||||
|     v-model="model" | ||||
|     mode="multiple" | ||||
|     style="width: 100%" | ||||
|     max-tag-count="responsive" | ||||
|   > | ||||
|     <a-select-option value="1">{{ | ||||
|       t("components.user.calendars.share_free_time.work_days_names.monday") | ||||
|     }}</a-select-option> | ||||
|     <a-select-option value="2">{{ | ||||
|       t("components.user.calendars.share_free_time.work_days_names.tuesday") | ||||
|     }}</a-select-option> | ||||
|     <a-select-option value="3">{{ | ||||
|       t("components.user.calendars.share_free_time.work_days_names.wednesday") | ||||
|     }}</a-select-option> | ||||
|     <a-select-option value="4">{{ | ||||
|       t("components.user.calendars.share_free_time.work_days_names.thursday") | ||||
|     }}</a-select-option> | ||||
|     <a-select-option value="5">{{ | ||||
|       t("components.user.calendars.share_free_time.work_days_names.friday") | ||||
|     }}</a-select-option> | ||||
|     <a-select-option value="6">{{ | ||||
|       t("components.user.calendars.share_free_time.work_days_names.saturday") | ||||
|     }}</a-select-option> | ||||
|     <a-select-option value="0">{{ | ||||
|       t("components.user.calendars.share_free_time.work_days_names.sunday") | ||||
|     }}</a-select-option> | ||||
|   </a-select> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { useI18n } from "vue-i18n"; | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const model = defineModel(); | ||||
| </script> | ||||
|  | @ -0,0 +1,6 @@ | |||
| <template> | ||||
|   <a-time-range-picker v-model="model" format="HH:mm" /> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| const model = defineModel(); | ||||
| </script> | ||||
|  | @ -0,0 +1,118 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <Card size="small" style="min-width: 100px; width: min-content"> | ||||
|       <template #title> | ||||
|         {{ timeToDate(props.day) }} | ||||
|         <br /> | ||||
|         {{ timeToDayName(new Date(props.day)) }} | ||||
|       </template> | ||||
| 
 | ||||
|       <a-card-grid | ||||
|         v-for="interval in page.intervals" | ||||
|         style="width: 100%; text-align: center; padding: 1px" | ||||
|       > | ||||
|         <Interval | ||||
|           :interval="interval" | ||||
|           :checked="page.checked.get(interval)" | ||||
|           :add="add" | ||||
|           :remove="remove" | ||||
|         ></Interval> | ||||
|       </a-card-grid> | ||||
|     </Card> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { timeToDate } from "@/composables/misc"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import Interval from "@/components/common/approval/Interval.vue"; | ||||
| import Card from "ant-design-vue/es/card/Card"; | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   day: string; | ||||
|   add: Function; | ||||
|   remove: Function; | ||||
|   checked?: string[]; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   intervals: string[]; | ||||
|   checked: Map<string, boolean>; | ||||
| }>({ | ||||
|   intervals: [], | ||||
|   checked: new Map<string, boolean>(), | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   if (props.checked) { | ||||
|     props.checked.forEach((element) => { | ||||
|       page.checked.set(element, true); | ||||
|     }); | ||||
|   } | ||||
|   page.intervals = formIntervals(); | ||||
| }); | ||||
| 
 | ||||
| function formIntervals(): string[] { | ||||
|   let res: string[] = []; | ||||
| 
 | ||||
|   for (let hour = 6; hour < 22; hour++) { | ||||
|     let hourStr = String(hour); | ||||
|     if (hour < 10) { | ||||
|       hourStr = "0" + hourStr; | ||||
|     } | ||||
|     res.push(hourStr + ":00" + " - " + hourStr + ":30"); | ||||
| 
 | ||||
|     let nextHour = hour + 1; | ||||
|     let nextHourStr = String(nextHour); | ||||
|     if (nextHour < 10) { | ||||
|       nextHourStr = "0" + nextHourStr; | ||||
|     } | ||||
|     res.push(hourStr + ":30" + " - " + nextHourStr + ":00"); | ||||
|   } | ||||
|   return res; | ||||
| } | ||||
| 
 | ||||
| function add(interval: string) { | ||||
|   return props.add(props.day + "|" + interval); | ||||
| } | ||||
| 
 | ||||
| function remove(interval: string) { | ||||
|   return props.remove(props.day + "|" + interval); | ||||
| } | ||||
| 
 | ||||
| function timeToDayName(date: Date): string { | ||||
|   switch (date.getDay()) { | ||||
|     case 1: | ||||
|       return t( | ||||
|         "components.user.calendars.share_free_time.work_days_names.monday" | ||||
|       ); | ||||
|     case 2: | ||||
|       return t( | ||||
|         "components.user.calendars.share_free_time.work_days_names.tuesday" | ||||
|       ); | ||||
|     case 3: | ||||
|       return t( | ||||
|         "components.user.calendars.share_free_time.work_days_names.wednesday" | ||||
|       ); | ||||
|     case 4: | ||||
|       return t( | ||||
|         "components.user.calendars.share_free_time.work_days_names.thursday" | ||||
|       ); | ||||
|     case 5: | ||||
|       return t( | ||||
|         "components.user.calendars.share_free_time.work_days_names.friday" | ||||
|       ); | ||||
|     case 6: | ||||
|       return t( | ||||
|         "components.user.calendars.share_free_time.work_days_names.saturday" | ||||
|       ); | ||||
|     case 0: | ||||
|       return t( | ||||
|         "components.user.calendars.share_free_time.work_days_names.sunday" | ||||
|       ); | ||||
|   } | ||||
| 
 | ||||
|   return ""; | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,66 @@ | |||
| <template> | ||||
|   <div | ||||
|     :class="page.checked ? 'checked' : 'unchecked'" | ||||
|     @pointerdown="onHover" | ||||
|     @pointerenter="onHover" | ||||
|     @click="click" | ||||
|   > | ||||
|     {{ props.interval }} | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { onMounted, reactive } from "vue"; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   interval: string; | ||||
|   add: Function; | ||||
|   remove: Function; | ||||
|   checked?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   checked: boolean; | ||||
| }>({ | ||||
|   checked: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   if (props.checked) { | ||||
|     change(); | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| function onHover(payload: PointerEvent) { | ||||
|   if (payload.buttons && payload.pointerType !== "touch") { | ||||
|     change(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function click(payload: MouseEvent) { | ||||
|   const pointerPayload = payload as PointerEvent; | ||||
|   if (pointerPayload.pointerType === "mouse") { | ||||
|     return; | ||||
|   } | ||||
|   change(); | ||||
| } | ||||
| 
 | ||||
| function change() { | ||||
|   page.checked = !page.checked; | ||||
|   if (page.checked) { | ||||
|     props.add(props.interval); | ||||
|   } else { | ||||
|     props.remove(props.interval); | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .checked { | ||||
|   background-color: #bae7ff; | ||||
|   user-select: none; | ||||
| } | ||||
| .unchecked { | ||||
|   background-color: white; | ||||
|   user-select: none; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,74 @@ | |||
| <template> | ||||
|   <a-flex gap="middle"> | ||||
|     <a-select :value="page.currentAct.Label" @change="changeAct"> | ||||
|       <a-select-option v-for="(act, i) in actions" :value="act.Label"> | ||||
|         {{ act.Label }} | ||||
|       </a-select-option> | ||||
|     </a-select> | ||||
|     <slot | ||||
|       v-if=" | ||||
|         page.currentAct.Args.length > 1 && | ||||
|         !page.currentAct.NoVal && | ||||
|         page.renderValue | ||||
|       " | ||||
|       name="action-value" | ||||
|       :change="(v: string) => {page.currentAct.Args[1] = v;  update()}" | ||||
|       :type="page.currentAct.Value" | ||||
|       :value="page.currentAct.Args[1]" | ||||
|     > | ||||
|     </slot> | ||||
|   </a-flex> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { nextTick, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   actions: Action[]; | ||||
|   value: string[]; | ||||
|   update: Function; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   renderValue: boolean; | ||||
|   currentAct: Action; | ||||
| }>({ | ||||
|   renderValue: false, | ||||
|   currentAct: { | ||||
|     ...(props.actions.find( | ||||
|       (c: Action) => c.Value === props.value[0] | ||||
|     ) as Action), | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.currentAct.Args = [...props.value]; | ||||
|   page.renderValue = true; | ||||
| }); | ||||
| 
 | ||||
| async function changeAct(newLabel: string) { | ||||
|   page.currentAct = { | ||||
|     ...(props.actions.find((c: Action) => c.Label === newLabel) as Action), | ||||
|   }; | ||||
|   page.currentAct.Args = [...page.currentAct.Args]; | ||||
|   update(); | ||||
|   page.renderValue = false; | ||||
|   nextTick(() => { | ||||
|     page.renderValue = true; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function update() { | ||||
|   return props.update(page.currentAct.Args); | ||||
| } | ||||
| </script> | ||||
| <script lang="ts"> | ||||
| export interface Action { | ||||
|   Value: string; | ||||
|   Label: string; | ||||
|   NextAllowedActions: string[]; | ||||
|   Args: string[]; | ||||
|   NoVal: boolean; | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,91 @@ | |||
| <template> | ||||
|   <a-flex gap="middle"> | ||||
|     <a-select :value="page.currentCond.Label" @change="changeCond"> | ||||
|       <a-select-option v-for="(cond, i) in conditions" :value="cond.Label"> | ||||
|         {{ cond.Label }} | ||||
|       </a-select-option> | ||||
|     </a-select> | ||||
|     <a-select | ||||
|       v-if="page.currentCond.OpIdx !== -1" | ||||
|       v-model:value="page.currentCond.Args[page.currentCond.OpIdx]" | ||||
|       @change="update()" | ||||
|     > | ||||
|       <a-select-option | ||||
|         v-for="(op, i) in page.currentCond.OpList" | ||||
|         :value="op.Val" | ||||
|       > | ||||
|         {{ op.Cname }} | ||||
|       </a-select-option> | ||||
|     </a-select> | ||||
|     <slot | ||||
|       v-if="page.currentCond.ValIdx !== -1 && page.renderValue" | ||||
|       name="condition-value" | ||||
|       :change="(v: string) => {page.currentCond.Args[page.currentCond.ValIdx] = String(v);  update()}" | ||||
|       :type="page.currentCond.Value" | ||||
|       :value="page.currentCond.Args[page.currentCond.ValIdx]" | ||||
|     > | ||||
|     </slot> | ||||
|   </a-flex> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { nextTick, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   conditions: Condition[]; | ||||
|   value: string[]; | ||||
|   update: Function; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   renderValue: boolean; | ||||
|   currentCond: Condition; | ||||
| }>({ | ||||
|   renderValue: false, | ||||
|   currentCond: { | ||||
|     ...(props.conditions.find( | ||||
|       (c: Condition) => c.Args[c.KeyIdx] === props.value[c.KeyIdx] | ||||
|     ) as Condition), | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.currentCond.Args = [...props.value]; | ||||
|   page.renderValue = true; | ||||
| }); | ||||
| 
 | ||||
| async function changeCond(newLabel: string) { | ||||
|   page.currentCond = { | ||||
|     ...(props.conditions.find( | ||||
|       (c: Condition) => c.Label === newLabel | ||||
|     ) as Condition), | ||||
|   }; | ||||
|   page.currentCond.Args = [...page.currentCond.Args]; | ||||
|   update(); | ||||
|   page.renderValue = false; | ||||
|   nextTick(() => { | ||||
|     page.renderValue = true; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function update() { | ||||
|   return props.update(page.currentCond.Args); | ||||
| } | ||||
| </script> | ||||
| <script lang="ts"> | ||||
| export interface OpList { | ||||
|   Val: string; | ||||
|   Cname: string; | ||||
| } | ||||
| 
 | ||||
| export interface Condition { | ||||
|   Value: string; | ||||
|   Label: string; | ||||
|   KeyIdx: number; | ||||
|   OpIdx: number; | ||||
|   ValIdx: number; | ||||
|   OpList: OpList[]; | ||||
|   Args: string[]; | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,588 @@ | |||
| <template> | ||||
|   <div class="panel-content"> | ||||
|     <a-table | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <div style="display: flex; justify-content: end"> | ||||
|           <a-button type="primary" @click="showRule(newRule())"> | ||||
|             <PlusOutlined /> | ||||
|             {{ t("components.common.rules.add_rule") }} | ||||
|           </a-button> | ||||
|         </div> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'enabled'"> | ||||
|           <a-switch | ||||
|             v-model:checked="record.Enabled" | ||||
|             @change="enable(record.Idx, record.Enabled)" | ||||
|           ></a-switch> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'name'"> | ||||
|           <a @click="showRule(record)">{{ record.Name }}</a> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button | ||||
|               @click="move(record.Idx, 'up')" | ||||
|               :disabled="record.Idx === 0" | ||||
|             > | ||||
|               <UpOutlined></UpOutlined> | ||||
|             </a-button> | ||||
|             <a-button | ||||
|               @click="move(record.Idx, 'down')" | ||||
|               :disabled="record.Idx === page.data.length - 1" | ||||
|             > | ||||
|               <DownOutlined></DownOutlined> | ||||
|             </a-button> | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     style="width: 90%" | ||||
|     v-model:open="page.edit.show" | ||||
|     :title=" | ||||
|       page.edit.rule.Idx === -1 | ||||
|         ? t('components.common.rules.add_rule') | ||||
|         : t('components.common.rules.edit_rule') | ||||
|     " | ||||
|   > | ||||
|     <a-divider></a-divider> | ||||
|     <a-form :label-col="labelCol" :wrapper-col="wrapperCol"> | ||||
|       <a-form-item :label="t('components.common.rules.edit_name')"> | ||||
|         <a-row> | ||||
|           <a-col flex="auto"> | ||||
|             <a-input style="width: 200px" v-model:value="page.edit.rule.Name"> | ||||
|             </a-input> | ||||
|           </a-col> | ||||
|           <a-col flex="100px"> </a-col> | ||||
|         </a-row> | ||||
|       </a-form-item> | ||||
|       <a-divider></a-divider> | ||||
| 
 | ||||
|       <a-form-item :label="t('components.common.rules.edit_conditions')"> | ||||
|         <a-flex gap="middle" vertical> | ||||
|           <a-row> | ||||
|             <a-col flex="auto"> | ||||
|               <a-radio-group v-model:value="page.edit.rule.Condition.Op"> | ||||
|                 <a-radio-button value="and">{{ | ||||
|                   t("components.common.rules.edit_conditions_all") | ||||
|                 }}</a-radio-button> | ||||
|                 <a-radio-button value="or">{{ | ||||
|                   t("components.common.rules.edit_conditions_any") | ||||
|                 }}</a-radio-button> | ||||
|               </a-radio-group> | ||||
|             </a-col> | ||||
|             <a-col flex="100px"> </a-col> | ||||
|           </a-row> | ||||
|           <template v-if="page.renderConditions && page.edit.show"> | ||||
|             <div v-for="(condition, i) in page.edit.rule.Condition.Conditions"> | ||||
|               <a-row> | ||||
|                 <a-col flex="auto"> | ||||
|                   <Conditions | ||||
|                     :value="condition" | ||||
|                     :update="(newVal: string[]) => updateCondition(i, newVal)" | ||||
|                     :conditions="props.conditions" | ||||
|                   > | ||||
|                     <template #condition-value="valueProps"> | ||||
|                       <slot | ||||
|                         name="condition-value" | ||||
|                         :change="valueProps.change" | ||||
|                         :value="valueProps.value" | ||||
|                         :type="valueProps.type" | ||||
|                       > | ||||
|                       </slot> | ||||
|                     </template> | ||||
|                   </Conditions> | ||||
|                 </a-col> | ||||
|                 <a-col flex="100px"> | ||||
|                   <a-button | ||||
|                     :disabled="i === 0" | ||||
|                     danger | ||||
|                     @click="deleteCondition(i)" | ||||
|                   > | ||||
|                     {{ t("components.common.rules.delete") }} | ||||
|                   </a-button> | ||||
|                 </a-col> | ||||
|               </a-row> | ||||
|             </div> | ||||
|           </template> | ||||
| 
 | ||||
|           <a-row> | ||||
|             <a-col flex="auto"> | ||||
|               <a-button | ||||
|                 style="width: fit-content; margin: auto" | ||||
|                 @click="addCondition" | ||||
|               > | ||||
|                 <PlusOutlined> </PlusOutlined> | ||||
|                 {{ t("components.common.rules.add_condition") }} | ||||
|               </a-button> | ||||
|             </a-col> | ||||
|             <a-col flex="100px"> </a-col> | ||||
|           </a-row> | ||||
|         </a-flex> | ||||
|       </a-form-item> | ||||
|       <a-divider></a-divider> | ||||
| 
 | ||||
|       <a-form-item :label="t('components.common.rules.edit_actions')"> | ||||
|         <a-flex gap="middle" vertical> | ||||
|           <template v-if="page.renderActions && page.edit.show"> | ||||
|             <div v-for="(action, i) in page.edit.rule.Action.Actions"> | ||||
|               <a-row> | ||||
|                 <a-col flex="auto"> | ||||
|                   <Actions | ||||
|                     :value="action" | ||||
|                     :update="(newVal: string[]) => updateAction(i,newVal)" | ||||
|                     :actions="page.allowedActions[i]" | ||||
|                   > | ||||
|                     <template #action-value="valueProps"> | ||||
|                       <slot | ||||
|                         name="action-value" | ||||
|                         :change="valueProps.change" | ||||
|                         :value="valueProps.value" | ||||
|                         :type="valueProps.type" | ||||
|                       > | ||||
|                       </slot> | ||||
|                     </template> | ||||
|                   </Actions> | ||||
|                 </a-col> | ||||
|                 <a-col flex="100px"> | ||||
|                   <a-button :disabled="i === 0" danger @click="deleteAction(i)"> | ||||
|                     {{ t("components.common.rules.delete") }} | ||||
|                   </a-button> | ||||
|                 </a-col> | ||||
|               </a-row> | ||||
|             </div> | ||||
|           </template> | ||||
| 
 | ||||
|           <a-row> | ||||
|             <a-col flex="auto"> | ||||
|               <a-button | ||||
|                 :disabled="page.newActionOptions.length === 0" | ||||
|                 style="width: fit-content; margin: auto" | ||||
|                 @click="addAction" | ||||
|               > | ||||
|                 <PlusOutlined> </PlusOutlined> | ||||
|                 {{ t("components.common.rules.add_action") }} | ||||
|               </a-button> | ||||
|             </a-col> | ||||
|             <a-col flex="100px"> </a-col> | ||||
|           </a-row> | ||||
|         </a-flex> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <a-divider></a-divider> | ||||
|     <template #footer> | ||||
|       <a-space> | ||||
|         <a-popconfirm | ||||
|           v-if="page.edit.rule.Idx !== -1" | ||||
|           :title="t('components.common.rules.delete_rule') + '?'" | ||||
|           :ok-text="t('components.common.rules.ok')" | ||||
|           :cancel-text="t('components.common.rules.cancel')" | ||||
|           @confirm="deleteRule(page.edit.rule.Idx)" | ||||
|         > | ||||
|           <a-button danger>{{ | ||||
|             $t("components.common.rules.delete_rule") | ||||
|           }}</a-button> | ||||
|         </a-popconfirm> | ||||
|         <a-button | ||||
|           :disabled="!page.edit.rule.Name || !page.edit.valid" | ||||
|           @click="saveRule(page.edit.rule)" | ||||
|           type="primary" | ||||
|         > | ||||
|           {{ t("components.common.rules.save_rule") }} | ||||
|         </a-button> | ||||
|       </a-space> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
|   UpOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import type { Condition } from "./Conditions.vue"; | ||||
| import Conditions from "./Conditions.vue"; | ||||
| import type { Action } from "./Actions.vue"; | ||||
| import { nextTick } from "vue"; | ||||
| import Actions from "./Actions.vue"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.common.rules.col_enabled"), | ||||
|     key: "enabled", | ||||
|     align: "center", | ||||
|     width: 100, | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.common.rules.col_name"), | ||||
|     key: "name", | ||||
|     align: "center", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.common.rules.col_action"), | ||||
|     key: "action", | ||||
|     align: "center", | ||||
|     width: 120, | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const labelCol = { span: 3, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 21, style: { "text-align": "center" } }; | ||||
| 
 | ||||
| interface ConditionStruct { | ||||
|   Op: string; | ||||
|   Conditions: string[][]; | ||||
| } | ||||
| 
 | ||||
| interface ActionStruct { | ||||
|   Actions: string[][]; | ||||
| } | ||||
| 
 | ||||
| interface Rule { | ||||
|   Idx: number; | ||||
|   Name: string; | ||||
|   Condition: ConditionStruct; | ||||
|   Action: ActionStruct; | ||||
| } | ||||
| 
 | ||||
| function newRule(): Rule { | ||||
|   return { | ||||
|     Idx: -1, | ||||
|     Name: "", | ||||
|     Condition: { | ||||
|       Op: "and", | ||||
|       Conditions: [[...props.conditions[0].Args]], | ||||
|     }, | ||||
|     Action: { | ||||
|       Actions: [[...props.actions[0].Args]], | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| function copyRule(old: Rule): Rule { | ||||
|   return { | ||||
|     Idx: old.Idx, | ||||
|     Name: old.Name, | ||||
|     Condition: { | ||||
|       Op: old.Condition.Op, | ||||
|       Conditions: [...old.Condition.Conditions], | ||||
|     }, | ||||
|     Action: { | ||||
|       Actions: [...old.Action.Actions], | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   getGetUrl: Function; | ||||
|   getMoveUrl: Function; | ||||
|   getEnableUrl: Function; | ||||
|   getDeleteUrl: Function; | ||||
|   getSaveUrl: Function; | ||||
|   conditions: Condition[]; | ||||
|   actions: Action[]; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   edit: { | ||||
|     show: boolean; | ||||
|     rule: Rule; | ||||
|     valid: boolean; | ||||
|   }; | ||||
|   renderConditions: boolean; | ||||
|   renderActions: boolean; | ||||
|   allowedActions: Action[][]; | ||||
|   newActionOptions: Action[]; | ||||
| }>({ | ||||
|   loading: false, | ||||
|   data: undefined, | ||||
|   edit: { | ||||
|     show: false, | ||||
|     rule: newRule(), | ||||
|     valid: true, | ||||
|   }, | ||||
|   renderConditions: true, | ||||
|   renderActions: true, | ||||
|   allowedActions: [], | ||||
|   newActionOptions: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| function computeAllowedActions() { | ||||
|   page.allowedActions = []; | ||||
|   page.newActionOptions = []; | ||||
| 
 | ||||
|   for (let i = 0; i < page.edit.rule.Action.Actions.length + 1; i++) { | ||||
|     if (i === 0) { | ||||
|       page.allowedActions.push(props.actions); | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
|     let action = props.actions.find( | ||||
|       (a: Action) => a.Value === page.edit.rule.Action.Actions[i - 1][0] | ||||
|     ) as Action; | ||||
| 
 | ||||
|     let newAllowedActionsValue = [...action.NextAllowedActions]; | ||||
| 
 | ||||
|     for (let j = 0; j < page.allowedActions.length; j++) { | ||||
|       const prevAllowedActions = page.allowedActions[j]; | ||||
|       newAllowedActionsValue = newAllowedActionsValue.filter((act: string) => | ||||
|         prevAllowedActions.map((a: Action) => a.Value).includes(act) | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     page.allowedActions.push( | ||||
|       props.actions.filter((a: Action) => | ||||
|         newAllowedActionsValue.includes(a.Value) | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   page.newActionOptions = page.allowedActions.pop() as Action[]; | ||||
| 
 | ||||
|   for (let i = page.edit.rule.Action.Actions.length - 1; i >= 0; i--) { | ||||
|     if ( | ||||
|       !page.allowedActions[i] | ||||
|         .map((a: Action) => a.Value) | ||||
|         .includes(page.edit.rule.Action.Actions[i][0]) | ||||
|     ) { | ||||
|       page.edit.rule.Action.Actions.splice(i, 1); | ||||
|       page.allowedActions.splice(i, 1); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function updateAction(i: number, newVal: string[]) { | ||||
|   page.edit.rule.Action.Actions[i] = newVal; | ||||
|   computeAllowedActions(); | ||||
|   validate(); | ||||
| } | ||||
| 
 | ||||
| function updateCondition(i: number, newVal: string[]) { | ||||
|   page.edit.rule.Condition.Conditions[i] = newVal; | ||||
|   validate(); | ||||
| } | ||||
| 
 | ||||
| function validate() { | ||||
|   let ok = true; | ||||
| 
 | ||||
|   page.edit.rule.Action.Actions.forEach((args) => { | ||||
|     let action = props.actions.find( | ||||
|       (a: Action) => a.Value === args[0] | ||||
|     ) as Action; | ||||
|     if (!action.NoVal && args[1] === "") { | ||||
|       ok = false; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   page.edit.rule.Condition.Conditions.forEach((args) => { | ||||
|     let cond = props.conditions.find( | ||||
|       (a: Condition) => a.Value === args[0] | ||||
|     ) as Condition; | ||||
|     if (cond.ValIdx != -1 && args[cond.ValIdx] === "") { | ||||
|       ok = false; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   page.edit.valid = ok; | ||||
| } | ||||
| 
 | ||||
| function addCondition() { | ||||
|   page.edit.rule.Condition.Conditions.push(props.conditions[0].Args); | ||||
|   validate(); | ||||
| } | ||||
| 
 | ||||
| function deleteCondition(i: number) { | ||||
|   page.edit.rule.Condition.Conditions.splice(i, 1); | ||||
|   page.renderConditions = false; | ||||
|   validate(); | ||||
|   nextTick(() => { | ||||
|     page.renderConditions = true; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function addAction() { | ||||
|   if (page.newActionOptions.length > 0) { | ||||
|     page.edit.rule.Action.Actions.push(page.newActionOptions[0].Args); | ||||
|   } | ||||
|   computeAllowedActions(); | ||||
|   validate(); | ||||
| } | ||||
| 
 | ||||
| function deleteAction(i: number) { | ||||
|   page.edit.rule.Action.Actions.splice(i, 1); | ||||
|   computeAllowedActions(); | ||||
|   validate(); | ||||
|   page.renderActions = false; | ||||
|   nextTick(() => { | ||||
|     page.renderActions = true; | ||||
|   }); | ||||
| 
 | ||||
|   // console.log(page.edit.rule.Action.Actions); | ||||
| } | ||||
| 
 | ||||
| async function showRule(rule: any) { | ||||
|   page.edit.rule = copyRule(rule); | ||||
|   page.edit.show = true; | ||||
|   computeAllowedActions(); | ||||
|   // for (let i = 0; i < page.edit.rule.Action.Actions.length; i++) { | ||||
|   //   const action = page.edit.rule.Action.Actions[i]; | ||||
|   //   page.allowedActions.push(getAllowedActions(i)); | ||||
|   // } | ||||
| } | ||||
| 
 | ||||
| // function getAllowedActions(to: number): Action[] { | ||||
| //   if (page.edit.rule.Action.Actions.length === 1) { | ||||
| //     return props.actions; | ||||
| //   } | ||||
| 
 | ||||
| //   let avaliableActions = props.actions.map((a: Action) => a.Value); | ||||
| //   for (let i = 0; i < to; i++) { | ||||
| //     const element = page.edit.rule.Action.Actions[i]; | ||||
| //     let action = props.actions.find( | ||||
| //       (a: Action) => a.Value === element[0] | ||||
| //     ) as Action; | ||||
| //     avaliableActions = avaliableActions.filter((act: string) => | ||||
| //       action.NextAllowedActions.includes(act) | ||||
| //     ); | ||||
| //   } | ||||
| 
 | ||||
| //   let res = props.actions.filter((a: Action) => | ||||
| //     avaliableActions.includes(a.Value) | ||||
| //   ); | ||||
| 
 | ||||
| //   console.log(res); | ||||
| //   return res; | ||||
| 
 | ||||
| //   return nil; | ||||
| // } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   let url = props.getGetUrl(); | ||||
|   const res = await apiFetch(url); | ||||
| 
 | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   for (let i = 0; i < page.data.length; i++) { | ||||
|     const element = page.data[i]; | ||||
|     element.Enabled = !element.Disabled; | ||||
|     element.Idx = i; | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function move(idx: number, direction: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("idx", String(idx)); | ||||
|   params.append("direction", direction); | ||||
|   let url = props.getMoveUrl(); | ||||
|   const res = await apiFetch(url + "?" + params.toString(), { | ||||
|     method: "POST", | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function enable(idx: number, enabled: boolean) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("idx", String(idx)); | ||||
|   params.append("enabled", String(enabled)); | ||||
|   let url = props.getEnableUrl(); | ||||
|   const res = await apiFetch(url + "?" + params.toString(), { | ||||
|     method: "POST", | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function deleteRule(idx: number) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("idx", String(idx)); | ||||
|   let url = props.getDeleteUrl(); | ||||
|   const res = await apiFetch(url + "?" + params.toString(), { | ||||
|     method: "DELETE", | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.edit.show = false; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function saveRule(rule: any) { | ||||
|   let url = props.getSaveUrl(); | ||||
|   const res = await apiFetch(url, { | ||||
|     method: "POST", | ||||
|     body: rule, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.edit.show = false; | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 500px; | ||||
|   max-width: 50%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,61 @@ | |||
| <template> | ||||
|   <div style="width: 100%"> | ||||
|     <a-input | ||||
|       v-if=" | ||||
|         type === ActKwTransformFromAddress || type === ActKwTransformToAddress | ||||
|       " | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|     </a-input> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   change: Function; | ||||
|   value: string; | ||||
|   type: string; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   value: string; | ||||
| }>({ | ||||
|   value: props.value, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import type { Action } from "../Actions.vue"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import i18n from "@/locale"; | ||||
| 
 | ||||
| const ActKwTransformFromAddress = `transform_from_address`; | ||||
| const ActKwTransformToAddress = `transform_to_address`; | ||||
| 
 | ||||
| export const actionsList: Action[] = [ | ||||
|   { | ||||
|     Value: ActKwTransformFromAddress, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.address.actions.transform_from_address" | ||||
|     ), | ||||
|     NextAllowedActions: [ActKwTransformFromAddress, ActKwTransformToAddress], | ||||
|     Args: [ActKwTransformFromAddress, "*@(old_host.ru) => new_host.ru"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwTransformToAddress, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.address.actions.transform_to_address" | ||||
|     ), | ||||
|     NextAllowedActions: [ActKwTransformFromAddress, ActKwTransformToAddress], | ||||
|     Args: [ActKwTransformToAddress, "*@(old_host.ru) => new_host.ru"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
| ]; | ||||
| </script> | ||||
|  | @ -0,0 +1,192 @@ | |||
| <template> | ||||
|   <div style="width: 100%"> | ||||
|     <a-input | ||||
|       v-if=" | ||||
|         props.type === CondKwFromSMTP || | ||||
|         props.type === CondKwToSMTP || | ||||
|         props.type === CondKwFromGroup || | ||||
|         props.type === CondKwToGroup | ||||
|       " | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|     </a-input> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import dayjs from "dayjs"; | ||||
| const props = defineProps<{ | ||||
|   change: Function; | ||||
|   value: string; | ||||
|   type: string; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   value: string; | ||||
| }>({ | ||||
|   value: props.value, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import type { Condition } from "../Conditions.vue"; | ||||
| import i18n from "@/locale"; | ||||
| 
 | ||||
| const CondKwFromSMTP = `from`; | ||||
| const CondKwToSMTP = `to`; | ||||
| const CondKwFromGroup = `from_group`; | ||||
| const CondKwToGroup = `to_group`; | ||||
| const CondKwAll = `all`; | ||||
| 
 | ||||
| export const conditionsList: Condition[] = [ | ||||
|   { | ||||
|     Value: CondKwFromSMTP, | ||||
|     Label: i18n.global.t("components.common.rules.address.conditions.from"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwFromSMTP, "=", "test@example.com"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwToSMTP, | ||||
|     Label: i18n.global.t("components.common.rules.address.conditions.to"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwToSMTP, "=", "test@example.com"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwFromGroup, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.address.conditions.from_group" | ||||
|     ), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwFromGroup, "==", "group"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwToGroup, | ||||
|     Label: i18n.global.t("components.common.rules.address.conditions.to_group"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.address.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwToGroup, "==", "group"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwAll, | ||||
|     Label: i18n.global.t("components.common.rules.address.conditions.all"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: -1, | ||||
|     ValIdx: -1, | ||||
|     OpList: [], | ||||
|     Args: [CondKwAll], | ||||
|   }, | ||||
| ]; | ||||
| </script> | ||||
|  | @ -0,0 +1,210 @@ | |||
| <template> | ||||
|   <div style="width: 100%"> | ||||
|     <a-select | ||||
|       v-if=" | ||||
|         (type === ActKwToFolder || type === ActKwCopyToFolder) && | ||||
|         page.folders.length !== 0 | ||||
|       " | ||||
|       v-model:value="page.value" | ||||
|       @change="(newVal: string) => props.change(newVal)" | ||||
|     > | ||||
|       <a-select-option | ||||
|         v-for="foldersOpt in page.folders" | ||||
|         :value="foldersOpt[1]" | ||||
|       > | ||||
|         {{ foldersOpt[0] }} | ||||
|       </a-select-option> | ||||
|     </a-select> | ||||
|     <a-input | ||||
|       v-if="type === ActKwRedirectTo || type === ActKwCopyTo" | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|     </a-input> | ||||
|     <a-textarea | ||||
|       v-if="type === ActKwReplyMsg" | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|     </a-textarea> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   change: Function; | ||||
|   value: string; | ||||
|   type: string; | ||||
|   getFoldersUrl: Function; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   value: string; | ||||
|   folders: string[][]; | ||||
| }>({ | ||||
|   value: props.value, | ||||
|   folders: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   if (props.type === ActKwToFolder || props.type === ActKwCopyToFolder) { | ||||
|     getFolders(); | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| async function getFolders() { | ||||
|   let url = props.getFoldersUrl(); | ||||
|   const res = await apiFetch(url); | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.folders = []; | ||||
|   page.folders = res.data; | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import type { Action } from "../Actions.vue"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import i18n from "@/locale"; | ||||
| 
 | ||||
| const ActKwToFolder = `to_folder`; | ||||
| const ActKwCopyToFolder = `copy_to_folder`; | ||||
| const ActKwMarkAsSeen = `mark_as_seen`; | ||||
| const ActKwMarkAsFlagged = `mark_as_flagged`; | ||||
| const ActKwRedirectTo = `redirect_to`; | ||||
| const ActKwCopyTo = `copy_to`; | ||||
| const ActKwReplyMsg = `reply_msg`; | ||||
| const ActKwReject = `reject`; | ||||
| const ActKwStop = `stop`; | ||||
| 
 | ||||
| export const actionsList: Action[] = [ | ||||
|   { | ||||
|     Value: ActKwToFolder, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.actions.to_folder"), | ||||
|     NextAllowedActions: [ | ||||
|       ActKwCopyTo, | ||||
|       ActKwReplyMsg, | ||||
|       ActKwStop, | ||||
|       ActKwCopyToFolder, | ||||
|       ActKwMarkAsSeen, | ||||
|       ActKwMarkAsFlagged, | ||||
|     ], | ||||
|     Args: [ActKwToFolder, "INBOX"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwCopyToFolder, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.incoming.actions.copy_to_folder" | ||||
|     ), | ||||
|     NextAllowedActions: [ | ||||
|       ActKwToFolder, | ||||
|       ActKwCopyToFolder, | ||||
|       ActKwMarkAsSeen, | ||||
|       ActKwMarkAsFlagged, | ||||
|       ActKwCopyTo, | ||||
|       ActKwReplyMsg, | ||||
|       ActKwStop, | ||||
|     ], | ||||
|     Args: [ActKwCopyToFolder, "INBOX"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwMarkAsSeen, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.incoming.actions.mark_as_seen" | ||||
|     ), | ||||
|     NextAllowedActions: [ | ||||
|       ActKwToFolder, | ||||
|       ActKwCopyTo, | ||||
|       ActKwReplyMsg, | ||||
|       ActKwStop, | ||||
|       ActKwCopyToFolder, | ||||
|       ActKwMarkAsFlagged, | ||||
|     ], | ||||
|     Args: [ActKwMarkAsSeen, ""], | ||||
|     NoVal: true, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwMarkAsFlagged, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.incoming.actions.mark_as_flagged" | ||||
|     ), | ||||
|     NextAllowedActions: [ | ||||
|       ActKwToFolder, | ||||
|       ActKwCopyTo, | ||||
|       ActKwReplyMsg, | ||||
|       ActKwStop, | ||||
|       ActKwCopyToFolder, | ||||
|       ActKwMarkAsSeen, | ||||
|     ], | ||||
|     Args: [ActKwMarkAsFlagged, ""], | ||||
|     NoVal: true, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwRedirectTo, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.incoming.actions.redirect_to" | ||||
|     ), | ||||
|     NextAllowedActions: [ActKwCopyTo, ActKwReplyMsg], | ||||
|     Args: [ActKwRedirectTo, ""], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwCopyTo, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.actions.copy_to"), | ||||
|     NextAllowedActions: [ | ||||
|       ActKwRedirectTo, | ||||
|       ActKwToFolder, | ||||
|       ActKwCopyToFolder, | ||||
|       ActKwMarkAsSeen, | ||||
|       ActKwMarkAsFlagged, | ||||
|       ActKwReplyMsg, | ||||
|       ActKwCopyTo, | ||||
|       ActKwStop, | ||||
|     ], | ||||
|     Args: [ActKwCopyTo, ""], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwReplyMsg, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.actions.reply_msg"), | ||||
|     NextAllowedActions: [ | ||||
|       ActKwRedirectTo, | ||||
|       ActKwToFolder, | ||||
|       ActKwCopyToFolder, | ||||
|       ActKwMarkAsSeen, | ||||
|       ActKwMarkAsFlagged, | ||||
|       ActKwCopyTo, | ||||
|       ActKwReplyMsg, | ||||
|       ActKwReject, | ||||
|       ActKwStop, | ||||
|     ], | ||||
|     Args: [ActKwReplyMsg, ""], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwReject, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.actions.reject"), | ||||
|     NextAllowedActions: [], | ||||
|     Args: [ActKwReject, ""], | ||||
|     NoVal: true, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwStop, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.actions.stop"), | ||||
|     NextAllowedActions: [], | ||||
|     Args: [ActKwStop, ""], | ||||
|     NoVal: true, | ||||
|   }, | ||||
| ]; | ||||
| </script> | ||||
|  | @ -0,0 +1,503 @@ | |||
| <template> | ||||
|   <div style="width: 100%"> | ||||
|     <a-input | ||||
|       v-if=" | ||||
|         props.type === CondKwToSMTP || | ||||
|         props.type === CondKwFromSMTP || | ||||
|         props.type === CondKwHeader || | ||||
|         props.type === CondKwBody | ||||
|       " | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|     </a-input> | ||||
|     <a-input-number | ||||
|       v-if="props.type === CondKwSpamScore" | ||||
|       style="width: 100%" | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="!page.value ? 'error' : ''" | ||||
|     > | ||||
|     </a-input-number> | ||||
|     <a-tooltip title="1048576, 1024K or 1M"> | ||||
|       <a-input | ||||
|         v-if="props.type === CondKwSize" | ||||
|         v-model:value="page.value" | ||||
|         @change="props.change(page.value)" | ||||
|         :status="page.value === '' ? 'error' : ''" | ||||
|       > | ||||
|       </a-input> | ||||
|     </a-tooltip> | ||||
|     <a-date-picker | ||||
|       v-if="props.type === CondKwDate" | ||||
|       style="width: 100%" | ||||
|       v-model:value="page.dateValue" | ||||
|       @change="props.change(page.dateValue.format('YYYY-MM-DD'))" | ||||
|       :allowClear="false" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import dayjs from "dayjs"; | ||||
| const props = defineProps<{ | ||||
|   change: Function; | ||||
|   value: string; | ||||
|   type: string; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   value: string; | ||||
|   dateValue: any; | ||||
| }>({ | ||||
|   value: props.value, | ||||
|   dateValue: undefined, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   if (props.type === "date") { | ||||
|     page.dateValue = dayjs(Date.parse(page.value)); | ||||
|   } | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import type { Condition, OpList } from "../Conditions.vue"; | ||||
| import i18n from "@/locale"; | ||||
| 
 | ||||
| const CondKwDate = `date`; | ||||
| const CondKwHeader = `header`; | ||||
| const CondKwSize = `size`; | ||||
| const CondKwSpamScore = `spam_score`; | ||||
| const CondKwBody = `body`; | ||||
| const CondKwAll = `all`; | ||||
| const CondKwFromSMTP = `from`; | ||||
| const CondKwToSMTP = `to`; | ||||
| 
 | ||||
| export const conditionsList: Condition[] = [ | ||||
|   { | ||||
|     Value: CondKwDate, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.conditions.date"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `>=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `<=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwDate, "=", new Date().toISOString().split("T")[0]], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwToSMTP, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.conditions.to"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwToSMTP, "=", "test@example.com"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwFromSMTP, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.conditions.from"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwFromSMTP, "=", "test@example.com"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwHeader, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.incoming.conditions.header_to" | ||||
|     ), | ||||
|     KeyIdx: 1, | ||||
|     OpIdx: 2, | ||||
|     ValIdx: 3, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwHeader, "To", "=", "test@example.com"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwHeader, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.incoming.conditions.header_from" | ||||
|     ), | ||||
|     KeyIdx: 1, | ||||
|     OpIdx: 2, | ||||
|     ValIdx: 3, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwHeader, "From", "=", "test@example.com"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwHeader, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.conditions.subject"), | ||||
|     KeyIdx: 1, | ||||
|     OpIdx: 2, | ||||
|     ValIdx: 3, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwHeader, "Subject", "=", "some subject"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwHeader, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.incoming.conditions.copy_recipient" | ||||
|     ), | ||||
|     KeyIdx: 1, | ||||
|     OpIdx: 2, | ||||
|     ValIdx: 3, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwHeader, "Cc", "=", "test@example.com"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwSize, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.conditions.size"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `<`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_less" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `>`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_greater" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwSize, "=", "100K"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwSpamScore, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.incoming.conditions.spam_score" | ||||
|     ), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `<=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_less_or_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `>=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_greater_or_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwSpamScore, "=", "5"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwBody, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.conditions.body"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.incoming.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwBody, "=", "hello"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwAll, | ||||
|     Label: i18n.global.t("components.common.rules.incoming.conditions.all"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: -1, | ||||
|     ValIdx: -1, | ||||
|     OpList: [], | ||||
|     Args: [CondKwAll], | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| function filterCondsForMailDeletion(list: Condition[]): Condition[] { | ||||
|   for (let i = 0; i < list.length; i++) { | ||||
|     const element = list[i]; | ||||
|     if (element.Value === CondKwHeader) { | ||||
|       element.OpList = element.OpList.filter((c: OpList) => { | ||||
|         return c.Val === "=" || c.Val === "!"; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|   return list.filter((c: Condition) => { | ||||
|     return ( | ||||
|       c.Value === CondKwDate || | ||||
|       c.Value === CondKwHeader || | ||||
|       c.Value === CondKwSize || | ||||
|       c.Value === CondKwAll || | ||||
|       c.Value === CondKwBody || | ||||
|       c.Value === CondKwSpamScore | ||||
|     ); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export const conditionsListForMailDeletion: Condition[] = | ||||
|   filterCondsForMailDeletion(conditionsList); | ||||
| </script> | ||||
|  | @ -0,0 +1,381 @@ | |||
| <template> | ||||
|   <div style="width: 100%"> | ||||
|     <a-input | ||||
|       v-if=" | ||||
|         type === ActKwTransformFromAddress || | ||||
|         type === ActKwTransformToAddress || | ||||
|         type === ActKwTransformFromAddressAndHeader || | ||||
|         type === ActKwTransformToAddressAndHeader || | ||||
|         type === ActKwCopyTo || | ||||
|         type === ActKwPremoderation || | ||||
|         type === ActKwSendViaSmartHost | ||||
|       " | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|     </a-input> | ||||
|     <a-input-number | ||||
|       v-if=" | ||||
|         type === ActKwRejectExternalCount || type === ActKwRejectInternalCount | ||||
|       " | ||||
|       style="width: 100%" | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="!page.value ? 'error' : ''" | ||||
|       :min="0" | ||||
|     > | ||||
|     </a-input-number> | ||||
|     <a-select | ||||
|       v-if=" | ||||
|         (type === ActKwRejectGroups || type === ActKwAllowGroups) && | ||||
|         page.gEmails.length !== 0 | ||||
|       " | ||||
|       v-model:value="page.value" | ||||
|       @change="(newVal: string) => props.change(newVal)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|       <a-select-option v-for="v in page.gEmails" :value="v"> | ||||
|         {{ v }} | ||||
|       </a-select-option> | ||||
|     </a-select> | ||||
|     <a-select | ||||
|       v-if=" | ||||
|         (type === ActKwRejectInGroup || type === ActKwAllowInGroup) && | ||||
|         page.groups.length !== 0 | ||||
|       " | ||||
|       v-model:value="page.value" | ||||
|       @change="(newVal: string) => props.change(newVal)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|       <a-select-option v-for="v in page.groups" :value="v"> | ||||
|         {{ v }} | ||||
|       </a-select-option> | ||||
|     </a-select> | ||||
|     <a-textarea | ||||
|       v-if="type === ActKwSendResponse" | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|     </a-textarea> | ||||
|     <a-textarea | ||||
|       v-if="type === ActKwAddTextToStart || type === ActKwAddTextToEnd" | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|     </a-textarea> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   change: Function; | ||||
|   value: string; | ||||
|   type: string; | ||||
|   getGroupsUrl: Function; | ||||
|   getGroupEmailsUrl: Function; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   value: string; | ||||
|   gEmails: []; | ||||
|   groups: []; | ||||
| }>({ | ||||
|   value: props.value, | ||||
|   gEmails: [], | ||||
|   groups: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   if (props.type === ActKwAllowInGroup || props.type === ActKwRejectInGroup) { | ||||
|     getGroups(); | ||||
|   } | ||||
|   if (props.type === ActKwAllowGroups || props.type === ActKwRejectGroups) { | ||||
|     getGroupEmails(); | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| async function getGroups() { | ||||
|   let url = props.getGroupsUrl(); | ||||
|   const res = await apiFetch(url); | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.groups = []; | ||||
|   page.groups = res.data; | ||||
| } | ||||
| 
 | ||||
| async function getGroupEmails() { | ||||
|   let url = props.getGroupEmailsUrl(); | ||||
|   const res = await apiFetch(url); | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.gEmails = []; | ||||
|   page.gEmails = res.data; | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import type { Action } from "../Actions.vue"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import i18n from "@/locale"; | ||||
| 
 | ||||
| const ActKwTransformFromAddress = `transform_from_address`; | ||||
| const ActKwTransformToAddress = `transform_to_address`; | ||||
| const ActKwTransformFromAddressAndHeader = `transform_from_address_and_header`; | ||||
| const ActKwTransformToAddressAndHeader = `transform_to_address_and_header`; | ||||
| const ActKwCopyTo = `copy_to`; | ||||
| const ActKwPremoderation = `premoderation`; | ||||
| const ActKwSendViaSmartHost = `send_via_smart-host`; | ||||
| 
 | ||||
| const ActKwRejectExternal = `reject_external`; | ||||
| const ActKwRejectExternalCount = `reject_external_count`; | ||||
| const ActKwRejectInternalCount = `reject_internal_count`; | ||||
| const ActKwRejectGroups = `reject_groups`; | ||||
| const ActKwRejectInGroup = `reject_in_group`; | ||||
| const ActRejectAny = `reject_any`; | ||||
| 
 | ||||
| const ActKwAllowExternal = `allow_external`; | ||||
| const ActKwAllowGroups = `allow_groups`; | ||||
| const ActKwAllowInGroup = `allow_in_group`; | ||||
| const ActKwAllowAny = `allow_any`; | ||||
| 
 | ||||
| const ActKwSendResponse = `send_response`; | ||||
| const ActKwAddTextToStart = `add_text_to_start`; | ||||
| const ActKwAddTextToEnd = `add_text_to_end`; | ||||
| 
 | ||||
| const allActions = [ | ||||
|   ActKwTransformFromAddress, | ||||
|   ActKwTransformToAddress, | ||||
|   ActKwTransformFromAddressAndHeader, | ||||
|   ActKwTransformToAddressAndHeader, | ||||
|   ActKwCopyTo, | ||||
|   ActKwPremoderation, | ||||
|   ActKwSendViaSmartHost, | ||||
|   ActKwRejectExternal, | ||||
|   ActKwRejectExternalCount, | ||||
|   ActKwRejectInternalCount, | ||||
|   ActKwRejectGroups, | ||||
|   ActKwRejectInGroup, | ||||
|   ActRejectAny, | ||||
|   ActKwAllowExternal, | ||||
|   ActKwAllowGroups, | ||||
|   ActKwAllowInGroup, | ||||
|   ActKwAllowAny, | ||||
|   ActKwSendResponse, | ||||
| ]; | ||||
| 
 | ||||
| const onlyAllowActions = [ | ||||
|   ActKwAllowExternal, | ||||
|   ActKwAllowGroups, | ||||
|   ActKwAllowInGroup, | ||||
|   ActKwAllowAny, | ||||
| ]; | ||||
| 
 | ||||
| export const actionsList: Action[] = [ | ||||
|   { | ||||
|     Value: ActKwTransformFromAddress, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.transform_from_address" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwTransformFromAddress, "*@(old_host.ru) => new_host.ru"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwTransformToAddress, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.transform_to_address" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwTransformToAddress, "*@(old_host.ru) => new_host.ru"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwTransformFromAddressAndHeader, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.transform_from_address_and_header" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ | ||||
|       ActKwTransformFromAddressAndHeader, | ||||
|       "*@(old_host.ru) => new_host.ru", | ||||
|     ], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwTransformToAddressAndHeader, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.transform_to_address_and_header" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwTransformToAddressAndHeader, "*@(old_host.ru) => new_host.ru"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwCopyTo, | ||||
|     Label: i18n.global.t("components.common.rules.outgoing.actions.copy_to"), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwCopyTo, "copy_to@example.ru"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwPremoderation, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.premoderation" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwPremoderation, "moderator@example.ru"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwSendViaSmartHost, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.send_via_smart_host" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwSendViaSmartHost, "[user:pass@]host[:port]"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwRejectExternal, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.reject_external" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwRejectExternal, ""], | ||||
|     NoVal: true, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwRejectExternalCount, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.reject_external_count" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwRejectExternalCount, "10"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwRejectInternalCount, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.reject_internal_count" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwRejectInternalCount, "10"], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwRejectGroups, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.reject_groups" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwRejectGroups, ""], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwRejectInGroup, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.reject_in_group" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActKwRejectInGroup, ""], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActRejectAny, | ||||
|     Label: i18n.global.t("components.common.rules.outgoing.actions.reject_any"), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ActRejectAny, ""], | ||||
|     NoVal: true, | ||||
|   }, | ||||
| 
 | ||||
|   { | ||||
|     Value: ActKwAllowExternal, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.allow_external" | ||||
|     ), | ||||
|     NextAllowedActions: onlyAllowActions, | ||||
|     Args: [ActKwAllowExternal, ""], | ||||
|     NoVal: true, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwAllowGroups, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.allow_groups" | ||||
|     ), | ||||
|     NextAllowedActions: onlyAllowActions, | ||||
|     Args: [ActKwAllowGroups, ""], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwAllowInGroup, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.allow_in_group" | ||||
|     ), | ||||
|     NextAllowedActions: onlyAllowActions, | ||||
|     Args: [ActKwAllowInGroup, ""], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwAllowAny, | ||||
|     Label: i18n.global.t("components.common.rules.outgoing.actions.allow_any"), | ||||
|     NextAllowedActions: onlyAllowActions, | ||||
|     Args: [ActKwAllowAny, ""], | ||||
|     NoVal: true, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwSendResponse, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.send_response" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ | ||||
|       ActKwSendResponse, | ||||
|       i18n.global.t( | ||||
|         "components.common.rules.outgoing.actions.send_response_default" | ||||
|       ), | ||||
|     ], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwAddTextToStart, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.add_text_to_start" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ | ||||
|       ActKwAddTextToStart, | ||||
|       i18n.global.t( | ||||
|         "components.common.rules.outgoing.actions.add_text_default" | ||||
|       ), | ||||
|     ], | ||||
|     NoVal: false, | ||||
|   }, | ||||
|   { | ||||
|     Value: ActKwAddTextToEnd, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.actions.add_text_to_end" | ||||
|     ), | ||||
|     NextAllowedActions: allActions, | ||||
|     Args: [ | ||||
|       ActKwAddTextToEnd, | ||||
|       i18n.global.t( | ||||
|         "components.common.rules.outgoing.actions.add_text_default" | ||||
|       ), | ||||
|     ], | ||||
|     NoVal: false, | ||||
|   }, | ||||
| ]; | ||||
| </script> | ||||
|  | @ -0,0 +1,300 @@ | |||
| <template> | ||||
|   <div style="width: 100%"> | ||||
|     <a-select | ||||
|       v-if=" | ||||
|         (type === CondKwFromGroup || type === CondKwToGroup) && | ||||
|         page.groups.length !== 0 | ||||
|       " | ||||
|       v-model:value="page.value" | ||||
|       @change="(newVal: string) => props.change(newVal)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|       <a-select-option v-for="g in page.groups" :value="g"> | ||||
|         {{ g }} | ||||
|       </a-select-option> | ||||
|     </a-select> | ||||
|     <a-input | ||||
|       v-if=" | ||||
|         props.type === CondKwToSMTP || | ||||
|         props.type === CondKwFromSMTP || | ||||
|         props.type === CondKwHeader || | ||||
|         props.type === CondKwBody | ||||
|       " | ||||
|       v-model:value="page.value" | ||||
|       @change="props.change(page.value)" | ||||
|       :status="page.value === '' ? 'error' : ''" | ||||
|     > | ||||
|     </a-input> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import dayjs from "dayjs"; | ||||
| const props = defineProps<{ | ||||
|   change: Function; | ||||
|   value: string; | ||||
|   type: string; | ||||
|   getGroupsUrl: Function; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   value: string; | ||||
|   groups: string[]; | ||||
| }>({ | ||||
|   value: props.value, | ||||
|   groups: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   if (props.type === CondKwFromGroup || props.type === CondKwToGroup) { | ||||
|     getGroups(); | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| async function getGroups() { | ||||
|   let url = props.getGroupsUrl(); | ||||
|   const res = await apiFetch(url); | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.groups = []; | ||||
|   page.groups = res.data; | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import type { Condition } from "../Conditions.vue"; | ||||
| import i18n from "@/locale"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| 
 | ||||
| const CondKwToSMTP = `to`; | ||||
| const CondKwFromSMTP = `from`; | ||||
| const CondKwFromGroup = `from_group`; | ||||
| const CondKwToGroup = `to_group`; | ||||
| const CondKwHeader = `header`; | ||||
| const CondKwBody = `body`; | ||||
| const CondKwAll = `all`; | ||||
| 
 | ||||
| export const conditionsList: Condition[] = [ | ||||
|   { | ||||
|     Value: CondKwToSMTP, | ||||
|     Label: i18n.global.t("components.common.rules.outgoing.conditions.to"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwToSMTP, "=", "test@example.com"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwFromSMTP, | ||||
|     Label: i18n.global.t("components.common.rules.outgoing.conditions.from"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwFromSMTP, "=", "test@example.com"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwHeader, | ||||
|     Label: i18n.global.t("components.common.rules.outgoing.conditions.subject"), | ||||
|     KeyIdx: 1, | ||||
|     OpIdx: 2, | ||||
|     ValIdx: 3, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `^`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_starts" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `$`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_ends" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwHeader, "Subject", "=", "some subject"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwBody, | ||||
|     Label: i18n.global.t("components.common.rules.outgoing.conditions.body"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_contains" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_not_contains" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwBody, "=", "hello"], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwFromGroup, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.conditions.from_group" | ||||
|     ), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwFromGroup, "==", ""], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwToGroup, | ||||
|     Label: i18n.global.t( | ||||
|       "components.common.rules.outgoing.conditions.to_group" | ||||
|     ), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: 1, | ||||
|     ValIdx: 2, | ||||
|     OpList: [ | ||||
|       { | ||||
|         Val: `==`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_equal" | ||||
|         ), | ||||
|       }, | ||||
|       { | ||||
|         Val: `!=`, | ||||
|         Cname: i18n.global.t( | ||||
|           "components.common.rules.outgoing.conditions.op_not_equal" | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|     Args: [CondKwToGroup, "==", ""], | ||||
|   }, | ||||
|   { | ||||
|     Value: CondKwAll, | ||||
|     Label: i18n.global.t("components.common.rules.outgoing.conditions.all"), | ||||
|     KeyIdx: 0, | ||||
|     OpIdx: -1, | ||||
|     ValIdx: -1, | ||||
|     OpList: [], | ||||
|     Args: [CondKwAll], | ||||
|   }, | ||||
| ]; | ||||
| </script> | ||||
|  | @ -0,0 +1,176 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       size="small" | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-input-search | ||||
|           style="width: 300px" | ||||
|           v-model:value="page.pagination.search" | ||||
|           :placeholder="t('components.user.address_books.with_access.search')" | ||||
|           enter-button | ||||
|           allow-clear | ||||
|           @search="get" | ||||
|         /> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'perm'"> | ||||
|           <div v-if="record.Permissions === 'all'"> | ||||
|             {{ t("components.common.shared_address_books.access_perm_all") }} | ||||
|           </div> | ||||
|           <div v-if="record.Permissions === 'read'"> | ||||
|             {{ t("components.common.shared_address_books.access_perm_read") }} | ||||
|           </div> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'owner'"> | ||||
|           <a-space> | ||||
|             <template v-if="record.OwnerStatus === 'archived'"> | ||||
|               <a-tooltip | ||||
|                 :title=" | ||||
|                   t(`components.admin.domains.mailstorage.mailboxes.in_archive`) | ||||
|                 " | ||||
|               > | ||||
|                 <LockOutlined /> | ||||
|               </a-tooltip> | ||||
|             </template> | ||||
|             <template v-if="record.OwnerStatus === 'deleted'"> | ||||
|               <a-tooltip | ||||
|                 :title=" | ||||
|                   t(`components.admin.domains.mailstorage.mailboxes.deleted`) | ||||
|                 " | ||||
|               > | ||||
|                 <DeleteOutlined /> | ||||
|               </a-tooltip> | ||||
|             </template> | ||||
|             {{ record.Owner }} | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HddOutlined, | ||||
|   HomeOutlined, | ||||
|   LockOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.user.address_books.with_access.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.user.address_books.with_access.col_owner"), | ||||
|     dataIndex: "Owner", | ||||
|     key: "owner", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.user.address_books.with_access.col_permission"), | ||||
|     dataIndex: "Permissions", | ||||
|     key: "perm", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
| }>({ | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/user/address_books/with_access?` + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 700px; | ||||
|   max-width: 70%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,23 @@ | |||
| <template> | ||||
|   <AddressBooks v-if="page.loaded" :user="page.user"></AddressBooks> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import AddressBooks from "@/components/common/AddressBooks.vue"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import { useAuthStore } from "@/stores/auth"; | ||||
| 
 | ||||
| const route = useRoute(); | ||||
| const page = reactive<{ | ||||
|   user: string; | ||||
|   loaded: boolean; | ||||
| }>({ | ||||
|   user: "", | ||||
|   loaded: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.user = useAuthStore().username; | ||||
|   page.loaded = true; | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,176 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       size="small" | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-input-search | ||||
|           style="width: 300px" | ||||
|           v-model:value="page.pagination.search" | ||||
|           :placeholder="t('components.user.calendars.with_access.search')" | ||||
|           enter-button | ||||
|           allow-clear | ||||
|           @search="get" | ||||
|         /> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'perm'"> | ||||
|           <div v-if="record.Permissions === 'all'"> | ||||
|             {{ t("components.common.shared_calendars.access_perm_all") }} | ||||
|           </div> | ||||
|           <div v-if="record.Permissions === 'read'"> | ||||
|             {{ t("components.common.shared_calendars.access_perm_read") }} | ||||
|           </div> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'owner'"> | ||||
|           <a-space> | ||||
|             <template v-if="record.OwnerStatus === 'archived'"> | ||||
|               <a-tooltip | ||||
|                 :title=" | ||||
|                   t(`components.admin.domains.mailstorage.mailboxes.in_archive`) | ||||
|                 " | ||||
|               > | ||||
|                 <LockOutlined /> | ||||
|               </a-tooltip> | ||||
|             </template> | ||||
|             <template v-if="record.OwnerStatus === 'deleted'"> | ||||
|               <a-tooltip | ||||
|                 :title=" | ||||
|                   t(`components.admin.domains.mailstorage.mailboxes.deleted`) | ||||
|                 " | ||||
|               > | ||||
|                 <DeleteOutlined /> | ||||
|               </a-tooltip> | ||||
|             </template> | ||||
|             {{ record.Owner }} | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   HddOutlined, | ||||
|   HomeOutlined, | ||||
|   LockOutlined, | ||||
|   PlusOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.user.calendars.with_access.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.user.calendars.with_access.col_owner"), | ||||
|     dataIndex: "Owner", | ||||
|     key: "owner", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.user.calendars.with_access.col_permission"), | ||||
|     dataIndex: "Permissions", | ||||
|     key: "perm", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   loading: boolean; | ||||
|   data: any; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
| }>({ | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/user/calendars/with_access?` + params.toString() | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = res.data; | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 700px; | ||||
|   max-width: 70%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,360 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-table | ||||
|       class="panel-content" | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       size="small" | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-input-search | ||||
|                 style="width: 300px" | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder=" | ||||
|                   t('components.user.calendars.events_planner.my_events.search') | ||||
|                 " | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|               <a-range-picker | ||||
|                 style="width: 230px" | ||||
|                 @change="get" | ||||
|                 v-model:value="page.range" | ||||
|               > | ||||
|               </a-range-picker> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col :span="6"> | ||||
|             <a-space style="display: flex; justify-content: end"> | ||||
|               <a-button type="primary" @click="newEvent"> | ||||
|                 <PlusOutlined /> | ||||
|                 {{ | ||||
|                   t( | ||||
|                     "components.user.calendars.events_planner.my_events.new_event" | ||||
|                   ) | ||||
|                 }} | ||||
|               </a-button> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'start'"> | ||||
|           <template v-if="record.Start"> | ||||
|             {{ record.Start }} | ||||
|           </template> | ||||
|           <template v-else> | ||||
|             <a-space> | ||||
|               {{ | ||||
|                 record.ApprovalCreatedAt | ||||
|                   ? t( | ||||
|                       "components.user.calendars.events_planner.my_events.status_under_approval" | ||||
|                     ) | ||||
|                   : t( | ||||
|                       "components.user.calendars.events_planner.my_events.status_approval_done" | ||||
|                     ) | ||||
|               }} | ||||
|               <a-button shape="circle" @click="showApprovalStatus(record.Id)"> | ||||
|                 <EyeOutlined /> | ||||
|               </a-button> | ||||
|             </a-space> | ||||
|           </template> | ||||
|         </template> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-space> | ||||
|             <a-button @click="editEvent(record.Id)">{{ | ||||
|               t("common.misc.edit") | ||||
|             }}</a-button> | ||||
|             <a-popconfirm | ||||
|               :title="t('common.misc.delete') + '?'" | ||||
|               :ok-text="t('common.misc.ok')" | ||||
|               :cancel-text="t('common.misc.cancel')" | ||||
|               @confirm="deleteEvent(record.Id)" | ||||
|             > | ||||
|               <a-button danger>{{ t("common.misc.delete") }}</a-button> | ||||
|             </a-popconfirm> | ||||
|           </a-space> | ||||
|         </template> | ||||
|       </template> | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal v-model:open="page.approvalStatus.show"> | ||||
|     <template v-if="page.approvalStatus.approved.length !== 0"> | ||||
|       <a-divider orientation="left">{{ | ||||
|         $t( | ||||
|           "components.user.calendars.events_planner.my_events.approval_with_ft" | ||||
|         ) | ||||
|       }}</a-divider> | ||||
|       <a-list size="small" bordered :data-source="page.approvalStatus.approved"> | ||||
|         <template #renderItem="{ item }"> | ||||
|           <a-list-item>{{ item }}</a-list-item> | ||||
|         </template> | ||||
|       </a-list> | ||||
|     </template> | ||||
|     <template v-if="page.approvalStatus.waiting.length !== 0"> | ||||
|       <a-divider orientation="left">{{ | ||||
|         $t( | ||||
|           "components.user.calendars.events_planner.my_events.approval_without_ft" | ||||
|         ) | ||||
|       }}</a-divider> | ||||
|       <a-list size="small" bordered :data-source="page.approvalStatus.waiting"> | ||||
|         <template #renderItem="{ item }"> | ||||
|           <a-list-item>{{ item }}</a-list-item> | ||||
|         </template> | ||||
|       </a-list> | ||||
|     </template> | ||||
| 
 | ||||
|     <template #footer> </template> | ||||
|   </a-modal> | ||||
|   <ScriptErrorNotify | ||||
|     v-model:open="page.scriptErrorNotify.show" | ||||
|     :data="page.scriptErrorNotify.data" | ||||
|   ></ScriptErrorNotify> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import router from "@/router"; | ||||
| import dayjs, { Dayjs } from "dayjs"; | ||||
| 
 | ||||
| import Export from "@/components/admin/domains/mailstorage/Export.vue"; | ||||
| import { | ||||
|   DomainPlaceholder, | ||||
|   EventIdPlaceholder, | ||||
|   MailboxPlaceholder, | ||||
|   RouteAdminDomainsDomainMailStorageMailboxesExport, | ||||
|   RouteUserCalendarsEventsPlannerEdit, | ||||
|   RouteUserCalendarsEventsPlannerNew, | ||||
| } from "@/router/consts"; | ||||
| import { | ||||
|   ArrowLeftOutlined, | ||||
|   DeleteOutlined, | ||||
|   DownOutlined, | ||||
|   ExclamationCircleOutlined, | ||||
|   EyeOutlined, | ||||
|   HddOutlined, | ||||
|   HomeOutlined, | ||||
|   PlusOutlined, | ||||
|   QuestionOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| import { createVNode, onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import { timeToDateTime } from "@/composables/misc"; | ||||
| const route = useRoute(); | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.user.calendars.events_planner.my_events.col_name"), | ||||
|     dataIndex: "Subject", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.user.calendars.events_planner.my_events.col_start"), | ||||
|     dataIndex: "Start", | ||||
|     key: "start", | ||||
|   }, | ||||
|   { | ||||
|     title: t("components.user.calendars.events_planner.my_events.col_finish"), | ||||
|     dataIndex: "Finish", | ||||
|   }, | ||||
|   { | ||||
|     title: t( | ||||
|       "components.user.calendars.events_planner.my_events.col_participants" | ||||
|     ), | ||||
|     dataIndex: "Participants", | ||||
|   }, | ||||
|   // { | ||||
|   //   title: t("components.user.calendars.events_planner.my_events.col_status"), | ||||
|   //   dataIndex: "Status", | ||||
|   //   key: "status", | ||||
|   // }, | ||||
| 
 | ||||
|   { | ||||
|     title: "", | ||||
|     key: "action", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const defaultFrom = new Date(); | ||||
| defaultFrom.setHours(0, 0, 0, 0); | ||||
| const defaultTo = new Date().setDate(defaultFrom.getDate() + 7); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   loading: boolean; | ||||
|   data: any[]; | ||||
|   range: [any, any]; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   approvalStatus: { | ||||
|     show: boolean; | ||||
|     waiting: string[]; | ||||
|     approved: string[]; | ||||
|   }; | ||||
|   scriptErrorNotify: { | ||||
|     show: boolean; | ||||
|     data: any; | ||||
|   }; | ||||
| }>({ | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   loading: false, | ||||
|   data: [], | ||||
|   range: [dayjs(defaultFrom), dayjs(defaultTo)], | ||||
|   approvalStatus: { | ||||
|     show: false, | ||||
|     waiting: [], | ||||
|     approved: [], | ||||
|   }, | ||||
|   scriptErrorNotify: { | ||||
|     show: false, | ||||
|     data: undefined, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   page.loading = true; | ||||
| 
 | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("from", (page.range[0] as Dayjs).toISOString()); | ||||
|   params.append("to", (page.range[1] as Dayjs).toISOString()); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch(`/user/calendars/events?` + params.toString()); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = []; | ||||
|   if (res.data) { | ||||
|     (res.data as Array<any>).forEach((el: any) => { | ||||
|       el.Participants = | ||||
|         (el.MandatoryParticipants as Array<any>).length + | ||||
|         (el.OptionalParticipants as Array<any>).length; | ||||
|       if (el.IncludeCreator) { | ||||
|         el.Participants += 1; | ||||
|       } | ||||
|       if (el.Start) { | ||||
|         el.Finish = timeToDateTime( | ||||
|           addMinutes(new Date(el.Start), el.DurationMins) | ||||
|         ); | ||||
|         el.Start = timeToDateTime(el.Start); | ||||
|       } | ||||
|       page.data.push(el); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function addMinutes(date: Date, minutes: number): Date { | ||||
|   return new Date(date.getTime() + minutes * 60000); | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function deleteEvent(id: number) { | ||||
|   const res = await apiFetch(`/user/calendars/events/${id}`, { | ||||
|     method: "DELETE", | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (res.data) { | ||||
|     page.scriptErrorNotify.data = res.data; | ||||
|     page.scriptErrorNotify.show = true; | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function editEvent(id: number) { | ||||
|   router.push({ | ||||
|     path: RouteUserCalendarsEventsPlannerEdit.replace( | ||||
|       EventIdPlaceholder, | ||||
|       id.toString() | ||||
|     ), | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function newEvent() { | ||||
|   router.push({ | ||||
|     path: RouteUserCalendarsEventsPlannerNew, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function showApprovalStatus(eventId: number) { | ||||
|   page.approvalStatus.approved = []; | ||||
|   page.approvalStatus.waiting = []; | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/user/calendars/events/${eventId}/approval_status` | ||||
|   ); | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.approvalStatus.approved = res.data.approved; | ||||
|   page.approvalStatus.waiting = res.data.waiting; | ||||
|   page.approvalStatus.show = true; | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 700px; | ||||
|   max-width: 70%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,90 @@ | |||
| <template> | ||||
|   <a-modal v-model:open="open" width="800px"> | ||||
|     <a-descriptions | ||||
|       :column="1" | ||||
|       :labelStyle="{ fontWeight: '600' }" | ||||
|       bordered | ||||
|       size="small" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-space> | ||||
|           <ExclamationCircleTwoTone | ||||
|             two-tone-color="orangered" | ||||
|             style="font-size: x-large" | ||||
|           /> | ||||
|           <div style="font-size: large"> | ||||
|             {{ | ||||
|               $t( | ||||
|                 "components.user.calendars.events_planner.script_error_notify.title" | ||||
|               ) | ||||
|             }} | ||||
|           </div> | ||||
|         </a-space> | ||||
|       </template> | ||||
|       <a-descriptions-item | ||||
|         :label=" | ||||
|           $t( | ||||
|             `components.user.calendars.events_planner.script_error_notify.resource_name` | ||||
|           ) | ||||
|         " | ||||
|         >{{ props.data.ResourceName }}</a-descriptions-item | ||||
|       > | ||||
|       <a-descriptions-item | ||||
|         :label=" | ||||
|           $t( | ||||
|             `components.user.calendars.events_planner.script_error_notify.error` | ||||
|           ) | ||||
|         " | ||||
|       > | ||||
|         <div style="color: orangered">{{ props.data.Error }}</div> | ||||
|       </a-descriptions-item> | ||||
|       <a-descriptions-item | ||||
|         :label=" | ||||
|           $t( | ||||
|             `components.user.calendars.events_planner.script_error_notify.script` | ||||
|           ) | ||||
|         " | ||||
|       > | ||||
|         <a-textarea v-model:value="props.data.Script" :rows="13"></a-textarea> | ||||
|       </a-descriptions-item> | ||||
|       <a-descriptions-item | ||||
|         :label=" | ||||
|           $t( | ||||
|             `components.user.calendars.events_planner.script_error_notify.output` | ||||
|           ) | ||||
|         " | ||||
|       > | ||||
|         <a-textarea v-model:value="props.data.Output" :rows="3"></a-textarea> | ||||
|       </a-descriptions-item> | ||||
|     </a-descriptions> | ||||
|     <template #footer></template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { | ||||
|   CloseCircleFilled, | ||||
|   ExclamationCircleFilled, | ||||
|   ExclamationCircleTwoTone, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { onMounted, onUnmounted, reactive } from "vue"; | ||||
| 
 | ||||
| interface ScriptErrorMeta { | ||||
|   ResourceName: string; | ||||
|   Error: string; | ||||
|   Script: string; | ||||
|   Output: string; | ||||
| } | ||||
| 
 | ||||
| const open = defineModel("open"); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   data: ScriptErrorMeta; | ||||
| }>(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   meta?: ScriptErrorMeta; | ||||
| }>({ | ||||
|   meta: undefined, | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,23 @@ | |||
| <template> | ||||
|   <Calendars v-if="page.loaded" :user="page.user"></Calendars> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import Calendars from "@/components/common/Calendars.vue"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import { useAuthStore } from "@/stores/auth"; | ||||
| 
 | ||||
| const route = useRoute(); | ||||
| const page = reactive<{ | ||||
|   user: string; | ||||
|   loaded: boolean; | ||||
| }>({ | ||||
|   user: "", | ||||
|   loaded: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.user = useAuthStore().username; | ||||
|   page.loaded = true; | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,255 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <div v-if="page.contentLoaded"> | ||||
|         <a-form | ||||
|           v-if="page.contentLoaded" | ||||
|           :label-col="labelCol" | ||||
|           :wrapper-col="wrapperCol" | ||||
|           :labelWrap="true" | ||||
|         > | ||||
|           <a-form-item | ||||
|             :label="t('components.user.calendars.share_free_time.timezone')" | ||||
|           > | ||||
|             <TimezoneSelect | ||||
|               v-model:value="page.settings.Timezone" | ||||
|               @change="updateSettings" | ||||
|             > | ||||
|             </TimezoneSelect> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label="t('components.user.calendars.share_free_time.work_days')" | ||||
|           > | ||||
|             <WorkDaysSelect | ||||
|               @change="updateSettings" | ||||
|               v-model:value="page.settings.WorkDays" | ||||
|             > | ||||
|             </WorkDaysSelect> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label="t('components.user.calendars.share_free_time.work_hours')" | ||||
|           > | ||||
|             <WorkHoursRangePicker | ||||
|               @change="updateSettings" | ||||
|               v-model:value="page.settings.workTimeValues" | ||||
|             > | ||||
|             </WorkHoursRangePicker> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               t('components.user.calendars.share_free_time.time_between_events') | ||||
|             " | ||||
|           > | ||||
|             <a-select | ||||
|               @change="updateSettings" | ||||
|               v-model:value="page.settings.timeBetweenEventsMinsValue" | ||||
|             > | ||||
|               <a-select-option value="0" | ||||
|                 >0 {{ t("common.suffixes.min") }}</a-select-option | ||||
|               > | ||||
|               <a-select-option value="5" | ||||
|                 >5 {{ t("common.suffixes.min") }}</a-select-option | ||||
|               > | ||||
|               <a-select-option value="10" | ||||
|                 >10 {{ t("common.suffixes.min") }}</a-select-option | ||||
|               > | ||||
|               <a-select-option value="15" | ||||
|                 >15 {{ t("common.suffixes.min") }}</a-select-option | ||||
|               > | ||||
|               <a-select-option value="30" | ||||
|                 >30 {{ t("common.suffixes.min") }}</a-select-option | ||||
|               > | ||||
|               <a-select-option value="60" | ||||
|                 >60 {{ t("common.suffixes.min") }}</a-select-option | ||||
|               > | ||||
|             </a-select> | ||||
|           </a-form-item> | ||||
|           <a-form-item | ||||
|             :label=" | ||||
|               t( | ||||
|                 'components.user.calendars.share_free_time.two_month_restriction' | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             <a-switch | ||||
|               @change="updateSettings" | ||||
|               v-model:checked="page.settings.TwoMonthRestriction" | ||||
|             > | ||||
|             </a-switch> | ||||
|           </a-form-item> | ||||
|         </a-form> | ||||
| 
 | ||||
|         <a-divider></a-divider> | ||||
|         <div> | ||||
|           <a-row :gutter="16"> | ||||
|             <a-col flex="auto"> | ||||
|               <a-input-search :disabled="!page.link" v-model:value="page.link"> | ||||
|                 <template #enterButton> | ||||
|                   <a-button | ||||
|                     :disabled="!page.link" | ||||
|                     @click="copyToClipboard(page.link)" | ||||
|                   > | ||||
|                     <CopyOutlined /> | ||||
|                   </a-button> | ||||
|                 </template> </a-input-search | ||||
|             ></a-col> | ||||
|             <a-col flex="100px"> | ||||
|               <a-button | ||||
|                 type="primary" | ||||
|                 @click="getToken" | ||||
|                 :loading="page.linkLoading" | ||||
|               > | ||||
|                 {{ $t("components.user.calendars.share_free_time.gen_link") }} | ||||
|               </a-button></a-col | ||||
|             > | ||||
|           </a-row> | ||||
|         </div> | ||||
|       </div> | ||||
|       <a-spin | ||||
|         v-else | ||||
|         size="large" | ||||
|         style="display: flex; justify-content: center" | ||||
|       ></a-spin> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import dayjs from "dayjs"; | ||||
| import { copyToClipboard } from "@/composables/misc"; | ||||
| import { | ||||
|   CopyOutlined, | ||||
|   LockOutlined, | ||||
|   RetweetOutlined, | ||||
| } from "@ant-design/icons-vue"; | ||||
| import { RouteSharedFreeTime, TokenPlaceholder } from "@/router/consts"; | ||||
| import TimezoneSelect from "../../common/TimezoneSelect.vue"; | ||||
| import WorkDaysSelect from "../../common/WorkDaysSelect.vue"; | ||||
| import WorkHoursRangePicker from "../../common/WorkHoursRangePicker.vue"; | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 14, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 10, style: {} }; | ||||
| 
 | ||||
| interface Settings { | ||||
|   Timezone: string; | ||||
|   WorkDays: string[]; | ||||
|   WorkTime: string[]; | ||||
|   workTimeValues: any[]; | ||||
|   TimeBetweenEventsMins: number; | ||||
|   timeBetweenEventsMinsValue: string; | ||||
|   TwoMonthRestriction: boolean; | ||||
|   Token: string; | ||||
|   Hostname: string; | ||||
| } | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   contentLoaded: boolean; | ||||
|   settings: Settings; | ||||
|   link: string; | ||||
|   linkLoading: boolean; | ||||
| }>({ | ||||
|   contentLoaded: false, | ||||
|   settings: { | ||||
|     TwoMonthRestriction: true, | ||||
|     Timezone: "+03", | ||||
|     WorkTime: [], | ||||
|     workTimeValues: [ | ||||
|       dayjs(Date.parse("1970-01-01 10:00")), | ||||
|       dayjs(Date.parse("1970-01-01 19:00")), | ||||
|     ], | ||||
|     WorkDays: ["1", "2", "3", "4", "5"], | ||||
|     TimeBetweenEventsMins: 0, | ||||
|     timeBetweenEventsMinsValue: "0", | ||||
|     Token: "", | ||||
|     Hostname: "", | ||||
|   }, | ||||
|   link: "", | ||||
|   linkLoading: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   getSettings(); | ||||
| }); | ||||
| 
 | ||||
| async function getSettings() { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("timezone", String(new Date().getTimezoneOffset())); | ||||
| 
 | ||||
|   const res = await apiFetch("/user/share_free_time?" + params.toString()); | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.contentLoaded = true; | ||||
|   page.settings = res.data; | ||||
|   page.settings.timeBetweenEventsMinsValue = String( | ||||
|     page.settings.TimeBetweenEventsMins | ||||
|   ); | ||||
|   page.settings.workTimeValues = [ | ||||
|     dayjs(Date.parse("1970-01-01 " + page.settings.WorkTime[0])), | ||||
|     dayjs(Date.parse("1970-01-01 " + page.settings.WorkTime[1])), | ||||
|   ]; | ||||
|   page.link = generateLink(page.settings.Hostname, page.settings.Token); | ||||
| } | ||||
| 
 | ||||
| async function updateSettings() { | ||||
|   page.settings.TimeBetweenEventsMins = Number( | ||||
|     page.settings.timeBetweenEventsMinsValue | ||||
|   ); | ||||
|   page.settings.WorkTime[0] = page.settings.workTimeValues[0].format("HH:mm"); | ||||
|   page.settings.WorkTime[1] = page.settings.workTimeValues[1].format("HH:mm"); | ||||
| 
 | ||||
|   const res = await apiFetch("/user/share_free_time", { | ||||
|     method: "POST", | ||||
|     body: page.settings, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function getToken() { | ||||
|   page.linkLoading = true; | ||||
|   const res = await apiFetch("/user/share_free_time/refresh", { | ||||
|     method: "POST", | ||||
|   }); | ||||
| 
 | ||||
|   page.linkLoading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.link = generateLink(res.data.Hostname, res.data.Token); | ||||
| 
 | ||||
|   notifySuccess( | ||||
|     t("components.user.calendars.share_free_time.gen_link_success") | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function generateLink(hostname: string, token: string): string { | ||||
|   // let schema = location.href.substring(0, location.href.indexOf(location.host)); | ||||
| 
 | ||||
|   return ( | ||||
|     // schema + | ||||
|     // location.host + | ||||
|     hostname + RouteSharedFreeTime.replace(TokenPlaceholder, token) | ||||
|   ); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 700px; | ||||
|   max-width: 25%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,55 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <Rules | ||||
|       :get-get-url="getUrl" | ||||
|       :get-delete-url="getUrl" | ||||
|       :get-save-url="getUrl" | ||||
|       :get-move-url="moveUrl" | ||||
|       :get-enable-url="enableUrl" | ||||
|       :conditions="conditionsList" | ||||
|       :actions="actionsList" | ||||
|     > | ||||
|       <template #condition-value="valueProps"> | ||||
|         <ConditionValue | ||||
|           :change="valueProps.change" | ||||
|           :value="valueProps.value" | ||||
|           :type="valueProps.type" | ||||
|         > | ||||
|         </ConditionValue> | ||||
|       </template> | ||||
|       <template #action-value="valueProps"> | ||||
|         <ActionValue | ||||
|           :change="valueProps.change" | ||||
|           :value="valueProps.value" | ||||
|           :type="valueProps.type" | ||||
|           :get-folders-url="foldersUrl" | ||||
|         > | ||||
|         </ActionValue> | ||||
|       </template> | ||||
|     </Rules> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import ConditionValue from "@/components/common/rules/incoming/Conditions.vue"; | ||||
| import ActionValue from "@/components/common/rules/incoming/Actions.vue"; | ||||
| import { conditionsList } from "@/components/common/rules/incoming/Conditions.vue"; | ||||
| import { actionsList } from "@/components/common/rules/incoming/Actions.vue"; | ||||
| import Rules from "@/components/common/rules/Rules.vue"; | ||||
| 
 | ||||
| function getUrl(): string { | ||||
|   return `/user/incoming_rules`; | ||||
| } | ||||
| 
 | ||||
| function moveUrl(): string { | ||||
|   return `/user/incoming_rules/move`; | ||||
| } | ||||
| 
 | ||||
| function foldersUrl(): string { | ||||
|   return `/user/incoming_rules/folders`; | ||||
| } | ||||
| 
 | ||||
| function enableUrl(): string { | ||||
|   return `/user/incoming_rules/enable`; | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,345 @@ | |||
| <template> | ||||
|   <div class="panel-content"> | ||||
|     <a-table | ||||
|       :columns="columns" | ||||
|       :data-source="page.data" | ||||
|       :loading="page.loading" | ||||
|       bordered | ||||
|       size="small" | ||||
|       :pagination="false" | ||||
|     > | ||||
|       <template #title> | ||||
|         <a-row> | ||||
|           <a-col style="display: flex; align-items: flex-end" :span="18"> | ||||
|             <a-space> | ||||
|               <a-radio-group @change="get()" v-model:value="page.type"> | ||||
|                 <a-radio-button value="email">{{ | ||||
|                   t("components.user.mailbox_shared_access.type_email") | ||||
|                 }}</a-radio-button> | ||||
|                 <a-radio-button value="group">{{ | ||||
|                   t("components.user.mailbox_shared_access.type_group") | ||||
|                 }}</a-radio-button> | ||||
|               </a-radio-group> | ||||
| 
 | ||||
|               <a-input-search | ||||
|                 v-model:value="page.pagination.search" | ||||
|                 :placeholder="t('components.user.mailbox_shared_access.search')" | ||||
|                 enter-button | ||||
|                 allow-clear | ||||
|                 @search="get" | ||||
|               /> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|           <a-col | ||||
|             style="display: flex; align-items: flex-end; justify-content: end" | ||||
|             :span="6" | ||||
|           > | ||||
|             <a-space> | ||||
|               <a-button | ||||
|                 v-if="page.type === 'email'" | ||||
|                 type="primary" | ||||
|                 @click="page.showAdd = true" | ||||
|               > | ||||
|                 {{ t("components.user.mailbox_shared_access.type_email_add") }} | ||||
|               </a-button> | ||||
|               <a-button | ||||
|                 v-if="page.type === 'group'" | ||||
|                 type="primary" | ||||
|                 @click="page.showAdd = true" | ||||
|               > | ||||
|                 {{ t("components.user.mailbox_shared_access.type_group_add") }} | ||||
|               </a-button> | ||||
|               <a-popconfirm | ||||
|                 v-if="page.withSearch" | ||||
|                 :title=" | ||||
|                   t('components.user.mailbox_shared_access.delete_found') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.user.mailbox_shared_access.ok')" | ||||
|                 :cancel-text="t('components.user.mailbox_shared_access.cancel')" | ||||
|                 @confirm="deleteAccess('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.user.mailbox_shared_access.delete_found") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|               <a-popconfirm | ||||
|                 v-else | ||||
|                 :title=" | ||||
|                   t('components.user.mailbox_shared_access.delete_all') + '?' | ||||
|                 " | ||||
|                 :ok-text="t('components.user.mailbox_shared_access.ok')" | ||||
|                 :cancel-text="t('components.user.mailbox_shared_access.cancel')" | ||||
|                 @confirm="deleteAccess('')" | ||||
|               > | ||||
|                 <a-button danger>{{ | ||||
|                   $t("components.user.mailbox_shared_access.delete_all") | ||||
|                 }}</a-button> | ||||
|               </a-popconfirm> | ||||
|             </a-space> | ||||
|           </a-col> | ||||
|         </a-row> | ||||
|       </template> | ||||
|       <template #bodyCell="{ column, record }"> | ||||
|         <template v-if="column.key === 'action'"> | ||||
|           <a-popconfirm | ||||
|             :title="t('components.user.mailbox_shared_access.delete') + '?'" | ||||
|             :ok-text="t('components.user.mailbox_shared_access.ok')" | ||||
|             :cancel-text="t('components.user.mailbox_shared_access.cancel')" | ||||
|             @confirm="deleteAccess(record.Name)" | ||||
|           > | ||||
|             <a-button danger>{{ | ||||
|               t("components.user.mailbox_shared_access.delete") | ||||
|             }}</a-button> | ||||
|           </a-popconfirm> | ||||
|         </template> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #footer> | ||||
|         <a-flex justify="flex-end"> | ||||
|           <a-pagination | ||||
|             v-model:value="page.pagination.current" | ||||
|             :defaultPageSize="page.pagination.size" | ||||
|             :total="page.pagination.total" | ||||
|             @change="pageChange" | ||||
|             :pageSizeOptions="['10', '20', '50', '100']" | ||||
|             v-model:pageSize="page.pagination.size" | ||||
|             :showSizeChanger="true" | ||||
|           /> | ||||
|         </a-flex> | ||||
|       </template> | ||||
|     </a-table> | ||||
|   </div> | ||||
|   <a-modal | ||||
|     style="width: 300px" | ||||
|     v-model:open="page.showAdd" | ||||
|     :title=" | ||||
|       page.type === 'email' | ||||
|         ? t('components.user.mailbox_shared_access.type_email_add') | ||||
|         : t('components.user.mailbox_shared_access.type_group_add') | ||||
|     " | ||||
|   > | ||||
|     <div style="margin-top: 16px"> | ||||
|       <template v-if="page.type === 'email'"> | ||||
|         <a-input | ||||
|           v-model:value="page.inputValue" | ||||
|           :placeholder=" | ||||
|             t('components.user.mailbox_shared_access.type_email_placeholder') | ||||
|           " | ||||
|         > | ||||
|         </a-input> | ||||
|       </template> | ||||
|       <template v-if="page.type === 'group'"> | ||||
|         <a-select | ||||
|           style="width: 100%" | ||||
|           :disabled="page.groups.length === 0" | ||||
|           v-model:value="page.selectValue" | ||||
|           :options="page.groups" | ||||
|         > | ||||
|         </a-select> | ||||
|       </template> | ||||
|     </div> | ||||
|     <template #footer> | ||||
|       <a-button | ||||
|         :disabled=" | ||||
|           (page.type === 'email' && page.inputValue === '') || | ||||
|           (page.type === 'group' && page.selectValue === '') | ||||
|         " | ||||
|         type="primary" | ||||
|         @click="add(page.type === 'email' ? page.inputValue : page.selectValue)" | ||||
|       > | ||||
|         {{ t("components.user.mailbox_shared_access.add") }} | ||||
|       </a-button> | ||||
|     </template> | ||||
|   </a-modal> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { notifyError } from "@/composables/alert"; | ||||
| import type { ColumnType } from "ant-design-vue/es/table"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const columns: ColumnType<any>[] = [ | ||||
|   { | ||||
|     title: t("components.user.mailbox_shared_access.col_name"), | ||||
|     dataIndex: "Name", | ||||
|   }, | ||||
| 
 | ||||
|   { | ||||
|     key: "action", | ||||
|     align: "right", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   inputValue: string; | ||||
|   selectValue: string; | ||||
|   type: string; | ||||
|   data: any[]; | ||||
|   loading: boolean; | ||||
|   show: boolean; | ||||
|   Name: string; | ||||
|   groups: any[]; | ||||
|   pagination: { | ||||
|     current: number; | ||||
|     total: number; | ||||
|     size: number; | ||||
|     search: string; | ||||
|   }; | ||||
|   showAdd: boolean; | ||||
|   withSearch: boolean; | ||||
|   all_groups: string[]; | ||||
| }>({ | ||||
|   inputValue: "", | ||||
|   selectValue: "", | ||||
|   type: "email", | ||||
|   data: [], | ||||
|   loading: false, | ||||
|   show: false, | ||||
|   Name: "", | ||||
|   groups: [], | ||||
|   pagination: { | ||||
|     current: 1, | ||||
|     total: 0, | ||||
|     size: 50, | ||||
|     search: "", | ||||
|   }, | ||||
|   showAdd: false, | ||||
|   withSearch: false, | ||||
|   all_groups: [], | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   getGroups(); | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function getGroups() { | ||||
|   const res = await apiFetch(`/user/mailbox_shared_access/groups`); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.all_groups = res.data; | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| function pageChange(current: number) { | ||||
|   page.pagination.current = current; | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function get() { | ||||
|   page.withSearch = page.pagination.search !== ""; | ||||
|   page.loading = true; | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("type", page.type); | ||||
|   params.append("page", String(page.pagination.current)); | ||||
|   params.append("size", String(page.pagination.size)); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/user/mailbox_shared_access?` + params.toString() | ||||
|   ); | ||||
| 
 | ||||
|   page.loading = false; | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.data = []; | ||||
| 
 | ||||
|   if (res.data) { | ||||
|     for (let index = 0; index < res.data.length; index++) { | ||||
|       const element = res.data[index]; | ||||
|       page.data.push({ Name: element }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   page.pagination.total = res.total; | ||||
| 
 | ||||
|   page.groups = []; | ||||
|   page.selectValue = ""; | ||||
| 
 | ||||
|   if (page.type === "group") { | ||||
|     for (let j = 0; j < page.all_groups.length; j++) { | ||||
|       const group = page.all_groups[j]; | ||||
|       let skip = false; | ||||
|       for (let i = 0; i < page.data.length; i++) { | ||||
|         const alreadyHas = page.data[i]; | ||||
|         if (alreadyHas.Name === group) { | ||||
|           skip = true; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (!skip) { | ||||
|         page.groups.push({ value: group }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (page.groups.length) { | ||||
|     page.selectValue = page.groups[0].value; | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| async function deleteAccess(access: string) { | ||||
|   const params = new URLSearchParams(); | ||||
|   params.append("search", page.pagination.search); | ||||
| 
 | ||||
|   const res = await apiFetch( | ||||
|     `/user/mailbox_shared_access?` + params.toString(), | ||||
|     { | ||||
|       method: "DELETE", | ||||
|       body: { | ||||
|         AccessTo: access, | ||||
|         Type: page.type, | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| 
 | ||||
| async function add(access: string) { | ||||
|   const res = await apiFetch(`/user/mailbox_shared_access`, { | ||||
|     method: "POST", | ||||
|     body: { | ||||
|       AccessTo: access, | ||||
|       Type: page.type, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.showAdd = false; | ||||
| 
 | ||||
|   return get(); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 800px; | ||||
|   max-width: 60%; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,277 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-row :gutter="[16, 16]" justify="space-around"> | ||||
|       <a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12"> | ||||
|         <a-card hoverable style="height: 100%"> | ||||
|           <template #title> | ||||
|             {{ t("components.user.profile.mailbox.title") }} | ||||
|           </template> | ||||
| 
 | ||||
|           <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.mailbox.user')" | ||||
|               >{{ page.mailboxBlock.User }}</a-descriptions-item | ||||
|             > | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.mailbox.size')" | ||||
|               >{{ page.mailboxBlock.Size }}</a-descriptions-item | ||||
|             > | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.mailbox.count')" | ||||
|               >{{ page.mailboxBlock.Count }}</a-descriptions-item | ||||
|             > | ||||
|           </a-descriptions> | ||||
|         </a-card> | ||||
|       </a-col> | ||||
|       <a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12"> | ||||
|         <a-card hoverable style="height: 100%"> | ||||
|           <template #title> | ||||
|             {{ t("components.user.profile.dav.title") }} | ||||
|           </template> | ||||
| 
 | ||||
|           <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.dav.books')" | ||||
|             > | ||||
|               <a-space> | ||||
|                 {{ page.davBlock.BooksUrl }} | ||||
|                 <a-button | ||||
|                   v-if="page.davBlock.BooksUrl.startsWith('http')" | ||||
|                   @click="copyToClipboard(page.davBlock.BooksUrl)" | ||||
|                   size="small" | ||||
|                 > | ||||
|                   <CopyOutlined /> | ||||
|                 </a-button> | ||||
|               </a-space> | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.dav.calendars')" | ||||
|             > | ||||
|               <a-space> | ||||
|                 {{ page.davBlock.CalsUrl }} | ||||
|                 <a-button | ||||
|                   v-if="page.davBlock.CalsUrl.startsWith('http')" | ||||
|                   @click="copyToClipboard(page.davBlock.CalsUrl)" | ||||
|                   size="small" | ||||
|                 > | ||||
|                   <CopyOutlined /> | ||||
|                 </a-button> | ||||
|               </a-space> | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.dav.login')" | ||||
|             > | ||||
|               <a-space> | ||||
|                 {{ page.davBlock.Login }} | ||||
|                 <a-button | ||||
|                   @click="copyToClipboard(page.davBlock.Login)" | ||||
|                   size="small" | ||||
|                 > | ||||
|                   <CopyOutlined /> | ||||
|                 </a-button> | ||||
|               </a-space> | ||||
|             </a-descriptions-item> | ||||
|           </a-descriptions> | ||||
|         </a-card> | ||||
|       </a-col> | ||||
|     </a-row> | ||||
|     <br /> | ||||
|     <a-row :gutter="[16, 16]" justify="space-around"> | ||||
|       <a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12"> | ||||
|         <a-card hoverable style="height: 100%"> | ||||
|           <template #title> | ||||
|             {{ t("components.user.profile.imap.title") }} | ||||
|           </template> | ||||
| 
 | ||||
|           <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.imap.server')" | ||||
|             > | ||||
|               <a-space> | ||||
|                 {{ page.imapBlock.Server }} | ||||
|                 <a-button | ||||
|                   @click="copyToClipboard(page.imapBlock.Server)" | ||||
|                   size="small" | ||||
|                 > | ||||
|                   <CopyOutlined /> | ||||
|                 </a-button> | ||||
|               </a-space> | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.imap.port')" | ||||
|             > | ||||
|               <a-space> | ||||
|                 {{ page.imapBlock.Port }} | ||||
|                 <a-button | ||||
|                   @click="copyToClipboard(page.imapBlock.Port.toString())" | ||||
|                   size="small" | ||||
|                 > | ||||
|                   <CopyOutlined /> | ||||
|                 </a-button> | ||||
|               </a-space> | ||||
|             </a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.imap.encryption')" | ||||
|               >{{ page.imapBlock.SSL }}</a-descriptions-item | ||||
|             > | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.imap.auth')" | ||||
|               >{{ | ||||
|                 t("components.user.profile.imap.auth_text") | ||||
|               }}</a-descriptions-item | ||||
|             > | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.imap.auth_login')" | ||||
|             > | ||||
|               <a-space> | ||||
|                 {{ page.imapBlock.Login }} | ||||
|                 <a-button | ||||
|                   @click="copyToClipboard(page.imapBlock.Login)" | ||||
|                   size="small" | ||||
|                 > | ||||
|                   <CopyOutlined /> | ||||
|                 </a-button> | ||||
|               </a-space> | ||||
|             </a-descriptions-item> | ||||
|           </a-descriptions> | ||||
|         </a-card> | ||||
|       </a-col> | ||||
|       <a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12"> | ||||
|         <a-card hoverable style="height: 100%"> | ||||
|           <template #title> | ||||
|             {{ t("components.user.profile.smtp.title") }} | ||||
|           </template> | ||||
| 
 | ||||
|           <a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }"> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.smtp.server')" | ||||
|             > | ||||
|               <a-space> | ||||
|                 {{ page.smtpBlock.Server }} | ||||
|                 <a-button | ||||
|                   @click="copyToClipboard(page.smtpBlock.Server)" | ||||
|                   size="small" | ||||
|                 > | ||||
|                   <CopyOutlined /> | ||||
|                 </a-button> </a-space | ||||
|             ></a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.smtp.port')" | ||||
|             > | ||||
|               <a-space> | ||||
|                 {{ page.smtpBlock.Port }} | ||||
|                 <a-button | ||||
|                   @click="copyToClipboard(page.smtpBlock.Port.toString())" | ||||
|                   size="small" | ||||
|                 > | ||||
|                   <CopyOutlined /> | ||||
|                 </a-button> </a-space | ||||
|             ></a-descriptions-item> | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.smtp.encryption')" | ||||
|               >{{ page.smtpBlock.SSL }}</a-descriptions-item | ||||
|             > | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.smtp.auth')" | ||||
|               >{{ | ||||
|                 t("components.user.profile.smtp.auth_text") | ||||
|               }}</a-descriptions-item | ||||
|             > | ||||
|             <a-descriptions-item | ||||
|               :label="t('components.user.profile.smtp.auth_login')" | ||||
|             > | ||||
|               <a-space> | ||||
|                 {{ page.smtpBlock.Login }} | ||||
|                 <a-button | ||||
|                   @click="copyToClipboard(page.smtpBlock.Login)" | ||||
|                   size="small" | ||||
|                 > | ||||
|                   <CopyOutlined /> | ||||
|                 </a-button> </a-space | ||||
|             ></a-descriptions-item> | ||||
|           </a-descriptions> | ||||
|         </a-card> | ||||
|       </a-col> | ||||
|     </a-row> | ||||
|     <br /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { copyToClipboard } from "@/composables/misc"; | ||||
| import { CopyOutlined } from "@ant-design/icons-vue"; | ||||
| 
 | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   contentLoaded: boolean; | ||||
|   mailboxBlock: { | ||||
|     Count: number; | ||||
|     Size: string; | ||||
|     User: string; | ||||
|   }; | ||||
|   davBlock: { | ||||
|     BooksUrl: string; | ||||
|     CalsUrl: string; | ||||
|     Login: string; | ||||
|   }; | ||||
|   imapBlock: { | ||||
|     Server: string; | ||||
|     Port: number; | ||||
|     SSL: string; | ||||
|     Login: string; | ||||
|   }; | ||||
|   smtpBlock: { | ||||
|     Server: string; | ||||
|     Port: number; | ||||
|     SSL: string; | ||||
|     Login: string; | ||||
|   }; | ||||
| }>({ | ||||
|   contentLoaded: false, | ||||
|   mailboxBlock: { | ||||
|     Count: 0, | ||||
|     Size: "", | ||||
|     User: "", | ||||
|   }, | ||||
|   davBlock: { | ||||
|     BooksUrl: "", | ||||
|     CalsUrl: "", | ||||
|     Login: "", | ||||
|   }, | ||||
|   imapBlock: { | ||||
|     Server: "", | ||||
|     Port: 0, | ||||
|     SSL: "", | ||||
|     Login: "", | ||||
|   }, | ||||
|   smtpBlock: { | ||||
|     Server: "", | ||||
|     Port: 0, | ||||
|     SSL: "", | ||||
|     Login: "", | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch("/user/profile"); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.contentLoaded = true; | ||||
| 
 | ||||
|   page.mailboxBlock = res.data.MailboxBlock; | ||||
|   page.davBlock = res.data.DavBlock; | ||||
|   page.smtpBlock = res.data.SmtpBlock; | ||||
|   page.imapBlock = res.data.ImapBlock; | ||||
| } | ||||
| </script> | ||||
|  | @ -0,0 +1,84 @@ | |||
| <template> | ||||
|   <div> | ||||
|     <a-card class="panel-content"> | ||||
|       <a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true"> | ||||
|         <a-form-item :label="t('components.user.recovery.show')"> | ||||
|           <a-switch v-model:checked="page.showFolder"> </a-switch> | ||||
|         </a-form-item> | ||||
|       </a-form> | ||||
|       <a-button | ||||
|         :disabled="!page.contentLoaded" | ||||
|         class="save-button" | ||||
|         type="primary" | ||||
|         size="large" | ||||
|         @click="update" | ||||
|       > | ||||
|         {{ $t("components.user.recovery.save") }} | ||||
|       </a-button> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { notifyError, notifySuccess } from "@/composables/alert"; | ||||
| import { apiFetch } from "@/composables/apiFetch"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| const { t } = useI18n(); | ||||
| 
 | ||||
| const labelCol = { span: 16, style: { "text-align": "left" } }; | ||||
| const wrapperCol = { span: 8, style: { "text-align": "right" } }; | ||||
| 
 | ||||
| const page = reactive<{ | ||||
|   contentLoaded: boolean; | ||||
|   showFolder: boolean; | ||||
| }>({ | ||||
|   contentLoaded: false, | ||||
|   showFolder: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   get(); | ||||
| }); | ||||
| 
 | ||||
| async function get() { | ||||
|   const res = await apiFetch("/user/recovery_folder"); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   page.contentLoaded = true; | ||||
|   page.showFolder = res.data; | ||||
| } | ||||
| 
 | ||||
| async function update() { | ||||
|   const res = await apiFetch("/user/recovery_folder", { | ||||
|     method: "POST", | ||||
|     body: { | ||||
|       Show: page.showFolder, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   if (res.error) { | ||||
|     notifyError(res.error); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   notifySuccess(t("components.user.recovery.success")); | ||||
| } | ||||
| </script> | ||||
| <style scoped> | ||||
| .panel-content { | ||||
|   min-width: 500px; | ||||
|   max-width: 25%; | ||||
|   margin: auto; | ||||
| } | ||||
| .save-button { | ||||
|   width: 30%; | ||||
|   min-width: 200px; | ||||
|   display: block; | ||||
|   margin: auto; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,23 @@ | |||
| <template> | ||||
|   <SharedFolders v-if="page.loaded" :user="page.user"></SharedFolders> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| import SharedFolders from "@/components/common/SharedFolders.vue"; | ||||
| import { onMounted, reactive } from "vue"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import { useAuthStore } from "@/stores/auth"; | ||||
| 
 | ||||
| const route = useRoute(); | ||||
| const page = reactive<{ | ||||
|   user: string; | ||||
|   loaded: boolean; | ||||
| }>({ | ||||
|   user: "", | ||||
|   loaded: false, | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   page.user = useAuthStore().username; | ||||
|   page.loaded = true; | ||||
| }); | ||||
| </script> | ||||
|  | @ -0,0 +1,27 @@ | |||
| import { notification } from "ant-design-vue"; | ||||
| import i18n from "@/locale"; | ||||
| 
 | ||||
| notification.config({ | ||||
|   maxCount: 1, | ||||
| }); | ||||
| 
 | ||||
| export function notifyError(msg: string) { | ||||
|   notification["error"]({ | ||||
|     message: i18n.global.t("common.notify.error"), | ||||
|     description: msg, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function notifySuccess(msg: string) { | ||||
|   notification["success"]({ | ||||
|     message: i18n.global.t("common.notify.success"), | ||||
|     description: msg, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function notifyWarning(msg: string) { | ||||
|   notification["warning"]({ | ||||
|     message: i18n.global.t("common.notify.warning"), | ||||
|     description: msg, | ||||
|   }); | ||||
| } | ||||
|  | @ -0,0 +1,47 @@ | |||
| import { useAuthStore } from "@/stores/auth"; | ||||
| import router from "@/router"; | ||||
| import { RouteLogin } from "@/router/consts"; | ||||
| import { useReturnToPageStore } from "@/stores/returnToPage"; | ||||
| import { useRoute } from "vue-router"; | ||||
| 
 | ||||
| export const apiFetch = async (request: any, opts?: any): Promise<Response> => { | ||||
|   if (opts && opts.body && !opts.isFormData) { | ||||
|     opts.body = JSON.stringify(opts.body); | ||||
|   } | ||||
| 
 | ||||
|   const headers = new Headers(); | ||||
|   //headers.set("Authorization", "Bearer " + useAuthStore().token);
 | ||||
| 
 | ||||
|   if (opts && !opts.isFormData) { | ||||
|     headers.set("Content-Type", "application/json"); | ||||
|   } | ||||
|   const resp = await fetch( | ||||
|     import.meta.env.VITE_API_URL + "/backend" + request, | ||||
|     { | ||||
|       headers, | ||||
|       credentials: "include", | ||||
|       ...opts, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   const { result, total, error } = await resp.json(); | ||||
| 
 | ||||
|   if (resp.status != 200) { | ||||
|     if (resp.status == 401) { | ||||
|       useAuthStore().resetAuth(); | ||||
|       useReturnToPageStore().setPath( | ||||
|         router.currentRoute.value.path, | ||||
|         useRoute().query | ||||
|       ); | ||||
|       router.push(RouteLogin); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return { data: result, total, error }; | ||||
| }; | ||||
| 
 | ||||
| export interface Response { | ||||
|   data: any; | ||||
|   total: number; | ||||
|   error: string; | ||||
| } | ||||
|  | @ -0,0 +1,79 @@ | |||
| import { message } from "ant-design-vue"; | ||||
| import i18n from "@/locale"; | ||||
| 
 | ||||
| export const timeToDateTime = (time: any) => { | ||||
|   if (time == undefined) { | ||||
|     return; | ||||
|   } | ||||
|   var d = new Date(time); | ||||
|   return ( | ||||
|     ("0" + d.getHours()).slice(-2) + | ||||
|     ":" + | ||||
|     ("0" + d.getMinutes()).slice(-2) + | ||||
|     " " + | ||||
|     ("0" + d.getDate()).slice(-2) + | ||||
|     "/" + | ||||
|     ("0" + (d.getMonth() + 1)).slice(-2) + | ||||
|     "/" + | ||||
|     d.getFullYear() | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const timeToDate = (time: any) => { | ||||
|   if (time == undefined) { | ||||
|     return; | ||||
|   } | ||||
|   var d = new Date(time); | ||||
|   return ( | ||||
|     ("0" + d.getDate()).slice(-2) + "/" + ("0" + (d.getMonth() + 1)).slice(-2) | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const timeToFullDate = (time: any) => { | ||||
|   if (time == undefined) { | ||||
|     return; | ||||
|   } | ||||
|   var d = new Date(time); | ||||
|   return ( | ||||
|     ("0" + d.getDate()).slice(-2) + | ||||
|     "." + | ||||
|     ("0" + (d.getMonth() + 1)).slice(-2) + | ||||
|     "." + | ||||
|     d.getFullYear() | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const copyToClipboard = (text: string) => { | ||||
|   navigator.clipboard.writeText(text); | ||||
|   message.info(i18n.global.t("common.misc.copy")); | ||||
| }; | ||||
| 
 | ||||
| export const genRandomString = (length: number) => { | ||||
|   let result = ""; | ||||
|   const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; | ||||
|   const charactersLength = characters.length; | ||||
|   let counter = 0; | ||||
|   while (counter < length) { | ||||
|     result += characters.charAt(Math.floor(Math.random() * charactersLength)); | ||||
|     counter += 1; | ||||
|   } | ||||
|   return result; | ||||
| }; | ||||
| 
 | ||||
| export const getTimezoneString = () => { | ||||
|   let offset = new Date().getTimezoneOffset(); | ||||
|   offset = offset / -60; | ||||
|   let resStr = "UTC"; | ||||
|   if (offset < 0) { | ||||
|     resStr += "-"; | ||||
|     offset *= -1; | ||||
|   } else { | ||||
|     resStr += "+"; | ||||
|   } | ||||
|   if (offset < 10) { | ||||
|     resStr += "0"; | ||||
|   } | ||||
|   resStr += offset; | ||||
|   resStr += ":00"; | ||||
|   return resStr; | ||||
| }; | ||||
|  | @ -0,0 +1,31 @@ | |||
| import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; | ||||
| import { Modal } from "ant-design-vue"; | ||||
| import { createVNode } from "vue"; | ||||
| import router from "@/router"; | ||||
| import { RouteLogin } from "@/router/consts"; | ||||
| import i18n from "@/locale"; | ||||
| import { apiFetch } from "./apiFetch"; | ||||
| import { notifyError } from "./alert"; | ||||
| 
 | ||||
| export function saveAndRestart(callback: () => Promise<boolean>) { | ||||
|   Modal.confirm({ | ||||
|     title: i18n.global.t("common.save_and_restart.confirm_title"), | ||||
|     icon: createVNode(ExclamationCircleOutlined), | ||||
|     content: i18n.global.t("common.save_and_restart.confirm_content"), | ||||
|     okText: i18n.global.t("common.save_and_restart.ok"), | ||||
|     cancelText: i18n.global.t("common.save_and_restart.cancel"), | ||||
|     async onOk() { | ||||
|       if (await callback()) { | ||||
|         const res = await apiFetch("/admin/restart", { | ||||
|           method: "POST", | ||||
|         }); | ||||
| 
 | ||||
|         if (res.error) { | ||||
|           notifyError(res.error); | ||||
|           return; | ||||
|         } | ||||
|         router.push(RouteLogin); | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
|  | @ -0,0 +1,32 @@ | |||
| import { createI18n } from "vue-i18n"; | ||||
| import { translations } from "./translations"; | ||||
| 
 | ||||
| const defaultLocale = "eng"; | ||||
| 
 | ||||
| export function getLocale() { | ||||
|   const locale = localStorage.getItem("locale"); | ||||
|   if (!locale) { | ||||
|     return defaultLocale; | ||||
|   } | ||||
|   return locale; | ||||
| } | ||||
| 
 | ||||
| export function setLocale(locale: any) { | ||||
|   localStorage.setItem("locale", locale); | ||||
|   i18n.global.locale.value = locale; | ||||
| } | ||||
| 
 | ||||
| var locale = localStorage.getItem("locale"); | ||||
| if (!locale) { | ||||
|   locale = defaultLocale; | ||||
|   localStorage.setItem("locale", locale); | ||||
| } | ||||
| 
 | ||||
| const i18n = createI18n({ | ||||
|   legacy: false, | ||||
|   locale: locale, | ||||
|   fallbackLocale: defaultLocale, | ||||
|   messages: translations, | ||||
| }); | ||||
| 
 | ||||
| export default i18n; | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -1,5 +1,49 @@ | |||
| import {createApp} from 'vue' | ||||
| import App from './App.vue' | ||||
| import './style.css'; | ||||
| import "./assets/main.css"; | ||||
| 
 | ||||
| createApp(App).mount('#app') | ||||
| import { createApp, nextTick } from "vue"; | ||||
| import { createPinia } from "pinia"; | ||||
| import i18n from "@/locale"; | ||||
| import piniaPluginPersistedState from "pinia-plugin-persistedstate"; | ||||
| 
 | ||||
| import App from "./App.vue"; | ||||
| import router from "@/router"; | ||||
| 
 | ||||
| const app = createApp(App); | ||||
| 
 | ||||
| const pinia = createPinia(); | ||||
| pinia.use(piniaPluginPersistedState); | ||||
| 
 | ||||
| app.use(pinia); | ||||
| app.use(i18n); | ||||
| app.use(router); | ||||
| 
 | ||||
| app.use({ | ||||
|   install(Vue) { | ||||
|     Vue.mixin({ | ||||
|       mounted() { | ||||
|         this.disableAutoComplete(); | ||||
|       }, | ||||
| 
 | ||||
|       methods: { | ||||
|         disableAutoComplete() { | ||||
|           let elements = document.querySelectorAll('[autocomplete="off"]'); | ||||
| 
 | ||||
|           if (!elements) { | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           elements.forEach((element) => { | ||||
|             element.setAttribute("readonly", "readonly"); | ||||
|             nextTick(() => { | ||||
|               setTimeout(() => { | ||||
|                 element.removeAttribute("readonly"); | ||||
|               }, 100); | ||||
|             }); | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| app.mount("#app"); | ||||
|  |  | |||
|  | @ -0,0 +1,98 @@ | |||
| export const RouteLogin = "/"; | ||||
| 
 | ||||
| export const RouteShared = "/sh"; | ||||
| export const TokenPlaceholder = ":token"; | ||||
| export const RouteSharedFreeTime = `/sh/ft/${TokenPlaceholder}`; | ||||
| export const RouteEventsExternalApproval = `/sh/ev/ap/${TokenPlaceholder}`; | ||||
| 
 | ||||
| export const RouteAdmin = "/admin"; | ||||
| export const RouteAdminDashboard = "/admin/dashboard"; | ||||
| 
 | ||||
| export const RouteAdminSettings = "/admin/settings"; | ||||
| export const RouteAdminSettingsMain = "/admin/settings/main"; | ||||
| export const RouteAdminSettingsSMTPQueue = "/admin/settings/smtp_queue"; | ||||
| export const RouteAdminSettingsSMTPQueueSettings = | ||||
|   "/admin/settings/smtp_queue/settings"; | ||||
| export const RouteAdminSettingsSMTPQueueManage = | ||||
|   "/admin/settings/smtp_queue/manage"; | ||||
| export const RouteAdminSettingsSettingsDB = "/admin/settings/settings_db"; | ||||
| export const RouteAdminSettingsAddressChange = "/admin/settings/address_change"; | ||||
| export const RouteAdminSettingsCalendars = "/admin/settings/calendars"; | ||||
| export const RouteAdminSettingsLicense = "/admin/settings/license"; | ||||
| 
 | ||||
| export const RouteAdminSecurity = "/admin/security"; | ||||
| export const RouteAdminSecurityBlockedIps = "/admin/security/blocked_ips"; | ||||
| export const RouteAdminSecurityWhiteList = "/admin/security/white_list"; | ||||
| export const RouteAdminSecurityWhiteListIp = "/admin/security/white_list/ip"; | ||||
| export const RouteAdminSecurityWhiteListEmail = | ||||
|   "/admin/security/white_list/email"; | ||||
| export const RouteAdminSecurityBlackList = "/admin/security/black_list"; | ||||
| export const RouteAdminSecurityBlackListIp = "/admin/security/black_list/ip"; | ||||
| export const RouteAdminSecurityBlackListEmail = | ||||
|   "/admin/security/black_list/email"; | ||||
| 
 | ||||
| export const RouteAdminDomains = "/admin/domains"; | ||||
| export const DomainPlaceholder = ":domain"; | ||||
| export const UserdbPlaceholder = ":sid"; | ||||
| export const RouteAdminDomainsAdd = `/admin/domains/add`; | ||||
| export const RouteAdminDomainsDomain = `/admin/domains/${DomainPlaceholder}`; | ||||
| export const RouteAdminDomainsDomainMailStorage = `/admin/domains/${DomainPlaceholder}/mailstorage`; | ||||
| export const RouteAdminDomainsDomainMailStorageMailboxes = `/admin/domains/${DomainPlaceholder}/mailstorage/mailboxes`; | ||||
| export const MailboxPlaceholder = ":mailbox"; | ||||
| export const RouteAdminDomainsDomainMailStorageMailboxesExport = `/admin/domains/${DomainPlaceholder}/mailstorage/mailboxes/${MailboxPlaceholder}/export`; | ||||
| export const RouteAdminDomainsDomainMailStorageSettings = `/admin/domains/${DomainPlaceholder}/mailstorage/settings`; | ||||
| export const RouteAdminDomainsDomainUserDB = `/admin/domains/${DomainPlaceholder}/userdb`; | ||||
| export const RouteAdminDomainsDomainUserDBAdd = `/admin/domains/${DomainPlaceholder}/userdb/add`; | ||||
| export const RouteAdminDomainsDomainUserDBProvider = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}`; | ||||
| export const RouteAdminDomainsDomainUserDBProviderUsers = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/users`; | ||||
| export const RouteAdminDomainsDomainUserDBProviderGroups = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/groups`; | ||||
| export const RouteAdminDomainsDomainUserDBProviderRedirects = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/redirects`; | ||||
| export const RouteAdminDomainsDomainUserDBProviderSettings = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/settings`; | ||||
| export const RouteAdminDomainsDomainUserDBProviderOther = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/other`; | ||||
| export const RouteAdminDomainsDomainSharedFolders = `/admin/domains/${DomainPlaceholder}/shared_folders`; | ||||
| export const RouteAdminDomainsDomainAddressBooks = `/admin/domains/${DomainPlaceholder}/address_books`; | ||||
| export const RouteAdminDomainsDomainCalendars = `/admin/domains/${DomainPlaceholder}/calendars`; | ||||
| 
 | ||||
| export const RouteAdminDomainsDomainResources = `/admin/domains/${DomainPlaceholder}/resources`; | ||||
| export const RouteAdminDomainsDomainResourcesList = `/admin/domains/${DomainPlaceholder}/resources/list`; | ||||
| export const RouteAdminDomainsDomainResourcesOffices = `/admin/domains/${DomainPlaceholder}/resources/offices`; | ||||
| export const RouteAdminDomainsDomainResourcesCategories = `/admin/domains/${DomainPlaceholder}/resources/categories`; | ||||
| 
 | ||||
| export const RouteAdminDomainsDomainIncRules = `/admin/domains/${DomainPlaceholder}/incoming_rules`; | ||||
| export const RouteAdminDomainsDomainOutRules = `/admin/domains/${DomainPlaceholder}/outgoing_rules`; | ||||
| export const RouteAdminDomainsDomainDKIM = `/admin/domains/${DomainPlaceholder}/dkim`; | ||||
| export const RouteAdminDomainsDomainMigration = `/admin/domains/${DomainPlaceholder}/migration`; | ||||
| export const RouteAdminDomainsDomainCIDRAccess = `/admin/domains/${DomainPlaceholder}/cidr_access`; | ||||
| export const RouteAdminDomainsDomainCIDRAccessPools = `/admin/domains/${DomainPlaceholder}/cidr_access/pools`; | ||||
| export const RouteAdminDomainsDomainCIDRAccessPolicy = `/admin/domains/${DomainPlaceholder}/cidr_access/policy`; | ||||
| export const RouteAdminDomainsDomainOther = `/admin/domains/${DomainPlaceholder}/other`; | ||||
| export const RouteAdminDomainsDomainOtherMailsPermDeletion = `/admin/domains/${DomainPlaceholder}/other/mails_perm_deletion`; | ||||
| 
 | ||||
| export const RouteUser = "/user"; | ||||
| export const RouteUserProfile = "/user/profile"; | ||||
| export const RouteUserRules = "/user/rules"; | ||||
| export const RouteUserRecovery = "/user/recovery"; | ||||
| export const RouteUserMailboxSharedAccess = "/user/mailbox_shared_access"; | ||||
| export const RouteUserSharedFolders = "/user/shared_folders"; | ||||
| 
 | ||||
| export const RouteUserAddressBooks = "/user/address_books"; | ||||
| export const RouteUserAddressBooksMy = "/user/address_books/my_books"; | ||||
| export const RouteUserAddressBooksAvaliableToMe = | ||||
|   "/user/address_books/avaliable_to_me"; | ||||
| 
 | ||||
| export const RouteUserCalendars = "/user/calendars"; | ||||
| export const RouteUserCalendarsMy = "/user/calendars/my_calendars"; | ||||
| export const RouteUserCalendarsAvaliableToMe = | ||||
|   "/user/calendars/avaliable_to_me"; | ||||
| export const RouteUserCalendarsShareFreeTime = | ||||
|   "/user/calendars/share_free_time"; | ||||
| 
 | ||||
| export const RouteUserCalendarsEventsPlanner = "/user/calendars/events_planner"; | ||||
| export const RouteUserCalendarsEventsPlannerList = | ||||
|   "/user/calendars/events_planner/my_events"; | ||||
| export const RouteUserCalendarsEventsPlannerNew = | ||||
|   "/user/calendars/events_planner/new"; | ||||
| 
 | ||||
| export const EventIdPlaceholder = ":event_id"; | ||||
| 
 | ||||
| export const RouteUserCalendarsEventsPlannerEdit = `/user/calendars/events_planner/${EventIdPlaceholder}/edit`; | ||||
|  | @ -0,0 +1,105 @@ | |||
| import { | ||||
|   createRouter, | ||||
|   createWebHistory, | ||||
|   type RouteRecordRaw, | ||||
| } from "vue-router"; | ||||
| import { pipeline } from "@/router/middleware/pipeline"; | ||||
| import { useAuthStore } from "@/stores/auth"; | ||||
| import { | ||||
|   RouteAdmin, | ||||
|   RouteAdminDashboard, | ||||
|   RouteLogin, | ||||
|   RouteShared, | ||||
|   RouteUser, | ||||
|   RouteUserProfile, | ||||
| } from "./consts"; | ||||
| import routes from "@/router/routes"; | ||||
| import { nextTick } from "vue"; | ||||
| import i18n from "@/locale"; | ||||
| import { useReturnToPageStore } from "@/stores/returnToPage"; | ||||
| 
 | ||||
| const router = createRouter({ | ||||
|   history: createWebHistory(import.meta.env.BASE_URL), | ||||
|   routes: routes as RouteRecordRaw[], | ||||
| }); | ||||
| 
 | ||||
| router.beforeEach((to, from, next) => { | ||||
|   let path = to.path; | ||||
|   if (!to) { | ||||
|     path = location.pathname; | ||||
|   } | ||||
| 
 | ||||
|   if (!path.startsWith(RouteShared)) { | ||||
|     if (to.name !== RouteLogin) { | ||||
|       if (!useAuthStore().authed) { | ||||
|         useReturnToPageStore().setPath(path, to.query); | ||||
|         return next({ name: RouteLogin }); | ||||
|       } | ||||
|       if (useAuthStore().isAdmin && !to.path.startsWith(RouteAdmin)) { | ||||
|         return next({ name: RouteAdminDashboard }); | ||||
|       } | ||||
|       if (!useAuthStore().isAdmin && !to.path.startsWith(RouteUser)) { | ||||
|         return next({ name: RouteUserProfile }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (!to.meta.middleware) { | ||||
|     return next(); | ||||
|   } | ||||
|   const middleware = Array.isArray(to.meta.middleware) | ||||
|     ? to.meta.middleware | ||||
|     : [to.meta.middleware]; | ||||
| 
 | ||||
|   const context = { | ||||
|     to, | ||||
|     from, | ||||
|     next, | ||||
|   }; | ||||
| 
 | ||||
|   return middleware[0](pipeline(context, middleware, 1), context); | ||||
| }); | ||||
| 
 | ||||
| router.afterEach((to, from) => { | ||||
|   // Use next tick to handle router history correctly
 | ||||
|   // see: https://github.com/vuejs/vue-router/issues/914#issuecomment-384477609
 | ||||
|   nextTick(() => { | ||||
|     let title = to.meta.title as string; | ||||
|     if (!title) { | ||||
|       if (to.path.startsWith("/admin")) { | ||||
|         title = i18n.global.t("admin.title"); | ||||
|       } else if (to.path.startsWith("/user")) { | ||||
|         if (useAuthStore().username) { | ||||
|           title = useAuthStore().username; | ||||
|         } else { | ||||
|           title = i18n.global.t("user.title"); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (title) { | ||||
|       title = title + " - " + i18n.global.t("tegu"); | ||||
|     } | ||||
|     document.title = title || "Tegu"; | ||||
|   }); | ||||
| 
 | ||||
|   if (to.hash === "#refresh") { | ||||
|     router.replace({ hash: "" }).then(() => { | ||||
|       router.go(0); | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (to.query.redirect) { | ||||
|     let redirect = to.query.redirect as string; | ||||
|     let parts = redirect.split("?"); | ||||
|     router.push(parts[0]); | ||||
| 
 | ||||
|     useReturnToPageStore().setPath( | ||||
|       parts[0], | ||||
|       Object.fromEntries(new URLSearchParams(parts[1])) | ||||
|     ); | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue