From 1ed72c51dfdf00dd3a3596c08fde991fb23cf0f3 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 6 Nov 2023 16:28:15 +0530 Subject: [PATCH 01/62] fix: package version fixes and mentions build error fixes (#2665) --- packages/editor/core/package.json | 1 + packages/editor/lite-text-editor/package.json | 7 +- .../editor/lite-text-editor/src/ui/index.tsx | 5 +- .../src/ui/read-only/index.tsx | 38 ++++++--- .../lite-text-editor/src/ui/tooltip.tsx | 15 +++- packages/editor/rich-text-editor/package.json | 11 +-- packages/eslint-config-custom/package.json | 1 + packages/tailwind-config-custom/package.json | 1 + packages/ui/package.json | 2 + .../comment/comment-detail-card.tsx | 1 + space/store/user.ts | 1 - web/components/issues/comment/add-comment.tsx | 4 +- .../issues/comment/comment-card.tsx | 2 +- web/components/issues/description-form.tsx | 7 +- web/components/issues/draft-issue-form.tsx | 2 +- web/components/issues/form.tsx | 2 +- .../activity/comment-card.tsx | 2 +- .../activity/comment-editor.tsx | 4 +- .../issue-peek-overview/issue-detail.tsx | 2 +- .../pages/create-update-block-inline.tsx | 2 +- web/components/pages/single-page-block.tsx | 2 +- web/hooks/use-editor-suggestions.tsx | 4 +- web/package.json | 5 +- yarn.lock | 79 +++++++++---------- 24 files changed, 108 insertions(+), 92 deletions(-) diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index 83f59a1f4a9..ab6c77724e5 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -2,6 +2,7 @@ "name": "@plane/editor-core", "version": "0.0.1", "description": "Core Editor that powers Plane", + "private": true, "main": "./dist/index.mjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/editor/lite-text-editor/package.json b/packages/editor/lite-text-editor/package.json index 47ef154c69e..3b6cd720b78 100644 --- a/packages/editor/lite-text-editor/package.json +++ b/packages/editor/lite-text-editor/package.json @@ -2,6 +2,7 @@ "name": "@plane/lite-text-editor", "version": "0.0.1", "description": "Package that powers Plane's Comment Editor", + "private": true, "main": "./dist/index.mjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", @@ -29,9 +30,6 @@ "dependencies": { "@plane/editor-core": "*", "@tiptap/extension-list-item": "^2.1.11", - "@types/node": "18.15.3", - "@types/react": "^18.2.5", - "@types/react-dom": "18.0.11", "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", "eslint": "8.36.0", @@ -46,6 +44,9 @@ "use-debounce": "^9.0.4" }, "devDependencies": { + "@types/node": "18.15.3", + "@types/react": "^18.2.35", + "@types/react-dom": "^18.2.14", "eslint": "^7.32.0", "postcss": "^8.4.29", "tailwind-config-custom": "*", diff --git a/packages/editor/lite-text-editor/src/ui/index.tsx b/packages/editor/lite-text-editor/src/ui/index.tsx index 6cd03bcfa3e..ef321d511bf 100644 --- a/packages/editor/lite-text-editor/src/ui/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/index.tsx @@ -1,4 +1,3 @@ -"use client"; import * as React from "react"; import { EditorContainer, @@ -32,7 +31,7 @@ interface ILiteTextEditor { editorContentCustomClassNames?: string; onChange?: (json: any, html: string) => void; setIsSubmitting?: ( - isSubmitting: "submitting" | "submitted" | "saved", + isSubmitting: "submitting" | "submitted" | "saved" ) => void; setShouldShowAlert?: (showAlert: boolean) => void; forwardedRef?: any; @@ -127,7 +126,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => { }; const LiteTextEditorWithRef = React.forwardRef( - (props, ref) => , + (props, ref) => ); LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef"; diff --git a/packages/editor/lite-text-editor/src/ui/read-only/index.tsx b/packages/editor/lite-text-editor/src/ui/read-only/index.tsx index 5dccbe97731..a3de061ae48 100644 --- a/packages/editor/lite-text-editor/src/ui/read-only/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/read-only/index.tsx @@ -1,6 +1,10 @@ -"use client" -import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from '@plane/editor-core'; -import * as React from 'react'; +import * as React from "react"; +import { + EditorContainer, + EditorContentWrapper, + getEditorClassNames, + useReadOnlyEditor, +} from "@plane/editor-core"; interface ICoreReadOnlyEditor { value: string; @@ -8,7 +12,7 @@ interface ICoreReadOnlyEditor { noBorder?: boolean; borderOnFocus?: boolean; customClassName?: string; - mentionHighlights: string[] + mentionHighlights: string[]; } interface EditorCoreProps extends ICoreReadOnlyEditor { @@ -27,31 +31,39 @@ const LiteReadOnlyEditor = ({ customClassName, value, forwardedRef, - mentionHighlights + mentionHighlights, }: EditorCoreProps) => { const editor = useReadOnlyEditor({ value, forwardedRef, - mentionHighlights + mentionHighlights, }); - const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); + const editorClassNames = getEditorClassNames({ + noBorder, + borderOnFocus, + customClassName, + }); if (!editor) return null; return (
- +
-
+ ); }; -const LiteReadOnlyEditorWithRef = React.forwardRef((props, ref) => ( - -)); +const LiteReadOnlyEditorWithRef = React.forwardRef< + EditorHandle, + ICoreReadOnlyEditor +>((props, ref) => ); LiteReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef"; -export { LiteReadOnlyEditor , LiteReadOnlyEditorWithRef }; +export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef }; diff --git a/packages/editor/lite-text-editor/src/ui/tooltip.tsx b/packages/editor/lite-text-editor/src/ui/tooltip.tsx index f29d8a49177..a2f2414e529 100644 --- a/packages/editor/lite-text-editor/src/ui/tooltip.tsx +++ b/packages/editor/lite-text-editor/src/ui/tooltip.tsx @@ -1,5 +1,4 @@ -import * as React from 'react'; - +import * as React from "react"; // next-themes import { useTheme } from "next-themes"; // tooltip2 @@ -69,8 +68,16 @@ export const Tooltip: React.FC = ({ } position={position} - renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) => - React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props }) + renderTarget={({ + isOpen: isTooltipOpen, + ref: eleReference, + ...tooltipProps + }) => + React.cloneElement(children, { + ref: eleReference, + ...tooltipProps, + ...children.props, + }) } /> ); diff --git a/packages/editor/rich-text-editor/package.json b/packages/editor/rich-text-editor/package.json index 7bdd0a58b1d..db793261cc2 100644 --- a/packages/editor/rich-text-editor/package.json +++ b/packages/editor/rich-text-editor/package.json @@ -2,6 +2,7 @@ "name": "@plane/rich-text-editor", "version": "0.0.1", "description": "Rich Text Editor that powers Plane", + "private": true, "main": "./dist/index.mjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", @@ -21,19 +22,19 @@ "check-types": "tsc --noEmit" }, "peerDependencies": { + "@tiptap/core": "^2.1.11", "next": "12.3.2", "next-themes": "^0.2.1", "react": "^18.2.0", - "react-dom": "18.2.0", - "@tiptap/core": "^2.1.11" + "react-dom": "18.2.0" }, "dependencies": { "@plane/editor-core": "*", "@tiptap/extension-code-block-lowlight": "^2.1.11", "@tiptap/extension-horizontal-rule": "^2.1.11", "@tiptap/extension-placeholder": "^2.1.11", - "class-variance-authority": "^0.7.0", "@tiptap/suggestion": "^2.1.7", + "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", "highlight.js": "^11.8.0", "lowlight": "^3.0.0", @@ -41,8 +42,8 @@ }, "devDependencies": { "@types/node": "18.15.3", - "@types/react": "^18.2.5", - "@types/react-dom": "18.0.11", + "@types/react": "^18.2.35", + "@types/react-dom": "^18.2.14", "eslint": "^7.32.0", "postcss": "^8.4.29", "react": "^18.2.0", diff --git a/packages/eslint-config-custom/package.json b/packages/eslint-config-custom/package.json index 12a7ab8c8da..11e970d0e23 100644 --- a/packages/eslint-config-custom/package.json +++ b/packages/eslint-config-custom/package.json @@ -1,5 +1,6 @@ { "name": "eslint-config-custom", + "private": true, "version": "0.13.2", "main": "index.js", "license": "MIT", diff --git a/packages/tailwind-config-custom/package.json b/packages/tailwind-config-custom/package.json index 1336379b713..286dfc3b686 100644 --- a/packages/tailwind-config-custom/package.json +++ b/packages/tailwind-config-custom/package.json @@ -3,6 +3,7 @@ "version": "0.13.2", "description": "common tailwind configuration across monorepo", "main": "index.js", + "private": true, "devDependencies": { "@tailwindcss/typography": "^0.5.9", "autoprefixer": "^10.4.14", diff --git a/packages/ui/package.json b/packages/ui/package.json index f76bd837460..72413eb7cb0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,5 +1,7 @@ { "name": "@plane/ui", + "description": "UI components shared across multiple apps internally", + "private": true, "version": "0.0.1", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index b4754f09836..1d9ff058648 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -135,6 +135,7 @@ export const CommentCard: React.FC = observer((props) => { ref={showEditorRef} value={comment.comment_html} customClassName="text-xs border border-custom-border-200 bg-custom-background-100" + mentionHighlights={userStore.currentUser?.id ? [userStore.currentUser?.id] : []} /> diff --git a/space/store/user.ts b/space/store/user.ts index cec2d340fdc..e2b6428ef94 100644 --- a/space/store/user.ts +++ b/space/store/user.ts @@ -2,7 +2,6 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"; // service import UserService from "services/user.service"; -import { ActorDetail } from "types/issue"; // types import { IUser } from "types/user"; diff --git a/web/components/issues/comment/add-comment.tsx b/web/components/issues/comment/add-comment.tsx index 71294b8c52e..4a022b578bd 100644 --- a/web/components/issues/comment/add-comment.tsx +++ b/web/components/issues/comment/add-comment.tsx @@ -50,9 +50,9 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit, showAc const editorRef = React.useRef(null); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; - const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined); + const editorSuggestions = useEditorSuggestions(); const { control, diff --git a/web/components/issues/comment/comment-card.tsx b/web/components/issues/comment/comment-card.tsx index e409122e929..d967fd3571f 100644 --- a/web/components/issues/comment/comment-card.tsx +++ b/web/components/issues/comment/comment-card.tsx @@ -40,7 +40,7 @@ export const CommentCard: React.FC = ({ const editorRef = React.useRef(null); const showEditorRef = React.useRef(null); - const editorSuggestions = useEditorSuggestions(workspaceSlug, comment.project_detail.id) + const editorSuggestions = useEditorSuggestions(); const [isEditing, setIsEditing] = useState(false); diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 6dd569f8fd0..a3c4289f070 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -38,7 +38,7 @@ export const IssueDescriptionForm: FC = (props) => { const { setShowAlert } = useReloadConfirmations(); - const editorSuggestion = useEditorSuggestions(workspaceSlug, issue.project_id) + const editorSuggestion = useEditorSuggestions(); const { handleSubmit, @@ -164,8 +164,9 @@ export const IssueDescriptionForm: FC = (props) => { )} />
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index b923bae6340..c6f880c1d2e 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -122,7 +122,7 @@ export const DraftIssueForm: FC = (props) => { const { setToastAlert } = useToast(); - const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId) + const editorSuggestions = useEditorSuggestions(); const { formState: { errors, isSubmitting }, diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index fdaa0f276d9..b54ce112183 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -112,7 +112,7 @@ export const IssueForm: FC = observer((props) => { const user = userStore.currentUser; - const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, projectId); + const editorSuggestion = useEditorSuggestions(); const { setToastAlert } = useToast(); diff --git a/web/components/issues/issue-peek-overview/activity/comment-card.tsx b/web/components/issues/issue-peek-overview/activity/comment-card.tsx index fc8fbf17909..4ca072f6eab 100644 --- a/web/components/issues/issue-peek-overview/activity/comment-card.tsx +++ b/web/components/issues/issue-peek-overview/activity/comment-card.tsx @@ -49,7 +49,7 @@ export const IssueCommentCard: React.FC = (props) => { const [isEditing, setIsEditing] = useState(false); - const editorSuggestions = useEditorSuggestions(workspaceSlug, projectId); + const editorSuggestions = useEditorSuggestions(); const { formState: { isSubmitting }, diff --git a/web/components/issues/issue-peek-overview/activity/comment-editor.tsx b/web/components/issues/issue-peek-overview/activity/comment-editor.tsx index eea88e201b8..91b3bf7f19a 100644 --- a/web/components/issues/issue-peek-overview/activity/comment-editor.tsx +++ b/web/components/issues/issue-peek-overview/activity/comment-editor.tsx @@ -51,9 +51,9 @@ export const IssueCommentEditor: React.FC = (props) => { const editorRef = React.useRef(null); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; - const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined); + const editorSuggestions = useEditorSuggestions(); const { control, diff --git a/web/components/issues/issue-peek-overview/issue-detail.tsx b/web/components/issues/issue-peek-overview/issue-detail.tsx index 780186b508a..1568a16a178 100644 --- a/web/components/issues/issue-peek-overview/issue-detail.tsx +++ b/web/components/issues/issue-peek-overview/issue-detail.tsx @@ -38,7 +38,7 @@ export const PeekOverviewIssueDetails: FC = (props) = const [characterLimit, setCharacterLimit] = useState(false); // hooks const { setShowAlert } = useReloadConfirmations(); - const editorSuggestions = useEditorSuggestions(workspaceSlug, issue.project_detail.id); + const editorSuggestions = useEditorSuggestions(); const { handleSubmit, diff --git a/web/components/pages/create-update-block-inline.tsx b/web/components/pages/create-update-block-inline.tsx index 1d0da501854..b838da0c9e8 100644 --- a/web/components/pages/create-update-block-inline.tsx +++ b/web/components/pages/create-update-block-inline.tsx @@ -56,7 +56,7 @@ export const CreateUpdateBlockInline: FC = ({ const router = useRouter(); const { workspaceSlug, projectId, pageId } = router.query; - const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined) + const editorSuggestion = useEditorSuggestions(); const { setToastAlert } = useToast(); diff --git a/web/components/pages/single-page-block.tsx b/web/components/pages/single-page-block.tsx index 12a0e9e308d..542ef78a2c7 100644 --- a/web/components/pages/single-page-block.tsx +++ b/web/components/pages/single-page-block.tsx @@ -64,7 +64,7 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, showBl }, }); - const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined); + const editorSuggestion = useEditorSuggestions(); const updatePageBlock = async (formData: Partial) => { if (!workspaceSlug || !projectId || !pageId) return; diff --git a/web/hooks/use-editor-suggestions.tsx b/web/hooks/use-editor-suggestions.tsx index 6247989f0ea..337fc5f0a0e 100644 --- a/web/hooks/use-editor-suggestions.tsx +++ b/web/hooks/use-editor-suggestions.tsx @@ -1,9 +1,7 @@ -import { IMentionHighlight, IMentionSuggestion } from "@plane/rich-text-editor"; -import useUser from "./use-user"; import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; -const useEditorSuggestions = (_workspaceSlug: string | undefined, _projectId: string | undefined) => { +const useEditorSuggestions = () => { const { mentionsStore }: RootStore = useMobxStore(); return { diff --git a/web/package.json b/web/package.json index f78d670043b..b2791978fbd 100644 --- a/web/package.json +++ b/web/package.json @@ -57,10 +57,9 @@ "@types/js-cookie": "^3.0.2", "@types/node": "18.0.6", "@types/nprogress": "^0.2.0", - "@types/react": "18.0.15", - "@types/react-beautiful-dnd": "^13.1.2", + "@types/react": "^18.2.35", "@types/react-color": "^3.0.6", - "@types/react-dom": "18.0.6", + "@types/react-dom": "^18.2.14", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.48.2", "@typescript-eslint/parser": "^5.48.2", diff --git a/yarn.lock b/yarn.lock index d7fd466263c..f25d1cf7d56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1343,10 +1343,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/eslintrc@^2.0.1", "@eslint/eslintrc@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" - integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== +"@eslint/eslintrc@^2.0.1", "@eslint/eslintrc@^2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.3.tgz#797470a75fe0fbd5a53350ee715e85e87baff22d" + integrity sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1363,10 +1363,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== -"@eslint/js@8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.52.0.tgz#78fe5f117840f69dc4a353adf9b9cd926353378c" - integrity sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA== +"@eslint/js@8.53.0": + version "8.53.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.53.0.tgz#bea56f2ed2b5baea164348ff4d5a879f6f81f20d" + integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w== "@floating-ui/core@^1.4.2": version "1.5.0" @@ -2758,13 +2758,6 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d" integrity sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g== -"@types/react-beautiful-dnd@^13.1.2": - version "13.1.6" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.6.tgz#a616443903bfc370fee298b0144dbce7234d5561" - integrity sha512-FXAuaa52ux7HWQDumi3MFSAAsW8OKfDImy1pSZPKe85nV9mZ1f4spVzW0a2boYvkIhphjbWUi5EwUiRG8Rq/Qg== - dependencies: - "@types/react" "*" - "@types/react-color@^3.0.6", "@types/react-color@^3.0.9": version "3.0.9" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.9.tgz#8dbb0d798f2979c3d7e2e26dd46321e80da950b4" @@ -2790,13 +2783,6 @@ dependencies: "@types/react" "*" -"@types/react-dom@18.0.6": - version "18.0.6" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1" - integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA== - dependencies: - "@types/react" "*" - "@types/react-dom@18.2.0": version "18.2.0" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.0.tgz#374f28074bb117f56f58c4f3f71753bebb545156" @@ -2804,6 +2790,13 @@ dependencies: "@types/react" "*" +"@types/react-dom@^18.2.14": + version "18.2.14" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.14.tgz#c01ba40e5bb57fc1dc41569bb3ccdb19eab1c539" + integrity sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.4.8": version "4.4.8" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.8.tgz#46f87d80512959cac793ecc610a93d80ef241ccf" @@ -2811,7 +2804,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.0.15", "@types/react@18.0.28", "@types/react@18.2.0", "@types/react@^18.2.5": +"@types/react@*", "@types/react@18.0.28", "@types/react@18.2.0", "@types/react@^18.2.35", "@types/react@^18.2.5": version "18.2.0" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.0.tgz#15cda145354accfc09a18d2f2305f9fc099ada21" integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA== @@ -3185,9 +3178,9 @@ astral-regex@^2.0.0: integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@^3.2.3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynciterator.prototype@^1.0.0: version "1.0.0" @@ -3428,9 +3421,9 @@ camelcase-css@^2.0.1: integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: - version "1.0.30001559" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz#95a982440d3d314c471db68d02664fb7536c5a30" - integrity sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA== + version "1.0.30001561" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz#752f21f56f96f1b1a52e97aae98c57c562d5d9da" + integrity sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw== capital-case@^1.0.4: version "1.0.4" @@ -4027,9 +4020,9 @@ ejs@^3.1.6: jake "^10.8.5" electron-to-chromium@^1.4.535: - version "1.4.575" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.575.tgz#7c0b87eb2c6214a993699792abd704de41255c39" - integrity sha512-kY2BGyvgAHiX899oF6xLXSIf99bAvvdPhDoJwG77nxCSyWYuRH6e9a9a3gpXBvCs6lj4dQZJkfnW2hdKWHEISg== + version "1.4.576" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz#0c6940fdc0d60f7e34bd742b29d8fa847c9294d1" + integrity sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA== emoji-regex@^8.0.0: version "8.0.0" @@ -4735,14 +4728,14 @@ eslint@^7.23.0, eslint@^7.32.0: v8-compile-cache "^2.0.3" eslint@^8.31.0: - version "8.52.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.52.0.tgz#d0cd4a1fac06427a61ef9242b9353f36ea7062fc" - integrity sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg== + version "8.53.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.53.0.tgz#14f2c8244298fcae1f46945459577413ba2697ce" + integrity sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.2" - "@eslint/js" "8.52.0" + "@eslint/eslintrc" "^2.1.3" + "@eslint/js" "8.53.0" "@humanwhocodes/config-array" "^0.11.13" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -6505,9 +6498,9 @@ mz@^2.7.0: thenify-all "^1.0.0" nanoid@^3.3.4, nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== napi-build-utils@^1.0.1: version "1.0.2" @@ -7332,9 +7325,9 @@ react-fast-compare@^3.0.1: integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== react-hook-form@^7.38.0: - version "7.47.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.47.0.tgz#a42f07266bd297ddf1f914f08f4b5f9783262f31" - integrity sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg== + version "7.48.2" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.48.2.tgz#01150354d2be61412ff56a030b62a119283b9935" + integrity sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A== react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" From 742143766f4d517e34d11e448c1a50c3614a7dae Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:30:09 +0530 Subject: [PATCH 02/62] fix: existing issues modal for cycle and module (#2664) * fix: existing issues modal for cycle and module * refactor: existing issues modal code * fix: build errors --- .../issue-layouts/empty-states/cycle.tsx | 105 +++++++++++++----- .../issue-layouts/empty-states/module.tsx | 100 ++++++++++++----- .../issue-layouts/roots/cycle-layout-root.tsx | 12 +- .../roots/module-layout-root.tsx | 12 +- .../issues/issue-peek-overview/properties.tsx | 4 +- web/components/issues/modal.tsx | 4 +- .../projects/[projectId]/cycles/[cycleId].tsx | 48 +------- .../[projectId]/modules/[moduleId].tsx | 47 +------- web/store/cycle/cycle_issue.store.ts | 6 +- web/store/module/module_issue.store.ts | 6 +- 10 files changed, 173 insertions(+), 171 deletions(-) diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 642e1dad9b4..53ea4500145 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,38 +1,85 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useToast from "hooks/use-toast"; // components import { EmptyState } from "components/common"; +import { ExistingIssuesListModal } from "components/core"; +// ui +import { Button } from "@plane/ui"; // assets import emptyIssue from "public/empty-state/issue.svg"; -import { Button } from "@plane/ui"; +// types +import { ISearchIssueResponse } from "types"; type Props = { - openIssuesListModal: () => void; + workspaceSlug: string | undefined; + projectId: string | undefined; + cycleId: string | undefined; }; -export const CycleEmptyState: React.FC = ({ openIssuesListModal }) => ( -
- , - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }, - }} - secondaryButton={ - - } - /> -
-); +export const CycleEmptyState: React.FC = observer((props) => { + const { workspaceSlug, projectId, cycleId } = props; + // states + const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); + + const { cycleIssue: cycleIssueStore } = useMobxStore(); + + const { setToastAlert } = useToast(); + + const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !projectId || !cycleId) return; + + const issueIds = data.map((i) => i.id); + + await cycleIssueStore + .addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Selected issues could not be added to the cycle. Please try again.", + }); + }); + }; + + return ( + <> + setCycleIssuesListModal(false)} + searchParams={{ cycle: true }} + handleOnSubmit={handleAddIssuesToCycle} + /> +
+ , + onClick: () => { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }, + }} + secondaryButton={ + + } + /> +
+ + ); +}); diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index a71be523f92..7e5edeeb87e 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -4,36 +4,78 @@ import { EmptyState } from "components/common"; import { Button } from "@plane/ui"; // assets import emptyIssue from "public/empty-state/issue.svg"; +import { ExistingIssuesListModal } from "components/core"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +import { ISearchIssueResponse } from "types"; +import useToast from "hooks/use-toast"; +import { useState } from "react"; type Props = { - openIssuesListModal: () => void; + workspaceSlug: string | undefined; + projectId: string | undefined; + moduleId: string | undefined; }; -export const ModuleEmptyState: React.FC = ({ openIssuesListModal }) => ( -
- , - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }, - }} - secondaryButton={ - - } - /> -
-); +export const ModuleEmptyState: React.FC = observer((props) => { + const { workspaceSlug, projectId, moduleId } = props; + // states + const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); + + const { moduleIssue: moduleIssueStore } = useMobxStore(); + + const { setToastAlert } = useToast(); + + const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !projectId || !moduleId) return; + + const issueIds = data.map((i) => i.id); + + await moduleIssueStore + .addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Selected issues could not be added to the module. Please try again.", + }) + ); + }; + + return ( + <> + setModuleIssuesListModal(false)} + searchParams={{ module: true }} + handleOnSubmit={handleAddIssuesToModule} + /> +
+ , + onClick: () => { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }, + }} + secondaryButton={ + + } + /> +
+ + ); +}); diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 29d91cae8a7..4d9fa7f08cb 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -20,11 +20,7 @@ import { Spinner } from "@plane/ui"; // helpers import { getDateRangeStatus } from "helpers/date-time.helper"; -type Props = { - openIssuesListModal: () => void; -}; - -export const CycleLayoutRoot: React.FC = observer(({ openIssuesListModal }) => { +export const CycleLayoutRoot: React.FC = observer(() => { const [transferIssuesModal, setTransferIssuesModal] = useState(false); const router = useRouter(); @@ -73,7 +69,11 @@ export const CycleLayoutRoot: React.FC = observer(({ openIssuesListModal {cycleStatus === "completed" && setTransferIssuesModal(true)} />} {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( - + ) : (
{activeLayout === "list" ? ( diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index d4f1efb28fc..3386725bc62 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -18,11 +18,7 @@ import { // ui import { Spinner } from "@plane/ui"; -type Props = { - openIssuesListModal: () => void; -}; - -export const ModuleLayoutRoot: React.FC = observer(({ openIssuesListModal }) => { +export const ModuleLayoutRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query as { workspaceSlug: string; @@ -66,7 +62,11 @@ export const ModuleLayoutRoot: React.FC = observer(({ openIssuesListModal
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( - + ) : (
{activeLayout === "list" ? ( diff --git a/web/components/issues/issue-peek-overview/properties.tsx b/web/components/issues/issue-peek-overview/properties.tsx index dbc1bfdbb86..303687d3931 100644 --- a/web/components/issues/issue-peek-overview/properties.tsx +++ b/web/components/issues/issue-peek-overview/properties.tsx @@ -74,13 +74,13 @@ export const PeekOverviewProperties: FC = observer((pro }; const addIssueToCycle = async (cycleId: string) => { if (!workspaceSlug || !issue || !cycleId) return; - cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), issue.project_detail.id, cycleId, issue.id); + cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), issue.project_detail.id, cycleId, [issue.id]); }; const addIssueToModule = async (moduleId: string) => { if (!workspaceSlug || !issue || !moduleId) return; - moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, issue.id); + moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, [issue.id]); }; const handleLabels = (formData: Partial) => { issueUpdate({ ...issue, ...formData }); diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index d99be8616d2..239940ef1a8 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -171,13 +171,13 @@ export const CreateUpdateIssueModal: React.FC = observer((prop const addIssueToCycle = async (issueId: string, cycleId: string) => { if (!workspaceSlug || !activeProject) return; - cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), activeProject, cycleId, issueId); + cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), activeProject, cycleId, [issueId]); }; const addIssueToModule = async (issueId: string, moduleId: string) => { if (!workspaceSlug || !activeProject) return; - moduleIssueStore.addIssueToModule(workspaceSlug.toString(), activeProject, moduleId, issueId); + moduleIssueStore.addIssueToModule(workspaceSlug.toString(), activeProject, moduleId, [issueId]); }; const createIssue = async (payload: Partial) => { diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 775cfcd1107..318d61cf556 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -1,19 +1,14 @@ -import { useState, ReactElement } from "react"; +import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { IssueService } from "services/issue"; // hooks import useLocalStorage from "hooks/use-local-storage"; -import useUser from "hooks/use-user"; -import useToast from "hooks/use-toast"; // layouts import { AppLayout } from "layouts/app-layout"; // components import { CycleIssuesHeader } from "components/headers"; -import { ExistingIssuesListModal } from "components/core"; import { CycleDetailsSidebar } from "components/cycles"; import { CycleLayoutRoot } from "components/issues/issue-layouts"; // ui @@ -21,23 +16,14 @@ import { EmptyState } from "components/common"; // assets import emptyCycle from "public/empty-state/cycle.svg"; // types -import { ISearchIssueResponse } from "types"; import { NextPageWithLayout } from "types/app"; -const issueService = new IssueService(); - const CycleDetailPage: NextPageWithLayout = () => { - const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; const { cycle: cycleStore } = useMobxStore(); - const { user } = useUser(); - - const { setToastAlert } = useToast(); - const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; @@ -52,38 +38,8 @@ const CycleDetailPage: NextPageWithLayout = () => { setValue(`${!isSidebarCollapsed}`); }; - // TODO: add this function to bulk add issues to cycle - const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { - if (!workspaceSlug || !projectId) return; - - const payload = { - issues: data.map((i) => i.id), - }; - - await issueService - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); - }); - }; - - const openIssuesListModal = () => { - setCycleIssuesListModal(true); - }; - return ( <> - {/* TODO: Update logic to bulk add issues to a cycle */} - setCycleIssuesListModal(false)} - searchParams={{ cycle: true }} - handleOnSubmit={handleAddIssuesToCycle} - /> {error ? ( { <>
- +
{cycleId && !isSidebarCollapsed && (
{ - // states - const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); // router const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; // store const { module: moduleStore } = useMobxStore(); - // hooks - const { user } = useUser(); - const { setToastAlert } = useToast(); // local storage const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; @@ -48,42 +35,12 @@ const ModuleIssuesPage: NextPageWithLayout = () => { : null ); - // TODO: add this function to bulk add issues to cycle - const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { - if (!workspaceSlug || !projectId) return; - - const payload = { - issues: data.map((i) => i.id), - }; - - await moduleService - .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the module. Please try again.", - }) - ); - }; - - const openIssuesListModal = () => { - setModuleIssuesListModal(true); - }; - const toggleSidebar = () => { setValue(`${!isSidebarCollapsed}`); }; return ( <> - {/* TODO: Update logic to bulk add issues to a cycle */} - setModuleIssuesListModal(false)} - searchParams={{ module: true }} - handleOnSubmit={handleAddIssuesToModule} - /> {error ? ( { ) : (
- +
{moduleId && !isSidebarCollapsed && (
void; updateGanttIssueStructure: (workspaceSlug: string, cycleId: string, issue: IIssue, payload: IBlockUpdateData) => void; deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; - addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => void; + addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) => void; } @@ -322,7 +322,7 @@ export class CycleIssueStore implements ICycleIssueStore { } }; - addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { + addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { const user = this.rootStore.user.currentUser ?? undefined; @@ -331,7 +331,7 @@ export class CycleIssueStore implements ICycleIssueStore { projectId, cycleId, { - issues: [issueId], + issues: issueIds, }, user ); diff --git a/web/store/module/module_issue.store.ts b/web/store/module/module_issue.store.ts index 5b5893b0d3f..165b51b622d 100644 --- a/web/store/module/module_issue.store.ts +++ b/web/store/module/module_issue.store.ts @@ -40,7 +40,7 @@ export interface IModuleIssueStore { payload: IBlockUpdateData ) => void; deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; - addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise; + addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise; removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, bridgeId: string) => Promise; } @@ -337,7 +337,7 @@ export class ModuleIssueStore implements IModuleIssueStore { } }; - addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { + addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => { try { const user = this.rootStore.user.currentUser ?? undefined; @@ -346,7 +346,7 @@ export class ModuleIssueStore implements IModuleIssueStore { projectId, moduleId, { - issues: [issueId], + issues: issueIds, }, user ); From 13389d1b2ba396784ac3e591f2a346ebd0b25df3 Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+manishg3@users.noreply.github.com> Date: Mon, 6 Nov 2023 19:05:20 +0530 Subject: [PATCH 03/62] dev: On Demand Code Build for any branch (#2668) * wip * wip * testing * wip * wip * wip * wip * image push fix * wip * wip * dynamic branch name and tag * workflow_dispatch modified * job splitting * file sharing * wip * checking * wip * wip * wip * wip * build fixes * code upload download fixes * image name change --------- Co-authored-by: sriram veeraghanta --- .github/workflows/build-branch.yml | 205 +++++++++++++++++++++++++++++ .gitignore | 2 +- 2 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-branch.yml diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml new file mode 100644 index 00000000000..26b8addd23b --- /dev/null +++ b/.github/workflows/build-branch.yml @@ -0,0 +1,205 @@ + +name: Docker Branch Build + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Dev/QA Builds' + +env: + gh_branch: ${{ github.ref_name }} + img_tag: latest + +jobs: + branch_build_and_push: + name: Build-Push Web/Space/API/Proxy Docker Image + runs-on: ubuntu-20.04 + + steps: + - name: Check out the repo + uses: actions/checkout@v3.3.0 + + - uses: ASzc/change-string-case-action@v2 + id: gh_branch_upper_lower + with: + string: ${{ env.gh_branch }} + + - uses: mad9000/actions-find-and-replace-string@2 + id: gh_branch_replace_slash + with: + source: ${{ steps.gh_branch_upper_lower.outputs.lowercase }} + find: '/' + replace: '-' + + - uses: mad9000/actions-find-and-replace-string@2 + id: gh_branch_replace_dot + with: + source: ${{ steps.gh_branch_replace_slash.outputs.value }} + find: '.' + replace: '' + + - uses: mad9000/actions-find-and-replace-string@2 + id: gh_branch_clean + with: + source: ${{ steps.gh_branch_replace_dot.outputs.value }} + find: '_' + replace: '' + - name: Uploading Proxy Source + uses: actions/upload-artifact@v3 + with: + name: proxy-src-code + path: ./nginx + - name: Uploading Backend Source + uses: actions/upload-artifact@v3 + with: + name: backend-src-code + path: ./apiserver + - name: Uploading Web Source + uses: actions/upload-artifact@v3 + with: + name: web-src-code + path: | + ./ + !./apiserver + !./nginx + !./deploy + !./space + + - name: Uploading Space Source + uses: actions/upload-artifact@v3 + with: + name: space-src-code + path: | + ./ + !./apiserver + !./nginx + !./deploy + !./web + outputs: + gh_branch_name: ${{ steps.gh_branch_clean.outputs.value }} + + branch_build_push_frontend: + runs-on: ubuntu-20.04 + needs: [ branch_build_and_push ] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Downloading Web Source Code + uses: actions/download-artifact@v3 + with: + name: web-src-code + + - name: Build and Push Frontend to Docker Container Registry + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./web/Dockerfile.web + platforms: linux/amd64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_space: + runs-on: ubuntu-20.04 + needs: [ branch_build_and_push ] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Downloading Space Source Code + uses: actions/download-artifact@v3 + with: + name: space-src-code + + - name: Build and Push Space to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./space/Dockerfile.space + platforms: linux/amd64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_backend: + runs-on: ubuntu-20.04 + needs: [ branch_build_and_push ] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Downloading Backend Source Code + uses: actions/download-artifact@v3 + with: + name: backend-src-code + + - name: Build and Push Backend to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./Dockerfile.api + platforms: linux/amd64 + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }} + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_proxy: + runs-on: ubuntu-20.04 + needs: [ branch_build_and_push ] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Downloading Proxy Source Code + uses: actions/download-artifact@v3 + with: + name: proxy-src-code + + - name: Build and Push Plane-Proxy to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7568602d31f..dcb8b86712e 100644 --- a/.gitignore +++ b/.gitignore @@ -75,7 +75,7 @@ pnpm-lock.yaml pnpm-workspace.yaml .npmrc +.secrets tmp/ - ## packages dist From 6eb0bf478511da5b82714c322f31218483396912 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 6 Nov 2023 20:42:24 +0530 Subject: [PATCH 04/62] Fix/mentions spaces fix (#2667) * feat: add mentions store to the space project * fix: added mentions highlights in read only comment cards * feat: added mention highlights in richtexteditor in space app --- .../src/ui/read-only/index.tsx | 3 ++ .../comment/comment-detail-card.tsx | 5 ++- .../issues/peek-overview/issue-details.tsx | 44 ++++++++++-------- space/hooks/use-editor-suggestions.tsx | 13 ++++++ space/store/mentions.store.ts | 45 +++++++++++++++++++ space/store/root.ts | 3 ++ 6 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 space/hooks/use-editor-suggestions.tsx create mode 100644 space/store/mentions.store.ts diff --git a/packages/editor/rich-text-editor/src/ui/read-only/index.tsx b/packages/editor/rich-text-editor/src/ui/read-only/index.tsx index dc058cf8937..46905f263d2 100644 --- a/packages/editor/rich-text-editor/src/ui/read-only/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/read-only/index.tsx @@ -8,6 +8,7 @@ interface IRichTextReadOnlyEditor { noBorder?: boolean; borderOnFocus?: boolean; customClassName?: string; + mentionHighlights?: string[]; } interface RichTextReadOnlyEditorProps extends IRichTextReadOnlyEditor { @@ -26,10 +27,12 @@ const RichReadOnlyEditor = ({ customClassName, value, forwardedRef, + mentionHighlights, }: RichTextReadOnlyEditorProps) => { const editor = useReadOnlyEditor({ value, forwardedRef, + mentionHighlights, }); const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index 1d9ff058648..29801c9e648 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -15,6 +15,7 @@ import { timeAgo } from "helpers/date-time.helper"; import { Comment } from "types/issue"; // services import fileService from "services/file.service"; +import useEditorSuggestions from "hooks/use-editor-suggestions"; type Props = { workspaceSlug: string; @@ -28,6 +29,8 @@ export const CommentCard: React.FC = observer((props) => { // states const [isEditing, setIsEditing] = useState(false); + const mentionsConfig = useEditorSuggestions(); + const editorRef = React.useRef(null); const showEditorRef = React.useRef(null); @@ -135,7 +138,7 @@ export const CommentCard: React.FC = observer((props) => { ref={showEditorRef} value={comment.comment_html} customClassName="text-xs border border-custom-border-200 bg-custom-background-100" - mentionHighlights={userStore.currentUser?.id ? [userStore.currentUser?.id] : []} + mentionHighlights={mentionsConfig.mentionHighlights} />
diff --git a/space/components/issues/peek-overview/issue-details.tsx b/space/components/issues/peek-overview/issue-details.tsx index 24dd656513b..9b8634416c2 100644 --- a/space/components/issues/peek-overview/issue-details.tsx +++ b/space/components/issues/peek-overview/issue-details.tsx @@ -2,27 +2,33 @@ import { IssueReactions } from "components/issues/peek-overview"; import { RichReadOnlyEditor } from "@plane/rich-text-editor"; // types import { IIssue } from "types/issue"; +import useEditorSuggestions from "hooks/use-editor-suggestions"; type Props = { issueDetails: IIssue; }; -export const PeekOverviewIssueDetails: React.FC = ({ issueDetails }) => ( -
-
- {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} -
-

{issueDetails.name}

- {issueDetails.description_html !== "" && issueDetails.description_html !== "

" && ( -

" - : issueDetails.description_html} - customClassName="p-3 min-h-[50px] shadow-sm" /> - )} - -
-); +export const PeekOverviewIssueDetails: React.FC = ({ issueDetails }) => { + + const mentionConfig = useEditorSuggestions(); + + return ( +
+
+ {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} +
+

{issueDetails.name}

+ {issueDetails.description_html !== "" && issueDetails.description_html !== "

" && ( +

" + : issueDetails.description_html} + customClassName="p-3 min-h-[50px] shadow-sm" mentionHighlights={mentionConfig.mentionHighlights} /> + )} + +
+ ) +}; diff --git a/space/hooks/use-editor-suggestions.tsx b/space/hooks/use-editor-suggestions.tsx new file mode 100644 index 00000000000..0659121b7cd --- /dev/null +++ b/space/hooks/use-editor-suggestions.tsx @@ -0,0 +1,13 @@ +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const useEditorSuggestions = () => { + const { mentionsStore }: RootStore = useMobxStore(); + + return { + // mentionSuggestions: mentionsStore.mentionSuggestions, + mentionHighlights: mentionsStore.mentionHighlights, + }; +}; + +export default useEditorSuggestions; diff --git a/space/store/mentions.store.ts b/space/store/mentions.store.ts new file mode 100644 index 00000000000..ca4a1a3c1e8 --- /dev/null +++ b/space/store/mentions.store.ts @@ -0,0 +1,45 @@ +import { IMentionHighlight } from "@plane/lite-text-editor"; +import { RootStore } from "./root"; +import { computed, makeObservable } from "mobx"; + +export interface IMentionsStore { + // mentionSuggestions: IMentionSuggestion[]; + mentionHighlights: IMentionHighlight[]; +} + +export class MentionsStore implements IMentionsStore{ + + // root store + rootStore; + + constructor(_rootStore: RootStore ){ + + // rootStore + this.rootStore = _rootStore; + + makeObservable(this, { + mentionHighlights: computed, + // mentionSuggestions: computed + }) + } + + // get mentionSuggestions() { + // const projectMembers = this.rootStore.project.project. + + // const suggestions = projectMembers === null ? [] : projectMembers.map((member) => ({ + // id: member.member.id, + // type: "User", + // title: member.member.display_name, + // subtitle: member.member.email ?? "", + // avatar: member.member.avatar, + // redirect_uri: `/${member.workspace.slug}/profile/${member.member.id}`, + // })) + + // return suggestions + // } + + get mentionHighlights() { + const user = this.rootStore.user.currentUser; + return user ? [user.id] : [] + } +} \ No newline at end of file diff --git a/space/store/root.ts b/space/store/root.ts index 6b87020ef7a..22b951d2070 100644 --- a/space/store/root.ts +++ b/space/store/root.ts @@ -5,6 +5,7 @@ import UserStore from "./user"; import IssueStore, { IIssueStore } from "./issue"; import ProjectStore, { IProjectStore } from "./project"; import IssueDetailStore, { IIssueDetailStore } from "./issue_details"; +import { IMentionsStore, MentionsStore } from "./mentions.store"; enableStaticRendering(typeof window === "undefined"); @@ -13,11 +14,13 @@ export class RootStore { issue: IIssueStore; issueDetails: IIssueDetailStore; project: IProjectStore; + mentionsStore: IMentionsStore; constructor() { this.user = new UserStore(this); this.issue = new IssueStore(this); this.project = new ProjectStore(this); this.issueDetails = new IssueDetailStore(this); + this.mentionsStore = new MentionsStore(this); } } From a6dea3af23650edeb9b2b4848db4f24f183d4105 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 6 Nov 2023 20:43:10 +0530 Subject: [PATCH 05/62] fix: render the estimate select if estimate is enabled for the project (#2663) --- .../issues/issue-layouts/list/block.tsx | 10 +++---- .../issues/issue-layouts/list/blocks-list.tsx | 8 +++--- .../issues/issue-layouts/list/default.tsx | 28 +++++++++---------- .../issues/issue-layouts/list/properties.tsx | 26 ++++++++--------- .../list/roots/archived-issue-root.tsx | 4 +-- .../issue-layouts/list/roots/cycle-root.tsx | 4 +-- .../issue-layouts/list/roots/module-root.tsx | 4 +-- .../list/roots/profile-issues-root.tsx | 4 +-- .../issue-layouts/list/roots/project-root.tsx | 4 +-- .../issue-layouts/properties/estimates.tsx | 24 +++++++++------- 10 files changed, 60 insertions(+), 56 deletions(-) diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 52410756aeb..28f5f765a6a 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -4,20 +4,20 @@ import { IssuePeekOverview } from "components/issues/issue-peek-overview"; // ui import { Tooltip } from "@plane/ui"; // types -import { IIssue } from "types"; +import { IIssue, IIssueDisplayProperties } from "types"; interface IssueBlockProps { columnId: string; issue: IIssue; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; isReadonly?: boolean; showEmptyGroup?: boolean; } export const IssueBlock: React.FC = (props) => { - const { columnId, issue, handleIssues, quickActions, display_properties, showEmptyGroup, isReadonly } = props; + const { columnId, issue, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props; const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => { handleIssues(group_by, issueToUpdate, "update"); @@ -26,7 +26,7 @@ export const IssueBlock: React.FC = (props) => { return ( <>
- {display_properties && display_properties?.key && ( + {displayProperties && displayProperties?.key && (
{issue?.project_detail?.identifier}-{issue.sequence_id}
@@ -54,7 +54,7 @@ export const IssueBlock: React.FC = (props) => { issue={issue} isReadonly={isReadonly} handleIssues={updateIssue} - display_properties={display_properties} + displayProperties={displayProperties} showEmptyGroup={showEmptyGroup} /> {quickActions(!columnId && columnId === "null" ? null : columnId, issue)} diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 00779cc3479..22a92a15991 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; // components import { IssueBlock } from "components/issues"; // types -import { IIssue } from "types"; +import { IIssue, IIssueDisplayProperties } from "types"; interface Props { columnId: string; @@ -10,12 +10,12 @@ interface Props { isReadonly?: boolean; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; showEmptyGroup?: boolean; } export const IssueBlocksList: FC = (props) => { - const { columnId, issues, handleIssues, quickActions, display_properties, showEmptyGroup, isReadonly } = props; + const { columnId, issues, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props; return (
@@ -28,7 +28,7 @@ export const IssueBlocksList: FC = (props) => { handleIssues={handleIssues} quickActions={quickActions} isReadonly={isReadonly} - display_properties={display_properties} + displayProperties={displayProperties} showEmptyGroup={showEmptyGroup} /> )) diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 57cdeb34bdd..b9d13a92bce 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; import { ListGroupByHeaderRoot } from "./headers/group-by-root"; import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues"; // types -import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types"; +import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types"; // constants import { getValueFromObject } from "constants/issue"; @@ -16,7 +16,7 @@ export interface IGroupByList { listKey: string; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; is_list?: boolean; enableQuickIssueCreate?: boolean; showEmptyGroup?: boolean; @@ -31,7 +31,7 @@ const GroupByList: React.FC = observer((props) => { listKey, handleIssues, quickActions, - display_properties, + displayProperties, is_list = false, enableQuickIssueCreate, showEmptyGroup, @@ -59,7 +59,7 @@ const GroupByList: React.FC = observer((props) => { issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} /> @@ -86,7 +86,7 @@ export interface IList { handleDragDrop?: (result: any) => void | undefined; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; states: IState[] | null; labels: IIssueLabels[] | null; members: IUserLite[] | null; @@ -105,7 +105,7 @@ export const List: React.FC = observer((props) => { isReadonly, handleIssues, quickActions, - display_properties, + displayProperties, states, labels, members, @@ -126,7 +126,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} is_list enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} @@ -142,7 +142,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -157,7 +157,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -172,7 +172,7 @@ export const List: React.FC = observer((props) => { listKey={`key`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -187,7 +187,7 @@ export const List: React.FC = observer((props) => { listKey={`key`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -202,7 +202,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -217,7 +217,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -232,7 +232,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index c304867ac4b..92a203f3639 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -11,19 +11,19 @@ import { IssuePropertyDate } from "../properties/date"; // ui import { Tooltip } from "@plane/ui"; // types -import { IIssue, IState, TIssuePriorities } from "types"; +import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types"; export interface IKanBanProperties { columnId: string; issue: IIssue; handleIssues: (group_by: string | null, issue: IIssue) => void; - display_properties: any; + displayProperties: IIssueDisplayProperties; isReadonly?: boolean; showEmptyGroup?: boolean; } export const KanBanProperties: FC = observer((props) => { - const { columnId: group_id, issue, handleIssues, display_properties, isReadonly, showEmptyGroup } = props; + const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly, showEmptyGroup } = props; const handleState = (state: IState) => { handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id }); @@ -57,7 +57,7 @@ export const KanBanProperties: FC = observer((props) => {
{/* basic properties */} {/* state */} - {display_properties && display_properties?.state && ( + {displayProperties && displayProperties?.state && ( = observer((props) => { )} {/* priority */} - {display_properties && display_properties?.priority && ( + {displayProperties && displayProperties?.priority && ( = observer((props) => { )} {/* label */} - {display_properties && display_properties?.labels && (showEmptyGroup || issue?.labels.length > 0) && ( + {displayProperties && displayProperties?.labels && (showEmptyGroup || issue?.labels.length > 0) && ( = observer((props) => { )} {/* assignee */} - {display_properties && display_properties?.assignee && (showEmptyGroup || issue?.assignees?.length > 0) && ( + {displayProperties && displayProperties?.assignee && (showEmptyGroup || issue?.assignees?.length > 0) && ( = observer((props) => { )} {/* start date */} - {display_properties && display_properties?.start_date && (showEmptyGroup || issue?.start_date) && ( + {displayProperties && displayProperties?.start_date && (showEmptyGroup || issue?.start_date) && ( handleStartDate(date)} @@ -111,7 +111,7 @@ export const KanBanProperties: FC = observer((props) => { )} {/* target/due date */} - {display_properties && display_properties?.due_date && (showEmptyGroup || issue?.target_date) && ( + {displayProperties && displayProperties?.due_date && (showEmptyGroup || issue?.target_date) && ( handleTargetDate(date)} @@ -121,7 +121,7 @@ export const KanBanProperties: FC = observer((props) => { )} {/* estimates */} - {display_properties && display_properties?.estimate && ( + {displayProperties && displayProperties?.estimate && ( = observer((props) => { {/* extra render properties */} {/* sub-issues */} - {display_properties && display_properties?.sub_issue_count && ( + {displayProperties && displayProperties?.sub_issue_count && (
@@ -143,7 +143,7 @@ export const KanBanProperties: FC = observer((props) => { )} {/* attachments */} - {display_properties && display_properties?.attachment_count && ( + {displayProperties && displayProperties?.attachment_count && (
@@ -153,7 +153,7 @@ export const KanBanProperties: FC = observer((props) => { )} {/* link */} - {display_properties && display_properties?.link && ( + {displayProperties && displayProperties?.link && (
diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index 63c2f128194..1ddbd53923e 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -25,7 +25,7 @@ export const ArchivedIssueListLayout: FC = observer(() => { // derived values const issues = archivedIssueStore.getIssues; - const display_properties = archivedIssueFiltersStore?.userDisplayProperties || null; + const displayProperties = archivedIssueFiltersStore?.userDisplayProperties || null; const group_by: string | null = archivedIssueFiltersStore?.userDisplayFilters?.group_by || null; const handleIssues = (group_by: string | null, issue: IIssue, action: "delete" | "update") => { @@ -59,7 +59,7 @@ export const ArchivedIssueListLayout: FC = observer(() => { quickActions={(group_by, issue) => ( handleIssues(group_by, issue, "delete")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} states={states} stateGroups={stateGroups} priorities={priorities} diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index 9a67b91c1f3..7bbb37f4612 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -31,7 +31,7 @@ export const CycleListLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - const display_properties = issueFilterStore?.userDisplayProperties || null; + const displayProperties = issueFilterStore?.userDisplayProperties || null; const handleIssues = useCallback( (group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { @@ -80,7 +80,7 @@ export const CycleListLayout: React.FC = observer(() => { handleRemoveFromCycle={async () => handleIssues(group_by, issue, "remove")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} states={states} stateGroups={stateGroups} priorities={priorities} diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index fd748bc6cdc..11704f3b652 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -31,7 +31,7 @@ export const ModuleListLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - const display_properties = issueFilterStore?.userDisplayProperties || null; + const displayProperties = issueFilterStore?.userDisplayProperties || null; const handleIssues = useCallback( (group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { @@ -80,7 +80,7 @@ export const ModuleListLayout: React.FC = observer(() => { handleRemoveFromModule={async () => handleIssues(group_by, issue, "remove")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} states={states} stateGroups={stateGroups} priorities={priorities} diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index 9e4937ffdb1..e66cda2d28d 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -29,7 +29,7 @@ export const ProfileIssuesListLayout: FC = observer(() => { const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; - const display_properties = profileIssueFiltersStore?.userDisplayProperties || null; + const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null; const handleIssues = useCallback( (group_by: string | null, issue: IIssue, action: "update" | "delete") => { @@ -64,7 +64,7 @@ export const ProfileIssuesListLayout: FC = observer(() => { handleUpdate={async (data) => handleIssues(group_by, data, "update")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} states={states} stateGroups={stateGroups} priorities={priorities} diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 16ce940b52c..6a832f835e6 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -29,7 +29,7 @@ export const ListLayout: FC = observer(() => { const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; const group_by: string | null = userDisplayFilters?.group_by || null; - const display_properties = issueFilterStore?.userDisplayProperties || null; + const displayProperties = issueFilterStore?.userDisplayProperties || null; const handleIssues = useCallback( (group_by: string | null, issue: IIssue, action: "update" | "delete") => { @@ -68,7 +68,7 @@ export const ListLayout: FC = observer(() => { handleUpdate={async (data) => handleIssues(group_by, data, "update")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} states={states} stateGroups={stateGroups} priorities={priorities} diff --git a/web/components/issues/issue-layouts/properties/estimates.tsx b/web/components/issues/issue-layouts/properties/estimates.tsx index 907946ca7ed..5737389754b 100644 --- a/web/components/issues/issue-layouts/properties/estimates.tsx +++ b/web/components/issues/issue-layouts/properties/estimates.tsx @@ -1,16 +1,13 @@ import { Fragment, useState } from "react"; - -import { observer } from "mobx-react-lite"; - -// hooks import { usePopper } from "react-popper"; -import useEstimateOption from "hooks/use-estimate-option"; -// ui -import { Check, ChevronDown, Search, Triangle } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; +import { Check, ChevronDown, Search, Triangle } from "lucide-react"; +// ui import { Tooltip } from "@plane/ui"; // types import { Placement } from "@popperjs/core"; +import { useMobxStore } from "lib/mobx/store-provider"; export interface IIssuePropertyEstimates { view?: "profile" | "workspace" | "project"; @@ -27,7 +24,6 @@ export interface IIssuePropertyEstimates { export const IssuePropertyEstimates: React.FC = observer((props) => { const { - view, projectId, value, onChange, @@ -44,8 +40,6 @@ export const IssuePropertyEstimates: React.FC = observe const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const { isEstimateActive, estimatePoints } = useEstimateOption(); - const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "bottom-start", modifiers: [ @@ -58,6 +52,14 @@ export const IssuePropertyEstimates: React.FC = observe ], }); + const { project: projectStore } = useMobxStore(); + + const projectDetails = projectId ? projectStore.project_details[projectId] : null; + const isEstimateEnabled = projectDetails?.estimate !== null; + const estimates = projectId ? projectStore.estimates?.[projectId] : null; + const estimatePoints = + projectDetails && isEstimateEnabled ? estimates?.find((e) => e.id === projectDetails.estimate)?.points : null; + const options: { value: number | null; query: string; content: any }[] | undefined = (estimatePoints ?? []).map( (estimate) => ({ value: estimate.key, @@ -94,6 +96,8 @@ export const IssuePropertyEstimates: React.FC = observe ); + if (!isEstimateEnabled) return null; + return ( Date: Mon, 6 Nov 2023 20:43:34 +0530 Subject: [PATCH 06/62] style: updated layouts UI in the space app (#2671) * style: updated layouts UI in space * fix: build error --- space/components/icons/index.ts | 1 - .../icons/state-group/backlog-state-icon.tsx | 23 ------ .../state-group/cancelled-state-icon.tsx | 74 ------------------- .../state-group/completed-state-icon.tsx | 65 ---------------- space/components/icons/state-group/index.ts | 6 -- .../icons/state-group/started-state-icon.tsx | 73 ------------------ .../icons/state-group/state-group-icon.tsx | 29 -------- .../state-group/unstarted-state-icon.tsx | 55 -------------- space/components/icons/types.d.ts | 6 -- .../issues/board-views/block-state.tsx | 4 +- .../issues/board-views/kanban/block.tsx | 3 +- .../issues/board-views/kanban/header.tsx | 12 +-- .../issues/board-views/list/block.tsx | 6 +- .../issues/board-views/list/header.tsx | 14 ++-- .../issues/board-views/list/index.tsx | 4 +- .../peek-overview/comment/add-comment.tsx | 34 +++++---- web/components/issues/comment/add-comment.tsx | 17 +++-- .../issues/issue-layouts/kanban/block.tsx | 2 +- .../issues/issue-layouts/kanban/default.tsx | 2 +- .../kanban/headers/state-group.tsx | 2 +- .../activity/comment-editor.tsx | 1 + .../issues/issue-peek-overview/view.tsx | 2 +- 22 files changed, 56 insertions(+), 379 deletions(-) delete mode 100644 space/components/icons/index.ts delete mode 100644 space/components/icons/state-group/backlog-state-icon.tsx delete mode 100644 space/components/icons/state-group/cancelled-state-icon.tsx delete mode 100644 space/components/icons/state-group/completed-state-icon.tsx delete mode 100644 space/components/icons/state-group/index.ts delete mode 100644 space/components/icons/state-group/started-state-icon.tsx delete mode 100644 space/components/icons/state-group/state-group-icon.tsx delete mode 100644 space/components/icons/state-group/unstarted-state-icon.tsx delete mode 100644 space/components/icons/types.d.ts diff --git a/space/components/icons/index.ts b/space/components/icons/index.ts deleted file mode 100644 index 28162f59155..00000000000 --- a/space/components/icons/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./state-group"; diff --git a/space/components/icons/state-group/backlog-state-icon.tsx b/space/components/icons/state-group/backlog-state-icon.tsx deleted file mode 100644 index f2f62d24a64..00000000000 --- a/space/components/icons/state-group/backlog-state-icon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -// types -import type { Props } from "../types"; -// constants -import { issueGroupColors } from "constants/data"; - -export const BacklogStateIcon: React.FC = ({ - width = "14", - height = "14", - className, - color = issueGroupColors["backlog"], -}) => ( - - - -); diff --git a/space/components/icons/state-group/cancelled-state-icon.tsx b/space/components/icons/state-group/cancelled-state-icon.tsx deleted file mode 100644 index e244c191a66..00000000000 --- a/space/components/icons/state-group/cancelled-state-icon.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from "react"; -// types -import type { Props } from "../types"; -// constants -import { issueGroupColors } from "constants/data"; - -export const CancelledStateIcon: React.FC = ({ - width = "14", - height = "14", - className, - color = issueGroupColors["cancelled"], -}) => ( - - - - - - - - - - - - - -); diff --git a/space/components/icons/state-group/completed-state-icon.tsx b/space/components/icons/state-group/completed-state-icon.tsx deleted file mode 100644 index 417ebbf3ff3..00000000000 --- a/space/components/icons/state-group/completed-state-icon.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react"; -// types -import type { Props } from "../types"; -// constants -import { issueGroupColors } from "constants/data"; - -export const CompletedStateIcon: React.FC = ({ - width = "14", - height = "14", - className, - color = issueGroupColors["completed"], -}) => ( - - - - - - - - - - - - -); diff --git a/space/components/icons/state-group/index.ts b/space/components/icons/state-group/index.ts deleted file mode 100644 index 6ede38df6ff..00000000000 --- a/space/components/icons/state-group/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./backlog-state-icon"; -export * from "./cancelled-state-icon"; -export * from "./completed-state-icon"; -export * from "./started-state-icon"; -export * from "./state-group-icon"; -export * from "./unstarted-state-icon"; diff --git a/space/components/icons/state-group/started-state-icon.tsx b/space/components/icons/state-group/started-state-icon.tsx deleted file mode 100644 index 4ebd1771f7f..00000000000 --- a/space/components/icons/state-group/started-state-icon.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from "react"; -// types -import type { Props } from "../types"; -// constants -import { issueGroupColors } from "constants/data"; - -export const StartedStateIcon: React.FC = ({ - width = "14", - height = "14", - className, - color = issueGroupColors["started"], -}) => ( - - - - - - - - - - - - -); diff --git a/space/components/icons/state-group/state-group-icon.tsx b/space/components/icons/state-group/state-group-icon.tsx deleted file mode 100644 index 1af52340036..00000000000 --- a/space/components/icons/state-group/state-group-icon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// icons -import { - BacklogStateIcon, - CancelledStateIcon, - CompletedStateIcon, - StartedStateIcon, - UnstartedStateIcon, -} from "components/icons"; -import { TIssueGroupKey } from "types/issue"; - -type Props = { - stateGroup: TIssueGroupKey; - color: string; - className?: string; - height?: string; - width?: string; -}; - -export const StateGroupIcon: React.FC = ({ stateGroup, className, color, height = "12px", width = "12px" }) => { - if (stateGroup === "backlog") - return ; - else if (stateGroup === "cancelled") - return ; - else if (stateGroup === "completed") - return ; - else if (stateGroup === "started") - return ; - else return ; -}; diff --git a/space/components/icons/state-group/unstarted-state-icon.tsx b/space/components/icons/state-group/unstarted-state-icon.tsx deleted file mode 100644 index f79bc00fc68..00000000000 --- a/space/components/icons/state-group/unstarted-state-icon.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; -// types -import type { Props } from "../types"; -// constants -import { issueGroupColors } from "constants/data"; - -export const UnstartedStateIcon: React.FC = ({ - width = "14", - height = "14", - className, - color = issueGroupColors["unstarted"], -}) => ( - - - - - - - - - - -); diff --git a/space/components/icons/types.d.ts b/space/components/icons/types.d.ts deleted file mode 100644 index f82a181472a..00000000000 --- a/space/components/icons/types.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type Props = { - width?: string | number; - height?: string | number; - color?: string; - className?: string; -}; diff --git a/space/components/issues/board-views/block-state.tsx b/space/components/issues/board-views/block-state.tsx index 16792c81bc5..2daba1226bc 100644 --- a/space/components/issues/board-views/block-state.tsx +++ b/space/components/issues/board-views/block-state.tsx @@ -1,3 +1,5 @@ +// ui +import { StateGroupIcon } from "@plane/ui"; // constants import { issueGroupFilter } from "constants/data"; @@ -8,7 +10,7 @@ export const IssueBlockState = ({ state }: any) => { return (
- +
{state?.name}
diff --git a/space/components/issues/board-views/kanban/block.tsx b/space/components/issues/board-views/kanban/block.tsx index b4de76f2b67..b2effc4adad 100644 --- a/space/components/issues/board-views/kanban/block.tsx +++ b/space/components/issues/board-views/kanban/block.tsx @@ -7,7 +7,6 @@ import { useMobxStore } from "lib/mobx/store-provider"; // components import { IssueBlockPriority } from "components/issues/board-views/block-priority"; import { IssueBlockState } from "components/issues/board-views/block-state"; -import { IssueBlockLabels } from "components/issues/board-views/block-labels"; import { IssueBlockDueDate } from "components/issues/board-views/block-due-date"; // interfaces import { IIssue } from "types/issue"; @@ -37,7 +36,7 @@ export const IssueListBlock = observer(({ issue }: { issue: IIssue }) => { }; return ( -
+
{/* id */}
{projectStore?.project?.identifier}-{issue?.sequence_id} diff --git a/space/components/issues/board-views/kanban/header.tsx b/space/components/issues/board-views/kanban/header.tsx index 5645e2b3bd6..8f2f2849635 100644 --- a/space/components/issues/board-views/kanban/header.tsx +++ b/space/components/issues/board-views/kanban/header.tsx @@ -4,8 +4,8 @@ import { observer } from "mobx-react-lite"; import { IIssueState } from "types/issue"; // constants import { issueGroupFilter } from "constants/data"; -// icons -import { StateGroupIcon } from "components/icons"; +// ui +import { StateGroupIcon } from "@plane/ui"; // mobx hook import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; @@ -18,11 +18,11 @@ export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { if (stateGroup === null) return <>; return ( -
-
- +
+
+
-
{state?.name}
+
{state?.name}
{store.issue.getCountOfIssuesByState(state.id)} diff --git a/space/components/issues/board-views/list/block.tsx b/space/components/issues/board-views/list/block.tsx index bdf39b84fce..57011d033fb 100644 --- a/space/components/issues/board-views/list/block.tsx +++ b/space/components/issues/board-views/list/block.tsx @@ -38,10 +38,10 @@ export const IssueListBlock: FC<{ issue: IIssue }> = observer((props) => { }; return ( -
-
+
+
{/* id */} -
+
{projectStore?.project?.identifier}-{issue?.sequence_id}
{/* name */} diff --git a/space/components/issues/board-views/list/header.tsx b/space/components/issues/board-views/list/header.tsx index 83312e7b9aa..fc7e5ef61fe 100644 --- a/space/components/issues/board-views/list/header.tsx +++ b/space/components/issues/board-views/list/header.tsx @@ -2,8 +2,8 @@ import { observer } from "mobx-react-lite"; // interfaces import { IIssueState } from "types/issue"; -// icons -import { StateGroupIcon } from "components/icons"; +// ui +import { StateGroupIcon } from "@plane/ui"; // constants import { issueGroupFilter } from "constants/data"; // mobx hook @@ -18,12 +18,12 @@ export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { if (stateGroup === null) return <>; return ( -
-
- +
+
+
-
{state?.name}
-
{store.issue.getCountOfIssuesByState(state.id)}
+
{state?.name}
+
{store.issue.getCountOfIssuesByState(state.id)}
); }); diff --git a/space/components/issues/board-views/list/index.tsx b/space/components/issues/board-views/list/index.tsx index 1c6900dd964..d6b11d026d1 100644 --- a/space/components/issues/board-views/list/index.tsx +++ b/space/components/issues/board-views/list/index.tsx @@ -27,9 +27,7 @@ export const IssueListView = observer(() => { ))}
) : ( -
- No Issues are available. -
+
No issues.
)}
))} diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index c7e7a468e9d..f70a2c5aad2 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // ui -import { SecondaryButton } from "components/ui"; +import { Button } from "@plane/ui"; // types import { Comment } from "types/issue"; // components @@ -29,7 +29,6 @@ export const AddComment: React.FC = observer((props) => { const { handleSubmit, control, - setValue, watch, formState: { isSubmitting }, reset, @@ -85,27 +84,30 @@ export const AddComment: React.FC = observer((props) => { ? watch("comment_html") : value } - customClassName="p-3 min-h-[50px] shadow-sm" + customClassName="p-2" + editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} onChange={(comment_json: Object, comment_html: string) => { onChange(comment_html); }} + submitButton={ + + } /> )} /> - - { - userStore.requiredLogin(() => { - handleSubmit(onSubmit)(e); - }); - }} - type="submit" - disabled={isSubmitting || disabled} - className="mt-2" - > - {isSubmitting ? "Adding..." : "Comment"} -
); diff --git a/web/components/issues/comment/add-comment.tsx b/web/components/issues/comment/add-comment.tsx index 4a022b578bd..28c986bc7ce 100644 --- a/web/components/issues/comment/add-comment.tsx +++ b/web/components/issues/comment/add-comment.tsx @@ -88,7 +88,8 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit, showAc deleteFile={fileService.deleteImage} ref={editorRef} value={!commentValue || commentValue === "" ? "

" : commentValue} - customClassName="p-3 min-h-[100px] shadow-sm" + customClassName="p-2 h-full" + editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)} commentAccessSpecifier={ @@ -98,15 +99,21 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit, showAc } mentionSuggestions={editorSuggestions.mentionSuggestions} mentionHighlights={editorSuggestions.mentionHighlights} + submitButton={ + + } /> )} /> )} /> - -
diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 34ed0238276..d9165aa2773 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -61,7 +61,7 @@ export const KanbanIssueBlock: React.FC = (props) => { )}
diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 1e252895b80..1f41cf39ba4 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -55,7 +55,7 @@ const GroupByKanBan: React.FC = observer((props) => { kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string); return ( -
+
{list && list.length > 0 && list.map((_list: any) => ( diff --git a/web/components/issues/issue-layouts/kanban/headers/state-group.tsx b/web/components/issues/issue-layouts/kanban/headers/state-group.tsx index 33c70bdddc6..47d258c36f5 100644 --- a/web/components/issues/issue-layouts/kanban/headers/state-group.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/state-group.tsx @@ -17,7 +17,7 @@ export interface IStateGroupHeader { } export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => ( -
+
); diff --git a/web/components/issues/issue-peek-overview/activity/comment-editor.tsx b/web/components/issues/issue-peek-overview/activity/comment-editor.tsx index 91b3bf7f19a..1eccfb33e00 100644 --- a/web/components/issues/issue-peek-overview/activity/comment-editor.tsx +++ b/web/components/issues/issue-peek-overview/activity/comment-editor.tsx @@ -90,6 +90,7 @@ export const IssueCommentEditor: React.FC = (props) => { ref={editorRef} value={!commentValue || commentValue === "" ? "

" : commentValue} customClassName="p-2 h-full" + editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} mentionSuggestions={editorSuggestions.mentionSuggestions} mentionHighlights={editorSuggestions.mentionHighlights} diff --git a/web/components/issues/issue-peek-overview/view.tsx b/web/components/issues/issue-peek-overview/view.tsx index a44fbedaffb..3342cbf4065 100644 --- a/web/components/issues/issue-peek-overview/view.tsx +++ b/web/components/issues/issue-peek-overview/view.tsx @@ -250,7 +250,7 @@ export const IssueView: FC = observer((props) => { issue && ( <> {["side-peek", "modal"].includes(peekMode) ? ( -
+
Date: Mon, 6 Nov 2023 20:43:54 +0530 Subject: [PATCH 07/62] chore: update avatar group logic (#2672) --- packages/ui/src/avatar/avatar-group.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/avatar/avatar-group.tsx b/packages/ui/src/avatar/avatar-group.tsx index 4abb4a93b90..129f9e3afef 100644 --- a/packages/ui/src/avatar/avatar-group.tsx +++ b/packages/ui/src/avatar/avatar-group.tsx @@ -35,8 +35,11 @@ export const AvatarGroup: React.FC = (props) => { // calculate total length of avatars inside the group const totalAvatars = React.Children.toArray(children).length; + // if avatars are equal to max + 1, then we need to show the last avatar as well, if avatars are more than max + 1, then we need to show the count of the remaining avatars + const maxAvatarsToRender = totalAvatars <= max + 1 ? max + 1 : max; + // slice the children to the maximum number of avatars - const avatars = React.Children.toArray(children).slice(0, max); + const avatars = React.Children.toArray(children).slice(0, maxAvatarsToRender); // assign the necessary props from the AvatarGroup component to the Avatar components const avatarsWithUpdatedProps = avatars.map((avatar) => { @@ -58,7 +61,7 @@ export const AvatarGroup: React.FC = (props) => { {avatar}
))} - {max < totalAvatars && ( + {maxAvatarsToRender < totalAvatars && ( Date: Mon, 6 Nov 2023 21:00:36 +0530 Subject: [PATCH 08/62] fix: In kanban issues can be shifted between the column in order_by (#2676) --- .../issues/issue-layouts/kanban/block.tsx | 2 +- .../issues/issue-layouts/kanban/default.tsx | 67 +++++++-- .../issue-layouts/kanban/roots/cycle-root.tsx | 129 +++++++++++------- .../kanban/roots/module-root.tsx | 128 ++++++++++------- .../kanban/roots/profile-issues-root.tsx | 124 ++++++++++------- .../kanban/roots/project-root.tsx | 129 +++++++++++------- .../issues/issue-layouts/kanban/swimlanes.tsx | 26 ++++ .../issue-layouts/list/roots/project-root.tsx | 55 ++++---- .../roots/project-layout-root.tsx | 18 ++- .../cycle/cycle_issue_calendar_view.store.ts | 4 +- .../cycle/cycle_issue_kanban_view.store.ts | 8 +- web/store/issue/issue_calendar_view.store.ts | 4 +- web/store/issue/issue_kanban_view.store.ts | 8 +- .../module_issue_calendar_view.store.ts | 4 +- .../module/module_issue_kanban_view.store.ts | 8 +- .../project_view_issue_calendar_view.store.ts | 4 +- 16 files changed, 446 insertions(+), 272 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index d9165aa2773..9d4c8aedd23 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -42,7 +42,7 @@ export const KanbanIssueBlock: React.FC = (props) => { return ( <> - + {(provided, snapshot) => (
= observer((props) => { @@ -38,6 +41,7 @@ const GroupByKanBan: React.FC = observer((props) => { issues, sub_group_by, group_by, + order_by, sub_group_id = "null", list, listKey, @@ -49,6 +53,7 @@ const GroupByKanBan: React.FC = observer((props) => { kanBanToggle, handleKanBanToggle, enableQuickIssueCreate, + isDragStarted, } = props; const verticalAlignPosition = (_list: any) => @@ -59,7 +64,9 @@ const GroupByKanBan: React.FC = observer((props) => { {list && list.length > 0 && list.map((_list: any) => ( -
+
{sub_group_by === null && (
= observer((props) => { verticalAlignPosition(_list) ? `w-[0px] overflow-hidden` : `w-full transition-all` }`} > - + {(provided: any, snapshot: any) => (
= observer((props) => { /> ) : ( isDragDisabled && ( -
+
{/*
Drop here
*/}
) )} + {provided.placeholder}
)}
- {enableQuickIssueCreate && ( - + +
+ {enableQuickIssueCreate && ( + + )} +
+ + {isDragStarted && isDragDisabled && ( +
+
+ {`This board is ordered by "${replaceUnderscoreIfSnakeCase( + order_by ? (order_by[0] === "-" ? order_by.slice(1) : order_by) : "created_at" + )}"`} +
+
)}
))} @@ -131,8 +155,8 @@ export interface IKanBan { issues: any; sub_group_by: string | null; group_by: string | null; + order_by: string | null; sub_group_id?: string; - handleDragDrop?: (result: any) => void | undefined; handleIssues: ( sub_group_by: string | null, group_by: string | null, @@ -151,6 +175,7 @@ export interface IKanBan { members: any; projects: any; enableQuickIssueCreate?: boolean; + isDragStarted?: boolean; } export const KanBan: React.FC = observer((props) => { @@ -158,6 +183,7 @@ export const KanBan: React.FC = observer((props) => { issues, sub_group_by, group_by, + order_by, sub_group_id = "null", handleIssues, quickActions, @@ -172,6 +198,7 @@ export const KanBan: React.FC = observer((props) => { members, projects, enableQuickIssueCreate, + isDragStarted, } = props; const { issueKanBanView: issueKanBanViewStore } = useMobxStore(); @@ -182,6 +209,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -201,6 +230,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -220,6 +251,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -239,6 +272,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -258,6 +293,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -277,6 +314,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -296,6 +335,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )}
diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index 82f4920c091..facf2ec9437 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext } from "@hello-pangea/dnd"; @@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanSwimLanes } from "../swimlanes"; import { KanBan } from "../default"; import { CycleIssueQuickActions } from "components/issues"; +import { Spinner } from "@plane/ui"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types @@ -37,6 +38,8 @@ export const CycleKanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; + const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null; + const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; const displayProperties = issueFilterStore?.userDisplayProperties || null; @@ -45,7 +48,15 @@ export const CycleKanBanLayout: React.FC = observer(() => { ? "swimlanes" : "default"; + const [isDragStarted, setIsDragStarted] = useState(false); + + const onDragStart = () => { + setIsDragStarted(true); + }; + const onDragEnd = (result: any) => { + setIsDragStarted(false); + if (!result) return; if ( @@ -99,60 +110,72 @@ export const CycleKanBanLayout: React.FC = observer(() => { : null; return ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + <> + {cycleIssueStore.loader ? ( +
+ +
+ ) : ( +
+ + {currentKanBanView === "default" ? ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> - )} - displayProperties={displayProperties} - kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + ) : ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> )} - displayProperties={displayProperties} - kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - )} - -
+
+
+ )} + ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index be40c6bcab4..c37cc555034 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext } from "@hello-pangea/dnd"; @@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanSwimLanes } from "../swimlanes"; import { KanBan } from "../default"; import { ModuleIssueQuickActions } from "components/issues"; +import { Spinner } from "@plane/ui"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types @@ -36,6 +37,8 @@ export const ModuleKanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; + const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null; + const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; const displayProperties = issueFilterStore?.userDisplayProperties || null; @@ -44,7 +47,14 @@ export const ModuleKanBanLayout: React.FC = observer(() => { ? "swimlanes" : "default"; + const [isDragStarted, setIsDragStarted] = useState(false); + + const onDragStart = () => { + setIsDragStarted(true); + }; + const onDragEnd = (result: any) => { + setIsDragStarted(false); if (!result) return; if ( @@ -98,60 +108,72 @@ export const ModuleKanBanLayout: React.FC = observer(() => { : null; return ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + <> + {moduleIssueStore.loader ? ( +
+ +
+ ) : ( +
+ + {currentKanBanView === "default" ? ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> - )} - displayProperties={displayProperties} - kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + ) : ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> )} - displayProperties={displayProperties} - kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - )} - -
+
+
+ )} + ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index 2849315b4dd..8d0cf172976 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback } from "react"; +import { FC, useCallback, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext } from "@hello-pangea/dnd"; @@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanSwimLanes } from "../swimlanes"; import { KanBan } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; +import { Spinner } from "@plane/ui"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; // types @@ -34,6 +35,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; + const order_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.order_by || null; + const userDisplayFilters = profileIssueFiltersStore?.userDisplayFilters || null; const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null; @@ -42,7 +45,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { ? "swimlanes" : "default"; + const [isDragStarted, setIsDragStarted] = useState(false); + + const onDragStart = () => { + setIsDragStarted(true); + }; + const onDragEnd = (result: any) => { + setIsDragStarted(false); if (!result) return; if ( @@ -83,58 +93,70 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { const projects = projectStore?.workspaceProjects || null; return ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + <> + {profileIssuesStore.loader ? ( +
+ +
+ ) : ( +
+ + {currentKanBanView === "default" ? ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={issueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> - )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + ) : ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={issueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - )} - -
+
+
+ )} + ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx index 179d628b4b0..52e60813e39 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { useRouter } from "next/router"; import { DragDropContext } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; @@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanSwimLanes } from "../swimlanes"; import { KanBan } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; +import { Spinner } from "@plane/ui"; // types import { IIssue } from "types"; // constants @@ -34,6 +35,8 @@ export const KanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; + const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null; + const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; const displayProperties = issueFilterStore?.userDisplayProperties || null; @@ -42,12 +45,22 @@ export const KanBanLayout: React.FC = observer(() => { ? "swimlanes" : "default"; + const [isDragStarted, setIsDragStarted] = useState(false); + + const onDragStart = () => { + setIsDragStarted(true); + }; + const onDragEnd = (result: any) => { + setIsDragStarted(false); + if (!result) return; if ( result.destination && result.source && + result.source.droppableId && + result.destination.droppableId && result.destination.droppableId === result.source.droppableId && result.destination.index === result.source.index ) @@ -87,59 +100,71 @@ export const KanBanLayout: React.FC = observer(() => { : null; return ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + <> + {issueStore.loader ? ( +
+ +
+ ) : ( +
+ + {currentKanBanView === "default" ? ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={issueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members?.map((m) => m.member) ?? null} + projects={projects} + enableQuickIssueCreate + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> - )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - enableQuickIssueCreate - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + ) : ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={issueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - )} - -
+
+
+ )} + ); }); diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 662a9723475..e25ddea3d66 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -58,6 +58,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ }; interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { + order_by: string | null; showEmptyGroup: boolean; states: IState[] | null; stateGroups: any; @@ -76,12 +77,14 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { displayProperties: IIssueDisplayProperties; kanBanToggle: any; handleKanBanToggle: any; + isDragStarted?: boolean; } const SubGroupSwimlane: React.FC = observer((props) => { const { issues, sub_group_by, group_by, + order_by, list, listKey, handleIssues, @@ -96,6 +99,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { labels, members, projects, + isDragStarted, } = props; const calculateIssueCount = (column_id: string) => { @@ -133,6 +137,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { issues={issues?.[getValueFromObject(_list, listKey) as string]} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} sub_group_id={getValueFromObject(_list, listKey) as string} handleIssues={handleIssues} quickActions={quickActions} @@ -147,6 +152,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { members={members} projects={projects} enableQuickIssueCreate + isDragStarted={isDragStarted} />
)} @@ -160,6 +166,7 @@ export interface IKanBanSwimLanes { issues: any; sub_group_by: string | null; group_by: string | null; + order_by: string | null; handleIssues: ( sub_group_by: string | null, group_by: string | null, @@ -177,6 +184,7 @@ export interface IKanBanSwimLanes { labels: IIssueLabels[] | null; members: IUserLite[] | null; projects: IProject[] | null; + isDragStarted?: boolean; } export const KanBanSwimLanes: React.FC = observer((props) => { @@ -184,6 +192,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues, sub_group_by, group_by, + order_by, handleIssues, quickActions, displayProperties, @@ -196,6 +205,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels, members, projects, + isDragStarted, } = props; return ( @@ -291,6 +301,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={projects} listKey={`id`} handleIssues={handleIssues} @@ -305,6 +316,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -313,6 +325,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={states} listKey={`id`} handleIssues={handleIssues} @@ -327,6 +340,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -335,6 +349,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={states} listKey={`id`} handleIssues={handleIssues} @@ -349,6 +364,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -357,6 +373,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={stateGroups} listKey={`key`} handleIssues={handleIssues} @@ -371,6 +388,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -379,6 +397,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={priorities} listKey={`key`} handleIssues={handleIssues} @@ -393,6 +412,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -401,6 +421,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={labels ? [...labels, { id: "None", name: "None" }] : labels} listKey={`id`} handleIssues={handleIssues} @@ -415,6 +436,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -423,6 +445,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={members ? [...members, { id: "None", display_name: "None" }] : members} listKey={`id`} handleIssues={handleIssues} @@ -437,6 +460,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -445,6 +469,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={members} listKey={`id`} handleIssues={handleIssues} @@ -459,6 +484,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )}
diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 6a832f835e6..8ed96d54ce0 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -6,6 +6,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; // components import { List } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; +import { Spinner } from "@plane/ui"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types @@ -56,29 +57,37 @@ export const ListLayout: FC = observer(() => { : null; return ( -
- ( - handleIssues(group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(group_by, data, "update")} + <> + {issueStore.loader ? ( +
+ +
+ ) : ( +
+ ( + handleIssues(group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members?.map((m) => m.member) ?? null} + projects={projects} + enableQuickIssueCreate + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} + showEmptyGroup={userDisplayFilters.show_empty_groups} /> - )} - displayProperties={displayProperties} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - enableQuickIssueCreate - estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} - showEmptyGroup={userDisplayFilters.show_empty_groups} - /> -
+
+ )} + ); }); diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index 6990c43ef00..0f5cac86b83 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -22,13 +22,19 @@ export const ProjectLayoutRoot: React.FC = observer(() => { const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore(); - useSWR(workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => { - if (workspaceSlug && projectId) { - await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString()); - - await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString()); + const { isLoading } = useSWR( + workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, + async () => { + if (workspaceSlug && projectId) { + await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString()); + await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString()); + } } - }); + ); + + console.log("--"); + console.log("isLoading -- -->", isLoading); + console.log("--"); const activeLayout = issueFilterStore.userDisplayFilters.layout; diff --git a/web/store/cycle/cycle_issue_calendar_view.store.ts b/web/store/cycle/cycle_issue_calendar_view.store.ts index fa80f39ac24..3b695d69778 100644 --- a/web/store/cycle/cycle_issue_calendar_view.store.ts +++ b/web/store/cycle/cycle_issue_calendar_view.store.ts @@ -36,8 +36,8 @@ export class CycleIssueCalendarViewStore implements ICycleIssueCalendarViewStore projectId: projectId, }; - const droppableSourceColumnId = source.droppableId; - const droppableDestinationColumnId = destination.droppableId; + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; if (droppableSourceColumnId === droppableDestinationColumnId) return; diff --git a/web/store/cycle/cycle_issue_kanban_view.store.ts b/web/store/cycle/cycle_issue_kanban_view.store.ts index b007e11ad23..0ecc96e60ad 100644 --- a/web/store/cycle/cycle_issue_kanban_view.store.ts +++ b/web/store/cycle/cycle_issue_kanban_view.store.ts @@ -95,9 +95,9 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; @@ -315,9 +315,9 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; diff --git a/web/store/issue/issue_calendar_view.store.ts b/web/store/issue/issue_calendar_view.store.ts index 5f9cc89bf1c..881e2ee836d 100644 --- a/web/store/issue/issue_calendar_view.store.ts +++ b/web/store/issue/issue_calendar_view.store.ts @@ -35,8 +35,8 @@ export class IssueCalendarViewStore implements IIssueCalendarViewStore { projectId: projectId, }; - const droppableSourceColumnId = source.droppableId; - const droppableDestinationColumnId = destination.droppableId; + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; if (droppableSourceColumnId === droppableDestinationColumnId) return; diff --git a/web/store/issue/issue_kanban_view.store.ts b/web/store/issue/issue_kanban_view.store.ts index 25d217b7fb6..827972694ea 100644 --- a/web/store/issue/issue_kanban_view.store.ts +++ b/web/store/issue/issue_kanban_view.store.ts @@ -95,9 +95,9 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; @@ -315,9 +315,9 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; diff --git a/web/store/module/module_issue_calendar_view.store.ts b/web/store/module/module_issue_calendar_view.store.ts index 313745a18d7..3bfed3140a9 100644 --- a/web/store/module/module_issue_calendar_view.store.ts +++ b/web/store/module/module_issue_calendar_view.store.ts @@ -36,8 +36,8 @@ export class ModuleIssueCalendarViewStore implements IModuleIssueCalendarViewSto projectId: projectId, }; - const droppableSourceColumnId = source.droppableId; - const droppableDestinationColumnId = destination.droppableId; + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; if (droppableSourceColumnId === droppableDestinationColumnId) return; diff --git a/web/store/module/module_issue_kanban_view.store.ts b/web/store/module/module_issue_kanban_view.store.ts index a8584a7ff2a..82e210f29ff 100644 --- a/web/store/module/module_issue_kanban_view.store.ts +++ b/web/store/module/module_issue_kanban_view.store.ts @@ -95,9 +95,9 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; @@ -315,9 +315,9 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; diff --git a/web/store/project-view/project_view_issue_calendar_view.store.ts b/web/store/project-view/project_view_issue_calendar_view.store.ts index 2f70df136d6..9bce218aee8 100644 --- a/web/store/project-view/project_view_issue_calendar_view.store.ts +++ b/web/store/project-view/project_view_issue_calendar_view.store.ts @@ -36,8 +36,8 @@ export class ProjectViewIssueCalendarViewStore implements IProjectViewIssueCalen projectId: projectId, }; - const droppableSourceColumnId = source.droppableId; - const droppableDestinationColumnId = destination.droppableId; + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; if (droppableSourceColumnId === droppableDestinationColumnId) return; From b372ccfdb351d2ce2c6c2938d5cb823db2db778b Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:00:49 +0530 Subject: [PATCH 09/62] fix: slack integration workflow (#2675) * fix: slack integration workflow * dev: add slack client id as configuration * fix: clean up * fix: added env to turbo --------- Co-authored-by: sriram veeraghanta --- apiserver/plane/api/urls/__init__.py | 2 +- .../api/urls/{configuration.py => config.py} | 0 apiserver/plane/api/views/config.py | 1 + apiserver/plane/api/views/integration/base.py | 15 +++- .../plane/api/views/integration/slack.py | 44 ++++++++--- apiserver/plane/utils/integrations/slack.py | 20 +++++ turbo.json | 3 +- web/pages/api/slack-redirect.ts | 23 ------ web/pages/installations/[provider]/index.tsx | 78 ++++++++----------- web/services/app_installation.service.ts | 12 --- 10 files changed, 99 insertions(+), 99 deletions(-) rename apiserver/plane/api/urls/{configuration.py => config.py} (100%) create mode 100644 apiserver/plane/utils/integrations/slack.py delete mode 100644 web/pages/api/slack-redirect.ts diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py index 49c2b772e19..e4f3718f59c 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/api/urls/__init__.py @@ -1,7 +1,7 @@ from .analytic import urlpatterns as analytic_urls from .asset import urlpatterns as asset_urls from .authentication import urlpatterns as authentication_urls -from .configuration import urlpatterns as configuration_urls +from .config import urlpatterns as configuration_urls from .cycle import urlpatterns as cycle_urls from .estimate import urlpatterns as estimate_urls from .gpt import urlpatterns as gpt_urls diff --git a/apiserver/plane/api/urls/configuration.py b/apiserver/plane/api/urls/config.py similarity index 100% rename from apiserver/plane/api/urls/configuration.py rename to apiserver/plane/api/urls/config.py diff --git a/apiserver/plane/api/views/config.py b/apiserver/plane/api/views/config.py index f59ca04a053..687cb211c4d 100644 --- a/apiserver/plane/api/views/config.py +++ b/apiserver/plane/api/views/config.py @@ -30,4 +30,5 @@ def get(self, request): data["email_password_login"] = ( os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1" ) + data["slack"] = os.environ.get("SLACK_CLIENT_ID", None) return Response(data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 65b94d0a1e5..cc911b53716 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -1,6 +1,6 @@ # Python improts import uuid - +import requests # Django imports from django.contrib.auth.hashers import make_password @@ -25,7 +25,7 @@ delete_github_installation, ) from plane.api.permissions import WorkSpaceAdminPermission - +from plane.utils.integrations.slack import slack_oauth class IntegrationViewSet(BaseViewSet): serializer_class = IntegrationSerializer @@ -98,12 +98,19 @@ def create(self, request, slug, provider): config = {"installation_id": installation_id} if provider == "slack": - metadata = request.data.get("metadata", {}) + code = request.data.get("code", False) + + if not code: + return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST) + + slack_response = slack_oauth(code=code) + + metadata = slack_response access_token = metadata.get("access_token", False) team_id = metadata.get("team", {}).get("id", False) if not metadata or not access_token or not team_id: return Response( - {"error": "Access token and team id is required"}, + {"error": "Slack could not be installed. Please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) config = {"team_id": team_id, "access_token": access_token} diff --git a/apiserver/plane/api/views/integration/slack.py b/apiserver/plane/api/views/integration/slack.py index 83aa951baca..863b6ba0cf2 100644 --- a/apiserver/plane/api/views/integration/slack.py +++ b/apiserver/plane/api/views/integration/slack.py @@ -11,6 +11,7 @@ from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember from plane.api.serializers import SlackProjectSyncSerializer from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission +from plane.utils.integrations.slack import slack_oauth class SlackProjectSyncViewSet(BaseViewSet): @@ -32,25 +33,46 @@ def get_queryset(self): ) def create(self, request, slug, project_id, workspace_integration_id): - serializer = SlackProjectSyncSerializer(data=request.data) + try: + code = request.data.get("code", False) - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) + if not code: + return Response( + {"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST + ) + + slack_response = slack_oauth(code=code) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - workspace_integration_id=workspace_integration_id, + workspace_integration = WorkspaceIntegration.objects.get( + workspace__slug=slug, pk=workspace_integration_id ) workspace_integration = WorkspaceIntegration.objects.get( pk=workspace_integration_id, workspace__slug=slug ) - + slack_project_sync = SlackProjectSync.objects.create( + access_token=slack_response.get("access_token"), + scopes=slack_response.get("scope"), + bot_user_id=slack_response.get("bot_user_id"), + webhook_url=slack_response.get("incoming_webhook", {}).get("url"), + data=slack_response, + team_id=slack_response.get("team", {}).get("id"), + team_name=slack_response.get("team", {}).get("name"), + workspace_integration=workspace_integration, + ) _ = ProjectMember.objects.get_or_create( member=workspace_integration.actor, role=20, project_id=project_id ) - + serializer = SlackProjectSyncSerializer(slack_project_sync) return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"error": "Slack is already installed for the project"}, + status=status.HTTP_410_GONE, + ) + capture_exception(e) + return Response( + {"error": "Slack could not be installed. Please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/utils/integrations/slack.py b/apiserver/plane/utils/integrations/slack.py new file mode 100644 index 00000000000..70f26e16091 --- /dev/null +++ b/apiserver/plane/utils/integrations/slack.py @@ -0,0 +1,20 @@ +import os +import requests + +def slack_oauth(code): + SLACK_OAUTH_URL = os.environ.get("SLACK_OAUTH_URL", False) + SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID", False) + SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET", False) + + # Oauth Slack + if SLACK_OAUTH_URL and SLACK_CLIENT_ID and SLACK_CLIENT_SECRET: + response = requests.get( + SLACK_OAUTH_URL, + params={ + "code": code, + "client_id": SLACK_CLIENT_ID, + "client_secret": SLACK_CLIENT_SECRET, + }, + ) + return response.json() + return {} diff --git a/turbo.json b/turbo.json index 62afa90bb16..7c3ccb81a7d 100644 --- a/turbo.json +++ b/turbo.json @@ -22,7 +22,8 @@ "SLACK_CLIENT_SECRET", "JITSU_TRACKER_ACCESS_KEY", "JITSU_TRACKER_HOST", - "UNSPLASH_ACCESS_KEY" + "UNSPLASH_ACCESS_KEY", + "NEXT_PUBLIC_SLACK_CLIENT_ID" ], "pipeline": { "build": { diff --git a/web/pages/api/slack-redirect.ts b/web/pages/api/slack-redirect.ts deleted file mode 100644 index a6b8dbf4b79..00000000000 --- a/web/pages/api/slack-redirect.ts +++ /dev/null @@ -1,23 +0,0 @@ -import axios from "axios"; -import { NextApiRequest, NextApiResponse } from "next"; - -export default async function handleSlackAuthorize(req: NextApiRequest, res: NextApiResponse) { - try { - const { code } = req.body; - - if (!code || code === "") return res.status(400).json({ message: "Code is required" }); - - const response = await axios({ - method: "post", - url: process.env.SLACK_OAUTH_URL || "", - params: { - client_id: process.env.SLACK_CLIENT_ID, - client_secret: process.env.SLACK_CLIENT_SECRET, - code, - }, - }); - res.status(200).json(response?.data); - } catch (error) { - res.status(200).json({ message: "Internal Server Error" }); - } -} diff --git a/web/pages/installations/[provider]/index.tsx b/web/pages/installations/[provider]/index.tsx index ac8a2fc22ac..243065a7c60 100644 --- a/web/pages/installations/[provider]/index.tsx +++ b/web/pages/installations/[provider]/index.tsx @@ -12,7 +12,7 @@ const appInstallationService = new AppInstallationService(); const AppPostInstallation: NextPageWithLayout = () => { const router = useRouter(); - const { installation_id, setup_action, state, provider, code } = router.query; + const { installation_id, state, provider, code } = router.query; useEffect(() => { if (provider === "github" && state && installation_id) { @@ -27,53 +27,37 @@ const AppPostInstallation: NextPageWithLayout = () => { console.log(err); }); } else if (provider === "slack" && state && code) { - appInstallationService - .getSlackAuthDetails(code.toString()) - .then((res) => { - const [workspaceSlug, projectId, integrationId] = state.toString().split(","); - - if (!projectId) { - const payload = { - metadata: { - ...res, - }, - }; + const [workspaceSlug, projectId, integrationId] = state.toString().split(","); - appInstallationService - .addInstallationApp(state.toString(), provider, payload) - .then((r) => { - window.opener = null; - window.open("", "_self"); - window.close(); - }) - .catch((err) => { - throw err?.response; - }); - } else { - const payload = { - access_token: res.access_token, - bot_user_id: res.bot_user_id, - webhook_url: res.incoming_webhook.url, - data: res, - team_id: res.team.id, - team_name: res.team.name, - scopes: res.scope, - }; - appInstallationService - .addSlackChannel(workspaceSlug, projectId, integrationId, payload) - .then((r) => { - window.opener = null; - window.open("", "_self"); - window.close(); - }) - .catch((err) => { - throw err.response; - }); - } - }) - .catch((err) => { - console.log(err); - }); + if (!projectId) { + const payload = { + code, + }; + appInstallationService + .addInstallationApp(state.toString(), provider, payload) + .then(() => { + window.opener = null; + window.open("", "_self"); + window.close(); + }) + .catch((err) => { + throw err?.response; + }); + } else { + const payload = { + code, + }; + appInstallationService + .addSlackChannel(workspaceSlug, projectId, integrationId, payload) + .then(() => { + window.opener = null; + window.open("", "_self"); + window.close(); + }) + .catch((err) => { + throw err.response; + }); + } } }, [state, installation_id, provider, code]); diff --git a/web/services/app_installation.service.ts b/web/services/app_installation.service.ts index 2a7a4ea6af4..17972103640 100644 --- a/web/services/app_installation.service.ts +++ b/web/services/app_installation.service.ts @@ -60,16 +60,4 @@ export class AppInstallationService extends APIService { throw error?.response; }); } - - async getSlackAuthDetails(code: string): Promise { - const response = await this.request({ - method: "post", - url: "/api/slack-redirect", - data: { - code, - }, - }); - - return response.data; - } } From d8c96536f0e55bfb9f7b44fb15eb3771288e3efe Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:08:01 +0530 Subject: [PATCH 10/62] fix: bug fixes and ui improvement (#2674) * chore: peekoverview edit permission updated * chore: tab index added in create project modal * chore: project card improvement * style: avatar component improvement * chore: create issue modal improvement * style: global style sidebar border variable name fix --- packages/ui/src/avatar/avatar-group.tsx | 7 +++++-- space/styles/globals.css | 6 +++--- .../issues/issue-peek-overview/issue-detail.tsx | 2 +- web/components/issues/modal.tsx | 2 +- web/components/project/card.tsx | 2 +- web/components/project/create-project-modal.tsx | 11 +++++++---- web/styles/globals.css | 6 +++--- 7 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/avatar/avatar-group.tsx b/packages/ui/src/avatar/avatar-group.tsx index 129f9e3afef..25a3c76fc5f 100644 --- a/packages/ui/src/avatar/avatar-group.tsx +++ b/packages/ui/src/avatar/avatar-group.tsx @@ -57,7 +57,10 @@ export const AvatarGroup: React.FC = (props) => { return (
{avatarsWithUpdatedProps.map((avatar, index) => ( -
+
{avatar}
))} @@ -69,7 +72,7 @@ export const AvatarGroup: React.FC = (props) => {
= (props) = // store const { user: userStore } = useMobxStore(); const { currentProjectRole } = userStore; - const isAllowed = [5, 10].includes(currentProjectRole || 0); + const isAllowed = [15, 20].includes(currentProjectRole || 0); // states const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [characterLimit, setCharacterLimit] = useState(false); diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index 239940ef1a8..12f9074540d 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -324,7 +324,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - + = observer((props) => {
-
+
= observer((props) => { )} />
-
+
= observer((props) => {
- -
diff --git a/web/styles/globals.css b/web/styles/globals.css index e3042593606..cde7993a109 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -211,9 +211,9 @@ --color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */ --color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */ - --color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */ - --color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */ - --color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */ + --color-sidebar-border-200: var(--color-border-200); /* subtle sidebar border- 2 */ + --color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */ + --color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */ } } From 37bf465fcda112a74dc59b17d734ec092c205874 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 7 Nov 2023 13:12:05 +0530 Subject: [PATCH 11/62] style: update border across workspace and project settings. (#2669) * style: update border across workspace and project settings. * update border width --- web/components/automation/auto-archive-automation.tsx | 4 ++-- web/components/automation/auto-close-automation.tsx | 4 ++-- web/components/estimates/estimate-list-item.tsx | 2 +- web/components/estimates/estimate-list.tsx | 2 +- web/components/exporter/guide.tsx | 4 ++-- web/components/integration/guide.tsx | 4 ++-- web/components/integration/single-integration-card.tsx | 2 +- web/components/labels/project-setting-label-group.tsx | 4 ++-- web/components/labels/project-setting-label-list-item.tsx | 2 +- web/components/labels/project-setting-label-list.tsx | 2 +- web/components/project/member-list.tsx | 4 ++-- web/components/project/project-settings-member-defaults.tsx | 2 +- web/components/project/settings/delete-project-section.tsx | 2 +- web/components/project/settings/features-list.tsx | 2 +- web/components/states/project-setting-state-list-item.tsx | 2 +- web/components/states/project-setting-state-list.tsx | 2 +- web/components/ui/integration-and-import-export-banner.tsx | 2 +- web/components/workspace/settings/workspace-details.tsx | 4 ++-- web/pages/[workspaceSlug]/me/profile/activity.tsx | 4 ++-- web/pages/[workspaceSlug]/me/profile/preferences.tsx | 2 +- .../projects/[projectId]/settings/automations.tsx | 2 +- .../projects/[projectId]/settings/features.tsx | 2 +- .../[workspaceSlug]/projects/[projectId]/settings/states.tsx | 2 +- web/pages/[workspaceSlug]/settings/billing.tsx | 2 +- web/pages/[workspaceSlug]/settings/exports.tsx | 2 +- web/pages/[workspaceSlug]/settings/imports.tsx | 2 +- web/pages/[workspaceSlug]/settings/members.tsx | 2 +- 27 files changed, 35 insertions(+), 35 deletions(-) diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index 3e664bef4a4..96689259574 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -29,7 +29,7 @@ export const AutoArchiveAutomation: React.FC = ({ projectDetails, handleC handleClose={() => setmonthModal(false)} handleChange={handleChange} /> -
+
@@ -54,7 +54,7 @@ export const AutoArchiveAutomation: React.FC = ({ projectDetails, handleC {projectDetails?.archive_in !== 0 && (
-
+
Auto-archive issues that are closed for
= ({ projectDetails, handleCha handleChange={handleChange} /> -
+
@@ -100,7 +100,7 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha {projectDetails?.close_in !== 0 && (
-
+
Auto-close issues that are inactive for
diff --git a/web/components/estimates/estimate-list-item.tsx b/web/components/estimates/estimate-list-item.tsx index becf16ff4e1..beaa942d3c2 100644 --- a/web/components/estimates/estimate-list-item.tsx +++ b/web/components/estimates/estimate-list-item.tsx @@ -54,7 +54,7 @@ export const EstimateListItem: React.FC = observer((props) => { return ( <> -
+
diff --git a/web/components/estimates/estimate-list.tsx b/web/components/estimates/estimate-list.tsx index 4895450987b..07770b18327 100644 --- a/web/components/estimates/estimate-list.tsx +++ b/web/components/estimates/estimate-list.tsx @@ -71,7 +71,7 @@ export const EstimatesList: React.FC = observer(() => { data={projectStore.getProjectEstimateById(estimateToDelete!)} /> -
+

Estimates

diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index bb64e6170a1..2d7eb9d65dd 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -53,7 +53,7 @@ const IntegrationGuide = () => { {EXPORTERS_LIST.map((service) => (
@@ -79,7 +79,7 @@ const IntegrationGuide = () => { ))}
-
+

Previous Exports

diff --git a/web/components/integration/guide.tsx b/web/components/integration/guide.tsx index a0876e673c7..d083de59024 100644 --- a/web/components/integration/guide.tsx +++ b/web/components/integration/guide.tsx @@ -79,7 +79,7 @@ const IntegrationGuide = () => { {IMPORTERS_EXPORTERS_LIST.map((service) => (
@@ -100,7 +100,7 @@ const IntegrationGuide = () => {
))}
-
+

Previous Imports

{isAdmin && ( - + {({ open }) => (
diff --git a/web/pages/[workspaceSlug]/me/profile/activity.tsx b/web/pages/[workspaceSlug]/me/profile/activity.tsx index 3b5e60f59fc..820a6259682 100644 --- a/web/pages/[workspaceSlug]/me/profile/activity.tsx +++ b/web/pages/[workspaceSlug]/me/profile/activity.tsx @@ -37,7 +37,7 @@ const ProfileActivityPage: NextPageWithLayout = () => { <> {userActivity ? (
-
+

Activity

@@ -153,7 +153,7 @@ const ProfileActivityPage: NextPageWithLayout = () => {
-
+
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( Plane diff --git a/web/pages/[workspaceSlug]/me/profile/preferences.tsx b/web/pages/[workspaceSlug]/me/profile/preferences.tsx index 842740f7ac4..5f915ec109e 100644 --- a/web/pages/[workspaceSlug]/me/profile/preferences.tsx +++ b/web/pages/[workspaceSlug]/me/profile/preferences.tsx @@ -50,7 +50,7 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => { <> {userStore.currentUser ? (
-
+

Preferences

diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx index 0a000e847bb..313d8e54997 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx @@ -69,7 +69,7 @@ const AutomationSettingsPage: NextPageWithLayout = () => { return (
-
+

Automations

diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index ecb8ca02776..2e9597b3d97 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -29,7 +29,7 @@ const FeaturesSettingsPage: NextPageWithLayout = () => { return (
-
+

Features

diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index cc87c20f438..6895a53ae7e 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -10,7 +10,7 @@ import { NextPageWithLayout } from "types/app"; const StatesSettingsPage: NextPageWithLayout = () => (
-
+

States

diff --git a/web/pages/[workspaceSlug]/settings/billing.tsx b/web/pages/[workspaceSlug]/settings/billing.tsx index 751b5cfab48..bd7fe3f2f93 100644 --- a/web/pages/[workspaceSlug]/settings/billing.tsx +++ b/web/pages/[workspaceSlug]/settings/billing.tsx @@ -12,7 +12,7 @@ import { NextPageWithLayout } from "types/app"; const BillingSettingsPage: NextPageWithLayout = () => (
-
+

Billing & Plans

diff --git a/web/pages/[workspaceSlug]/settings/exports.tsx b/web/pages/[workspaceSlug]/settings/exports.tsx index ce0786634cb..fdf3a33930c 100644 --- a/web/pages/[workspaceSlug]/settings/exports.tsx +++ b/web/pages/[workspaceSlug]/settings/exports.tsx @@ -12,7 +12,7 @@ const ExportsPage: NextPageWithLayout = () => ( }>
-
+

Exports

diff --git a/web/pages/[workspaceSlug]/settings/imports.tsx b/web/pages/[workspaceSlug]/settings/imports.tsx index c75c2d7e7a2..932c939f607 100644 --- a/web/pages/[workspaceSlug]/settings/imports.tsx +++ b/web/pages/[workspaceSlug]/settings/imports.tsx @@ -10,7 +10,7 @@ import { NextPageWithLayout } from "types/app"; const ImportsPage: NextPageWithLayout = () => (
-
+

Imports

diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index 90c9848ca63..74a9c672497 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -35,7 +35,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = () => { /> )}
-
+

Members

From baf17a109b2a99ff8300f3302dd8918fee6aa7dc Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 7 Nov 2023 13:13:14 +0530 Subject: [PATCH 12/62] style: update project description as per design. (#2682) --- web/components/project/card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 533e94ede7d..9bfe6b7a77c 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -168,7 +168,7 @@ export const ProjectCard: React.FC = observer((props) => {
-

{project.description}

+

{project.description}

Date: Tue, 7 Nov 2023 14:31:29 +0530 Subject: [PATCH 13/62] fix: kanban card state name and drop down items text overflow (#2686) --- .../issues/issue-layouts/properties/state.tsx | 14 +++++++++----- .../issue-layouts/roots/project-layout-root.tsx | 4 ---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/components/issues/issue-layouts/properties/state.tsx b/web/components/issues/issue-layouts/properties/state.tsx index 5bd0edb18ef..62bd2a45c46 100644 --- a/web/components/issues/issue-layouts/properties/state.tsx +++ b/web/components/issues/issue-layouts/properties/state.tsx @@ -66,9 +66,9 @@ export const IssuePropertyState: React.FC = observer((props value: state.id, query: state.name, content: ( -
+
- {state.name} +
{state.name}
), })); @@ -94,7 +94,7 @@ export const IssuePropertyState: React.FC = observer((props
{value && } - {value?.name ?? "State"} + {value?.name ?? "State"}
); @@ -104,7 +104,7 @@ export const IssuePropertyState: React.FC = observer((props {workspaceSlug && projectId && ( { const selectedState = projectStates?.find((state) => state.id === data); @@ -159,7 +159,11 @@ export const IssuePropertyState: React.FC = observer((props {({ selected }) => ( <> {option.content} - {selected && } + {selected && ( +
+ +
+ )} )} diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index 0f5cac86b83..9fa9bb986b0 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -32,10 +32,6 @@ export const ProjectLayoutRoot: React.FC = observer(() => { } ); - console.log("--"); - console.log("isLoading -- -->", isLoading); - console.log("--"); - const activeLayout = issueFilterStore.userDisplayFilters.layout; const issueCount = issueStore.getIssuesCount; From 4de64f112f06950b05e8d6b5f85963278b4a90d1 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:34:30 +0530 Subject: [PATCH 14/62] fix: slack project integration (#2684) --- apiserver/plane/api/views/integration/slack.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/api/views/integration/slack.py b/apiserver/plane/api/views/integration/slack.py index 863b6ba0cf2..6b1b47d3748 100644 --- a/apiserver/plane/api/views/integration/slack.py +++ b/apiserver/plane/api/views/integration/slack.py @@ -59,6 +59,7 @@ def create(self, request, slug, project_id, workspace_integration_id): team_id=slack_response.get("team", {}).get("id"), team_name=slack_response.get("team", {}).get("name"), workspace_integration=workspace_integration, + project_id=project_id, ) _ = ProjectMember.objects.get_or_create( member=workspace_integration.actor, role=20, project_id=project_id From 040563d148f056e8d5ccf23716ab54845a08bf96 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 7 Nov 2023 14:35:04 +0530 Subject: [PATCH 15/62] fix: replacing jira importer image (#2685) --- web/components/integration/guide.tsx | 7 ++----- web/components/integration/jira/root.tsx | 16 +++------------- web/components/integration/single-import.tsx | 8 +++----- web/constants/workspace.ts | 4 ++-- web/public/services/jira.png | Bin 231268 -> 0 bytes web/public/services/jira.svg | 15 +++++++++++++++ 6 files changed, 25 insertions(+), 25 deletions(-) delete mode 100644 web/public/services/jira.png create mode 100644 web/public/services/jira.svg diff --git a/web/components/integration/guide.tsx b/web/components/integration/guide.tsx index d083de59024..e521ca8f59e 100644 --- a/web/components/integration/guide.tsx +++ b/web/components/integration/guide.tsx @@ -1,11 +1,8 @@ import { useState } from "react"; - import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; - import useSWR, { mutate } from "swr"; - // hooks import useUserAuth from "hooks/use-user-auth"; // services @@ -21,7 +18,7 @@ import { IImporterService } from "types"; // fetch-keys import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; // constants -import { IMPORTERS_EXPORTERS_LIST } from "constants/workspace"; +import { IMPORTERS_LIST } from "constants/workspace"; // services const integrationService = new IntegrationService(); @@ -76,7 +73,7 @@ const IntegrationGuide = () => {
*/} - {IMPORTERS_EXPORTERS_LIST.map((service) => ( + {IMPORTERS_LIST.map((service) => (
= ({ service, refreshing, handleDelet

Import from{" "} - - {IMPORTERS_EXPORTERS_LIST.find((i) => i.provider === service.service)?.title} - {" "} - to {service.project_detail.name} + {IMPORTERS_LIST.find((i) => i.provider === service.service)?.title} to{" "} + {service.project_detail.name} R%KO?1Ot+|AYcf{k^l)w-nlon|KEG_fAi+Onb(<)K)$)lxo7#D&pG$| z`+@zobG5$JA`l33ckSG6Paw?fClF>)XU%}W%*=ZH1pfJ)y3;kBK$!hC`hN=HTKYEx z!t^f!9bJyP?AvQ~m=a>_bAX`L% zJgyb|jQ(u0d>MY^DA{`X7Bt~9mwgA8ZKH(wF0(K;H9Bl+x^~(64aRFM)~&NJ{c+h^ z)3v51rsgJV)*7v`urgg|wPyXYPya24*}{(aS=n#@>C;^BoAvU5qerP$CMHo)QN~eb z#*{FBlQkPQY%nohYqEB&5j{Uy42|gXGbQ4v z^>SFLW(y(Ie}@eX|0E_5n8`68s>vE-Q?w=g;E}`sK1YoR3&xWlIc(w^>>J`6dNdrK zUGwj=)Bws+N_YU}e>3#IAOBAifN1ya`*)82b}b#IQ88|NQ%|ZQBlnQTzgf z;e&8{+pWuX?bv3qW`l)=(OTm*c)9!bS?vl9Kk5^D*mu`<>*XLB%ED-F^ zC;0z)9AuS9ajbR*hQpCYf3h0~-(Noc6dbq=>xz}nVPpf=%MYW&_dT-w)1L$XpCzCf zHY&gu2L0bDh31)Xir>*FpD^Do{vfLVqsE!Q%1w}|YBpi=zwQrz_8(^b&*R`L(2xH* zDfs1IH|HA)zAX%#n51)$k}&nSeAo6Zj>ppbl~svLc3n0ZaGUePii@Ss(?dn-IUX?v zPuE-4J$JiiJH98v^N+#jj=S^qGe381Tm6XTczBSLdG@OQyDzU~l$DkF`st3P3?85&wM1-n@SK_R7__WblS;n4Eq)}MY}5?`zP=|}C&-a4OtoVl!c zrsl`^sb92D)%;jHWmf;E;Y5UKvQLjv37^G&`q7f`|9#lnH|i0!LaiG!7v|jj_;JM` zH(F2~9T!)9U1qZ2td3opaGydoK1NjfGRbJ&ysQWR%l>lT=5$^lbrALH^M_zN@hfD5 z8p(W7@BTV>bY-U3tw&YK_Az^t81$~OY;)DVI=8oIsK@)Td~EaQ^Kz5zIm+?zabMxj z7cM?S6YM{6c^v+sc6ugAoeR1{R7J_0Q)6~U!<4c|AjkUz)qNF^APKDg-+C%Ue0bsvc21n3-q%`UnTNp z!()ZJ5|9QgInHa47j%cME@WN_M2DZdMf3LqMRQf@nVi4(qO-O1G(ZP0+WeCrkMcnC zr%-(Amhl&g>8&5+$$}ebII4^0ujzar<)x5crOm>W#Q(}eoBskn5*)P$f+?vOK&&ReBwg(qq*Jcpq-Lt|5(QVRu4Xs z^$T@s;9r8JbB)Eg;!dH7$0u=RY*m!G{Aj;hpBAUE$xd$3Q>${hOM-uaXQ@*(;?@4+ zSLx$gjVP9Arf6z>`}S=}h#zftUuK)Ma#pLm`4m~1cS6V<;c z^tg&w&!Jqh(Ag2dwBIdU8s>o(o7jMrZtl5XrLCQEhc6n+ zxNBOGwwCX~s@yTOyihG|HL37*Vf5oQ+ik^C(=|e-;l1ymuac{pNa|2eGE~fDd#k6# z^smYuuh)#R9gjf{NRYN}$X&v-YbZk+{_6QxXxn~4jfHl7zMC0!KFYtaoZ9DNITiM_ zHORS2Dc`wG+M`jXb{qUj!^e3CxU8UJZqe z{6Yquk>jKM{#qcTb|vhwcGp)}E3Yg}k%D>jg1l`M`PcXCTDGxhrj8s24R_VutFqqQ~On z135p)Oacpzzk(h1TK3g{*2@@E5H~sV?S7SdZ8wuNmZDR*g8j+)Hx}buy1ZMa8tEyk z=Fr=;yj;|7ENd0`O=eOWws@~Rq$8p?><98>d-K9omz_K8Qe-m^X~XRTE%1HXbe0gYR6&WZou;fIg>H2ii}XcpQx^$%PHn)Hrs3V z5Ogm7?29y4?mha*&H41*`5*YtuLV*v&^e~=$EUO~cOlQDsHl-8ycv+AM2`L7FW?Aj zS2`enP`l%ISRbdMkSP$&7FSU2H9KU3p;bM+GYf{$#_C+V@?LcQ=>CZY`Q)#g3i(=$ zaEBDyOI_mUNNjh#u}K;K?GT}C9JQgGWZK7UsPJ^*eyurVXZ&%KCk5GWgkhYIvT5%P z+HO0>0+2>4^O6W{Vb6D1R)tbOUaq4fZ}x|=H~M9E`d5)4YrEwY+e~0F1eu8BLToH7 zLV=*|JATZXoJyIUWQgKmaR+^Dd|V6JSd)`YDP2oURF1xtyy_8pUMGhtec=4qC|roX z!ZNko2%fIHvbjzAKz=f>u}nQKQ;z)d8cD5ZH?qE3*Qe;%Xvnp=y|&9)F#bUP5Sw`X zo+H>wdOo}lG>+NbCvE`oos#-SSfBRAJ~QH@#lz{sz<-vPLTQX zr@;)C3*Fv;!=Wz7*!pUrnb;x5)s-0iC6CCy&(~HT!{MSf8-oLCOOTm3S7Jw0jvIiD z)XWl(Jj;0NqnTzic7ILx_tp9FOj_Q&%4O24YZQ|mXD>PNDJ{cttL)@orK`Myl9(+c zC%?gmL3P4TmRd6WLGFn>AlHr6!k8S0dLaM$U;&asO*wLt>C>@#t_o~|&7G+PAzf;@ z7HD!82{1YEcO;)(6CkqnzP`$`rHzH^SA*(hw~4<%d>+}hC^~j>auAG-kpAs_6kXD9 z_SDIf-+)=s0uw+xw8aUsTyjv7`iic46G@Pp%-J}451#Y(z^bzUCsv7yK_4HaIP)p? zMzC*kf=MAU8pT`e2}S7vnk78(;|cegi5CVA-I>p`V|`S=jpBaIV;(PP8@6CHb_Se7 z3)Q#BYJSl0WIMQ+9z6~>BtaT&7}wMs9y|H?G2oFqplNyKXm3YuV>O(&$(ffs>gz`{ z1W!vgoJ_v>L2eeocIz^3dE>FWStqRX)~NhD7@T0~gaf#xo4BFS%Y!>%RpEJn!Mk}? zrk_2ieGfUUZ*Cz_5ua6tMA=eM?73%hgp?LYBnb*i(3JBuQ{H~z$FmD*Mn0)Sp38aC z!|U;>F;I+*ZECf#R!s~I4e_vl+pp2_#GgQ4nvvTC-Dsa9cBso2Gamrpuy(Fo~< zX5eqEPa_>1L;lx2QaQN-Efs|+`c+hVPWM*pna8^zXr1ts)%8|#Wa1DZ7vDe*eC#Kiku};C}bS;D4J`tlr0;_p;LPw z)^}OXteyO8uEeJWVxPa@YYJ)O!(AwT)?^oe zppPFvHtEpV!)9!W#S1%vPC~?Q?T`7!ys?ZacuRbaAV&O2Y=y<)9kax%*u|diaqq8f zRrC!W{t5vqDnavQhYl@qE+Dmw#Tl$)8G-8YD+|s(I(-ChC-w)VP~NM~1a+S?6g%uL zde56JemO%U;CUDew6j^ig7>H?&ImEZ$FdE_&EklTbXPzeT!m84(~%F~ve0~Mn&w;i zA*ZAT;`PlP4kTLFS9;}7v3kh%VVCbdgfL#Yhbyxs)oJ`ej4ZFP%&zaz$<-i}V$Hd> zmrG7d1+Pn2AxynnKP3gLY}HzLqUi!YJ&W?Qb@IHWs_+P9E7x0Ndfs~2ffQfx_*JSO zn9%&17iX_})A!2q*X5ILXM(*_@(Z1(X@k*(LPtb8Y48Tsi$d7!ZTfjsY?P4=yO z87DQW=z;fa>b{6aw6W<5-3Za>^}`$1ov#>AI^O0cxBUF)PCX$9!^`+{8tebDN(%|} zK}?%;$;rGr6<}QtINbhc8l)Gz70>rDauLri93C`z5JQqJxm4Xk2gd`_Nw&8SLGB?K zYxy@VTH*@nViXTz5<8b0by?fk>}QNowC@4UM%aHD3*>!!jIZ)YH0`>*{M(k<;tfs? zXD5HoQAWYa)IX)5*b$CxF0&7E3*jabfAB}-6rvA$m=yNRn`b7W*vCBo9XZB(Sc+eq z5r|-K77yj!Qg&v>CC3JEMA?Hm4B>6$jWTUf9NA1VEL2aN9;{ef0iefW_tx0`yw<%6 z6dl3ERDvB7_MgOIN^Da4*-C&6 zg?EIvl3VyML26B(j8T0bE8EG@z86w^q?S8!oCt0Z&2~oFblfWvS(21VF=w$PLbV}4{ z+1gT@u_~eZyX@p?(vP>U1gc++nw~@8MpS|!hw)}7_-4!^o_g}(ScTLsgx?m#PGTO< z8gukj-d+H^U|6FdJ)1E{_oGHARicr zOw!&}r^6d5Xl*pcwbVC&ajHw`Lh%)di*37ot%G~PMeNgGiHB%PLttODeSS*$%H`C)#Flu4HlMKXP8#yaBTk}LU=QrY-={lFx2?%LRr z(=#{S-gv~97OK_9^dj*e^5#p1S^;U3P@G6N#X9)=hRau_mEF>a-F@VpCWWUZT7CkC zB?dn2e`pr7q_sib`9aQtKlw%og#781IUk z&*;mm!MrV(Vq5r{`UfCNxjhQymC{;zikZX#Wv$3NHH>^>szTso05=?q?JcH9(~JsF zPrc$N$Q@lGAaYIq_J=2_f8lK{`P0pX@nZ_aZ(<{BJy%yo){@lA#&;oKSNjyl#wB^V z#j0LEUe9o`uH_so+Wo0mbhf=N{ye*k0&!`vXN2{VCdUva`!RRJ$BQujH<<_oh>pEl zLiVNYa?yF56^xxS#S!gy$tt|xFQ3Bq6^VHzHrXa4$b-MO|kBN2a8tE0Hk))%Km~++e>q) z{YSt*E>*P!0Y{r&9GnA57CCkjqmSDA*zw3jfGty}NvSp&pm96<_GBy9wRUYnrmXxL z$+4gR)RAZB*-*4l@X+;`mZbG?%w)K(!0y*Ekc-0S6cS~kZ<}=0LS%`XPD^ViAZ3F2 zy%|_d`XBE)B}LJqCABW*zIFOSEDCKqd5f;bK`Bkqd3x8^)yVw8_Rl`?hNqs{oxy63g+aI5)`$ZCuxRG6q zq`#2&u>08EDm4{=zD?&V2qBs$D>P3YK!J-F?80%w20>;do><&Uv% zoZIcAEE{xuDBsnkt#@}X!d4+UG*%kQrNvljgC8R{OM?E_KaCNbPt7ykoqhFFck1JO zI&urbx$$$8Z?xqB3AHhf_3?Nzr~HYDKhBgyB`{ReDJ-(NI}R|ywNsGnfzrtWRUlbu>?!%l)Vb2a1M(?BG z#BT_~7pvX?ak$(M0anTBF4 z+<_p*=Ew6|dRQNuSE8gLLFwp01TqFf!Ux2p6||M~8;50~DBx{_EpBSYiQA=V2{R$2m{vTA za&j3V%d?@Z*+6m$hp%l|S*HzaJr9S3X?I@~SN-s8rIZj3J`%B5|M}FslJkAR0N7(* zY3q4^ZPzC2Tk{`TSiENCS63GjMHeG~j!^z!R0O0%Ee|6CQ%t+mlKlaG+sCY_Pz6=a z>zwM2v-a9eSfm+;9zZtu#%cV0`_xZS%MuBr_L1S%TWR6iVnF37;N5!`1P(P}y}p8X zXA)}vB~LGAOVx0vwi;#c#w7k!cV{O75mOV(a6)%C!0yKLm(7JHdHFMCuU`+Sw(SO{ z1)Ey+a^zHudD#u%QIJmsz)PqKW4qZz8AbzH>Khx!0)8@n59ZX3n^razHh=!%_%<+i zIBsB+_9B};lz*0d65`8*l%TL6AyUh(iUN5c9N`H#D$#Nj-Q&;N<3mmTf@dfBm^&fO z??1i`uTpfDv2hfkVD0aUFtnv8n3f@$IIdj6o4TaVF_36&Dt(vpjWaw({Sw(50wyt?Mp7OL0@C3t z-c;MS!1g|V3le$RjP@I%1!Ss5)km|CvHlRHGj&VtUy&?VXx}1U(itfAE`)DO7yvh0_}xT-4%Ruh2mv!{%K{;)%K#k;aSQgM0goqn_)2-Y1XH07yY-crHuiA_4_Lei zFU0l9LriDhED!@4&8R4HG6WZQX#q)~N8JUYcE`tJ8DSz2d1(?ZR=%#~M)s#Q>SO7y zT15d;h!KKPZ=?qwatsu=tvRUXV@X0tNOa`L79bd0m>1y)qFsuNY^d6p7->3(z+f{wb1qVCRz z$*Xhl-n3xT4rx(8M@J}MXnSQ_Q~QPuV)5n=YaX~=GFLT^M?Ow`KQKbUkR9dZ;jj+? z0{?P*i7D95yn*+;>i=PDX1;DFc7UQQIH}NCZE=X5)X}F+46cwMXSWS2eXOsaG7^lU zku~!QW-LT57@YN=yDF_Ze9C&W@E?wfx))0@_FzGw-P48`g7#sES7)xu-XrW9i#?~T zkF; zCma#n%g|xbK9{sQU^sk7^IV=_22;8(MM?;^vuRa9T&pZBbM^(l-hT$k-dpTuCVtrf zNg`nPBBz3*>_howGuv+yq*Sf?fUFG-QhSqg zTfv=~?fUr3Wc=mBfMdLf$}k0DF&$!FKDgcpN9C~FYg>$ub!{lX5Nv!Tb+ z&cCyZp-RuGi1!}GyO57B=f0gzkWqrpF)zwc?2+L$yu5i;8pUKd zcyJFxBzGDo%*46rCt0|#=Bf}|-!_9mgslCv=V!G$G_u8Xh3OHk<%m}Gf`SXMiu9z0 zJ2Ne{@zNI~5472tRYJc8dLv8IBTxmT%STzobcUu7;1*$B`{kD? zes|#RgP#Dx!xNBOTj!H_0ACYNVuQ)~B$q^ot{Frj3|~~$%*_@mRXTk^?5|!-UGgXB zKrE`>qohky^hV{RiMK-aXzI2iU?wyL?Re)pa0U@dv`6dS2gV0neROJvFs15}ijeDX zn=09iHzXiC2PZ84PBo@;x#$Hl+JG5-Bp zlmzV5e*}3hl)I8@+rlQy%bWvssAW0NRIdkYFbEsPt_xZQqJddeQG-Un)e&;HAv<`s z0)e0()+Z$;RiMnau!owXC@RqvjRv5U{txUn^s|ddQnezN$x#CN*2p4$7M)P@5{Af~ zBVpoD=#PMNZGypx6VBn{4b@0gYAI8l2Lt{3PrV!9+W@qRWG7$50>)L-Lz!`YC>>Rk z{gl7tHx|znjp7in9G%|XL+kBEj$SI(B}5wPNOIMxedyTl9z*`3HsR{lB?`Rd@hD1Xa>2^GfJiQ&nSjL|Uw9A87z zT?onE#e}Qq#H6$}m*TM7kEd6ex0C}G(t~T7pFW=BtBzOyke!@kF+Rp5&i*h(^aQnB z9L&b`%YPK^OGts|B1}Q$K)%@85R=$}%U&GEc9Ycn{wc_7qD_!bClmuGpt%r&g#ZTFHRaHHl3FJqu?=`n~IGO3eoi3}4 znXXW7IPJM`q~><97x4dth5J!n--B~z|93@98`?$^uiy@GpFlMA{MTrS(a2ZU{w_x_ z7?k>|v}c>IHDLqtlIcYZB0|*h(IpI12C#8{JmxC+pb}iM4EZ&O04AzQjCM0-$3^wB zA-0GyqR4&PAUA{hwGHgl^5=uuxV6I)m*FgZqJ3*V$VU+Tyny;2wgh18n!XyV zu|Bfz6N$J40XiC#Q_N}6B|=XJc!X9>U&ql4V8}lrwYCh~@hH7mwf7ZGg{nAyhLAOb z{}+{EDnIx2557LiA|`!Jsj2}e<=cfbz1zVRZzOf*YAlC(4Ikl9)vmsJ;%@+Ru?3{# zjb24Sr*adhv*mms#$l<+_PbrPX;@GOXckOri*|Om{o>#Y&R|pbh|Ax@ zM_*v@Th*7pfF6+AA*==$U>zk8MB0L~0bq~|90hi#IUCz=f`E-7R1id#S6vxfaw5kG z#YR|r0&(HP_FbReZQ)+^d0Kv<-7Wx2nISmjbYTDt`fLx-sw9I?G_r<=(hj(WDnN|$HVINNBl1axPuatn-W|FF14Lv_K*Aux z#WkP^#g8SQ9|v7pavU`nsD@{Kn9`n*_kbSLxD~@V(N07rc{k@UENw{q9v>PozGtWF@fwHi~Jm->HPyn^z=i;Y_SWXXO2$AmrAe$pzI57#ie%Wn{QdO%Rv8xMo z!G&=ja9Kajys4Xz%P(FvL7Zh`h%y813R7*rXd10|C4uU5Vz*`$n>8^ies;Kn6ED0H z+!{T@2T0|38Y&?G285IYYBxELe!c+RI?!gMN+YV2c8m?ZZ$P*w@E%T|+0Aw~6ITWU z;m`wXl1cQ-)aP3q#+W8IklN5td9Rj^b+an!QVEn)k{b$FfsYfNM=3=7LxdP>Ltot* z4f9h%1S`6yORxkw7yjsj!oRm|4&zCPpEZHn51dEO{>{jVx9cI@@jly7=>GyZ-W#a| zf(c>+5i9i)+K^DhKZpEP@nVGLy%q4_ZxC^oTte1|6xo}LCFB9XLoM%1b#IfF9WqJ< z^gy_koPjtI_^vH7xAu#WC@7klB%T9A2xQ!%6okrR=A$JJ;3aND`0ah4o+lR}iQK;@ zV+i|?qx2~f`yg`NNqKStf(biZ>{jcDQJk8G;+}UDUPKO->Mp<#$@VQXn3o~^1)kQa zeTtAcV!dkzu&DPzRzOsLRI-XQF6TV}S4hZx0}kmVp6IQnP0Z5#CGiT~yj0f>aE@pm zik+?Ehp#YC^&d!MlzT4Yo({M!Gd%KvgIoOktQS`S1tOSA4TDH~XO2V%m2@mgI9g@~ z`zZmt7m^{zn~CWC8<(dNN-?q3u@c$Xmj3{ijfMX0qft9Zu)YQqrP_6oEzCJ4Kiv${ zE-3UzhHp7@Vs9++x50lSKR;``nV8g8Iu|@N)fifcP$i|Y)$%*2+~HoovYKm9leh^9 ze{N`gIRF&!x(88JZ0SS;l&W6N*kVoSIie*&O6wZPXiu2}4hT8<%{x%98x*4chLSot zj?nZ7Il}~CSwb}|%PwSYw{bkU6yS$E=a;D5+g{70qb^ zi-Br-hXo0NYf&eb!w8aO=K0f~j!2i_eaH~>o8Z}kI?bT@r6v>pX6+E*l@^Gde;g8= z=%z2r)vK}aw8s@cJ{b-@8>$DWMYInUB|ir?bQx+9sZGEh*w=-))T|VS*$8FCm+CIe zsb-*w@9wml@3CIL0D3kbZ8Q^4A(R))ksw|2 z+!Hx@YG6ZQ&$3DDsqrRm=tvKJiw->X-G)y+6}Rg$YFs_IYpQe!8OT*4CU+5T*$}m$ zys-9Vtb8Wdj~9Lbp(?eQs5bLJwx~r|G9P8VcaI=LY_dOiUAhEW>q3%r$*+rKgpUGE zj-kFW2Nh9r8)3eiDX&VEU(rD==RJ*U+wi|p?U zdji0>BfqnxrrMgm2$?%(j;OiK{scMbC8gKP(e>_v*DEZRzGALxYw&TFDCRP z7vdmxRGArv9aPt=-HaPcX2zROzk!1RwGM0Ktxd|QFClV3aB}2@AH4~3^tRrjA?hq$ z(Sav=9yIs$vtDHjo+Q|gtZBDHsTXxO9wE6JA~H0Jyd9%PK|F~!I^(Bdy^0qg{mXp= zEu7G`l|IAAn$Z6@s0!|i$;F+niPx@5)9(RqghGdZV19{RUu6r%)cX@5ip6IolYYAD zszccP1bMLiS1&_4y5zc>3z5Xn{T|Lkge#vKb}X^2pjf3E@rP*$`%vJb?nZ8XNeTos z=zgGqN#T!>W@Wdiepq<}9(;`JzlmGd;Q$_V63(gE8>6SLbr9g=4Y)T4(#czziDnfBHDjfX+(fnv2)l zwe2Yaj$jM4$bI=9{*dPuJBfP}8laQZTa5(Xe+b$lO@3o0_Vmme+(1H63J!N6MGKrm zADTVCjH!w$K>ANZprhCU{3%et&^R*Bxs889{(EtT128H}A#vX9gO<7*2M2AGrqtS0 zUqN*hs45?FH7ABtWUe=TtmZIv?|_Z^ zG7uB`D?>g`5nVyKP3?C|+?P;0f&2cVUA)LAz0648_uQwcuIY(sGncqY2H&)WUDX{J z)G7X!km-vkpn7%WK0EbB3x$vk*yL-3YufW6Vxg?RV|Hu3mZFcw8nzKQ{nGsB6T|-S zdSq_xE+$mq)bpto*QT=Q7= zsClN>s`Mi3S%lN3q;??GkwZ2O2s#G=4EMxVVK#m7RX5R?ACI4ZF3VMVFjg_S@ur05 z@6DWiH8s8!HRm;!B62-G1Qio&|BmsYy?Db}UtnWplHX8tFr@j$c5pHAt(ekLpi*{x z$gwZc)dgK%g+Xuq;plI?W)F{yW#9gwt9@U%1u{ZSm#XS>SnL*bG6HaPpPHiU>n1k+ zHkdF{6FJ5{Pb7MV&fdRa+N-4m(I`SfKvixk<3rSo4?*qU@$6B7DD@^hN&{t|h(0)- zzMWe4-LV7H2u+VMt!b^K2p=s+ab;rszuN25*P#rg0k{6cT?2mpyZSTo>b0B&q8{e; zCd09|tIDb5~1HupLu)Zpd-lItV5dPlwf!ythXY8y@=i^>< zZP_E?MuH{97p1*o18kUvwee_Rx$&R_|abcO)tB~nJ?%m>h{l}u?L%)pN@5tfAug>CqihR>GnBL zk%E*zQcEMIl;pBqX!js?XkADsEAI@7n`pjR$!WSb;CEbnGi+;J+HdS@D; zeLnJkX$Uwx`??7r0?D7pH|TetKgRkX-{olSMNTF5Y)X;!pW(U_s2eU9v{mTW%t2`R zSLAT=;VuXSAJb!cw8kAqBe{pOLI%ml8Am;LWm|W=ADl-xbPBwOD{hX7|58owoer$U zp*lvf3zX1W#OG_AwlJCQ8*S{PiKLIIJ!4Jhc#96sZ7lzkAnMf2A$Z$;x~DJ-O|_Hb z{u}OXao}+%=gxCg{0&cxB4- z;FDZi@Y>i0jgQ05vp+P*`@9XW*aguj6Msm)&QOpf{(J{XsT7zSEp031881FrK)!m3}mXRyN}bqFcPd z>7j*YqS|xqbi;zUjC=+(XBdjZ*`km7Vi*y~2EGvEGJw6T=_F1>f-h_E>+MM2)8D&tNb5^ri>z{rK{15ULUx#B~& zbMrHtp}!T1J&$gTAGu{tj~?i4P+ZVb+`h+gO7NmnI7N|jS!2)M4#N2$xPhjNpPj6J zct6aKM(^5LFL(RsIeUoA?3;dV#e5n2ZTFvX4Au@hy{k92cPMHGVKua6qCD*|=G zG*Gm4pAXug64J)v5*w|@$YVL^*D-p6lBHCOwO&CYq|_uLi{yI zH*v%A4|vN5;DQ1GEa0;P_R}~;>mSlv7s#y-niBp0rbV;5ITtJ@CZfL>ike2~g;FyS z^$6knMYW6S(S?A9Pj_iVOW7xr+?txNyK?ikk=prNCx^oi3`b2R^gi_$5G(an>o);1 zLP(mrC=@7Y$R2L5<&fB2`Juu`aSGnU=_|r}vNDOp+(=sY*dP5TlP6yh2!9(vt53{c z;WY?z)MzARe*@6kQ_@Q%4>o1f6O^MiK@LRw1g~$ehwFtTIf_yhL42W=Q)q?km&!>R zf$-B_GfD240IsqfLMX0w(HTu$x2Kq6(PPcnm*?f9m(blBX+&-2Rz(JvrO=C3(hEN0 z@>Sd@Np~b!FST9>ZZi>>#`_DVLFXUxPEC8^VwGf4P(8NG(MPBy31^9Dugb?+786-w z^7O%j@m^hN0{>+_gVPD7{CNU@^+p5*&7zRAOZ&YcM~qt%qg5)($$ zyNK-)tDpGJv5nkZp2|jR&~_`$I0=DR56CoH>W$riN<lYBr56wVR|8DqD z4*6?Z>kP)aW1$JWgO6fDY~t4Sa5feBEiCOPzoxMSoJ~%%Wm)FoS0|z5mS(UGb$qH* z5q87$EcGT-U*P%=`+iY~?fr1WWRo$rpta{Ge_qkXxAf~;ieaCV0bWPFg7_Yuvs3le zrEpJdqi8pz7^u5U(|(xF8EAmzZ%RBd+4aYG9JHJy=C_ZGWL1tD)dVS?YbkmKLHzY4 zMQuyt>+MxniJd|{K)SX1DDf0kBZg)Fv)%@|J?jmvDRRR4;IfRtvj(HKew~!J=IX|< zs!o2)7vJ4$;wjHGyzqt8t#fR6CVX zUOCCTqh2%>h4Amxn6J&`?a1Umj~{v8{4Jp!!VUyRqph*1hhu*yS1$(Gl0WoSTMn%+ z)C*ZdwEsQBzEf6XzSu^qFLwS=cO_fO+p6e|&9YFo{yALTOCwh;_+UGhR!<$9sKBi7 zT2LrduS4xn&~f$O+}#Tc0a$^m+oI8GEirjjo58cJk(iEyX=9Tv(R9aOx~?4tM`#r9 zl^ay)G+h{UNiSm7?T}RNmrBpFADU8$hY8-P$bDPtAV=}_GxAA2_5(^|VN#iZlcamr zpxJ%OF*2Q2$=UZ_P{DKTs&ZRNlL)e6Tl4QZ2QpNXi>BiKm9F=aA*PB583BQ?YA134 z?;_2S7k4v{t6VyLj2$`NI&`~+&N0!F+7?`PHyX3e@5@>+{+#_HF|~s|mTtn}5o+&1 z<1EJxLKxKzCEAwuz_M3ON}gBB^lP+>Vo9S}kICexjBtP%GLM7i$|r{ueX+xXeTgAr;M%ElDd<-ND z?a7CuZcq5+-Q!zP$;fg!YC4yYc>J;x!-7boxB*g&ts?04_|8Xhbk)w)!r6VchW%M$ zgTZY@)9GSPm^_Hlr871L*+BetxKEK+X9JKQbZPTHRIP)evoBxfN;PR5FRD9vEXB`( zE%Bb6;5Cyo*}Pt-59n5cEH51=Ew#Vk;dVl%*j&C%?&&^ZZ+PfV!bFYJaGDc8shpE! zV`jfkPh!Y-+o(%wwY~m%jLsHn43PZVzz&H{bl;%0z-<;$0CLOP-T?VnjWiDBNNl3h zvQ8^r+EZOpTEz+Z1udCdrgcR=CwsxWR5CL*_z0u&y3ni%T`s8Qpd64|jZ`M~GW1?? zfM9l;jz?MezU&uDWBB0oAy)Jl%SqZ1d_hBb+~aAUt^B9 zHh75pHvtvrxzSToq~YgI4R=?vv=m)&$StgS>yR>VqrZBHtmJB^1M^!Jo~NHXZl z+RSZ}dmdxBp^7-9{7{i3*k-rrsyCD^gu+ih6Ew$L=VmdgOxQT{Dn1)?T1gwF^I1dg zH|ykXnM6qu`wT@=5Mq0LqVo&J;`uUGJdvBTx;DvRV8lGEGl!T}(DtF$a0=m={wxVa z!2FEhy%4QYF_K@xne%33!MKAcg93f6{gfILNx>VN?CANjR+W=EZQe245pfUncQytl z5lcMouqRn@j8V6Y=AkrIAu4-4zWo&>#E24UUymZBwr|KvuLR85hAjFk)#zir$i{ib z$b`aehw6vn6?~}XZRI$>?g9qE3_KzGkOR4@ylL2mH{c+i3?n{Xr7lHv zj>o7gz6Mx(XzT_)0@Y}|ZJ`4MU7rjw3T`y)caVQo!vAc{(|wW`tcZBQ`!?Z8P-VYDi(K{C0Y{h^ufQnipZ^3wn%nz+FREc^r*ML_or<*4Y-Ah0dm7uUbp zq|h#~TFXX95*zZ-y>uW-U+awRd%kVRaI8_cZOI6r+?@u%1SUuf*|Y4ie=8bl-H;>UPoe5#OD9r1l;~on+SwUB+V8 z@QM}3!zjkFM%BhA$DI@to{JTe6}E1#E4H^uSAi_eq=!iU(_o62>M}VO4E54ENW{fXr8EPd#VfCeQCc1A2~Izr}bY%%60O7---#w4L8dIw9{%A9jI=Dw%Zq&5KO) z>ZsWN8+X_Av7L)2-F8|F{5ZVj+(U|;Rf=9SmVt6ZM>nVM+ZY|QWbWdnZ!I@!IMbf{N97`1bFPlY(?Yw9`s!=9`WF?kD3wQ*uhT22=JNoFsScKQrA?>jv?e*a|-};(I7YXS5&rZ6X=M0c5bGoKZ?~buG=+9b1 zU@P>iiGM)a-NQ)AgR7SNNnZ-^wV`61hMAWci0fh5V)?K+(c(JM&BtpaW39cf(2C-5 z(6lq?ZP&F|o*TQaMKEV}vP7Kqxi)LR6{HH=#9w?deKg##-}XVQ%og%U>V9bb+6DPp z@dl((dtVaI2Q?L%r1;Rp?E%NU=+2Vy(U-dlN>m5#zX?d8Q9Nn%Lg!zEvq!U*aYzTf zs_r5R^4T;P@8cOu$_VoNJWfM5niM{fy@T~mU z6q$llB}8xB$nE`9gY#A(T{{WYs9b2+xni^G8|2A`bdrP~1+ct};bY*+~aUTfqfap_zj`qv~#+0WpbBq3gkAn=Sbfe4xm z>#vSZa0KK#&c6$_24U!|iP4S>cj4No92%rH`Q?L+4t%f0fS4EpPp2OrgZq!a5H>Z{QG zmOdLb16m+J6R_HFJM4ldZTKc6~zt5K#5sz)h_*4$thCgBRr?~ zA2hYG_hFJrLOwk_#!Ht{Q?#@rwK80iCAqzFveR{FMappZJzn5BFM1TYioY^*J>Mq3 zED3y0trhZN_}WnB@`A#mrb4E{yOG@!$sbrft7^0q*Sa>acPn^*DEf}6CgNz_vO8{X zny-_Ef+om>o>aKK@9AKj=T5SopOM>>J!-r;lPX@J;`Utngy%7?JG%4Oa|d4gP`hU; z!7>KHBA0MKNF$qfgbc)*_1hXsM}$w3deB|YwRb5jF8?d_7bE?vU`jU^>l)_QZ}<;} zZ%5E37KeMflY`kcgRH8^AJx|dj%Tq@tD7v&OBiM^v&rulUapi3k>|gw`B*vUPV9Gd4rPDAWQ5>tQei>V zzJupEQ>xOLEG`ZiN$j(h8;~UpEtSfH;V$Q>+zO zFeSfGro`_-mqlizfDuhYmedF3o3s)jSe|F)%bcEY_V3hVKP|HU=0L{!uey?ENZDdR z>g|wNliQT(Bi&=k-_bZ+=Wb43gu5)2pT_;E=SBXAJNf60vWdZk(3gtn8}D>*uy>E) zVto=r-Rfm;Q405=)Kq}OwnU^>Ag&J6Wa0*R5=5^eXj_PCkS40;a>h2-6+5XWn(v) z&Y93*ct?PJ%-yQkUqTJv>3;qsOKUQup@c$GB{{a+4#q0#B?83;-rQ65b;DsBgP2~k zOKkn0+Rj&WZ>Xu0KY8+_LU2goB%bW`l)i5-C=&u5Sv5*!FnZx0c>fMhU(|@R`8pEj z&PS7fr9m_DQBT8PDbwGvohEAP#xK;3uD+7w*Trwj1EwQ#oULTDL+xGl0;fM>*H-dB z<1C;iId<~hp4%Ep+}p<4RBi+fXvO9t7K!8)8}MCjK3%a10MsD^Wb8kK&4@0(y0yHp zxkFUp_q?LqcB`_IG{Op!?_zz+ZJY0XYdMi|j$`7bC}(ed>qI(M5K~R^&?9y74R{ss zW{rECN^rSW z{0|k+4Y-pW!Jlrc;(OVhcWQ(?Qk1{l?xroWvSo?gnIWX^v90nAVt^%_at{?YNq%~ZnpR!CnyrXVS;h9)>@kl1B(yLoJE>&FdkGKeL1E_*1vC9vUJtpawyW1A%6XjN43*R0yog==j`Iz~o%X1qD* zu2@YKeqYv1B;I@V;oAeT!u&$Y(!+aIW8cSYo&_)UfY*EIRR0KteUTvTwYV)^f4f09!y4f z){_^}j%8M#3TRx>p_nyfd-9E^^qApay4E}`ph%zfkiUH?6x?}ra`ms22))%6d)(d^ z4Zc>XpmU30c^h#zxYYQqAQwS6sw+4YXsxh@twgT&%J9lC6X+6!&avD+a%kBD&KPw| zG^0+k`kBywI(@e!UmjMq{vju#u-tml=fd@YmxTFuirDhzqTKniBw$5>V?%e_Of`4z zQqfx$(piA7Ue4gY&`ODI8=X*eb&lV@$(gL-#VM{iJ)%w2u%zX>+n+EBIzvf(N_Yk> z@MtKah=PkY3OSQaPO*yT;sW+_N@LL=TTZdQdq7w3r3tG4B<2}$fQZqQLH+ys^adRL zdCk}LUd_%1A|N;Xrq)=mi)tgpP)TWbR2lD?pbL0<{>|cJmkbn6y&k$uaI}YnotG!l z9NWhp$eu^ayKBhTACet5PM%g^b_?~>)^;OO#NQu?I?vmkvvbI#p$T#Nvg!pn{QMWT zg4Y#pZz}R9$$WmytncnEgd;JnsIy@RgA%+r%+?;HTfb+5 zOUY4hf}3(W>?Kf7N3X!52BKqd4P@U>G7yuegc<$?e9hQrgI#kbJns@)25jWYt{z+6 zWdYY0dN9Zv)%{lQosKr=l{Z)PG?X$EQ#!J4#J&A|A+hzIbD8#mmo6l2!P%gW1;e!#q|Tk1J?X?L4dM-EvZ( z3pq=6dL3PQ0=z&X+i5y|UOzG4g- zBhIb_Wq{#?Q<}VMlR^+7&Hva_-mCZdkUMF7Vz4|QNiE1xS56~P2M{tbP5>8T8GR7n zNV3othqE@@2-jCMDo0;>o@W*vK5~jybkip0i_G$J9;ZwByRN6@uVJdm-pGQj)=uG*aqmL0!!tzj$AXR zH)~?xd1Qf{V=kl}y-X8#-abb!kaIiv*J7EBoN85o@CP%=o|@esdHu4us`p$OeEK^c{06q@gdR3I8G31ctVYAYGa ziXN=7mPoqQ^@}_vZzb^}-kj_Jj`HZ=7mCZGO=HkzL$Zqrd zYqTbBE9n!jYG^feF|VOC6y6;&l#0uS!AXqbgeipJ3rK{!ssQl^PJaxXGZ3EzU)3x) zGv@cQg2y9Y5?XO9imKN?m$tbLq>kxZaVTsHX?LXK1)J**v&G>nKySj`g=T`ExU$UJ z-bng&n5%qD@P-kgo(LC6x9tFxJEAo}zELBBi6YR(-ff4r&cK2x?K|U_B(!ABmpu1S<8)Nx*1JG-EexUTf=nlCFTaz6#k(XNF%Pl1^w38%vPGQgYKT*Z zkfG{~`z@3AH#f5DR)t6GdC>uQTL@I1+hQhY?Bc3ToF?*~dA$|9byg2K{Og%x#eZer z9i6#=K(LTfqtrfH5_An#2NA#c*gk(5`~dDzIod;uAZTXI9hP&nFn=qgB`N(AV=q1% zLU_fKO#|#5Il%_rD{_=j`@{Pgw+<#8o43>Uer&<+ngu14wS4;iE5w%NjL-yHSrI9O z(RW|E?lBaUqjREVt>oQsC0H-mhd>}BM`&vTuik3m<7OU0=_sAgVK*Vkd`V z>x9CH1lqf=3?(B()TCA`50VU(p00sioDkNf)ur$N$7KDf3H{#NCN`O>uV$4O1(#(PCI zCWHH4=x0t!hE*Oh* zWk4jn#X$IzFKu)}pb}Ws(GHr^kM3hB*l-h!ZCW*-$}cDTL~t1E<_w0|rpUU#gY2ABDnoOehOLh49z zqVWx$kiUytM73>`vPbIbWNycp>ZTvS8wh_ax)Bw6lv1Z;$CgmioL;8h{~t7cdtB1@ z_rKMe%dRV}wMuJl)ikqJT4qXDX|*g%v>>HEt-KJ@GBriOE>`OzxiT|v6JfiX)PaYrwOnp@_E9 zeYPj)N%ao^>?EK_fTK|CHJoh%UcHRZT`^o}OJvmu4lG~7%Y1`$57=nlFS9z^&G=8U zy#1K8)p$~Po~F`+)iTr(bbn4DR&?wdv}`+2bqgegEkJcVSkx9Vn|1ggXzmBu=-l5H z`ZKQ<#^I9EpVBF!=@%4i6(MIp#j9pMwU;I_^sDfRI@U~cV_(t${xY_u<8s8%?93J< zuUEW1{r2l%B_@SO8#ug1H;l!Oy`pKX9MP)%9c}!{PH(56>UYR>8QBs#`S=3#ZhN8< zlB{)ruW#m`w1A?K|CFBFGH^Mfbx!6hu0FPb)_hK~qDitx7Q2Q>zcUyVMzz^i#cQvq z#Jv*JlDFYRV)Ny8GGIU+Hvu&zGs7WGFVaJwDKb%%ApB$h#SV z!@PEvuJ&CO^D+1ea8{z_Q&(g6C3#;nWDc_)bt&RFWbMHa%kQ|MbX;uR%Sy9 zU)&i&c&Fg8CcLjZ^DW+dGx_tB&hS<(s1^DFSD9$9O#GC{0x^gVSQZ7(TdASGxVeWy zwzho(V&3E!7yaD_Bvf(w_60Bx8T)&0O@x7xp85-Pw2GYmjtBCJ%h5QxzSSRr6CiKU zV%>_OG8W3kuikoc2kV@_#&dfKIYY=^sjdD9L%;S^QvJBo)OU&AuK=YAEAyDc$9=%+ znD|e*{3dCwW+6sFFvmI{w^2#D)hQOuAc~@ac<1eEl)RW7h~DTWAVH{_2x%EPcNjTO zQg=rY8y@n-cLtVT;nQM{9-1bu?alVG+U)aD^ecKS&9y;M9!#|Iw7HV`O;fgZHVTgJX+)|&NFcZR>XX_=hYlfklPlb zAJ_vFyQ1dSPe_NP+ku~!7dvpAQlIL{Y&m-oUaV6S@9+#3?N_-V_is`6UMAgGhECejb-`Zh~-ZB!AM6ZYv39`@3OD>7n=rJeX zv{7p^nIV<*-HLIBe#2P-0cU0zNR#~wc5c!b4>O8+SL@dMj&XXsXlp;po&4wd{la|h z)z+F$z@Z6LQNUiA>J64(0cxR7_5s3WWisWt{f@p5`^;#x#cB#FdK=TNjR6CmcsHxq z?}{qSdn6)<#^36YlzTnFc8aixAK?Bk#C-o)sMInAHT?QAW|oi_Q0d$ebiSwKUcU03 z!FU|J2$Jh3P|w2QSZMI}-aVH$xB`SjJGU3__})dQc(d5NRZ2(@dKp+W)f9xn>=X+d zW))I2obAA*sOC#s;-yu0juMGQ_ws|>xdu5Z?1E#u@_ zo#86sQyb8H$bO6D`p!dnjB})~omOIzFFxPB9%!SdkBgin1LN8%rxX5nP&e9~PLIDsZ@2D_>G)2+b*P(z z@2yz?4Yls&Bbl(SSG+zlwux+YGF{zK&I$4OIrD!K09j0qnAhr+#q6+WgdNl>P7eem>}`tLa}#9U4N^ z%5H?XDf~h;yT}(qh>$T)nYc;KULc^c@jBmZi|4kJTC^p2zcl_tK;fkq70Q%OV85a> zY5F#$Hh!L6;HA@cBTc^R2k-~d!l64CLL8tPsppn4@$Jl)LIYQOYkO4$5-jwi z^M@sSu;oQZ7^#?ebBQTx)x{{k^pOI9o-?=* z1grhei$Q*K0z}SLB#jx}sUHb0<5OD?5ap zD$tb)1u1X57|WM~775B+9#CNdZB58%Tcc?RL9R-}=#jd$nJ9igXb&j=8*;jw-md&j z-`D>t8iKmY$mpBs`DT$6G@K9%Evo!R~lM1jAiap`6`j|=^|3e5%z zvlYTwd;VUnWU7(`Eyp1gMr=KO_)Awc6rDf3RrwCiOHUPdCcww$j^UIN5OX@EDU4IHIO~N6sOt+$Tz@KA-xLmv20xwz+m=pQL-gL?uGc zzxvp)uSksg9BRI(r#Aa4KWJ{>-Asj}r)(nndy{t`pshsvjhiXqIL&pER0jirmo@sD z2Tn~{c6e4E0W-Bt!8X7N+!OuLB=dK zizCTWuGetaBV>wL{?iS?od~GQhcPil_6L#5ZC{z;O5Xi{zhf&3q?cW-`s)Ua@OqHq zC7m$FKDYC{dH~f0?;h8QF)L)&29y-?obcT?o>i_F~_( zFkFi7CijUf>e``L(y%h6;g_jHLi`|B5hilTk&c@F#9G9i6GsNCWyS|IfcU=$e)X1K z13>@`d%wrFetY-6^rQDD)Drr2iHT1wMiE}c^^-rf;L9R7-J0BmRs6B)Cfz3QVPz^{ z>Co7RiR@|$RY+W~E7wLdDhI)nnUbCVkR{s(I@=K1?BXl0v{>w?jKQD(P zdRHq)Gn6Y{PsL~wkP44qlrB>FIgB*xSD(L`S(Nqs*P|9(ozI zJXSJYKYUBy*su5&q!zpOjL##G`w7sVkFp57_1N`R8ua^g@%6%LKSNj^R*#dx1t)(tvQyw{7 zo~vW7vqsDsDSaxL(Tp|!QrY=>9!Jl-_ZplWzW_F|4ju(6XgLL9Ww3)ld=nc!82j8+ zP*|@5Fl5H(&{Go})I>Prt+${J1gxm*dOS(fvBwJ6(_t-5qAwQ(yHv9hYiXYRMs2*5 z7+m(sMayz_zGW5ohLyFkc@(m_vvr#aFa%c!NJ@pTY1%%_4 z+op;nZ$q8of_!(6ZftKO$wBq2+1@UJkT>vktA`gtphz+aK+_%KAjhcOubpNGKuPVr z@BfIt%74XbHts6mT6gso%RZvzg#FUjoI|QrsM&Cej0iTaH3HIMYs(fG!$`JIcELea zuHrR3akju$fv1mb(jHe@qwvD(!J353cCMFoA&6tl6_0dV!ghHepZLg-OvV(jJ|NU% zrM-@gnfCVf^Ip*IWNBR6(82uzin;my7w9yj{yybi=4-K&Xu>m6i@wonT$Z@5FZ-NT ze)6lvB%#u5sM|DU4vr}+j3TNqQ>$bZS3y6Q?L5)=fjiyOQPF5sJCe#fxqMMO2hU`G zCLB*MH$f+WI6K9w0Q$A1)t#AstJZqNx(n^NLi5p)<{P*d+bbc|f!G7+{|l#|CUW$IoX)U(OY~c5+)9jb`x^bt6YB5lbb6fH+!Lu^33}gsaLjy3 zQMtZMr%ji&U0Thk#}H3zI&@u!tVXkcTt3l}KoTYU#&eAXRgO!gi89<`XpwjQfokaGEM99+kB+Kcg(Q*f>mif zA{L*Sne|gaac3)dj-3u)M+-X|B>eUSSVFhu4C+p80Ls!6)d)o#b4QwFW^SSn(kn#@ z`Sy+M@IA>f{Q(>C)4JNn<*c$@ivTm%VY% za|W>r%dehW5n-!LdqT5F(lc)O{dL1c&#gsPw`mM|AvoH2d+&f&)~OnWav(-v z7e;J>MI;X{`wxlr$_v(oPKL3c8g&IpvU>oRc`73l47YC4Z{w)GRUQ*dwO^S%b?)Tz zL|(~#HdFZqU=SESx23Tbcz&N|ChviXo-oTs zi&Sf94*frxpHUgYUElHgV(lALO$%4ErxgFi*kVV&;2!Vq&f%Zr?n~yvGkDCzzvq)e z5gPZkbk9!x%~P@Mx@)tpm_bC8cx!HY!*AAzd|T4N)?Cd_kpM5Ub)|u(o@?FdP~Qph zs#I_vQZki@+!96_wynUAvYn0gql4DfWss_96?t^@sU=Ym801ut`@@%{Q-D*eGsnls z4M(!l24;a;rtX7hi$3fv(lFaJ4kAbOiMWX#aF2au5hXJ@1CPxtVJhG&A7 z6CW2AB-F54> z^!-U8Ix=GA(+I@L4V|C^u!IaK)>P2 zXWZ zz$qtmJ=Nk3kYaN*6eyg+Ui!ynjeE>Cns0OQtXuqPXJKAaC&TD=d#fY(`LyN@xIv^^ zv^pkDLpm>wJEj@l1IA5^RM4q>5POFPhL-Sq(m&M~1_N^5bNzd{@mBe7R&u$-SD{jB z2oye-(Y*FX^2jac2_{JAg&c@#OWrCqI<4lS*l%tJC};3z+c>8*xl6kmgw&QH?%VV^ zs%tSs&_G5J^|tk~W98iGeJr0Y0vX!p{tn3=-M);{(b9;07-igCb`_@iH>(l6w7jv( zu%Rf`44lMpL;~PaV{&t+6uPT()`e9XY>E9BTMV$_DnF#A*!>P_^IVK-7#sGG5B|52 z=g4#_=UkBRV!R<@D4esDg8W4pYZfJ(FXpF~D}#x;P0q)SY37^LmoUpuc}(o-ry(vj z+Y0JhTY=otfm{e5QAAVC`*VHzun1nGRf(_mHIewW!7G%vf;GY(64{3}+4cU;KQ!^# zQqE@UCn3Pm65AHwJF0i1f}j7cCj1v`54;|~x~6TzkFw0izf0ssrE_x9%Xy9dgNcVk z@K{c+NfmckRIgkLuwJ#;F})E(?py$tByc}oPsf0PiT5ZX&I8w^=viOEP=o-a(7-_u znJiA9ADALZPXDpM7n92u3QS5Z)=SF@8;wGcIHiHp0k+RGzkmrhe6oJ5+3BzYM*G7K z$e(259|^s5lN85SA{EmV8uTZ*u|NN9*Z`8>^Q-5+^0_gB%4BCghw| z{vi7UH)*3HOjYc0-emaS9a_TjR(525a*A z4Oai`5z{j}nnsi{9wiOG_^JwoT43+4pvt_Kc2PpTn*F0Mf+EJC##|A{^xy`h#okEf zdd{OC^__GxB2}ZDfk=r$A3CX%R<4$tfdf(yNcv(%JN^826Z@y`uRR z*MXJat1{FhluR?C`#$I@r(LrZ;GOW11~m1g;XHgXhbz!KaT@K_XybJKiSalSG9^9q zh9pZ4+CO9?x0kiqx-{I^yBExL74vAWFPIT3Gmn)-N#+85;P-(BYW8M!FcAy&yspyC zBsN*NyGMIjf>MlSz`I|m&qhm zYWWy9F%|hUdL^-_NtM<4468b@T~e>OzVrngvqn5fP!Vejr&4n|V##b=Di-$^>F%p& z`DD=w{iG5cu9!$N>^mNjo1v7b(^3A?vfIh23uWmRqq^~i4JL-N_=|nZ`ehT>p1hgm zF$P#vCQJ#?&_7adxmbS%>Xokcm$$q0L%CA$xS&z;#fa`jT-!6gfU0WzY>qxaBTc83 zGk))(xdN*N%mOR9zH$8oE-nAm5NqGz32{ zLj1d2P1xx!UHBVe7Q9w0e@6#$#e6FnlN@&$gpF^bRPNkrnUs+bTTI+maT-5`7`>Au zMyPINMNgf7xz{VImD=(l>{z?Y3#t||)!(b~<;V3*uo{EQd483?Ts=8Y`Tp+jS@59; zj6bM^#R&X+88cNs@$ zVZxKKU#&@Pw=TK+nvT-KLEk^+&w7vLTBwR0(dIVi?*#W|Ocg30Gry9Fmx$yM62JYD zHU)`>!Om>U)v+_p;v1lhkRW1xZ#06IVU(EP9>*;Hl6KwjCp^bcbf??qKT6WMY!MFU zflJA)snbWrjUC|nJGcR+uxd%qaN1<20r>WG=iDEb(zJpLr~l=?8AKHW4{TKIdQ+pH zS=pIOiVNDcT_l&A7M~an<0czC;dIg+_|6^oR2G+tF;*-b;Ey$Okz6Sb#DUH(c#`Jh zs8gSAI-{80;yPD3AbK8$Q=O`>!Z7l718DUXqd$~huSyQ3(CwS7!&@(w|HH)ptx(nq zJ6riplNh(R(slEW%O5oA04lf|%S)T-FFdGVMzn6LiC^}fI?rB-8w`8pp|4YX&y`kT zz`K2f8X0opeRl2ekwErJ=)6g|{=}2EB$*z0Pl?**2t+=FmC(GaH04S(cm432+E>va z^}dT9S$BO3V1|xjuIs)s{76ro=MY>B@4@Ei5SiS|SSQ=~=<^C#8I;7oY0^ee$@Pc^ zD?Hga7jyknw1fimRkb=k(?egZ+AOWQ8Zdd@27U+aPxlF}2QOjnc2g5dXcfNfOO(vF z$o8O-wt`-XO$}dML`sHy%s4J7%WIIem%&?~3@6#!!{9plVm8lc0vbuRwWKH~9sSI4B$-QFI+0K6y%6Amt5KV<*gU1_9Wv8)#7WppZrfpN5tuYS@5j;Em zCt)-}qu^BXgJQQs3$auGB*SQnVrRZd6leu`zJz84yN8u*0%nj(VAfR{s3<_+V%Yj8OK6)O(HY|;M!X6{13O*sP=2--Ag{ywAmT}^MM8^ zz#YAgHbyD?Vd01&+mz!*vnwNOFBjt zSYM=DKkWjP#$G}Gx~8#eSSjfmbppop)q=Xs)>7SC?uREY(g=l<1wXrVvXTv~W$kyr zVB=>J(fC%i~jF&=yE88cg<(Tq=SO0=9Q7c!Z1j4BD##FHtcpSrhq{|~O ziYSqL=~&jXdeacsV!Vus%GZcm@bu7sXcbrSQ!v_Ph{j9dH?=$0%v(_-xi9fGd?>Z` zqYdKXNYWk1a&`(mk6s%#@u0mDKk5(HAJC+TdLq{Aov=i44s)GMCJpbVFdtZr5Dq^2a1*n zB?o`JVnF`&RqRgC@{u#H_{qzfCHteAv}U^j-tDa1|GPz5d(>YuhB5C(hP9XkAk|)m zj&#qA`!~7>GZX48W@n&eyx7GUo18A;;k6m+lM_U-*~R=g@Z>urRYgX;IiQLDnsdV= z_2R)AZ9HxWx!TY2G4>~Ag&VLWo|qpMqrV=KWvDBFZwb*VvBy&!Z(vhb6db4bc*Tix z8fi0WnD_4-D574Sxf9yfyQtv@%l@Lq-bZsa3d%=!ma@}tLsQIeOR%dKBkMkQJvkY0 z+%jLsw+9ULSYk;w`T7~wf;i2p73?KUuqD3lco4CXdN1;#2RCR~531&O9>}1t^ieX! z1k5zYQM9ZnIG#%EIe)Oqyl(JFlS zlm^-WwNV+;vzA1i!?hS1E4VXzJhdBqbcB8M3IQ2u;M+reE-_6xiaAI%g?4af*ny!J z7Vh-ku|GCVn{{GMbkvHf;5eNN)&VPH>1E$)&T8CpIJ<%m78Q!V6G ztf-;i-nr%|5flYjd1c&N0A?+qH6OY3*Jfy>(7=k7p^(*++uzN)bgZ!Vv71ft^aF?^ zy}yw$_yRFGahAlidShu%wdIofunx^rs zBr8e}*2L>AtRx^S!Vh>vec-i>{@CbcRXP^F;NdFZRLa#zp{H!T4$K4KQ?W!H-Siu| zuiuOMeLrlr65#2WyHBFba z7{THwnO@58zwviluf$J5PuNH!mp@tD!mg{+8{42Bl}B*|9(mg^X2g`wg^oSx!iDLj znImVGuBwp;80GjNmyS9^l<tsR!sbc;-=~ofVr3Yu>%v={kBWN)ydXUWn_)G0B zZxr_)Q)nrq)3nq=B(cuW)1IrebR_@&{mdokj+`}+q~cUC2_TQzOjIQXgF^DL*<{L& zPlev#H14e%yjl~YmNi-r@$IyMHEU-cG4~{uQ$8jQsD)mi3A))cM{O7NM6r<*6y!Xm zK9!G?L)jgh?x2BwoTPXdmqY{QZ&3A=c777_^nLal9g?w%ogtG!w7|Ol#y0~XkE9-_ zS7gZ@#$%kPlH~hs2~h+z({&A+>G;uj8p6Vqfz`!E=u{GlCYqa_L~{t}jzb zkgM5m1{=pr5XCb+#?~NqXtKBgX3u5su@KDB(hC9SiH&{Py^a41`H<)+NQ?S9`KH?m#R+Z1cXN>Ee58qKs*j^;z|YnS1cI-(mQXl0#QkuqVBw}L zykgreQ!miDzcb5a9^>Ex5e%Xo*lGMfVCIiElM3ZSkjHNBeP*YbA=+?pKGgj$X7EWj zRsehVn4I2R6pW#SCfN&Q@R1eY<05TDks#{3xKvHI(<|yX^ITN$)e-9q6r9m4sPhY9 zeBC(s8Nd9lB)Q71R>)TN%P7?6v;jNfw-IDBx^ujsc!ox|z08~2ZRr{U2!v~c8CETU z-H=?iHJ0dVqOknvze4(=(+}b=gyL*8WNnhR^V7&&45p*#{JM|$oRkViLXJ5$-xhmk z_ywM%2oJowT5=B#4y7a8f20u!ne2T?RREa1j3wW=_xAXwcS6{$1Y9Rr$-}M{8}b7` zNJ`93Yf`s*_J4-LSLMrHBVC;mdFxy+{+MR`ebOk$(be(!s7j%JqL!rCT{JByb_pKX zdijt=TRcKti^?SDAaJ)~ zIo=+^A7n@<XLSsUepoDnGb*a`%F;R{Z#KLQcEbWY(3(dc;YC7ksCV+_dK$eFa9Ic5Uy+qFm`t_hJy8 zAe5tl($h+1ys71&X!;GvXV@-Xp^!xG);#(Z&d@PVmI_N!Gtck(0gpMVje)j^^QO`q z>hfZ-++Tt_u2JSP7_OzvJ*wDi8nDX=Si0}UDPk%Ng^6j8txId@X=9x-HgVwk${0}i zTTd=3mBOBL8-oO^oy6>}IFZ|IELmA6Ge?u~$N%6z6#*+bo+)&D3nI5+eZ-6Q@y|(& z5M9+(-38OdQzkJfo^05{ua!)w#&#+d!MTR~Ws2abg-z7$H)g2KY(1-cID4i!@nS@( z(CZoN)hhN5irzbCU#jav1#ds-n3Q9_^1MlFc9B*^=7+zO4#^^J{`Xkv!`c}TiS{K2 ztqfBSu9FIVo%U;OBD-V-SK`Zfv}eENjnZKpx`Us0lF|?r@AIS9H~ZW~ z=HMLI$=g3a`tStBzXM}>_Q0#{po-Z;R~wz3ZY4D+ILA<9xGhyNkvWn-Qh z&GS37_^2uHuXmy^wZ)GDXAw*ov;$|=@o@kh8f^Gd%#J7zaftb5n>sn)7SS7Z`X!8I zaIFA2r*XObmIjHrw;JB!#_r+9L9bP%;m;RklbNI6<2y{T$7bU$3E8b!PB%%_m_}0IL>^F5HN(6vv$p&>zum01Yi=K) zS4eSj9#N5oQK!jCtFB!*4Js9j#Z<6)xNC(`#w>qMnL`;n&>=DI8VF|0DU6cpy{wII zdJ#Q-@EC-OaXsKbcYaztl%$;x)~3ZX8Px|y zRsVw)=t&=!>6+ls4d3lCcA=+FEr~BaoLsJ?Ljp-kMHFW_lidZ!uxippl7_&$*|Jy) z%xEIl`Fr_p691tj@{>lSJrnz9F(-2X;v&okpXN$23r;Zy@Sn|cH<~2Lqc#ybm6Vd{ zFLPC4b+&XWXzDD5VcVmXKNj%qYG5DA<;L#;WdpL(X&2TT4NRy`Kp%Zdf;%ipxLZbV zaAwU#W_*;z$>5Pm|0Si)Nemo4z)Q@A+sqQvs=}u3LuGNoliAMm@snde>Mp8t z6@)gb%rz6%g~26k^4jeM)Z60c@4@?|SPYA#??0>(TNf(T;cqB2PTV=^%lw39AaT zn)dkKpEIr(5^4zKR?^L6*C~ppcD21#kd0yD%c3|BJwBZ+(s=)QVrg15cz<-6-7KL8tE*!6zpOm%mbAa0jB~Vppgbt zkarc>Dm-SU#_nx$u@*s&bC3+&@)=1eHgAQz-wdDB>pBw=UHWpLXtv_}|Dn!H@xH)R zKHMglGZnfg*458kY8mI;aKulkq^QL8mM&pu@S?^v9}JPOlkY=MrE$;vD401(z?P7n zSh|wKkE`R?62}i>*I61j5AX1;Vk#ZjQI4);l4dq{dxiX#q>Wdf%52Dk>F2ti^rDxw z3(tBh*%PA|-I6;RY4ND6^GoW&e}(bp=aKvB+XH_0iCR(GyUl!>1+ol|Xnrq0BAH=S z1QQ$wn-@!}oVo15>sz1N0&pLIL{AT7S9_pik4~bX-!zZaN>&A~8>y}`(GHAtY!X@UE$QTB+5n!Qdy&c=L{NB{ zkf^tvEra2H>&Bb3&1jG1kA5kEuwR|=o2C=oAXn>l6L*YX%R2lx-6Ip;5g=K7XHzm? zAxR-PO_<20B31LJGfIDwHg2E-rs+C!b%}8-2iD z5MXN3#g^C$6h?gfsC2`=6WLcC+;cE3Jv1PHP#`j)*w?#`gwr-z8t)GosRStJi6LIP ze!rH{_JAC781{G<7^m~Eq2*^jbopge^p$)F8!G%y{8f>oX5rM_jNte`Fe&Zeata${ z=#XTZXhJt(@^9s!~EXJM6F` z?BNKJ$yh*+r^WbI%gik&dgQUkw;HT3uASJ5|G>Pku(f$p#-6~timxSZ$FPa-`VC#k zGYU89#~Tzb$956O=5~vr*~`Y-FWSMGE)x>g(vH$8ckC~!hGV&j&n%$2un@QrBc5(1 zPoKWxktBvMtl-@135pKqJ=6c&p8gV2>ll-CphHU7xj7Da+D+jP+Qzd9 zVj6~5t^I>M(W2W_k|N;)63ulnM!*doPSf|+X}-I6hi|v!)WF{+_OM8^XwrKTH`+J= zrIX&JyS~p|Yl(Fm26GxBYX*Z2IVg6@STuhJ8T|Zo(=2o8)2hhhYSMX4K1Q0bkjN9y zQ09`n6oEVTVOG~U9Q3=B<&ZELfdAtCl0z~<#$elOSiBs%k4E3i{$Jbj#~y{_f)~8# z$$Zce0P%d&E#kxwJc8BB2|4N#vU4~W!|bweN}|ERerNx<&Dt15fU#dr9#XF7**5tUAiwd%dyidS}EN;ph?gQG+Qz#h=tr z8D}wPz{_Z(f$qfjD+=-)BAR62n;1lo&j7O+a64)w&XipWPN_vDX*o1Ff{rHBTzh3) z@b4(iX1tY|J_6{AFX279%YE+_kR)X2QJCWf$UE*$q;K{;X-TK5+%B?mI>Nqjr)>kI zK+5>>lCNUVcqUbE?U!BEWj+0r|Jy7enU`uX!!c#Q#%eeZK{%u)k*JZ3Nyw6_ z>4&if0;Q29UhJrKIU zWx)_CJNq+G;u&O=TNI#LqdEU963p0VhfxcbIBtVvu=9I^**wibaFL!3fk0D#eb|uN z<)ilW-+rE>lJqpgbplC!I{Q-E?H%G7SFpI=#Epze+|aMZJZA)RCYHLBn`Z!Pp~-A{ z0Qjk55+k82I-0=wLE1)?@VVXq?FM~+@1f=no-m|Ng-jd^J#;%Uf-fVJrND-^hixa0 zA7%+GC7-Hnvampyw*+WZb1OQ_U#R$X_b#IQ%afPzN%nF|Bv31d$ z?*O2^t6%8q%fue1FJ`l0b0Zg|B<}M|fyr6RuMAeI59Zqz9_AHJLuzC~?8)J!8z`9{ zk$#d@!TsA;D#FDPqp{?z9RWK@wkwnRi;n}=yaI6My@^mf(TI&RR z>;cUuK1K%)8w=o*FBPZHJgbmT!L^wWf}>t;Qt(RTJyOB**i&mrRAQQ54Ji(Q8CsgG z_>MG(g^av3&?x*_YsjJ`a*39)4HXVoPeLV=I8N2*IkRX0L; zTGRO{&XA98&|Qt1akMx%r)Sn;TTEQjkcyrD8976#|Bkt?55KF8^B66P7{z!d>D^)7 zu8MZHNev-OQ!BDJ4@PXUcgcP)bgGKjjfoMN&7}h0?h|EOJs-&l+H$X6)|+0j-X_8V zCvk4L>lnq}=yx%qJy$B_q?%Zd6r@BIsiv03jTq?YYg|K9@f!_SR9s!*6-FpZLbR65 zEsq38#V9BPU%?ir|A7C^EjQ>&UZxS`dP^T`4xYC~?^UtWvexyL0#kV$6NhVImZ$hK z7|W|DNv_|CBlBaUBFnEvAofie(Sa#Pxm6N! z1`>_xxrVcJ{+<+)+`j2MDaXaSga2M42a!piI~gnhbssv+EnYo!N&Hz2ik>G0Z|S%F zMxM1tK_wj-_IeCwKn<*y|B{y93?JI#V-USwRf*Pu@d^@Hn!wf!QIr zQFDt)$X<_$7pb`Rj#P`mh&#>XG`bPYv?o_JU*^i@~Th zsQ<3aQ8)fJoIO{n7|lp{B?v8e=bT(0hCK5cocucCq+)^W^zd+rljc&6sMGp3Nx za7M=1f0Rzs;X$|}1Ivkg&oUI}tqz7|y&y{Ig@A#K^KwwR6cHNW+j^s5Ya!!+zmtsl z{wCZehI}?3!S0D_BEMxI|PDy}Wy%J8zfhi>qjGl9ej)5#>w+H zko#9Of}sal*Ihc5_9IG8@#k=&wuC=%$93lxb0AwaD$+s_XV4@2KW{{M+)h@EeyUq66*aiYu$&69D_*i!eQMA ztnA5N{7oW}q{-%U&DtqJOTv06?YT+KC%Tl@h-(meSI_qqvgTrlJ5i*u3P2)cO>p-& zuOM9udw}Lsoq%i6s*q%JRrhmsk+OdH)0 z-Rvk3Sl<4977VS~hXdo+7tfm5M(EeqA{D`pn+1pI3&$& zsN(^&)8wzs+;gAg7H&1-b`I-i@N^r1wGP-5&{!JKaDyCXgY31j7rtA~^i8eJ^-V<2%+K1e5~ zZsmSJt`0ZSx`VpP#&b;;q<7WzKQv&h!Zfjug7LV*clfv8e-DK~z4>X^oI&FS%QHZk z_+<_E+xuIWy}A=I1baX7rpXofRxI$=ADWUKN~rRY`qot;oEOvp5lw3qWu^a0s{Ir-&(Q<63;)IS8&$JT?(Qs7lspLf_3y`^ND zqaDob$$dfkgAAJjR28~fV>t0X_!M%>fEj+0yFA4FIP}ioD0*BMV!?gP<2!f83+usPc}a@fS6tk5eV+$6vB}>8zmPcy zl4Cx->p>~G>a}Qt3GYI~F2|3_?(%IdgJG)S(h{1%FFv=Jp)HSc9F$<#<(kgltTRZ5 zzN+qU;BU0Y2SJt_^cI)n!CErl{#`7No~uZIvAf%{D&9cwv9lh01(6RuLB-SFYIWxw z=|+XI62E`EY2WWW8YhYFdJgN;SJ!XU%p&JwrrL6)0k8d~CS|2ZW6GgtZEp-*pmdUF z8UatMOH9L=#gH?H9b?B+9Y*{Sb1{Z7uKN+gFb>u4LiXmU$4Al2Z$IxplD)xazwE6GSXU0?hb$^Uk#02QbKU zn3VQ!&yQog8at|b614U!v9SE)SMN3a`n=-(N3-k$vzi<>KTJe|f$c|koyr7E{X`!= zHh;tfcnG=Pb1j|o6s8%AVS`jk3d>}W^oVTGORZH~8yAn`p7ArvHYgNU(c?b@ax`#G zK_%2`72xj~+BDaLRxlJM$Pqg0FkWx`ae5S1Gp|T5)&`O@-};}Ua>Eoa+Q_+h=x)22CEAE*VONBFBg>sL~XN@N(s+PW<#K2=Y&(`WjowC_0%~gsY z7^}1+GPUOO=j`7jD{oB8{`TdLEqNy=-1y^%@$Cygtr_PU|M#yS=fvL1`})Mw1;&*1DT&=Qc?T#YnZyr}vREY9KV)q|&x#CAD z?`E4HNDDW*QajI2JLf%5S{uXv`%~-UY5#bip~i@&E+$s!TkQK8g&CS8g~$}iI#QQX zx4B9-TfaD+u1F;YbaOJD9!&1{d(IfM0d9$t)3HS_1N1`uvc{qUutxMQS_|IbjF@Lb zTk`96OUdSTLm^lOIP;7x6@9DvTGL0|a_!vyNrA@+0e6nZ}%Rprx<<+tNY~4XJ`l zLhkJu+59(if8r!-=!O&Chi_^NPpWeYPX?dsXteOU_t34ZQr*#Y9h0An<}!fub8=ce z|8%U!5v3=9j(dxp2K~-n}zs&vKKMWPwOnt$-Thoz?_MppI9KxR;RGt5j z7u2#VJ@*;CQ;v1^WT}4pJ7t|chZpWQKS>&TEq%d|XuIXX))?LM?E#-TUCz5LH}^a# zY|}s3x`HpRn}s2!_!Wy+VFFhOb|zS}o=J`)k>PyR!6!UH6MN&gM`1c&y{U zjPv*e2FZZR&cbhs0(zo~h8HpHZL-inh2V%n)@C;yhC2$KjHl;Hy^#D2>c5lB`73x! zjy?SCf8u(09h5hNb4BYUb%Dhd-JB&B?@89j7f;mY^;kFCj;Xbp-qv2yRuhHxLiax*jT;l02^3TiApQ8PA zm9u1^AqYcE#C_x!SiSA%_SelG-2ILH(I4+^g4DaTms_MYe;7^~z3$op-Mssgv!qf` z$?L9J<1-z-Y^P(JLPD(%wjBd_>{!BFaGq6PUQoB2-}Y(qg~KN*rWrp36~q?L*{L|I zs2iU1&j{xqw*|Xm610oj(mU@PDmxqUmJaeX|TYGlK37DqBw zyX=8_v~re56LmVCmtvU6P*=haYObXc3=^bZ~jQ3cixe$dmnRt%>Q-|Hg!Cf zb!Tl4Pz3f2=N;Qq@mZUU-SDA;zjy*Yc_ej09yKV-aL6l?A!ja{xMqH-Oz~#g_ry6n z&-?8uKiTlZ`CV$=L;hDEyiQT4o1!afmo1=Smt)pnk*)h!L%k=dl+--v`PLXYHoXI% z627ljo7PboSrZ;=4V`4knPTdDJoOW9R!aQ?%pTL^Z*Pu9Z~oq~X#G1D`rnK4jz3~I zIO42*zWXQ-3kJW2%${b!Fy^?vPjy{_wdJ)h6(^=?De$A>c}?o^$Vo#eCRHpOA~fe!!^XBRq5 z6bTcD8=!8YSwdu^YzPy`QOLH81MRekY`RYc!OjQ&?v zHQGiLFf<>&0#MaD#b#>>C2|+xQ$qNxV_`S+hSFgx&MAh`?N2u3D~EKi#9TRH{^Z8` z_+tg9V3x#W6w}NW#~27@VVX})#~p~FS0!f(9Y1Bi3AnWC8?CeR0AZWM7I#dECksJy zpR`P~#QyLC65jHV(_-}65%{JLjH`Q<`g@oh*0jJ)fqtJS+4tIg3S+*+4wrdo8mV2e zg5Dr(C81mZy5G@1uF?5rOLt>^IGkdB8m(j3us(fvWk zw+}ZStHM>%uiDxX>^SP7M0l&J@8BjBIx}60+S{WEM5##P=lLc!SeqwUcscRvqPMe} zv@B0t4Qv&HuDASh>GnUHvN;Ft6aN&wAb)a}6zPPCXh7<(VoC?Muw!MWCOv|UnO}bG z>2KEFdgIz#LzD^cw>9aO8$U)cl(r!&-i;qz?l2{F@Z8NVx!W$#9YC$fiQ{nJNxSuG z-pMQ^jW=RZ6=uKH4~4Q$VjXtb$w)V?|0b~n^e^3rP3N@q(?+SB@{yc99PZOCwk*tn zQ%E)%W2X;>S$21E1ND>z<(4})pR>ZTqQcznR!U=B*y+|Q!2U^*t#}g#wC*YIa|PkB z9bsDXv83#rHk5UezImbzjpVh^=yS}KgVH39)wk!pbp-q0bDaC2`ni^z08t}3dY!vd z(#r0n^{P~#oX?3Zi3ms^bfiouWp~`wlY37}vqz`W3UiuLoT^U%**NiAR|fWakYxzr zT+$ysID#o@val9TO%$JkTP+Q`)T?<HMCKQpYg& z-{-J|Rd#Wj45IA-E+tO;u9u&z2LtWJ$M60CL?Ruck;z8F(H;1ctTbr?mv{{1phm%V z1TAVR&tRp=nLPoajS{biz6kiEFl+r)s_71AG~NN}uuPe}Hj2sJ!qd&M%UsFm-QU_` z{2tP`a||4GLs!nud#fjb%0SADKzQS1(QwO_G)vZ-*TJ$p)(X{mn$oGekj(ye%b;&V z-qJ`-f>$}MwGXpF>DD;S4Mbqr5ogW=^$$pF)3?_$#F4cOaj8>#;yMi+Fq-2zBSE`V z)NZKcOwtg2<=j{^z|?m9W&ew_yMh`xe-E@1*L=pthw-5a+WtGG+RW>~c9y#6$CQ>l zHB3LBY#%y#(xqI*5Enhg{FyU{I{ezc6^9(ZVJ`^$)p zecJ41^yS?bC4^Nv;e!ub<(O4#o0LI%g(nK}$&j<)X)!)>X`1`s9gh92zGtXjr-ONm zEoY!IQ^TI?iVAC_7agBA=X({7@IBW#6ygrxksLRQ1QkK9dA>MTK*7?H@%rJ6QlsLU zgrstK)f{%$49@!@vJ&*S!!7~g9l*YPI=HdD@NB&=2!#f!g91=tOc=% zJ6i1gIQXsMZQ6Dzv7sd*%#qcie?@>MaO6mT&%fjK_1;-!qC;-I_B=5N8lfkch#vhC z|HduF+N)6B^&?C{FPRt5chyPW5UrSrgP?t3p8D}3)YKKqYXHiL>>}|oPuj57Z#d#& zL!&VJ^}CdWRgIF|IBofAY^U~hFB^<&Yxk2c$YV=K*!^>C6{>u$2Awve9B&}b(Sf(TlgT!g?eRF69WiB9&vNv6nvJBC1kx*ppF@H1{Hff4_yAV!I1nHwA;GwfJ(zJo-A z<%4fIVvjk7@iFFyio^*l>(~##>j6)y?Uy+I5(|$S0DSS@IE_>KZ~%WVqI0PWO>^&; z>E*+e?fmca_Cp?bU_BkwbgYThZ#z)*k|8fM>zhca+NTgc&}sg0;laDXkW(tu;Br}{ zFV?yauK!(y^7)?8m-1)7?0RE3w~0hqIYl6Dd2LHmXIR+qFy@N6bW%3WZwAED@J@Pe z>XiC7N@^t2o>_lco+}X{hAXEFKYO>qFRW(7f1UrO&%$ljCcCPu+GMw_HJ%%HqLLUs3v!w_?0Y8smgX_r zBZp3rjPb-*Q2nPg>0Nt=UFGph6wYLg;?}HWC~XQC*!nCXl@lA-Cd~UNlO9zH!2j}7 zX$HBl{^Uh?>tcqF+ubD{S$MH0S{NPz}oDG z3fj4TlnLi@7W8JLs*^Mv>@hoz-@wQsBxPq*BqDTK~7aj*L@Z%l3nKz7ZnB;0Pz5X`rAS@ zRz7^jK^?0-$(J{+0TR^7`!)E@Iu4P^bp%p8&7Bn&JVS+x&ez*q$0f-)I7-3?y5~l! z)HbmFf=zj#!|!5-VE+icZmO6X=WyY;oxtjU4Ao4V%J%AUlh=1lUnqdypk>KuR3EnX^74Ig}e#dyDh8an{GJ)u(hT z5Q=4bY@YM`q6Iu7t}gzYU>`IEpP#56Mq%TfKI?JMhFqAvfpfIA02aI`>02lch~ z6!7jHd=C$Q1##Dygjbr7jNNW5u$PKs0mjlr77$h{9I-c#7gHjTdW)d3Cq`#L;f#a~ zI7*dDZWKsElI=yOXzo?ynU}KVy%m1Ssu3=DoyLdNLO*G;``3|*=fI^ZRVz1T-Us%= zbzgK3NC8L{GUMyPDO}hM0@$du1u(jB&NU!V->Blbs@^@Tnk+#u(yhBN2_IPIpFsx8 zJ})e{e#tm5&$f$Ru(aw`{aU;@c!??nK+Jcfc=sf^-7IC!)1;4~eP8Fxq$XZ3%r;7Ie@OP|9APzApxHPIBzx2b?XMovx|rMRR)z20 zFoZfaM>SK~D#^yRj#u#SKqtZ)$2kdu^!euGJe7AQc&si=FDI<`51;>0P4P(vay~-ZUfDRWh@W#3 zhG{Mm$XIi_aBhK zQa%r@e~>pwD>W)2{aTbW^?pb1}IIKsO7x3#khU&G<~M(wJhJNY>a1wREg-m97aSpM?ydWr3(j2+F6CD0p_ zlsqiFn6|syHijUog8>cIek{@3r#D1_W+r^JoWY11h-y-{d3R!Pa^+9r=Dc!pj-cQY zj7%|wPw&pZ@#`D(M`Z5TW2xL_NQ87+k6WCTNt>^ieSHf?%@_Sct3&gK+yYi32;yf= zdFfF2#0y&6bk9DTqG&s9Y7A|%hY;t0O`e%E*8}e40UpkV{Qi}HUDoU9e7{l4t~p!e z14%IwR60`e=-6GoA}=&s)gVh~N21gRAA{yXtN!AI$|(s$lq$+Guk$7p$9&BoMwB{Z zGFQ8Qk!E+QKwW8SpHwJAzHKBY4zeOp%|o@y1_`CpqQX_lZIo~qLx71UJyuY0if9ie z_MVQks|v*+zzb!CJJp|5()nQ|RlR6eAw-t6_R)pj>xt^a_wWMAs{yN0Geq=YCO)~hmz`^SEt zc!4oU5;qfByo_hX-NlC+G(Eowg3dd36#-us>Ugw1L&>IOZ;EyeSc(~C?6>pUhaFBB zbxHgiIVxo}QqOp`j$&G6^cCd31)*bk=5pD&R>jRN$+9j7JH{}PRhSE<$YS=(n5!!* zSHIKJRRA8|xYpm>uR()X&QM7aDgrBbX+yW^|792XjQ@aaCo8Yq2vw=8v>Hc!0VK>sRQ3{$H5>;{LTOnv@9T|wh z-vsDUisy!C^R6_@?t<=>%I8Y~50=^BsXd76IO|!Cva@aF=HSH);k8S|FJxP>{u(UP zazgfN-M_aM75utW_`S6QQd7s@*;{dY9JH^dsPkBySVEcq*mL{6uMW`RloR)TLhMp= zo8=1{&~;`vntV%CUdAX@v&XRP(!-To8ZB%G>K_gS5NalOpjLap{7*mBUhZfd6gIMh znBGW3zxVYq>#j;xN#|M<4qV_w?TlvJ?Z&^5G>j^pr}?!PR%@yn#6M?s!-j)^9}T8I z=^IIwG#RMTJ++Ba)u5AzN~VFQIuhFuUlNZ+EAU~vmjP>M7j>};x_8*Pu4(^YDIRrs zFR}Og@$Dshe-J^uS}KN(jRbvPvt)Ha;1TiGY77ZnA24LW;q@1lZ@0M8_)GlH;OUyI zgXqS2#wsv4q1yAyE1^o!nEkewRnXD z8#+;951{t`oAb8o3U$7pl}F9Vc+4H)yX1|yA@LZ`*Vn+*YBmelIc^`aB)hT z*q1q$?4mmtI@++m3(owmUgyLuq_ye7~^l8*H>}fcYAVQOuv+wH!Nq?YwM#mD|{dZ;b6h`M3jx&u);Bqw8lLoE^!T z3^Gu;l`M!C#*R+l_a^hmuW!%+ioD)FZ21mY3g1~4nR_m}7GA-G?*WxOlCh%y2E> z^$KcM&W@<4zvH>+Fz{{2Fd*u~j5CO1Z86q2WO^i~lLy{3di9b6mP>X_p~uJ=7%|X=_U?gK!B@{Wa8^LkG`T~ zhOaAS=r0OZji`Gd45t=JXpCF`KQL2V*0!?tkC{mT%CX$g;QjojI8b-B*|M7-JMoCw z>Tj8q7Er8&gwF}(2!8SHMQovhpAb0E-dhrb0+$TtwE4bsvtro(3wtQ567ZL39TweQ z9e&)WS%;;VK=2^{JSY3oA>}^{2Gw!5e)(VDcb5ASvniTXU#~V=h@^nBa&uWxRH*R2 zM^ppcSl+`G@43({7AAZGAfN_0$s{@+3eS-hq>?@LQ?-1JCU84&NgdElHU8kMMG9YO z@xtW%MPsm}{>^vl>%MD&ZE20(I^FDcW1~UC@7`M6HVr-FHGo3sIo@S_5QO z*bd*9aB)<`Es^8S&EJI&kG5U1$zuV8BRtt+ls=sDBBwON>u(~iGDN`_*KXQ*ab$*G zZ6t`miCWhs02XQpNcrERKBH&kogZM^;kU-cR88toLN4X8(vm0RPgW~=L~UDe zjL-ES!@YAi;mT4oMws5N5)YRL-BU|QHJ3gpc|p{4jd(Vb&4^}+;-DhWi5FqX`5-u9 zqJifAL0jjbm{%V`PIx6a0y|W#g&tiby#cA8upfCLh0NYIVe-LV? zzO1z`|Bdw*n4H{NP$6l;IU-{wUT{LVU5&x=2!?ofk9+d{0X$XWA_ZVR++n5h4EOy% z8=TKJd}=ncE_S)D9K2ZCV*C!Snx4R<$SMRC{0t80sp-oeHR|x`T4yV)6ja)xGEFu; zeNJ;O_$WL0W9FyS9#Yv)2))s&k=h)k^u}j)GObkzxsD^f3Y9xyQ?^|(`oKLLKy4l4 zT*dR)%1ZYMmSo$1V%;pyuvCM%l%W*~Cf5$ZpLR&iP5sjr!%0QdAa&kCGL!%<2Hzw- zN~vv_aPP!qs=WPR-Qh4Lhi^07AqhJn`Wq8v(+Z$6pXnKtqhd2FNI-qS8j7nI&SRMU zc_2Ox7C3T3yqgl|GaW4F$44~G5C1HBP9BalNVL9$v|=PQfvSKiL3`0F1^KUq!gH&XrxG39FJ({^{$I&eN=mBm(;Q4;t{ttJxT=p+(b<6EB^Aryd ze%Pwm{=A<>i?>)CLGBktr;2G#%BKkeR<&^&_krS#VKpReTpA(=YysM0Rtwq$><67egr##_3F~#CKPGnn5OKu~XtKS%|P6(?wb9*b8 z$h%-R09+#Y8P5PKLVXnxkgf`qRT&^OW4|9V?^6}?=VBTielTSqS9xRMJeL#kvpG72 zvXVRnaZgdGll9{|(u!oaB)`|`yu$rnvC<)qm$VywCH{*{OfH%zbLd7e$0|4~UN45` zK2Kpd=eO)B)6XGzURpje@e90syK*YsViX!0VVqHlJE)4GEA+=C}ZNI+g6noGrFApPhO&=Z{bPxlv~ zW=#ECgpEPj%d!1SqM8KjNf3rBO5XN1|mj4H$evSvvJRuN(jcQmk$wUH2 zW*D0IF7Dw@agpCqKY0*BbT5E)@A6Ldv-M}-><~79mnX#uEN*DdQrBU@J{KkM|JNKU zn|kLs_|rgDrD=5J)!B?@2~}1-wgaB)=OV>AO>~m-;%r6$w#Y8w_<;(`-MQ-feWF3u0KnG+ z7RY5=i@72D`>#vaofG_tUNOvXF<#-yxyo1edlMoO1b^=5gn=1!_as1e<%H=u$wFic zM~mqA_B7f206k(VEy;Zx0Dx12{x19Gi@NcL)+bp{(hC&}Xlp45*v$SxXf%KqKVucmn?S zhO;%#7qp+8c*5~e?=poIi=|Jc&!z&*lv@>V(_pB%YeqMU&QsdvrM!6E`^NXVjZ^jc zy-`j~V)k!dc>Sq*N*$@c#b|_%dmW+~vdUpQ9m)gey2P6xMuT$RIJ7Ni_{7o;maCq% zsi>D}R$oQ#n}Z0uS|`b#CRVVz9MqC+8*P7E`8 zlk^9Fu*?yT+aaR3DjfdC-|r92QD~`mu`gCz5`mowU(3Zuw6#6-n;>?&eA{Hf;yG}7 zSvEJBNxfE`C+-;_iVlt>x58OyN(2^NGFGF>%N-$!e^2Ji5morl*4^Qj60S3+wT0&E zk>&ksuW44>F-ox0{k$?)#n~Aa{-WBeyf`h8$~f#;a2fbwQm4AybVHlE0SeDONKJVk z_s}5GQdtu}8`POcFHpa2TX&)#HW2yHXVR>c7Z<*;A1L!q=Y_;A0R+?!5Z<96? zuq(sFNRT71n0aPerd^1R>#;S4!vM%(m-D-e&RZtFM;@%78%*(-rK;uGFF$#l0ovS% zE}o*`t7FECkfgR~n|Kf_-6o_Zp`G^msmu2#b^UMwx+_DCGw_k?##Fk2q;WcA37NB4 zkZP`GSeep~)gRZJMoTi|>tDKC(C6K5GNNQ9zj&bEW30TV2Ep}Y zV)I{()SeY)T?Zt~s*u^Pd6`dZ(MGB_`{V?Pr5a@^SvB^J2b z%uDo=!_FP(R8{1)&krSy1i^QtETl0`vQC7_2YThvBed*xQUl?b6cb=?Rc?Cr<}r$( zko2qxet9 zw#Q5l0`?&}WR8uKse<+XBHex>JTHbGhfaj$kuoFj{bLx{;`ws%I!I%EiU@J(D+ous3SD zP~u^|i`OINqk39_`kvAZn~JSv0JMiz^7bqULx4=V37i#fvVHyCO`&+ zee5ojBli8z__K?F+WTAh%mkK}9YE@b-iPRhWEfPy;QBEROzZ(X+*j#yiA)3Zw4eve zd345>ZdnK6p7(LKl}%)8y)lx{lla;D`MXy~2lvL-@3IblUVY?R3^Q0Za@Y73q;;ul zEGO-uoa^Znj|(sWMwCILBYN`%HLiP!;wY-z&Q%jq59mWvfLRw7!Bkx9g6(%*d1Q6~ z;PPeTFJ$AD7}3FT&H(WXYx(jgn~4{MH*8>2de^pMH*crpZlQdO&B1`!BX_)OA4(vt znj4TIy(j+PBpR-pNH~zxQDtE`=;D4&|NAgKF-eVQgxU~r&5!k&v^K3C9-DVViCCBR z(KNQLu@n#;x{!a`AOGawgJvqbOTFw|q~eI8Y~AQ-ul<${o|Z%I>yll~kcwW{2}r5`5hm?XXtP9bCiSHr>#4W#cEhn%5E=W<-u78rS_?=v6Sw0tqH{SFwo_MNzqLVEN#&P*eU!j@-|JZK92FH@ zWU1b*Hj@Z(^A zEXt!5&H)|^>0fgUU*Kg_=L7pTqCD?2@sC25x`N0e3Wg;<=HCo_G-mbN4Y=CH4caw* zv<946gg1HXbY7m;;^5S4Jz$9!f4hX34eSC(FUEVeGUV}8s2Ae3MSYF&5YKtych*Yr zX(zEdM9>>y*}Z@}l8>uP*7$2^?jPxm*mtpEdJ9zuo1Z;QUWXSly((#?K5b2nv>~gn z>Sx<>I^lYYQ%zh)c*FxD5-aCelg)jj|a6-N%AB71dn+StKGG4FU{1k zJOW#+zO&)PkZaJAw6HzeKw)?^KrYCd<0wG+Ex|f-ehwKVIJ7&Q zHg%yvZO6g&AYoxPP1A;2-BU+rEpNSD+l*p!c95V%=p(&`XRp`vF^s--?5i;1O&qJL z%zJvc9FNH(pSJ!;Ob|VXyRSrdF8g%Cl|Q4@!PnZDbiA-AAki5@%?n^JvCRWM_$X`5;|#WiEm$NDWeJYnRb^leqLRF!u*Oz$Bj@4P5Iz#Sp4 z%yYR=^yry~!OnDidpImi@=C3wBNPay)#l`6R3XyO{)nt zDMG*4!1EgZIenXxW=i|rqN$cbz`1L>=^U=t^v6hI#X=NY_WwrY3T+5?labc za-tDvU2A{+MxP9`gzImuj=*-VUo_}C0n0Di58I-wKsiv_brR1nk5+5F8Njo6XGt)B zGBv{;Af#Qxw4keaoh?A7#}0P}h3PeUp?%L-r&C>~Ay%S_Q_QjK03pSkHMKj5*9JOx z7;;~Th41^sVkC^xT|bB6?B$8V_LERw3SaHKnbA2xj5a@~rmO|sHYxCiC7qmKJ^gyx7V~PLb#Blz|+?iWP?^8Bx!@lNC1YBD3IR$4gk3J96%eADLsfUvJ^trbAz9j~$Hl(qI zbbQz_k=P)kG)kx5@d7!mZa~d$s(Ja(FAp5!;0&9Exy9wcVr3VmgAY>#-J*Y>6?3=n zCpSS@Yiys6av3QgA`lUVWql9yVJ#FXUJBf60 zf<7zZz(@M*d;m(_z<0#l0}K$;i1L++j13g3I+_}p3oRVR{X;|8q*`CZC?K3Cue#1d zl%a@ww{yh&xYu1ZG$~A(C_*&EO>7D3RQN)%A}TFYa_}lASd>8K$eL>4=IWuwF0`xz z;g&zO6#C#E_V#iw}bstV4fnbYx`k^q(sF`$}+P2h_1`8vcfnt;6Fk#7L z-=&ypC1rht)A#o_n-iZZD7ahV0HtJeS9J5#2z->cQiM5+J9F}3OjTROs}Z;Q{RV0_ z-8sV0Z(^S9-i+KgTGYVTBhaLWWtOj_WFF}|x4@+Ia-E^Xt^_`82m43b)I@c6uq9vN z|Hx98@yqr@x&KiSILDkGUW$U-sUJEzP`n(nLFNv4~g#{H;xHVJTo3uAJC zRTBJA_v@&4^=%m-D)UapI(p&Y_riLkEY8S`i$2{cx=%rn#z>;6d8Z5pCLR#n+NXuQAqKWv}s{lg=<`mn!aHR z{2zSh1mTz^Z;ZI~z!=9FWDmeK=MD3?apx@r-CY;*r-Pc(>knRnN);wk1xVSyI9bRl zWrI5kANt)msANd5&$m-Ho`Yc_MwYx^kk$2LA7q&woYrMbv6o^iJbs^yE-p{tbtLSN zl*L>AFpk8jW>@~x%8$RyVrjdw*`zhhnVxarJz+^jiMvk64@)7BYeFV`U$|X0vNrMk zoU0qfO?gLi&V`gK*x(RwMNI;7^`QUwMd>jzAI6|883uJ=pKRW|9VCGW7D z4O#~_<|=!HODh%wnCUVJ#ykv3_&&Os@97ZTcNx2p>%K9$IXC#+xO89%nqs;+N>Bty zbU>fHvm~H#|9xQMtywT+g||bN0Mp3AWG+suemY+enWs+WHOeE*Y~HipEKUu61pE@; ze(4Ba*5QCrgqh96GeBLvEMBMGs{XNF7SU1`*oOj~43vzI1 zmQxEwHSn}CMeZFzp zR;>;1tqp5k_F{kDG(%XNemP#jnZLpr?mL!{OV}KSet9vuz_WzJV1Z1Q=rM`A7WhJL=_Yocr-#p@mHYVc(vVwA3CFr|?1JIe=|!$m%ADehJ2 z8;44Z9Lr5NRyOBOxt$wRk6`-JWLzbS#94XV@O;`gGevx{6>oE;)TQ$YxWYIhHYAf9Tcn0&&fzNI3uL@G{UXIXNB z_#MADZ8qYk;qDLKZ6W)3q%62T{3#$UZWX?Mc`$4M?sPJF)iDTAWn^v_REB*k*{`73 zGw5YFJ|gf{6D5ABC__=@FhaYF!E=8k0~CM~J}Tuij5L%>>$mM2_u7a5=q_R{D*z8c zJ2shv5v-0%d81!AKAo@EmH=H_0@=wDhF?xGl>qKdF5ol?n4GT)NCp(+qVIT+ec%15 zmq~)W*OW05KVsTmOs5ASCOo7~632gIzYn^X#a`~5xM%zxm_f2Uhf<@M++6C?^4k3yx0*kSyR!{>5!Dxl@Ug@y}3dh_+2zsa&&(@kv&4j?gZq60I!SRYI8 zj{466@iB1?PJdp@13aE)G=02s6~H|LeCq1RmeMlUfdHv6c7ZJf44eY`nIFz4j6QPQ zlrcdS0C30Se_Su|WHP5chlm6^p-HL7uklo6D&MZ_huVNv9I2mSc7W;fzC<{lBJ0t6 zmWW`5lkH7|hAPm{HY@pQe8X9IIert!cY$4hKuX!*QGvKV{jd~h$&=a^gZ1UUe(Y8; z5|v>+4qL%ex#1!&3481EGb!zUoa9 za~+)*YkfjwTcP|_5G+I<&^Z^|AO5Q5(AYxqxuuvRz}N=l%JT$8VC(qas>H`;9WKRT zIrXSjZyr|KAPz_iNu)g(qZZ}JN%YWLzkko)u(!foKsZX|yWd_)_u}#SX}lr}f5%ct z_f*}I1Rp!5tgKeGMCL8+wwXqbH7C4AqEwSj#L+@WzJkKw~wze?MnM0sK+v%HF^bg|A1TuxGuHB9>wIlzHqRpiE53X~45m|QmpF0Z1ff`z=#Aaz3%e}I=xn5>>E&hha6EOGG$z>YCv0&9B*stB?OOKxM! zt163MWvuQ`;!Ust#x5XFv82LIJU3EPK5uh4a=doCXsZi*KDy+31^j9%vhTGmSN83F zA?{vhWyg%U)T@))w{-eDG`(iB19v6_frLmu71si@3tcA&Z9(Kdy}MzeXXI`^Fm&gE zC-vh48sHajCcaEbH9VrH@H70zXY9fdhNlejRqx*Ey;_r@*bIEF7kKW6ujG`vX>ICz zZM1o9o6lc0c>hW=y0L}A6W##562?6Q>bkn18v~W{X1r&mn-{9c7FXEvetC=V5%a@R zQ|uBpaA}m{0`M7}KzY?)-6%>&nkm2Cba=wz^FmxW;axNqjF`di*72Z9`8VNo@UJrUMGdfcL!zT ztj8&MOu`V{H~lEX5xq3MsX!lU{3!rox|c9sL#}tU9AW;~gkIYIN_dw8Y|z{qL3=p~ z5T{+HNRK|vzefVIMXCnEdsrUmPI-dhq7{ikmJOh7FzD7o3G#qWN0rOyiYfBpVO*rH z+HE;rNfd$~ni!^0F$;*^CuQzv(IR^yu`*HL&_(}jwK#cWQPznnVCiwgxPJ+pK)ufq z58dMAp*)B`D}-JaNu8~z^vDo*V0;yo&8(8*tU=ED73XVt`(e;ONJz{^k`P*l-ZwAS z*m>ZS1RN`TWW%)A?zO;^E{yWk^ij#;rwXlNl$i&j)HaAV%8(UWLvD zIjDd9!hKv;-vsFEHCxH7vv!vn4o_`sT{AT3iuc+c?iZ~{Gq*9VY#NMMj&mRMF0&C8 z=ASL@FeKRlu@Oa6`NrHPU#xOEu+#Lnh7D$Hgk{lZV0ex18mP;BIN)IQa<}GL8)rIo z;&xsl-+p@mjWWTIBpzedfQ$54h<%G&`t0c#Iqnkx5Vlswy^LNJUf6wtZ~l%MljN(a@s_(~=t%+U#h&_)T%@=s*-8wEB*?%h*JBqmgviQDG6X-r3! zIK$Pr4$ha8W=GJi9g?KXqet^nT|>iWEKOW$Fnfr}JUQt+A>i|7@hlPNU&`K{>sPJx zYoYXLrp@2_R&q=qRp!||6bFHg7is{r)=c8HCR+N$5ayqR!LT_87Y@&U`=IbSOgQ{= zPAA(QaD6_2)%Uh7_4$&NF46$CGh>j^ziHrmr-|vbnRdO|KkRs#jFsI6YX2vhSQ3yQ zv`@mtz7^v&V&vDgr4f68>ky!oi+krnx$TlD_BfiE21(84DfrKO0VdqKqMeacD6e3m z{4Y!L6FNE)CSJg%ssVdMKP%yAKIt})gD_$iqa5GJ0A{c!oBuHJN)+7&+#HEGpi>It zw;vqRLzdq+``d3=NWGDJ6r^zW_A&-4Ba80Q$_o|1XxEN;v8pfS3umqKh;yp?p zLh4!;Tn0EK`bV<!)Ceb*}*P zAW_*(wIqTMpPeE9IAIG|wEhl24W63F=4AR|csaCIU>@pNI@J5iuy5j;$z$6#E$H*~A(XcO(-3|lMT+iZ z^kkaYJy2~?)z4q;y1lrYT|^(d7BIt!f9c|1vmbR4C(WMsDtaz;YigDS87xK{dfNfh z9GJaYyTTjT+xls@DBr{YfWre_!M}k0U`Mq&l?3#%k%18$)5?(3)@+(xaeQuKp55P_ z)ZOagPuKSZBS+utj$9fl5{7WWC4!t42Om5O_!s8I&~gP}T22kgQW9gw63>civec;Z zWnZXzvI4GlptTr(Q5_y#?^>Hoi8s@37Pb8bkQ_HieSoVk*n@d$xX3PSsrgof%MJbh z>Gcc~r%K}<2ugKofE1GRu!0!?k-MU-QMUChiPdn2<$rho_Kk}#!3k5dS=TGx{byFx zNOB-&1_DTq0>d0J{-B|ADE!v5nqBRh73H6ZvL?J-Z7#BFI^H~TCu;}F|70M$Q0W*X zpqY9EI4l;}?PxZxa1?6>o1QNr&`?F0@FP(}abMzaUNx=)GgQD(wguK)@kh`jvZwkW zj|1RW?eiH8p0R8Q{%=_2vVJ^ZiY}BJTE{n;FF5?YSn;UQ_B~`%%N8*9;;FfS>oa?u zVp#G$>T}>($2ogoGP&gy4vf zS5g0s$X|v!JkflG(bxGnq#YxQw>qmeNX-|@|3Hn%x~!X<+|p@{97q3~$uohIn>~G> zj{wbV;T)AA{1bsb0EhtR^FltokY(QBXsxP04bS@B+5gU!-H~qUTRG$0M9Kcz+q`&a z|65jK1UUGC5s`j4#q?3(;d*`zpgo*p_T|MF11`2_z=VAhJa>zWtVI3w)W?t!(jm)X z=YfXA`|v1|67Ts@dhIPSmaDO?!ks~>9sNp|zi}Cd^W+Ol-Y+?AswxWy>`u2e5mLt$ zxEW{B=}a4t0e@e^71x}A6ltxKk`PB8eJ-0=O1U6$H5EU`l!^@t56Ra5?QNG78_1Jm zHypG67*+ZtVF09*+<23A0EGg1;V>m#eJe>5N*>YqzTIDKCSxs$DRdR}P;u{*rC&f} zzl*Bj?(ubRUg`n5OvCR0$y%Il30w%I^&mVPQ%#L{);`#u=VVro4MaX_6CDge8ejub zfWb>M0u@r-OMTJ-7d?X;WGD+=`t%fk=FA5TQ|WjIuXV6GB5vr|9Nps!S~~A8(l&)_ z3v$l+ZY#qCus-Jw`mPT#G$YT?D6S5$UlIb3g8Snokx#O*yX`7>`LfKc$ zXm{kW`$90XKTYWnx#GPiJ2e#JOIhe}<6pRv_ApBg`)N4{ox)P}^tsic@+=BC+&C+> zMXxd&C%gG99ff>Uj0+q{9%LQk$_TO=StvwNrV6Cm{n>QY=Ux~(U-tDKOZD4cq(CoA z=FW$(2)v6{_}b?uYKQ+Y>b0;PZ|Mqy9jjt9EtqJq*A`xz zN39Fnc~-2XpbojN6*3elF_HwpeavY#m{8v(*X1HjBk-n=Q$P8Nbi!oZtTPsK2hqbzSf4{dvD% zujgyOK?7hDj6HYR;f7V&J)Se|g70xfG(g0`AJ~Q=IJW(1>oAXaH(h<1+=(j9k=ec$K>e%f00J|_$z8YHWs9?o|5uB|& zfL$~TI}I6!7jN|9=>+kd^^1+9YW-9hc(!8mg6h@@~=KS}9%`Hv{Eo4ny# z{;C2wHhSM7S-zxxGldk?mJ?oZ-=B7tS{Gu6@ZXsK)O`s%$5FMdKZ^5MezJ^?Syy%8LlQehn?<`~xhx=;$Sxe7HkbqyX4NQ zJ$Fg_*OM1~XF>#XOzE#LQO3^>)rsl$Ux}4Qa*7zJt1%>3Hfe zWK0~tvlBk@i8?=BJz>4$>*{rh6Aa+fkYDj>MPD~#`#OuJ*IKYy@ndx;vbw?fPZe5z zYD|g3oQ`n1jl10xKj63&G@@>GmCV7?YbPS}5(`dqb!K3VGKKtB189d##gVsu;u||h z;3y3;lKU?7#wWhIR=w!ciiRCn*x2kDk5+XuA-U%w(-Ep_0x$qJRUXXC5nAc~Sd+Pt z#0krpm4^Tn(4gx!72kjO$oMK0@Pz-#!OG}yQ(g_ImXrer#SJjbkG|WRCH2P**Q;;$ z5eRQXGnufnpMb=`MAckT7^37Q35f3B-YzS%;pX(?+Fnqqt%L(tv@kmtvV926pPN$* zxs2CMA1>D0=@Ls?NN7)bGKHn6`yt1y5YI&CtBmO+ufbjaz*-(|$>O;bN(dDYX|VE9 z^J-f9FNSWcK?OFfI=xzcasSz0)ITIFAN71uWmICnzuMlE6#OAtHfRf}+3S9zmb$ki zgoQcXnD?_1x=14qVyKO>!cA&Z){ng6lRMog@2QH6hZO#dNBkfM!wFs(Y`J=|aI6`Y zC?5>X)Srq7tj#a^0!Lxi+)GzDUiC%`w)@Y_zbUPAp6{h~ z(p4X(l;r^mw|@eXg6y7dJUoN`wAhxqb|$RH|=0O z%(b6*>&&)7DSis7bi_>K*LrLPR&f zvB*I8QC>)xL8|w)|GyGofMaZ$-g`@1I)2m^!;^qRf~b^Mpm3jO*0r0IVJtQ_#Oo4A z9Q?;BE)n4Ice-<={4uXJW{Cw44+tOW@AOmSryRDQ8FTaJY^#G<*7%K#L=5(5$Wxm0 zVX)TJzI!Q-s>SXU=eLMt*wn$GjL~uBrl3T{iEe*Z@8sWA#)8REGDE+?YLap01mX*! z{6e=TM_7X64P)wK593MAop52|8_rnrI=}i+GHT?r-xjVoIW~C7!{RFy=v9*pV9ago zJnK@Q0t`DYtCffUJ>(}|J->Kv2QY!X*rsg-`)LN!uow!KzIj<#iE(3RAGAJ1=aiZI zjMWqdSlZ5mrVQ;p>TUibWLt2KUv-Fiu7i(k&C&#Zy}q4Q2~%Cfc56n~cf6#16!7`B zS&dVIz9^A5CZvS=u5`-A3ikYgSu~Er4IogbY8_$~og)Mqom5Rc=`n*GO@ixr)i%Hr zN8Fmg+C!%H!B|k0@KV>VOF=F~OcI|q1_PpoNZ{3PzB4s!LaeV?tR@#uEqGe8G<8+t z=lP|Yx__4&_O5YX%^1IZ9RvPwhYEdv!6CCdP>6U68~I4cPXFly!vWe6kA|1Ly)@!Q zZgsZ!tv!jo&q-)u(A_9LH*Ip|WXO?;u%G#G1=sq$_f)?LQ9(sv;i}WIXl~Z-tL>|; z6p!MC@U_ZnMW-CKm+Qw1NKz$C@urzp+v)Ix?+NOtylY3BNqL-?)|DTWeb!4JAg2DF z4!lv7x)U9!$FhCnfE@Cury++}jh1#A;L}ICgk;aB@O170B-L(UIXPF|TpG`(tWq6x zg4%pLU$h;q-Fj!#V$} zcB=1kd)}QXH0&-4c!DF%RT{hR>#Og)F;UD7wJb}i>v-fjx%Zx`QRmfrZrY(r{W8nsIPbCIlVK2RQ_tcs?_W>>{V1@Ts=AG;a*G5zDtt=FM=xIp66qZQDWrJrsFkv zpPpUSpQok}2?nfvT;j={t4Npw7NswYH$HCA*aqljz>JmAi^rIx3w~U3@Q2uu8zeKe z;KTQ^OWTU$4`9by!!V&}pJmjps4&Qc2LwrlyEZN+5d!I%17}-iwhf>>K|Ibom;0Bt7Xit!++hot-Y3hmkxboV8q;k_1~_} zJ+D|k-(i5m@4<37@(>mnUp1VfE8)3!IDY``7xgK&$Ohvn6P3J=F8yp6}T` z!vxmkkkym}{dyx&8fJXYit<{|!FJ?KGuk4J6k})@o`2q$@@THu`ovTIFhN}H$=Wxp z_tDR#?@}sRAu~%^An9J?qrExf;B9&s{|5(bCWi60dSQ^E{Bas51HDlR?LwHTH%M#X zdxwg141~6gvog5Ff~=1CZ!NLks6GG7wD;}BL)Vc@yqJtRK&kMWm%Sgs#W?T`xy{mR zL+3wNxB;ybT_lN9-sE^xfgB9x{&mb za|86;?^}ai{CsmRl2L(r2T5(a9?3GoYZH;(J(Gra4$1feuU>xYle{iuB^HirKnR8qE^#($=cus?Eyv{n6N(hl_s&jL(}aOM`GBA$E2#AITR^yp zpzj)D1uL_^ZF*QgE}rw$exAQ#-mX6MO@L zi`iqclobCuzsm6-o8-nKUnT;dO^Rd~5sasXPQWJez5hzRuMz~GK@2G-B9fTELDQai zl)0gRngT7h4tN?oU2u5|{2?0sH)P2+e~&f3}T5XDfNCh)mgP{z0V zp-I!~BD5xmul3`FTSwKGf`G-#g2o*N)zsIYO}OCMBj((#0Jms6zzMBjlj#Qyi2Lt7 zHCQih+*J`(`ee&Bz?4<{vje8r+$l%b?ijRQUeJXAO6ei9iAQvX;5rAgye(%Wk1R%p~TnMt5wjqrOFL-r%kpy%EY30-$5pWZd>!97d7p^;tkggN~C;O)O;lO<(` zw72Oi)$A8l>s$~`O5gU}aa%pmV8*Xi44&g6F@cEfM9Zdwt&dZf@ya9u(*%y;he5!q z9;P7|vjTlb#PugRS1fyuI9-B@JDQ5s{?@-1sBgO8vG0l@>WJ`LHw)1oKjK(oDaxoU z0r(PrB;X^=sPF{5G}QT8t$-LtV#yMNQbkQpTQf zS`qh{*!%{9_3}6_u$B9vnjN6_UB83lx*rz6z+CG5X63+B3@|y10P3CdpDu~z1DVk3 zMdjK7z^9zuYZibHCMIa3(qV#7*lbY2;E8d0>11h!KC?4w56gQM3a?Ak@5bA2+Qf2< z5n~;)Vq{19FCA#FY`Q0NK1cpC+n_JC+Lz8a3VV*yJyjz-)l{aB^_`W?HcR5=yPYygpJ>|son##sS}*;z z-YYsp@F-i*vqjPRr4JFABBz4+ij0aVPYS%h*u3ivW~?ai4?y;KIgGHQ{HuRWGC&TU ziUp77D(!*n;yNM~o9T5v#yo+XF$H}Y;}S-VU9C4QLtO3R-JI$j??r^jf-D*{c?CT*{tv7LI0G1ov&`ec!Y!=fI*VC8jL#5%BWNO|K^*78GZ^0 z&`_`oQ@;0MuHc9PXNhJ+)ma@E%tDj})X-XDjt$!F+uJ1qglciBcEdo_-lsjqWn23l z%Tg>V=k3WEFHc1Bt-!n#sk>wx3cvbf&f_!tYNn0ni+C@7vXj9R2f4c2ohfmZH=M=a zFQ843#Jzfl9DTv^;v`U4^HEpT(aiCMC@w57thfKh^lHnczZAao`pJytFqJ|HP>e?% zpx_@Gv(DtSSNCjcXwx4oT~=?u9ob0K*o@3L9iU z(fVAZ?!o;d3{TZFOB@`Dmgg68qv>T$o(J-+suL(L8T4n5t=6*`j%UFt)b<{V^`;pW zi{c=4f2fI&JKm=|-GX5>aW)JlOEpu7jHOd)(3 zv$0P1hC(8+Es*vN^i4T<8Vy5DVcRCD@k!R^U;WYGs_a*4KUn8M-}|oZ&z$M}S0R*+ z<;^E%NUm?;S4Eq=(6w<7cf|Y8GGT#<{f^ZYtWI(}$@RLyq20qbgjru~ z_-6$@23yRc`SidyI*;y(o&2BEaG={w>*s+bsTKv<6%q%pICE2aJe>D2-D4NIbs<{j zhv{R*(l7q)R{O5KIWMINJ^c3oF1~L45qyNOCUd1cuvOEidToe?;f2fT9&V zgW;x|zkvVzT}?Fy{?o!^U=dZRS>XA8ZIm?Pp!30*=caY7h?|n8Q~NPcYpqexZAfByW7gg z7B`slJNpOBPOH+S-E^9Bn5d&D2N^1;*Z)ABMIZNo*V*qt*3oyZ?JND$b}q>J#||TQ zf;3$71`2Bs1}LkZ2CD{<5__4SI@wtjDoU(?%i6be!Sn=Q@Na`5GSR|tk-21!;8F#&ipUucy$vZR=yD0zISHR zzA)5ovmeZ%bskzjdAeb7a1IG!3|K53MG9MdKzZwAa z-i}@ddgGvLZZgH0#v-t;w6vkeAj*<;IJI zeRT0V<7xQySDftdG(Uee;q}bFS_PKg*P8y${cEkrg{)bq!`Sq@n8Pt+t%;@ey5>+^ zRH=CQ*^G(a+niaywVxydrVchiFk((-!67m`KheuqmV1G%DY8D89pM+@uZG!AxX{D) zdsIAMN1UE|cueu@p@gnjd9gJ(!<6YOA{@oJNv<{e1FGEVm)3Hll2!+EC0v9MF{7bY zdOVC>%?d-LAk7&jW>^pw8Z$RlE295CTzmO9#wowyBd`-}S2|_YbOiJETP)+0`8d3# z$-zUgXSghW08F~($%4Lwcs?fz&xM8_$pIx0{NhXxKTo&aR0X^=8^DqIQ+PH+N~{Q= zbYwVN$gT9=j?E^O$SAAx75ujVpB^xrXOQVI?Nvm~hrmCVlicc33Uw}-s4%h%vY<+o z=thzC`Gk#+TW9wV`GdW`0$mQAw2F(NERjb?2K&Spzp45&GCh^;()7eNM}9etq=}ZG zrg(i(F|qDSK2;ct(rbIepsm;>pW%C`tbKH?!e@;XrMGzueKmb8%7UyAv_om`OC-b9 zngm;7>T6}S85+rhZ~Gz^Rl9&4z!Mn9`YtSI?aUuurs|Zi z5Vb&+-mvAZ6H4zZv11AQiq^aAqV@K3Oz%+_Xgbr^c5_rOCQvU2h##L(jFy)#wCzji zM0=NdkME@fcOVLBc&l5(F@|g%Qu|QiFbVz|ao#KnOoa5dw^vg$QV*!a>L}fn!M-`k znjr5Ub&61j3I^_mSL}1!3YJVr__NhQd7!ag9yJs4-Smy8#>$974OXyhhTfx~Wbpz>n=P1-2f8S6_t z&d&pXidt{8**AI9yI`#naL*=i%u4PDTtwoQ0&fBkNojB|v6lkoXG24{2%us7W1`!8 z)TFvRejBABo$%T}XR6vo?lku2guOyUMR3dwM7U@Afh7jlhyS`Vz_YXH&qq^MeSsa# z8=yCoO8pgg>Btm$A@Vw1YI(~=X@_g3o(Jr++H(vV6DPtb3j!gV0=le_Omc!&+=U{m3e1B`$7Nf zEs*O+CKYa46L7MQwh~g!4WvZSZ|kcp?`hIP&HJrbpNkhNq$G92!c#^O;q8Ka_$cr% zv4Q^Z?KwuV{ty*(9jQAE;|e)_)h}FVE%=26yh-vZK9l{(N}np)21B< z>T35YtYS#>lhyL`p=uxYXFhD(rx1dJ9h4b;t98(KEt;E@{e{ZIe#IsO^Va4+&TAFU z$$Hu=LW;ElXC_7-j=A2IF3{Oy)^^xp&TN(?!5Kh?%cKlgg#9(Yzip7_bdhMts`Olc zn8ibw&0qkGc8iWv+xkq|kxu)V7Tt6S$oXlc^myw9*w0(T7x@k3(g$K2him$GZCUk= z?W-lIcW$EtD%{jCVM`Y}03w|3f{ar|U|b4K2M7vwk9@-DT0&R-dO4w-ulE;N(^*}e z8BSA5inMK%u-2t9ATg2NCvF8y5Kt`gfBzIa6|4Jy2LprHVXe*XaIvp6!dc0#ROA}3QLpGA<4)4_G zup-cXNwKzOFwSp#1{H1-bgTnc)44svE98KYc{@oFsN_2W*==yF`2+v8srRIznKx0} zK8clq0ctW8x*wFgQ-UI8^$q<|TfM~-x3D{P^A$r;Y<2hVE@EIGkZ$z;YWZsVshv5} z{s}-OIbUBf|E9-9(}sZ^Xp3+-dl>g3IEIGL7g(Kg`uUpy5Txj#i=75r5RU+g<9gNmzj`JCK;!19lCm(HAF^oi5=4 zAPx^(p@=RfH%JKsM9uYZqMw;xVZp`8Zo`C)Z3!C}%P#<30LSsZLS%HxS2w*v<&8n& z(k?Wn^y80J;T)IJt!{c87G%;;zG8?TRJTXLS5;R^g6$WA-XOT7H02{~5)ZI9w0gpC z;&po+(DuGKzD)akZpofrW-;!!?K3zhckxVAf_^;9d4Cq7uReecK!5fnKg;2`0UA4J zW~xsAl><)gx%UPJVH0Z6+c+7LbDa0kl?}t86A6~h*ChD{QfixJbr>S8dj94zpQ3dj zVnZZv!Q{mkB? z=}qk4!*Vc|kCXl68Ea5uk~0;je17TSPs6h|aDr7{&&bQI#CH-)=@mR_gHZI0!mpO3 z@UI$*Q;U%Dfj6&b*kfVt_wTDk?Mce;J9Mn713S@2t%4F4(lGVqTh-%!oCPCcVQj}djjvN6MDRg5M3=WnSxE$J`Fb@% zMjCsR5I=;P8U*}>MzZ$&0e;^ai_m4-F6r?$xMs80jCA!?yw$8_RnkgDjhvWT(ap3aJiL0P<&s6Br;ZyKiS&sb$ z#0tTt1h*MBLhPn^UrU**|Fa7kD`-~ANHPA{RsQiqKth?NEo@D}+8i73v)_n+yHKKD zz9jB@?NvmAtJqhwez>9tBJ@j%BD_xIjakL3V)^|D#r9b%>;cx!o=Lo5WO zEPZ^yUxU!~a&Y}zkihs^UI1f`T?Eq?Mq_phL`q8Wwmx6MutR@XB_fkMFA!;*2wbwE zYdZ9a>t~c(3U^pgk967+F;>nz(~26IgaNKI7z)-ZG)$v^`-oqHy|mxpk8hN6&`Pr^TZIiPzxO!LHdadbd=y~E|RY0ZiqQYk}qXVKD@?ARk(~}R7+4FA{gHith3zxvdzvfZBxc&9x^wX4C%X+EGCv(u>m57`< zy06*6KZdZyC1}IDU=?CZSD+wt|1GlAGwG`DPA4{3(%S~L;yycb?DbP=!vw|XfNl7f z>|K}X5U0ApjBFsysn}z=y4@PEW>Ee*T+fFl)3KpHZ&R%b*>`5zMTwq<$V$}CIf-C0 zG1h?MG@ofj_TR6$6twd-OG>*W>Jx4<$AWaV-HeJbL>)YKHd*T=`z$(8bW9lH{jM%B z_i{%8WP%RZ4FR}qpZ(7N_uOeM1x~+fd-DJ78{3&mjp1!+ozqm>Um`Rpf?vTMWQ3`P z)9iLyjrUnY^T?;^&r!gYFi1}wJLl&+;a^l;q44tUF($)vA8hj5kQ4hQo7Q8B!bB>Z zi0a;)48-@*`&cK;A2-hGOZS(|02MSNte?}FLhK2GLAMVjwo8^@NF;POhgvV+EH(>q zdIRVpM!&R)0t*M^(M9N{ihPz|$7}%DCKS8`-C6{(2HO@9OLCj>Y+_n+fxeE?nzGR* z>@f7(iC3z%=?by`jcGY6v<>Y3o79EP4%;RPykXj(`3|xj=;b}#IeZh9-u}a{Ois7> zaeL(aN&I>j)l>a>Kf2+{8UuX+5|S5H5^HUzoy>pIG(gH(GjL zL%!q44D(28csQo+emkw<(#Oi4p(*nCgze7x=5cKd{vBXq6l3Fcn)<8<_z!5=wWK{}3u9YriM5opRV-5@=eM!Jm^qg5(uMnpDf3L0CmytOmlV**;R!!1Yw?{iO_HKu3Zj|#I z{R%yOi9AZ93&K&=$Z1NK3dd?|g9rOdI&-4r!v#N@sYT2G!I7X_t~to(u$gb6 zg>U|BtLaYD;IS^jGGg(Muu)Ev zPWMQRE&X9PkmkM%f!jXAOVtbj>&b6hKWq8RLfy7+AJw8)Gj`$NqjZJ-{gT&Oz^SSA zHn?-R=7#{p8~Q<$d(HX~SRIMWX@9dxICvHQcuQwsj<1Pcb%iwhyjNTo01Nh38dK`y zUYwhH2pH%oPl7-Dne{?!<}u?nFt0h=9Y-$)O^ha$m+KC;+`;kaz@0MXZ^G&9bD{*l ziyb!G7WB}z!&r%=o%!(&=0anTdy+=)+kK4rWnNZIFegG?RJ^xALN~5@oQ_ak++sMa(1Gju+d=U zK9I9$7gzdCs9BWv&H0gUqP>zk;B4;FgkDsRq$+FTTx(bVUG>5KPrpK|m+x8o?9YbY zkgh?z)j29RQvgJIXj!}t%J@rxE{(qR(2cSk%NpT`<7Z#?q=o3oLiYBps*^qd?(X<%NH)t(*r@aL z(#;|NLWYJ90RsqrEI&;L#C1W18R#p?_wMurp4I_l8bLE8>92*N<+scvg^ew7^?o z-1$hhTvgy14EFM^|9HOZ&UlfmxwyE>tCd{3J5u-LcKB{+;EkQPdW4l}2R&zGHlB#> zp9UQCn$ASlH12gLxCreGsvPsy^B43SxE1`#0yRVbjli|U#;X#gd#>L(~Av%Vh5~ z5Q|0NTDoSTIJyR8p8>!=!=oA-Yo>VAB7%$JfB%k8^$}Oxu6d@CZ)USe|1?jUAIWzJsX)GJnorb{?oL6FuD7IFf%3 zNXDvM?qwbywd}qD*K(xCMDZoWBwtL|?AW!<>B4QD5yCNj5{4(L%TKJ}Sm~KN$O>Lg zI!!OwF704df6UG*WoyM1YWf-ql%{tcX6`nk#|^ zxm9P8?I}1wh&1U6mIh*HX=Z)_^RfULk3T$JwmM6Zk)pa-0dIC)6Gmwnq$7Rp)6B=M z>M(=G2S(=!sjygOf@d_3*TCMU1aSfNe&s0#%0>@Ryr$n7t!sr6 zoGB{%#t1>hD#x&hC^ETWLUh1(o3O7fe%iPfVp5|VL?#LU^rCwS(u4r^sbr?wO)?6o zjH!#h>RivgNu$jMB!U`>tb%U?sN6J#Wftb*J?r)5!PAH$8)L?qpS+#lQW0b{!1?V< zHv(5x#<2WWHz^IpW)61al6a5E!dvEx#oxBgiIvYN!d(`y_7E`lp!s z;zPyx?)&y4E#TxnFpD?%MU0M0EC@=HlML4NO*Q=5q{!w)m#}iPnT_k+31t!ihLkeV z4(M0zKNoZ{^m|oG7EKiaA(4!@4UIH2!Gh`UFzfH*T^UHG<5|rrc~F^L7#LhaHjt7x zcsrI-b<@qdY&JF_0qARL+81EnkwOyM56Fzw1o*?bl$~I;sxwE8Q@E)Eza3sLUzn9l zOBZ*HYeO9`v$VNTPWDq~xb;#qJHHoPhunw|lPSPSKF<6rxrY$ibvUbJX2k^~eH7To4CJY)Ff#6jz z5=$FTkHCRS`66N@Z&6d9u6d1Fa(QE?)}17rpOP{cQ`M8}jxBBp*SsA8pd8-Dur$P_ z^xig+ZCKi5n*brcy!dvVOIlJBoxqs|e_c$U8_9a^r2F6zcGwJ4kQ3uq=*O5DJbh!Z&Mb3mvS>WM63HywgH=+|F{N>4yl$2_!exFkUp3^*Gh3KpO=P(& z_*`e3asMctw}%5xlxQk(TojRR!%2()tEVX{5mV5gKcU6jQYJW9<@8#^BJq^#nP!<(+R}*5`N6`}eW9;LiZp(EYhGEf^`U< zh246TdvDjnL^o{x-WMT3H_TVZ)~~m7)O8IHvPh;}Z&ze1tM?Gxm-V$=k=XH4Y)ts2 zw`u$Wy4QM+QC7DKvj0@k!%F$nIV08?=kNUL%Q&`!s5WbdxJ0iem@s!>e3fWO&ZrKK z=|g9$PLJ7;S`a(^J*w%q_b?1xg=3GyCh8ln>v9!yf3@$uB(Qzey3jm|0b@R1!i>5Z zGO@^IYIx576-V(`{yIp23i?_iwIi`uue;O*EiTxM43E1*?hvgHt z@2 ztwXhOTXDyG6js5-r0wSqTslgK_a}nQ9QPGA$n%5guE}J^xr_DpPQ2hS%=?yoHw|?R zpG#v$KfgAxr~c5xIp8-NF#H1h*f^%Dqy#|OXQmwRt$C9I~pU`7*Xi`6Cpom-npDVnyI z5sf5fe-d7chZ;Z4c*&exlLVatMf)xHP4?ee^h5N1ZwuVaL4=EPkKNCFp5!WUy51RL zMkymF?(8#GbQ9U5H=BdFa8&*P#okn0!!(QyB|%CuxY4(c)y+J9YiH38A5G{FJJ6rJ zTRC!~BJ;4xw&55;9JzSWv>GZsKK?v^OcqpOKSy}limIi!NoUXa0f?oOkVTWh+)~AL ziVP+Ur|t})43&%1emSw=nLSTF#z(BJJi)$mrS6!bYf)2H2epk zfi#9qj6w>i*ylpWGHPRh{}fGBel6%!4oucwtaqy`{lKF{2$F_Nuex z;fXbzVh?f9KZERq@gsFV9=k!C8xNf%4Auq5`GYxo)oL@)edS+>YysP6O~p;(w1p`W z)BVr)!6U$-)4>@!swb>Pl{&Y4?Ui550{7s{gzL%cmegz8l2})x?-xf>SHMTznsE;& zikJnf3IwMGm*(xr+AvgHr1-fD4+xk;Lz?vkMK|w4F2h#LYK3(~wI40c3KiSLGK^Hv z;-5OMT)1^izY_$~^uTx*_Z>IpgpOd_+5`}${%6AF!ki9t2bm-vnY#~BfT)0@*8NZC zezjg3kDHG!c|E)U0~QiMS(tV)kWfO8jVvGce38#;EcxJvQ#aEltX8HQpQ^q2B2$5q ztCMBr^o3QP<-quDQwI92ugimXo54(N$>kvJ&hg&u%)i0ZcPi4oJLxLc(4S9`&e<+( z0g%X-P-S0EF`|j2tGmCb;SS`8?Y62N6^Vi`x4Nr{v|@ql)qzv0;8Eb(e5*WqyWCiX z&Q$LX#*V+v*B~RDx`4!RLC~;bSH(#bIa1ec{PV4ehB|tz@u{7yFwVQ^$>9)?tm>F| zrF~Lf`mkA1id?imSHJ$SbD^zUZ})VS+jzGhnJ*5h#~9i$0ib_^v|c@ljrKXbbDRWy z8WBiuZg{QKGY6BD6qn-T-)5kfd55*ztgm{KUpoR9_rC&9?tI7(K}?TcV{w&(jJkdy z)|f2Va2X6LPe`f0zzEwQiUZXB0+S@+RGdv&9?B*n|!^3 zEPT&x@6CPTms5yyUKV72zCw)9+?GG@W7^?lUv^hN#60)`vCXcfNUHH?V;>-D8yyi)KreEhD3f=oDap7T`BdAIJ|SG1Fj0{G!2 z-sZ>X5gr-p?gLEJ8c`}>D$hNfvo|Z0h7Urd6eIbgRW6r80tvP=Cn{NpZ^yDt)WM5u z^R*FUFzw4!23{ss$U5Hm3aD6iV_PF3*~Kgj!H03lu45i0y_p+ax_;o!W~GYRJ;Aby zVcMvpJQb|cOJs6D%PYu!Qv9;->Y3X0e;Moy>!JJ&EaUFA9~Q8|X5pGmQ;nhzZ11U| zp8OL|E-$B>J+>x=*Kawl8cYmKX`d{T75e)*Na-xTloN}ndPKit<+ zzxiX9h;0{43W4wF6xo*4cA!tyX>P(P$Y*^nC`VO{U(GYRW$n62Y@&06XzlbAa7k9_`?+1Q z@>cv~?c{@uBy)^%eO;TRg%3EB0jwxu&Ikxqa@?Y#|GGWNpNlbyo#b@-6Nvb}Y8oTe zs9Lbe-iKCL7Ge^nClOU!)?;9ms1}UHZY&D{GQSeAPDvS^K9r7NB~k!d=}&>YS=981 z^DMBBfFId4lcfz63f7w2i+VtZt!=MbQ@!mE#~;-hdlcf2x(+xb#@@72+m)XSTFzPg zG8LTU^(#zKM6nHF&F9)H+*0Z(bHgd7@Y9*O9Jfy=j=5Zjg_TSSG`#^e$2P(9RHUo4 zp~jR_qw7W68jp@7=XXI37-Drpghz+`He}pSAVJ<&$NkcIxNhqXX^`=0^HF-+$!Y=K zcRSW86ig^4iY}W`3S+3siXJ|ZEP~=fXbP|)g^51*TK(-}ny%8ALD%IkE|jGyv%g81 zxO-bfm$rZk`OZOm!}eOPS(rJp=EWas*86TkWswvlNnAu}Vi(W4@t?9jJ^GVqbp@k=H#9Z}`*d zmNl{-ZiBXnXRE4;$X4WlzV;hYes>6plJ@6*+G$wlV zpWEY=F?`S(3zwtC&%%C2#rL8@R5o82xl7io*XPp^jqqFe{C|GHEBMMsm1KkehkS59 zX`w7fp8x;TD7#uT9k*QE(bJL(^AbK{&ozYTN$~a+MIF&jh$auj)sXS$W-)}{?1xZA z6!IyPTf+nc^=-(4mH5)`u>aXTZz{BU z{xYWZY}i;OrQJWX8y3}_*V&IZkAKOIKxdJWGswxFQw0tD8hw7 z;$jMJhq;6=|Xlvy<#W1tkNA>j`3|%e~(nGY+upfbVLHoq+dS97!B=YRbK7{y- zb622C9Z8kQlvJi6F@oGRB(dsG?3))s&2w#@mO5>~EkxPvBHK6IMmJUtS2tga5sZ4_iblkz1C$*bn;rXKNtR5zGBqvE$0bmUH8s zE~B1o*HJpQebU$Gd-mh_%jd|iJ69M>M``%WOpgz~nh)6*y&`=hn1AlpKVp8@F>Ai_ zpmHp`i*e@Je(pr7e3VSJ?r7CqB^U6k9@bf?G9Zsj*r*yc^Tir`Pc+h-~RpSt5;^^7^ zJF)ik*-xz5BSq=cl0M!na~J>H`hT{eNLy*1Z3G{=0bOuGdkeCgM+bT`dT8?ODJ+zO zw<__wp#f0^2DfYKzP+&l)`w`(SdR+DF%?>5bZ`wWTiIu~ zbC@-~2bVH7bK&*t20sTpY0YBIKGyuEAp~y=S*q&X?;3 z>J)}p#2a?_3%$LArS9MApbW?$IJ4PRJ{GY!vT zc&K!ZB3*S##GJSt=TfAs|FB0}F<%_d(0;xTf+x^hKmHRRPr?qa^<)n8>#z&Rek1K8%e6SixquBEe)e&Yj@x~We%~fpSHAffZ6iFD& ziV_Asdp-LkGUB7tC5v8Gb!47p`0NL}qmX9&k}5ap6Vpm$utFgNm#%ES)Ss>rKtv|88_BORO39W#t$+RCL|SdWnY3PEvq&eAjHPIRqZyMQ zRPMPTZkdFiox?vPI%gG7fiB$ML-QNx_VcFYsX)D4NcjJ+y7s(gzZ&!@cm8k*xJfmQ zbvIhZ+%e~lKwc!+(&Be`fK%}mW0p5fMp~hR{?PDI0O?G0!a449LJDBZ#w#3Ma+Q|`qVLTx@>J9Fqp`X2nFw3|BR(eesp<5cRf|CC*g zB_Ey`eF8u&XIEFK)(bs$@=2+Q@-)hh?N~qd-21tw#i}YJpEadufQ`G}rC9Z4ie?p_ zHrABS&BkO$B(l-dPJHC6P7fdQ93uY~70Z)VPdY=2;2h*GHc4Ji+b^{E~$$44Ur4Q}uM!)+y4O(uFLSN`Ve#&(5sgw@@od2!HHBt$Bj_~elVkKKj`~r$2n!LibS6C ztJe%7(Y*O!oO+N?Hhu^4Pz6==qfPnJ+migC;77%rf^wnf&QByPqy!7S>s&V5C{V?< zk8ZVF+jkT(j%wHG4rHM53Jf$LR@A(%ed za2S6GuaoVy)puE7Ql%BvVX2FO()N~W^%bhYLFirBBad!b;xG((M&J<>GP|3rnaBB$ z*En)%JXnz`FpQMs`3KM`vS(@_*tXIQ?&CI`47Ef(YOEk2qI{M*wSP2f zwGnU-lut4(7Rv>EQOVMEr04i_1$7| z@SJO!`_4|t#*6(ZyQ6(O>l_QCO)nLOz8D!t9ta~b5=z}8RqVRzwC_Qq*)xU<66AFB zr6p(s+ahST9BLK%AS|~{jw&yvS5S<8PGIgqGu=sR(e>+Fte2(kDq(D~Om90&G~u*P z^0>iYG4*V|j41S2y>L%BKPNShK zFh5}3sVkkCmBAnIx`X)qFBE-mF$koa1cFeotLP31&Aeioh!@!-S+G+0cv_tX0$lg6tHT0vzw6)L95=4C&01ry!GtxF%9Hw!-KccP#RUh45N4 zxv3Q{k@R7+^cEHine(Lj#WChW{Sk=NN;{+izc{q^4-?Q{{C2%)`qTN3joptvabIns zbiy*$=Bp$Q6LhxPIYFQd+4oGJH=bk_8-Uua+=n!EWHGmh{kCi1s>1OLYZA?X4Po+r z;aigLn_V^71i^IQKo`_#dz#ae<-T?-K@r<1H$glWrRFzoD7Bz!pAlRT|p*!iGW}FXc(O711R$P?L(-{#Q@421yuHHsZbBW z7t!X$Ny=gkM$AKGK-rld9 z=kxivTYSC4>fd!Yr~Jc2`K69A65~HvqbV9y_<2~yw1erLcw^@UxJ2h|*bUep+nDLU z#ClBCptp=bIL|%)MA3(K;`)76DPb#SM{Wk{f^cy+J6Qg5n0n5AW-zrcw3M(Neds|A)r-4Zm+aoo5!_sIhowY|^4H~6GuN#3z#8JPC&x}b#h**A~uI@b-@*p2NGcQ~aX|IbZNv$Ld)8XP| zRLxrFM>Gu{RPC5=$u-+LH)Hpt$-^MJ7nU%NJ5XZtH;~l~S$@LkeXEi*gUq$n5;1weNOfC7bhBp6X-zX-j;9=(OXK=UpDEE`kcx>-s&x@k)Ir z=HQZST(j8*<4TZvXi%EA9<-raA{tTfh@I+I*B zViSZIX7IYf`FVqvBC$Cwjq5%U8-9nkz<(osgi5?It9%Zt%%Ro-iYWd`SRfsN`DF}_ z7mQh0_65u5*;!IFD*>3dc;!obdqRz!2t0cxvTtgmEz{Bbh^NF5b7D%;zDu$X^LtH= z{0W&?WG^t398c-07rA=b=oQIaZE{$Adly&5eAA5@HLPPj%3x`0>eBdJ8xym?UWwm+ zZCvQ*Tx5$6?c5nbZ<<1#R9HXhgv{umlW&Mt7FHHhgYMVT#$b|Vy5oux1P5jL${e3X zSXn^I$%-$Tj!lbWh@d6jxw&us)QHhc836~Ft5*i*LA?-DcB*?aTpR)CK9!ZlvIaqRyI>ncg;Pd5p3n=`)lUDl?7qOGmy90GgFkfU7raX&2%E4ZALd5rxAm8jw3zwqa>zL>HZPAbM}48@tTMiemH-@e0i z6wtE(E6U_)wWgCPT4YxOFdSj3&I*`xYrE_#GVaVEbxIeKwEwDPd^x}&EZ`54ZT46( zVH|%*K5!l!Y+tR!#tk%&`X zMmc(#qYBp31aOkGB0j~q5um+`2gcBg&p_^81ZfUr3HS$X;jeSgUHk9#^+S6zoeA1# zr6f7N5L6nC+n)SsoBk>jaav1{n?3C2p1f&`{D5~u+q%eb*U#iGi8uoOmvp%twK`#c zdFYl&;yaR<4PuIOci7G*bvR7`^_ZU|>x*>36a}4wP*3Al8|3dhcQ5LWTe`1=n>|># zH_)buw@h|9Fc9Z}rln5Ki~r{4tq2#Y#FN^`%Y#`~^c1U#t8P!5XO{R)2Sy!wsYus+ z+Irpw*oA~jG(*}=aRAvRWE7LW96L0}rxkt-nx(%)?VDzy?svToaO73$uS9I^{)A=z z&vC79taaScFH>dYI54@2Uj-Viz}20F&_3Ap2Wovy^z)L%`o8mks~rii->Wuh*I9Ta z0O*4$ruh?YpNiSfmMNHvt>^2L?}U4VPKsA=H*P`8>uRrrtWb(FcG{NP<4EKri+t^gZu%$LMP~0TTbHS=88gMH&|KNT$dE)+OkEQIIW}^Sgz!f0QG<_Lx6NML-j^FI~fm4Cb>0+lk2LqCAlDfe~8Mk9i?Qh)FxibznLIk! z8e+Fx(G?H@xz^wF*gZYy%jx~c=+Smp#oS9px>)Aur?vw<-0Vd~DAV>_@|lAwgOESM zs*i<}>WD%HcCt3AHQ7$D3q|erDy0YjUz#GKM#_O@aBk;ieStv*^YnizmD61CX%X#O z2?X|Vj+dUQj9dg8$LL!GS=$>H;I9n+bl5slT7)dfmSe)9X5GpUo>@Ycp^?lV$Nnav!B2bsJd? zLy)b~y36N56n3Gf;~cehKAGu3ma~plZ{;V1sHuKJ?K&?jbz&@T{C>i5O3uH?Rf^ry zc)(<~1(IGot5SNK@*ba6NV!EgJVuC|um@oe(=u3&%s-3oS0cmTP8kMZrC*8sooWDx z564u{K;?>im!!>ax|r*I=a{(6rn~aw%_uehn&{rGMkCY%njn{~ZX-y9X$QgxNHzIv z<(M{=%EQ>K7EkfxzfF0w?Kf@WTfyoBQdS?ZR-fy z35|ZuV$wlSCN)}n*$x#D^q>8K(7tliLDPp}w7Udsh{pslFJ8+?5G+1*KE^7oMM@lg z$$l^uRf(20v<84C2NiJii9OjxdkA$z-45l!o-*>!Ix%osT@EjeAS1#rRVRKc>e| z)?sB{)7dkY01ymAR(p~YL&cw5~{#DDUt(Xxs_WB$sjF+ zJ@+EI_fXWKr{80fS}flbe}6^FQel?`h=Nn7uhXyA8!@hVKHAwv-90QXA9v6^3KRh5 zWi12Zi3sVIP@@Tic-GxJ#=zH=;~iW0Za}>kj@MKL5q6ZzL z3cfA65=)eaeBd%jgBe)>hM3cv?tSfR9H@(s&(Dn4PgJg-*D;!+M2$XzQ&OS8;N$$) z;8ffXiRMt?^o>sWORy-+8bvk>;$mfmUp6jQfY~|7zwmIm-%+WkI$Edr$LqGO9{#Kx(XVm#0$=62cDvOdwos3;UHhf+Zw4Tbz z2!I=-90UWf*7{C?Ljja7y91J9t;N9D(Hx{eep#8dYQ{jn_JRbpQhSr9AeCFfHTVbk zfU7CL*dg%6n7><%r+xyjs2tgP(WWG!%q$eMf_RE_>fZuQZ*T|_P9e;NV)I>m~*{|g#>|G%V{Mzwq zj=G2*i-XV%ThXn2Fpvwgt0iDS1UBG}Gx&*${+(k{ebsZr9Qywg2WGya=GZ%Jb08JFNFl&6TwOV;N12R1B>Q2~(rb&%O-XD|liwSjd7PJ&1B zB_nJ26BkvbyD#{V)xjU%0zB=GU%>2+?)^dM(H^(qjB?o6Lk&43vq|k{(7`q3#cw?t z8;{sw`Yua!h=ta}>gG_ot;nZoPf#yNp z=J0}#N7CdSM?+Ey{imSGIxb4L02nzYq|V*2Seb2J8Cs!4IA(uOf>j~9OuR7u*`Y;K zdEaEKNB1^^nRkoB9WXZ80%QCZYeyhByR$rWyB2HtL`QZ8N2}jNox}QRRh40m&xJ;f ziFF?DV#W}{)XBIqJc4mX*HRR zwpsx{&SHdb?&ga%8Md%@{_gLPHQ2ZrinrP|Fku!tFj%E2XKdBdBdD zql|CC9tusp&I}o=)jO7+$zU|cl9${oQ3ek*S!I3|&g6wbg44O!K%D|jZ_`X+jvRAn z$YuAy)rSx)%hFc|L{;g^ODIBCzs%fyV0=2RtWL9wa1@`hKa7pcpbMxppImi#z+5Av zb*K-N;WObgJxe;c2eczk!e-v-)u$k?{kccxQ@-tM=QR>sM#Q*~f#Z`;^P?7h!MFYt zwEf2uH3UJw0?E9j9kuU!y%uo~xalj)=iAELMpao5usyTc=0Bq+*ZD?#I(h{XL|6UW ze|uV3F&K#!8VX7<6wxIo5_1jXdpuN43o*X3281cjL}wkHpi02scHM}_$rL$ zB4kY3PXMvaU3Fn$128>MnjcH}KJ#)dx+166VNZdYpgE#6dzKqf`X*7wtQ2|ryBF3K zkTI0%x+*?VS#_#k1fh1bDt*6FdT@T9*ZkcN{BI(t`(|Kp8BT7!M^YGK@N4Cn{6*wu zqgET)m8lz&5h8@^h}w&W&sK=TcL+P-o@40A8vV&85N7pO(Jz~)dZyeLkh1jNC&1j| zu@Sg!!|I(>C**qG3lebcLE0X7%=Z_Oo*yx1mFAyGnM^2`$GIe;0~<4oj8V-#sj$)k z6YzDwBqNr@x3v)kbB{c2(gdRE(~QOYKb0yRBaVNw5Cz*lYV+&v9e~-wJ_(Mnr|ZBG zW{Bv7N`WO{%h2lhxdZg+8=$eMwnJ?~ls%@xCWo2Yo6D<1T99i5Gw7I`ui2DiFgjHc%|LkPdKA|N&35FCy45Rk*lg8jzyv|J z6dO8*6keIs;2Q2^g0M*MSXhs!kRfF{i_8{VN;1@m*d%Rt^4h&pAQJvX_*(10wd4n_ zLohaQaCrh0^g0U@m5$wj$qg8N;PZ{P_$>i4{*C(zq{+B&9h^+=0MM*BG1_H zOp&yWro*aTr1sC+8_Pj3vSuF8g}9gN)A3|2!Bmp9db4SZ&8tcf*&z0l=@2zO)g43w zV>qbo>P2|p=Io$@+uj0LCZfJ^y;U>hnk_1s;G#F+bknaOf-oo6!(Gy)Z=*lsbC$g- z);f)sn5zA8vBpIOwsBq@@Fv>m1DYL>rzs_f;Bl=fAjaGy5xB`5u@T51_WJ*J09g-r zfXVO3WkBYeoKK|I9`JKqJC3r!04oDwDt%dDdfWTs4ZydXD!gMp^oi<1}R%||f ztxDk|Ului~Vg_9IoVYbFOxL_Lx&EeZay8~C>65`N!-rpaSKid*gnI!dVKfJw{>7bK zDyV5Q9-^?-XIStq42T`UAJ7=e*n}9NStJc-%J1H&1L0Lot1+`xU;Z}Aw@L~^+@8!u z@B11+8N6sN_fO{T*|I_?T}hVnd5&B#WLvSAs=R8qu|N7dx1N{*>gChG=QPc=bII|& z4Uk^iw?ud`7Ck;sBi*wXkc2gJ32Y>n8I#Tk3r9>Y#(6(1N8EY+i4p?I4BPKn(V*p( zDn{?Gv&~dg;pfc3*GaGlI_QKX$|#Xcc&BzzrLB+zv5jW@3u0k@Eo}k}_ zdn7NfU4F|MHgqthFZTQ9?yr%Y`4M{eC26>xPbHj@n$hH41vZLZpX0)7AY02)i>3;CjaA#XR>2)NxB6enbxM~u#7V|4dnUPvY$q900m zg0`c&4;{LW6P?)v=N|V1oq(5USU3KJ>749h^ofwMrclZ~=tqA15#w?XEswN#3$Gu$ zz;v1<)HGn(4I4BOo-Wy*5p!8>M2>$}85pn7iu!!sWZ5$o4N=sy#~u+$aDK&bQ&ISN zXWe(LH&*vmJS0I+g* zL@LqJZ+8hc%M1Y|Q=9Nnf3u$iG~o=bHJOkjPl55OFEP=lAa7@$T?zS&`3SxoJnA(3ELFy5%n%*6LP6a`BYOh+Y#0}4*r&X+w?Ujb<${cPXKY|g-p zH4G}PJ)3pwWta9EvJL)Vc}FLxZ)%c!n_q>GzUe2W{~h+;x%a9}9H4)Jifhvt3Qo#) zN}dEHLwR!3XBv7z2)Q|&y}Vzk)=f}CAme? zl%K7c8UT$)D*KXCdqHV3{?v3Bwy%v4HA`rHf--?kc7{sQmmnvy5$3WA2ULNrYqZt& zSlE0q6E+!Nk805h-+8I{8P0=C`G{>mClhkQ0$CR3ZYWz*x@M7pGA z*jsGhJ^5I=OniNfC6-jeW~J0-K*Ux$0*=+i_LSd!87Ku>ZPTauAu3Xicm$VJRB~j& zg4Ri<2t>pM${m+yg8=wo+|_a5v(d% zr@M-61HxHlCZrUr#`vdp*oPZ`!Ds~_8%%$@7y(=JGpjT{j|j23%i7uC(O;YKB0s81 z`*FY%fSRK!;lCe;UiT-y&hHH4USY$YP!anHN3B zAv&kNJl0C#`vk}W%8`=TAvy|5OtPf$<+pkUOuZgT@YF!oPv;%}U@)DesY+yRxnO0H z>i~dG&(zK1lm^gKalVa@(hyzS#DKq)Dc+;91U!CP#z%Udt{hb^D~zwxIJ7{bcMbIt z3(KH4JAL*x_y{jmJ*{iOQ4IizY914vuZpvm!onF|cpM9y_Qhh4cM@wM^K7z)WT**I zc_!G_@W z*WG(2PoZ2tp!699p_BYOr+`|R2ORyy%J&h@NF?u)eRBW{GGR0o!5g z+ovn)w`xJzTW$68G8`S-Kdto?ltr!Mr3Yh%1=MYIN4(Btm*U<5*J_`D#?wtV?9SK~ z;Jx{)N}SX;n+?Rq)D~cNwt&0GO%O|8*6^zM`QbbokbRaCxZ1OJa~dZ;5Vp;Bo^Otz z{E%u1OqQlrlyBpXA1Flj+ynuj-+K>jaQ)di0JKwYgYx&^dWWnl!&y71PldGf7B!kBM{_Rlr0mGg>pu}6R zEWQ75U-)e}{I=i9JH_g>lmoA9yA=40#-NU_l)p&$KSv99vWFa<_W#y?)B_-}XV4z5 z+q%+!LF4;Z+@gBk)OGr&W{`bV2g0_HY=ts&rw;KbF!m{}_q$UD3>+(mmPF(xSG=ak zrzqb#=wKXj(w*Zg=lc)++D)Qa_~Yew$!j$U1}U!7rSeH_H-Va zwIx-{jpL$W4Jk-*y@xV?=l<7O^;zB(7?^=cw9u6r9iLiP*T35fI%fYECpsE0hzxv| zwRIOUqRz7=TLxdt{{@o@_?%Z(=~|32_^pBA;I@CeIdwoRDcTGgpWoBK z(;F(EehHZ$!vplru7!Djz>0AwKc!;gjpN17(UCLM3n;ws5TtDf}FGdf1 z#=-$Z9Fk$V!^R-@fQ5uwm7{|^`E#~VR<;<_GfOyg+*{4bR~uPg=aX=F%D*o~pP3t4 zZ{2o~?1i$+KdtNN#2fU9QL%c0%UVB!F(oFkR>Jn(+U+W&CW1>gNo#ep!Bhoy*6rou*eVnVEijklI_(q2My{uTku#sqEh5k1*Na}Ll zB5^7av9ci&nUUN0r--mnY(oup}&QGrj+(>vR#6K8T&1<&e$%d0hbn|Wh!oEW`!<>?gDkaEf+gA+A zXQ&7=w=p%}@TXlBl*Y4t_;HjC>MI()yw&EZR=|v%7=b#cD`5!2y;?fqG@yq}Hug?J z2g(wB>C7cscjXHop$%{2MS3!?KXnO2Y-_Je1s=AyRhD#}6dy-Eavu+Or)CZwUDo@S zj}@gA{80E}WWhXdXDW6zSzBjLrSEsYt<$Y0G`=2nDVg(q`VWRa}**a^+xmW9=MW~#5S-N)V^K<<^;Cu7Y zsPSpiKTH)BxZ3Et5>b3a%6pt+AGnUle}N!S|@h_X!bAP z+^yb45I>}$=}pMSGXFis`JvYlMwK=*781$QBig1rd%2c7=J8N79!-(pj@JVEppZRxQ!jUrRtWPm6QOhl^-T| zieJR?s3YHt`=p8=vfHYgXJ$!%t03=>K26sGGmF!tm4KC7<7po-Ym00^x4+rF(1oQ3MrFT z6K2Xu(65!L?>t_ef6|OlYL3BY-a9 z*z+LO9hfUt(UWQTu1+l~{j%guM?a~vr+9WB>ZNa_9FkFI_G>yckkV_qBN+zrd%nAcgSD*seeDq(7FiK->gK2t*<^EWb@Z6C)e!W9I z$omQK)!sm^S-kixy3MUw*W7uz983Rp`(*k#`WW^)p7Kmq{3|rqvre=um!v;J1M)Vu`@fD{D&%Rc=lpYE6xRN8d&jkZ3@J;o{6p(zAie-+ zDr;N}^wCK%-esA5ks0{?=S53`nhsJ~{`Sa^7a2D7-W^$6Iv22R4YouxWkd{-}%ZS94Glr(<34Ib2#$ z&zJD(Q*<9k*&{;^!GiN4K>thD)51%|J+tCuxc~gkY+0eeVbAUR>@Yn<^R+Fy#~W(z z8RS0|5St%Aa*+4NX#Y{3()Qlvgh?rASA&j9lYjw9>pvj7B|xEW`>s}j0%EWAIjsjdn0^PNqJzE1Z%i6?F9If`=tJZe;ZNc^}5t&VZH zs?GBM0nEl*62Pfq=EO);vEH)-lQfKt@Rg?0Q8Mx^STqI-Lbg?!JFjt&Z3u(O{)>$M z19dj5Jl@9+q8Fc}lZl3Pi5HVTHwJ+$`3T%;4DlQ=0-56n`m9lX8UJLrRGmZhT77wc zBzi|pNHfl_0EDNv-syLhCFQ1|#@}*=5Vn9%WsCwG_BkLT)Wm0O8@T?UANXtfgCGS! zcjx|KBRIdm@qcxA+CbweaId2Pi|3NY*XMx8qhkI_b>{bccE4(#t8@@FH}6IVu3yK2 z_kCN{&CMG%c1N>*N=m(OKkN)ER&J?Jcyl6_VZwrXM@sXlm=|3-Gydu8U1KJwSJ3hu z^eN11#Gf{zp{2KQA45%3gs)IZlu}nkFo7jdXf}9|GP%LCgeMKDfBWW@8 zTVA4{zNQL z@M<=iyK!m!Vujl&VEWC}w#CJJtERYF>-1GP6YUiO@&(*mZHZvbOVhS)eoCk$wk<$1 zRZT@q*1<%CBm#{LL`Vwz%I-rzpx^7G=6J*cjkPYGg=k8LbR>6Kn1aq{4xX*FL?UrG zkE`fqZWH;ez!smhm#E53-zcHDVA@xprTO!4?i~9M_CMF82`yQ{st5gpHAwk7cDveO z6DT|)v(>U43!tqj+U>*Hg@xxR(L+pG+-O&DQM}N-uSxT%={1s}PWyP_o&@Y~IMpjN zfrTha^y`X-rq%3#)NUv|n>Yd&``iBE0|J+3s0%=(Gl6cWw(21gChropF*=2851o7z z%VvgU$&rXV+kVo*Jtvin!h>9PlH07>54wQ{hcADauF#rNJ~zb=Fz+rtnVv`6h)y_v z^Al9DE>x-zwMIc4m!X|Q%SO;NtTh1!*!#e4j19RERC!Ky67apaNF(&uRD}6ut2zi+ zYQ*$ZB8LP>KLNtUn*nr65YD<6`8p0hPatRP_a~z3;~xQ1($r-Z)9hDg#1<;}k1;nM z36Xc8*Iq{VVHjXYh5hC`JaquUF~jB1+81)>{W1? zHkO*0{y_zWT%6PQ<~VnkZFU#nklP0;dQw-|fVJE5=*P-sr^@wd>tAP2xZR6sZG_g< z3>_8e#YNOe4e*B-VegTT<6fVl9%c<#-kBec(Civm;QO@)&X9gN3L~IuD3yF)9@>=* z6Ul6sNUhn8xcPHR^V915 zhh2ZmY?jTx0kctt?M?$q{bR^`%VYYW6|h|TVJiqi?|dwT(|`lHk$bx!R!8Om?`ge5 zz(AiWaKwoS0)DbOU@|!`*5g!>SF8X2CvDPCDM#o8fwZxwggy`%vg4-<4?1yO!#$D( zhoLMDzO5J&2>=ifX_*Yeu{k(`O_i9Xj)t=N_i={JkoN9@l*Oe;4q?$g4kI9N?=hsM zOjz!!3WqJWkx6ZGNpt;$6uE&4o42xeCw`9n&&-*4zW4OSr}m(bjs`n0TAV$*G`>Z} z?#Dt;hw`scgs+Vva>me%Nlb!K_{rk5r0qtYKaGA+y&|D4=Q??Z;7Cst;L#VZM!3Qw2f$L1lU^Om-&Mr)Ifb@a(G>Cz&xUE3vV!8(Q!? z==5a+W8i8>o{=7QvN96Kur!e+%$#m7`IE2eti0+_0=Aa?hd_*(5q0 zb(t#T6n>P_G_s*%O@ew29LQa$zy>3OeVG%LZ3nizjB3gLg4|SC)O6O_;UHUh_|%)zWrAI=)S0{Q5@~cW@25f9tg}0Z(2< zZ&#r_{-mlD&tdzgSaD@x8!*NnnB$-C%0@W&8NdDaeLN=It-I3W=oQa>DRHcRAS7|f z6#^T%LKX2;Ok=NleG|1A)lXgfbB?KwObkx$pNl>YINixd(IBaY9zXggpraugqRusRI?KVAOW3Z0o-__d4M3|2Mb< z`yEzF!4W?2&rQ!ch3Pqv*8F~nIU`Dne=uIF@N5&4p$VIJ%W^t0%h>8!0?vQ~}20 zZr=7zN3krsGtJk+J?IUOQerO~Rd~*aJ-oJux$D5_IUMeRNIND*r*B~FRwW6)P#O?4 zYc)rKO!=i6A?=7*anh}nt7G=Q+G|4lkSYAe->cjFx7J@gyC(Yd3&g{t%BHYop?fh$ ziHx}Z-vRu)Mb(JGCI=NeXvLqi{?z_B%kX&Da?;=yhK3)rJ^QoApLVaCDs*1r8z`8Q ztl0jfVLjie$57Xo_PcjGGP-a_VazXKTg1=0^3gouu&6HKW`B>=J)JhWq#M($$+BQY zGG*i1lD`(UH=~<6{hJq)SIxC>*Zu>QF~O7|`}w-PV7Ptg7I$R~-8`y)NoC~Ohs~O$ z8Da_j5=LH ztPZ;S3$^oyVXZCHg=Q#2P9arnHHRmn^O&wd`&P9-l)ikta%=-l-+ zv^N-n!Y>fd9h$?tSm0$NozhvxVtb|81J9IsrKWi3sO~y_*R`BmW_AwBxBbs##Q|`1 z0*`=<#p!V5^@~$>j5Yr?qDe%sz65~+B+q+R9niEhl->9{;OfPOm6%pjparHT>fF5lP?}hF zzH+fceQU}uQ#Bg$q1GZ(+E4tmkZLj5-k^QJZuxgpmnaiDfdB9hjo#aYqb?R1J^+AE zL%3+4Y}`0m#XoX$`jp;uJSmdAWZiD2)#4v=y#zI6dXr5_gu&k?wJr3!YE=wU_z;i~ z=FikVioaLYS-7|4agy~TY?ykQ?UOoIS-SNw7`t#QiD$bXPCe1Q36(yM#!%NDHrF_2 z$*4@$X0;Z8L?f^N*3BE6qUHXoU;Q1&5|sMv0eA*kF#7P`6c-HyADQ37zwMRb(NJeO zj7{Kfd0+B;e)`wr&og(dQl3bZN3CyR9z)%$D(8ab_U!m7j|4GxQ1WE0pNs0S_#TRZ z7BoD3s--Kd-0iBxXo#LU$=GH&RNO>$of(E4w1Gx^LvBT7ByKj25 zl|D>&$n5|zozHN(d9)y|u z8HdF=`6GDP;28&>(qfcNJ}M)OF+BU8+#`E(-SeidE$ToKdxjQi@rrXO)?F7C(GWBI zU!(mn>9OoFq(^AJJPd;v9}qkT0V~Y>y*F>#C)JO%DB^gwGIQTL2;(vkgtJfXOpagr z7h)dWYv%wqk#N*#rqB-rE#4|G?kY^9L=2vH0nQBuN<}FH1sZ;e|*WdX>+p#R90Sx%)3cJ)zCBK-xt?NbE@{U$Nqjz;>L(sR|IyI?1YCgh~br-o+qI1`JmvZ8XSvsRre~10NcE`1SMP0bh;QqgHALQ4{ zT(Y~uiv1VDZDlb|G|eeq4XT$Z1nfNS6L1b$f-A8@{acuTa#wTG9u5FD@z$Y3Y3D!* z!o2$vy`$)~zAS=`Ia{?>Wt#IPU6Wb-H||8d!^+()NtO;i4re&R9EKbGTx>rbm2WMi z=Bj)uyMACpT*lJ8j=7di8#JyAV_6U|n2Do#GZJH&@w9K%hNGs@RC_SXV5@Nn3^h1p z1%(Bi=wVl2XG(DFRM=y^!aEYDFlP1^B)2r3|_ zf7|N*M^ea=H;c+$WB>}MUw+hV@WE>w<}JFV!_Q;cG{Fnf*XxI7&tyg8vLrg6aj!A7 zI?Z>kML=be9mWW?3xW)kY+`|gE*~4Vd`z25MRX-Xaq1&Ue(Hv0F3zq zD0Gzo>9=1wOW9uOdVJ&2HR^zcYJ=LbuCx)Hqqn?P%l2wQakYKf=^hi@k9}@0Bpa$B zoi72E&bn$&W-H_cC$;(^yv>s>|H$_&Js4$e!l8lyLq~z@XneDQ_cGkKHK}fAJ7s2k zmbpz#RXbxVj*RWjZeuMGqtv{|X5JT=9cfXoSE%3VFVQV!$G~Ngm3%oQ2pF`I1xgWG z@HRm+(9u2J@Gs}2E4<-H@EiXKo^t@u{Xk~A(NbHD+O^V*xna|=ExsM zFOm7Pg6uf>=0|uv6w(^`VpY&Z8J>px&-MJfAf? z{nz)jVzT`SCOD!Rw<{kH$ZS>8^a|BdhDpwg-3v71(v(fQ!yQ-j=@VtDAztf}M~^U4fHgXt2D_PoW&V1=8`Vt_%EHHIYc8n=pqqY?Ma7)YNeUWD zfYt%YHk#Qz8Ibqrtl3D}$&guoVRmM|}vv$J|u2HB;O(OOv0^F9hcaqE3 zNilWXp4O7I;o(IL|7M413JCD43ug6cbvgW2M!Ii{Y4em^pxDOrJAysTpMitE3 zIJyz7P2`AxJG#MVFW=TsQo4F+y>7wR96Yaaho7a3a>>Tt ziL!sR<``&==3}&RC`E@+(Lfuy-1S?^xUF=3U8=;+HRurS6YvmqKnXhgp3c zn!WXW1~t%gJ8V@>?$_t_B>V;q|BX4;cYy)fuPz>*9!d2(A4h*QecwFT;6UZxKI^`X z9U`jX4j$B%p(q++C>JNR4`*LQ^=huu4_sY)$n>G&(_#Uckv&uNYU3w6LQ~_?#{yTeq6xDv_~NqN}>&^+c<@WHkxdx0(GGrN?a7C zpSAqMFD`=d;B5I##kvNAM6F$|K(tfaW>HZ(5RV;kDm6Wlsb~~+^E`IWbc4?4>XRh1CkY9Erx=AfawCRFMK7YbD6RDah=-q+yCwTaps`g3s-XHQ&2bC*#IOn zf?5C~Jyf)x6?aINhjlo*?;SMfp$1RY&WqRlx3BL}$m+9|%r(g`x#~9KDc+|9-8f-B zbM!f3+gRkjw8Z^GQQV}C;VYJzP%qt2tymAgU}2WEivH z_a#)PF9 zy;jOyRYt4}$-HziL|0~_uSl*G)S*GlTBSJL_bfZu9rwzaw3 zh*DF9@9_x&B~27a=8(-ax`P!7&M4RF<%v-hyPvjwCl;RGE^<_rO}xl72570>Pg95I zU;AJ^x2i%D4!{{d@2#|o4R+-yKIiSMV@~S z(}pAw3vmQwTQ!V1X##TO<#};s`ZuZodqfxP=kHIbcisO*X$8nUKdrGQDgN&p97AO! zADBq06t{&D(UW%KPd<-detOn5`)t7NmI{l^_Y@yAAMUd!!hfMHe*;{@e$Doo({MgT*`L$ZsPN(2-wC`(Dc@fgG*q!PjR1WL}~H{y~MDMf_-| zx!&~J`wVSnom)RVc97r@cVdO0Xo_*3@$)VFZOTuaBl9=~M_NKb0;Y&?cob$Ms;{!j ztaULlHE1v)b(gv?!DM-gu69j&@{@6zp*vlnahM})4`us9oF0gJgaEFTG5Rf$M9JIn9{Jt9gD|(41Ac*cY6z%3LY=8 zJQ7Gj;8aZ^TyHcRM= z^vwiBzaG1C3g`zM{{QFu1a2MI&B}9JZws#*kEI+QGiTOm9)0wn`Th@$=X_Iq$79DG za@}8uf#24jQqwJq_U!)518EoK1J*1^=V0;i{TE0KlA(C~soMzQ9HJ20)3lZibQdV+6Kz;)C&c80gJ^GpSC&{`SoIJ%4wY z;|Hy?*mvq5V7vxXL@dJ0X;;Y2pY+uHtrtxU-gGYYFse9nNtpH3cC`&rBPO;?^hIqx za8gDqCz@$K9PX9+yo}Gvs!;`Iq1}=1QoGcoU9#F*IQssk;t?3v+v%8l$J^bVT#n06 zp49VYE3dV#K$mTA+fi5c{fz3vR;hig;&hl7r@TDypw@4%ZJy~4jOEc;9nP}JfL&Y! zJZC*G2Hkv(D4;<}!9#iog;su2M&ebs>9|n|$QU+NbUE>sC2Z7M2y>j5r~8pxoPGSOpA4QM)^gLGiZ z&=21cX7r9{yvByc=4-5l0Bt(DZ2=s$eI&OmFzlZ8W{z(}*4|(5X01d!@5SR?#3No&$mF$CGK(@drI@Zu18SYma{W{Y;O2 z=O)^NlT9g%urpDD9-v!PNuxU%orNMAy1wXwoD)+DHCD#xM20hBlb>`2X5*@p(Of5$ zm9^~rObI7x-ZpS4Vhd!7n=(;fMFssE@TlYd$VCi5>Ao$~dNy7dR* z$6d8cx@C-C?Yk90U@D}jJu!~(GMtLGEMQf;R|aWK*T#kI>+DP>PQ?r9TIDnbLs@pf zw|=knM94Bl9hjBtZ3WNcNB$-aQHkG8UgDa+QE+-~I56k`?$AyZy8;UOIi9V<*fGW% z9`U(N0S;n=GDlpHCyNXpPLDU$M)5p0_%I)Al!8$7^AiLcvC|}a>V0Qpi9}%FyQvt; zwg26xf&%Y({~qH*MoX!N*YyjftYq5c>g}1kGoyNy#K2YNYyW{7M&|Yoz77VHFumJEpNOTkw>~^)Bs{On%j^S??^k|SFk~N3|=@4mJ9{{KBs&(mYV`@Ziz_bjjTI_C@rZcXFb*lO*!t@!@!DKybS>I*$? z{gtq@@95FT8hf#uH7_1mhWjm#y!ZU~dy0~v@yF{2;Te9QFe$|eK-jsa!-f2x1-$@2gq=$}18aWQG!yxBb=?;07}{%1yeNH^IQC(kpjTyZ z%CWgI*^5%v@~-(TKBLMa1>At$0l2-sZQu8%$7baGFYgDJA!bgVr5~;y8|dM0G3zf} z^1G4W9ZX74=DWj(&`(CFhoaxbcLs_VdY-rxs-be@;}H940j%EB$?f~ukK~3J!;+X?WF_%q{*OnD$s0Y*hF4lcz)XWw z0lWIvu)usRUvemPkMGw;!{Mnq_*Y56aPV)syKDT1-(e(a-2*g`ZdQzkb?Lc0?B^w7 z;z6-^=nH)wxfRW1o456%3&G^u+I8Z#%(l5~OLm#+@zHyLHE3S7dtTMnn*q|#wk_ZW ztciIFbO}ywz7SU5`FdTUX$pG7cGHv}Ziu*F5I1zC$%fU2-%$58v)nC`%hLJTy&XOL zR?n=sX2QTdc9?gBD*1*sx@ymA(9wKGI8agg{tp+8gvL1h!Lvu(P6SbkkDts84qR#| zc9poa2msxH3+NgQncd=NGioJXJxqbrXe{HyoaT58jS?joVL)X8^oMO5856m3d)M2R zU<<)oV9PUC!#7SAUD_09ud@?Reo;MIjPMHfI`TN_JSn}p)!So5fA$#YR1wl!FnJ|m z@#QS(VFBUJ+K7IPQ_)WQ8%esUouM{|jTBK^nt}p|y{I1B$B}!HvH!GuEj+^C9olCZ zVzYi0?vgS5%I${svKy&ns~!muD?-X1s38lqqN=AZS+?cz@qp=V$cf8`S#DG8eP%3YZJQF5D_+H=OnZ4 zFOCqK85Yz8eErnr<2iwlBlp=kXf8XvN&!|@BWkr6OHROzF|OM`^jQ0j8Kvv1jw!6& zo+BqAVj}H=}ujTCsLj#zkDKFCboPPBb$xr258W$(kUrE^A_B!LBr7SgU`U^k$ci$O%MQT&nf>U{U@4coc9tAw%$ySE6i%^U4P+-VFAt? z`!Vy41F?>!`;}9T$rwqEThg`_YGN=J?ZQu=7+nlV{TNwmuVTCvXn9e)%Ni33$xRkQ z7b~n99snLz_rUD+P{TC7VZyl{^qb_exIOS+Z)!mV>pxPjbMTb=>C##ExenKTZXdEr z=n$)7jsXWqENOs@ow7gv4e&mI;jH`Pz@z=^C(BHDss7XVg1gp^_f|a_*<5|sp+$ORM8POPnmoH};%PgOZ zkQT$tXGO0B)`*?BihJ2*%Xd{4Xy-Yu{gt?9EwfVNu^v>M+{qX$SA*WoNVlKTf8f3p z4S7SqfJ5y}4P_Ay&n+$>T_3R`w$%ZD_t~a6RTtrG*Y(}5HSz*5R<0aGPiLA7-_08Q zt0aoerQF^_p}QIT!yD~`H4hPIj<6%N?`~ajpP$N~+}7w#ZvFJ(#&7PvO1D;UQBAI( zH{37Mx8d0&Z0ks^4eGcx`gV@w+Sfhd0zgLY^2B-C;DWO6Ptv7Oullc!AXFyk+2Bq? z=tIisUGm=}ut$tRy1>V0M~f+guF;W|X!%t3AT9Y`D${V8qNm7k2=P@^Mnl-DW6rVl zvM%I7 z4RQCa$mmQ++m#pED`%n3*S;2l+_&JKo_q595mjYi7UV?!biE7i&)fPU0F)O&gmqYP zAks*c9}WtQwNJTaaMxPY?!<+wiTsi}M9Z}Y=;)JnxzV(^S^ ziBn>)g_!(e{bqasLi=7bySvsTX8=?!xVC&Rtg}TtapDFxfo3aZxF8RU;o3*~rT*_} zd2t7bJZ7Tu8PLAP@j&~&n^79$WLr|^`LD2|9U+`WZeV0}I>e=ey?CYX=IdE)8Xvsj zh+#;KS$d?5*KIyjl737}+rz;khM__My!GZqNoTcSz#0BAMz+;y=i(b5hn}zRVN?zj z>&eVbYfe-w-<1%smf==CLHLJC4tQg47C#VX5I;NG@MMe=O7Ay2oZ#mJ2qw?LX0$z< z+>&$_^HbiSmL|bw%ca2@P{bLlRSF!UE0McQK7=MMA275XB?_j`91GpG7q{1N#}O96 z%jk;CZMdPA{9p=nIc2+u8bQ3?Y(c*;4n~`t+y!R6g>)5QYVtPtwkO`}+j-(079LK& zIn&jxd7)pFQ)cphpU)m~)IwdxqUfhb~E@OaKZ9^uN%ccx!^EaJe#`-NAvqGQGh>nA|pTqlL^ z*CA zk;U$f0d_>+`~TW`3O<3POV_?gyZ?(dBVUSB;0-qIm0 zJMe#6hF`xX7f=KnGRdGw2=g|ceD8TsI%R`t@^oh0K=_Ukgl!^dCIjXS))Lkoutds^ zv<7p0uW65uUp(QS_fFnHEBol3)0Xkx1g`F#?8UCX9Ikb2=eCHXutRN5@1qBcMGvb5 zWhWN{O<;2G>*Vm%#ygBTqiqfIreGz&+_j?MY!Eh(|yx-v| z`zWT8##x;r0Eu7Jpax>F?Cm%5@{HF1=?VIAhbWZn>pKwaEQ&j*oKEapNuj>Fo`Yw_ z$1T|tNO^p$_F*>j;64h9ukV zZTqJ+!mprx_QL~Ed-u6*dhcjKPVN(eBJZPwVMLM_o7HQdJP=6)gG`1_{W>ldMP^TC$!pUGAN;BMjTYx-7vHLyK|4lWdUuKkUz_6I+d zEop6Dki|6gEP1>-rl{$T%*seHh6<^Jn@=Gvgr=p-HAAfPLH!s$&fuRDg^!y#x-IJt*xRRi94VJ#(zr8UL_}pR4$umSR05d^dcoFd2t#3l9 z{+%J+hCV6E7?N}A^;iau7{WW%m43hITjR^j!9@tH^V&CMYxIALAWza%>LIJJi#u=ZJd`)%zVDzz-~bj%aQU9M5bBkOLIMM8 z+W^!K3V2DTcCB)VIRqM&+T526=!@Hxi-5ZnADU@79CRqRZoNSE;+-^kYu~7=pBth* zWS6B5CLlLm1a6wBvqfQ*E!QFxU@7%4#AlKY3(Nl0NM6)yF;?55?{wj)<0{PVJ_ZgE zdIWP2lgCR*SiZk9HI_E|ntVQAUp3aIhFtStga1P3WgXixI6L zZ-1orZ?jiNCa8l^dyNiFr;f{O{dWibIP>gA|7@3!k@Qrg=%FUMH#&GSf$l7CVSzc0Ipi$w+p`gG#nh3%8qT>(BNf zM4bDQQ&Vl}Qa^{IU=^^wJ)N)qf2}o0zjtig4iYJ*7YcpfLn3kzRB~6lRmWyr$ zHd27`NgC-gHmfvj7_eLmCO~Jd5Q9SJ!Q>GdBTQtQ>T5BcVO}eDYi&6BPcWk=CQRIu zb9*Fcz$&B6Bk`Cn-JtjF87-@0EnbHx$MU01oCRLZ zDxmoy1Ga|j^o^=2dpNVnFp3WEme^>5u~>%H*Z*k?gPTvD&6}N(8;Bz9|0VZK1$iFR zHsDu}fHvq(C*{?+wp&jCKmDH6J)XMR7tS9qcvcX>HX4c=dY;9tsUEQXRscm0^~WDV z2QVj}Y>;ixS}rx-IH$@k>W2;*JTlg_5-pyst^+fJ{CMc$GvT*#ukX>@wcZS#p{XP4 zk+gc-q=4pwVrp5XOWu!}*+^4A0`pGcx8s!dr-sDSx|RtB?~fle{_={jcNWW%s^w}mgVGAFZLnFV z&C>rm<}Pgp!-<5)ZeCKCg0KuohGu&3_eNcvm;lV(;4ZC0z0O1OfOPv$Tfgasn1)t> zb}}^Cg8|%lg67Mt!EF)B(65sPTSpMfWf52t&GVI{L%Gb^q+wBaxF~;IFeHXMiWpQz za*TAK3fca{7q8MmtHC{OxC;$ygCR_5X8*A@_kn*byQS2;vWh%Mn220;#rCy*El}`VWf{OzY4Wi5uSj6z=;c7$1 zRm~uel54Q7d4B1;3K@@A6+*z_{rwT(TwMFk!$?6c+fOm(N>nHaNWhiArm zZ6D9XyaQyWfd9Bom5ygIcN8gLADRYQv_b175W`J4ep=jGC2Djys>i6c9qv%smoS;{ zRqqAO0}i0dzwKVTi}P&t*ohFG12$kvc9Fd6P*|NlnAKi^j9t^@(kd?abVpGnz|x+& z{@XuZ+g}MPkU(Yk+F!Q@h1f~QGu~QPf{NT$cX}XX;_RX7PiQ-!wke+6HDewt#eQ3% zSaM?BNR3eT?Q5nW>E8X(*|Qm;j*QUOe>Eg1P?E7{8@HAH{lO0~&!X$la3fhRvdu53 z(f>NB1!%Sbp79C40!`h8d&+52g0r;F9=WSYzT?P@Fnevd^^?)BG)|U) zXfmZ+66$8`kF*d0a6CB7>wH`G_0L&@<>Z}qS8mPbAwmn*F>A3+LlIiP;b~|yR0Ta{ z?^YV|qJxg3^K6L{#AgW|I>`DUi3=YwDgT?)8oNH`#caU4nMfA@ClSI z|GS_+X7`QALq}+uJ2EYb+UzWNqVavUZ}r015sRlmd?C$!%s^En3>vuC8zk5Etmw(8 zHp>$2v`a79xGS`$z+^@@1=K7kYBBdB#=uaRqf3C(lez9c>$M~oc&Ijbi0uM1*4TXk z5V^pJb8Ha;x$8rO;`xe<68Etz0cNWh-auXV%F-75)CP5bL@WWV*Ur=-f-uYo1n5yV znVP3kQ*zHVBMI ztYP)=|FRmOhCF4(Ev4yTCqdQ%CHr77)Im5cB1P@K;$@KK`O69oc-O?4DuS>-vbCZ5 zla`TeHYNkG_@$QBW0uO+>%EACWtXXq*Rm41y2$q{ro<(usyzN$v{om1Y)37M!uPYO zz=;)t1?ihNaVXLswZfUn@0q8dSm2U- zZKYz?)tm3?Pw`uMuyy54WvZtSko(HK1EYI zIgWLqNAr`!dZs4SwI50LqSCdRoruJ2^83wong{AMF#ffAlrPo$5>b2l+yhFTPR9@; zf&`&YQYA285QGYm8D{|U`vs$^nX81zY%5ju&SIPO{f*BlsS@S>2H z&nUy2%dAK&=aXZW3dC1K+a!gHY@J-%nxkR9N$n7V+I4T0F!dUqw|uPaoj^+@@?n`$ zw!vZvr7m63_zG`o1pd=03_7Db9ZhzZe||M1b;!Yf@#EEc1m5=`6;j3pY>9QY&Jaf6 zVNfMAYQAUinYvcvbi_X&4aQ00%4jaz3;Vh{ocq+5Lk6Qf?lA7x^;m;O{OL2*P%i!Moj}P(D-Be>x71TOBOr z`?=WEkgszS$JwO}$^{E!k{5N@l{`QV_s6~9|3w<|_yK16+9-$BKb%}nZHa-Uyo!(s zGL+L>0NVyuT*T{c935rw^;#EcNp~j|gV&uYEav%l>b7vVtx!?Jt|TjdU|$(v1ZDeN z?$3@}rEB~c*WeGUNlsl=A|50CA6jUUF{yw{gr!5~l_vD!ie={Wa8!yK(QGa$( z%4WEyow@~Pf2A%9s(8yMn&G}Uxh?yFY8O_#PGLATC`MR^Y3n{WNf2DQeW)lU?*oBG z>G(U6Y^HqD7@^EWom7iEFMfBc#C<4j^Co$%LT|ABqb##{Tm6z9bNhYpvzpX$Mj<|N z3C{^i%PaJS#N5m&ry7Apk%kf%G9I;~=&yBXK3YV<^dOhvFqx%|5^x+|_O> zMC#9<_LqV&q}OYu1(5P%KjP0)-sIMR-QVw!N1}w~kP+SEx`zf?e!GLH5jh;xc!Krt z1>)CG$1puBBa!_Z%%IY60S`_LWexTT%y#dZgCaZWP zN+9Xl{m?oDi{dl)i27Y?=`!m(QqtPF90*yt;tibyZFvbB%uJrq@o+~3E_6Kr}@vF%LW zVd{ZpJsO9H7vFW*b=_$biJeukKCAA-WkEy~-PJv-rIbz1ikp5K@PUfTr9Ip3FEr-w zR&bj0%PFm<k|I!nuXGeONd@oVYMhI&t}_^m+m&AP(TXwl@JcY5DPO(a`otzYwg zpllf2qD*cxaGz@S%n4ZWs!g1P-aFFWck5L*Vy@mk7Y24(@M(x<|K_#UrD&~B7Y)Zq zl1QyK-dKoobBA<8@&kFERb5Qab59YRf=rgm&B_d-t6w+9!Q^FEbo8!xED}{b-H@T_ zV&YM~<|Li{&v2z`CCm4|k@crm;V^?C%~FZ+)oOtUuG&8lXs+|SjC|xKoj>ggWaGjP zfi2ikY;ytYqZF)O7YG8Ttbk z8EAGj4g~H}x)z7>Qvmkek>)1WtEQ=O5@6@ffGUG}Xz=Z_V&{KMWiTJ}KEI3^#fW(g1tF=!9UjaZKvl-eFfcY`s#6bFwtlV2oUZ<}Eoz zUeI~_*{7ATJ)`tPsdKZU0-F2@#&#hCAIE;0x2t-t4($Msjn&US{%)^hIgw9|;)inL z2$gHuT-T|tm*0!)E-dh{3Th1W4@pn~Ib;s794#o>UWG!l)=Aa}W4rqw2cqFv|8Qpy zx$Q!jEg3JHZvWpydK7qHm|#TbldHaZ8p8;A*`S(U15noBJFldZq;61`@Iboj)){Kj zqegk&Et+fUk&_ksXmAHnOFY@JOTaGP*2E$avMzdx!nWLV9ca@^w3s+-H{vwaQ5jK( zqna5Voy2KplaJ-Tm9GYX_t53n;A+8&*OEVjlf8AjUbWl=IH<9m>z8m*8(mjeh4oo* z4)yXG}*I5`aJ(c(UNbdIrVyy{cvr>f(O3JQDmt&ze;Yt1Rru#>GUZyyc!*Gwj` z)((uDS6Rvna2bhtYxb&LU4%7~(8S*IeW#}t8)GF3rMfK+78m`Z3Zm3Pwz4o+9mE9o zEV~hsFXzYWIf9U0^X&^2i%JayNX*Qe9U!aKHyw5J1p; zMO6e_Kf_`vv6W1q6oW%jN0}H-f zH_^r+-wrBhpz#TuyXW>FR&y~PVQf+1E*c^#2%c-t6tMPUlr5es*!O3oomG?HCnF|LNUEh1iR7hY%#G~rtUj1SX zgZZ9_f8+u$wRW;GCKgkqaC89|`^yg}bW__CT~oM|1uFken@<;#Il(*?-4dO|+~(nN zsTy3ueYQ_>QDaC$MoOr4do3HCV;P7uXkp`-~p z8vH z_OJ+PZB2>Bp0b?vJAFg&%z^pPLL<+RDR4-OQgZam#`ZZ(6tRckUj!P;5&sf9Z|opO zH!LnM{L!=MBzv8uBNLfGwt}ag&FigSpIuB^&AWw`1MZ@|JT?G zTaJ7yP2s z#t3HQAP2?$!R)8ZTU0;agwAVw=fk-l)JbCbkchZ-*1)d-)+(M z!>W%>7z@uimF|>a;vb{)TdU~h#NQoqfXp~ z2^beLC5bLB=Ku{o_l+EL%76ARsI!{$Y5Ms}9qT{#pM6NrElki*Bs-f5kOOMxg|Ogj zbqD)VC$E25h5vNE)78uIrfA6&%9-xyjs1g+LfdOiJ0xTIt*(89<{|NRx5 zn>L#DMHhFiI*_DtwUJixTtXoSv+qn@OS#B0uM)~dvOFC*{DVFm^sRc_5(g0fz=Esm zqNE++ECc*~mN+>|!jwseQqCn;ydjsrOJPKjh813@gczkODsA@94O70NjEW!ZtX;M3 zofXo1?*s&HY6_92*rUFvbA74VdrwJaDSN6E-bOR;9|@++5AP&c$JEyQg2jH%`{O7 z#AYh2ry!k8wmjTs?U#kNa>@YXY9^T`c;chb7Z>*F1vaq-ie~_^O1U{7X?`{7#JMk) znto4}l>2}OY%=VdB0kO@mF+!2z9U5?E1y(ONB`CB>*AjYT!|cNMO#50h4`KN0(;*= z`eVP+Vrt1!2jAF{P>jH}lta7V%_o$B8?STMnS2a5kUH`6k6HTc8vU1r3h(m}SoT}K z<8J$XlVt}bsiz8NA5~8Cd>=FZE-E2@Ud6{G73Hzp^6!jx)9$TOWNXhlMZ}IRT!O8u z!2g2ET@r;q8_X{dAQjpyyjno8k*W91hQMIUUgPNWI4KF6Mut@4rKq!*7+Xc$nyz#I z`m!!U&m3xOP~<&8p__)!ruw6)c9kTyQ)*z4l{NFvqd_etDdhEW{iryq%o+v9PSx$H zCs#7axbT_G(4>cZUnF8O%D1o_+ZZ>ar{QsHBT_s#GKD?tiD;6k5 z*rgQ?aZxN+Yk%XQB_jRTi)E<67}h%c_frZMFP`NT8`x5qLSa^lmAD*TTJC2Je$Bo5 zf;!Nww7pJacGro4)l*CMM0%av*+QqYqv#9 z29oQse-z4wdXexchd8XGyT~2Z*Gne;Z8Pz~YKkixc05v+3Es+R%g@kre<#jjvGhLf z0(7}zE23Uj_Uxu)8$|mf$I~vjd#w1LJ&Mc!EX8Y(4;;~WWf(X?B8H)#*5TYl2@R3e zNh4+k{w3C4tj+kAYAK0nf8-+;>x1An-;D>dC3Wncg9!KADp;F^llE)6*gfMig(aj2 ziz@=OtHSV!)zaq?X9ls~Y79-sQqq0VOC~OS(Xi2cFHsi$z>S~5R0FR?#@dJtLh~=| zpuzu#hIcoOLW=1qQ$|-!lN0A}^KY4YS>xbc+Qw!0*|#s*!R66gW^rcAAveQ(iOb2l z3aeqKGFwIA5A{Q0@)%Qj(RO;YN8adlVd`1TZ2Oc2cbG{5a<&Y%U0-Dqd3piVp{Y0# z_%M_NABppERnb6p0}5zaiXG4)uvNQ(-mJ_%CCzDw{zQCONz9ROzJ(SlPCKzqJLQpF zk_Akq14dB@r|o(kVoWJJsA2_R1Qsmit`j=f92?~Cbru87+E2KtXCTawg8)9^?`vwk6y!1g={qaW! z717G%${7~hGymx{OV+MAnF=(F^Q~EQ{K~n)vqHuSA^I9Yq+d*NfOJn%5_5>hc`D0( zfkG2nD`W=a)N0A$Co+S{>`M`eM2vBIfc^I^+3$ttaplKjc~2RU zjJC;=w+)Hi2NQwVm~c)CUG4{687!T{7wxQyD+1}mQrKd8k-ekGrZ?itXv#jHclnCUASTxV*x>c zFq3#Wdc{j{mayWRnWS;fxVT7Y-6Wm9Bh_2mML~xOwj={Z*SGj@Cyu>XKo#aw^hJST z3yQ711{cQVM49$d8>e!7xW0u0Wl>Ux_jt$s=$0C`A|&|{i@9Wc_AZx&I;#x{ZLo%&fzbZ8B45z)73*#J+(QIs8@pV~f~Pbod1n;3 zEiIMg^7neFVD_%Bv0j;Vf~tq-h7ok-7zjy_Cf@r~TBUWV(s*}fNcD8RN^4F;Es{;R z&-H9JziuNHr3flqzX{p~utgQbQR;&bGVutp6a>1+Kw-9%>>_7CaFp*Ck9NPb20LE( z9CxaM%3KPRKiFv}#(WguMYO{CCrYP#PcD;|{z6*`p?GH#5#Azrk4zXqQ+{DhJ8k!f zGM>qdrlKP0Cm*7P6x$$C$q0>d&gS5{&jPPk+H#h{zD#JSV}9kEc;0!bfBw^F5{(YY zjr<7s;yB&=2`k4W&C%{Ib8wZp7HZFUATo4-8d!~{k9s#XmxHx;a|F#-6cR-~9m(v~y(+_sqlXMfcfLmiD1focgBjrsC1J-h&0 zr;~)CIP_)2O#l$F@d!rb6ofE#oeutcEo=RkMX;p%%xzgLkjsG8*Az;pQ_R~`V~a48 zf9-b_hz~Q0+S<}KT@7uGiDPBiLO(?d7=s(T+Hea-s5mC!mwN7@9jCeTvvtg z0qYWy$Zz8NaQtxyXM}CdVYjPZ{`6Lmk6tPZxGY}cVH=FEF7ys$;7?53Ai;Is>aqaK zvbNTIN7BGx@u%*gqKTxe()69(d*{)%Fanla9G1x6|06Cc&2=q|U0e8sd-aVb=uF6J~LhC48xQB#|F z7z8VCQIzr-4J|C=3`5>@aMC+`Cr$+&Y;A9mP^OU1PQT-)fycyZQMJ7(SgyL9FA{~< zx+}o%bccl=NvMY)2pDXj&M(w`vgmdswM|oG&*y3g4ZqUDJZwB@cX>8(S`Y2TLPIU7 zQ%-G6bDD){$qdgc`IKU;M0J=%Z6Sd`ALf?MPL-l%GnN~`w-q1R#G#MZD8WOx1H%&$ zd5>3(LAnuU(wep!=yD`uuNea+5^s|=L@6V4YG`Q}y^U{Ao(jWkc-F-3WK3b@R&|<7 zUm47*j)=t0@F|`bse&aH27Y^z3BIPjTCQU2;lX>J<_g21|50EC_yhoT!-_M(GK{&# z>TNb@4u9{qe2cmxAnB(`+DF0eBOeNLKh8Hc&4-{voN;^(2{b4m#Dckha@4hHb zA{wgQ!jv73O`)Lv90y=L!&{I@6jO_N7!OETjlO^wY<8apc#t!rx~<&iyM_WmIZ!Cd zD+UTCZ%cl;hhGwwrAcV~aaHs0mMf3M!uVFzsTP#onYV*zorg#LVWmz%g=_pHO#mRjWg99vD z;Yr*EqVF53Sr*Dh+Ujozh3WMbA!@w*l zbvpS~4FH!_KzOUJlJP75wE`#|toS8w8=lVRXB@}OKGqnEZ?^fhtTt zu^{{l5ygv!%jBv*xwCt!*rH8Krsbxi?9|5Ra1V=C%@HKVhR=&u<{hAYUdI>jC_iK% z*un%-d1GEXeQ5Q5t^BlD`l?j5uaIBSJItU;0riP6#;KPwfTaBGrry%cpz@Sx|4$7WP$Q@#V54TD~#5I=q}n?TiIbD z?v9DBw((1_nd!pLVbw4T<)K0qLiKyymRn2f$-{6ES=;yNof7)m9AI%fcGyenFNE zOTxeWX5*KErXq5l^U~&hpW{EBqh<}^xi=k4r4HU!(B{Lqox=X??X`#B@}k{m5b6yZgj?9#- z5}te6sH#0~TyG1-2(beLEhI{17_+8F)KbSDYONv%2&yGcFO1n`+Wti`PCG;`FVQVQ z)#T$C!*_U$P_PBMdnUK1(Zg+!Vaa8ojP77?F_$yv{OW&HXpj}mtpp)=)-^Q*Hzv)9 zkoH<#1%_Lp)GI9M3EPOD1{VdALo93dMSaDVqcwD&Vz{URMF4%Jgd{ugR*kI%{`9p# z^}0RHs)sPx3y3Ze2&7ulPOGq+_qV14z`iL+dyKmRxQd+gQ0uWT^A&UedDh z=vuK46B#<3y@v018}Mj;I--!Gt^e^2c=Zljdjr5h#!4U{z~!Z~#Nxq!%e7){jRx@Q zD}wFR=-(i~bI`<~F@D~v3&KfcLB+cinW0!fV##=dN-3$7<^Ts@*)PW5=M-#unx4 zY=|TVPG(z3St~IBb6hT%d);u98u>Gk;Kcw+bi0-m3gBSrm4&*rqK7iquSnDHl9G$J zLeNjCIzzrU@iu42**nxd4-|gLnBGPJwy9s%pyDKnQ9$&!Hh@#<7Rff0fZc-`Mw3eE z_+Nw=%q%y2FGO+#{BoY8QBppCAr z+fK)LXuT5{U<%A|3}}y5{Y3Gka(V?J@U;@H_Q<01r+zsZMs;yfb454>|ByTAjR5me z2+{boaXcHvW75&!eI(u zDW$8y*D`|mQlW(3ucp&`W(jI(N}76-uM!NXB`G9!+4q$g<3j%RU%+Ra2~1mBr`9y< z2*$W73we#q_G~F3@I1)-<#qn8l@y=Ora%j_>aF<1mf4d!z#RCL%ww|iX$WWFhU2-b z%sX*q_146~T&51#&Zy#rLI0zqrYH>FR5NYy6)mLl#y`59Hxx zN6gAHtD(cniFk?iC#{{!Tmf}czFdgJ{34`O{_oN`;6=mgy%UISVattWE?;NTkM?MO z&)y_s$qSMtp+q6|1E_+WaC}M{VHx>LzSr}osm%!<4^2&AWK9@gEuzsvs+2Nb<>a#Xo#1>9w0chvF z-Yk^kFu%}GZ`0!p=moog57&2_+A97+4&W(63F{#_bu%K@xGm!#*ldw)Sb>mAu?lks z0#0r~#ZT3I_Rt#;W@UfY_7K&ApdYP3GozHN)xfw`{ogGD$ts`#MX2Kdu)?1M-w0z( zZRSvVgV?VALP4%EsUkv$U39rZ0|P~pu=1yfFA+}O(ZH~NnZW&8NH9=>eY|xZPAm$G0}y6?$O=}SIlPt95MVgVB9I(B z^;#$dqg9c(XnDJ~2fYXvRE{?w|3xz^!%5RXg5dT9XFz=DAXxn+_fjhr$ly5zCeWSY z=9oP&Ysbb2;F3OC<364hokq>Y*-%v-PBRxHg%e1BALQBf@`bNUlUB?4-o$s0ramo^=in&5?xM)%Z2t4HI z4G*qD8}J@7KUOx@6-pc*gdYARtEqO}IRD$zXP$U2M^P=A)q@1qF)DCSGQe~ZqjIP; zz&v4#g=N|@776|c-J3gWdm zdo1a4FjQW}Q7hc@K|^__ujxow1WXVjD09h%HIw6>yYNwlg8U+oLN9d&CssaJO~{%6 z*a|;%BImf`(@mwnGv3e;@*ZLzG^R(RxzBw)ei<(?9jox$Mdz`y%xb1zTnIrc7i7Kw zFW$7vi1QOme6@|$)Gyh_Y0L1n+1dpR+~6|7U-^)**XI@L;x>dM_l*>Avnfif-EE9= z6G^JdF;HM@Mah!V8VE3il}p1H@Z;Mv`@p|p&OByIg-g4&Syfi&x-5{M12oMGfx050 zULexO`!!XIM+m}*h1o*NfJ_)@v=5S|qy78Jedl+`+Qr+ZqRMAau?}KM4~=F4K3=?> ze2cfTM6=BW;5t3+yc!8h*gMBa%zFp{>$6*K{Y=%?6!!`ovr;d3s(L97#+JJ3I@M zto*`$-m{J#FPXg!iX&mCA1stNx=nY?{6Ln!X9K>~Fa`mqqtT|maEL*6^uatpUU}uJ zjjOlXT~e=VsL`^Xb4{HSv~D#iqmfRnO$nAE(&xS&+d1HgpV>R&qqVy+~4${<1cy{<9Z{eNUag2CwE zK&WqbTWTNx{V7<5Ng1xgcn`Q>8-MWE0uS~Y2+ybsM=~#O1y8<2+of{XT`H}itAJX} z4$}rdt9)fRL{*|58tXMJl;@X?4Jxc{tdc1Tv8zACX!{L1=K4~aTEfUWd6f@xSgCVZ zf^#dhEU>&LIwhi33S8mq`_Fc*^QiksN_TIw83Kz8UVahT?@Iy>l(;*ZNSYwYz}Hr) z|94m72uht}wT_I1*D6$^RX>&E3-)L)=KtoEqJ9j$MZUq($$F2;@cc>{ zXB@*tQwZl|;H&a0Saazu|Ft@c4$S#>&K)Cr6(+?g#+?f;|kvQ`OMq1T;K12hqsr};!A5DIYK1QJg93^hW)4%I)7><>F2Eh%i@B9sz$V%HPsD;v2Y0n)%nvNqe`9yRg z1he!7Pn`j~yB_==tP|3T(BFL4^bXVFxu~!{Ka%Z-dKfrPQnEt2rB9^OBHLNE(=cEF^N&f%Nkubxg*^r;d^@Q(j*{YH_NoI;J45&K9&RnI379;zp(fGiL$%+b<5 z2CkENxk~hioJISi_dkU655ysM1i<8s)im6-1d&R2!aYt2tSYXZDQPA{3TuC-mY8L& zJ0!zqGbNA5xL9%_pQ1cB7x37R9nZ0vPo%~;#ADERmBLg0U@k* zS;jj~ubfN2j`{*6-=#6H6W&fNRM=TU`<4EDPafqKRzO=dDVdFqs$(xzx6r7{5)6f_ z%o3W-i^Eonc}GN3OdKO^lA@UsaF}(R2jKIuTHj~H{{S?JM=(4m&{-Bplp5Ceop=Hjd1$3WIi^#Z(D>}k&> zlO4*|fK+@amICDNiZfjf{jT{M^qeVzA-z{B{~uZJ9?x|9$B(bN-4&hh&m-pwh?jmHVYz&){Lx$zBjm`MJ-qYv% z{XHJPKYH|t_1^n^y|3$ay z-Z+gjohv_Owo6r-PclM$cS8O+r_C11gCDE$Q35o+)SuefP8i+;*N7g;Z2c1TYHDoM z*>k$ia9DX{`dOn_%tSgsPslk4d^5R7fCZitbTcb5woKZL>IjbeA`ejJA67htgjbQ& zTB%A!`U|L6+qp-MNc{NaT7!T$mIrSEOczx#qGF&YCyfuG%Zd6||QV2y9Yo_MpUTW#mnMaY?ZQLYbt zroo;UvCsVkzaceL$46<@Z{h~I$k9iAn;%Su)Zz5c=TncrGub^=YT8_CxixYF1BTQn zc9yh=tuu$Li$)-?SF6?E?Rw?WsVN;GR=@+G68Zr;8T>vp3Rh@qUbO~$DB_%T$i(mMQdUmSWX%#or_*C@G@UT!aO!iG}9Oi%iaR|(8s)rCH zQ4im!0f~-%j6B_^sy+8FlB?(a0l)UpbZgm%G{^Gv)oZHbqdMFkKF)&t^r@lkKo)!` zWUiu|q{gQ6ZU;QKiMkIhgo@&k$@dO15H(Sz&Qt3)Bik)%tMr3Ap8b(y@iq*eyklay zwv2AEE)NM58xS6Rh2{OJKc6GS#ftOEiL!9|Q25_)f#hwB%E@hXcKN0FsHMp8EcM9!bky)NYK3kVeDkWC z{Gu_I$~Gzixl_yZN@RrpESr3~4E|pERpbqSz4{BcU@ArUIz9*fvE2n(uj;7vFQhsl z%Z#$h+a>bweB}oCZAflexdUZRztUhc%MHmr^7K^HuPRCu3sQO54TN!Oc&ef=7?`%K z=BK*IYd>w6>|>@TQFSCsqa-r>Tt0fKXzOOQHvF>gzPXs>OP<=c{O4e5`8PWh&W6cR zQH+Eq_(M^ydzmx%PpT38Z1`LGODMi4CHYl!M5NZd->QlNlnhw6=2JUiC@L~~O@_Vp zD2rMLpqBmeK@4}WUAM8GIJp2&(P)RtdB|$3@czk>eRSlCR3hk@raMH}owy=hNN2V# zG(nV}(GKgTyz7zK61|VCj{MOM=U`eP5$nI)R-RziMfE$tNw%oxRJ5=9WuY!G!pE1E z>bsk}e}z|wO4vWu_(+|detPFTR`?nTbVBGvNZdQ4_~A81YvJD=OQh(v>gn&jN6m9Or%8Ki3u8CF;>>fD8z&R}kIztEGzFJmI^MZLw~MV@pUI!f(0- zTq%JPeE@FTqvW(7d!p*!1--r()DhI2yaq4VX+ExIpGJOTR;%Z6!XUr}=#xI|=d*~* zJJjU4Xou+#^!Rs$S~@qj5d=2K{opDUlnsyF%p`TFv_Rx7cs#@TOqmwSYo|v$6WN%M z5smf6@Z^08{Tg|z{(oYn9tK(8uTIt@Z4;E{$&6*{w7?y|f8_+iz}2tu^7JZg8dcM9 zI_~Y&7!h3`00uT6>1s^XAaK=>LkkCx(EoE{oT!I@@M=*>K1Y(EyA85s5MJO=l7R&7 z^;Gr4-b1dNSzR3!!B+l20&1Iqa!~0NHIaB@HC*n=AcF43${;(BAbCt)UI}k-RaUtH zN3GvzJ3r9%>@7dq)1?_ZqfEDNdQ?K!(Zm3z#o8-Hg>Ma(BJ;~J|HeUezOX$Um`gm{ zUqia+P4r{N7tliCm@3$p!UU^|G*BhT=8cC?$=Rv3C;xw z`AgKpwB4?;UvYT045sm)Uy$+oaS54w9c>Zj;?~@Dx1fQp^GgNcZgmp@aM!5q+K`05 z3yc*d&FS~@Z<+c8Ok9yf4$8|vOt|X*E`&PczWKllBx+@DeC$SGmOurE9oZwd_qEYR zrqcF(hr?8{yVFFYxA{?5j?YEfs`E;w*DmL`fgzEffQCK~b1FG$p}CluQ^_HStGh$zVh;jmpXS+iv z-zR62;S&{`&Ci}MxG+gKvlhj3HkX#t5As$<>ePaGysT~Jp1K~!0P8K9I$iC2I2 z2Q;a^NG=S*4^-Jo&Ui);nC1Qtkr{XR&Ht8Lud3vZFgq_wdEzT)Aj&-`2Kk=Ed-4A4 zkCVt$KZ?x%ILs95#yqh|KQhG)z2uGy@K|mr4m&rJT8Ay2+L5@uObWH$`LTuc7Eei3 zdiXg<1LTHLn~~KR{hKe6G7^nknq077u>(WO$;1sxjspAhSr9j0}1J@m^A~VAy5sZ9f_Fni;-nQ!RIo<(+ zGOGB-_zM=jg+45uMGoqULI>DEk~4n`6U6y(NVZV4wn_m7^aHF9qxCD0b=jC-k*h4*;4hH0PG;gx ziPW3_*7fkByIpuLRnlUUVQJBhTt)Ka4)|clvVQoOCr#B($TEb^$}}X8`qOi2n{=eJ z&Y)PGh!;KUhIt~4mEyD}YGqlQt}w0)9~X4Ny`k_CD`Url&XfQCBM-^Mr}Y5l$O}R) z!WzW~aFOjIZ|-8As7R)eEL_ZE$<73m@e!`*9%oJ_i%w--@F|Z}?_8HO#Rl$mQw_Qe zc~RtHY~m!$x1`boa4nQbEucBwdmeg95Sp$AGE174M16i>PazwB51|A~(eR?gHVPbMEV=5~OYF9ezZCc?m_#H}e!F=Fx?>QL#KjS%b@>s9= zm?5iWETdj_(0G|)O^C3ysFFl88Bk3CC~9$k`WA}(>< zRPGDtROj(3Wlp6n{+)zbo~ge1hoAlVtgW~UBkeV>)y-rC+L`8El=44Nz8S*BQ0s{S zz}Z^Q_`n7>gadw{^GzZAGyz}FQH@1Zn8cLgnZM8TFbHoTogA^$? zcax_#!pBiNV*+DDH$|~i!Q1?=svvbQ8Hz5ZZ~YiU{OBzCk0L^4%jc;41sy?v`z|_eiQ49YKsI<6$$A z$d#qAyK^XtN+b(1eK1&b;(boqvtyeXv@YK~VCHdGViJ8M@0s%M&sgPjL2xVC6D^m7 z-k+u9*0+X$MJI6XaFb zBxNoaVnYYID8{$=N_95)&UinKQd-1PON#3kFX_WYs@4%Qh_zsfg&&$oL)xh$oTu=qnooocCJPgROr zc?d@36#K|G4fBEIQ?5JdVgPAFMc0pRo|!6~)uJtXh$!QFTBc@XZKK@3Q3?Ga!+3_8@9x`9*KvKRdl(ZfZGc27GXP~jQ=Sr z2J*tx;&No%qYIvzRg$#Cm@xLd?+mzQ&&&Lo=*L=KK`~}&+pPFPT8SK3xnjBL>Ka>P z80iKVvjcn|)e27&mlt`2{Q>H4K~3wFkqO-q*~Yo;JKu*R^bfLo zs`$)wHv2WtmWpEu{c@3;^KA_J!C$efxOX1u1-F23sJfm|3)PR$5a2B_pA|;W5UoTqbJbK_bG}FJ~q!Y+4bIM4Ji$ zGMg*hUIlH*(IqyO&FkWyz7B$`_+U&r)?^eGG*PtFxmVIowY@BS{tVU|l%77S-<`Gk z-x&O2pqRZ@Jzl&Fe_&ksA)~&bQtaMB6Z=Xd4gXDRT{1NgcFS2#jQYH#+^Px}t2`^J z2^z&I*3KZ~hf*QXM2~bpiS+uoPC;M{J7D2XHF9$LKdpfmV2s>xt~XJdpKVLaV_lla zxCATB`GZRlyN@)Ugwvi(4s&msz%j{PyMK|4z_WCWzS}w$)Kx6~7Bj?%Myd*IQmrr< zlAa%Zm(YNNE=m`7BWyJ4cMQ-s%0nE({&PKGdtJ>bI%(FeGudEi7XN zheZ?gGFisECYL%|5YKtVe&e+fBSb{{AL1O)+t{K0C^=FJdrfrb*-en?U;ah*6T!W` z*L(m0wVLZ=6Q%6=9J|;N-YSxKJG&j@-c9{+32%0XkLd@?n3j=s;!&JAi7ctSY-svB z@@T}kQ-198c@;ROwznD`%-d{?%wq9o;Np%$NB zD+|<^cyMP&$)bwvL9VTXF9&%g3)X3h7kYY%1ZSL@!>8Bg);r8IQsQkq9ewv>U;#cl<8W1 zfno4s)&09qSz#h8p4D3#MJ~JDR`k$o^$(fFX4yWfKhiVG%RQ0e#?U(+L;gvCUx(bk z_iqONyp=(H^x|R100KJ>6T`rEiAD5WL8@4lghDZ7fTm zlAf{_@ae6V_$Kpyx|T+nylxP7EN{hSY>?5BKt+qzC{5tFIM5&+= zSnC;T_6!gzRfCx<6kBQj4{v(eSKt6v?r6Jo~CiVhe@j#Gx2g&9>OS~iZw;kP=@PQ;y+LvcBbuuJdD~*1 zUA6dV*%lUV_Ly)LiCh+z!-@+?Iqvo)(#ca~z<)}14lDnKaFtvbj5lc{yCVg{CcC27 zx}^qjHaUcWYqLmQRR>Aa9jOQ;GC3l_GIIE9QMivPxBbxEGUIruvj#`|Gd6x~*i}_# zvG+BRhZAb{6gW$HL1@zwW*nW9=5kx)I|JDi-hxu_nfB!0+xsePL6xB98lwDZu~I!` zz?T8@iozdEP1BbUA-(7#uP4Q6cf+#1GEQ!VMZq$i2lDm~%xAlJ+1a!OZ-GH=b&?w_ zwK5JJFvcg8?L@-Ur>w0|vg$<{PYhcozLV_+&(tTb8fG6q-+u8Tnds?v=hmWM~^U(t?`v@0j{5Tbl(>{ z$@-Z}-tdRiAL>*Z5*;Km|Hz%su`UqePkGJlwA({gC4uG14!?i3!O3YrN;bQ~>Tb1L z!)e=Q_jwSRay7|A@u z37fI=uTr5qlqu@c@Xx~y@Mf{DQ6Kib4>@*`9qa`Z24QZ}6E&>r$_M_%O?xHK{4v@< zY<&5xUcupN)zOPanlu%?+r<{?AsmT8aZ@Y4fytM%Lqvg&X#)j9V{8I^&(dzJW>BY0JA+mr)WfjdRA4u8J>m&`p=I#TSPKVD4-+!F29R^ z=l+fA^vO(5XxB7ywmj^X_A~67QVPR9j7>>(34-pp7a6HPMHgVeEOLN-Az8qVHbgqP zz_`d+?gPI=@m@|7zt!xE$p=&1#Zc*l-A%GCz|9^V=C$TJeP;Q^jyK{@`jm&$%;w~Q z$CudMp4W{F7KgNJeq{&g6;XxzTZutu+j%jwd7VzJusk9=nXl2Y<@-`j#sKj}(1|XT zy|l5NPHcvW8RUf#(Waweo*xDsiW3hV}K8wl0u6W7Rk>hwX%+A?Z1XB-r`%Sx{XDsRw3e{VdC{4jw zqK|EsV1(FeYW*uleHp6m!UB`@nMReoG2|hiwg#IQGB1mzQb10mZQN5 zPZr89m`F48E(Oxow&U-5y~qRMzuj%;Cb0!JEf(R7cx2no^z`NU>+)FV_g^IADbuJ0 z1V5l%@dT_poguVY5Cl&nipo8w@eQPwcGF!dY8G_ znzn-N?js_wm&=F4(zA)=@{jjKu-=yEccwF{mw;q70>ljXtEziYdWGf(v40hFqzI~D z`q~nzZUZhsm5Sh_@BR_sqp+Yhb(} zGbXmL(pLAVLbl{2O1ETjcZ+k7yHC@Q;BHRTwvkkmP0U^?){EME>9Xa(aJp=ax7oQa zf>CLcU741?q)mKt*8Xl}Atc`q@KagFO6e(Q{$I)^wO3NoAmm0LUpr|n4fbQ7WIm$m zQ)d$OYOw{Kv^6aL+NggGDWB)>jKZl_;ca*_59hiIj1@N7evG1*2B0_hTzwSEURVN% zc%q>t<2?+JRi3W807jigifRt48n^}U9rw?+I=QAXU(4MqTm%jNPm8r*k(5eK{}~6S zXZd8Auml|P+I|Q%bJ?2G)40g6hKUEj$^{;giGt2OfKZ$pFiUx9(76ehU1SH`--$pB zDDPS9%2f3uY}r!4v9P`nmt9LowFX)cV!A;h9U)=9WvqJ46!Yv7YsXw5#R{c2o9u(LKa^})e_<;Nq1_LtV;zn@@}n|kUq{3~7b>n=$H zL;;T@+h=+W7R0emBIJb-BZ$?L+tM8-NR2Hn%?`K>GN^r!DTNR56Nrjy|6nD>$Ill_hTdC3AR4lt^@yQl{SHk>A z4Q6XCcqVsE(#|1Ku_Ip#EJqa5bhnYl73rXewJr%+?GB|u#Y?D5ys~d<^{clk)M*DU zFc&>D)QyDLzh2Cryte5{u5&Y1X+$5Q%kz0hBT~b}8d6@2rf|hW%^`ZU4Q;^-{?PqwehiJ5_RvZ;>dqo<$TbZRI zjk+=`g<8F!iVN+Rj--=`x1D37?PJI~AI+LpplN&MGk@5}u!R%~F{w=-t=Y|9U4B#0 zL-K;bi9Rueq@Dn34MujECfZ;~XN@0OaOAbFq@Fhe*Ec zzCN2px~N{?&C3w{2AgDFK)I0n8Z@gncT_Wu zQHV6pPWPMScqwm}EwV~l$6(Qm#)g^nA3|C-A3q}aZ0A$Xw2i=fdUxpckYyPWuZwg8 zsK4|A4H;CXkQ#BAwzJi*X4ydN zFVT=XAyCww8-kAGo>TKLs_Y0Z=aA+MVv9M_A08x+0G!$p=CSL?-g-qw)%%=#9$3fZOD~K}hLjYuP1H+7JvGmNKfSfG2|X_8WD8(<3Ec>co|fKL^pKiM_bJzx{;J4qUG_a5!FZN`K;`&bbAt; zo`xTQAmG{xEdjP8@Ke|2$QLymTNP$bYnE&wiLbC10#FbtHh$f+B>&;C@VwLCW(F)7 zwO;)-+H%$T!!hj1RlJ_0T7|x|?E6S19wQ=zWd$?y4CaapppP;BXmXQ!AWhuN#dF2v zUu3^--ga@CpSAy7z|;+xgMZcoHbgC-8t?@4YI8wHXk26B0B^`~84$$4tRN`ffZ38z zeIZ_ERl_xsu$2z^oF1WyW2X6?-TCch6BJO-$>$oQ`OiekgGlfi^5d=B?`_bFxG5t0 zZxB+H{TZ9;#-R1l&vy?y>lYLT*p_M^r4~NsUwp;?N-4xpx-7C`^=fE1lO}q1$7HyA zYCqdL&RaFUX)pha@<0q5hyR@FC1Kefm$kq(A1-+hge2Sj-d=mA_keW>LF1qg5G&~x zmIcD>2M<{vKVI}(dJs$hO563SOxoRIc812M&6?<~oJkhZav6o(G@61ftDI=uDs^~Y zD7V__g#9Y6&MJeK;&2A2EiuyB*k-WkJ5rMG)6aLd ze_TV^TGwuEo1G_N;XW9|NE(%XAVW%&5!r5akz&`N;5Y21TH6u{lf(BRett{2o+xv~ zlxz38%tTO&jI3G(Z-tL)&P?S@%m2yjR>Gu<-ql@CWRPDi*v}4BTF~y%e^>^lNHjzM zP1(Lp!ZSprf)!}@k$^CA;qMYlhsGI2+UKw)J?WYk7&Apy@@Lq;V`VKTcu8Fpq{_^@ zBK)s8(wjI<=MXfcqG@R6SI#jX2~)wfg_=0p4#PvAtcF#DZbY0o3X)Gv2On@P z`Tk*28z!SiDhPbglas8{jUA`or_Bh)tidXC?7IwSakK8V5jO>HCSYFr?<(05ynYU& z?C@Wu9IH-TRPcBr|L8!WH-;-$xdoJeUct7MwLF?F@%X(4roB5CP2W1^#OD?u_aYIr2ywygbEbZI=K=I0fe>uJ|m`UWrZ z2_}pW3Dr)wR+WAWW#0X>J)6j9Y|g+?q#g|o?V*-w(2Dy~v)k8?$GEJiO#GQAQXwRT zxIN3G6fPMX|Nbu7O`rLOWklr%o8acB(X+>SeFH=6Jlo$Olt1E_h|I;PMVLWbgW^Z&g9z}MmJ=3;|KZsj{>D#NCC$CUueMvsglTs+*A{K!|i z@-#2#vR#PfLWWJ7khHDgP=W=VRW;ziI8cA#QY2X(iicf{3eC7W>Do+MEY zXows&*usb0v{2K1K|r(+g7_Ww`h=K7DPoDW3+YS2W@(4~efVQ(qI7CZDQe53m@=}r zYi(a>p={L}PVdZd9uras-KyRdcG;s+rL2=}A?~T^ssT@`L8?gp-&SyBV3f=S=m^ zx?en3WR?VJ9f{sYGw&;K9(TM(097&r12%NAUGwGuo0njnzSe+Fs3$) z$Pj&$#pIFSQLBtweid2&o}HWaTj2SYGi2=SWam6Hx!FFK`_Op4sy%!bYv7M*B+V!l zreWG$0F;&AVKr)KwBU*BWdhvskRux|xMf)hU7QvGI{*Rx^$I+*J zi<%IDYDKzZ$vQw)cIE@z?QBRRO+k_Lo_+4_CAAMYUrpaEd?K+&+Kd%AC9sC=0rg7Wb z2<(6OExcDcbD{QI`uGgRJ)<@LTW7UtZtuxqF0sX8x}BNqHrdQGjI-qeO?2ROhF2^8 zB7yun?Pjz*BFu2=-hLoPpYMu{Yfv_79m@|?1bEqPhbkxH1U%T{X2Q;T=#2eelgcwd zsq1kPun<&ZC+tV7V)>$K0`y+!s!mB%sj^x$C6kdz|5`;{N1+2_&8Usa0;4YGZtkXJ zc^se1>}~@mmaZYkjRs6QU-II+w@_=CUCrLR6r*p?i^`_zRZiCwG1m9OzPPD)fp z+S1Tj4R?Q7W|vU2`;=#dZ5LFpBB_CI*dFEE!iB^m;0?;{X1k1zEfwxtfy=Tv=3@p@ z`8ZA*(a2p*be=i}dXmr*?;KT=1m z4s0UZV$q?FvC*ej0-0t5xLRzOsjT{G1&h#+DjI^&5AqTw)ELLm$1O_3eVBuD@XhPk547kg!FY0rs+4tz=c;~Oj56wLiW*xLX16?~Fm*Sus zA-`Z3247cgmgGjQ0?X>v#!%prtr0;Es_upw!FNqqAWj1a9BGn$&Hk1XHue$V?JqBI zX&&LsLoks|I?{_2yc2E#=wqiO%>P)akIA`Nl|sb|S4r{y#}ipexWR<#BQNs2zB)7b z0x#8$LfDNvwr?(&e6_gQ;L(EruY3<#C`E>5H;Cwh$rFEOMZA70jWo+iv*vQ#hc*$| zeLZ`e`F2$Qg8r~O<@+F z-+i|5fY2kWrh;AX1;m^UiGv;gz=l%c7YvM}sYXGaM5yi@_U&Tp{<0rac~%6PHWcjy@f$+ zyQ0+IOH0|m6X=cpz=nujV@))leprL)@UwCb^0&^vtsU}WsasZ$#^bZrnoGlCN-d^H z?`lbAWeNB@c)F9Z*&40BbZ-xX6U;j3{%$;H@ZuGt@Dl?UfE2JnMXC6yFRIHokK{*_ z5Brk3W-ep&36&nd0FQl@j=7j>a8Y%)*L*FG5%5#rzIa_>c@Q*Xq7BDDE*0$b|-C#pZkn5{znhAUG68Xp`Ki`ErKN?D^tW{K;S>pXCe%Jb|X^dW^6vl z<_-=#HljJP8m{+#{f$vc`P*^B_1=P-9x)eCq@qp?c%2zfoB+H1vY!bMauh&b4yExE z5Sk}nNWgcLRu&;~{Na{pfo2(Uq)9?$O_<-8ZucgsL|8Y6*+rqP6=mbNMP1_km{B4d zyn4g?Klds6#RU&G)<2Z9LwDe@gp^jrWUq%Zr;GAeeq{-P91Mj(To#fjpMkl&8|gd2 z?I3;dXsa_%KLz3GQs*)55o&mRg{kIbnR9D=i_mSQq_(82<1dl{qZX=IoKf>FqnfVH z#!%?s4xeE_m#>GufmWY}^^}lyRZGlVqzuh7 zg&k!FNUhh|c}GqBeDn_3=!B0?Ary#tq*Is62~!>^ZlZlpy>uew1AX%bU%gfYL**wnb|xxIsKyYfD(Fuj--i=S&YIO zMr6?Q{7Xc$Z$+lgL+;6#x(a!Y&JO$^5^+MfB9Y=3jY9v>;$Osf?_y*C$3 zjVNJ%mPBa-Q0uK=M(&(Q`3~tZ+;FVxmCSkG$JhP6?75DrqZ+!E6%RVEp z#kbu6gvtAeuLxT27ukld(%8PI)7*sSy^MS{ID9k&D8Q1F#uIP487rG!#iK&j7p5IQ zfM61Yv(&e?q0$Uqk-=eKCxHZixz55Ut&xmC6k8ktSxDA?&s74v64j%PXV}UB)*Sla7|- zOSvue3d8ING?OBBUetU0Z;dxjinnI(#FeaV$v(z!#a$y{GO@c}v4Y;glCPHWi&9U} z9U0pcWjubssrVbvVa#2C3lMtI(6+=gHzGM631N%v&zahtfCOM?+}5$4n2O0OSq6Xp z40FXGG|+l>E&ygh$1%6^sj?>&x>1+?t5Y%8RFf-Ss~%{QjQbzpn_79Q7e=DTwaMX( zFv`|yT&&2=)ocE6L9vXNRL1>EPI)+Q(9ZZ{0m??RiN1N>x*Yz?1}q`+%H*oXPKe~| zDitGpyrVAVB+683P8~3fGCv}qxoI3&qBw)yPt%e4-wOhV?*=MPrqC?Wt+ zo#qaZO?Zz~dYE!JU&m2OT*{*+ujCEkdJQmBE+8qiR~nkx4*79NBKaRD^haNY+U<1h zIp5V|ABBsEx;?T2q*80lclz{&@x3I#-iye&7h)pvW5LJOsx zj&;GtKMLb$HI<4S&kOLjn1|DzEqP0=FJL&eU`KlF1YM-3kxOM*i3Br+WeH0jLSbnV ze0@sB*)JuY-+V1RmFwA4^X89ZUArMcGu8G{jjzi?&-xku5ne~aGp}2p%??0!*Wctn z{v31m2X1SE#$!i|VZdS1TVZ!;!>y}H9%kPJ$GjOV-FtQXYl1Hjb_)0fLXo@wc`?}& z`e1US&up!&qnp!1yEAbQKJN20&JT4LQEk^Dij=F;f42Fk65-H`-x%lz3+ZIUAyxh4 zG#5B}Dc8mpih<SgMy|S zH_`>h5}WLkazVXaL7z=igq%oknIMY)S*ns==va-N8yN4a|(a3^J+L1syJ?vOCn7U0Ct8;DVc4yyR19DLp*P zOjg0oeWN3p4MngYkkFxpRp3dsMbP1;GgE2)jIt9A&f_2QVJ1WuG_`)U9vA}hQu1Y% zqe01hPSfGCE#g+qA%4~Y3fBLauNPQZO3aT1_1hdNcge{168aTz60(ow;?6CO;W@uy za~M?$uv(7aIu5>pOkv+V<;Evo+4uMImWXKi+y+L!M@lV;Q2aIr|KFMtpnr$Vn^q+08eHyOFOIqeT{8oG$c$56>O$O~DQ0f_6e+eUZk9cQoqX|DMLnzY4G zVcUK2djsfj`0MDh+cOo#PoGb6_R@byL!T87!CYX}3Q0B&7HxcqLCi3im54|DE>3#H zj}%*!?zZU@c4+j4I+fb(?b!@|DbWt;RYc1I{T?_IWy0n!q;HQxJq9zA@gANqMg~Y< z$P(zlf-a9=K5>@sE?iOu-5pj>wsVqyE3R&EFDUIz3Qtr7Ok!#N`*Rt(DRACzya;^Gpj`$lpNtixCBWme)CX#6^uGRPq!uc0;XC)j4Gy8;!rYij{#_5IM z$htZ@ycw@*91kX+7tg$$al5{V`_^Xfh&c1AG(OEhvV&!7puUMu1y+@cy8Yf!YrZYC zn8pf%K#4`Tp9wad|7q+6#s=;2PL>pC?HsioFZ<&u z^r9#W3D{ir*}GsQlzDaQST796!k>VJ8}v3hu3a$~v%Q-%j~SZ1Vif)6^=w7)3isN9 z9HLG3l84IYSAo;&e7%3*QyN#<7K-sNfkOd)SX)((;xX=D?PhTJerWfb7+-_1`+0x| z8(==!wYF%ij&EGFuBWjkA|>qBvf`r}h{bP=^jvr8re9>b|8ff@E?sIl5Gn_;ifss_ za;L_B4O!ooD0QxAaVP0sk|`bI^I2&82F_lKvtdG}_Sim?61I^~G2uE^LZ+>u@pYo! zN1G>o3HH46w7jQ@H>TK5;E_dO zsC*rfG6$AqQdWMdrm_ysSSi+bSAvF?t0q+?fVF9a47I@Uv75o&H38*m%o=%izqH8r zcCjdawYBDa?F* zzKMFqxGWE4o`VY6vkddn_AJh1v*thNCN5JpDasHJnBx@c!Bglp7*KXMklBvXdHtV`*HR>BU{ZuV{J(6sccQH;?`$QA#|U?}pv% zt=>PNp@064B92rn`MFZ*To#MgdI z_AAPJ5Ct%VokT2Wa>cV2ne)y^%pu&fkGS$o51Q^nBK`xT!0D0OIOSx@(z4JjCh-lK zX+immZ%Zqon!4T<-xPF;dpbxa#w_OH%etIMAZ3lcXfH8GtjV@l3xWv`33K*;wsdF z-grc1n8|5x?U+J9xH|U^m#T@o=f@qjhW6B}D00F6n*(=s#vL*Fk+%a6Fw)HP2LMP( z8C%j_^dHr|3%hRVw-dMy*cBXjC!Cwg!iTQA_z=jq3k|sgy`(|Rzji(C$=Qm=78#Eu z4kM*)7W|6x#>az^cJt*5ef7o-Fb(eryOD$T!C|t)V{LY}r4558!*CbU@{u0?*%?7L6S{p5i)K;}T|4)&! zo_E6GB`2-L)_4bye$q$I?;Q<29@LT+1SPR{BE9ifgXJ?xWz#4`$TFBM&gWZkSa5_B zv+ck37i}^3;G{+#WIQ!+vDrWP*a#-9vod1b_4C@O>-Hzvvf45N&4M#AFWu{MaarF` z!00Z+EPfBacYAE_4!&4!)RW#v6i+ZBWf_=R1GUBdo{RnoOI}9LrM|VPkrKbyXv6k* z;x1Tj>Rq%Esx1?y8KhXGLW1ok?VG@V$nN(*8F9NADOnrM3mo8fooMNo7*-xK z`&Q>it{XT#C|?oupOkb9R80f_0iS9qZlyA-HgbIyw{kcO!(rjfJg6my02*So@K+1a zc1+*0@Xhv4J`8FKF_-kTT$-d{)1|+Cn}u3%JyDvqHm`c6g)vf|{%@Kb0xtJFF_z|{ zgDva16>QfeC$<)JA0*NAI4sZk-=&k9dT396>*=h>;%WNWQ&^b=k9IFIOmFdzN#*98-wRv4i2z?$`h z=1kI~|8=V1P9Cl{l@&L^%Sjym7+`(oy_RzU9O@ZF#4^GM{yZ+?8a>E#YIcc$>^M&d zecu{>TKL~?c2@#K4}de5aIH28dLZ`U(SaFc=AlUs+-gFQxHEn6JhyA;-Q@|GcV4G% zmt*AMS+S|`b@j%Xq*p(!eSdvw6aB}@qoPYd@zaizE1N3JX&($;|4%2wf)!M8Kett;9gXYVzNmqy1{=xh6BsC=fgu?>)oJ?fJ#yvPD0ron4| zJiDgXyh%tMq*mnl@yk1K*S<4%c^%K#(t@{^Fy;D*BLg;3>vu@qhh2E3g;^G5x*?!< z6AuIjhIq~W(M7tz67HMb!tJ`JXE-Xi%+6({n&;bed9ZNCE99&BJ-Wp%}2p ztt5V4)VyvK(RXb!8ZgM32Cu-Q8~r06wdy4T>b``2Cs zA6j0CNUN70aG$X+^d#H zR5n^K$?<&U61Is-bwBTKUe2F4kgf%M$HE6$*}@V74QXTXgE0SV4K9Rs+?_Pf$bUQw zzoy7}MAotIxqoKnQw9b5_uK6CGy~BFhzMVTTYwBa--3Q@*1l*{J3Vz*y#=Z;=-8|w zx4rZEjrAbV3oOX^y4WQ5rQHbkN}PSVPI?6MqDF7-5dA~`$6@PIDGzt*E}RPKIg6)h zkuDMSYOHDrh}n3OYI^Lhy0e1O<0p8Wc%_jh!N3!abNT8zvBqN5F=(+8I?`KQOZk#VBfCY?ng-VrwqpOxK z#Fg4xCQ(zEuM=3#`-W>)l&u!E%6R>ljV_la{&-c}7D8T2A}zY@B%WQ5CD6<%46YEA`Zz{>?v6 zV3kcDaj|gcCxqNH7xcojjxv11KcDqjId=ehd`%Wkf?H;ur_gKhe@Nc9$|?^)hDaZh zj!5A|-`V-&qV^Tq?;JD05OfI<4onFfbdw~c@P$Lf6vft(<|qH$4z*r6`>EWmXYfG0 zs#n8^H)oEDxK3|9VncNc{1c3FqQ+o%j_RJuL}JasB`~SKsSOrF5%UK=fsLS)E23xp zP)kL0jFWE(V(SJrK3O$?K$>JUqdOpsje!$>Uwzkzbc}&xd!RMO9!%C5Z%UNme9vY` zr;gbpL7wu&D6k_*EpDh(T=lY66TTejk9MLYP))%pg5_R;&Kh4ijr}uMn%E}FmCWkp7N6U9cIh#@*P{=Nl)Y;&TK42q^av?UBkr73Dbuy|qb+SOs1zDU z4uwBi#{P4kEuTnc9~rT70L;h58L?7+xYL{3@}F6Hc>Mp)kLi2L4xloyezUd%C<7#z zdc`%1PAgj9K&xATnHN`puB7!Rh?f`Bi!ff6UN|4^ z2ToF88#`IX>jXXNo|Ta~*DLgIem5Ubn&s%LwHZ)xE_td7f<|=6R)xlPXV5vVs&U&z zcA>kcEeSl#Zep~HM6^x0x$VSUsl}r@EoIqJp`gg<5Cq(xy5^BNevk#NF5 zLH0yo-9#f+Y)&`PVoHl`GKSe(Wgj%(!79P!0cjflenXU+ld&NYHQL<8^JwmP0OzHJlqbNJMUqzUhq3V3Y>~} z$}IT3lqb)0ZUd5&Nl8`(sbNW3pfv09rLak&?xo)9^u>$PS;TfBx2c9MwYZ!1H-U?> zjAG|50+PYXl-A?l`S%%On^f{vRK@tGl=MV5s>!0}6KB5a{;#c%FnE_ED!hxIWN=!h z*VM3sRe|sxa*D+TddCi}nLSsN9P1((H-Nv(Mwthuqt|Ncfbp%6zQEEh`wl(Kx%I1^ z$uMBzOrrr28~4`Q?wRgR70c)d>sR8vj2XZXFm z-_a$g{pet_4zTQA?SMq;A&l6H=?$oL1ZmuMjO{ZAz~P-^fWJnXqc*`Q$c&nJ{G!K; zJV~qL0pO`q1zAUYV;%^}W&l_@6d$pTve!P#gOjdZbiRtY_sM$CZ_Ff0Na7S-K>h=; zX_<}1$*>%F%fs&E1+y8Aun4w}b~ajmi|IXnL3`q?C$D&A%%mdJ9n|yN$@MwaYhX{r zI0uLXb!ME)rieghdnga#9CxgLWG*(&4tmMH# zMOAX6FlmwYSz=IWrqgE^p4*Fm`96L+Cv!1wX*PaA+_^o5k{$1Po~Nh`0(-#{g}J+& zu5w5Lv~F8Ahihv4GL^NLN!~j6`f(6}yni$0+S4`XgL z!=;JU&CTJ~TQ+1n$6X2`hz>|!hSR(@WV_0C6U{mbjp%R=NzyJ(_j@{Unubi}?~9x8 zH)e7JQc{?N7mLtZ_3RsP7K@GMKt{3NdLu>#^={g0t7pE~U{#g{nB+4p=06b?b=aICld7Z<{%nsxf)*8@DsJJ@fRE`1{wUxnqwKa2z z<^SR7%;S`<=CRBX)1kewE#sDbIdHsEpr3GvYaeSrv)w3 zC>b{vaYNidvrN;Htbp8bDZ@M>Za_lV@1gqs{;5~5{yB$p&hy;QeP7r6!m*f24{$Kj z=`+{>7mXtS!T^4YssiOVWZV5F>5*Ns^VJ#N$a?Zd-vHVhvFe0(4DsEPM=E>t?VW3| zF4(X;F3NdEP<_cc2m|8yv=+d|vw%~Uz8GB556aj{47VH1b{eC+O*-y^yptv3dcc12 zS)wm@w%vahTD9aarlyPFBMKvi&4~U!Y<1q~uhV2Xu_x3?rgtOSZJ+g5%9`_@AIJkmV#eLyGQ}Ov36T7>O}!B^Chfc|HcqxKOvBV^?BDu_?^M3 zG7_n+!QdxoMqw(QU^B|G;RNG0>{e$^SbTmH&+xpbK#7~^ID+dT9JVsb=y=*z13KUl zF$*Sawx^N~b2SIB{~jy$NA-25OM$C&nN|*G+;bz?A!zf^HrqO9JZh;{TfWneO)^3_N5rdLb!xTJBGL6nL>qW zxo}F*)q{$eMvzOfyetze%I27_+wfXp&jyoNUz4Jd7P!Oz5IvtDjI3$3EIDN?uJyJg z7L_pW|MFH$#;UMeGXEQS7D{p9u_z0m0AE{3a;-yZH|M1(jpmDjelo6fgo1qpuv%RO zVq9Q2woRIQ+o%1bu11bad5T^WPdsS!4n7!%xXI|pY4y`iT(4m3XE$gSS+xBnSMX#| z;_&S0JrCk2eWNiU6)nS2sOE;bwK$)?kSF!MidrvsvQ(^RPS#J9{cQ|wBaZWdG@Z9} zN|SY${W2m9pTg~ZNT3I_<1KK^t`J?FwG^zDAaLObc>CAPBdon^@_)ND(D{@yNr5+h zh15Y>S>AX_czEx1bd7CofN{#Sa2wTFjx{b#Oni~~WlF=8q{5L>aWm|^nPBCt&|$_Iw?*hskrw<> z#n42@n;GXFUUA+LcSIHwd#}9XR@qW9nDu=oo(B8&qb1JiD=s;uH`i%}&h*@+b>B5D zd%%-t)q%0oxeq^rI+DI>BESL@!tAZfpEv9UtmYErM#AI5xTF`$T|lTWIC1FHB`w}fcn zH3al$Fc&S|GZ`%xE^Od5DgBNU1f}BWOhlL-1-SDIo922d*oNt&7&jTQYih-@FJFF~;gCf8b(;8a7E_{sisxg&^K zU%Sz&);Hv4za2`XCp%FJ8>35WD1orqtfQ@UieFs*M{-oHk>^cN_0!NC52vu!x~o%?n%rug+7 z>Sg!Bt+&8riIp@vY-7M@JbG`CAu6fnh}-?**xf!ZJkK>zI5V*+ga0U?U1b`tESyRX zWtn#2__?oiDaX!QaAVn1=!Q)Bec{r-xE+f-cmKyipp4Uv6E zkh46`mV-_b&!_kU@}@h*E)~G{Fw&+H^?tte$h;-B(Y$5!G3WWuU(W`7*zF*>22_-* z)!He}otA__mT`H)rMY)r?>FXj&?2ciV!v-7jWxPSU6dS9(xM;0op@tF$fCJF^kYIP zgIFm1Y-s83LYkOXE}{^M2gGV|{NqY_bV#lR*~U-|@rz41Fy*iVgVHk+=XF8%@JtLX z)!|Ryeu-CMYMbJ28@}eBThVoIbU_Q!3B7=34K1tueVxb24C;oGaJL*v1vYen?g^_?chvcvky~g+%BqUYA#Yp)-6`JX58C09*8%VvUeTdyA6F*G=2K_G zj9+YUtrn>Wp=7Qgxt?YDKrLm8>P`lxkMuIH^qlXhYo(q~2UF`R#m3jlju7ISn3~p( z&sO|dhJC&Bvih{+j4)Zurz=VU@H2i;m5oHt*U1!pYhaduljwc5bY_qDL@`yXD9gxMB0o|pPpUMg44Ve=!bw%(u(7FIqJWniEp z#Cy2$i%tc&V6Nc0+8%a?DIJL-9vqH7jO6ZZCcK2iThBjCFu5Dg$sFl5=YIJQzmye4 z3Ska$(~iR=6}>w_4~9tezO-}BQngl#9vH=gas@Q1J`{m$v?oA12ozV}`2m09PZL=* z8SsF8t3PU^pFzF;p4l{XOMlpFuZ@Svyi?>C=n;!BE!ar%()pWddChylNvy@&Lrd)C zPy+KQ_oyAcGyp#y7xSMhBA62M??^_lC`MsRRA)&4<5nTR;Oa z3@Pz@@+ijA4VALzSPV;qSUk#v1#9jzVlV-VAe=oCkqhZv1WkFb*BkqBzyFsdv-i^t zV@A+NgJgRYr1U(4Ju?Y`PN>iGwsm_n;SW;Yc6lu9;>%4<7I{dNJ%m-{DUEPce}$Qy z;I8*F;Esc~o&9WZ4Dcwof${6bMsqkn`z(v}`Mclpp55EIOmMWJ0w?|L`*Np)70IA7 z;2k^Lzr}wEF6Dxl;A>*&>l$eEkDn4`M6rWiLs3K-+n$y93Rb^Exx#jV88hDEzDCOA z8lLmBEIffh5gSmCD0)g2=>nEu_DB76;jx<$PAxqqK;&)0KSfCzmt~H9P3I{)Jz#>4 z712Vo+y-$%iRe(UlQyDRzH1S9_ZEYy-SmfWBs=FdsJ1Td>-av9odqOyFN}iWhl)55 zO$)3z*+Cb|g(JwW3~lZw})fI3hCz7fWH->1j=s``yB|;Q{y2k+ep@ZOzD4JZXNN{jd(l31`i4*-)UN3req_HK+a$eM$?_#R1{uoLV(%sLQ} zf$H*sePC(?iovM_yhg0F#BB;EI@PNWOf=zazf1#Sb=WMP`k1Ta0ffh;#Qe97?;cec zzJ`LB(>)G+>he7XUDoCLMAmH=dI5(N;x0LVp?9tK#(>Y|C;jWo-H*!rbB6qX|KNZ3 zZ)Sh(YdxK#PhaA5ew#R=>55ys-5yTmIK(s6-BlCXtFVYPYLxMX`|-A=b+>i zrLvvV*iyaAXkQu9%!hW1y0y~p+FIlv@V+H^tagO=BNHT*;2MGobg}=zq#E=K4hK|9mO_4?w{EG&CF={6`}D!P9r+ zE2h|<;+|TzGrQeenw~WO2@D_Q0F(uBO2|%)kcF;z`UX?g>(E8ytB6Y_s z*H}s$c9-(#xY7Z8$~B(-;MrYv$P*&FCuNUk1SFyaRo>oHchstG|0xi-9&MniIivu!3Z^D=rotJg4 z6T8@u8~Xtz;L2B59Nk0D29Xr2^*_g)S?$CzB;f-J*?y^T*p}vs>v0h_#8{kD3xRTG zoa1vS;A{qOflNJ`E5T0qFCH&kASbtd-CWHVLb-LgoVQqGe7*zlq}wVpdQS_KV7jKf zt+oyV{V)rlY6s;OI^zzgy6*r0lu`qRTi|g!Xt8RFgX+s$etrKhn9we2W;PEZts=); zUWl&K#)c<5t74Me*+`BY>-}X*1ou&?OQsYcK(}D+FDRT=74NSS6JCTb!sg>~u^tgd zsqjH+B8i<+jaocT-7zAbvj=@V9!r?;TP`1cb;+!G=PW(r1<4sE%Z(YI=;&*c@$_YG zH%9)xQ*5l5n(U0O1rF7^iDt+=(2UB_9eCpr2+j(6o0Gv$d_-$$9yZzeV=Z4AQNYcH zmgL+dg}0aTT!cW@{;1l%;cv_otzHeuiJeRn#aWzB48|3^G8FDwxJPVY#qGJJ^R_#S zxQxL^)SUeLn#>h(c3fJK+<0a@77^%DD{NEyni%*(a_6+Tq@~c;(yHOIt`sw2v!#%K zDUrgMa)#Eg2y0i~5K4%LguGSx>5Lg3j-T`yPc1D(KRDV{dE6;d)drKaSV7*E~>yw*iF4xNu2MI3j z(SJKkXa%DzBDrc%?6I;-c~qwempa?K`7?TTwWCk&fG>kb%x zZqx3RYgfGg(EHuPr}KWfoixf-3`RX%7L5a9%%Ge5NZ? ze-HghWn9@mBIAAEaxg7EZnK@yJ&w=SzY3ze@LVeoqy&9+A=lR7TS3VyhX$(Rt13Yp z@_Y57$j^CgAQJogik0Hb*J>ug>l=ITY{r2{K%L&E(tc!|=_D^fEJ}9`@-yo4IUEs? z7=s_5SosD+*#bn@uj*O2;c1WlMYG*xqN)aXM)V#XPawCVMnY1O(R$_LOqrcM$T!NM zo^;Xo6`ipP&pm)0*uvpbFtU&y2T?^DzP^;_7cA54@oWH^LPH``SX2dyCTJ*w=i-=E zKfYYRMn1*MxK8A><=SZ1n!_xH33_?t4)7X)OZ_hJZNIN2m1Z12{fdzlHbHF+ziT`M zM^>YY3PeH7uxpQY84Tr3*cg0LJIkPdxpBkONSlHQ#pjs#e(I=SroO#PefZHswj3&I z$h&lx=^IGd+N3NxG?y)XT0vPYTfeME>VNdN+_%^>F6Zf@wR)=aT%qhYSzNl3-D>k@ zHS)gFJof|YXh}f3Bi~bWpZzog+i?DH#5R?#PP$f@+4IY(u7wu6;9zeJ`PqINx}tbl z+44U_lkCJZm-`b0#mAFHMR`JC^g4RTd1q|^kpa{Hl($qzLo!-*+EVf+AWk-KUIY&8;B6cIZ*q?7fDKZgT*`UCkU?lzL`nz81{CkaAyMy#QS3fuXD& zKeq;OtZdwLtx4tbjrA=-sRiv}nayEL2|#43vBtGNQfVK@{J{o4f>5s!b6^Ht{%>@f zh#NsLaYXPGKxKJmSYuefyywAhGGql|3%9VU8L)b{KYiP_nIK!QV_WZEKF_6g>mRDF zspLo~`QM@s?iai>e7qyL6wW_mIVISC%L~SG6jj{4eYHnV3H2;GaPBgesH@Y^qA`Cx zqb1+6w598(ABPDr(~k5v539cHa?r`LC<1}2sAHbrBE4M>_vd)ii!+(K9~&GyXxJzE3SYmnCh*IY{aA` zM#$!F3to^Z)(H)G*PF$Ty5kql_IAUk& z;#sjbd@=4O@Jg!A1epo#D$KJ$H5kHwa5iZ`8*-@=0-Sk^$D>|q8AmKWt;B?t42QWqC%i0=jX7ukaymJ|dIGq#<1Jyh#U0j!tH+D+M1TKs@9;0M?4g#o(j+pYs(EasFeYI|Ic0 ze-{veLw(zA?U8SH2my>T2=walJlOlq)vTf`7lect7_M`K6RNRcrHotu`VD>8I7b*Y zWe;~dbRgDsuT_o+1pm~t_}%rflPKTMdcy}^vMrWKxcVbr_L^h5g>p>=uV2NBdvcA` z(c0`t&D~Id5M?}+s6t{lp>{5m@z~OoDRBZrc4RlD?iT86XGoc|?GvYq(Ty+h;3v+K z`2P?q`HOb${I(guF~QYFbplAfZ-4iJf#1K0ko1#vHBX2rZyS*j&={)AX-c+lz!BUb z>@TaN8MKdu|9tUIAt4ghBE2E$#+h82DHbmH3>p^rz7^Q`P%hkPB7_p9aQG8NhKSfF?!%X;F_lGy&*l=6xECY1D{eQg$!Cn~cAY8pt$jQ7~NEQmt6Rr*P=%;a& z5X@MCL3W48hQoZ4_YrgAZuDjm17Juw`KZ9)6c@ualddsle@?H0!^W5wFSM;yp9?3V zh6BPV&N~Ng&|Wat^*TN`;L-(hLBAaUAAyYO?0OqRL8!`#)PR6Hj|9;rm2n-f{G($B zuf-(}HyP>d((_)EIB*Qq3G0EPM!O6!a3HMfY(~aDS_gx3%2*+E#$kBk{Yc`>9*sHG zcm(Iu?Sn0x5_>nIx$#W~FgnGO#siU`a?i&hM)Pc~^uI6^edoXJk909TkFRK?cJ+&c z(3o#l2%C~p9!JQ3AXopc_BV_n-BYWZyR-Zwhx}7d61U{B(8$>E1nCE=jy9ta_7y0} z$Zaa%`iWf*^i$7X;u}3rE_?mM*!fJ0!xm@*%bSAFYti6W3$fLIB#=v^X@A>cj~4cG zOxLz)RAu>1gacM1j0?M4Z5|da%))Q!nnZgv%;mKBhOsOv<{h#9IF{kj6CHBVC;8RJ zyGk3+R^A`czWqm_ru!o&(n$qWgKIN5p9rR;Rqo3Xrflz&rGpqx_x=EAm28sHL<(mU zgBk9XnVVY+hs+zx-Ud3kI=$}x$v4tjo0Efi|IK#;<_Fv;lfigIn}-1a559h?zOmff zLvgo9ICVWr#`8ftbEXonhmQCYZ;r|HLcesf2$g-o{da=A7h5ocV8+kL-?e$IpTi*3 z2P`ych2#gxc5z+mR-Fn@AsW5$!M;78+P`U~>}&eGcqk|RVZ}SWja>;2=f}gN9?oz4 z1-L&YuN9(?PRnhS#n;0v?JtfST6Bkx^txWaln7%%;mSeLrwIf*ee&1Kp zwT`p0d!u@rj(1GXISctfv%0rxDKvdFc-g?~d+24LGc})u{q|wv_0PI+2)OS+Rr|gb z6XmjT$M@9YkZ=<4a z3}FVCZ+yJXh`A!(#k^y$)*!11$hIl_0z@z7Nl4{FTrR+LJf6D^Gx^(Bl4g?XW3#-I z<>&{xzaQ6&R;7UMo=`m^@3<9%C^}KUZTt1Wwv>(dEyryT#TU{ab(=Rn_6>BpsQhJ2 zcTj5&s9B`d(t--S7VisK=#|YahKwCw8otgRi36LBenCYWfd#8F%;PUEDEE55d4x`K zhKN~56LO1=?bg?mPv@1o7!1j?t&u7fTOK9Ec?_PuVEJ^dx1dbvi^x4uOrjju;QSZ- z%gr!u^=VSqR?yApRZ;40Po!f;X$=?T+W0!6OMpc0&=e7V7B?h@rfTyJ55WwXk;8a3A4U1ZbbV=S$6R`Tu%UiVDoF!{qenyzU064M3ea zAE@pwK7H`BV-1cBZU7Co!46`+KA1-1!jn~`M56^hq8z<;v~hgw<75+aTQj2&mc)Pa z(VAbL7)(=PA47{)OGOat#KdEf)T!z(+cPh_!`s7%=jY{+m zYZNRJ7psTTexjGa0#OWJT;L{=e3QGh2NHibxIRp@ex_{N%yQd7ziUyfN*C`8_1WSQ zXVjRWEKG&0?m-S zn|O#@1rA)8xaKF$_8n-8HxpbB5L@8q)IpCujq;Y875l|ky++HJPq3-H>yxjva6 zV0LPH8V=#ZtT$$=hhy`*oeeszpM*c7jqz>Pr@Fanx~Ofy5W4VIx25eZ4cs6Ge>Jkt))J7^dax;V ziJMgkC5RVroSgKBX9pDObCGQ=rOF7X$Jktd7l(1n(UmLM^(;hBMq;?mJ-5@puLtBR z(EeWKdUBaj!VhjWNH6du0|M^S^c&UjU|cE7c+rM`)e|tB;^A#v>=R{?@?7OaHx{EB zPp3qqt&$Ln*XV_JiO#w!YP*_^#CpG+4~X?nVgCU^YxN#c0}I&?Z($&u9nz{^(H$Q0 zI0&^OM=x6(n?zFZ#9>DNdInsjjS)Whr>?hXci0(!iFASI(*ZFYT>LU(Tn`POQ1BKW zN5I#e%J9K_@NsZIC8^z0FeUEXjh)TW**XMTk06rUrML~`XI^q$plJL{_I=UQfy!V& zvvGBz1dxJbDOtc?{DM|D%;UKjj1(^59k}vE<;2iIlXlPr?6|SDjULaXo%eYuHQ-I@#nk99b7lyUbl@_aKVG6i%#Wmkt7?W0fxf z5yzLJz%^f-v&f#acwqj24b^sV@@%vkY=qfCqD*8QMooo{ZajB~WsFRM=QSQ;+3M?4 zrB-e#?+rjzIbBXWDT_wJZUJ)oMQqYSDEeS=L4Zm zjZ^uX5q>Cq3l4Au3oxD!$h>u5P6g&j)J6m| zk*rq7A_o1JT!(|{P$EGo@8;^*&=dXX`Y1X=rfA`6h!lCueY~y*3QT>98x$J^GdN-& z943)hiEb1Asu4VeiYq z_te@35NNjmtT4=f!BFmT@8!+_2*5fkZHGza67*2hJRL~}*lirZx_Ir`LG^QOQi0CN zHCi=P9s1L*AfQA9zFE*^u>LjsnC>W&jCZYJ2k@>MR|e~5XZmkkuGcCIDN9Gw^miDH z=gICEuk=&PJ&c4nWW%^1R6W=2&5oAuP}TAm+gn%kP=o4Ydjo8rv7eHS|Ll1e2O zYdy!fb49dezRq7@Woh3~y6E)s$3KFzE-umLzhJ=f$hA4w#$bp9GboT6V60qWcF)iC zLEH`FDi3w$FT$BvBLlgnOy&xg=C=}623cVK&D zN5z~fu7P0EiV?^$FM&<9sV#`1BQ_YW2$fnT2SG&Ux*mgz+%RaGWl8wPYd9a+G@=YV z+{}&ZyyA0hjM)!HRfd5fh?{HJx53&LQc(+UddBe3x(?@Vbn59JSd>eq*x3RR(MT$( z%DTa>KE|(My4)b+qbW=v7lN~`gofw_!K148V|)01k^^1MTgWg1H&$~m#-edJ@U&B@ z^E|@I-9OI<;I(N#nv4E66Y;I;f9qt|XfDi04h9+A*Qhm1QmF&G=#^~YkY0~i)ZD|V z9Dyi(N}Iz)3m$ahVY&_Sd*YHSETQCW2H%+d7N~vv5|kS9Kw_H%b>$#=K&(o{rQ8#z ztR)B4A2#UX*C2_2?1z3N^I!F!XuDr93wU{vpRU_HYpvaXDiDvd3>aiLXNdV~bosnQ zS?{7qG^n-D0b7yp&*71D+uON%z+hBsUu>b6!&O#MiN_NL?~2o#hKlqBwD^ps{(PZ~ zTWGQzMY}{*=E8vYN_MlJLe!{(P;yyL`a-d?W{R7UEE;V#JV0DluH&M=3vMm7j^X(s0=DJWH8d{;eLi?c9s!J1>w+R;cXj zOZ~^`3%_6`sUzIH4-CEs*(tIMI9#vD8_U5*srUbPp_r|2=hoNLwPW=VG`e4&`DObv z?bYMx3)J34!$@>H@xVX6KuBcmK9kPMQ-SZBS!I~ z5uszZ{}R0mfshpWq9AM|OoNuURa@GdoceKzy^cjMLEU>uLv ztwxg15p8EoX#(Z>LtQBg(0}Rx2wbgiz||Vqv{bIxpEB_d{OsJkDm7Z~0wT=p)j|sO z&9*1Na85!|4UXnT+dEw4ws-=mh(M`=mJ<(U4zb&mRq`6Y#joLxz;%2Y@&y9s??Kc6H}N6w4GCCyaN?Gek2Wli%TVGuDa^EUEbyR=h> zcz6}oY;r(v2Ce+X_VuDj?5XrXfXMV;8~Gf(E##jg9CJG0 zrvF`n>J5?RWzojWi!a`U1#++Za?Oyxu*`ZfvSH@&H=tcX_0o|sasL^1)%mWgt8UcC zQ~Azq`jho5d$31+b7z^#nO{2($CjuGSmi zVGX$Qw8tNzl1;TogHC1Ei}FbCj)C@&WKyBKD?nRefk03kSg}z**1dtP_s%9|KT9#@ zIQ@Uk2xkC$b=2V*7&S6mxT5Sf+B*5Tk2OfGj*}5RVRge{vD@}q-7P^SV)&Eh7Auf+ zIOvDBqZ-l0$1+(yH7A=K#HwGhMXQm17)rZi`1Q>%J7}ZBVZom`#_=f8=T~zbepldv z7hOaRaKzW@e3EKzr-hbBZ**8z_%*$g708XaQ1-tI2WK45zA#9xmxcWG#}kJg07BEj zZ`=LXo8*A;w0VgUYZ?=xV9k9_>dc+zjF=-u4j-K$k7&gq|>2 zO!FCpt2E_r@2xq^EwpDM+2@qYVxUKKL@LnCy~kW^)d$h?I{CxnZm-TY$ZHt(nEu~n zc9@JUmbg2%N)JoC6@%iS?!mDmxJkl45~cdXBZ-;`p6gMInDA5^@hzxmF1=Gy8#8%5 zrt@M9L@XGADi%PA?KW+tKnLmf4ih-ny8CX5LZF`*@nFDwmo3*@V|Ith`dq`*lW}+Qx!_NO|511qQejD0MF2K zP2$M|U{&$_nACepOKSnO3IjBJ{v1aAkbNZd@W3(XB|tV8h*}a{8|2l>`h0}-US^{} zzmTB=@aFqtxK^QPDN+$4WiZ(##$x#6=wbB=A_;n5)N^SMsj^l~2$Kz6_A)Myd)#`= zl_+%Rt#dJb2WLJlL(KUF|i1T?|=gd{&AwkoBe@=@U0Zo+W;Qdcz0RAkG z{X}M&R#FlnwYtmk8^nPAb}+~fAn-9>M(&uwViV5j#ULV?B>NHG6I;b%}^`uf1UT63S-a}TFq{7DK zYV=?MLc{o+eOq?K>a`LklE)&VT+Tm)lqFZbiBq-^h>kZZ73nr*MDI5hrntW`B126n>W$7Rh1y?L2J)ZUfI_qJ%X1u?xX z2xU)jDJbuw-0|OgD#ohQV;KYL&8GwP_$Ze&;u%WzN)k@<*htm$}Exf2w;So05MA8 z!IA-#*shvbS;)f1$=kaZgiT!f0FJ6_|F61v@tVi2eBOX$c9rE7eK&fvW7Z_nLje_8 z>82r_%X(G884poQQr)bqPf69><<|P6@@aSk*WNv_%(w`dOn`6|h|-nZ!-zu>%;OP0 zw%Pa2Ke?iG1`T$v#?Qn#pkQSlbuiFjDkjO1od*4MW4VwpYA`=Ea16=Y>I6he9g~MM-{hz-lS3mh_)Bs2)maqc610G+)TF}RR*Zhk=PmNt zxpN$Pp;XKRq)8#}9Md$`FOa&YVEYFGuD%N6#>%lI1GI+@-ea4ZR;@gLnD zgVuITdb>s_3QpggQ`G^D-#hH-YzKSX->PrqM-adX>ODMsSq3&vq27eK z^YvF$qstWA_p-^~7zu&Bp4X3;gjx+`v8+T!F}Eg2o{L8>eE_LBVAn_o+1HVKH7-}y zS0|>&5*XO#5X*0)zC>*{0kxEs>@F zt_K{5hPPpZX*&wv@XqUAJV7`1BVS zJv(+h!7{&|l`);p=^j${6d_3-D=(nB)8LjwVQAtRW=j7CaZp!+5t5C$gh!;vkX;b} zwtwmn6E4P^w4FIHhkfDc=pHtq4zsG}U=Q$Qz3D3eHQnCUO?PX65pXb~br#Gc4D6rz zwrf1LPAg&s?_JLXuqdgA>q%Q89{eU$K5{l>KVwz4$Xeu@sTlbORh~{n7|y33Zv4vo z^-8@NELmfS%=gx7K+aP1!a6*qSTPB~?nh^&-T5EpIS#k9Hez0YCn(pVQCO zop0w(8aTL|4kqF(LOS7LGVb+QdK31l0{h4|fa((9@+n1$E1hS9bEKDM@h7p-FT7-( z%li#ffNgnuCq3KDd&x+-?3y@4@6*&5AYS()Zw}zo52hy{=3+JX<{T0vy~h}(wsbF! zTQkWeGz}om;LMlY82t2@wOyl#QOgJ&UP1L;>mz<0x1jz;z6$y{^>hP>E|v3n8fB=j|aOP z0%T@RqY^euSHqJ*Knzx_2pU$862TA>`SdXZ{M!>*NY)9vODX}XyA)!quGjy^(H~qDk`N8JS&3@<;p7OeP9xBjfbMvKdMVh-V04`jDM&0M0~nLK)&e89(Lv? zm9!-|y}9OUVUZg$MWi(Zl&2k^Va7cgeqLzeHjjQJbk{ z2T{=q%s*e4vz8d6wIRCY&YlrRYNV|DDS=|3?;y2b58s5(Xbb8T_3JO;o{St-?rZpm z)W0!D`S1gAN#;lPyYzXDfDuExXc2Y|e8+GWlLHXaBFsK{efSpMNK4(wbNtOtz!2%T z-2&J?zvOR zY$wxhf(+xtfpaP--Z+1dO4EM3ndHDIq?zv`fqOaoW$W9A-@PinS98v?_7Uy^O)N0Z zSH+Z?r%d=;y#$mKaDLM1e-I=KV_Al7Vsa`%-;-|m{LhwB;m+oo=(op!HV=B$IFXpS zRFJ2fP6qx;pnZ(Slb;+@#B|u)eciuIMyz1BVW4eXr!3=79+wId4;#fQKIBP@IYK{b zjTlS+>h)!JifEH%?Sz%1$;@t}+;-vYh!d(tPKvvU=<>1kL?62l=xP6wRmEiG(8Z>3 z8^h8j^~RUlv-UEwyz%V%(naYx*=N7me7BC5mOu|4BmwVgL~%JMSbIYj)fpZ1tUO5q zQYmH-=u^)4&D}x>ecWgz{1VM%DKxM}|4jkIo;IOO{=5Ck9v*wIm=YxA`w95D8Y#P-h1dWmw|G}OwyAkH5M;Q? zm(~_8Oi|?L{I`ZQMY`%PiMTL$N&awZHzu`X2@&fqkYhy)XZ--z1fpz%Pz_0y&N^>_ zB6ycScr@4IH5pKYLQO2tSc<*_)GC1Bi!*>8|Fbg57I(Qn+rLYEv#K$}eR{M)sue8* zn7qcm`$L>}(YtK3Ga--9A&-u(M)`>lj`t4K<^17t!W9)JoKH;EIc6$p*{ebYm zrqH}nIBkua>M-MmhcO>T|Niownt5{D0}a<5?6sy{sy%qAY8y27h9UDu<(q4;w|b&U zh5N-dB_c!Q`*z7G6Mc})RLPj8p% zZ7qdYWC)Vlj81~Df0ZMMEFx07lBcF^u zRZ%+Z+~+9{bp$y+lsBdU4MLFZvV)&^mb|sLJJ`|@0K8BgeW^-Q;qI_L&p^_QWAL?U zhc&b7GRizRAWn;HqnyEv1R@Ig?lWCfjm|kxyL4#F%j};scd$k7n7rC};WU@W1h#~vIAcgY*QEN2neET5&iT>rUWwgTxU_gAScv4C4tl2jMz+QE>0!%2f26X{_ZL(Z*R zH42m!@!gb|-YSwc6l_M&t15(nnM>c-6E{E%)D+f)U87O^oqTV+QTvpL+)-_Wo(XWSb7}!Fo3y#yr-*qai_Ay zum4OwG2hg$WK$2oA%;|p9~n-0k6M04FBAP`)vE#)YIA$cDdr?Mc2*qH-c30|CMMA& z?!zatGCUfNlpA=L8(g2Mn%gYyrbdg@eRl9+^&0kLR?b^||GSuUKpkpv^_9(U6h6h5 zrS16Acnusk5BKkDxc)O&Q-vUWP0Kzj10rj798%u5KbTOcBpA|Xc;5)(2jWt5n*USg;+?jGT6Z! z*zN!?oPgb=qI$64vKfAXE-?$Mv4zH`JdMGyh7T*1w30IY()f!Zx1~_rP2*DAsPJ~S zkB!b7{wtE~3{DeX#w+Yhz~;2p!R2-AxYq222Asecje)s_f0p25f8Om>_m&!(*cT#9HRTs^rd*Y5uV0 z@GT|0BShAek>3%K)@>!4LLHM`C~feT^%jt#?pOoL^@ z@!xfG3?ohh&{3o_cs1>&R2FA`+}0Wpslkc94_L7bjyqB@waR*{tUUCKR|C<&K_pE^|C`+ zQUwe9m-pfwAj1IENoy;3&#%%d`Q32w+F7}4&K_?i9w-B)t@_@3>N9{$NjUXPTNuvNad)bMV zU}@a*bG^T(FFiskAQ}nVcYIk1@T&SxxvM9q%Dl*$9882+m*jPeugR(rKKr!e*Nq3u z6-aBZibuYz!P&_uRLog6P=%yZiJc+$UUPH`)Y5RXMvWeoD}Ps&d-1zh4zxNT?7tV`S>fEU9J#nZ;3&Z@L+^W*-@a{dHQ>ep+t_7>eA z{?Gmgc)pbE(U&T(lVQkvAySF8-N-Nm2Y-i6Qd{I9r9zvG$;NN#MI2I12b247sGhh^y?NebBC2^@?_qbV@`=HA5NRJ8Ohk|w{n6=O z7o}_?<>}`jv(|eZx`vIdGkgdn$^zC?LzrM#6?fCwq+a8YDq?;1kG!2L1KBb5Bm7bS z(!aTqH-lXJOiO&?$G?B$G67NSqw&YXE>WLD75?$FyZVIoq5kkKR4>8SoT!0t&5i+Fb>CLB(=6H3TgqrDeU)K>orLV}aOYaaZ8EHG%PG}yNR5b%Mu z;RZ6y{1r*$THRaN^CrZ23U78CHX}C$k)GJ2GUOYdD$%EuQ14V4y+|D;I2m?0hRgZq z9EK%Q^?E<5y0HJ;3z&+!7i1~7co=^v2i6(ZDW-yl3RUc6rK!&Sp#W)k{{v(n{)%KJ;>Pb}fh%2eaB1K%Q#J1nwxe zP#MA6BCP|5YlwT&dW<846fy256!#W%z-1<>-`ACNVVjq*%e5k%4*I9{S#rCP6eVCr z4HyBDmG&6{996Xr_6+C}fzqNh-I*`WKAUt732FpmL9@t>cOWcGLDt`bL9`MJqEeh2gb1w;F}bI;BRAaOGLq5niXdj=F_(~~MBR%fL!eRuG^D^egiyJ$ zZtDhpE^~r3lao%OoXQ|bsBzwx}N)9T`)4Ka0k@j?g84C#m z)#8YOQJ#VEOl)Pu60AowF7~1+k^u*8mf9o57v;he=qytBjD4#4np;dq!Vw%3q!hCG zb&sB?RY=}ATEO+$6h>Sri^iNLTI5`hx2+dG2|!FfNXeM8r)_x#c-{6uIZ1I%+LHN^ z$g1z(B5rme17>N$R16mb<*nxm0UJ;mA6xd~n28>j>v{gtKVl%kpvAF1JljwBT4WD6 z&N!#8kQ)l!mK*owIu2kVY>NLs|GkaV+c^-R<+65rwJ1>23iKDFft5ZUo7rsHS5NYzMiL6Xzu3-`bk*dZ~u>~w~uFffB(QGosv!`M4b*fDJkL{ zl@sQsI!difZf4}}M5PpA7-s0k$xWM}K&9dA;7(^}L?X=k>ZW)EO&0;EnV;CW{iZgTj_%jj8MivgH%UmQMg% zfR=-1hS10z)RlcpSHkNq>o50o^@&;^M8X403Ix9=jr9*T0j;@p^dd#iFw=KjV;*siwz!i{!)m#rV!v*FIibHEtzU(iQ@=UBDxR_%AC zTsxIZM>NGtQiwY#$)nOwrTuWcbGHAgXy3Dz8}ulE-!B&P?zZ z1trD+(~qPS^k=z#Tq>7>+JC*+KMX8+DH34?OReC?X;*@{{=n18O_V&p4#ovliFWS^ zMs3EipVG7dcPHKaUyJf2xx?y$7ZMgGh;W4?l9RTrg_$PQJ`R zcgoamNOc4J5?oS|x?ql`M#_oJ|`Cw^hzRpj3 zSER|kFBeU3RGEiwa#?rCD{j8Y-Cfk6W+_(b`uh4FFl>rOMlR42a=NZ0X)k)Oq6rDc zS=9mfcdH{GtsBJ#j5yxJXu0J_`>b{DfmZa;UWbJB&_3HrOk~1 z6zO09-KUKUMVgoP!ky|}sS@AYs&6V{*Md-)#o8XaeyDbB=6W5Z*2rRIrAwLS_oVgG zCHvcgI*wdli3->-mmi>}t$3;VF!7R^T16|R^4zoiqb=hJehc@%DVX)lp@sR@py1E9 z7EYp&isi43?b;UW6Q>_p-&S#xZp=5MtpC{SG5aaHpe4^^;KF#Si!~mqx7D1}c^mxB zS?FFcOe~dtE6RJTtBj+a{WWe`q;!*G_%ed|6+BYc>w*p)Y7YflXd*#Bnx`!)rfoafjQ$9jD# zvsk9}_w_%1&}yz!9r#njHEPXG+1ZQKe2%Sh&qwt&^;`9_CugZK5qN3sf`lz&Rr+#z zRnE-@^N50HJqZ4geMnH77&@A6uYL!A)*t41S#--YZ{0k zC8S2OT{vYoNBqk!4u{5BdIc-muza|*=-n?o3Ap{>xUDS7vW$?*_Rxu+^eO!(v=E~5 zP2?w4WrCg``^LTGf$1B#X-NT+MnIfZ8Mk*k!0x(vC{iNXH_$oR;(o_g1vTiw_=ib^ z<=MvVMe{C4hV_?m5{X;TAVsri93HcTYztF zN3iiJHK-5#*YG(h+6}cd-D{>EY(`vwVA)&NBV!Y&hZuwHOvdn86;)NyhG^817d@L@ftK zs)l62j&~&xQ`Zy{2Q=QbX#3Oe7aM*)ksqeAnRZVz;(t>nTHODzadBZtwVi}nvzQa$ z7MS33p|)@MjI!Le|m}j|q$AAp=Ve^4wxs zLC)iJmT;RmqBu9`H674%A=Z&hzT*eb`{3p4gA7uF$^X!y1LJ}@ZXHzT0V`ZmiygkcH*|pg?L_L z9;#E;x%XbCTuc03(BUue1Rh>U+qXR|@M>94?+lrjg{kW_ecU=@N!!7MlMNJYkTu^< zjxN=_rjkPwgWHy+!CIv^0~%KfMTIn<#_d{8n`T#i{>ks(OQ_Yl#dk0rG}DmbH;_81 zgKM4U*q8iG`D?vf82J=}^hHNSKW7#J(@sw}?!O?3V&RPDK=&F!p>4CEv1m8{oL-Nt zrkvLq;|>T&pu&WBQP@}4u+}fnBAk_vU`4%hr#r5Nlm%@XQk7PB?_o+JIwV*JZx)5Oh!{vJghSj^IJwmL7L@fGfxbTRLHW$(42@N~U2(&LP_)Tx--_TgRfoYgcTD9>O&b|>m=1{o|u zU3^S(Jef$GDrR^qBdc|U!jbdyx6j@>pg#xWPR9h_39)hZuD@6^v$9VNPn5Kz4qKL? z#CgMyse{S|jM7hUQ;DJ3=cwRqX;EtBQ)X?I(S!D)+lBY+XIezC zLn_#=|VRn&t$Bc?(6`}wW2rHtXEjmVa|@L1ycCTueSxPlOl(H3&Nf!CEx zuMZQw@B>h+#EP4iPblhm4KyNd_%O+26M_Kj{{x5&fp$a*w+B}&wpn|ZIw220<{CH6%oW!bv zgyBN;q_QLj^+D3rVYS^}Ep9AYhhfGU-FB*iGs*KYbeLWVJ+AgcEXyPuBtevY zU?6!AGd5PK=OTc{z_AjM&h4dg@ zH0;;75kV2w0oh{R*8lK{t9rJR6s)+TURG*Hw=j-(1zkK&Q^-uNbth9lp{&N`?*vw!@ z5#|zynZ-Z}i}I$yy%fA}SdN^T7M{VR-h{Lk&%(-Z_A1wkDNPoqpkFv4Rqh2O*@Jn3l zEN2;A#+Be1Dx#m{tNSyYtyXRv83F1MrAOiwZK=P(KHm~T>w_*NRC5=1veG#H=fWtg z!Eb6UL%(flw}6LtwSj^KoY}t>_=oT_~F4dB%7%x)*&_p~NTM zIcf8AKw$C>*7n_9#WI#Waa)>J=FXo-R=@nQ`kx6+t+~TV-YbP+>5K4XoZ(EyvO*oT zCWTpghjJ5AO`>~mOf2-L!P{W0H$}G46~9`T-sYj^;wy?};$huRy5re4N>#y(Q6!=X zyIzq}67=Bjwhl&aH+B!q=C3ceBqKz2A3h`Tam1bB1W_`>xg{pPx2MU)IpkiZkYQ_*J1UhD|iE>{F{G7_be9WpA#5jjmv9%7RAzV)>qP+wLoM z&0E?~Wllv_N$c?wH<1YDR)klft=qMbT4tkGpQe-%bzW`PdKhOf%J`JNXl?s1PDEzc z%pK(<&&vP$#u2^6azl!UV23- z#d_5lW*shfAu>R})cW3=w)14a4_K|=D&qU`$etq1XP3kjUiU;GOwW=%o-2g+DL~fT zUOs&iC*YSNWR4E6)a!RA&cJlMm{fDumS3NM`M3y1oS+`|+0|M=g!S%z^b`A9{{$%5 z=6KwAn#|Nsk|x^-L&4rVGGND+lWk<;Nxp9jrJeZpJMa9q8PuOGS@9ot4y}F}sDU7x zo|dYb*St?>rK`{nBiCy-X=;7;Y|N9JmAbAhl0{_oih6~%@)oCNsQDRcsbs}W&4^}E ziGj57TC~U`PkL|X(HEy~x;aioz3III+qiV1aCg|isV2gHtwqNK#zfR^ZGnCZ!lHC< zmBM`&p;@>zzb%T0`CQl=HYxN^7!R?Waun1XuE#pyNN#Z!Fg~YDSMZFk5W2248fKbD z7XHT086)id%IJbIO&G(ZU^)7PnV4&#;IPqclE$TUm_#h>G&H&?oH1V2CV4qQPL!EH zxJGpAGp|QK9(u$OnC3)Y*a2=j{D;M3osMhcJW68#8lcJ|HGsueXRP#pAL{2*E=ozW z8^LX#qztvp>k5?IS|SXN!NMC1D?*}_BcU}BIOTX#!7~c(*=yX#7l`?!Cb8wTL~{CG zSk4>>^D{K34}1%>v)O5EAm_c6JgoyhJEg?spc=v)-w^D?ZC<4*hbn_E0kVyGnR4MMG`;i5gA_yX6MvKxjrJh4~P%c>4@oZVK!5a?SE9HGNcPzPl*wE z(D8aqaAy=ILJS=e|}?2ioAE!dk;uoW|BXrU0iTGk)F{|y-6Q;1|mtj#E0zP=C~%*T7} z4E5c5r_(*~`wW?#Wnu-wNcKq}Z@z5D%ZEuyQY{tHGA_ht%*#}6yE|(1=20fFXroor zkvn)o2Ui{))x0gKl`KtLSeS>^r}K6L_nB(^F{FK&Sl6}aQ8571mL9ZUKFCQ+n!qJ~ zCeca#8_skalkN$gmA;0T-)e@eBQ804AT3N_F~eK);4i1Btty6>TttG56g z><%1XXYNhplV;&ITXBDiq~z10Cw%7l=nCL14QDgOXU%px5NEmiIZKvL6EZMd6fTuk`W#mG=)BoC@VJ}Ww z!5_|uJiC0y@U-O(KMT5lS-^R#QS!Xqals#TCJl=nkO1dXlV8!Nr>0zYS~^(ycc&!U zw4B(ffa)3U#`T713%U|blpGY_wi8wq8#MEW$YWIs4J!JW=khDo6kj_hRm3@r*>Xm&$a=_OT%we93 znd}bzZ{6dNJ=km>eYzOkd&MMiR4@Y?8)*|u8w28!csn-3%qipynKS5Ky@3OIU(p%3 zw_>L2W6fYFN3v(oX@t%M83`klv;H9n^f5;XlU+G2Wqi1BtlIc$E20WMEQp(5!rmqI zKt=3AC39Zf#-9Iaux(bVWVd||NmVkx{xEoKPo(cSzd6HfR)hJ6_cdr3A@UI*fq)i| zzwpqkK?8c7hc3`sLs5%oo+MH@5+-^<3g&?m0G==6$EU7;Z&6*e3?@QdD8@C%rr zEd>^h$lsYzFo-7^_+p<-E)}fvp-+l@pfJct|C_o3&g7K;o-GYRlKbnZ9mlT1aNYek zbcY4w{t_Pn5!Wh4pJ_|7KG$eqU|lcGuKqFNG5h!EDv|XWy*Rubv#eO$vP)jWdn$Ip z-N7ysYujgF#uX+NZ%dyPc!Bph9B~OlHfULdRl&asnnx`yT;1ze+9FnEwtNqO7dG8? z)OS$xevsDZP^QJD_s}NYJ@Cacb7}4^1&BQMK41HdC6eWFVn_*U~x{~VFP5O zKjLFcSRxHi?MDgSTXXn{`;*3V>vWD-ZcAcJMmas@%E&1R;Xps>&i0#6f=tmb@VZTv zaAlOgsT_apSaxuJv{eG68LHfXc^pj1;M^z{E1mS`s_M5d=GU;*F+1|x5tHq1HUH2K(}x!=6$OVQ;(d&Ab##Bua=KF#~TK$57-Pg&XWz-(Nr z01A?GEZAOI=O@i!C|gXL#jIDbSlQ;C7IyM}SbqL6UEw1V|2QEpuhH>AylW#esqIrk z%n$&q)KA#eyES88CRhBy4w<==$;**rcP@Upv0gzn(Vw%lMK)!u!HiVt9JOF0=o85l z=C=jmaYo=^aIv7;xYm!pbopvZX#v5wteWBLI7MR*Ke%~@zL-`A zcE;JBcrz?CS%EHdfPn`M+A1{!azyf;p?^G^TCKRtp30C`#^rfM1insgmSgY;b68w5 zjRlAlnCl^4c|4z-rGhwm4g?Hs1=Al~H;SYoygw*jAM&G=Qi=GCRusn)_Ti)(^ zIu&4gbNJ-)PxkgRt3J1Bu*L07ANUy-|JeVvUZVWUPzfIiL-PI*T*yMm(z1uQzqW`LwF$rLB9uPzi=iDc!}+=MIW1C!%kp}SpS+QoJGj5p@~S?jW|I`qIuYzIP-N= z3s4$zBaH9vJnTR3U+_7ZtM3ti=N0Oqp+~}m(|m@*EIRwPvd!>b zWBe$rW{#9J3gg|6x_(@S+#gs{o_-B~?ANvo;Kg`3nRhqp>5&*jz3J1`;h`IwhY!Vd z3J^%2sj+uTHid?MXTA!39cV-6N;KfVe9>pH^xJ|aL1-!NbwAlRDw&=K$dKzUW0arW_1;ASGCt!_rHYDLRs~qbLs}er`t|Ef zK3K`4sIdL2?KP5e!)?Y{+IIb`Ss&MiK=zIZf~$vIP0Nv~@tb)$%;T zywza9dPhe4fK()gUeG!tjc2^hZ|jeyMK}HG=jem^axjrRC6yyRZ}BnF9caVR_+ZE1 zwn#!+*J$fE#hPbqOmT(!Yg=60p2!8AJ7_jze8vy%E3($1*JP#~v|quLpz@!<{z8)vh<4iOBETM5Vp!4Ob27ejN3uf&c)9lS_vv1{xmETku3x64BSkVO$F>I#!%1|PX)po7vPYNLtM?z&a zQrnx`)ltQ1iM6Q(i2aElBcBG#K!u6asa?A*-+4Giq6!X#l?&lY*#5W&Fue`z$meD= z?tSIB9QY{70gVlEY>~Q%-E+9GY?-o&v{}TrXHxKsT@i>Ut~4>WKvYAf7<{&j^W;V< zlpotfmU@20U8dlCQ z-m3YQUj8(B3cP%GTL$aJC0b)kM40ge_ScL^Pi4;C*E_;>8Qq8N`BNp2{O`bz` z(Usl{#p}-x9{h+&c1!%?6la42rNqF_M~mJx`tOP02nVdjrun;I2S-(Fv3-6 z^-9=<+~qs)VFDlFqx+Rur!!L$lWqoc9A!M~lhA9X0&I50YNr<_9u|e&GpEJJLQI}V z9vdGCjk^!QvK`X}&J6gwVR1M}cuS>1m z=;_&8PYa-(if8*?e^R85PvElW#j5TL>gCS>j40GdU>)mj4E|s5dpJi9azXR-*f$J8=o_tpA+kr!rL8J z3gmgDf8OTL_3poK)zn&xn*fph%lH4C5Br^M`}y~w%tl^|)iEkyI1??=uV?%TJ`q}T zfkgM!R&hxm=&@93@SNC6x2^udmO*A`jbVF;v7=rye}G=a3OCSuD4)S^*&5y$2bvYo<8Y-YiIFg(@3~~qtZlph$fo3cw zXwZ?hlM{|htKH_d2!ZDLehm(BhMj2}fSJgZRKxdZhgm`qrX1K{l=Mg^mgTk)knmn! z(FVSuP!p)9QjoV^ljuH+?x{0_0k)SViw=@Bpq-s;R=Pq+I#(xs(7jl`OZ;w*>ilEL z)9^%*Nn~k8Vb15MlEV`B3Hpzy!g5eA+fikG2r*eI>XJIItZzT2jhUOiF_tDIVO5knY@>VE$ zm!R*%Eab!LVG-!Et{6p%7k8%Sr}RVu@#%Q=)Ob&8eXI+lHe>2JFhQ)5SIq%(FoLr~ z6N#1s<2Kr?g|{q5gpIPAQ9DN^?m!(K2NhC&LRx;Z+s=Zvr`b~t`a*J%i7voj_|n7& ze~XQc{Ekvi-IUx~1b?Qjx7@IIEI{`(-Cm?~NZ{jQ&9#1dI^Wx_8(wmvv~85b2?^4D zu$bb2Kq>-Da^MYTo}_ZYPZGkfx{BqD=fVbr_EZ<9V;jr@k`yjT--jy{q^!Na#+Zrp zR#E-(V(|*(c)x`1tM|SC_**!`F8D~x*4GuSxQEETqbvIL)c$Qxfm$KxFc8Om;(0&; zO#a_Tg}vtf5BaIpTFMC_t^A6c`6LNf9z2$A?}|g%M}C;R_Snk^cWhxFR}y4VOnUYJ z_pA@0w>CaFD!3%56kd8tOx|z5@}U3Ith<=bH0>qk%6tW=$vaToGl{O`z+`R%5PQmhNMSU%nlTj|0mfTVN2n zhv-^GsOcg`4~qRQY#pE46!^xljNJ=Y>d~lrSGr9xg<98##+%zVY?kM$3eZWO4Yo$W((0pwUNxb{$~71f4+oj zDOOH2Gnd=t-n$cdgo9swZ&S7*4<1_yM4(DvPXxZ)hO|oiCtYaxuh2E~ZqGMqoMeFJ z|9g&HA4*FXmXO#AL`4_E@A7BDHwCyFH`E$d<3%(7KyGU*8aH8w-*9M7b%pulctzcT z$Mm{d6nK?z0?&Y~Hj(-L^Bl)nkOxA>ru*p52}|kIWV6RFq=}5&Z$fEOkmWl>o)sQm zFmBR{`CFv(6_eC4#U!4eO!q%JxwAxmzjht)&&COU5g>h6y)OV@Zuh6sM*XF2KNZ?^ zR@5Wtau26j&Uj`T>QI%bprJP|V}q~eQ(|gv9x4A?)acHCAw)1be)Jkkj%iG{1kIM8 z;4xcZsKpM@nT-l&1pXuC)R8b|v80*fyt{1W{PiPjtFh1~Lc)^uX`LzXE@H{wV6C?T z{~PI(cLstiYZJp+L+8Q<7&b8urzV9Y@vsYmi96=m^3oh9alHwC_YPN=yTdGx6|*V_ zX>6S^-G~9T;FT^$!bbAKrCI1wtOc1V<)tR|Z-m_icp>@iH^LA@*B4ZcB$)809~m^^a)ZMXH7V(5=rPH~O!78uS~_T$9X zc7`=KqoKM}24&2wvrSBn5vXc1!%P_hGf!)7#XFc{b_!u{jk{pcBk{|-wOb zohuC_()lPcY>X?Q=M#G%>^Y_UbndcA24>aIjP9nmPi8xFWIu%i(Hz$ZCD8vLto3iW zWH$2&RE2_((}kV1Vz9e7cml%;)|Dw8Q#wxFG-DaoBkfTu@0(M<6FYWn-c3)i7_x>5 zU4P39@I6xJWXU^_*Xu~#I@mRzT4Crv#$INkun{S4l{zSaK@wazb=HNC_v&_u8?nA8 zvwHBk%ICeHj_@YLl;>Cs2^RH^5H>Gj4V6(I4NG_Mh>j(fPmGYo?;mAL9e+b$w~}V^ zq=0q66y_wx?hyye2Nlb?@APA?pKQ_E&4{#5bWO`61fm0gYiXqEd@*MR|K1-BuBZdinHEC^WNg6|zp@HoB(mE5j1kXoC;^qS z5(Pk+CqF7IwIeL1%=D+>8wL-Kr-a7n-#~r`8*@)>+4Gvx%S1wLz;Egk!l~PEi~&B3 zE#usH(kMxSem!TEfRMzQz&96~hL1l-Na4~~(WIvU_tyc{99YxuscD+rk@@%&heliE zNC2-xOSBRUbiSfe2CGp-s1bf7q#xo|Ph zhTjzBjGT28Sr4OH5=ChP;!CE1TXM!}oweEuW^V5P1tQ0#mTYYEJb^t0LXCW2kId3Xy0*Fh4SFiE41r6Urq z2#@*`Kk*uDmD05C{8DLMhsWR*m;z2uJ%m^&-t|$FO+sBX89(ZO3}D29IWf&xEH(Sm zs9`HSQMi^!7UF~BfF-~E+t>S}U*|^+$H)d!OTD#h6k&EXxC;?M`olcIU;DDfU6BgI zqQIki>#leV)zgnqg&k-`dv4CU5SIx1i%V=QazyuGZq9v2Zbk*EI0C3t0X2^}Z9*uB zH?v-V>hPcS)zfks1_Z}P23C=&O>?_T4@}Q+rEiWti1EHtd^0a7Zju~q^Rb$}P|vYB z?#*J}F;J#(5bm5`7*#=;t1~Q_wdyh4iYRPFc8#Wf>*j_C26jSd+G3dJ|7f$s*^GnS z5I~Gfue*#p_HEz0u(8^VgG7H71UBd@zpx{tA_fJ;rSA`owiKrp?grEr=7ALAs2ubB zH~mk$%VH57hOGFE$xQ6J%oGTbHbX8pvWz!@e`_PmIKaNXz$(LO#a@RzWI#J};(gmp zNZkT-%rz7NWxo@M$RT8kX@_GwpG?(?c{b-=GT) z%3FXq`1Zn%9keXzmcmS)V`i8+e4z?LMS=&jyW0a6;x33V8H-}2s<)$r=M+ni=?^DCFJj4Q~tEhvo6UbiLuH`Y5R@-M3$o%mbJ9+jMeS0U4R4- z*oKoXq5*0XH~5W1k5&+;B7}!_Dca<@MPc2g88E!0eFl`P+yLavlvSrNo@MO}VBj%l z#jf&A=cAZ($@>e}x>7ZPJ=V1P+zo)(f#9JOIsYNWVd>-#e`^kkme=5Y9w=dKGc;86 zW+fzHI z{ma@iJ!uiPx0@8d69iL%!xRuf1U-?IEs2qB_cE@k4v;og(s1>X^+N)piO{fR3HH0l9hbn1;x;P$TU6nPw|yG(MA zwzvZ$s;rt+eh(0W$s+$NECKv0YABeUCV67;$zRjx8Ib7fx}YWfva`BZF@wnc5^{Bk zoH>3YCeD5G2FWtSiT6`F=~Aq)iihB=etgq`z`A< zT|Y%*U%R&Pe_cNVR;C(kk{0{I7e?y=j}no&m*Cdy#(H^X?9G6%@p%GB9AWr6Y}}CCs+Jt>?Km)d3*~^(8Z| zs}s!<0mWTf4=Hj}1|}pn5Jq{J0HRLsK7V3YKu!ho==ICsgr*D&L+^CSW3rhKOdLKw6v~iZ|J0B_zx?sGOa<9ILn!{{Q6htZMAlD(^Jm!eQM?K=jJSk-5kZEyg>l?nEJr`w*9i$psk&-@H zBjRPB{0c5BO8f!)$mHQr&#W+}4z9=Hqy@3-Z3h$%Rl?r4y=fUBPh0B6wktk#A4~xp z?QHS*Kt!de(eKgb_6-IeS;}%-B@;Q-3R7|oDuMmg0EZ#fa2Ss*s(N|NVd{u$tpBh) zu_s4vXPRy-WH^YWivXq9OWaN-T&9_)k^L4Uqds~0@?pdD2zW3wN)7IUF(m6tejU<1 zZuq`6zRPLMCiDvQ8Dc!4Df8zZSWX`z=bK^4r8*Nb_GOzHVHB3F5Pw2QjyBEx%%P^GJeV!K zxU#sbUOMA+3s8l@BWN)ArLP^#JZQ3rVjys(D>0kC=)5M@g+H<_3y8BI2tsYNYkFE$ zgra8;NILJtt%kN$^-y!l*-Ov=+OOW&t^g?}n}bgKQW(r}10$T%M~W2uk|7=7afDb( zs(1g7g(@o6VOA>h4?Ovb$01uI=8gL<&qZ)jwK7{aXux|4zF5vM*`}#cFCYKB9&mjr z-1;S*S7E&?S3Q+@Nz?;)un4qeC3miF^hd;1d(-qgvJ)Xn@B;*+f`G_ai;*_PyOP#* zdFYH8Szf)<22Urn!H8mVFgMDTNx`0(X3u)K>D}H2lXWjJr2{vrlpDk=&LuBlj>&|x zGK_NNk|%Pw>ej$?3EnaE`q?}86jHU@l;SD;1uQSQz59q5O9Cw91Oox51D~~rcnvs5 zKn|(es7%X;0=a3vRIK#1@k<7Fx&k2g%nw=;UCmc@$X#P{3aM{XuiY{J?+Aw@Rp0x@ zy3bupge#-GZogOnDO;JYxYC&oXc3VJz&*jFBS0T9gG0z?=VI3q=)<5AP9&R+c?${) z>i(-2zw!p&X3+BFr7nj^GW;TKvM!9bm>^S=L$;uiQygSU#URx{T4^s*pe#yZ8^?^= z*Q^q*7#5-gKCXE|+4mHndKP7P5V1HbIVppXi|~rpv)SWI{zdy<7PZcT>{RzQDy?j& zxgq(ee3_^w@CU6qYYqI1Szt-jUm3W^&wBqg9$fzgg1iri2Gm5BU`QU=g?oXC&8R4H ztBVe+Xl0;)QnA$DE+1Tyd-zPiWHXlYlS%2eF{@rer{`f=5X*2_=M1wzY2t;jNpv%U zTWs@UD_~lE2HlE9Ue8AHs!qQ-L z0U$Pnc`7s3dhFj_jfj*mdaHiiA6U zv%Zga{bbZ~RDI_u%ZgX(f0WbEe+JgVsd7ZK&*>fB*X6e8Z~Uw(s{5lg48w=Uj!Y3_JXy z0}L@)2J4MMy#9^I-?t1Mr=$|+odwBgLBi>hK09MeE;+K+ec2i-4-4tdMq=)^{q=!- z%FdNDwY9POtmCX1#(-xRsN<)qoMmY&tz_kV)K!ZG#8H$ihLE*ZU&e*;t6Kf$?<2y` z4zFat=GObLsKs7lSZ}C#4Pab~e)ei@rb1&M={VIHi25vanmRDz`>YSe4mXF`yS53sUC(Kp{^#TFiemWMl2bW)D@4Zu`Wh3X}YW+idxW!%QBRUH;FM<7Vhw= zaA{<{x~-)5zTy+5*III>W6CcRCh*J4@k_I{Z34<#NyyzYR~DX21|+&$->e%vYWJl- z+UM}dVLnV-$qR-5GXRh*0g9Mb8u2nMnwAS-rI`$8*J~#i0I6Ntkm*vRM{-xxkkh+!aD3u;@jL5Y;Q1}Gw|2F`ZQ#F_88%sqa}Wct z4wwsyn`&#%((?$^f5hp$p7VE|@qUN3D@+DY_fZT)=nV9ixW+wp67X24V-`AB?$`r< zhS)m??O!IZ84yb2^={x1=Ra^Amtp_}l%Rh=AEQ<2Lr^=ziWsmZ%ORz=?K{qUi zZ+PA1TGi!wW(od*3Ohpuqb2(dor`Nm+FL;HueuHAJU5I)c>n>ei{>~>E(%u=+ATpH zXLJ{R6>+kZ>X~X)2NZJNULcgxmZWh7=L;ceR=?pB7(%m(`ZCYnyUwC4{Oi^OE8GJ}p%ul#c1fQ6SNTJdP(v1dZyc$;d zF@kdBg971rjrcukBc>>9z^;Xqz`Dk2IXdND%~KGD#^#p*&sVIi^5h1qm_s~J9B!;x z7HCl!u5$gTD;2j~SR&tV;atmJQcaf*KqW7h-dP_(7MlYzEQT}q!Ii7Z_pJvPvH>he zy8#^2b5zKnYo2`Fzk>{dg|EF5M%no!PsvdL8(_0(zHPG@hm0x~6+G!`Bn3w+e&LL-QsPf~3UpQPXi$ zA~t8|r|#KX@7TjO4~k-#B@GL_oJTP`jxp)O)Lh8t(8L$k*HiD+qxawcP0q{vtlFaYWIpHvZiI^k2~d#a(p(mXvzaK(8GRH#UHZXs-w89sFm zZ8YG5?r{fBGVx)7es^*C?S*hQsa@oy>)O*ziEb(?$@1L0QG0M#&EmC_HcE?<5 zIWe|xFzXk?>uF{J<7cf1NcX{zw1%a00`CV|(Eh;W_(^1l&Bu}7DAncgDuTl98ej?F zxHy%*4a8mfn6ZMi209ci9iUWe;Z9yI`p+mIy8{qd<3|C*mUk_$IU+{s@SB35BE*fy zTTb+%ED{J2fZJBc@fv5~d@aOYtf){<2SQwZCBuDlyt&Z*MXtazxPCDC!4qJ=Y@*~i zU|(X2V~l0h};*h62t&aves6BT+=f6AL_jgxJhPjw2vO22Rv;i^84+ID7BSU??ST-u);xO zk_6{T>>2D$S3u%7HPN2|3Q3fBF`0>Y0G>z)_q9Cg$!6D!Or#!bn!rzpC1gBLHwyBZ zri~C`0`QB7@Rj!huFb|+$?w3}7Wj*CcB`uKwC|P3d(RF2mbUIJKz1iw&y3$qxWe5V z<)8O!vu6g^+BQkhLE;X-xt1gJ)%ydl=s0`VXFzQ=BoMnVCBLKm9rrJFfykTj((hc4Ap+uJ-`p?7^q8 zdRFWr=&G>p!2CqU(e8u?C&6=8qO{3PTt=dJX>Zi>t~IpWmbcqt0r74t)c8+uHA5s+ zCHAtlGlea?>D}G;&~1@rrLT=TQG$bj5JLClIf?DV-DXo0+m^kGDhDhFl)PN6d_LWD{-U2r-?u1ukyBbm(7 zz1{RDIcST?F=v55Hn^Aa2wvAZ}Ek6cq8(V|ZiF&#xa_<5$KQ8Hr`j@4ID{JwUq^&lfp$J#cGHUKt8q#YwgJ3#{gB~lS3qmo%Ewg zILl@uKK($vGw0gd^Y{Kr&44xj9z3t)yYhkQCGL!v<7ODyqE-2J^ZN_=t$Y+ueK8jx zt)FYyw3m1OUqQvhvVFGk5-mDxxDmGZE7V1)>R=OtzPt~vm*pyn#eK9u$(eW5gINwkjDt+mySB6CRFtnTiPWP_JN_ z(m}z6O0ISTLj=Y@_|fdq3pU<l!y97H~>}ylr&wOYMrZuck#SANTCw zsUEWDZN1pj(CDT9%Vuc7mF+o=K^w}OW9+~fCw1%{99H%;Pm|;IG~j8&lmG0hO@6?n z-bYwx2Oe(L>#ssE8zr&k6yQVNI$45_aN3gIA>TEiy7yusB_^dl^9mwymLM;mc7MSh z*7X+$bnx)6LFMiX&ba=jkem_3fsn0tIp6B9jXz=Vv3jnhhjKVM^iFJ5OSJ3JyL2*%o&DDx)StrmEZeFzwCEUprM4tb_Ldt zvPbCNl7gBULHwq{XlGax^JDiJ%FONkt8UTI=_};Dm*-Y#83Qn0YwQ0^XjeXQWPs|= zZ-9lPjHfU~v_>H@Lx*mMLqnUy*yo&zVjBJ-3Q_u)^~@z@J04XS8>Z_4Y@^mEW3Sk6 z2Q>~1*L?^VEI8S<#ZU6OcZDRSi^03X-fa7OK+tEtUJ>YN(g*qMCxw-$(t-yh(St`L z1utOH79U9Kiyf)`mnG~zz-9LebD9fk+O$UwGhfLq5AFV#|7Mslo?Y+HBllCx7>>VN z1e1M!NSXN{BplnRlxJTuV?d*u@&TiLTF17X4807)?`!7tfm^sy!>T<$jM;AX{vSo? z59}{{=ey0a;ZDoZGa>T%h4lPZQcnU3=B@jd?ii{&L=dOK2GjrIQp@0@URPMXS0PUy^bUF%SP90LJA!dnLsFb3ZncKGRHHtdg zp)!jSHnt>}8Cw@j#448CW}3@#+stJ%Gk%|`^Z5M%r!(8<^ZvXquh;W^mH4ugcnH=O ztIITu+*46@ab)qqI|ggLDbwlhdLOyCjEiT43;=;cHeesa{Cu{Q~2l9s(k{h58%zr@kZ zZ(lg{pD?lkxO&yQ$T-RKG%oU35@|1In3Zy-IRVtgKG>R(ly-=ijo7+Gls738%gW{c zPE}8F1JQmV>F7(^UQHwFc3E_{#NOA5ytPQ&GDgb`O>}!n*_L|LScVSdhl!le zPp@J!QEnC&9DlfA@q`uXE`D(Hp)@F6K|ZubcR=)DI8mTC8g&cZijBl!n!_)72&glY zrox?7D|G@s@iYh9>t3|3(5{FAwk-t#->cR9M0}0 z)2a=7t(LqahPexSr0X0^1`c6Ea*XcDLh8KD)mc8wha>&|bf_BE_D){8BRmVJ{5j2cSOmFQ)RCk8x8;Zfie?czL8k_vbSaM-?BF)(;E=1|lES?7s89%awoHvhtv z(j|BTBiD#InYimPiXxgU4Z*?1uj+~i9S+<#C!QIv;1!M^xbdUTg_YWL@ru{CL8arp z)8=m@RkjxMKt0grO~}ub)y7%+vK}Z}$n+y$M5I+BvnJ(Oco&UXs}K&2?ubgR(#e(_cM6i$*m2tV*4S?WGHDD$d%`$y%Gdps(Ba%<*m6v(o* zc}-8>=Y1eteFFto--_mj=*c-q1QJ6YVRBBzr9cP%oLLScqy)4Qb?ldX>&MeAk`<|= z*=a+v(C)nVX=?ccxVmL(hWy?ss$DXPKq}l@KYRKNU<)VF-6!=6uFmqW{fKycb+l2> z*?{gS!AQRNfn3V9gC-sm^ccx!Ry< zPpWMDF}V6B3tDea%;};sdj`s}vU}Wj$d0{B|TW2^`XFN zCYe`ga<%f~N|j@}J*&=EIv;Ku$wt|nh@YL2M5v$s`Q=A#{UXtZ$;$s4^jW=cxM`?` zUsOXw>kS+}=GF7Wnw|_EPm@*@DgzmXbxuNADsJFT+KFJc(G)F$eIvm4QTD-Gv}2XM z**vqN%DvXyQlmC2N!jLXh(QYZyXva*5{1-MiD-2OG7w%#Y{=s#*d0d6b0H&tmbd)j zaHrs!k$M7e5$$JJmG;y?X7XLXni&yE(?AM(#-ueZ`r&>^W%5v5#nzL<)rf$Kqjl5d zJaobNhj>~3&}k17k{t=RG#a%$dNubh~=oa z*v+>p7d}7qoq==)a~_*25(%Rtg6aFLkH7^QWX1qv{Va;(D9urkzIs+FND>Gu*7prA zYz@XIqjM#(7B>-RauLhKux+vJL?vAut=mPSOAnQe?->K7dl%(qA3VvLCp_uY5MVrV z;v6uMJ&Eq(mQ$HE!&Pa@tWWbTlG;p5sYG(2>wJLB7U;SQGZ`kt%8^Po$qiV2H-r|k z-|#0d;H+cxcNQ7pJ_J)3QGl4n#wyKx(9|B8^Z)RxwQv-j!0)ulmD|Q|^%(a?e*SiX zYpVEc>_>#%2+L8SdI0af%`qIffaO}>bxav(1;f_UoafCufR7p2;!CJ!eR))iUCv{j$1b$oH{}+D88Rxwz%HC3 zuf#rike*=DDv7zi?Y*^um&0X5mI8K@;iW5~s#Oj^hr>IBlL&ItA9F0-*W?_+pLfjA zGy=1)y7aP98!5m!MQP<$(>UxkZT8mG+h*mLKM+YfD^TU0-3l5Dn*U0)3)s>wS}z{X zJ0Aa8w~mHkJFU4f1J{B7j8Zwm?{I&gcP%ZHTJB*d%NnEqi|b*SDpq6|F60vylz*7C zx99OFFL=qymb}2cwA28LRP3fM=+o%JSm~^3y3tMY*3sHBj}uM2M}~KJ?}fCpo>vo< zE71^hmnwMWGP7ZHG|xRMubUk`RJE>`22;mrcIt+)VYlLk>74E}96*r)!?6EFrMUX7 z$umXXF_av!Ir4VWS2l*!_ht(MlGw0!*gW&Lcm&%$^21-LuN=lgt&&KK^q1aa+VU*& z#G0=YzT>yS1O7J{igktUfU<^5_fvfOJq6S8=S-wBlL^vsb_Dv;F^5l+n5sWjz$Q)_ z>2VknYDkr`j}op5^uDqA&9bGNEp|D0)58%k+1e(mb$Jyci^3vE`f;n%uNjlP7z$dL zk8}IxmJ?VoX{OZ~mY~dWp9=8gwg2e4q(Z7|#A(MY< zqtYj-qTi?0|42G(WGaWiSHC{K9PlXj>Rh4z!b;L(l+6)UbWd2HK?pdJ+L?<^Wu{l_ z{Eac7by!FX2>jZ;LT|~fId}>96IEH20GM%a77H>k>=|j_bk@VI3$U|kG z=4tYWldi?KfmrECpom-w-4Gxg>=0r?50TM`W^c2^Cl|qTy=l`4x%7Tf)xe~N%6rrN zIQoc$SuklXpCjpX^2-Q%L-M{dp;O?osiaX`DmI~eF7dMot_T2I_T~_qph}4vVXF>* z)!grIdh&rG>h}F=OL(!`I=qW}worYtK->Sv#WBRu#Y48qUMr}v!?bPh-cHE-mgTQQF@JXfE}$md8c`@n8)+bIbaWR?V~^xr zpj3_YF@4%+e>KLlrOC7&Gs;sA!^Er~NRqlyHeg^d8!=)w<6-I}@pOG=j6K(mV2<$d z7I+9fI*GMRm&ph?;iX3g;|wT+#CxUpgehc>Snp_MW(g;c1s|yZHB5vA3I&Nq6unZF zKV5JBxrotorc7os+aJn5-`FEH$%|?K5^_vKeyj9GbCM63ESqkL*`y%>!&;T*ykMGh zj)E87`QBV-(K@5!bltrC_BR!PdnZzHANeRPq=puNnyx!LHr7GmQe$Spkn&zZ^)W7< zlWJ5yIkSzQZrNkZ69ik$bB;&O1V||FaI8NaXI|+2IlJ=fz4_!yVv|V zDL_?4%MJ|M^vq-~G{?LqADzQ*WyZ`=R>aHQCjqjPDojrY5)po3-KNJ|?V=E>Z-v-g5UyGHsW}I~alKn013|GG7FJd2EX!D}bbvAOz+s-;m z>y&#F0?X7{T90fl8>5D=NSdY>Dm90`C(hxtlC_sj+M=X4e&#>P|JfL=ZziBANN*Zw z8YEe=tAsjX$h_}8zfE{aAY*BK#s-2%hKUQ8&m2LFbV*`O3A;Ux9*eNf>CihSXJhSa z&?x99Vnceo=vCw8B^GV09gV*@8Sl&w`7)fVr)08q%J;oIqPfC)N_Db&Bd;2M&?g-Ua2XBhRE z*X2fU(G-s~!YPokKG!L-A7vd=2*o#c+J8WAGC{!99vdmkN8Md_Nh*1Aj4p0K7ZDZI z?E1b)9A(=t{W*|%GcdH6Zp5d|2x~&C$PGPL0A}e~Uf& zk8|*JMIPt`>dBM&%l7iI@9Mr}yYsPEt<2a>8x7JhKbt9vM;@Nzglk76y>42~?)dTl zI=5NdZA*Ep)WVaDk6p5u*Fa)|up66<0vALV$k5EueOp#x5R@JV>1Lx3K{=)xc{YM6 z=O>}nE3HHF(eE6?M%m=blWC4%dG8i6Cxv4)P7C{q$69AOSGX1u40@w2kUuEI{l{z` zc|eXvVZFCtQH zN=Z%}o?$oYJuDQw6b4#xaqOpwLGGb+liN`_X9G=Yhq-l{1 zmeYQu3#I}4sqbq1^eS1UGG2rji_^T7xmEKTH*Exfhe6-wvV@?p3%WJK6kFG~-P>wC zdp-+Kav})WPlWFQ+OGG$HgAjDCnhWp`>^5jVvFJ>Sf(wrRg;gtfu;<@?(stAYaD1C8dH&_+Mmg444I6pD4V72RlNVJEmmYOFLu1?XBBi$fn>QPhZzf%MGV8Y#Z(+jS z;eNHSQS(4X>C18)CH2kQx?@wL9QP4FlQuKawdTlQ?=tvh=%jdCc|tPHF zUFHKfj|=!`(=;ta#>WpbHR+0w|CCWX8-7U2I5NPR349kB{N}`~H~d4(ezkxWSB{ct zWx0@iqqdOh^wA|k@fF%p36B=IHXmSIky2Ti@oJp!;%USlW=wXoUtzusGfR0^D8BQ zUzP?+_Wx;rsl3G-^e)D3Eb4115eId0KJWo*gjB-U%^h$~loZKQxksg7jWI6*krH&9 z7B~tVX9p70n**?82fFYX6QVL!7{-pQ|H9}xdHmS4L~RZ>fjAUoZfQZCOwQP8+AAJs<|VHZ%c+@5k$&$hTzc`CI$5kuB-IgI3O* zaFLs`vyfMV?qtc(PK1GqlhKj4O3^I!1bxK5zE1EA27lOWMy5ofEfqSiLEt*6X)%l!U z_Qez^w?^{CrzBsp?b-RM9k4mfWWm)kr!^LVk~o?Q{(8#FXiwy(6S3msk|Z8Zy3;?Z zRl;u3D>g~0?Nj*+Y)+j@O@7!K%a@cpKuNg*Vqqi{o8KgRzBwi(6cj-CbI&umOeqa3 zM7C>EE9)%zo9;Q4udmo!w!g2b=3^N2ZSSKVBxYNC5uga@#NF3En$;Ws3vah(eo&=} z2-}5HQc94+FzG#t`DJoCRBd|~FIE6)&19y>TA2d!8Rzzd0aS&3U{Yj%ftO?T;~mNe z?|Ti%G1@#4Sihv?)0{uj3U3xIedf{c7e!Nx4p^QT3?*H>&yYZ`Bo)8i#VzW|_c%dz zOy_3_(GE?um*}->V}u4FPhqdHR3N~@&^Ztv-s4iGao~mGY;SdQR%JylNd|0!+^iYC zdd$SYmz(c<@~05a`qJmiPjc!raEoznoi@$|ZTU8oTKiAyOX-Qbxb9sexro#Ri&|@L zQZ))18#b$spk2e-J*AyA?42D}n4OPlN)e;WAQp zb#yVK>JF#Yb3vZ21BawoJ{?swrOTU-@?MxT^6fj7_3Nqq|i`%|SASvA#lnG}5#6 z3nh3{uQfJa0>y^Ryl^gzaxs>pIV{vIzK#8t!0E_e{!W#IZ@KI(c|mbJOorg$ze}$! zv><~VP6~J4?2Ar(UArec?Xa+c^z5iZ`ef$jZYS=U(bkCREPAYfK!h*P0(d^yOUg|J z+(RP($N{^?omk=w|9m|{xjV&qi!4_1FXhy^FZle4Op;RBvTTp>FnX*f3hwvH=BLhX z&NaO zBz5Aa^8-~JM`v~d#Zn*Pn1gp2tUqj~RH>_<|9*O6-cPLQ*RWTRaIb7yF31i}x44yh zUih?dQ5zzn7emCWr61;RjJ_cBG%PAaf_8CK8W|{&{0=qe#%HVp&^oFu4$gt5y&ZG! zy(%Gs>WTJ(vVRD3UVNICXrCs=a@VhJK+l-Y^AAn#JU=b1O~~-zpu1>^wLm6nU@wpVV;`u< z_5=$1PJ1Nn4w}YBIRvuNXuKscC>)pn?^U%z*pB5Gs6DfPY2gVj{Pb6;HqJ|>u>u+=M=gu<} zZ@W>@2}cemVNxONNmIKqY*mb`q&Ta;a|mki3aH?jyd8)0U5uqdv928f@yAOu)|JA) z#sm`5QCY2^k%{}h?C4*_q>;Jsw97ft71ZykG;=sN*sU3>8JvoZ8@(k?4R!tzC zRY^$?)&U;QMXmb%ae1Q~SrUI^gdP1-(nl5AtY{1s?EOb7jM2wV1S%Go*e<=vR;Bu1 z(cki)J)10Rl@>r72WYrAv;~lM0NQJOXa2kOUt1%OetB9aT6jMX*bZCIc7TMt^p1-%2 zB7n?2QnW{M`SuvGk7aZ-Keat_gg!7Ea*I^yM{MBlbUavQ|HyD+%ihn)4e_o|7|@K( ztJIM#Ox&^-9KE*LqY`!CHla3xBjT0uHB=RoneC>b$58wSvPDW(Z1pEnil?GD`9Phc ze@t)oTax~@z`>=gXMG?8)y|-MOct;Ct#NbVx4+>#g-cJ^hakOi|I^q_;K^a6A~0hz z`WeR6F=9g?ajImyIl8PL*^EV8!EeH2r$;C4+goyR122FST^<)n$uYXs)a}SSSwkamGENJ1*OV=Ht0@)k-B>|bBiwuM zyn+IqcSz;gjhtU;tfLdD^`7?{U;g^oyf-@AY5f73JPPLMjouuYBvf8WJ8RDMjVNq zuM(|Wk^DFKzNJqMpYkOy#QXeoLn}6Js^{N^b0@++MC9+Cdwi8gnibI^)jKya(POlZ z2|u9{P^>|gHY97NPU6G(dr)MQ=6eSNQQc_v-+XAwv2ppe-l88tIz5JNo?V<(Ci!m! z0JYy257}=EQ|A{Z7~c|}@i`%J&`k}nj0e&vby0}+9*0r%u{mo~1-3QsrL5j6hh@a= zv&+>dST#s8vl>KY=)jgUD7~3*!RDdB=OvzcUhaX45?t*W6tg*P1;MC7W8fXJ=?_FZ zhLB&-`oX#Er&Vh$;bU2E|Fj2U7sM7O*qmUj*%k` z^h+$fU(mxjjOA0R*)Y>F_;6yz1{a=NGw-&;n_7R7-V|S)#uu4beDYlLJ%(c=g*+X! z|J-o?miVFDT7ZAkiECZ_E268WX1?n4#(Rjl?sI=vMZb}RdK6Kr#z=3VtTD?~iN<}8 zO=mprNr%f3g%V})!9B988d?S16g1r9vl1c^?PW4oUZe}kofu8l|D<)#e#jFvnj2U^`~cQZviVM3L)$6=HsU=@Ti=u!1>*~cM- zG!aVxP{hc54WfO+Jp0hH@yMK;0Vj^wm<*dxE9>^Es^ES)6&T*4P`#t#D&#cx?W2|>O@n{0?V%947N%E{wDvV*pQfWh|AN- zy=G@S5vx=lp{s@Pk0NKz>?$t);wfL3g^sK476gQnpLg4}yMPi$Vb7|^z81KRne-})bI zNpT7Pgj_coLwxL)DA=;uw5n0_ONMVNicKcSYWBh3DBT}UQW&jz_}%%eLeqitYs-vsps2~4H0<;7O%TPzvF=W3!3^yH6S|Qv-u}yR>Yk0h_9cM0hotd;v z$pT!g$5QNdR5@yXwn@>1T5Pt*F6NdGzm4R64RKa~c@8W!%jDahdA9b`%GYy%j_`~+ zJgY#E7mu)264AJSt_MZIJ(AWOTvL(oeWbw~e&IOciy%J=5>?=7CKAT}af)+;Re?^R zMGVBid0(DS@&A4W!+jrF@~Vw&8k&i>ZHXThH!9J99aP1BbIG*O_1wTAo+Jh{z}Qbj z5$E~hiw7bScPmQ|m0FCRi0ZDf6z1M591p7DlExW5+~M6@l>=w(SJjMwhI%<_*W&AF zqzzm>YyS&b0HeOGi%~fQubHKq@r2!~Y|!8iGo69CN68D|r*t9JOiK+^sn$ zoKg9SUt)Fe)!m5fgVXo1l*3A=Ej-yu+vyrdzr2^&O|!R;A};a38j!a$>QklqAySCXl$7LP4HFxUEG25F|IvTdX5Rf*=-mm4u4H#EtU^Q*GrBuja~pH$1G)vDJbrdJhL=3DXxKG4;+!fGQauBD$RaNPn6Nw*;bed-sF`Ro{O8}2ZARtQEeKk~=VQx2)(B#f4;SD|K&Gv9xImw!lb1zWyf-%_2T(2xH=J_6nt8&YNpmxYoz)Q! z^*&nzdZ1i&)M8YkW-0Eko^j7?J0a`xf0EW-ESOHeHxqWYi+dDtP3@FMPpvr?^X#7; z$FC9X%?nNQ$W4$`>}Bb%Js!u=p;ng;*HL(AwY6_&ZVeU3KG3+ z)-~+-t%MJ@v8=deSc24mDGk4b*G@QbSe@j=lfbC5mR}$4H4*aFWCp6H6^vk{4;r0O zpg^k7OvRt;{^U3^@GB@lnzf>SBQMyb>5CN;4S-d->a@3K}B z>_pZ_F-S1!JHkYAp1zd0`Vht#*G$3(mT5|sCxr;9V021M_n%(d>3kY5%Y?zfes z;gBBj1C3aiihN)=(XMCO%s>vTl*elCFcN=&U6p3#cVxKP#%nRxVvAste{#2mW<<2-Kita$`{We zflloAvD#lH8!)*^?NC;8`|T({-HPEf9BK26hvV+}=~42`n2FCJ6OThtls#o$Aro`f zccEOoaU(OR4EXTkvWF>Ndb7am2lhJpo3U0*|9sfwbPT9eg1ss-PyDf~zMu!QMs^|q zs;_++{#qP;k@tMVH8AccE11`DP_~%Pm=)vs+4iH13q1%}EI(FKl^_G-yN{o_eXZ-1 zKJ6M~yD><9%>$*GeQ={*9_bCF9Fn2-J&rQqXC2Q1z?VY%$(>%gAhc!&PY8rNB#zm( zPrX)YND9wFNlih`$&4zZaEn*1UQgb;0U0BxTCsFusR(nQf#L zu7!`_yP$8918|$XBslm2GLIsw$B2_alCGz6znWOq%nSQt{#^JRvDX0;$5QwnMURD# zEG>r>(U;EIMoyoj$HYkh1_C3m5A{JjM{@vA5H=O_8gF-lcoPyIRQp7t4 zH&ty=9j(UNRnS`d&If2zMRVDsYIl=mbt6F*J+!**d_Sb@P3vNi`OxZ%K7Qsz_F{-oR0&V6$&AsCD0gYoda8Z){qiGVpj zaGW0aRuxwbgA%44Sm?>=$)gy8oy+tsS{T|6iBuck(D;|&|JmdsvgZDNztzl_a=)9h zt-#+vis+HUrg$YV;j|Q&2WF-^u8YBE2kCTL)8Z(o$H=>AVSMWBu{q+*Snae^{WKFT zi{=CV9x7+)h)0&+P%nwrqu1l{JK!tO?oCPqjeVf*WqhYCDQg22OY9We$QO1+y^Iwr zYX^deujJX0f*9|Yzxvs6lYno;R#3_d2_6W~vr31z#IEbg$z?!DE$_*kmu zUt5Xr(F=u;9=G*2*DA1=>|9Z~Wymx895NRHV!foUQy=I`2_5AEEz! z^r7&PL4@-aT4q~mTEsv&qHM5Ke|}}>8FO`XmendgV=&Ib7V@{NjZSBU3e8}1?(Yo2 z^BLg2Dl^z=p1b}B4FTW$x%J@F>zw-*@b7D9be!Z~|C^x8;HbC{D?scejPd}Z=!Gmx zg%yL8`OJhco({u7|K$#h#Ga8sf(Z#SI$d-3$P&b-GHguCEUbfZwX6k>s~sZC2CkmA z96ekcxm&hb9JQO?l;m}CZ3jc7cLM>YSr-|DR4H(BO2$I&S7XC6Kb10u>k-ofeJTLD zVu*=5z{rU||FZyVt1Ltdu#7|w&vxaJ0IZF%Y&Tl7t?m3nI#A*0Jd4mKLeC<)Mfimb z0{vU}O!<=MqIrP>-N42(nalXw;yi~tu#t22&<)+{J{Y{!uo5~ z#QmrmL}Qr@zkRi|2E*~UDtG$PLd#szxo8Ha?m9CQKYlB8UM3D&FX=1&sVoAe|HR5| zK74G}${8=E1`cqTNcRDXw+Jy@`>?L3Ompu^g7YJYJ)J~~d z(MdCA03>wbh-{%cv{fEDV11MnxR@h1V-Y%^wbGHYVzh159#{#Q~%c3rVY z7WTKN#_qMD7Os%p=PXW3GA(hSQ&A}WoXvUYW0@3d+dl+geh$B22JX{Tw0Y~g#DOG6 z)=*mFm&(tkIuOq!clzky)}Uh^bay4rXR{)yo*xsq_zW4|=Kj5SHV z=xi{ojPmcr!qkxuB|g)vwZ291^VFEX23`=ZW}@^?GtIb?9ppz(U7zg)O|*P;mS-vk zr?p(Vez_G<;gxF$Jm@X9L1lMd*-#rw4sxG&?4t+T=hTjPy!Ts`X!)L{2v2Inh=iUF zBY+=i+Pe8;KY@w(C6n7H40)tETCw(|1-&KDyss1A3H1izjm1r5Iywia`EFk&x^ajT zh!am~o|J3xfFiigM^8!8N8L`tMXpLa1zIwfm2BB?7i0N=`$#uU6>~<8Je*iz|Eeti z%p~H17;dqjR&H2k2|N2+!nL)jHBHePC74_`YVg;k$7ozy*oeule$ZWo>swMQ z+a#Y(F3)sIp?SS7pIy+)xXeE5S&_a&wZYbnkh+^E2?t<2{?NE3$65+0PCZ-~yC%$X zmVnW2uF|>illCHS{lyuoeTS(nQ(a zcVTyUuPdvtq%DLx45I~I;UuqMLuT*go+37nFJ`Nv|ARl?7-KST0l%fP?;}(Ac zfElRAzNBJSB`Sr*`0asNt#a){c5$aR{rpQm7aSe%nZus5yqKTzf0L0PnKLA&3ZQh- zURovT8Ns5_VS^e8Q63)Yf*5cPf+^(?ZM9m(wv|=_7Zk(fPbVt9#^_4lF0dQ%jAbUa z1>r8a+8mnIkPngxN>WfZ$5FPGkAa`kwZJ1<`dWesf1a|q5G$sl;gVQ(#(MOPizpBS z0D^LixHZV3$8u<#{IG{#LlG#K1K6J9CtN=N`~kcqb(?ONYPd!-GxgSCB*Pr=NUH<1 znZ$A9udhHH)-?klSDGq{JyXniG!WoyfD6_OBSoKM*0eIm7`f5SV0A;FjgHsrILlnb z-lQH_>R3D2i`TmB`__M!q!%r7{}PxV{;<1+$*!>epeT45TN}?2RTwfMYuFD6N>l4C zxsy{p@If6+Q8b64-K5n)?&EaD%FQ$@w-ikL23yJq9J7v8CpTV`i8G(PfD)@LTr6hkE-3q2z2MuyMq2Sy406-J=S<2bLo5l11bhmS>}>T+6|xm!|9 zs={`kbbD0%(Nlt#ykxivje{eGZ(-U~c31^>0Edx%Q@@H<*(NW1169JgymQKtLJ8c4 zexX~ye&a1`2m$Sa1L=f+M{k0Sg@BsmPMqo9b&x2lkM&pq{frxYBVIsvVz}1i;|ApUc+02 zOGYr$effp5{JyMlhS;wi9mSG}v-xH*x`jq}$d7R%zf>vlSpd+XYf2xr6Q{`bDCcx1 zY2+4Ze@ie*?>-Qm6}~K_OKRtUx6Gf>Q&adIfpj1|4#9&$24p7Ed|P#rEa|q>c01{| zVF=Vrgd@;a8Uoq&_P)Q@0YnAsDB&E)CB}QSeY%S$zo{x@#4mI#coNTfo^40K68Wjh z(T{!Hk-}KXbB>W^1Ejr@whZMk#&>>-8Rf?hC)qI?4*-!Rt1=DXIHPn=TVxH$NjC+= zel_0W34BwQVL^UKY-(ZjXKahI)Hh`*)omH6;J(uOmxa??mPpFe(1kte;M0qTOngk; z^g3Yuv8(3jPjS_F(gwowX7C^QzC-9wrb>5ju0WW?71{3l*~&YtX|ic2ON75Wq|GCgU>e{N1WwCJ~*X9O5yEqaaL?W z`^ilWTwljIh}jG&ooUod9(Rsvh}C_Tj26$)rC{;Tjw(siwbn$jDdp5(1=*#;X#EYT zy?^{t5D%9f*x*7OvH~rqe$p4$ayyv4n2k4c#OeoWXydVKa)aV(LhJO$<(2& zX|1(nal@ZZ<>l=_BKdjI^?$K~3n07;(kFkynvFlO4P~ST`o|9Jszk7haj)i2%=~7d z5^kg2y^scRXB$mg{wAYkTIaJ3ds&H|KD`M4WmX0YwpJyTh94(BIXt87Xq|Cvk6_0R z9N~pp-O!k&CSFUh1w>`y1L3A*ene_Jawqsc;GcB(>`C;6R16SiHFEpp)jLzw{Cjf_ zt4Q!(>D0{66^|Z6fb?dg*|a?vul1hNMw#B@9~9pKnOqHHJ5B+NOr5C#5iewUdCAESsp$lHl!vOjbyuYFJV%#&fc(kEzl zZm4Z@lH~^02*o&=728Ag9raPlgq3c(eBGrL%U|0>UI@{6+(JIRG8AS|!Ph0a+x*+{ zmOcq;6-NZKpZAo@ld5gq*vI02zc2e4tRa&M=_a2J{c*rI73j1)3G2b9v|kJ^SpeSD zeZBi@xwU~6SE8TeDO&hpC|XoF+zxE9p0#Qa4pIxdhO2kR)>K!E5odb%up6+xqJE-QPI7 zoil1s&X1FlPJVE0mzd6nkCx?SwSntHJ1@W3)ajP^wR`(AVBk$_s{({bYj1!4T^aZXLk;-5 zF&(rWk%TU`l^x!48+OwRbcbZp@rOh(i%jlPNCn}jx=hJf;1t~C@>}#+=1nr}&tlUa z|9gPbD4A(dM(veTX)JU!t4N1G`IdKl8P=IEGg>q*X-g@16&N0GFRq1>m4=<#OwJUj(8&b$pu76 znc+ROfE33O8v+!BfDtk>__3^S`YBy4Eq$jv8r6yDG^>(emI0KCp@O_$ngz5agS812 zalIcR)ubW=CJN^QeAM%Pqf+foD4ojni=TY;e?P*O6^0yu=ky3Wl`)J@NCo{tUnL!n zLr9>2Jc%Q9_k3X1^{ur$4iu_}FDJ_Y?c9CviL~26(-#=RG8({2VI)T8O@4TmVMp6h z%PNNbD5+sIOGxUK0Bmz5*fr)OL=dync_AND+aa5}JWYP6j3{g9y9-m_G%UAXA|NWU z`EL<){2I!`D-V_MLw_7C;8jQSn--;IFTY!i>P_E>o7He_o4F3uyN-7Fx>^kQYI(UF zLn-7gRL1!9oD=XQ&E7zG`#z{j70AZf#!kW(8@v=O(n(9OO~0oM4}Oi@gdf<;8?y)d zQo-g%BLSEXVj?7~5j*^(vn?gKYmZq*_gTP1!wI-^p*EQFx_@Vs{^l(yPSKDcFygsm z*6m-*Seh_^=Sk?1NCDVmdaD?Sg;m)(hC@Qhh{=f0chp?P_cqpTm z*@I?9@5Q!A1?|M@mfPk-9~lt&vD=u*V~4@g(4QI7J|Hra%7cb?df)%iufhR3P{}aa z=PM&7?50Z4CYC1k6!El+v4p(dB@)1Cz+8KH4syS;k>xv1zJmg*BtD8+5tu`D z1tu$fSE+%x)Q+C858E5a@Hj-S9JSn372UvCUe6Q}>tSu#r`8nIZwOA%!1`~Eg-g@j zRw97a6RN6AXyNBx8FmnUu39jBc%eZupEZj&mmHCxe!0Uv3)f#3ofQ8tlzqkHB{q~c zx;Bw8u&AZH2w6NI0fqmcbmhM-1kLC7*==QvS{1-v)4lmjgjnODNvJs9yUj;g0sQZ3 zYixo=8@Td7)^y|xR+qrWOGiuq@aN7k(lm>T<+y(LqsN z=6OaBEd9TaW;LGsiR-6tCDwqnPUT2NM)3EhhD)(`+(vC{s-p$gpXUI^EpaHr%9s-^ zro{_}st}@*;hi!5ChV-Ui?~ajmzx&@Vt1iy;?=L&*4XIfTCR+|{NTg6$pLE&l(Tz%h>6e>OP{SP&H(U8APM z-shzuxYogVm-f6ymr^7%Oi;xfn78(#cvjdq;MQnop*lRRB2vfcYUMi6Ytv@9<9;)n zo?dmCXMB1BKCk(GM=9J)U04bVr79)1l^?=zou}i$yswPL9@@o=n4XVHMLMft)f_`f zHiBCfGiG$D^5FdqGK_0&n)0ZzB;QsfbwFNAsE5@`0p}8L(PoYt6uu^wu2MHLDU>&G z`}QPEl7*cHUMLS;nVv{Zq^X2&V?JM-BR|siB&&56@7#y~PVcWEY_o6Xz}~m~HjEhV zq>bSjjZqXOAhb6gmI)O8)r2QtOHmJ8`jO50b5fpV4Dj$g;&@o*iW%@Z*M>&lEMW0)Br~A6o*!ve4DEu(+7du3HGU z;lXaHj~|@PP=VD6-yyYq`HmoA)mqlt=&aU17ZBdeAsSk1q4~Q02<$&9l&<;eL+(91 ze%4Wl8tHTgZic1NmbF|ob+xK>N+q9fyhORC1Vghb#0Hu|R%V*Jb#$Qlh#g zGl4Y;eCcqfMDmd-MXFb2l_m~XfdsPA3`OJ^P1psp4!#Z28$wi7TZW(|j*Qv}065tB zOVQ2G{7pn+H@k85vj?irtG3vUD#6BcLR$pmsycknUrzKE%8Rs*HuNthb)zAgRUz6V zV5#ljjbHdLw%s-cc)z4%HJSLx#bCUzQjEZ68Ed^jFc9bHO`a;Rg| z&JuRn%_qODp|`Od~L7`=6libgh@Lc`d!7$qkx8= zxDEjmWg0=&Ou7cb>&IWt>IO=iGN3mh?D$ zB+;NsM9b{E4^;|M0gC?|L01><2Ym{cy$vYiA<{GF15Md#F$U3Ef(?n$_F09)oiTm% zz0R@@lnvBt60k6zav4kKO7$(In-&OlZAg*B2$1P*v0(?Sy(7y>9f+s?sX~0JG$)|G zo`S{RLtG@g@Y`PBF3>33diCHe!uWnSCz0n>(3y%Fzq$6B%wyApP(2{zOuXzjP_i@*Nj#rBt7Q7>vVNRba3I zxgZgj9N8Fa{{}K!JIlN zV)~(%!#L#be35?O$+Zo>bhM<}Ww;y+!`7lcfR=6+BU=-$4tKm8z~uKaNt-#3qdS2R zj+ICoU4rbID@~e?Op0v>Xk_iFeTmk>*K~NRzFjn%l9bivoQ0khz-bfJz_1?%Exyd? z4p~%vgylO=V4CEY0erfInigQuAc-P13HeWWQRKrn7_+{Kvw;(@mtlG~7nr_-RWbzA z!1inyjR-ArRSW!p|C+DB^w$Ba?w*9M}Ts zAhA3n&mZ`XexmL}7P5z&2+eLKj?i{^)}F37y74^8egpI48IiZc2r^lv>8K56)SKUK zB`SrG-v0K{en3*^oNNg&XS)`VrW054?I8)WF3HB3j42i6Lyn3PXM=GpSPBi1;EdB8 zH!$w1!R9C_(muG8$V;D{lFmH&SL1q*@_UpjOy1MXNB}0_I{DVDH&6xqbqzWYD;<3t zfFXC}i;Gk1I;dgMcG3+V)%3x~e{8M{i&ijY{rHO0j}G2v45FNGK)ee3$QJ<4W^8A- z1`6op#95WoZr)>Y&1})$bxFfufcr8<%gxpaTAdTy@niS(uR=@48jCL3A+s^}BYmE( z3&aZ?)Z=8VJ5kxH5T2Q|yd0CU1wZ^OE%aV1=<2xp^t05(j-$w9bHfe=Q2<|fjWG*a z4_VW2D|JV9Bzg|%I9IrzK}R|>;8kekHjj7o!RP;A9!0;$q6?uv+MVJf2_xd0xz|}> zy$(M18KZI(;X0cHOU*f$4jTCJX2oZ;=2xA#h{fD8<;j0gtlm7MYlR*wA36fX^t6G+ zdvNFAv!H7L_KE=Zr1>?YJV4^OCNmusuC<_=gIYFA4Nn4`tn6#+B_>XSOXyvQ+%p!{ zw211bMrcGRT`FR-COb^e4!m%A(}Yz$XnY8jNW}o)7;-Fc|p}xf*CgzP1eN#9uGn#f*1rl)bIMs+3p< zHt0Ubw2Pu7zC$w7O>^$xq`1ac4Xbh-0gGMqmDZ~i09#=B(sQJPHi>p3|4vypCjc1@ z)?s8P%oHAig-YwKe!w|NlJAX@!Eql;3L-@cnL_^j3S=@?`3}t2{xJ9DSqCjNgn1(m zma!b{r5;2PABzQW5(?1h!&IfVi3mmzQHgAx-E21ofKS@q%y{l%KHRP|B-hG|df{>~ z*kmBD?5p)&l?YCRn#t*g2~@CZS$GGnQhc=C@3(2jY6WYx6nRWpczWzAaG1PSd&_%> z{iGC>ka6sE@8JDzhgd%QvaK1*=xF|HrqM`W2Tg zgYwFQi74E@79t^LLwegx3CN;seXgW@UUBcVbMYBeS=y&kUewk-_)QTA&1I!&;+-i{y5_WLAt9c?jw`dIGG+&n4w?!E znR!H^mCCXSrJ|;SCMk&mA}S}5-v#>qd>_BR_IR{ExV`V|dL5q!zBJ`GbNXY>Ubp|% zZIN49Z-_m0UBeCIy+pu_?z%OV&{Oyl+MrPzy80Szt0>=7xsVKgrvSVVU{#7lX+Zx3 zrePE@pahpFU2vl=0n@fY#wD)G@_nx&D}d5TuP!Q+1smX1 zgajySQ|Q)}#VUGoyBBT$mnr%36x8d2$AmuvgFiBN$|yjYSlvuOPeRyClkJa1B-|X2 zUFVdw>VA1ve81sSW{%KvXbt&Qq$6G;*LyCrjam5}2{$~h(vq4oCy*#T!zW$N*OOm} zYCZ`a##3Am5ZQtGZ+xpE{@^kUaZi89%Tsoll;R_TAAt&mqT4P1n+!<-=g{}rm=HsP zvO%;X9w=);1wbJ~2lK0cv*o_13jQhXJ-5=j5d}~5twSF;&)hP<;mN(WPOiCHXF8c< znEU_5Fn`}St1AJVuP3`11a+L`m&B_zfh{go!rOkHQhdY#^hMcXa!uZKk>jA>f4%I z6K-w)e0u``jqpo7X(X^r&<9wdLi$gWJ<7$zfv3>0imsdTZzwX??Y8Dr z;l6tog_7+4Tbaw!VOMgx_Mfc&>K@AK)~AqS(ly5#S7GPP_rz`MeKP zA!F$#DISV!8~{5$o|IEAGaHP}$NFkUgad!EK3#})0i^4ZwaqCu0j$(Suv(J4WXJ>GGfxPbH6aPT6*f>29VQVUh zE(>hTbJQe6-hn$#(nD4gMh`8+;p+D7?E@I8#+os54uU%{WZeTq{f|3WXN}$_^>i^7 zX#A`3vG0+6A*Dtw*U4|`ypU;Rs#bcZ_u7?Lg6Pet{s8lzx)l>qoS z45a@FMWxjJ;sb65DuO~Mk_ze3itO2~Xur&a&$D1U=FK*o4`+vL28pcFfNDSOHj(*; z7~BcQ;)CLP;7nGP{Jqm5>=kA4=9(u%7PB<~#M)C+g5u7T@!K-n8!DGKo*4aQ8EtGU5D-u}9Kza#yXE zHX~^KPPqcJEMZ{Zl9afgCOWyPG;m%fFRu}Q(7J|@1sTY=SpPz zH#=~bm;SS1=@6M!kA33@P$oi6I@Dm@658YD zJafk*G;d0tk~H6F6&hRt+uWD=lJ-LtP^0op*wIu$e(PA3pgea^HK9d@aE>G6G&~el zPm@ro{N|Xqn;K53Ut?&*9cucPl@28_Olcc;GjEdE5Ub#Mx(;xsOwu6-X4&d5=q3t~ zMWnlai8ODeG3Ak2_*(-9F!ft*XbE z0Fc@SK>;)S_`GyyM}kigm;ffEIz4_DE1GyUo-) zO78a#$7RdIyyi=0psXe11DBAiyhYK2s)K-8FFm9RqWU@HGI0x=4f5VeRD81YZCKUx zm0!V1Sl#?`hN%bi3(z#}Fpp2_+By}xwxNzMU&umFZ(meMtoG<;7}x{%PF(` zh{M;zdBUbuxj3sRt-|_}}0R z6q7I@9;xGl%4Zgt=EKSR%tc!G$2lTczw_9h%ik00uJV4GPt3uk zuWqq?s?-L?<2pFry-6<8t~8fB9p-Xp#~zK}cMn0Q1bU8NpFJJQ`{>^vy1RC#JYw92 zOK_Rsj(n^}CRx~r=STO3@*9skRp;=+6y2_<^ugbtNs%fAyxRw9caQY7q`2G80MSsX zsBe$mWfvY0kWqk{OO>0Wo+x}>LAjnM0Slb+nm003;a`v;>{F87^$DTkOw@`;wSQqh z1m@S*vQzCKFJr*3%((Xw70}c0Po_3~+QMsyei>$bB{71ktj^OMj$4$0#;G5Sy0B{F z_*XUcj}2S@T4?$U#xA0+cU^4uzrdT1A0*;=seV9fBsJ1DeDEp0CzHj-4x%${n&3Yj zDQsf~x6)NpSPBcUzoFnkXz~*f_}H}lQ#a%>;<8$@_CWfeG=FD3*#d#V!q0XU{sCd| z%k(@H@O~oPx{mVh1ZG9#p_yV9TmMD-)=+VKdG%Ij_Cp$&ulHTigxx0*Xv++78v$zG zS5V4Dfh%^>%AESjM;{tY9zC)6?n8VKG{B7IUY;FLG!MAYN2&?YkWa}gr9OvZ*jZ(X zM%XGum1^r@OlKdm)90aswB|`=b77?52DJWCdL16>ByMrq(a{or{M}1Op04B@2W~)X z1Ae}S_K2O>h2NclLkx}qI=f^rPlON3RF$DN0h9av#d?a?tG0BBDyltTP19(kg zozne2m-up6_aj%fo}BKnO%Z%Xg*Q$45&1*!1OkA#K(yr4G`Ft z5~Yrwt=}u12Hc}UCAIx22{^nVGlNxtV5c&E1m2 z!2go&$vsi(qaaHZO(A?QZmG_NFL4d&)^|}4iNG8rVZSKUU2*=f@zvD(FcJ&AO3`QL zIzHjDyR1s4c4$Nh(Bx?O8AZ3vC9pSD2_%C+Tkl^J z*^=noz%1ZVJ1P(ZGiY2l>@1=wW1$U%YpNM3*U0iZl-;~AM^fOPDEprU*{b5!R{Whs z@?G9P2$J}Ry%$a1*`KFsOBSl2fpj@^=gVGI{rFZ7AUWg-Iy~z>Kn4k_1|-V!<;0(h zLGDnS7r=FXY-?H7{}zsi*_Ukt0}G#)iQ~F}@L&|+Bp~)?a<=ZhLnT4Gi4q!L!u42r z&HA+vI180=VbJ6Xigi5TO8E zV(?ZS&2N>l*!W35C#Ib2w)4Y(mLfen4&n;tpGp3~x!cJ~r&1;xdKta|lsj_+C)-M4 z5l_OcA|3eyRXgnzh!VW9jblvP@QGCeMiWRht`z2S%Gv)L9BBA^S?wxvaazY9MGc=9 zRv4}DHP8sl)!#t?(1EcnZ~m*l8~Rf~p!-x%3e=E_bKbw5uRl}GlKgiWiX&zDnH!G>}CWC>PVe$WX(9^uk$$j z7IETnK;^pHrUHJcex$FetzDx{uK4$Uv%|p}IkHartuRx!EOa$(A0OyCv{g_u)1DoC z5%2PlnBa5`pBhQt!cL?^e9ub3Rs>AMDqVyeO^8_8w&`fK4cQ05QmT^zayTU9GQG)Z z%ffYZTjDBx?m%R15q80^eFT2ljCT?Q|i@{p~A|XcI_R%j&k&Yd3}ORJ|PFF z31+de9dLUByE2Y@+hwVJ?OnAp6(3Z16dD%8%F^y`_s*)bS@-f(?iEAnJw0QaPFgG2 zeVGtWh@?@N_}_fUf1rV-4k7>KFZGx3_oZFjq=0)&fVfUNdC2sg`^~7We$*oX!GH94 z_UinNdaKbG)P-9kEIVw<*omF6&^$HeHZ_{rs+JI4n(&|<)&S#2fyCes5R}-#_4o~H zbu=#wbfZAO(vy-!9B82WoM$c2@mlKQMIXGaNP@DBBGxwPp&fXiK-ABT?wM5}!@^l` ziRL5rHUGHac*eH2vuPp}EBxOeOAWMSvf0ykJmIGTyf~s;Kg!@oX9zQH#+vn&`|Hi~ z-!a(b*)i6zbhMvcN)ak`7%J&C%T-YNGrZrvaJ z96m2KSWIY)r7l#t3Je8*5%6C^957)IYXCAQ>WIg(4gR-KICt3D9?M4Mim>}ndi&_u z#Q901;hFCFU?Z?tD@-9jN|%Nx^5l26#ww%#%+ZJ5Sw@RSP9M63B_5AR;z5%mYMC6Y zVol5pi}mJ0L1+`2m@#Kw>vi+1z(Di@p8wa(;3fz-2>wTXT+a`;Ey_X(cdQIQkb|yZ zMdMoq48E+o&vG17hZP4d)C@;t7rdpmms59+(We~uFOY(6Z#_F{*=VwG%|m5__DNLu zfp_u?)9~VmAwjuFPv30wj&S^5G?q8>O`!8AYtK?t(mD9S1b{Hk$lSN_^Eu-Wv-8K9 z_#UER5fBtJ{`bH%a(V&p2OZ^(lFX_DMNc4ks&gU$al<<6!`C15H2L%HMd#)0dym+g z1I&B%_+SBOUq}3>Lt~||vm7bEEHsM40{&yzU@T(fA>zFCFvSB^!$(O~Zt=C&L{_q( z2I;Tql}w4Jtj?XPqrUQN5$rx6xB2B-n}6bZ0Y5>u_wo6<78O#B$kG|9U0}!e4#{l& zU>vIA=nLXHKuj^b$ykNLU6H=RLVhP-9C2m3*_+A8T*4rB%HB7r5Cw4L}h57evw?a?0qsqs;2bI1bu&-_Wz!LyU>4i|ER!)3CEH?o z8YhKqHZHq$IARhRAmzPA6&^y|{fjX0ln-Li#IWjpnAHtfOmfBg7lotvrOw%xCu1T; zgoY_YHI5pT;CJh7#zKH$!oJVQbnzU`Q1$c?wD*>n7A?EXjn1ysozIPOmcnP1S%d1u zBVemVJ}UDa^H=*sXxX1 z`1rF^fteF%v)&y*v#n4N_9?4TQP5{<* z?dA9oxvm+9xKrMPkGCz7vVLxWq7t2bVOH>^rVG`X9gU*s{YN8hM})i2@dyaad%B0h zPUjrA1lG}7Lb%R z3oU9;g)Mwl{!Te_|5<>WDM7L5;XZpmu%)FWjEAJoCXG}*LYGuD)A~m*Om`&#TkE#t zYdpq1PnT$lvUR--9S=BRZ2Td^{*zH&N^&0oC0>-_!!GdoMOLN?#R3Z)iA@p9LVn+7 zzbkOUjs(#3A4QI=sP9VTRLO@SkCbKF_uhCEFwGtSr#x`aSmr^dE<&b@%VSNQ#{A?Z zsu@Kl&Ff^}pn^%kK1&S zoDN)L4xlhyCIfozBM+$lJH3g16xZQbX5RPh`pQ&7zc_Qajc6~c`ZCv?TYKMtCD_Tf zN+>G5s!wnMlJPbGNDfTR-p5Bb7B&rz3`z>J5%vtbG$ga077xUn^#KivxNdl!O~ybq zguNp3zl`2X&9FUG<^e2hzo6EN`5p@4ISY4Erk-VUN$4GVXQ#FZ+?dkOQhG&H>an`1 zZdE<2S?D}qQ`<(p3DOnFQm?TA9;_ zVrYNkK)fnIYCG>e*j3+PXR)yFxn*vJ=?PR@Y0>_+M!ME>bDuW?CtE{3AAZJ)G5z26ijp02qVuY2;TTP64-0!8co3i~zWHg>p9lHai|NE13Xh1gk&NPn_O86Y>_~fAn9(w)SMT zj|5EM5~PIi%#ToEG?PbCCuJ=fzb(J>(EyauSr}xm+d?8hsQj^H`gP0=Ma46juMW0# zWrOi#u=yPxH5bYMr~5A-0-4=D*Rb3rfLpom9++Fh9!&p114Ai}G(7Co*Ia0RJ#5uj z#Od7?){l|hDbA^|pS)DXK%eBFp9b^rtLsu%l4!Ba;?`S%4x<74LFY%Vu3cz>iNKw7 zhsZOApwrG4QPeaBYUq42<@N5%NUOd~#pWTmd*{Hzy1Clh}3{`(IZz|(* zE%3@*;b6MNb(TQkFdhsmBdldG4Dn>lyISjhP}8LWbbnIKc!DdEZKFURxu$p9 zl8J0OukNl=M4Hx&B@9M&vEox^z8&CRQ7!+X9cTFb^2cM4QWth*vxQ z*I7F3DSSn}e_@n>zLu#6FhD0o@Su}$$kU^UXn$!1RkAb14&dOj@nr+0&_HEkmt@3M z0+1NQV6li&v>?^iPhc~On7FZAQ{$+}Xpf~bA26WCVY}~&Yv>o}36y?YFdeWDz`c<2 zy}T))^&`ld=?qrqT80;-_6lthU9{Ox-!_Cyo#0(0to0@~p&t3$I_YVRmV%RAVMX<< zNOC^s$c6j92LGcZz26qcYK`FKRrwv3XRRls%AD>qc4lZ*m4+igA7#d^BNrHLSe)5x zzBirYg^vxU>2>3A^W@jh6D&R+;^lapkiT;j>p(fr#&%1TjFYVl2er25ql8RC66mD` z2?}W$R3df(@}#ljupm*R?852XfIh)<2xsy-B$dVjwhSd2C}r8KGD@G1=G>;mCqQbT z{d!dKY#~6{(b_eBcB|H75Pe&@J{WNzpf8I}N-n0a?;K+=J|)D#&+LHk&g980tbIRPOUbaIxK{k4f%~6&!z; zbSKPBd_|wjq!{UdlxY*P<4XN26a&TjS8YL$1ylZnip z$d=(Hp%?#f`3;Qw(o3PGVhbfbsuu&};X6AFB^i#acmoO84<71`j?$fxr6 z=y*T%32G3}u66#~`9|$E@RXU2avp$6I#E}~fyWk-n(o>`#XNzXt)i2C094HYi=zkf z?i4&DDNip;!(S_RwoIz!A8zd4wEN#qygp68gJTdD88XaV-D>k5^Ne2e__6oH*<`pE z$`MX_-bm{UT{gx07Li#)gZc{V6a$p@=U^TY+W6=~x=RR`0I^Jx#QJn^ms0Bc+S~AX z2rh+sDRujyv&M}!+-Ej4@>80FLf-|WOm6;sP?WSeAdUvf_Cw)@_2eHN)h^5Qgp8ZZ zj2m-qE|S*-UQElKcT8fOM$D+^D0>zw7n)ObeL9f*sX1?|CeC!exp7N_lugq9BJ@(1 z45du_%X9=duE)=H6%nKn^5>`G)%%&hk`$*NCfb#e$K(xqS6Q8Pjrg`}y`eqGiH|Kq zmgrS^A&<{!Htsw&V>LVuUzIj+Q~^X+fcpW zPDn|l=LjV_6%ks~4acNQ{&<3O7d5svmgJwcsbGH1g7?zFk?MUxNqKWII zI%!IY*75kwhlHCVl%NnYNUNp%PHCPhAwie|gg{&c1b2@7TO`46SX#~s87lL}>4&q=jl{jSZ{Ztgpx%VJ#t#tcc(flm!5mSIOy!`O9#^F-AbUK~Fp6@duXFF0=tgyS>O}V>Q!PpH3Alw2@AKv(KfM&gULq&~;H~P}rKbAQmL^h^T+w zg^Z7LwZt+L^DGNsD1IL}CHVqhqEspO*6Ume#K-$Cp zkgC&+G?)9#XQ>J5X+fS#)(Y24(>I>z^XQc$Zwj_)5F5RBfyeWhy(FK_f%t)WX|Weh zgx{b7;^)l_~Xj-H{rSKeE5Ei_n2W6(4u?RVt5s71;qD?9QmZ8x?L~M zM~U#UV+G@=D>q&`Z754Yt3L!Jt-AUJ=Jjv-^4Ys7ikXGh0DG{eCvN^v!PvgEn3NWEc36 zFERP^6uxvZ!RB75%%$sCdit8~S%%V?0ST4ZT^X+j^of$zD(2#GU@OrL@ZS>OtLTz= zCWD?0Vd&ZP%hR?8yc^o&^e?yA#v%eQ3yhVz$6iP0@=$F{+2e4H`!QZ?|Ht zp?2pFQ%}IsnSk5~;Lr<{G{WaNN}Sa$=ePKMt}&0ic-xp-*h5^soSQ`c z3uUR%cND%xtT`8Gzp(hWa67LkPJ-nDNIRbTMx+RqP{V%Q>As+=5?eQ*B0tdFC&k@MXx+% z^mc7_d5IDZeog>w%Kw+TPg2a^oM)>L)Z;^1bn$xWNq{K>y7uPjL;j5nT~&q_nhVLd zfQZ9MXio#rePs_YYnRgVrgCoS{iN@qbLqKP5F)6h=t_#Tfe*xj(AN?RB*Y!RpEPN! z<8T98^|;VOL-Z5X3oY7i0XHc5dk?>vHO0Ou?g5#7%_u}H>-k7qwGbELKlOU|dA9>Z z05ENS0AySgJyL9m-JbnE=Sz~QViJMzyLXf+Tc}KCWpnOZ)Lj^5R1rX&>P^OuNrR zQ~=Za8e5msd{)t`qC8^7IsH@ko1)i1VF6H3yM}RcMOr6RIDp1->0%@sr3N+ZlBdWgbg1kIb^5@6-&)E)2zt&|5F*adA95HUpxcKvK_!+ zt0;ds-B~>0cdAnsJP3f&!ib$kh6^l5HRG>(noNE{M!q!x6>z{tkgl7gJsIk3K{zq( ze#$#0{p15sqlSQAc?sIR|K&WFxjo*yNh7W4b#C>mjdAJ`ygVlBD-WOEMhXGX9psPd?!{7g8>jkg;k!-Rq?zL?oXs|NGpE6z@Q z?oOBb+T1vLnIlhFANj7h4we?d}7WeToz1zhZtJ>33aR2>s z-L0?Df=j049yyrIkIufVL)NF}Zn%FJUnjwSXl)FdpQbZ_IVW<3v3JLY zrTxJ7U^;gDS*Z_l`+34h^Jk~a0ubj7-RAbp07^HV@9ZO*EWv<4@1c68mLBr#eHlIkk^}j{KqbzNbJT>>H`$rC6*R!6@BmH8@LtUG)?F1%T zQqU1Wg^l=^018`K=3Rm{7BAQO?*~KkO*rSfuw&lY-r`7nXA+m5^0Bg;)Jb{B_`t(c z&yN5SiQwGDVdV~7D@%EY7vI-% zs72t&2bb2JeA&CCkO4R_pPcr-TPZG$Fi;Ky4LR4p_de0DIo)|kjjI?@KMB9wZ@206 z>Uv!|(KLhZKJ;O>bel9I$Hra;Jv2n}S5}!ihZp);&Z(G-u38i^<%s+;30cktUiYDW zRgmQ*VAm-g4M`)^amx~TG!8K6t-8z<+2+oO1W8oTS}f2ykgPjM9tx-@r)CJl&$Je! z()O%nM2z|6O}U}cBsyw)I*AY`l4=Ci3m4X$+~S1Xe&0XMDf)7!nVYV|>V@lQ%T^i+up;$2Nmru_fuO@M${}tU9!qFhW9C;HhLkR{hZF}cCw7Fxw4%4dj4D6 z_4c*!zxCyB<`~lb{x@1{`F}Ssq{#=8VCOW#wBf2fyk=K~20x5%o+4_4%OimUNi zYsL+|=g!6XFc$2p_+u5K)2K3P`i#>iJYa)U+T~7SJg!f5!)Hz`87#)nH~e;EH}dXLgv8uH*C&8!gn7yx zX>Trwm|sI}GZLo^-Gx)8vB{WE&5=A>u$WawxlQ4QRt%`V5%}Dkj}Sewks{Hy9REy3 z!~}VL2KiUSlcm&>;97jbWO6|6UG_e_lcEODSh06JMpe~bc=EAbGQ`7tLIEva5&SQ# ztQy+}i!u02{pz2NH|*6BbE6`?Nr0F_+hzcAgRbG{j{Nu`u47FrICXWA=6Wb{17miH zT=#=Ku@hn_z|g(Cn`{(7IBU7Mta2Y7G`M4E~ ztjPM_LO%Xs#!BpO_n7W23;K2IWPU(oYySxvH=6gZo)m^jhn)?JNBINqHCw-IWDBf{ zzUNEQ(VKhjCl1Eo>**#ko(0p)>poL8Zx%o=+DzJSs4Kr~-6u!~2!Lof_A3B7Mgm16J{`Gzpcs)5?e8I zEZZC{`YEc#K!&+b-mAjojo_p3gGT{?t{imCv1;so8+`p;0P|Rh=&n!e=fvH#BCij_2$G{8#COlYw_wPv04hV$% zokjIoY0vuar>+!i9U1U`c>{DxM;+CaE?MwL;3~CscuW;h8=`exR`F?dQ^|?GN!Yt?!5Px-WI*U^?GL=OM2GwnvrM?X67~Q=s<(#T5PDKK8;cKe zsA<0}ruJ^lQpW+(Fuqz=6tZ;P7T3xU6D$H~?3!=d$*bo2p8wt4L9)fyj=)?cfM6`G zhKBnAa%5i?8q;u10Te`Eu~!r|1@w>#Z1RgX3;-k_Hw`t|1QlPL@7Xb0$E-|AkP=gf z_%_Qft!CRMB9u0>9tko)V1>-?`;Td;*ZzdG+zsD1=`JB=hO>4##hGQvERtsj&~>Y( zGM+1XZUac~8q>t@jNh1PHt<%p7=vHk%EkEK=SP$85fpe?ue~B|yEo7j#HP26T4?Xm zd8zgaN=S*2H8DncMElKq>s`?>W; zL07L;@Lte6WCn#J6rUFQfn^sd%vV+U-$h z{DM(g*Ufa0_`e18=z7jR1m{Y)15g`4*GQNFS=17r-070nMR zRs65QX|W_T%WJIi@@|8?H9bTc?~)BKLpk!@CVJ5|9jcl`YQyscLLJFkEht-qlL^au zsnLg&k+0hgPkg1#01@#Yt6KrKZt|`xpCP!#Q4|lk%K%aBKzh2`3woI*hj3!bb$|#d zPteo&HOA<=fKdore|Oj&;pI&z33H-MekV80@OI3uD!==kc{2AzHv%644w%K*_B+)8 zh~@k%*hME4P0EUPr;+VAJ0k_p`O4EkZs+C2y}D_d!Y%LTI@(~QBQ}a1p1cLu$9()6 ztZ14Tf7hq}66XVi2I#{JbO2vqqrmEe1_<`yJNR;~!?D2?ab*_fCST-azG;d}U_w1R zSWiwHja5EcJn^d)UJ5i8$OpYK#ZDzBGcF9zKT}PO1}RrKumDhvWiH(8EmnQl@Jy8+ zoE3PL0T@|7|BJ8={)xVEUR%3XtJ|E-`UG^!XK240+5vb&y`~JzC9!D(524{tbP48y z01IA4kQoR?nT?GBmyKO6=Kk+RkK{(I(w4F>y}PQ4vdbr)dw88(2W(S-;8Z8;;=+Wh zX)9@zl+*QkMiT)VsrUCpTgs8Z@M^_WK!L)}=JZlKTH{i=w#2Yzz0%`~gn<%dqpjZZ zm-IFNgcfe)=1Lwl4Q`fp2^wNt0jA~DNDGL_{f)C@qnX*Z{l#~`F{3l8-fnBGVbQxs zfNQ72psUOPR7L9em{M>z)A~H+s0dyP0swPAJf=RpFwvXDm@phhDtwm8DHa)j$CaH8 zU;Z_p4m#KIdD`3EoF$Si0PF}QOuOwd*v@C)^~0UqQ53gQx)T4Cn-JG=vf2b!6U`PH zipvp3kV;I);baz23X;h79h1%nyOor5Xe%+e#|N!17PD(3eHg8S@XhF!niJ>^qCj79 z^SAPBl1t6YGQ&U=JC+G78H;%Vn<20LGA(}VP{04u`!Z-4@vwq33~EZG7Xcob2E zxSsTw3n~ZlZgk6=mP6NodQK$tbhRBAI;WsLfmmkxo zvUq`XV=-)z{)$J&<4S_$G1bE$c0fHo3#Uw_ZA7-DnrhQCYLn6^5bF$@GdoTe)~&_Y zTW7O?gtw}9l36Q7_1)N}6@26FS2eb)1t)#V7dryqUuK^8kRMp<_SJ~Q1XqwzSu5aH zBdAf}MeFik-=~_gI54w+74VylPnLBJT z_NtOVS?OeE;P%Il^|}FXM|niQe`%5-=eZhbNCw`^JI7fe8k8=b?Aj-&C-tOrs&Q`9 zbQ69rBJ(3K;EEyw4oy6;T0=KP5}btgXmXUPjb z78k`%3y8^^0_H!Do|xm8!py=ygWq2qYkXzbDxM1#r}A?@J_qterLcGKY`X7o<_V#I zgilDe=V0G4cTR}Qyh3I4TUwiAGtCnkxL<=f^H-teq`r(M1Qroxdz;fGP0S0^UPf2d zu2Jl2W0*2BlSlsg$pU#7Z}l|s&%162F6Lq~)p06k&!Ut7>3av%0$aYx2Fv1%d5vt* zup(tHczottHX~N|^{wt+`2a|GJV@)&wQCYe>G22*8y)n9J;Ij>PHVN_7GM0r7GK1L zc2~s#@jzl7-AW;C21XXhQSy(>rlx4O-<^4|RgvB~u7AP8N+$Dr0=@^_M0JD-I*we; z251+{i84f$I30@Wt&Bh9KJscch@}?ETyq+FQ5g@Ms^%VPjThd3+A?ADS)2Pnn%?(5 z_426S0vYc(x&r)utBAEZrPncJH-2y(V4$PWb-zo_DWZTJ3sj;}5kgX@r_*m;XGWzi z2)N!?d8duo=c*J5@({Hxod-Mr+YAzd`ex*9rUCEC2HiN&M7KzWW*@~K z@bPA+B6W$8=MQt@x0ZRXyw~xx4-B^Qt5BQtu*wvX8xH7nqFeTP^Wh71z&Ho=DB?W5 zsIUX{4t^(0C>P|2kLXPcoa>nqK(bcg!D+Vn2#OxsR%UPy6tbIrWvi$>v5OTg-%%m8 zH-~m0<7q1CEZFDfV#xW8Tk3B;*VyLV(zdOb(sx*X5LVlk)kitwX4Y0@RGc~{WhkZ~ z&qHkhK*|jM4>C}SMU#^1bro{@7Z%9WX;&06_itmuJap!#wj&BqW8|rk@w(W$fNE%i z4%I#5fCgZGjGR#NySFK^S!g~tWn1u{xj&!y@F*^(Wn^ne*5X@F>*H168Io?gaZE^M zt6qu)Vs9?kAv1y4=OB0v<}dXblr*oUp8&kV`6??_)i&dB5|?j1fU2FY#=WfHIg}i7 zKo4BLU*V&Y1)=9OclNK6rdJ%?)mShT?7bBCA3!>ItulRwa`SgMv(~t3R@StC(oE=` zLf-{*zz^UC+gu{Z@3mh7W*N)`>?oz58P!aXdV0pNQ?p|z6MjL<#@rfNHr1$Zt-%4X z7xtd)YuQ726rn&;(p;K=mk8(_g9S0rZbH&sXm>Sm)6BP(q9{NwuZMx)ZSMPF9MqJV zX3*TMyrIS6sR<2Weh7;`{V$+-k4Clb!@c*rb-%Ph(W}s{dq`mCS_2>@{#=H;8y|Ob z)r%ImA~1MJ(#1PiAH&;yowyc0r`XrX5o7*W6FOFiNXvPFuURY#UPujJlzfu>N?vq z5tt$P1NM=0fJf-f_hG{ABq5XCEPjMkK7% zOtQ4NY95}Pxp^=~@m-Mt%QYj)N9vwG*W7TeY$hP~S}J-+w8@fYl3E_u$x1n$H7}xB zUx&!_p2G7#c@F1I@d|zRk`%h$vc08;Hgk^rcwGS&S;ML#b*bGtb8-I;(OaoN8TWR$pS)IYSkF5Rk zcuy2|^NW*gi@IqN)gKXrv_S?*7Mq%0vAF-wE7OnMWvC&G!HL;dkvDsJL$$|x*mg22 zImko6UlsI=)2YVcSUni~{?L7l8Wg43W;?@1*R&P6i^cWuf9eN-dqCWSp9Y`u`Tsid zF(G=t;=Dc^Npdf~XY>f3piBjf@<0kA)ZiW>QyrwDR3W|`?Z5CSTuV`oTm^zW?V={w!4f9cLV>& zLUUJmj8oK@ajm*tE=(RgN;1T868q>=1434#b+-~j%&kw3^%bi4e3)1&gb0ur z7ywju*B((LTQmU?%vNzA|B(7_)8CROLx(eU_z%Q=wC7W0h%60REXo|wPOnKwbM644 zruL-&dE+aDp@%aKi!#+j4(3^?1#kRS{tRh0&i{}z`X@nVQqpJ-c+p4l{K@#|&UD-p zQ`$`h%ZWG3u2Zl1KVarlt*4l$2Ji_dA&xf?=qk)sM2wQlXZ~CU~7RlSBMtS82SG|ImsL9I!>?Y{YSnv_&5O zvUtc-2I)yJJl{hX^bzPt)Cm)`KHb)3HeLppzj?p)xRdK#cbk~JG>Y(xWs`q|ymun* z>dva~%B!G;lpR0dNE=^5WjMg8&nFBr&|tNCTsjqLRYLg@VUo%X4epa9yU2Cqc8wr{ zH3J`x>*gjweQeryU`wDpBCT5@spEG(HsMYzdM(Z2DB_n>a|ZGK7snms-SD7nt@N-! z;xLhOMCpGJXc&Prf}7dKp!uFj^Jx|u*N3*%<4uBm@?bG6DAE%6wd~koFaEd^vy#Cx z_^+5Q-!KlhG6@Py>Dm5NCXeng6;rA70plQsDSKC&du0o7v%3L{+`O5HoX=W3`Rj+t zgq>BXf*lwkv*QZ&aSa;NgU5}~tH&b-rmps|M-W+H0lc9fRGmcETR`bx_gjI zFSPXQyzerCJ7=VOypaB>lB4_q_PzsX%SoEzk9fU#3}x|RaJFhf&3)K~WjEL9DuOrp z7CQ{Va5Bho>W2Rr#n9jaq5#w?z6AAuw9TYnGT*`6`vKA@SxDYF8K9xI*MqV738Ccn zaz14X+H$;%a?!v`0fTDg5wZumagS*!WRbkUoc~UF((;|=;CjnK4*^0q&@5CBV)M=u z5mo-toLU*f>Q2FHNEK3|Vo4rXzriqlwUqUFK)EkqEQz7!St6_kgek&@XL=dIgMrjN zN&SeM>#xGz)dNzx-ytznjWJJ1&iI*E!WLB@OptW5xUu3cV50N@OmYTbvH*<)CNL5x zQHUpZb{d)^^1epwWigu?WxmyX8Avt*|D_>o*ksMDjNgP%48F`!?2hSz^30p)2pd-D$qrKGl;pS`8#BBNOuRqd!U@6#o z-`RR2_f&S$;{%M(^^abD#^mR7ufrVkjCGPajy0$ z&WEA-1oCRmqkTC}(XxgaFW%&aRUFqv2PaW`)!}V5qm;{NV46e#d6mVA6qLF+g4wf$ z*L-vBn>=S0mi+A~ZnR3Kb!>$a3#L*B3!u*Y%S!TXbYuRBHu{mtxVkI3=ETbai zOfB~};K7_PlHn5)UE1jr<4D(3@PC4tk?%amK}&4^=pJhX|8NEwl7O#bwhqxlG}`n| ziQShCGYG>g1aVErjzgwbUTxm?TO_Za%TFsK3_6zMTmBwo8p^YQk|VOzF|wz9DzP14 zPb0W(izkpb+&=+`N;ORS$&A0ikOi7~2Dxd5yj1c(XV1-aN5T4>X>dDCB>j?!P$xb@9;1KppHjaV`7!xkV8yZLk z>thjJ`=q0aznR6(d?TtEp{_BlBY`9ZNLI<&K;Ma5y7&VsH9dC;f(yh~p#cJN`_Wc} zGi#U?9ne56;P!C`?@ku2GaZ0avnb=S;MqGrVcI^b5%*^u!Xmixr@-iD;#$v)_cPaR z5Ri71Hz+o;(d1=_rp+ZMK^}CCthm;R8s=QdT%6B6eU2!p=J$-A29ihg$}2P;L7rux zom?%YOHvoU#{q#F)-5$?JNxF7B7jr!7na~(`dd_yBV91pp!bDM z5H}H>)CE`9S}U;WF(-udXsyVzl6M>tHt}}TDUarDE$P4C+z#e@l+Vzan$3TKMlMrj zqh@1!19ju&!RMxhzFwI_H*3nhyim>DBeMp+1qDT&tT=OuOy9Er4Z~*@M}iRK{8{4g@yEG&@I+2LLoO zAPv^Dj;N*Dkh}vo&>tcuYaI1c%Qf#%%(*4SncTTwTtD}VP9}K6l!*`1x`(sl7K^nn zi@N92UT-`(5l9L#d5`EADpSS--lSU=G>i?G( z)tZ1$OyJuPmyx@UgMANlCt1GJnQI~}1p#?|Jz+lrtcEbPlPNE3Vx2fGd+h}Xx@-xm z__Vrb`#{NsMgBKTO6O^wABcBv^xvpnuGJeW&B`klW4hTl zXunVMsCftIVK#3Br{@e5SQ`Bm=O#rd@2b?^okUTAHnI=dC*$tQ3};&+2)bPPK$faF ze-JCE>?>(y#RT~P^XexT#u$H1lP9J0TRLqX55pPX*sl4{k(ZXc+GtWIQjrh|V2&uB zGVQnSjsAIg0{qthzM*~Ka+&(BY0lW@a=|pRIU}B)7LzVdExH2~?KWv#-RtR_(3xX4 zt}eut5+0D6Lsia)w)cA;#*ky?vG!f0_9#aff;h|Ht1gr~QyhoQ^nADGHrKiDpPj8|sv7F|;8`Le^Q% zDN!n?)1s2?RFrKLQI;8z6epY}Mz*me%NY9%X5n`~&-D3xzu*7hcdqMPSC^T2zn|q^ zUia(1pZi%ql`g9EFOJ(2A7(Eh<=ydisk@qJJhbvqg^Xo}PU>BfZutD$!IN{;KMZ8^ z@<2;`f5x#=k>+;^y4CmN@AATtvt3b3;regdlg(1Y2&tjZRPGHZ$MD}@eUc6ti`lZ@ z_Q$u{t&r-uNDogW7)IS53=foKIALm_dYu)tv?t>g?a((m|J5vjI@(+~(%941Zf4K{OP zw#Kt{>P_2O&LwRL_S~S$J!!l=-yEhK!g=E(V}$F<*U*wlrt`hB^AsYjUjq_F9lh!m-1V-wfOs03}$%@HVfk)h5v2ATJru* zl=`5b;g2k5;=smV&i%=%T$}XmVbkg91)}nuuTN2TN8kzz9ekFP!BvpwffZ1dyS|A52|8|>GM>_4@2`==e2v>2s-mW3$jLb}X!qIvMi(t#E~ z`wD2Fa6=rf4+rV>WZ~C!>Q5iN@moGL^0*9lcKAt##&E?L5$rFYX8MWgi`VwWO#j=C z2$-Vaa}71;{`6bpX_^n&NPB2|bgerqz-y>3yueV>@*LpN#)V1mzKI_B!6iH^Cere# z+wAAJPfnbUm)HzJPrLEM&&2Vgl$Z9uE_o$ej~Xk3Xk0LoJL-kJPEQN7&T5=`X+uB-Du;~V_NZ5Z#Y{r`tZb>o_8(Rmp+RRhHev`dFgN| z_Rvx*>J~Cj0z~{iB{me9cU?H~dncwd1s|gifrLfzT|NS#4jDKDlm_nOQU(m9RSX=ehN*IR`W(Ixs*a~NDFU(mQ;>hz@TqZQF_T@BcZ0 zJjYM-{F+KA>MOY6UkAsZ;c8L4#xQPd_o3I*+D84UPv%q`SU0skfqvzVPvPdZ*SqBcP-Y^rkFD=M|JXxujW74mk=4c{554|;Z>Ux36I~xDU9mktfNA`jjW%Y zw+@&N(>gLB#?>jN-nnkBUM+a9fhQF2UYe&`h zm!6}5Ht%CIv#ob_mt8V>P|Dfs_XSZ8gwyviK|h3>VWBQ#|HBBRSUg`+rKNM7dN#;wST|B)7bt-9 zu24FnlPBGu_F~5AhfQ#0r1Q}$4SSzbGPMSwJO>(wNKVsE@9#<&{Jj(IbyAemM4y>-Q|S$ot2zS2t|- z4}5B}Z%VU$-CHQLJIIS?*QA>+@2vI`K`j_u!f?iZp{9DoUZE>=<*FWqOUfS(L(|CP z8Hi-DL*r&T4>NeL*r_!g# z;Sjekl=$LIcpFKsNrRVVie}$mb{Av}7A@{L^xi3D`9Cmy?)>B)T!-nBk^XEr^+uSA ztvd5tX2Ls+fA-lKHC)sYd2DEt}v^i_odbNC%*dH(h*_x=xx7oYw=U z9>x1q;H+lw^|Z~>tJwfmcu(Fkm;ZeD*@%DPzW8a+qq0!#%5)ZVAN%aCFH>{K%E`)|qo^)n z@YAFQT=62Xx;n12leJ{vI6K`wk!iQJ|M>L5RN3L7#CJKPsq~2o+a{5>PB=cCPlV&{ z8$?M8ROEPUgLgum=hL^qWlo&F=HE~#THQjuG5B<}_4Sa8w63|?|NVHvwZiBPZrxMh zD{ef?owReA)O{$-axIRQ*-j6|Btjwo!3;{s1@tIieAJ!1CVlo3YQ3J{5t0K7+Pw&$ke+R&n(S&z%8v|X!Nt5L*pi5R$v zPS+>-lrLt#mrYd$OdP5RHL`i~4k<95JTP0V^|Igo9U@q4a3sd~0qYB~?faFQRZkzb z#&cYuC&nMHD`UE?#T~^pkE`ro_Qf+2kNe>R$dj+1$xJ_J7M&h^x8FwvM*~b-|SaozkXz{TNFTzfQm?Izavd3n6-#fx&X1roUK?_%-hTlB)oO*%(%@K;koC#;8_dP6GAs3ff9U5^jJY#Ta zv+o~@8fH@kR4gqj=~)AnN&{+N?HVO}!tAaE`N7qh(Y%v{gHj*3Lz@QrY`X{HMmF=u zmuk!Q{wUTu2Iasps7(W5fD3U6x9fm~5JOV*1Bw`+tHib&(8aPfu|AvGm#>>#=xuwt z#{RIEQ1`hj^s-sL>QzHZ*AM59#*{+y6F&>u8ISQ|?{sgwQdT-;nleS7=J_<`>3a6eH$6j1gp}SUZq?+3L(xrGpt^2;=q)TSl7G_!vDRz$p zF0@!$uxuhL{t(ADexhl?u0)LFnU1x~`?n&4-3DxK(9@?W9ggp)@lMsj&k`#P%e1n$L< z*E}+n)I|Dm%G+Nrd7?Xmy;q-CcNew5^1EQcDzs0q-2y#jE}ED@U*S7ye)U%8b)x(SI?|Q10`5;d*8MVduCuRvfQWNi(A?b&df9YRvp+os{OKGHbT|Xi3K0=)P zGYuQA67QLjvZ}}DLxQq~;Xa(gQ)~XUJzb@de4cB2y1Dyx;QiSnn}lkIB{$c-aakF3 z9GWYh=B7W_^P$;XluO|`b&OjX3*in3C=E@u#m8;hUjDUGG!%NSW5hD|i1V7#AU#oV zA|yaZi%`MX!L5UK(_cfnM0eJy6q=-125&j_ds*vB`{90J-!uXaa$kFHsa$zf$N) z{n}hQG;~UA87};d)dw}{M+?cV7jfwzG5%*6syu3&bNGAhQ^~$vb=h=1za^*FChl9F z}XYPi|U= z9OSOxtR^(t7I_OQe7+wQd{}m+eChTtkS*%&%3RXx(l;4STJwonATUifYt@YjS})T+ zCe2;;M%h@2mYPOkzLjqHw@~&<#xaw2rus;PJUzKC1}~CtcM_>dD6evCD* zD)~$Krt5V-Xp7WsDc+E6%m2bG7}7pQ>&mwGtLXN7=MKjJPAIXoOnX`an`v1Tc440q z3I+-Fg_ss9xqj*MP0EuB`n$i-YVQg-O9}qzp%V0fhZgN?c6X27PN)7FS!Xit;uG@n zy}1Ay=i9>}N0IVjURmYI#PPaK+9OuarxK?Gl&L!bs6vw%AK`5aLjNFq<7J9R=@Jk6)a53gT6Vx|l({#OUAcFG zv_FeiF0W@NiWbTDRlNpSP_$yMGZ9s}g&MPLRl92Z(~KLhr_%1qrrE_3u8)>2ceb=H zY0chfPUG2!Hb>&sJ}`dlS=VRDy0~YpzhTg({9pfbExi(;ajMkQfN|&FHT~*8dFNc6 zv1M_z-kr3ok1bzJkrZne+i5=0AU#l5q|kWIMT47@$Mg97py$sjSmB92CW7=Vqf8d> za}ckd?$LLogZsloV7xE|cJszQIM#d4(nK(Wk>zy4@!|fpe&<8Q{RQ(SQ>Pc);7|Gc zENG98@pek51ZKkZpRo#(f(knqeKTHx4Kx&QG~7GBf+*xYv&7O{_j~@0a^sP>ZX*Eha}CH(R&*njEjAR*o|zWFMW&!b!{G zY;_{3X*Sd$3v!@sUeBdlA07*|vb77x>Qw54b-rOfadi`2dqf4jviyt>zG9jkMVHr_ z!eL3bRngV0^T*wrlqbuxS!qz{&A2ch7$B$5pS^r5)d^#mR;KU5fr?}u^VXB4EVU!P zg-fPi^1FlX=ki1!iLM-fynk zR+wh;`D`ocJU-8iCjNVx^X;b@#A0Y@7+}#}{zCY8-*XPD{Lu_T>)^xe7=x2$%bGGy z@GhL+_D|>!;V$Qjy?&s%M6rCmRxX501}?O!KaAilJNxy|A1zJve=Z zr$k*1Oh1kAH%QCrr9)oETwl%;vRGLDT6ikVI*7oOz`0tn$t$LUc zpGn`fK0`4c#?rDH4zc#!lkUs~L9bVsLzMh-X6Ys0g5tsXl7cLWi}u&J0k$AbS}?>) z>!fiWQ#pz5b=}Os&XCd*KB~I&p>BE+#?2#0-vHGqrY#M9e`ph>bnL1OLeHkiX zNyly>rAU2X5%z{eWZSOo|KPLKm>XE&YwFJC3#j4$)*Q^g!1g?3%=_%c=Etd>2SYI2 z)HT@kK^$dZ2jjrtP-ZbstWQbUc_Jme!j4+ER$<}BUac2|iz*zx#feQ;cw-bb8|TFNw;D!JxN|961g*>7vyZr(qj0?`%6(f%i5*msvtCd^ z)%l=;B_(J(UHMpDO)036Wn+wU8bB3t$8X;yZH3*8$R|jtHT%S zt$NUJGZGt78O>ayeSfdMTh-NannZiJ-)69pyo{)|j^2=7bhb1>Cy?J+)wgzxpVD5n zDyZ&jXk91&EPHba?_?&8jQ*0#5W9ATJ zf5FBWMznSD?x(Dm6s#ZX5S*VV7TlIOhxTu=u8&QmYz_AXQ!YHJYr;n+R}0w#)$Ej7 zSBQ_#R9zx9J$+!v6c;sE(&1U$&tCelE_v>y2|kA-Is@6Jf!nE*eCt1gwyySF@b4DuYTr;-=KQhryt&H~ z{b`AG*{DX)lMmu~$SOTrg+>vJtWwF^wf!NPX?25LHCr-E^=TVunO>!=0e;-fai7~7 zO=GgYg8t}yerYHNe%FadKOa{5 zo&RaiY7G^y&7}JeTVGPw5lN<^I@w2KJK4Z(ZfmTseMEB?uWUq^a%a+xD|)%yY}z5n zbo#ZDEk|e^>ecjOE?ktatmM9J{Ypgi``r7Kh zW1INM{X0px}Q-}F><1MF*rm|L1w+`uNy`kKn-#3G3ump@( zl2;D`@mBMrrltj$in7Ct2GUDyj318-9(9>;eW^n~G>IGiRT#)kOA%DT z{??RX+i(U2HYMKk1T)+$_cw4ZrvaXa=t6JqupK4cIhs;*BuFwMjt2|I*s^a*n$OOq zLW(u?TYJY;S>HE}s;&{eD_@UK8G&)Br9^LVwymd2#74`#R(uIpSF#9GV4i+JjazV1H@o4fi-+j`jGnzI+FwLr5)5b zx$!N!j6^B^j)|ZQptI`cm7?4yy+O>gYiL>PZv#9D=A|*{=P#(=%JD~5vOgYu-Cp;S z2!zkA1SWMi?aE{t)5yAha7m>K^G@mj4!!1?y`+MtxvaJ4@8192@xGk744Tu4FF7^& zy~`taKntGiZH|97b-pGE9(-P0!SgOkTrH2T%dod4;)dO zXC+6Z)D+laTV35bB6hL!pjyrYn`5kov}x{t%NpZez_%8s)wihFx#*I!j=i9*g+aId zg#=FR8U~Qh^cXQu6CUd`p>aSTe3-NmnVlOuQ%OWXgJxADd1ilxTICinRTW+FLS$GP zQP|Md2Fhwa9{(_bED67va`R*N#`rm|9-O_>4laTswLjp;SfI!7G_9N9G1hf^r6b%l z6Ml@}W`cMx*Im_^ZMpPUuVSJm{-3Qx%G*dp$Yo|6H^MnIqh{4pQK z*Z2>piOK50Z0<>0N?m424YOMVhgVn73O+(eu*Ls;=;;>;uIbKyBv!MTW#aU>CSlwT zBtG)NIVHn03c64H8seAvY_6JEGoBuUykAsmtJuQ9Y$^KUTC|dxA=4ARReeAFo(z)# z3w+u0`~_A5My|C+XqryiXg3b%tmS(k?MTu>>zg|OYId;}Ua=;w2IgP%q3)v~_tXpE z*i3eS-kS6Ga;$Ttm*E?X{4}SFz6J1)EM&B6e~M7C@tBNGvj`)M$u6R?ctGrg*aH11 z6_h>6^b@o|jv-9b=kf7$9MK?5te_U8BnvOT75>8>_(R3Z_b=e9VFz6=Y7F-e-i0>$ z>+y&C(1)-@E0o0i-qaKff(aKS^+}vdBI2Cd+WuZ$q>^Lz(UMj#1>4--wNzBA4Xg?h z1NnILVLWZwpr=~`y@#B)LBq2C{?t5_KVTYsM}fcIO}w?6toP|m=&xIgy(Ml$t-W?? zD!lbNM1;#@pz71~H()!=eT(SQv2w5|YPLtv=bq;s zC#dZs8#=!HY&3&N8s85)W}u3~@XK0>85CK(4SM`9=<%v*@OX1GmzHK5L;|b@-BewO zBDun%}8Oy7RLqXl^8=Wat$HT z{ES#$VZ=MV#H`582(i2MyGUUgSnp;}JT#y;i_Y9|m-1<$$_}n7mk!D7>!wB2|KFotaQNm&+TYXi2?p(KPvxRCq zLSDF{G?p#e%xg#5%iHyVqN9J@A&E+@n^0QyYOY)MRsK*_`f&T=k!o>WDiY~=Xm7rr zQqngC=kwIY#`XuH+C!v!o=7hk!J%&^@OlSgm=P7VQ@rfNJ$Rw#C81h0GEN5u2_%FlQFyjqz5Jk7VAW1~BqjAOh$uo~OVG5l2Q;Vl?yyAmOG?0l~bO`~HQ)7RdZJUAXZ!$zu42 zuGM?$mmY%^|Hcs!2D7{LBmq;!Pi`43LoaPeKsxaB`Gee;C}F5HwJ8MzHUVui!90$} zlb0LXzEzWktUBe6HorYH4gDe#58C~swPXuAsj#r%b-dp3$&jC_YVc6v(K8!@+5Z}sB!wG(buCjBAUBn@3a65+P*iPv*Skek59 zS+(|G!q2WGJEJI?kRUI~SlOk&%RaNBMjH`QV}Gmfm1w#u1qNEK)!VDP-=u3WHwzd+?PRTDuf%S1&@aO#JOCTg^^@Ogv8k~1$WW~gK*TL!ya8=j{~i~_+yd-Q+Gn%&)`Upt-BgkE2wA8SKZh%5jN`|kV@80r zU#WT;8dKX464Y!f7*o&1op)bSXPfl(eY@b4KL=jcV21YCtp+xyb}sOcpht<9vVKK# zjnqbSoduA=TxH{ne@((&Ytf$t*=Udop2C^=;N_j{U(TVNv%LvGkhozBZiJ`+-^Vxt z-n%a=^-~R7TENN#Ge_L0+8YjYFn~n}xY--330jSaP_%i<5DI@l6I_mT?cC}>{;+l9 ztaJOL4UN2PEw>5l?Jo&`Lk2)C2Fx;XW4EWGx$~v5WyttxC4^$jRjZB`CwGUZV#z^- z9Goxuu9*r3SkaZuDuy2CCBeODHd?wtjaC`11~Ffc$92ziYq+{p6ygArzFE!A4_xhZ zfaB5<+wQ(n@KeA>WFX_5 zbTZ;Y9tz|8om(k2#+WudxKb_~B0PSD_Y?!9teYGo&CZxjk9Z@d>GKL$*&aQ#c(tow zmyI76RkWH&+mUi0E1e`k0#IuWK3_qTa>ciR_6muMIUT?AIBY$^M6QSs?~H^+3^*W{ zzt+kCnL(X({Xz(0k_z!7+y^zxsLvJ%{7b`#yqCK&60P9W1fc2PJ%^zYo9 zm`|N8+K>Of1})?bSat0^zbeCvls+Q2X+G|VB$afaA8dx1W`gTIG^x$Y9hXOMXbQIq z?dqn`t|ReyfV5?5322LXjA3Bo!hso2Zv}Dr*%}JoLm;-Q8E@fpTIhdX?Acf{1hPk&ebL;T1r>7_?QDiY{cd5Xgd~}PyHj4RM=9k#*36}(}#s$W?gI2v=(y5|3X$lm5c;` z5$6mrF-tyFgnt4m%0MrKPLiDu^p|9G>_Xd6b`ovRqW$gVQpdknb53qB>(-xezNu(% zR?L@R8+8pY%4LCLDzzzcMqBX6hfw~|_m8kt8<>}?2*DgKb}wKlVrw&j#K%ny#OK;y zQ(_u64xREBJ+uj9_*H%L=LktjNU+|aoC2YBKmoJw=1u7L1fKSlv|4iMZBohNjW0GV zcdQp}YWQU^!}jkJPDd{y+MY37i( zDZH(>az>+qw>kory$;FIlK0q{YTceY3mB*)owLUFg}k8QaKuOb${i#zuI#W)j{0zs zAc3A|KH!)t)1)q-%^oLY2N&`Efd_W3aNmj6|1&o1K)xFMQr{jFi3hP1?b~7{VRA8; z(FTme<$*rotsE~3Qc{9p7x>;xdH()IYK)lgj9iT6hT~qy&>TR~Iq}&M)aqvVzt|1Q zlG&`45!q8Y$P#-VL;F2s5kg{-!x&TVQ^7`7*!O4XUl#f#E=0xxy*&FvPOWPy$$Wgn zi~io@5AwqQ_A3O=r4Cv7{!(PFlYURGnc8tUqauHWMv^I@mI&d-2M^;bdcS-T`y&lx z_#ha^*oEzaJhu|?MzgB^kf^?0BN5HB?IqGtVzbU=1vH`VuVaRq8E?%tBQsFqjDMY( zVIgq@TDa$7ctJwvC-nNFJ7{AY_NX^61W%d{tBs5yza|si+2y(q=|%R--xm-6!*NX{ zl?)@{@hWhQWU{A*9soN4+fky2Hl?Jx6*74v=lrHu9jo)A%oAv7!uC|0Tr=ed3ZHdR z9Mz4cLAaKo>9*PR!5EDb&L2R2F#g2ET958wCVi?CrAKh~0Om@~R@Z2IKax$RL?qHr zOv6{ojew2;vGT=lOz&D%ewxIpRd3sU)t%bvUgN&Mx|*Axjwm~#6-8P^(S6X5PLr)9 zYMui9W(q8oIoXj7?xo`dO0E(ODtLQT(rd|@npba$Hfk(%`)lMG5Hpc?B7veYMUN77 z$pNA2V^C@SUs{QP7R_LbbeXB6gI44Z=VWNbn$T+*J5bJ-=)aC$8(iHL0N)ETYsO|fS!d5-j)adF+{pDn@|vZ~a6YwlnTaC%;r zmMA18jMpIu!PpVE)6yYEF`FK32(a|x&7gV}ZN3)P^ho3d2!)hq99Gw(8DkHUXmnUM z@VSWRAe<0=O(+b=I#cz#YH2sgnddiqDGr&0c>1oKYC}Z z;~6`Fi=5nD6#fu5DlfqbX}#FzyXj-UE*zZE*X2$sd<~j(J@_@kSV8eeW{g!~}10^i>9Jt~EikH}`oU4t^-)YbA*J&$eteMdSy@{98~^NW=?~LM9wh zoxW*{EZEV?ag4MBVA7$Lg3de7HnYE{Kdya0}}r(T2T zTcY#}(PM-k^K<&*Q_F|70F5keoVDY2q%hjh#kK(yX=7rVDawH%J^6mN8Mf)@-gE5n_pYRn5_*|ihbb6dOy;7;Zy}Q!AI0+5-H-z)d zc^^?pNNU~tXv65Jd2dLy&B6oehl`-=wcd!WV63{=)ec&pGzigtzrVXov8x)> zyXHJ>d0llc8A0GX74$ZyRy`>OtOqwgkZT173(S=VTG|j8Tdimk=GJO~tgeA-dfT@c zs6&2vrJCtymMmAFb%PiuttX>a(@agE|`*joM9$oHK))M*|nJ~nIx)szOc5lZiN?6>6ew;WMM z(m>3?Aqf*DE%4M z=2lrO+>wx-_%PneWSXVGZz99;na3i3K_oz_1;E9~JcttZN|SN)d_W*~SxG(-wVrn) z_oVNUc$JkAu4@vrDD(t5)B9}*shie64UT5sG6|JE}o4rMguCAms%@>OY@$NpjkL>BXXZ&2>;oT*KGPhlE zPsoonL~jvKF&~j+VV5cc7E;A)c}o8u`S0!96+ePkv8k!Xex*c&?VaJe3ys~}u`jcj zcbKIys`VyPIed(pl5}Cgi8Uqb*u1@ zE?`E*ZC1Rf#ChY3Gb9(Y$Jyjmq%v)RNCgPzV6epdLP%3-90*8~4sLp?pY?XI5WNs0 z98ha(bY;sKD}+O`(PO-q*|+O|${m*pF#smxloJ3_#91&_B9DYZV8F?Rm1SbG?uGh% z>T+bkaNIK#3R;`_l_Pvjw&hw$?`7zZAtW)h0C@$%^_ig;J-;t^%Spwux`-EdtMR3mV4rYk-B(gQI@9p2ds>q310XK@r zSw`H%&V)v>?@%%2M;8MSCHf=r>qsAVNPv+nj7qf$OCNSWI+D#&Z5&590ci1K9#ljK zP<#@gcYqrUJ_0iEP|cV zC>2LVadL9N%;ZFAOlQGK=Qd;f2CK&$^~+u>>s27ev3s3E*2d@%$EW6D0gYwES9hxA zwz%xU)AV8#8ZZVX4mE+$6RMAn8Q)#~A$pH}5`CnNmCiw;_zF_LfFbSMVFqF4{)Dmo>kPoyJ^6t_vc5!^?okIy zoumF^cv;UJ{HUOw+(>n(*tE3NE)z$kR*R*12r&>XIC3(d3m?;Tv&sgq!mfKE_(iP5 zGMl9JKW;(EokSk9WG!Gv10=yH4)P>Os}Kj1Y_{An&oxp*Dc%Wt1UkD&8rJ@4{{i$l z2`~8ouofUXqOMi{11?AJ+6v1clVhlP?5CP4?l-gaBGIa> zF8&7r$aP-|`@&h}sjvOv)FwnDF{TVqs8H)?2PE8=9i}jigcmhs;?1f2(Ij$W7OJTt z#-g#ds3TKTb@~(VWB2u9s~jHNCbxh1I+j&e_9kk>r?D>;n{j5$%kphnPAhU(=8D*$ z_FS=>wC3!i#yi6g!9Mu4GvOhj#PSpO|0%b%fy zU@9@oZqa0AyeL$V?CP8j<$~fzGDsm4PFT?0CQd00Tm8NI5P#U+wA^l@;$9Z^v<5a< zdS~bcfG36tc;2<;YJ@4%s_`KFaFpRTdqF#I10}|HBl)oDDF=W=6k9HeLWp-2Xn9L^ ze`i*wcDzmF&|Y8LDgMl%BH$}LS385pqPf5`FCr5`;O)9rv*DO>Bhw_uy6LwI(fz$2 zbt@L}W1mZpD#*RrHMu-0tq%mqZ3~n4$hf+lN|W3}Sq`ETWmpK+WWX&^7M|?gzoifV z^aSI`HxfSJRS*sw-xEqW5LL|tbTj<bAN zIB){N8n>?_h=MtO_+gfrr31F)jJ0Ti3C)g>RO+VOm|Cxzp|9Jieg!lhj-|7&ps0+< z$Si#;EEotUO=*5i$y~WW!R=-W|IS3@y)0k{qTOY#bodf+(G9(Xp#vCSQ}fvCqq~41 z-1)%Tx{(tECBK$x;iY7ZQ;3h)fN#rjx(;!f#XCsjK&6XM;*YJT!JHTH$M>KpnJC2m zV8}w<0OD9BTmEohBgX@{2hv%*AyC$jA_+bV3?q#jsWe9dWiOnq&Cjn7m{crtCO@lPjQa2{~2nV276Dh0Qxs zhuC5iG{qlQ;E%6KOaodhO!5r|bnP-;+@i}iVWG(l&TM9{_-Zy;Q@-$~Nb?EP|GaD| z8(yRe6p@@p3MIck$Vkl0vkh-(0;MP1mSMWf&BH{{dvV8DJ*Zs$h59%ofo}Hr{jQk} zUhJcKgMeG0grOyy*(xr;u4iaDhAP>LIosZT_OfjP(>tZD*}Bbzvb4TtKe7x%NAMF` zA2)bW6a>i|AxNFhfsjsI9DWGiy$i=uMC+NCE2dHA7Q!3n>ow26E){kSi1R(M>NE0j zRFJI_^4Bv?!e-?%akF=QyM4x+El})Al(gb_qGVN1ZYo&jL|g9Q`V0tXglKBnD){Z9 zy{ROh52<~;CH38SVa_d*|2<0&fL={9kcp1IGVd}mpuM-8!5qY;6AGBhhQg80Ud);V zs;&)}{SHtSKz-6ee#97jL+t_9t>gFZKpu5*^XMrk8lr~_0;n!n^wb{UolyhBt*}Os z&!);&fDfz-pjv;|nn&1gm24`$heqM8%x&xD2$=`#9NGCh0HT=B!n-%uevx@#V~(O- z`jFPK)W8Xd<*?cJ#C#)Tb5CwJ*iCZ2&-bThVfwk`piv+Y8~cMRk3eEKXHuH!jseVf z>MF?^gtFnp&6D~m{Gfw<8C+*rE^}`z2yHzh=f+TG7n7)oI{a2K+BALub-g?mt<}K; z%UnMj=d zhdj!&hQT}l$jA&)ot6LoMPMWbA&gZxI;>saRm*y{g5qhW7jhuc7Wp3TYw$}b$3*nt z>H{MF&?0-Nb4r!H0AfreQR6fB<-WgEyIB|ijI)NHhX)>6KctRg(&TRiaaKx)DGA=@C;pi5fM&MQN<9{l^|61 zJERvhm-lii1K!AnZgbnMK1T{+gE>*L-b1K@2U4|Ih{pE%k;rAYAz*oXM1Br~=+Q-d z0}QeA=9X3VwKYxf+g2W zLM)G$-{Y0Oc!-P4^fNE$Z5GDY(tI%~{lbN^kjm_-p1Dxi!_dMqX>cMZsymq^cn>IE z1C+Ysi@cVLxQ0r22*!3qOjhaiP?ex!&5RZPg7s}elk2Zb0K8H#ChV{14C6 z5JN-r?U4FI%+0D-zM?Z}H0Ap4GYX#HmE{g;S8D>E3uwACe*Q2r# zg-!I~2Q#|i!B5rdyVK!#hQ($3T zMfK=NkJx%qI{0J^?P~G338R2U`}Bnv5#Gy4d>OVEp54QepBNwl9Mv1mfJL^H0vj9f zzZl4NZEBDpVdzytW*H?yZbI9jgdMzW$?^v#0%uag&1DzG59OYDGxl%|A4BUT4i=IQ zOTd=&DTyyiq^iJlG2=>@Vu38V3whd*-K4ufOC&@HCtn_On1&+;DyZO|$i{3Q(eHL9 zSr%HrqP?IkPtRrva*%`K;iYo3IzC?j_-%S3KQT4rdyr0uun0ii+gHesoCFT9kr~Tv zgPXA6nFibN;DVp5f)IZAOaZy0_cge6be;hK;9t^7U%HnBb;+ppMhVX&Tc#cKVj;Z< zjH;HV4Xi@u%6?@CQziw|!@g$l*cwgDOfsn0qY}P`Z})_`qwP{lKq^Iu+~y^NSkup& z+&=g6rC(hMuR=5p3wXFoh(yP1;ZvuH2xw z*^6z>r0#HFPyHk@+X}xOa>6Ux6NReb(+^OW+ujn@SctDDQ)MS)8NKfC6?0Pzc^Eg| zYqwE2UT7L};~|zq3?MO1B*Nj1j~6FKLZw2%$^F?ObJiy zd3wIh^-#Se$v|aTyapiabdDhzRpGXw)s+2xo7)4(1^*Z6fg&aRpA$M3ga2bJ$1;NcorH^y@n1dvz>h=! e{{PiirF6y9vs#S@jJ?s&_UyE=%(8I%^Zx_ILM$2p diff --git a/web/public/services/jira.svg b/web/public/services/jira.svg new file mode 100644 index 00000000000..5e5cebc843a --- /dev/null +++ b/web/public/services/jira.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + From 98974fdc50ecc28b1c2ce32533d43e3a6f804a17 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:14:47 +0530 Subject: [PATCH 16/62] fix: cycle and module bug fixes and improvement (#2691) * fix: cycle and module card issue count fix * fix: cycle and module list progress icon fix * fix: module card progress fix * style: cycle & module empty date label updated * fix: build error --- space/constants/data.ts | 22 ++++++++-------- web/components/cycles/cycles-board-card.tsx | 17 +++++++------ web/components/cycles/cycles-list-item.tsx | 8 +++--- web/components/modules/module-card-item.tsx | 28 +++++++++++++-------- web/components/modules/module-list-item.tsx | 10 +++++--- 5 files changed, 50 insertions(+), 35 deletions(-) diff --git a/space/constants/data.ts b/space/constants/data.ts index 29d411342fb..909b74c120f 100644 --- a/space/constants/data.ts +++ b/space/constants/data.ts @@ -10,12 +10,12 @@ import { } from "types/issue"; // icons import { - BacklogStateIcon, - UnstartedStateIcon, - StartedStateIcon, - CompletedStateIcon, - CancelledStateIcon, -} from "components/icons"; + BacklogGroupIcon, + CancelledGroupIcon, + CompletedGroupIcon, + StartedGroupIcon, + UnstartedGroupIcon, +} from "@plane/ui"; // all issue views export const issueViews: any = { @@ -92,35 +92,35 @@ export const issueGroups: IIssueGroup[] = [ title: "Backlog", color: "#d9d9d9", className: `text-[#d9d9d9] bg-[#d9d9d9]/10`, - icon: BacklogStateIcon, + icon: BacklogGroupIcon, }, { key: "unstarted", title: "Unstarted", color: "#3f76ff", className: `text-[#3f76ff] bg-[#3f76ff]/10`, - icon: UnstartedStateIcon, + icon: UnstartedGroupIcon, }, { key: "started", title: "Started", color: "#f59e0b", className: `text-[#f59e0b] bg-[#f59e0b]/10`, - icon: StartedStateIcon, + icon: StartedGroupIcon, }, { key: "completed", title: "Completed", color: "#16a34a", className: `text-[#16a34a] bg-[#16a34a]/10`, - icon: CompletedStateIcon, + icon: CompletedGroupIcon, }, { key: "cancelled", title: "Cancelled", color: "#dc2626", className: `text-[#dc2626] bg-[#dc2626]/10`, - icon: CancelledStateIcon, + icon: CancelledGroupIcon, }, ]; diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index b480680a6a2..89e1bef893a 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -44,6 +44,7 @@ export const CyclesBoardCard: FC = (props) => { const isCompleted = cycleStatus === "completed"; const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); + const isDateValid = cycle.start_date || cycle.end_date; const router = useRouter(); @@ -64,9 +65,7 @@ export const CyclesBoardCard: FC = (props) => { ? cycleTotalIssues === 0 ? "0 Issue" : cycleTotalIssues === cycle.completed_issues - ? cycleTotalIssues > 1 - ? `${cycleTotalIssues} Issues` - : `${cycleTotalIssues} Issue` + ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` : `${cycle.completed_issues}/${cycleTotalIssues} Issues` : "0 Issue"; @@ -225,10 +224,14 @@ export const CyclesBoardCard: FC = (props) => {
- - {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} -{" "} - {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} - + {isDateValid ? ( + + {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} -{" "} + {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + + ) : ( + No due date + )}
{cycle.is_favorite ? (
- {!isArchived && ( - - )} + {issue?.created_by !== user?.id && + !issue?.assignees.includes(user?.id ?? "") && + !router.pathname.includes("[archivedIssueId]") && ( + + )} @@ -250,7 +256,7 @@ export const IssueView: FC = observer((props) => { issue && ( <> {["side-peek", "modal"].includes(peekMode) ? ( -
+
= ({ issueDetail, handleCycleCh diff --git a/web/components/issues/sidebar-select/module.tsx b/web/components/issues/sidebar-select/module.tsx index 9948753d422..0eff28a1e72 100644 --- a/web/components/issues/sidebar-select/module.tsx +++ b/web/components/issues/sidebar-select/module.tsx @@ -74,17 +74,19 @@ export const SidebarModuleSelect: React.FC = ({ issueDetail, handleModule > From 1986c0dfd4bdd689322fe542c0e4d621ea53a304 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:15:34 +0530 Subject: [PATCH 20/62] fix: state icon (#2678) --- .../filters-render/state/filter-state-block.tsx | 2 +- space/components/issues/navbar/issue-filter.tsx | 2 +- .../issues/peek-overview/issue-properties.tsx | 4 +++- space/constants/data.ts | 14 -------------- space/types/issue.ts | 8 ++++++-- 5 files changed, 11 insertions(+), 19 deletions(-) diff --git a/space/components/issues/filters-render/state/filter-state-block.tsx b/space/components/issues/filters-render/state/filter-state-block.tsx index 8445386a48a..9b6447cb6d3 100644 --- a/space/components/issues/filters-render/state/filter-state-block.tsx +++ b/space/components/issues/filters-render/state/filter-state-block.tsx @@ -29,7 +29,7 @@ export const RenderIssueState = observer(({ state }: { state: IIssueState }) => return (
- + {/* */}
{state?.name}
{ return { display: ( - {stateGroup && } + {/* {stateGroup && } */} {state.name} ), diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx index f7ccab18f08..54e9c4f6afb 100644 --- a/space/components/issues/peek-overview/issue-properties.tsx +++ b/space/components/issues/peek-overview/issue-properties.tsx @@ -1,5 +1,7 @@ // hooks import useToast from "hooks/use-toast"; +// ui +import { StateGroupIcon } from "@plane/ui"; // icons import { Icon } from "components/ui"; // helpers @@ -63,7 +65,7 @@ export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mod {stateGroup && (
- + {addSpaceIfCamelCase(state?.name ?? "")}
diff --git a/space/constants/data.ts b/space/constants/data.ts index 909b74c120f..bb903069661 100644 --- a/space/constants/data.ts +++ b/space/constants/data.ts @@ -1,6 +1,5 @@ // interfaces import { - IIssueBoardViews, // priority TIssuePriorityKey, // state groups @@ -8,14 +7,6 @@ import { IIssuePriorityFilters, IIssueGroup, } from "types/issue"; -// icons -import { - BacklogGroupIcon, - CancelledGroupIcon, - CompletedGroupIcon, - StartedGroupIcon, - UnstartedGroupIcon, -} from "@plane/ui"; // all issue views export const issueViews: any = { @@ -92,35 +83,30 @@ export const issueGroups: IIssueGroup[] = [ title: "Backlog", color: "#d9d9d9", className: `text-[#d9d9d9] bg-[#d9d9d9]/10`, - icon: BacklogGroupIcon, }, { key: "unstarted", title: "Unstarted", color: "#3f76ff", className: `text-[#3f76ff] bg-[#3f76ff]/10`, - icon: UnstartedGroupIcon, }, { key: "started", title: "Started", color: "#f59e0b", className: `text-[#f59e0b] bg-[#f59e0b]/10`, - icon: StartedGroupIcon, }, { key: "completed", title: "Completed", color: "#16a34a", className: `text-[#16a34a] bg-[#16a34a]/10`, - icon: CompletedGroupIcon, }, { key: "cancelled", title: "Cancelled", color: "#dc2626", className: `text-[#dc2626] bg-[#dc2626]/10`, - icon: CancelledGroupIcon, }, ]; diff --git a/space/types/issue.ts b/space/types/issue.ts index 206327fcdac..4b76c75e8d9 100644 --- a/space/types/issue.ts +++ b/space/types/issue.ts @@ -24,7 +24,6 @@ export interface IIssueGroup { title: TIssueGroupTitle; color: string; className: string; - icon: React.FC; } export interface IIssue { @@ -40,7 +39,12 @@ export interface IIssue { sequence_id: number; start_date: any; state: string; - state_detail: any; + state_detail: { + id: string; + name: string; + group: TIssueGroupKey; + color: string; + }; target_date: any; votes: IVote[]; } From 26de35bd8d31baf0880cf9212061a0468ed8d6dc Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 7 Nov 2023 17:17:10 +0530 Subject: [PATCH 21/62] fix: environment config changes in the API are replicated in web and space app (#2699) * fix: envconfig type changes * chore: configuration variables (#2692) * chore: update avatar group logic (#2672) * chore: configuration variables --------- * fix: replacing slack client id with env config --------- Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> --- apiserver/plane/api/views/config.py | 6 +- space/components/accounts/sign-in.tsx | 4 +- space/services/app-config.service.ts | 11 +-- turbo.json | 4 +- web/components/integration/github/auth.tsx | 16 ++++- .../integration/single-integration-card.tsx | 16 ++++- .../integration/slack/select-channel.tsx | 19 ++++- web/components/page-views/signin.tsx | 32 +++++---- web/hooks/use-integration-popup.tsx | 25 ++++--- web/lib/mobx/store-init.tsx | 72 ++++++++++--------- web/services/app_config.service.ts | 14 ++-- web/store/app-config.store.ts | 47 ++++++++++++ web/store/root.ts | 3 + web/types/app.d.ts | 9 +++ 14 files changed, 189 insertions(+), 89 deletions(-) create mode 100644 web/store/app-config.store.ts diff --git a/apiserver/plane/api/views/config.py b/apiserver/plane/api/views/config.py index 687cb211c4d..a06a7f7fce4 100644 --- a/apiserver/plane/api/views/config.py +++ b/apiserver/plane/api/views/config.py @@ -21,8 +21,8 @@ class ConfigurationEndpoint(BaseAPIView): def get(self, request): data = {} - data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None) - data["github"] = os.environ.get("GITHUB_CLIENT_ID", None) + data["google_client_id"] = os.environ.get("GOOGLE_CLIENT_ID", None) + data["github_client_id"] = os.environ.get("GITHUB_CLIENT_ID", None) data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None) data["magic_login"] = ( bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD) @@ -30,5 +30,5 @@ def get(self, request): data["email_password_login"] = ( os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1" ) - data["slack"] = os.environ.get("SLACK_CLIENT_ID", None) + data["slack_client_id"] = os.environ.get("SLACK_CLIENT_ID", None) return Response(data, status=status.HTTP_200_OK) diff --git a/space/components/accounts/sign-in.tsx b/space/components/accounts/sign-in.tsx index 0c6810198af..b55824e6c67 100644 --- a/space/components/accounts/sign-in.tsx +++ b/space/components/accounts/sign-in.tsx @@ -116,7 +116,9 @@ export const SignInView = observer(() => { )}
- {data?.google && } + {data?.google_client_id && ( + + )}

diff --git a/space/services/app-config.service.ts b/space/services/app-config.service.ts index 713cda3da1b..09a6989efe1 100644 --- a/space/services/app-config.service.ts +++ b/space/services/app-config.service.ts @@ -3,12 +3,13 @@ import APIService from "services/api.service"; // helper import { API_BASE_URL } from "helpers/common.helper"; -export interface IEnvConfig { - github: string; - google: string; - github_app_name: string | null; +export interface IAppConfig { email_password_login: boolean; + google_client_id: string | null; + github_app_name: string | null; + github_client_id: string | null; magic_login: boolean; + slack_client_id: string | null; } export class AppConfigService extends APIService { @@ -16,7 +17,7 @@ export class AppConfigService extends APIService { super(API_BASE_URL); } - async envConfig(): Promise { + async envConfig(): Promise { return this.get("/api/configs/", { headers: { "Content-Type": "application/json", diff --git a/turbo.json b/turbo.json index 7c3ccb81a7d..ac462d08bae 100644 --- a/turbo.json +++ b/turbo.json @@ -5,7 +5,6 @@ "NEXT_PUBLIC_DEPLOY_URL", "NEXT_PUBLIC_SENTRY_DSN", "NEXT_PUBLIC_SENTRY_ENVIRONMENT", - "NEXT_PUBLIC_GITHUB_APP_NAME", "NEXT_PUBLIC_ENABLE_SENTRY", "NEXT_PUBLIC_ENABLE_OAUTH", "NEXT_PUBLIC_TRACK_EVENTS", @@ -22,8 +21,7 @@ "SLACK_CLIENT_SECRET", "JITSU_TRACKER_ACCESS_KEY", "JITSU_TRACKER_HOST", - "UNSPLASH_ACCESS_KEY", - "NEXT_PUBLIC_SLACK_CLIENT_ID" + "UNSPLASH_ACCESS_KEY" ], "pipeline": { "build": { diff --git a/web/components/integration/github/auth.tsx b/web/components/integration/github/auth.tsx index c94bfacd572..9d5816f3b09 100644 --- a/web/components/integration/github/auth.tsx +++ b/web/components/integration/github/auth.tsx @@ -4,14 +4,24 @@ import useIntegrationPopup from "hooks/use-integration-popup"; import { Button } from "@plane/ui"; // types import { IWorkspaceIntegration } from "types"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { workspaceIntegration: false | IWorkspaceIntegration | undefined; provider: string | undefined; }; -export const GithubAuth: React.FC = ({ workspaceIntegration, provider }) => { - const { startAuth, isConnecting } = useIntegrationPopup(provider); +export const GithubAuth: React.FC = observer(({ workspaceIntegration, provider }) => { + const { + appConfig: { envConfig }, + } = useMobxStore(); + // hooks + const { startAuth, isConnecting } = useIntegrationPopup({ + provider, + github_app_name: envConfig?.github_app_name || "", + slack_client_id: envConfig?.slack_client_id || "", + }); return (

@@ -26,4 +36,4 @@ export const GithubAuth: React.FC = ({ workspaceIntegration, provider }) )}
); -}; +}); diff --git a/web/components/integration/single-integration-card.tsx b/web/components/integration/single-integration-card.tsx index 28fca6fcdd1..999a12bb5b7 100644 --- a/web/components/integration/single-integration-card.tsx +++ b/web/components/integration/single-integration-card.tsx @@ -20,6 +20,8 @@ import { CheckCircle } from "lucide-react"; import { IAppIntegration, IWorkspaceIntegration } from "types"; // fetch-keys import { WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { integration: IAppIntegration; @@ -41,7 +43,11 @@ const integrationDetails: { [key: string]: any } = { // services const integrationService = new IntegrationService(); -export const SingleIntegrationCard: React.FC = ({ integration }) => { +export const SingleIntegrationCard: React.FC = observer(({ integration }) => { + const { + appConfig: { envConfig }, + } = useMobxStore(); + const [deletingIntegration, setDeletingIntegration] = useState(false); const router = useRouter(); @@ -49,7 +55,11 @@ export const SingleIntegrationCard: React.FC = ({ integration }) => { const { setToastAlert } = useToast(); - const { startAuth, isConnecting: isInstalling } = useIntegrationPopup(integration.provider); + const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({ + provider: integration.provider, + github_app_name: envConfig?.github_app_name || "", + slack_client_id: envConfig?.slack_client_id || "", + }); const { data: workspaceIntegrations } = useSWR( workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null, @@ -132,4 +142,4 @@ export const SingleIntegrationCard: React.FC = ({ integration }) => { )}
); -}; +}); diff --git a/web/components/integration/slack/select-channel.tsx b/web/components/integration/slack/select-channel.tsx index fb5393f3a51..eaaa3daee37 100644 --- a/web/components/integration/slack/select-channel.tsx +++ b/web/components/integration/slack/select-channel.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; +import { observer } from "mobx-react-lite"; // services import { AppInstallationService } from "services/app_installation.service"; // ui @@ -11,6 +12,8 @@ import useIntegrationPopup from "hooks/use-integration-popup"; import { IWorkspaceIntegration, ISlackIntegration } from "types"; // fetch-keys import { SLACK_CHANNEL_INFO } from "constants/fetch-keys"; +// lib +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { integration: IWorkspaceIntegration; @@ -18,14 +21,24 @@ type Props = { const appInstallationService = new AppInstallationService(); -export const SelectChannel: React.FC = ({ integration }) => { +export const SelectChannel: React.FC = observer(({ integration }) => { + // store + const { + appConfig: { envConfig }, + } = useMobxStore(); + // states const [slackChannelAvailabilityToggle, setSlackChannelAvailabilityToggle] = useState(false); const [slackChannel, setSlackChannel] = useState(null); const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { startAuth } = useIntegrationPopup("slackChannel", integration.id); + const { startAuth } = useIntegrationPopup({ + provider: "slackChannel", + stateParams: integration.id, + github_app_name: envConfig?.github_client_id || "", + slack_client_id: envConfig?.slack_client_id || "", + }); const { data: projectIntegration } = useSWR( workspaceSlug && projectId && integration.id @@ -97,4 +110,4 @@ export const SelectChannel: React.FC = ({ integration }) => { )} ); -}; +}); diff --git a/web/components/page-views/signin.tsx b/web/components/page-views/signin.tsx index ccaa7d2c3c9..547170632c9 100644 --- a/web/components/page-views/signin.tsx +++ b/web/components/page-views/signin.tsx @@ -1,5 +1,4 @@ import { useState, useEffect, useCallback } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; import Image from "next/image"; import { useRouter } from "next/router"; @@ -8,7 +7,6 @@ import useToast from "hooks/use-toast"; import { useMobxStore } from "lib/mobx/store-provider"; // services import { AuthService } from "services/auth.service"; -import { AppConfigService } from "services/app_config.service"; // components import { GoogleLoginButton, @@ -24,12 +22,12 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types import { IUser, IUserSettings } from "types"; -const appConfigService = new AppConfigService(); const authService = new AuthService(); export const SignInView = observer(() => { const { user: { fetchCurrentUser, fetchCurrentUserSettings }, + appConfig: { envConfig }, } = useMobxStore(); // router const router = useRouter(); @@ -38,12 +36,16 @@ export const SignInView = observer(() => { const [isLoading, setLoading] = useState(false); // toast const { setToastAlert } = useToast(); - // fetch app config - const { data, error: appConfigError } = useSWR("APP_CONFIG", () => appConfigService.envConfig()); // computed const enableEmailPassword = - data && - (data?.email_password_login || !(data?.email_password_login || data?.magic_login || data?.google || data?.github)); + envConfig && + (envConfig?.email_password_login || + !( + envConfig?.email_password_login || + envConfig?.magic_login || + envConfig?.google_client_id || + envConfig?.github_client_id + )); const handleLoginRedirection = useCallback( (user: IUser) => { @@ -114,11 +116,11 @@ export const SignInView = observer(() => { const handleGitHubSignIn = async (credential: string) => { try { setLoading(true); - if (data && data.github && credential) { + if (envConfig && envConfig.github_client_id && credential) { const socialAuthPayload = { medium: "github", credential, - clientId: data.github, + clientId: envConfig.github_client_id, }; const response = await authService.socialAuth(socialAuthPayload); if (response) { @@ -195,7 +197,7 @@ export const SignInView = observer(() => { Sign in to Plane

- {!data && !appConfigError ? ( + {!envConfig ? (
@@ -211,7 +213,7 @@ export const SignInView = observer(() => { <> <> {enableEmailPassword && } - {data?.magic_login && ( + {envConfig?.magic_login && (
@@ -219,8 +221,12 @@ export const SignInView = observer(() => {
)}
- {data?.google && } - {data?.github && } + {envConfig?.google_client_id && ( + + )} + {envConfig?.github_client_id && ( + + )}

diff --git a/web/hooks/use-integration-popup.tsx b/web/hooks/use-integration-popup.tsx index 58cfbc009b8..fb9aab2238a 100644 --- a/web/hooks/use-integration-popup.tsx +++ b/web/hooks/use-integration-popup.tsx @@ -1,23 +1,26 @@ import { useRef, useState } from "react"; - import { useRouter } from "next/router"; -const useIntegrationPopup = (provider: string | undefined, stateParams?: string) => { +const useIntegrationPopup = ({ + provider, + stateParams, + github_app_name, + slack_client_id, +}: { + provider: string | undefined; + stateParams?: string; + github_app_name?: string; + slack_client_id?: string; +}) => { const [authLoader, setAuthLoader] = useState(false); const router = useRouter(); const { workspaceSlug, projectId } = router.query; const providerUrls: { [key: string]: string } = { - github: `https://github.com/apps/${ - process.env.NEXT_PUBLIC_GITHUB_APP_NAME - }/installations/new?state=${workspaceSlug?.toString()}`, - slack: `https://slack.com/oauth/v2/authorize?scope=chat:write,im:history,im:write,links:read,links:write,users:read,users:read.email&user_scope=&&client_id=${ - process.env.NEXT_PUBLIC_SLACK_CLIENT_ID - }&state=${workspaceSlug?.toString()}`, - slackChannel: `https://slack.com/oauth/v2/authorize?scope=incoming-webhook&client_id=${ - process.env.NEXT_PUBLIC_SLACK_CLIENT_ID - }&state=${workspaceSlug?.toString()},${projectId?.toString()}${ + github: `https://github.com/apps/${github_app_name}/installations/new?state=${workspaceSlug?.toString()}`, + slack: `https://slack.com/oauth/v2/authorize?scope=chat:write,im:history,im:write,links:read,links:write,users:read,users:read.email&user_scope=&&client_id=${slack_client_id}&state=${workspaceSlug?.toString()}`, + slackChannel: `https://slack.com/oauth/v2/authorize?scope=incoming-webhook&client_id=${slack_client_id}&state=${workspaceSlug?.toString()},${projectId?.toString()}${ stateParams ? "," + stateParams : "" }`, }; diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 780b12d9992..66d81b5aa86 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from "react"; -// next themes +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; import { useTheme } from "next-themes"; +import { useRouter } from "next/router"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -import { useRouter } from "next/router"; +// helpers import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; -import { observer } from "mobx-react-lite"; const MobxStoreInit = observer(() => { // router @@ -13,16 +14,19 @@ const MobxStoreInit = observer(() => { const { workspaceSlug, projectId, cycleId, moduleId, globalViewId, viewId, inboxId } = router.query; // store const { - theme: themeStore, - user: userStore, - workspace: workspaceStore, - project: projectStore, - cycle: cycleStore, - module: moduleStore, - globalViews: globalViewsStore, - projectViews: projectViewsStore, - inbox: inboxStore, + theme: { sidebarCollapsed, toggleSidebar }, + user: { currentUser }, + workspace: { setWorkspaceSlug }, + project: { setProjectId }, + cycle: { setCycleId }, + module: { setModuleId }, + globalViews: { setGlobalViewId }, + projectViews: { setViewId }, + inbox: { setInboxId }, + appConfig: { fetchAppConfig }, } = useMobxStore(); + // fetching application Config + useSWR("APP_CONFIG", () => fetchAppConfig(), { revalidateIfStale: false, revalidateOnFocus: false }); // state const [dom, setDom] = useState(); // theme @@ -34,36 +38,36 @@ const MobxStoreInit = observer(() => { useEffect(() => { const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed"); const localBoolValue = localValue ? (localValue === "true" ? true : false) : false; - if (localValue && themeStore?.sidebarCollapsed === undefined) { - themeStore.toggleSidebar(localBoolValue); + if (localValue && sidebarCollapsed === undefined) { + toggleSidebar(localBoolValue); } - }, [themeStore, userStore, setTheme]); + }, [sidebarCollapsed, currentUser, setTheme, toggleSidebar]); /** * Setting up the theme of the user by fetching it from local storage */ useEffect(() => { - if (!userStore.currentUser) return; + if (!currentUser) return; if (window) { setDom(window.document?.querySelector("[data-theme='custom']")); } - setTheme(userStore.currentUser?.theme?.theme || "system"); - if (userStore.currentUser?.theme?.theme === "custom" && dom) { - applyTheme(userStore.currentUser?.theme?.palette, false); + setTheme(currentUser?.theme?.theme || "system"); + if (currentUser?.theme?.theme === "custom" && dom) { + applyTheme(currentUser?.theme?.palette, false); } else unsetCustomCssVariables(); - }, [userStore.currentUser, setTheme, dom]); + }, [currentUser, setTheme, dom]); /** * Setting router info to the respective stores. */ useEffect(() => { - if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString()); - if (projectId) projectStore.setProjectId(projectId.toString()); - if (cycleId) cycleStore.setCycleId(cycleId.toString()); - if (moduleId) moduleStore.setModuleId(moduleId.toString()); - if (globalViewId) globalViewsStore.setGlobalViewId(globalViewId.toString()); - if (viewId) projectViewsStore.setViewId(viewId.toString()); - if (inboxId) inboxStore.setInboxId(inboxId.toString()); + if (workspaceSlug) setWorkspaceSlug(workspaceSlug.toString()); + if (projectId) setProjectId(projectId.toString()); + if (cycleId) setCycleId(cycleId.toString()); + if (moduleId) setModuleId(moduleId.toString()); + if (globalViewId) setGlobalViewId(globalViewId.toString()); + if (viewId) setViewId(viewId.toString()); + if (inboxId) setInboxId(inboxId.toString()); }, [ workspaceSlug, projectId, @@ -72,13 +76,13 @@ const MobxStoreInit = observer(() => { globalViewId, viewId, inboxId, - workspaceStore, - projectStore, - cycleStore, - moduleStore, - globalViewsStore, - projectViewsStore, - inboxStore, + setWorkspaceSlug, + setProjectId, + setCycleId, + setModuleId, + setGlobalViewId, + setViewId, + setInboxId, ]); return <>; diff --git a/web/services/app_config.service.ts b/web/services/app_config.service.ts index 5843c01c96e..8f8bcd42381 100644 --- a/web/services/app_config.service.ts +++ b/web/services/app_config.service.ts @@ -2,27 +2,21 @@ import { APIService } from "services/api.service"; // helper import { API_BASE_URL } from "helpers/common.helper"; - -export interface IEnvConfig { - github: string; - google: string; - github_app_name: string | null; - email_password_login: boolean; - magic_login: boolean; -} +// types +import { IAppConfig } from "types/app"; export class AppConfigService extends APIService { constructor() { super(API_BASE_URL); } - async envConfig(): Promise { + async envConfig(): Promise { return this.get("/api/configs/", { headers: { "Content-Type": "application/json", }, }) - .then((response) => response?.data) + .then((response) => response.data) .catch((error) => { throw error?.response?.data; }); diff --git a/web/store/app-config.store.ts b/web/store/app-config.store.ts new file mode 100644 index 00000000000..3a4d9efc0a5 --- /dev/null +++ b/web/store/app-config.store.ts @@ -0,0 +1,47 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "./root"; +import { IAppConfig } from "types/app"; +// services +import { AppConfigService } from "services/app_config.service"; + +export interface IAppConfigStore { + envConfig: IAppConfig | null; + // action + fetchAppConfig: () => Promise; +} + +class AppConfigStore implements IAppConfigStore { + // observables + envConfig: IAppConfig | null = null; + + // root store + rootStore; + // service + appConfigService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + envConfig: observable.ref, + // actions + fetchAppConfig: action, + }); + this.appConfigService = new AppConfigService(); + + this.rootStore = _rootStore; + } + fetchAppConfig = async () => { + try { + const config = await this.appConfigService.envConfig(); + runInAction(() => { + this.envConfig = config; + }); + return config; + } catch (error) { + throw error; + } + }; +} + +export default AppConfigStore; diff --git a/web/store/root.ts b/web/store/root.ts index 9af4db4924b..c6d781b2805 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -1,5 +1,6 @@ import { enableStaticRendering } from "mobx-react-lite"; // store imports +import AppConfigStore, { IAppConfigStore } from "./app-config.store"; import CommandPaletteStore, { ICommandPaletteStore } from "./command-palette.store"; import UserStore, { IUserStore } from "store/user.store"; import ThemeStore, { IThemeStore } from "store/theme.store"; @@ -107,6 +108,7 @@ enableStaticRendering(typeof window === "undefined"); export class RootStore { user: IUserStore; theme: IThemeStore; + appConfig: IAppConfigStore; commandPalette: ICommandPaletteStore; workspace: IWorkspaceStore; @@ -167,6 +169,7 @@ export class RootStore { mentionsStore: IMentionsStore; constructor() { + this.appConfig = new AppConfigStore(this); this.commandPalette = new CommandPaletteStore(this); this.user = new UserStore(this); this.theme = new ThemeStore(this); diff --git a/web/types/app.d.ts b/web/types/app.d.ts index 2b03f6975b9..c762fb76fc5 100644 --- a/web/types/app.d.ts +++ b/web/types/app.d.ts @@ -1,3 +1,12 @@ export type NextPageWithLayout

= NextPage & { getLayout?: (page: ReactElement) => ReactNode; }; + +export interface IAppConfig { + email_password_login: boolean; + google_client_id: string | null; + github_app_name: string | null; + github_client_id: string | null; + magic_login: boolean; + slack_client_id: string | null; +} From 1412c1c94a21514519552a5b97955d01c0eef998 Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+manishg3@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:23:32 +0530 Subject: [PATCH 22/62] dev: modified the branch wise build (#2702) * cherrypicked branch build code * trigger on pull request * branch filter * checking branch filter * checking push * checking push again * code cleanup before PR --- .github/workflows/build-branch.yml | 45 +++++++++++++++++++----------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index 26b8addd23b..6159cc6953e 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -1,33 +1,46 @@ -name: Docker Branch Build +name: Branch Build on: - workflow_dispatch: - inputs: - logLevel: - description: 'Log level' - required: true - default: 'warning' - tags: - description: 'Dev/QA Builds' + push: + branches: + - master + - release + - qa + - develop + pull_request: + types: + - closed + branches: + - master + - release + - qa + - develop env: - gh_branch: ${{ github.ref_name }} - img_tag: latest + TARGET_BRANCH: '' jobs: branch_build_and_push: + if: ${{ (github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) }} name: Build-Push Web/Space/API/Proxy Docker Image runs-on: ubuntu-20.04 steps: - name: Check out the repo uses: actions/checkout@v3.3.0 + - name: Set Target Branch Name on PR close + if: ${{ github.event_name == 'pull_request' && github.event.action =='closed' }} + run: echo "TARGET_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV + - name: Set Target Branch Name on other than PR close + if: ${{ github.event_name == 'push' }} + run: echo "TARGET_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + - uses: ASzc/change-string-case-action@v2 id: gh_branch_upper_lower with: - string: ${{ env.gh_branch }} + string: ${{env.TARGET_BRANCH}} - uses: mad9000/actions-find-and-replace-string@2 id: gh_branch_replace_slash @@ -111,7 +124,7 @@ jobs: env: DOCKER_BUILDKIT: 1 DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_space: runs-on: ubuntu-20.04 @@ -141,7 +154,7 @@ jobs: env: DOCKER_BUILDKIT: 1 DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_backend: runs-on: ubuntu-20.04 @@ -171,7 +184,7 @@ jobs: env: DOCKER_BUILDKIT: 1 DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_proxy: runs-on: ubuntu-20.04 @@ -202,4 +215,4 @@ jobs: env: DOCKER_BUILDKIT: 1 DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} \ No newline at end of file + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} From 63d1ad286b9e6b9c4f325abbd7fccd5f09c0172f Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 7 Nov 2023 17:39:12 +0530 Subject: [PATCH 23/62] style: update font weight in project general setting section. (#2697) --- web/components/project/form.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index 1bc942fe142..297ab7110f9 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -200,7 +200,7 @@ export const ProjectDetailsForm: FC = (props) => { value={value} placeholder="Enter project description" onChange={onChange} - className="min-h-[102px] text-sm" + className="min-h-[102px] text-sm font-medium" hasError={Boolean(errors?.description)} disabled={!isAdmin} /> @@ -236,7 +236,7 @@ export const ProjectDetailsForm: FC = (props) => { ref={ref} hasError={Boolean(errors.identifier)} placeholder="Enter identifier" - className="w-full" + className="w-full font-medium" disabled={!isAdmin} /> )} @@ -253,7 +253,7 @@ export const ProjectDetailsForm: FC = (props) => { value={value} onChange={onChange} label={selectedNetwork?.label ?? "Select network"} - className="!border-custom-border-200 !shadow-none" + className="!border-custom-border-200 !shadow-none font-medium" input disabled={!isAdmin} optionsClassName="w-full" From f7cc2eca36761d9d2430c4d99428764c4a25bfde Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+manishg3@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:41:26 +0530 Subject: [PATCH 24/62] dev: Modified the branch-build action yaml (#2704) * cherrypicked code * removed PUSH event --- .github/workflows/build-branch.yml | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index 6159cc6953e..58c404e376e 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -2,12 +2,6 @@ name: Branch Build on: - push: - branches: - - master - - release - - qa - - develop pull_request: types: - closed @@ -18,24 +12,25 @@ on: - develop env: - TARGET_BRANCH: '' + TARGET_BRANCH: ${{ github.event.pull_request.base.ref }} jobs: branch_build_and_push: - if: ${{ (github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) }} + if: ${{ (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) }} name: Build-Push Web/Space/API/Proxy Docker Image runs-on: ubuntu-20.04 steps: - name: Check out the repo uses: actions/checkout@v3.3.0 - - name: Set Target Branch Name on PR close - if: ${{ github.event_name == 'pull_request' && github.event.action =='closed' }} - run: echo "TARGET_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV - - - name: Set Target Branch Name on other than PR close - if: ${{ github.event_name == 'push' }} - run: echo "TARGET_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + + # - name: Set Target Branch Name on PR close + # if: ${{ github.event_name == 'pull_request' && github.event.action =='closed' }} + # run: echo "TARGET_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV + + # - name: Set Target Branch Name on other than PR close + # if: ${{ github.event_name == 'push' }} + # run: echo "TARGET_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV - uses: ASzc/change-string-case-action@v2 id: gh_branch_upper_lower From 86379c51b77cede75abf1e093489f7bb7e64144b Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:05:38 +0530 Subject: [PATCH 25/62] dev: worker count (#2573) * dev: workers count * dev: update the worker count variable to GUNICORN_WORKERS --- apiserver/.env.example | 3 +++ apiserver/bin/takeoff | 2 +- deploy/selfhost/docker-compose.yml | 2 ++ deploy/selfhost/variables.env | 2 ++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apiserver/.env.example b/apiserver/.env.example index 8193b5e7716..d3ad596e559 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -70,3 +70,6 @@ ENABLE_MAGIC_LINK_LOGIN="0" # Email redirections and minio domain settings WEB_URL="http://localhost" + +# Gunicorn Workers +GUNICORN_WORKERS=2 diff --git a/apiserver/bin/takeoff b/apiserver/bin/takeoff index dc25a14e2d1..9b09f244ea4 100755 --- a/apiserver/bin/takeoff +++ b/apiserver/bin/takeoff @@ -6,4 +6,4 @@ python manage.py migrate # Create a Default User python bin/user_script.py -exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - +exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index 33a0f6673ce..c571291cfac 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -10,6 +10,8 @@ x-app-env : &app-env - SENTRY_DSN=${SENTRY_DSN:-""} - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""} - DOCKERIZED=${DOCKERIZED:-1} + # Gunicorn Workers + - GUNICORN_WORKERS=${GUNICORN_WORKERS:-2} #DB SETTINGS - PGHOST=${PGHOST:-plane-db} - PGDATABASE=${PGDATABASE:-plane} diff --git a/deploy/selfhost/variables.env b/deploy/selfhost/variables.env index 1e507a54b6b..b12031126ce 100644 --- a/deploy/selfhost/variables.env +++ b/deploy/selfhost/variables.env @@ -61,3 +61,5 @@ MINIO_ROOT_PASSWORD="secret-key" BUCKET_NAME=uploads FILE_SIZE_LIMIT=5242880 +# Gunicorn Workers +GUNICORN_WORKERS=2 From 6e461dd8c3a77934680b0e7546649f30b42cb8d3 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 7 Nov 2023 18:17:44 +0530 Subject: [PATCH 26/62] style: update user profile button alignment. (#2695) --- packages/ui/src/avatar/avatar.tsx | 11 ++++++++--- web/components/workspace/sidebar-dropdown.tsx | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/avatar/avatar.tsx b/packages/ui/src/avatar/avatar.tsx index 2c6b5b9a3d7..674d82a264b 100644 --- a/packages/ui/src/avatar/avatar.tsx +++ b/packages/ui/src/avatar/avatar.tsx @@ -42,6 +42,10 @@ type Props = { * The source of the avatar image */ src?: string; + /** + * The custom CSS class name to apply to the component + */ + className?: string; }; /** @@ -94,7 +98,7 @@ export const getBorderRadius = (shape: "circle" | "square") => { case "circle": return "rounded-full"; case "square": - return "rounded-md"; + return "rounded"; default: return "rounded-full"; } @@ -119,6 +123,7 @@ export const Avatar: React.FC = (props) => { size = "md", shape = "circle", src, + className = "" } = props; // get size details based on the size prop @@ -145,14 +150,14 @@ export const Avatar: React.FC = (props) => { {src ? ( {name} ) : (

{ return (
- - + +
{ {!themeStore.sidebarCollapsed && ( - + Date: Tue, 7 Nov 2023 18:18:51 +0530 Subject: [PATCH 27/62] fix: add issues to cycles and modules (#2659) * fix: able to add issue in cycle and module * fix: issue activity message --- apiserver/plane/api/views/cycle.py | 14 ++--- apiserver/plane/api/views/module.py | 11 ++-- .../plane/bgtasks/issue_activites_task.py | 53 ++++++++++++++----- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index e7d247872ed..02a58319643 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -479,13 +479,13 @@ def destroy(self, request, slug, project_id, pk): ) ) cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) - # Delete the cycle - cycle.delete() + issue_activity.delay( type="cycle.activity.deleted", requested_data=json.dumps( { "cycle_id": str(pk), + "cycle_name": str(cycle.name), "issues": [str(issue_id) for issue_id in cycle_issues], } ), @@ -495,6 +495,8 @@ def destroy(self, request, slug, project_id, pk): current_instance=None, epoch=int(timezone.now().timestamp()), ) + # Delete the cycle + cycle.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -511,12 +513,6 @@ class CycleIssueViewSet(BaseViewSet): "issue__assignees__id", ] - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - cycle_id=self.kwargs.get("cycle_id"), - ) - def get_queryset(self): return self.filter_queryset( super() @@ -669,7 +665,7 @@ def create(self, request, slug, project_id, cycle_id): type="cycle.activity.created", requested_data=json.dumps({"cycles_list": issues}), actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), + issue_id=None, project_id=str(self.kwargs.get("project_id", None)), current_instance=json.dumps( { diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 48f892764c5..0e345493220 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -266,12 +266,12 @@ def destroy(self, request, slug, project_id, pk): module_issues = list( ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True) ) - module.delete() issue_activity.delay( type="module.activity.deleted", requested_data=json.dumps( { "module_id": str(pk), + "module_name": str(module.name), "issues": [str(issue_id) for issue_id in module_issues], } ), @@ -281,6 +281,7 @@ def destroy(self, request, slug, project_id, pk): current_instance=None, epoch=int(timezone.now().timestamp()), ) + module.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -297,12 +298,6 @@ class ModuleIssueViewSet(BaseViewSet): ProjectEntityPermission, ] - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - module_id=self.kwargs.get("module_id"), - ) - def get_queryset(self): return self.filter_queryset( super() @@ -446,7 +441,7 @@ def create(self, request, slug, project_id, module_id): type="module.activity.created", requested_data=json.dumps({"modules_list": issues}), actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), + issue_id=None, project_id=str(self.kwargs.get("project_id", None)), current_instance=json.dumps( { diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index f0a20eeec39..4776bceab9d 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -691,6 +691,10 @@ def create_cycle_issue_activity( new_cycle = Cycle.objects.filter( pk=updated_record.get("new_cycle_id", None) ).first() + issue = Issue.objects.filter(pk=updated_record.get("issue_id")).first() + if issue: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( @@ -713,6 +717,10 @@ def create_cycle_issue_activity( cycle = Cycle.objects.filter( pk=created_record.get("fields").get("cycle") ).first() + issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() + if issue: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( @@ -747,22 +755,27 @@ def delete_cycle_issue_activity( ) cycle_id = requested_data.get("cycle_id", "") + cycle_name = requested_data.get("cycle_name", "") cycle = Cycle.objects.filter(pk=cycle_id).first() issues = requested_data.get("issues") for issue in issues: + current_issue = Issue.objects.filter(pk=issue).first() + if issue: + current_issue.updated_at = timezone.now() + current_issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( issue_id=issue, actor_id=actor_id, verb="deleted", - old_value=cycle.name if cycle is not None else "", + old_value=cycle.name if cycle is not None else cycle_name, new_value="", field="cycles", project_id=project_id, workspace_id=workspace_id, - comment=f"removed this issue from {cycle.name if cycle is not None else None}", - old_identifier=cycle.id if cycle is not None else None, + comment=f"removed this issue from {cycle.name if cycle is not None else cycle_name}", + old_identifier=cycle_id if cycle_id is not None else None, epoch=epoch, ) ) @@ -794,6 +807,10 @@ def create_module_issue_activity( new_module = Module.objects.filter( pk=updated_record.get("new_module_id", None) ).first() + issue = Issue.objects.filter(pk=updated_record.get("issue_id")).first() + if issue: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( @@ -816,6 +833,10 @@ def create_module_issue_activity( module = Module.objects.filter( pk=created_record.get("fields").get("module") ).first() + issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() + if issue: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( issue_id=created_record.get("fields").get("issue"), @@ -849,22 +870,27 @@ def delete_module_issue_activity( ) module_id = requested_data.get("module_id", "") + module_name = requested_data.get("module_name", "") module = Module.objects.filter(pk=module_id).first() issues = requested_data.get("issues") for issue in issues: + current_issue = Issue.objects.filter(pk=issue).first() + if issue: + current_issue.updated_at = timezone.now() + current_issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( issue_id=issue, actor_id=actor_id, verb="deleted", - old_value=module.name if module is not None else "", + old_value=module.name if module is not None else module_name, new_value="", field="modules", project_id=project_id, workspace_id=workspace_id, - comment=f"removed this issue from ", - old_identifier=module.id if module is not None else None, + comment=f"removed this issue from {module.name if module is not None else module_name}", + old_identifier=module_id if module_id is not None else None, epoch=epoch, ) ) @@ -1452,15 +1478,16 @@ def issue_activity( issue_activities = [] project = Project.objects.get(pk=project_id) - issue = Issue.objects.filter(pk=issue_id).first() workspace_id = project.workspace_id - if issue is not None: - try: - issue.updated_at = timezone.now() - issue.save(update_fields=["updated_at"]) - except Exception as e: - pass + if issue_id is not None: + issue = Issue.objects.filter(pk=issue_id).first() + if issue: + try: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) + except Exception as e: + pass ACTIVITY_MAPPER = { "issue.activity.created": create_issue_activity, From 8d3853b129e0f66619d376a428d147204f680018 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:19:32 +0530 Subject: [PATCH 28/62] fix: issue draft delete functionality (#2696) --- apiserver/plane/api/views/issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index ff7f526915c..104bdafe291 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -2230,7 +2230,7 @@ def retrieve(self, request, slug, project_id, pk=None): def destroy(self, request, slug, project_id, pk=None): issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) current_instance = json.dumps( - IssueSerializer(current_instance).data, cls=DjangoJSONEncoder + IssueSerializer(issue).data, cls=DjangoJSONEncoder ) issue.delete() issue_activity.delay( From b56d188a83777448f2f09f0e822b6d96136e97f9 Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:22:52 +0530 Subject: [PATCH 29/62] add errors for duplicate labels (#2706) Co-authored-by: rahulramesha --- .../issues/sidebar-select/label.tsx | 12 ++++++++ web/components/labels/create-label-modal.tsx | 10 ++++++- .../labels/create-update-label-inline.tsx | 29 ++++++++++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/web/components/issues/sidebar-select/label.tsx b/web/components/issues/sidebar-select/label.tsx index 464554aec24..e997b6f4971 100644 --- a/web/components/issues/sidebar-select/label.tsx +++ b/web/components/issues/sidebar-select/label.tsx @@ -18,6 +18,7 @@ import { Plus, X } from "lucide-react"; import { IIssue, IIssueLabels } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import useToast from "hooks/use-toast"; type Props = { issueDetails: IIssue | undefined; @@ -44,6 +45,9 @@ export const SidebarLabelSelect: React.FC = ({ const [createLabelForm, setCreateLabelForm] = useState(false); const router = useRouter(); + + const { setToastAlert } = useToast(); + const { workspaceSlug, projectId } = router.query; const { @@ -79,6 +83,14 @@ export const SidebarLabelSelect: React.FC = ({ submitChanges({ labels: [...(issueDetails?.labels ?? []), res.id] }); setCreateLabelForm(false); + }) + .catch((error) => { + setToastAlert({ + title: "Oops!", + type: "error", + message: error?.error ?? "Error while adding the label", + }); + reset(formData); }); }; diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx index 7f42f009538..6fe57845924 100644 --- a/web/components/labels/create-label-modal.tsx +++ b/web/components/labels/create-label-modal.tsx @@ -15,6 +15,7 @@ import { ChevronDown } from "lucide-react"; import type { IIssueLabels, IState } from "types"; // constants import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; +import useToast from "hooks/use-toast"; // types type Props = { @@ -58,6 +59,8 @@ export const CreateLabelModal: React.FC = observer((props) => { reset(defaultValues); }; + const { setToastAlert } = useToast(); + const onSubmit = async (formData: IIssueLabels) => { if (!workspaceSlug) return; @@ -68,7 +71,12 @@ export const CreateLabelModal: React.FC = observer((props) => { if (onSuccess) onSuccess(res); }) .catch((error) => { - console.log(error); + setToastAlert({ + title: "Oops!", + type: "error", + message: error?.error ?? "Error while adding the label", + }); + reset(formData); }); }; diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index 16cab89ed31..f27bd517e6b 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -14,6 +14,7 @@ import { Button, Input } from "@plane/ui"; import { IIssueLabels } from "types"; // fetch-keys import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; +import useToast from "hooks/use-toast"; type Props = { labelForm: boolean; @@ -39,6 +40,8 @@ export const CreateUpdateLabelInline = observer( // store const { projectLabel: projectLabelStore } = useMobxStore(); + const { setToastAlert } = useToast(); + const { handleSubmit, control, @@ -60,10 +63,20 @@ export const CreateUpdateLabelInline = observer( const handleLabelCreate: SubmitHandler = async (formData) => { if (!workspaceSlug || !projectId || isSubmitting) return; - await projectLabelStore.createLabel(workspaceSlug.toString(), projectId.toString(), formData).then(() => { - handleClose(); - reset(defaultValues); - }); + await projectLabelStore + .createLabel(workspaceSlug.toString(), projectId.toString(), formData) + .then(() => { + handleClose(); + reset(defaultValues); + }) + .catch((error) => { + setToastAlert({ + title: "Oops!", + type: "error", + message: error?.error ?? "Error while adding the label", + }); + reset(formData); + }); }; const handleLabelUpdate: SubmitHandler = async (formData) => { @@ -74,6 +87,14 @@ export const CreateUpdateLabelInline = observer( .then(() => { reset(defaultValues); handleClose(); + }) + .catch((error) => { + setToastAlert({ + title: "Oops!", + type: "error", + message: error?.error ?? "Error while updating the label", + }); + reset(formData); }); }; From 9f206331bcb0c3b8a8568ccec4c10f9c6a1ec013 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:35:05 +0530 Subject: [PATCH 30/62] style: spinner component improvement (#2708) --- packages/ui/src/spinners/circular-spinner.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/spinners/circular-spinner.tsx b/packages/ui/src/spinners/circular-spinner.tsx index b961880305b..e7e952295a3 100644 --- a/packages/ui/src/spinners/circular-spinner.tsx +++ b/packages/ui/src/spinners/circular-spinner.tsx @@ -1,10 +1,23 @@ import * as React from "react"; -export const Spinner: React.FC = () => ( +export interface ISpinner extends React.SVGAttributes { + height?: string; + width?: string; + className?: string | undefined; +} + +export const Spinner: React.FC = ({ + height = "32px", + width = "32px", + className = "", + ...rest +}) => (
); -}; +}); diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 105d161289d..61d21c2e03d 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -1,8 +1,11 @@ import { FC } from "react"; -// types -import { ICycle } from "types"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; +// types +import { ICycle } from "types"; export interface ICyclesBoard { cycles: ICycle[]; @@ -12,9 +15,11 @@ export interface ICyclesBoard { peekCycle: string; } -export const CyclesBoard: FC = (props) => { +export const CyclesBoard: FC = observer((props) => { const { cycles, filter, workspaceSlug, projectId, peekCycle } = props; + const { commandPalette: commandPaletteStore } = useMobxStore(); + return ( <> {cycles.length > 0 ? ( @@ -53,12 +58,7 @@ export const CyclesBoard: FC = (props) => { @@ -67,4 +67,4 @@ export const CyclesBoard: FC = (props) => { )} ); -}; +}); diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 03698f1d81b..0cff682afd5 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,7 +1,9 @@ import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { CyclePeekOverview, CyclesListItem } from "components/cycles"; - // ui import { Loader } from "@plane/ui"; // types @@ -14,9 +16,11 @@ export interface ICyclesList { projectId: string; } -export const CyclesList: FC = (props) => { +export const CyclesList: FC = observer((props) => { const { cycles, filter, workspaceSlug, projectId } = props; + const { commandPalette: commandPaletteStore } = useMobxStore(); + return ( <> {cycles ? ( @@ -53,12 +57,7 @@ export const CyclesList: FC = (props) => { @@ -75,4 +74,4 @@ export const CyclesList: FC = (props) => { )} ); -}; +}); diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index ea154d48b09..1c7d0c58bca 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -317,11 +317,11 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { {!isCompleted && ( - + setCycleDeleteModal(true)}> - - Delete + + Delete cycle diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 4106f443b2e..42836b0eaa5 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -31,6 +31,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { cycle: cycleStore, cycleIssueFilter: cycleIssueFilterStore, project: projectStore, + commandPalette: commandPaletteStore, } = useMobxStore(); const { currentProjectDetails } = projectStore; @@ -139,7 +140,6 @@ export const CycleIssuesHeader: React.FC = observer(() => { type="component" component={ @@ -148,6 +148,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { } className="ml-1.5 flex-shrink-0" width="auto" + placement="bottom-start" > {cyclesList?.map((cycle) => ( { -
); -}; +}); diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 42c01d5319b..1eec1cff4d0 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -31,6 +31,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { module: moduleStore, moduleFilter: moduleFilterStore, project: projectStore, + commandPalette: commandPaletteStore, } = useMobxStore(); const activeLayout = issueFilterStore.userDisplayFilters.layout; const { currentProjectDetails } = projectStore; @@ -146,6 +147,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { } className="ml-1.5 flex-shrink-0" width="auto" + placement="bottom-start" > {modulesList?.map((module) => ( { - diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index 8b9fa433d80..a0bf29d0526 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -24,10 +24,11 @@ const pageService = new PageService(); export const PageDetailsHeader: FC = observer((props) => { const { showButton = false } = props; + const router = useRouter(); const { workspaceSlug, pageId } = router.query; - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { currentProjectDetails } = projectStore; const { data: pageDetails } = useSWR( @@ -78,10 +79,7 @@ export const PageDetailsHeader: FC = observer((props) => { variant="primary" prependIcon={} size="sm" - onClick={() => { - const e = new KeyboardEvent("keydown", { key: "d" }); - document.dispatchEvent(e); - }} + onClick={() => commandPaletteStore.toggleCreatePageModal(true)} > Create Page diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index 634dd0c385f..0a3fd53f61e 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -18,7 +18,7 @@ export const PagesHeader: FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { currentProjectDetails } = projectStore; return ( @@ -56,10 +56,7 @@ export const PagesHeader: FC = observer((props) => { variant="primary" prependIcon={} size="sm" - onClick={() => { - const e = new KeyboardEvent("keydown", { key: "d" }); - document.dispatchEvent(e); - }} + onClick={() => commandPaletteStore.toggleCreatePageModal(true)} > Create Page diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index f9bf6ec5897..efe4dcf51ca 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -23,7 +23,12 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueFilter: issueFilterStore, project: projectStore, inbox: inboxStore } = useMobxStore(); + const { + issueFilter: issueFilterStore, + project: projectStore, + inbox: inboxStore, + commandPalette: commandPaletteStore, + } = useMobxStore(); const activeLayout = issueFilterStore.userDisplayFilters.layout; @@ -198,16 +203,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { -
diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index 3c56f239ea3..fa51493ed61 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -11,7 +11,7 @@ export const ProjectsHeader = observer(() => { const { workspaceSlug } = router.query; // store - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : []; @@ -43,14 +43,7 @@ export const ProjectsHeader = observer(() => {
)} -
diff --git a/web/components/integration/jira/give-details.tsx b/web/components/integration/jira/give-details.tsx index 622517439aa..8a7c841deb4 100644 --- a/web/components/integration/jira/give-details.tsx +++ b/web/components/integration/jira/give-details.tsx @@ -16,15 +16,14 @@ export const JiraGetImportDetail: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); + const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; + const { control, formState: { errors }, } = useFormContext(); - const { project: projectStore } = useMobxStore(); - - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; - return (
@@ -190,10 +189,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
- + setModuleDeleteModal(true)}> - - Delete + + Delete module diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index 5ed28b00b7e..b7fcb7cb6bc 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -18,7 +18,8 @@ export const WorkspaceDashboardView = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store - const { user: userStore, project: projectStore } = useMobxStore(); + const { user: userStore, project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); + const user = userStore.currentUser; const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; const workspaceDashboardInfo = userStore.dashboardInfo; @@ -67,16 +68,7 @@ export const WorkspaceDashboardView = observer(() => {
Create a project

Manage your projects by creating issues, cycles, modules, views and pages.

-
diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index e4bc46c66cc..59bc8c84225 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -1,19 +1,18 @@ import React from "react"; - import { useRouter } from "next/router"; - +import { observer } from "mobx-react-lite"; import useSWR from "swr"; - +import { Plus } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // services import { PageService } from "services/page.service"; // components import { PagesView } from "components/pages"; -// ui import { EmptyState } from "components/common"; +// ui import { Loader } from "@plane/ui"; -// icons -import { Plus } from "lucide-react"; -// images +// assets import emptyPage from "public/empty-state/page.svg"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; @@ -26,7 +25,11 @@ import { RECENT_PAGES_LIST } from "constants/fetch-keys"; // services const pageService = new PageService(); -export const RecentPagesList: React.FC = ({ viewType }) => { +export const RecentPagesList: React.FC = observer((props) => { + const { viewType } = props; + + const { commandPalette: commandPaletteStore } = useMobxStore(); + const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -46,9 +49,7 @@ export const RecentPagesList: React.FC = ({ viewType }) => { return (
-

- {replaceUnderscoreIfSnakeCase(key)} -

+

{replaceUnderscoreIfSnakeCase(key)}

); @@ -61,12 +62,7 @@ export const RecentPagesList: React.FC = ({ viewType }) => { primaryButton={{ icon: , text: "New Page", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "d", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreatePageModal(true), }} /> ) @@ -79,4 +75,4 @@ export const RecentPagesList: React.FC = ({ viewType }) => { )} ); -}; +}); diff --git a/web/components/pages/pages-view.tsx b/web/components/pages/pages-view.tsx index 699cbb58276..5aca1de1bc9 100644 --- a/web/components/pages/pages-view.tsx +++ b/web/components/pages/pages-view.tsx @@ -1,21 +1,20 @@ import { useState } from "react"; - -import useSWR, { mutate } from "swr"; import { useRouter } from "next/router"; - +import { observer } from "mobx-react-lite"; +import useSWR, { mutate } from "swr"; +import { Plus } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // services import { PageService } from "services/page.service"; import { ProjectService } from "services/project"; // hooks import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; // components import { CreateUpdatePageModal, DeletePageModal, SinglePageDetailedItem, SinglePageListItem } from "components/pages"; -// ui import { EmptyState } from "components/common"; +// ui import { Loader } from "@plane/ui"; -// icons -import { Plus } from "lucide-react"; // images import emptyPage from "public/empty-state/page.svg"; // types @@ -37,17 +36,19 @@ type Props = { const pageService = new PageService(); const projectService = new ProjectService(); -export const PagesView: React.FC = ({ pages, viewType }) => { - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; +export const PagesView: React.FC = observer(({ pages, viewType }) => { // states const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); const [selectedPageToUpdate, setSelectedPageToUpdate] = useState(null); const [deletePageModal, setDeletePageModal] = useState(false); const [selectedPageToDelete, setSelectedPageToDelete] = useState(null); - const { user } = useUserAuth(); + const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore(); + const user = userStore.currentUser ?? undefined; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; const { setToastAlert } = useToast(); @@ -163,7 +164,7 @@ export const PagesView: React.FC = ({ pages, viewType }) => { }; const partialUpdatePage = (page: IPage, formData: Partial) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !user) return; mutate( ALL_PAGES_LIST(projectId.toString()), @@ -264,12 +265,7 @@ export const PagesView: React.FC = ({ pages, viewType }) => { primaryButton={{ icon: , text: "New Page", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "d", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreatePageModal(true), }} /> )} @@ -294,4 +290,4 @@ export const PagesView: React.FC = ({ pages, viewType }) => { )} ); -}; +}); diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index 37432e367d5..0a090f63649 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -18,7 +18,7 @@ export interface IProjectCardList { export const ProjectCardList: FC = observer((props) => { const { workspaceSlug } = props; // store - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; @@ -53,12 +53,7 @@ export const ProjectCardList: FC = observer((props) => { primaryButton={{ icon: , text: "New Project", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreateProjectModal(true), }} /> )} diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 98d3cdb02f9..cb501a697ea 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -19,11 +19,6 @@ import { IProject } from "types"; import { useMobxStore } from "lib/mobx/store-provider"; export const ProjectSidebarList: FC = observer(() => { - const { theme: themeStore, project: projectStore } = useMobxStore(); - // router - const router = useRouter(); - const { workspaceSlug } = router.query; - // states const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); @@ -31,6 +26,11 @@ export const ProjectSidebarList: FC = observer(() => { // refs const containerRef = useRef(null); + const { theme: themeStore, project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + // toast const { setToastAlert } = useToast(); @@ -254,12 +254,7 @@ export const ProjectSidebarList: FC = observer(() => { diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index b09496cc468..d801b277203 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -8,14 +8,18 @@ import useLocalStorage from "hooks/use-local-storage"; import { CreateUpdateDraftIssueModal } from "components/issues"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; +import { observer } from "mobx-react-lite"; -export const WorkspaceSidebarQuickAction = () => { - const store: any = useMobxStore(); - +export const WorkspaceSidebarQuickAction = observer(() => { + // states const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); + const { theme: themeStore, commandPalette: commandPaletteStore } = useMobxStore(); + const { storedValue, clearValue } = useLocalStorage("draftedIssue", JSON.stringify({})); + const isSidebarCollapsed = themeStore.sidebarCollapsed; + return ( <> {
{ {storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( <> -
+
); -}; +}); diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index e3eca37ff12..bc401a3d3ac 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -25,6 +25,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { module: moduleStore, projectViews: projectViewsStore, inbox: inboxStore, + commandPalette: commandPaletteStore, } = useMobxStore(); // router const router = useRouter(); @@ -131,12 +132,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { image={emptyProject} primaryButton={{ text: "Create Project", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreateProjectModal(true), }} />
diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 0faa835ada7..1d76f7754a2 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -28,7 +28,7 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store - const { project: projectStore, user: userStore } = useMobxStore(); + const { project: projectStore, user: userStore, commandPalette: commandPaletteStore } = useMobxStore(); const user = userStore.currentUser; const projects = workspaceSlug ? projectStore.projects[workspaceSlug?.toString()] : null; @@ -96,12 +96,7 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { primaryButton={{ icon: , text: "New Project", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); - }, + onClick: () => commandPaletteStore.toggleCreateProjectModal(true), }} /> From 621d551c4a0b7960586c7bdae0ac28aaa980c7e4 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:33:26 +0530 Subject: [PATCH 33/62] fix: project select validation (#2723) --- web/components/issues/form.tsx | 6 +++++- web/components/issues/select/project.tsx | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index b54ce112183..2c9fb4eabb1 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -272,9 +272,13 @@ export const IssueForm: FC = observer((props) => { ( + rules={{ + required: true, + }} + render={({ field: { value, onChange }, fieldState: { error } }) => ( { onChange(val); setActiveProject(val); diff --git a/web/components/issues/select/project.tsx b/web/components/issues/select/project.tsx index 08f245b1480..ad55471ce7e 100644 --- a/web/components/issues/select/project.tsx +++ b/web/components/issues/select/project.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import type { FieldError } from "react-hook-form"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // popper js @@ -15,6 +16,7 @@ import { Check, Clipboard, Search } from "lucide-react"; export interface IssueProjectSelectProps { value: string; onChange: (value: string) => void; + error?: FieldError; } export const IssueProjectSelect: React.FC = observer((props) => { From da799b5a6309c1927f17bb3bbb223afe9ec34ece Mon Sep 17 00:00:00 2001 From: Ankush Deshmukh Date: Wed, 8 Nov 2023 17:34:09 +0530 Subject: [PATCH 34/62] Fix: Render bar chart axis labels in lighter color when dark theme applied (#2721) --- web/components/analytics/custom-analytics/graph/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx index 181eec8bd9d..5fc21a3ecdc 100644 --- a/web/components/analytics/custom-analytics/graph/index.tsx +++ b/web/components/analytics/custom-analytics/graph/index.tsx @@ -114,6 +114,7 @@ export const AnalyticsGraph: React.FC = ({ analytics, barGraphData, param y={datum.y} textAnchor="end" fontSize={10} + fill="rgb(var(--color-text-200))" className={`${barGraphData.data.length > 7 ? "-rotate-45" : ""}`} > {generateDisplayName(datum.value, analytics, params, "x_axis")} From df8bdfd5b9150c426a43adca7d96d1ae36a4aebb Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:34:42 +0530 Subject: [PATCH 35/62] fix: project automation settings flickering (#2680) * fix: cycle and module sidebar z-index * fix: project automation settings flickering --- .../automation/auto-archive-automation.tsx | 92 +++++---- .../automation/auto-close-automation.tsx | 193 +++++++++--------- web/components/cycles/cycle-peek-overview.tsx | 2 +- .../modules/module-peek-overview.tsx | 2 +- .../projects/[projectId]/cycles/[cycleId].tsx | 2 +- .../[projectId]/modules/[moduleId].tsx | 2 +- .../[projectId]/settings/automations.tsx | 20 +- 7 files changed, 156 insertions(+), 157 deletions(-) diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index 96689259574..1083073daeb 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -1,7 +1,9 @@ import React, { useState } from "react"; - +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // component -import { CustomSelect, ToggleSwitch } from "@plane/ui"; +import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui"; import { SelectMonthModal } from "components/automation"; // icon import { ArchiveRestore } from "lucide-react"; @@ -11,15 +13,21 @@ import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; import { IProject } from "types"; type Props = { - projectDetails: IProject | undefined; handleChange: (formData: Partial) => Promise; - disabled?: boolean; }; -export const AutoArchiveAutomation: React.FC = ({ projectDetails, handleChange, disabled = false }) => { +const initialValues: Partial = { archive_in: 1 }; + +export const AutoArchiveAutomation: React.FC = observer((props) => { + const { handleChange } = props; + // states const [monthModal, setmonthModal] = useState(false); - const initialValues: Partial = { archive_in: 1 }; + const { user: userStore, project: projectStore } = useMobxStore(); + + const projectDetails = projectStore.currentProjectDetails; + const userRole = userStore.currentProjectRole; + return ( <> = ({ projectDetails, handleC projectDetails?.archive_in === 0 ? handleChange({ archive_in: 1 }) : handleChange({ archive_in: 0 }) } size="sm" - disabled={disabled} + disabled={userRole !== 20} />
- {projectDetails?.archive_in !== 0 && ( -
-
-
Auto-archive issues that are closed for
-
- { - handleChange({ archive_in: val }); - }} - input - width="w-full" - disabled={disabled} - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} + {projectDetails ? ( + projectDetails.archive_in !== 0 && ( +
+
+
Auto-archive issues that are closed for
+
+ { + handleChange({ archive_in: val }); + }} + input + width="w-full" + disabled={userRole !== 20} + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} - - - + + + +
-
+ ) + ) : ( + + + )}
); -}; +}); diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index b0aad20cd4e..2ccebe8b2ea 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -1,41 +1,33 @@ import React, { useState } from "react"; -import useSWR from "swr"; -import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // component import { SelectMonthModal } from "components/automation"; -import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon } from "@plane/ui"; +import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui"; // icons import { ArchiveX } from "lucide-react"; -// services -import { ProjectStateService } from "services/project"; -// constants -import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; -import { STATES_LIST } from "constants/fetch-keys"; +// helpers +import { getStatesList } from "helpers/state.helper"; // types import { IProject } from "types"; -// helper -import { getStatesList } from "helpers/state.helper"; +// fetch keys +import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; type Props = { - projectDetails: IProject | undefined; handleChange: (formData: Partial) => Promise; - disabled?: boolean; }; -const projectStateService = new ProjectStateService(); - -export const AutoCloseAutomation: React.FC = ({ projectDetails, handleChange, disabled = false }) => { +export const AutoCloseAutomation: React.FC = observer((props) => { + const { handleChange } = props; + // states const [monthModal, setmonthModal] = useState(false); - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { user: userStore, project: projectStore } = useMobxStore(); - const { data: stateGroups } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => projectStateService.getStates(workspaceSlug as string, projectId as string) - : null - ); + const userRole = userStore.currentProjectRole; + const projectDetails = projectStore.currentProjectDetails; + const stateGroups = projectStore.projectStatesByGroups ?? undefined; const states = getStatesList(stateGroups); const options = states @@ -72,8 +64,7 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha handleClose={() => setmonthModal(false)} handleChange={handleChange} /> - -
+
@@ -82,7 +73,7 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha

Auto-close issues

- Plane will automatically close issue that haven’t been completed or cancelled. + Plane will automatically close issue that haven{"'"}t been completed or cancelled.

@@ -94,87 +85,93 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha : handleChange({ close_in: 0, default_state: null }) } size="sm" - disabled={disabled} + disabled={userRole !== 20} />
- {projectDetails?.close_in !== 0 && ( -
-
-
-
Auto-close issues that are inactive for
-
- { - handleChange({ close_in: val }); - }} - input - width="w-full" - disabled={disabled} - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} - - - + {projectDetails ? ( + projectDetails.close_in !== 0 && ( +
+
+
+
Auto-close issues that are inactive for
+
+ { + handleChange({ close_in: val }); + }} + input + width="w-full" + disabled={userRole !== 20} + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} + + + +
-
-
-
Auto-close Status
-
- - {selectedOption ? ( - - ) : currentDefaultState ? ( - - ) : ( - - )} - {selectedOption?.name - ? selectedOption.name - : currentDefaultState?.name ?? State} -
- } - onChange={(val: string) => { - handleChange({ default_state: val }); - }} - options={options} - disabled={!multipleOptions} - width="w-full" - input - /> +
+
Auto-close Status
+
+ + {selectedOption ? ( + + ) : currentDefaultState ? ( + + ) : ( + + )} + {selectedOption?.name + ? selectedOption.name + : currentDefaultState?.name ?? State} +
+ } + onChange={(val: string) => { + handleChange({ default_state: val }); + }} + options={options} + disabled={!multipleOptions} + width="w-full" + input + /> +
-
+ ) + ) : ( + + + )}
); -}; +}); diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index fb30150ca25..f1e0cf08412 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -41,7 +41,7 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa {peekCycle && (
= observer(({ projectId, worksp {peekModule && (
{
{cycleId && !isSidebarCollapsed && (
{
{moduleId && !isSidebarCollapsed && (
{ const handleChange = async (formData: Partial) => { if (!workspaceSlug || !projectId || !projectDetails) return; - mutate( - PROJECT_DETAILS(projectId as string), - (prevData) => ({ ...(prevData as IProject), ...formData }), - false - ); - - mutate( - PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), - (prevData) => (prevData ?? []).map((p) => (p.id === projectDetails.id ? { ...p, ...formData } : p)), - false - ); - await projectService .updateProject(workspaceSlug as string, projectId as string, formData, user) .then(() => {}) @@ -72,8 +60,8 @@ const AutomationSettingsPage: NextPageWithLayout = () => {

Automations

- - + +
); }; From 83e0c4ebbd4ea58199d770f964e3bfee8d5e4bc0 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:35:45 +0530 Subject: [PATCH 36/62] chore: remove active ids from the MobX stores if not present in the route (#2681) * chore: remove active ids if not present in the route * refactor: set active id logic --- web/lib/mobx/store-init.tsx | 13 +++++++------ web/store/cycle/cycles.store.ts | 4 ++-- web/store/global-view/global_views.store.ts | 4 ++-- web/store/inbox/inbox.store.ts | 4 ++-- web/store/module/modules.store.ts | 6 +++--- web/store/project-view/project_views.store.ts | 4 ++-- web/store/project/project.store.ts | 6 +++--- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 66d81b5aa86..f89aa72c79c 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -62,12 +62,13 @@ const MobxStoreInit = observer(() => { */ useEffect(() => { if (workspaceSlug) setWorkspaceSlug(workspaceSlug.toString()); - if (projectId) setProjectId(projectId.toString()); - if (cycleId) setCycleId(cycleId.toString()); - if (moduleId) setModuleId(moduleId.toString()); - if (globalViewId) setGlobalViewId(globalViewId.toString()); - if (viewId) setViewId(viewId.toString()); - if (inboxId) setInboxId(inboxId.toString()); + + setProjectId(projectId?.toString() ?? null); + setCycleId(cycleId?.toString() ?? null); + setModuleId(moduleId?.toString() ?? null); + setGlobalViewId(globalViewId?.toString() ?? null); + setViewId(viewId?.toString() ?? null); + setInboxId(inboxId?.toString() ?? null); }, [ workspaceSlug, projectId, diff --git a/web/store/cycle/cycles.store.ts b/web/store/cycle/cycles.store.ts index 06ac6185465..15cf4a28eb4 100644 --- a/web/store/cycle/cycles.store.ts +++ b/web/store/cycle/cycles.store.ts @@ -32,7 +32,7 @@ export interface ICycleStore { // actions setCycleView: (_cycleView: TCycleView) => void; setCycleLayout: (_cycleLayout: TCycleLayout) => void; - setCycleId: (cycleId: string) => void; + setCycleId: (cycleId: string | null) => void; validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; @@ -131,7 +131,7 @@ export class CycleStore implements ICycleStore { // actions setCycleView = (_cycleView: TCycleView) => (this.cycleView = _cycleView); setCycleLayout = (_cycleLayout: TCycleLayout) => (this.cycleLayout = _cycleLayout); - setCycleId = (cycleId: string) => (this.cycleId = cycleId); + setCycleId = (cycleId: string | null) => (this.cycleId = cycleId); validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => { try { diff --git a/web/store/global-view/global_views.store.ts b/web/store/global-view/global_views.store.ts index c9915b8d898..588f0a4d2cf 100644 --- a/web/store/global-view/global_views.store.ts +++ b/web/store/global-view/global_views.store.ts @@ -19,7 +19,7 @@ export interface IGlobalViewsStore { }; // actions - setGlobalViewId: (viewId: string) => void; + setGlobalViewId: (viewId: string | null) => void; fetchAllGlobalViews: (workspaceSlug: string) => Promise; fetchGlobalViewDetails: (workspaceSlug: string, viewId: string) => Promise; @@ -72,7 +72,7 @@ export class GlobalViewsStore implements IGlobalViewsStore { this.workspaceService = new WorkspaceService(); } - setGlobalViewId = (viewId: string) => { + setGlobalViewId = (viewId: string | null) => { this.globalViewId = viewId; }; diff --git a/web/store/inbox/inbox.store.ts b/web/store/inbox/inbox.store.ts index c1ca086098e..b29d3685597 100644 --- a/web/store/inbox/inbox.store.ts +++ b/web/store/inbox/inbox.store.ts @@ -22,7 +22,7 @@ export interface IInboxStore { }; // actions - setInboxId: (inboxId: string) => void; + setInboxId: (inboxId: string | null) => void; getInboxId: (projectId: string) => string | null; @@ -100,7 +100,7 @@ export class InboxStore implements IInboxStore { return this.inboxesList[projectId]?.[0]?.id ?? null; }; - setInboxId = (inboxId: string) => { + setInboxId = (inboxId: string | null) => { runInAction(() => { this.inboxId = inboxId; }); diff --git a/web/store/module/modules.store.ts b/web/store/module/modules.store.ts index 91a11cd76d5..0dc122438bc 100644 --- a/web/store/module/modules.store.ts +++ b/web/store/module/modules.store.ts @@ -34,7 +34,7 @@ export interface IModuleStore { }; // actions - setModuleId: (moduleSlug: string) => void; + setModuleId: (moduleId: string | null) => void; getModuleById: (moduleId: string) => IModule | null; @@ -144,8 +144,8 @@ export class ModuleStore implements IModuleStore { getModuleById = (moduleId: string) => this.moduleDetails[moduleId] || null; // actions - setModuleId = (moduleSlug: string) => { - this.moduleId = moduleSlug ?? null; + setModuleId = (moduleId: string | null) => { + this.moduleId = moduleId; }; fetchModules = async (workspaceSlug: string, projectId: string) => { diff --git a/web/store/project-view/project_views.store.ts b/web/store/project-view/project_views.store.ts index 4c4baf487cc..76c58002dfd 100644 --- a/web/store/project-view/project_views.store.ts +++ b/web/store/project-view/project_views.store.ts @@ -20,7 +20,7 @@ export interface IProjectViewsStore { }; // actions - setViewId: (viewId: string) => void; + setViewId: (viewId: string | null) => void; fetchAllViews: (workspaceSlug: string, projectId: string) => Promise; fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise; @@ -82,7 +82,7 @@ export class ProjectViewsStore implements IProjectViewsStore { this.viewService = new ViewService(); } - setViewId = (viewId: string) => { + setViewId = (viewId: string | null) => { this.viewId = viewId; }; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 976654ee9ee..5b24467e7c8 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -44,7 +44,7 @@ export interface IProjectStore { currentProjectDetails: IProject | undefined; // actions - setProjectId: (projectId: string) => void; + setProjectId: (projectId: string | null) => void; setSearchQuery: (query: string) => void; getProjectById: (workspaceSlug: string, projectId: string) => IProject | null; @@ -251,8 +251,8 @@ export class ProjectStore implements IProjectStore { } // actions - setProjectId = (projectId: string) => { - this.projectId = projectId ?? null; + setProjectId = (projectId: string | null) => { + this.projectId = projectId; }; setSearchQuery = (query: string) => { From 4096136b440ca3985101b62322028c0689979acf Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:51:32 +0530 Subject: [PATCH 37/62] style: ui consistency and improvement (#2725) * style: create/update issue modal properties ui improvement * style: create update issue modal improvement * style: modal ui consistency --- .../ui/src/dropdowns/custom-search-select.tsx | 4 +- packages/ui/src/dropdowns/custom-select.tsx | 4 +- web/components/cycles/form.tsx | 2 +- web/components/issues/form.tsx | 52 ++++++++++--------- web/components/issues/select/assignee.tsx | 9 ++-- web/components/issues/select/cycle.tsx | 16 +++--- web/components/issues/select/date.tsx | 11 ++-- web/components/issues/select/estimate.tsx | 6 +-- web/components/issues/select/label.tsx | 8 +-- web/components/issues/select/module.tsx | 26 +++------- web/components/issues/select/priority.tsx | 8 ++- web/components/issues/select/project.tsx | 12 +---- web/components/issues/select/state.tsx | 8 ++- web/components/modules/select/lead.tsx | 8 ++- web/components/modules/select/members.tsx | 6 +-- .../project/create-project-modal.tsx | 6 +-- web/components/ui/date.tsx | 31 ++++++----- web/components/ui/labels-list.tsx | 2 +- web/components/views/form.tsx | 2 +- web/components/workspace/member-select.tsx | 7 +-- 20 files changed, 102 insertions(+), 126 deletions(-) diff --git a/packages/ui/src/dropdowns/custom-search-select.tsx b/packages/ui/src/dropdowns/custom-search-select.tsx index 8ba95c28cc6..3f1e503a8a0 100644 --- a/packages/ui/src/dropdowns/custom-search-select.tsx +++ b/packages/ui/src/dropdowns/custom-search-select.tsx @@ -87,8 +87,8 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
-
+
diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index 2c9fb4eabb1..cc581ed6df4 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -339,8 +339,8 @@ export const IssueForm: FC = observer((props) => { onChange={onChange} ref={ref} hasError={Boolean(errors.name)} - placeholder="Title" - className="resize-none text-xl w-full" + placeholder="Issue Title" + className="resize-none text-xl w-full focus:border-blue-400" /> )} /> @@ -348,7 +348,7 @@ export const IssueForm: FC = observer((props) => { )} {(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
-
+
{issueName && issueName !== "" && ( } + placement="bottom-start" > {watch("parent") ? ( <> @@ -603,24 +602,27 @@ export const IssueForm: FC = observer((props) => {
-
+
setCreateMore((prevData) => !prevData)} > +
+ {}} size="sm" /> +
Create more - {}} size="md" />
-
) : ( -
- - Assignee +
+ + Assignee
)}
diff --git a/web/components/issues/select/cycle.tsx b/web/components/issues/select/cycle.tsx index 3df4948a7c4..742774851a4 100644 --- a/web/components/issues/select/cycle.tsx +++ b/web/components/issues/select/cycle.tsx @@ -55,17 +55,15 @@ export const IssueCycleSelect: React.FC = observer((props query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); const label = selectedCycle ? ( -
- - - +
+
{selectedCycle.name}
) : ( - <> +
- Select Cycle - + Cycle +
); return ( @@ -80,9 +78,7 @@ export const IssueCycleSelect: React.FC = observer((props ) : ( <> - - {label} + + {label} )}
diff --git a/web/components/issues/select/estimate.tsx b/web/components/issues/select/estimate.tsx index f2d8750728e..f3da8a12ab9 100644 --- a/web/components/issues/select/estimate.tsx +++ b/web/components/issues/select/estimate.tsx @@ -21,9 +21,9 @@ export const IssueEstimateSelect: React.FC = ({ value, onChange }) => { - - +
+ + {estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"}
diff --git a/web/components/issues/select/label.tsx b/web/components/issues/select/label.tsx index 47b1d4e5b02..a85b073ebb3 100644 --- a/web/components/issues/select/label.tsx +++ b/web/components/issues/select/label.tsx @@ -68,10 +68,10 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, />
) : ( - - - Label - +
+ + Label +
)}
diff --git a/web/components/issues/select/module.tsx b/web/components/issues/select/module.tsx index c0d9c6c4c3b..cb9d6252bbc 100644 --- a/web/components/issues/select/module.tsx +++ b/web/components/issues/select/module.tsx @@ -55,34 +55,24 @@ export const IssueModuleSelect: React.FC = observer((pro query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); const label = selectedModule ? ( -
- - - -
{selectedModule.name}
+
+ + {selectedModule.name}
) : ( - <> +
- Select Module - + Module +
); return ( - onChange(val)} - disabled={false} - > + onChange(val)}> diff --git a/web/components/issues/select/state.tsx b/web/components/issues/select/state.tsx index 024024641fb..4bab99c4a9a 100644 --- a/web/components/issues/select/state.tsx +++ b/web/components/issues/select/state.tsx @@ -56,17 +56,15 @@ export const IssueStateSelect: React.FC = ({ setIsOpen, value, onChange, onChange={onChange} options={options} label={ -
+
{selectedOption ? ( ) : currentDefaultState ? ( ) : ( - + )} - {selectedOption?.name - ? selectedOption.name - : currentDefaultState?.name ?? State} + {selectedOption?.name ? selectedOption.name : currentDefaultState?.name ?? State}
} footerOption={ diff --git a/web/components/modules/select/lead.tsx b/web/components/modules/select/lead.tsx index 9e12e2017bf..69ba71bf962 100644 --- a/web/components/modules/select/lead.tsx +++ b/web/components/modules/select/lead.tsx @@ -50,9 +50,13 @@ export const ModuleLeadSelect: React.FC = ({ value, onChange }) => { {selectedOption ? ( ) : ( - + + )} + {selectedOption ? ( + selectedOption?.display_name + ) : ( + Lead )} - {selectedOption ? selectedOption?.display_name : Lead}
} onChange={onChange} diff --git a/web/components/modules/select/members.tsx b/web/components/modules/select/members.tsx index 94c74f05bb8..17b80a4715e 100644 --- a/web/components/modules/select/members.tsx +++ b/web/components/modules/select/members.tsx @@ -55,9 +55,9 @@ export const ModuleMembersSelect: React.FC = ({ value, onChange }) => { {value.length} Assignees
) : ( -
- - Assignee +
+ + Assignee
)}
diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 6be880dcbfb..ff89af2c798 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -265,7 +265,7 @@ export const CreateProjectModal: FC = observer((props) => { onChange={handleNameChange(onChange)} hasError={Boolean(errors.name)} placeholder="Project Title" - className="w-full" + className="w-full focus:border-blue-400" /> )} /> @@ -298,7 +298,7 @@ export const CreateProjectModal: FC = observer((props) => { onChange={handleIdentifierChange(onChange)} hasError={Boolean(errors.identifier)} placeholder="Identifier" - className="text-xs w-full" + className="text-xs w-full focus:border-blue-400" /> )} /> @@ -316,7 +316,7 @@ export const CreateProjectModal: FC = observer((props) => { tabIndex={3} placeholder="Description..." onChange={onChange} - className="text-sm !h-24" + className="text-sm !h-24 focus:border-blue-400" hasError={Boolean(errors?.description)} /> )} diff --git a/web/components/ui/date.tsx b/web/components/ui/date.tsx index 31cbdc9fd5d..acc8eb1eac8 100644 --- a/web/components/ui/date.tsx +++ b/web/components/ui/date.tsx @@ -21,22 +21,21 @@ export const DateSelect: React.FC = ({ value, onChange, label, minDate, m {({ close }) => ( <> - - - {value ? ( - <> - {renderShortDateWithYearFormat(value)} - - - ) : ( - <> - - {label} - - )} - + + {value ? ( + <> + + {renderShortDateWithYearFormat(value)} + + + ) : ( + <> + + {label} + + )} = (props) => { {labels && ( <> l?.name).join(", ")}> -
+
{`${labels.length} Labels`}
diff --git a/web/components/views/form.tsx b/web/components/views/form.tsx index 55ccb2fee7b..05169c59975 100644 --- a/web/components/views/form.tsx +++ b/web/components/views/form.tsx @@ -88,7 +88,7 @@ export const ProjectViewForm: React.FC = observer(({ handleFormSubmit, ha onChange={onChange} hasError={Boolean(errors.name)} placeholder="Title" - className="resize-none text-xl" + className="resize-none w-full text-xl focus:border-blue-400" /> )} /> diff --git a/web/components/workspace/member-select.tsx b/web/components/workspace/member-select.tsx index 3ba1cbd899f..bae16de015f 100644 --- a/web/components/workspace/member-select.tsx +++ b/web/components/workspace/member-select.tsx @@ -48,10 +48,7 @@ export const WorkspaceMemberSelect: FC = (props) => { : options?.filter((option) => option.member.display_name.toLowerCase().includes(query.toLowerCase())); const label = ( -
+
{value ? ( <> @@ -81,7 +78,7 @@ export const WorkspaceMemberSelect: FC = (props) => {
Date: Wed, 8 Nov 2023 17:52:34 +0530 Subject: [PATCH 38/62] fix: bug fixes and ui improvements (#2703) * fix: gantt chart duration in decimal * fix: Loading text instead Spinner in peek view * fix: cycle more popover typo & icon overlapping * fix: list layout properties alignment * fix: project search empty state * fix: calendar layout issue text overflow & redirection inconsistency * style: urgent priority hover background color * fix: Cycle issues kanban layout empty state missing * style: custom snooze modal placeholder text color * refactor: replaced unwanted anchor tag with div * chore: removed empty state for cycle kanban layout --- web/components/cycles/cycles-list-item.tsx | 4 +- .../issue-layouts/calendar/issue-blocks.tsx | 63 +++++++++---------- .../issues/issue-layouts/list/properties.tsx | 2 +- .../issues/issue-peek-overview/view.tsx | 6 +- .../select-snooze-till-modal.tsx | 2 +- web/components/project/card-list.tsx | 16 +++-- web/components/project/priority-select.tsx | 10 ++- web/helpers/date-time.helper.ts | 2 +- 8 files changed, 56 insertions(+), 49 deletions(-) diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 097a1807098..d104ae5e530 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -231,7 +231,7 @@ export const CyclesListItem: FC = (props) => { )} - + {!isCompleted && ( <> @@ -243,7 +243,7 @@ export const CyclesListItem: FC = (props) => { - Delete module + Delete cycle diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index c4795739ccc..2e270ab7de9 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -34,39 +34,38 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { {issue?.tempId !== undefined && (
)} diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index 92a203f3639..58944c76c7e 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -54,7 +54,7 @@ export const KanBanProperties: FC = observer((props) => { }; return ( -
+
{/* basic properties */} {/* state */} {displayProperties && displayProperties?.state && ( diff --git a/web/components/issues/issue-peek-overview/view.tsx b/web/components/issues/issue-peek-overview/view.tsx index 78557d86526..9268c2d69c1 100644 --- a/web/components/issues/issue-peek-overview/view.tsx +++ b/web/components/issues/issue-peek-overview/view.tsx @@ -7,7 +7,7 @@ import { MoveRight, MoveDiagonal, Bell, Link2, Trash2 } from "lucide-react"; import { PeekOverviewIssueDetails } from "./issue-detail"; import { PeekOverviewProperties } from "./properties"; import { IssueComment } from "./activity"; -import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon } from "@plane/ui"; +import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui"; import { DeleteIssueModal } from "../delete-issue-modal"; import { DeleteArchivedIssueModal } from "../delete-archived-issue-modal"; // types @@ -154,7 +154,7 @@ export const IssueView: FC = observer((props) => { onSubmit={handleDeleteIssue} /> )} -
+
{children && (
{children} @@ -251,7 +251,7 @@ export const IssueView: FC = observer((props) => {
)} {isLoading && !issue ? ( -
Loading...
+
) : ( issue && ( <> diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx index 4cc13a50d89..e434bcae8fb 100644 --- a/web/components/notifications/select-snooze-till-modal.tsx +++ b/web/components/notifications/select-snooze-till-modal.tsx @@ -171,7 +171,7 @@ export const SnoozeNotificationModal: FC = (props) => { setValue("time", null); onChange(val); }} - className="px-3 py-2 w-full rounded-md border border-custom-border-300 bg-custom-background-100 text-custom-text-100 focus:outline-none !text-sm" + className="px-3 py-2 w-full rounded-md border border-custom-border-300 bg-custom-background-100 text-custom-text-100 placeholder:!text-custom-text-400 focus:outline-none !text-sm" wrapperClassName="w-full" noBorder minDate={new Date()} diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index 0a090f63649..f364be084d6 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -38,12 +38,16 @@ export const ProjectCardList: FC = observer((props) => { return ( <> {projects.length > 0 ? ( -
-
- {projectStore.searchedProjects.map((project) => ( - - ))} -
+
+ {projectStore.searchedProjects.length == 0 ? ( +
No matching projects
+ ) : ( +
+ {projectStore.searchedProjects.map((project) => ( + + ))} +
+ )}
) : ( = ({ ? "border-red-500/20 bg-red-500" : "border-custom-border-300" : "border-custom-border-300" - } ${!disabled ? "hover:bg-custom-background-80" : ""} ${ - disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer" - } ${buttonClassName}`} + } ${ + !disabled + ? `${ + value === "urgent" && highlightUrgentPriority ? "hover:bg-red-400" : "hover:bg-custom-background-80" + }` + : "" + } ${disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"} ${buttonClassName}`} > {label} {!hideDropdownArrow && !disabled &&
); From 2d713777228d91c898f3b29b7493e47623f4e29b Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:54:59 +0530 Subject: [PATCH 40/62] fix: added empty project state when no project exists. (#2727) * fix: added empty project state when no project exists * fix: duplicate import --- .../empty-states/global-view.tsx | 46 ++++++++++++++----- .../roots/global-view-layout-root.tsx | 5 +- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/web/components/issues/issue-layouts/empty-states/global-view.tsx b/web/components/issues/issue-layouts/empty-states/global-view.tsx index 7151b5ddc86..bbe5f16a527 100644 --- a/web/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/global-view.tsx @@ -1,27 +1,49 @@ +// next +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { PlusIcon } from "lucide-react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; +import emptyProject from "public/empty-state/project.svg"; +// icons +import { Plus, PlusIcon } from "lucide-react"; export const GlobalViewEmptyState: React.FC = observer(() => { - const { commandPalette: commandPaletteStore } = useMobxStore(); + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { commandPalette: commandPaletteStore, project: projectStore } = useMobxStore(); + + const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; return (
- , - onClick: () => commandPaletteStore.toggleCreateIssueModal(true), - }} - /> + {!projects || projects?.length === 0 ? ( + , + text: "New Project", + onClick: () => commandPaletteStore.toggleCreateProjectModal(true), + }} + /> + ) : ( + , + onClick: () => commandPaletteStore.toggleCreateIssueModal(true), + }} + /> + )}
); }); diff --git a/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx index 018a63d2aa6..345a33db37d 100644 --- a/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx @@ -28,12 +28,15 @@ export const GlobalViewLayoutRoot: React.FC = observer((props) => { workspaceFilter: workspaceFilterStore, workspace: workspaceStore, issueDetail: issueDetailStore, + project: projectStore, } = useMobxStore(); const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined; const storedFilters = globalViewId ? globalViewFiltersStore.storedFilters[globalViewId.toString()] : undefined; + const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; + useSWR( workspaceSlug && globalViewId && viewDetails ? `GLOBAL_VIEW_ISSUES_${globalViewId.toString()}` : null, workspaceSlug && globalViewId && viewDetails @@ -94,7 +97,7 @@ export const GlobalViewLayoutRoot: React.FC = observer((props) => { return (
- {issues?.length === 0 ? ( + {issues?.length === 0 || !projects || projects?.length === 0 ? ( ) : (
From f8002852e0857714ab2c610e7ed459b012aadbe8 Mon Sep 17 00:00:00 2001 From: sabith-tu <95301637+sabith-tu@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:55:28 +0530 Subject: [PATCH 41/62] fix: issue property height and peek view date picker border radius (#2726) --- web/components/issues/issue-layouts/properties/estimates.tsx | 2 +- web/components/issues/issue-layouts/properties/labels.tsx | 2 +- web/components/issues/issue-layouts/properties/state.tsx | 2 +- web/components/issues/issue-peek-overview/properties.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/components/issues/issue-layouts/properties/estimates.tsx b/web/components/issues/issue-layouts/properties/estimates.tsx index 77b5a79b34d..432a39f4d7e 100644 --- a/web/components/issues/issue-layouts/properties/estimates.tsx +++ b/web/components/issues/issue-layouts/properties/estimates.tsx @@ -110,7 +110,7 @@ export const IssuePropertyEstimates: React.FC = observe
- } - position={position} - renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) => - React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props }) - } - /> - ); -}; diff --git a/packages/editor/core/src/ui/plugins/delete-image.tsx b/packages/editor/core/src/ui/plugins/delete-image.tsx index 56284472b21..48ec244fcdc 100644 --- a/packages/editor/core/src/ui/plugins/delete-image.tsx +++ b/packages/editor/core/src/ui/plugins/delete-image.tsx @@ -15,7 +15,11 @@ interface ImageNode extends ProseMirrorNode { const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin => new Plugin({ key: deleteKey, - appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { + appendTransaction: ( + transactions: readonly Transaction[], + oldState: EditorState, + newState: EditorState, + ) => { const newImageSources = new Set(); newState.doc.descendants((node) => { if (node.type.name === IMAGE_NODE_TYPE) { @@ -55,7 +59,10 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin => export default TrackImageDeletionPlugin; -async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise { +async function onNodeDeleted( + src: string, + deleteImage: DeleteImage, +): Promise { try { const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); const resStatus = await deleteImage(assetUrlWithWorkspaceId); diff --git a/packages/editor/core/src/ui/plugins/upload-image.tsx b/packages/editor/core/src/ui/plugins/upload-image.tsx index cdd62ae4836..25646007313 100644 --- a/packages/editor/core/src/ui/plugins/upload-image.tsx +++ b/packages/editor/core/src/ui/plugins/upload-image.tsx @@ -4,7 +4,7 @@ import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; const uploadKey = new PluginKey("upload-image"); -const UploadImagesPlugin = () => +const UploadImagesPlugin = (cancelUploadImage?: () => any) => new Plugin({ key: uploadKey, state: { @@ -21,15 +21,46 @@ const UploadImagesPlugin = () => const placeholder = document.createElement("div"); placeholder.setAttribute("class", "img-placeholder"); const image = document.createElement("img"); - image.setAttribute("class", "opacity-10 rounded-lg border border-custom-border-300"); + image.setAttribute( + "class", + "opacity-10 rounded-lg border border-custom-border-300", + ); image.src = src; placeholder.appendChild(image); + + // Create cancel button + const cancelButton = document.createElement("button"); + cancelButton.style.position = "absolute"; + cancelButton.style.right = "3px"; + cancelButton.style.top = "3px"; + cancelButton.setAttribute("class", "opacity-90 rounded-lg"); + + cancelButton.onclick = () => { + cancelUploadImage?.(); + }; + + // Create an SVG element from the SVG string + const svgString = ``; + const parser = new DOMParser(); + const svgElement = parser.parseFromString( + svgString, + "image/svg+xml", + ).documentElement; + + cancelButton.appendChild(svgElement); + placeholder.appendChild(cancelButton); const deco = Decoration.widget(pos + 1, placeholder, { id, }); set = set.add(tr.doc, [deco]); } else if (action && action.remove) { - set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id)); + set = set.remove( + set.find( + undefined, + undefined, + (spec) => spec.id == action.remove.id, + ), + ); } return set; }, @@ -48,19 +79,39 @@ function findPlaceholder(state: EditorState, id: {}) { const found = decos.find( undefined, undefined, - (spec: { id: number | undefined }) => spec.id == id + (spec: { id: number | undefined }) => spec.id == id, ); return found.length ? found[0].from : null; } +const removePlaceholder = (view: EditorView, id: {}) => { + const removePlaceholderTr = view.state.tr.setMeta(uploadKey, { + remove: { id }, + }); + view.dispatch(removePlaceholderTr); +}; + export async function startImageUpload( file: File, view: EditorView, pos: number, uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, ) { + if (!file) { + alert("No file selected. Please select a file to upload."); + return; + } + if (!file.type.includes("image/")) { + alert("Invalid file type. Please select an image file."); + return; + } + + if (file.size > 5 * 1024 * 1024) { + alert("File size too large. Please select a file smaller than 5MB."); return; } @@ -82,28 +133,42 @@ export async function startImageUpload( view.dispatch(tr); }; + // Handle FileReader errors + reader.onerror = (error) => { + console.error("FileReader error: ", error); + removePlaceholder(view, id); + return; + }; + setIsSubmitting?.("submitting"); - const src = await UploadImageHandler(file, uploadFile); - const { schema } = view.state; - pos = findPlaceholder(view.state, id); - - if (pos == null) return; - const imageSrc = typeof src === "object" ? reader.result : src; - - const node = schema.nodes.image.create({ src: imageSrc }); - const transaction = view.state.tr - .replaceWith(pos, pos, node) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); + + try { + const src = await UploadImageHandler(file, uploadFile); + const { schema } = view.state; + pos = findPlaceholder(view.state, id); + + if (pos == null) return; + const imageSrc = typeof src === "object" ? reader.result : src; + + const node = schema.nodes.image.create({ src: imageSrc }); + const transaction = view.state.tr + .replaceWith(pos, pos, node) + .setMeta(uploadKey, { remove: { id } }); + view.dispatch(transaction); + } catch (error) { + console.error("Upload error: ", error); + removePlaceholder(view, id); + } } -const UploadImageHandler = (file: File, - uploadFile: UploadImage +const UploadImageHandler = ( + file: File, + uploadFile: UploadImage, ): Promise => { try { return new Promise(async (resolve, reject) => { try { - const imageUrl = await uploadFile(file) + const imageUrl = await uploadFile(file); const image = new Image(); image.src = imageUrl; @@ -118,9 +183,6 @@ const UploadImageHandler = (file: File, } }); } catch (error) { - if (error instanceof Error) { - console.log(error.message); - } return Promise.reject(error); } }; diff --git a/packages/editor/lite-text-editor/package.json b/packages/editor/lite-text-editor/package.json index 3b6cd720b78..52f27fb291c 100644 --- a/packages/editor/lite-text-editor/package.json +++ b/packages/editor/lite-text-editor/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "@plane/editor-core": "*", + "@plane/ui": "*", "@tiptap/extension-list-item": "^2.1.11", "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", diff --git a/packages/editor/lite-text-editor/src/ui/index.tsx b/packages/editor/lite-text-editor/src/ui/index.tsx index ef321d511bf..26df0802561 100644 --- a/packages/editor/lite-text-editor/src/ui/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/index.tsx @@ -47,6 +47,7 @@ interface ILiteTextEditor { }[]; }; onEnterKeyPress?: (e?: any) => void; + cancelUploadImage?: () => any; mentionHighlights?: string[]; mentionSuggestions?: IMentionSuggestion[]; submitButton?: React.ReactNode; @@ -64,6 +65,7 @@ interface EditorHandle { const LiteTextEditor = (props: LiteTextEditorProps) => { const { onChange, + cancelUploadImage, debouncedUpdatesEnabled, setIsSubmitting, setShouldShowAlert, @@ -84,6 +86,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => { const editor = useEditor({ onChange, + cancelUploadImage, debouncedUpdatesEnabled, setIsSubmitting, setShouldShowAlert, diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx index cf0d78688fa..a4fb0479c17 100644 --- a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx @@ -14,8 +14,8 @@ import { TableItem, UnderLineItem, } from "@plane/editor-core"; -import { Tooltip } from "../../tooltip"; -import { UploadImage } from "../.."; +import { Tooltip } from "@plane/ui"; +import { UploadImage } from "../../"; export interface BubbleMenuItem { name: string; diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index a0dbe7226e6..2e98a72aae3 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -1,8 +1,13 @@ -"use client" -import * as React from 'react'; -import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from '@plane/editor-core'; -import { EditorBubbleMenu } from './menus/bubble-menu'; -import { RichTextEditorExtensions } from './extensions'; +"use client"; +import * as React from "react"; +import { + EditorContainer, + EditorContentWrapper, + getEditorClassNames, + useEditor, +} from "@plane/editor-core"; +import { EditorBubbleMenu } from "./menus/bubble-menu"; +import { RichTextEditorExtensions } from "./extensions"; export type UploadImage = (file: File) => Promise; export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; @@ -14,9 +19,9 @@ export type IMentionSuggestion = { title: string; subtitle: string; redirect_uri: string; -} +}; -export type IMentionHighlight = string +export type IMentionHighlight = string; interface IRichTextEditor { value: string; @@ -24,10 +29,13 @@ interface IRichTextEditor { deleteFile: DeleteImage; noBorder?: boolean; borderOnFocus?: boolean; + cancelUploadImage?: () => any; customClassName?: string; editorContentCustomClassNames?: string; onChange?: (json: any, html: string) => void; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void; setShouldShowAlert?: (showAlert: boolean) => void; forwardedRef?: any; debouncedUpdatesEnabled?: boolean; @@ -54,11 +62,12 @@ const RichTextEditor = ({ uploadFile, deleteFile, noBorder, + cancelUploadImage, borderOnFocus, customClassName, forwardedRef, mentionHighlights, - mentionSuggestions + mentionSuggestions, }: RichTextEditorProps) => { const editor = useEditor({ onChange, @@ -67,14 +76,19 @@ const RichTextEditor = ({ setShouldShowAlert, value, uploadFile, + cancelUploadImage, deleteFile, forwardedRef, extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting), mentionHighlights, - mentionSuggestions + mentionSuggestions, }); - const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); + const editorClassNames = getEditorClassNames({ + noBorder, + borderOnFocus, + customClassName, + }); if (!editor) return null; @@ -82,16 +96,19 @@ const RichTextEditor = ({ {editor && }
- +
-
+ ); }; -const RichTextEditorWithRef = React.forwardRef((props, ref) => ( - -)); +const RichTextEditorWithRef = React.forwardRef( + (props, ref) => , +); RichTextEditorWithRef.displayName = "RichTextEditorWithRef"; -export { RichTextEditor, RichTextEditorWithRef}; +export { RichTextEditor, RichTextEditorWithRef }; diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index f70a2c5aad2..9878fd00a17 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -76,6 +76,7 @@ export const AddComment: React.FC = observer((props) => { handleSubmit(onSubmit)(e); }); }} + cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspace_slug as string)} deleteFile={fileService.deleteImage} ref={editorRef} diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index 29801c9e648..ab09b2490ee 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -103,6 +103,7 @@ export const CommentCard: React.FC = observer((props) => { render={({ field: { onChange, value } }) => ( { + this.cancelSource = axios.CancelToken.source(); return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, { headers: { ...this.getHeaders(), "Content-Type": "multipart/form-data", }, + cancelToken: this.cancelSource.token, }) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + if (axios.isCancel(error)) { + console.log(error.message); + } else { + throw error?.response?.data; + } }); } + cancelUpload() { + this.cancelSource.cancel("Upload cancelled"); + } getUploadFileFunction(workspaceSlug: string): (file: File) => Promise { return async (file: File) => { const formData = new FormData(); diff --git a/space/styles/table.css b/space/styles/table.css index ad88fd10ec8..8a47a8c59fd 100644 --- a/space/styles/table.css +++ b/space/styles/table.css @@ -92,7 +92,7 @@ transform: translateY(-50%); } -.tableWrapper .tableControls .columnsControl > button { +.tableWrapper .tableControls .columnsControl .columnsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); width: 30px; @@ -104,26 +104,42 @@ transform: translateX(-50%); } -.tableWrapper .tableControls .rowsControl > button { +.tableWrapper .tableControls .rowsControl .rowsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); height: 30px; width: 15px; } -.tableWrapper .tableControls button { +.tableWrapper .tableControls .rowsControlDiv { background-color: rgba(var(--color-primary-100)); border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; background-size: 1.25rem; background-repeat: no-repeat; background-position: center; - transition: transform ease-out 100ms, background-color ease-out 100ms; + transition: + transform ease-out 100ms, + background-color ease-out 100ms; outline: none; box-shadow: #000 0px 2px 4px; cursor: pointer; } +.tableWrapper .tableControls .columnsControlDiv { + background-color: rgba(var(--color-primary-100)); + border: 1px solid rgba(var(--color-border-200)); + border-radius: 2px; + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: center; + transition: + transform ease-out 100ms, + background-color ease-out 100ms; + outline: none; + box-shadow: #000 0px 2px 4px; + cursor: pointer; +} .tableWrapper .tableControls .tableToolbox, .tableWrapper .tableControls .tableColorPickerToolbox { border: 1px solid rgba(var(--color-border-300)); diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx index a468580f4e5..9a1d150994e 100644 --- a/web/components/inbox/modals/create-issue-modal.tsx +++ b/web/components/inbox/modals/create-issue-modal.tsx @@ -15,6 +15,7 @@ import { IssuePrioritySelect } from "components/issues/select"; import { Button, Input, ToggleSwitch } from "@plane/ui"; // types import { IIssue } from "types"; +import useEditorSuggestions from "hooks/use-editor-suggestions"; type Props = { isOpen: boolean; @@ -40,6 +41,8 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { const editorRef = useRef(null); + const editorSuggestion = useEditorSuggestions() + const router = useRouter(); const { workspaceSlug, projectId, inboxId } = router.query; @@ -134,6 +137,7 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { control={control} render={({ field: { value, onChange } }) => ( = observer((props) => { onChange={(description, description_html: string) => { onChange(description_html); }} + mentionSuggestions={editorSuggestion.mentionSuggestions} + mentionHighlights={editorSuggestion.mentionHighlights} /> )} /> diff --git a/web/components/issues/comment/add-comment.tsx b/web/components/issues/comment/add-comment.tsx index 28c986bc7ce..ee7805ef743 100644 --- a/web/components/issues/comment/add-comment.tsx +++ b/web/components/issues/comment/add-comment.tsx @@ -84,6 +84,7 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit, showAc render={({ field: { onChange: onCommentChange, value: commentValue } }) => ( = ({
= (props) => { control={control} render={({ field: { value, onChange } }) => ( = (props) => { control={control} render={({ field: { value, onChange } }) => ( = observer((props) => { control={control} render={({ field: { value, onChange } }) => ( = (props) => {
= (props) => { render={({ field: { onChange: onCommentChange, value: commentValue } }) => ( = (props) =
{errors.name ? errors.name.message : null} = ({ if (!data) return ( = ({ return ( = ({ block, projectDetails, showBl {showBlockDetails ? block.description_html.length > 7 && ( { + this.cancelSource = axios.CancelToken.source(); return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, { headers: { ...this.getHeaders(), "Content-Type": "multipart/form-data", }, + cancelToken: this.cancelSource.token, }) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + if (axios.isCancel(error)) { + console.log(error.message); + } else { + throw error?.response?.data; + } }); } + cancelUpload() { + this.cancelSource.cancel("Upload cancelled"); + } + getUploadFileFunction(workspaceSlug: string): (file: File) => Promise { return async (file: File) => { const formData = new FormData(); diff --git a/web/styles/table.css b/web/styles/table.css index ad88fd10ec8..bce7e4683ad 100644 --- a/web/styles/table.css +++ b/web/styles/table.css @@ -92,7 +92,7 @@ transform: translateY(-50%); } -.tableWrapper .tableControls .columnsControl > button { +.tableWrapper .tableControls .columnsControl .columnsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); width: 30px; @@ -104,14 +104,14 @@ transform: translateX(-50%); } -.tableWrapper .tableControls .rowsControl > button { +.tableWrapper .tableControls .rowsControl .rowsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); height: 30px; width: 15px; } -.tableWrapper .tableControls button { +.tableWrapper .tableControls .rowsControlDiv { background-color: rgba(var(--color-primary-100)); border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; @@ -124,6 +124,18 @@ cursor: pointer; } +.tableWrapper .tableControls .columnsControlDiv { + background-color: rgba(var(--color-primary-100)); + border: 1px solid rgba(var(--color-border-200)); + border-radius: 2px; + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: center; + transition: transform ease-out 100ms, background-color ease-out 100ms; + outline: none; + box-shadow: #000 0px 2px 4px; + cursor: pointer; +} .tableWrapper .tableControls .tableToolbox, .tableWrapper .tableControls .tableColorPickerToolbox { border: 1px solid rgba(var(--color-border-300)); From bd1a850f3557603aae87b93d393ddf1862a8c377 Mon Sep 17 00:00:00 2001 From: Ramesh Kumar Chandra <31303617+rameshkumarchandra@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:12:36 +0530 Subject: [PATCH 44/62] style: kanban card label overflow (#2722) * chore: kanban card lable drop down items overflow * style: kaban card label text overflow, tool tip, hover cursor * style: label overflow in list layout --- .../issue-layouts/properties/labels.tsx | 66 +++++++++++-------- .../issues/issue-layouts/properties/state.tsx | 4 +- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index 3f0f6552d5c..1b35f0495a4 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -65,14 +65,14 @@ export const IssuePropertyLabels: React.FC = observer((pro value: label.id, query: label.name, content: ( -
+
- {label.name} +
{label.name}
), })); @@ -93,31 +93,39 @@ export const IssuePropertyLabels: React.FC = observer((pro }); const label = ( -
+
{value.length > 0 ? ( value.length <= maxRender ? ( <> {(projectLabels ? projectLabels : []) ?.filter((l) => value.includes(l.id)) .map((label) => ( -
-
- - {label.name} +
+
+ +
+ {label.name} +
+
-
+ ))} ) : ( -
+
= observer((pro ) ) : (
Select labels
@@ -148,7 +155,7 @@ export const IssuePropertyLabels: React.FC = observer((pro return ( = observer((pro ); - } + }, ); Button.displayName = "plane-ui-button"; diff --git a/packages/ui/src/button/helper.tsx b/packages/ui/src/button/helper.tsx index 82489c3e81a..48b1fc94a0f 100644 --- a/packages/ui/src/button/helper.tsx +++ b/packages/ui/src/button/helper.tsx @@ -102,7 +102,7 @@ export const buttonStyling: IButtonStyling = { export const getButtonStyling = ( variant: TButtonVariant, size: TButtonSizes, - disabled: boolean = false + disabled: boolean = false, ): string => { let _variant: string = ``; const currentVariant = buttonStyling[variant]; diff --git a/packages/ui/src/dropdowns/custom-search-select.tsx b/packages/ui/src/dropdowns/custom-search-select.tsx index 3f1e503a8a0..0fb4c67cf32 100644 --- a/packages/ui/src/dropdowns/custom-search-select.tsx +++ b/packages/ui/src/dropdowns/custom-search-select.tsx @@ -35,7 +35,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState( - null + null, ); const { styles, attributes } = usePopper(referenceElement, popperElement, { @@ -46,7 +46,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { query === "" ? options : options?.filter((option) => - option.query.toLowerCase().includes(query.toLowerCase()) + option.query.toLowerCase().includes(query.toLowerCase()), ); const comboboxProps: any = { diff --git a/packages/ui/src/dropdowns/custom-select.tsx b/packages/ui/src/dropdowns/custom-select.tsx index dd4d1d786e3..b62ff2cb3b5 100644 --- a/packages/ui/src/dropdowns/custom-select.tsx +++ b/packages/ui/src/dropdowns/custom-select.tsx @@ -30,7 +30,7 @@ const CustomSelect = (props: ICustomSelectProps) => { const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState( - null + null, ); const { styles, attributes } = usePopper(referenceElement, popperElement, { diff --git a/packages/ui/src/form-fields/index.ts b/packages/ui/src/form-fields/index.ts index 49f6f1552fe..9cac734283e 100644 --- a/packages/ui/src/form-fields/index.ts +++ b/packages/ui/src/form-fields/index.ts @@ -1,3 +1,3 @@ export * from "./input"; export * from "./textarea"; -export * from "./input-color-picker" \ No newline at end of file +export * from "./input-color-picker"; diff --git a/packages/ui/src/form-fields/textarea.tsx b/packages/ui/src/form-fields/textarea.tsx index e53979edca2..8490326b89a 100644 --- a/packages/ui/src/form-fields/textarea.tsx +++ b/packages/ui/src/form-fields/textarea.tsx @@ -10,7 +10,7 @@ export interface TextAreaProps // Updates the height of a